0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ポーリング処理について

0
Posted at

🚀 AI 音声メモアプリの録音 → メモ完成までをつなぐモデル & ジョブ処理まとめ


🎯 全体の役割(超概要)

録音 → サーバーに送信 → サーバーで文字起こし → Memo 完成
この「途中経過」を iOS 側で保持するためのモデルが PendingMemo

正式 Memo が生成されるまでの 仮メモ を表す。


🟦 iOS(Swift)側:PendingMemo の仕組み


✔ 1. JobStatus(ジョブ状態を表す enum)

enum JobStatus: String, Codable {
    case pending
    case processing
    case completed
    case failed
}

✔ 2. PendingMemo(途中経過メモ)

struct PendingMemo: Identifiable, Codable {
    let id: String
    var status: JobStatus
    var progress: Int
    var result: JobResult?
    var error: String?
    let createdAt: Date
    var updatedAt: Date
}

Memo との違い

Memo PendingMemo
完成したデータ 途中経過
title / content あり 仮テキスト
保存済み 完了したら Memo に変換

✔ 3. UI 表示用プレースホルダー

タイトル

var placeholderTitle: String
status 表示内容
pending 処理待ち…
processing 処理中… 36%
completed result.title
failed エラー

本文

var placeholderContent: String
status 内容
pending 音声を処理しています
processing 文字起こし・タグ生成中…
completed result.content
failed エラーメッセージ

✔ 4. completed のときだけ Memo に変換

var completedMemo: Memo? {
    guard status == .completed, let result = result else { return nil }

    return Memo(
        id: result.memoId,
        title: result.title,
        content: result.content,
        tags: result.tags,
        audioURL: "",
        embedding: nil,
        relatedMemoIds: [],
        createdAt: createdAt,
        updatedAt: updatedAt
    )
}

✔ 5. JobResult

struct JobResult: Codable {
    let memoId: String
    let title: String
    let content: String
    let tags: [String]
}

✔ 6. JobStatusResponse

struct JobStatusResponse: Codable {
    let jobId: String
    let status: String
    let progress: Int
    let result: JobResult?
    let error: String?
    let createdAt: Date
    let updatedAt: Date
}

✔ 7. toPendingMemo()

func toPendingMemo() -> PendingMemo {
    PendingMemo(
        id: jobId,
        status: JobStatus(rawValue: status) ?? .pending,
        progress: progress,
        result: result,
        error: error,
        createdAt: createdAt,
        updatedAt: updatedAt
    )
}

✔ 8. CreateJobResponse

struct CreateJobResponse: Codable {
    let jobId: String
    let status: String
    let message: String
}

🟧 Node.js(サーバー側):JobService の仕組み


✔ Job 型

export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed';

export interface Job {
  id: string;
  userId: string;
  status: JobStatus;
  progress: number;
  audioURL: string;
  audioFilePath: string;
  result?: {
    memoId: string;
    title: string;
    content: string;
    tags: string[];
  };
  error?: string;
  createdAt: Date;
  updatedAt: Date;
}

✔ createJob(ジョブ作成)

async createJob(userId: string, audioFilePath: string, audioURL: string): Promise<string> {
  const jobId = new ObjectId().toString();
  
  const job: Job = {
    id: jobId,
    userId,
    status: 'pending',
    progress: 0,
    audioURL,
    audioFilePath,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  await cacheSet(generateCacheKey('job', jobId), job, JOB_TTL);
  await this.collection.insertOne({ _id: new ObjectId(jobId), ...job });

  this.processJob(jobId).catch(err => console.error(err));

  return jobId;
}

✔ getJob(状態取得)

async getJob(jobId: string): Promise<Job | null> {
  const cached = await cacheGet<Job>(generateCacheKey('job', jobId));
  if (cached) return cached;

  const doc = await this.collection.findOne({ _id: new ObjectId(jobId) });
  if (!doc) return null;

  const job: Job = {
    id: doc._id.toString(),
    userId: doc.userId,
    status: doc.status,
    progress: doc.progress,
    audioURL: doc.audioURL,
    audioFilePath: doc.audioFilePath,
    result: doc.result,
    error: doc.error,
    createdAt: doc.createdAt,
    updatedAt: doc.updatedAt,
  };

  await cacheSet(generateCacheKey('job', jobId), job, JOB_TTL);
  return job;
}

✔ processJob(バックグラウンド処理)

private async processJob(jobId: string): Promise<void> {
  const job = await this.getJob(jobId);
  if (!job) return;

  try {
    await this.updateJobStatus(jobId, 'processing', 10);

    const content = await openAIService.transcribe(job.audioFilePath);
    await this.updateJobStatus(jobId, 'processing', 40);

    if (!content) throw new Error('Transcription is empty');

    const [title, tags, embedding] = await Promise.all([
      openAIService.generateTitle(content),
      openAIService.extractTags(content),
      openAIService.generateEmbedding(content),
    ]);

    await this.updateJobStatus(jobId, 'processing', 80);

    const memo = await memoService.createFromText(
      job.userId,
      title,
      content,
      tags,
      job.audioURL
    );

    await this.updateJobStatus(jobId, 'completed', 100, {
      memoId: memo.id,
      title: memo.title,
      content: memo.content,
      tags: memo.tags,
    });

  } catch (error) {
    await this.updateJobStatus(
      jobId,
      'failed',
      0,
      undefined,
      error instanceof Error ? error.message : 'Unknown error'
    );
  }
}

✔ updateJobStatus

private async updateJobStatus(
  jobId: string,
  status: JobStatus,
  progress: number,
  result?: Job['result'],
  error?: string
)

✔ getPendingJobs

async getPendingJobs(userId: string): Promise<Job[]> {
  return this.collection
    .find({
      userId,
      status: { $in: ['pending', 'processing'] },
    })
    .sort({ createdAt: -1 })
    .limit(10)
    .toArray();
}

✔ cleanupOldJobs

async cleanupOldJobs(): Promise<number> {
  const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
  
  const result = await this.collection.deleteMany({
    status: { $in: ['completed', 'failed'] },
    updatedAt: { $lt: oneDayAgo },
  });

  return result.deletedCount;
}

🎯 全体まとめ

録音
→ サーバーへ送信
→ jobId を受け取る
→ iOS は PendingMemo を作成
→ サーバーの progress / status を監視
→ completed になれば Memo に変換
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?