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?

app層(dialog)の現状実装メモ(ASR→LLM→TTS 直列ワークフロー)

Last updated at Posted at 2025-12-20

12/20 まででメディア(送受信)を押さえたので、今日は app層 が現状どう動いているかをコード位置ベースでまとめる。MVPでは状態機械というより「1チャンクASR→LLM→TTS」の直列処理になっている点に注意。


全体像(現行)

session (capture 10s) ──AppEvent::AudioBuffered──────► app::worker
                           │ ASR(transcribe_chunks)
                           ▼
                         LLM(generate_answer)
                           ▼
                         TTS(synth_to_wav)
                           ▼
session <= SessionOut::AppSendBotAudioFile { path }
  • appはセッションごとにワーカーを1つ起動し、AppEvent を受け取って処理するだけのシリアル実装。
  • SIP/RTPには触れず、AI呼び出しと履歴保持のみ担当。

1) エントリとイベント線表

  • セッション起動時に app ワーカーを生成:src/session/session.rs:108 app::spawn_app_worker(...)
  • appが受け取るイベント(src/app/mod.rs:12):
    • CallStarted { call_id }(セッション確立・イントロ後に送信)
    • AudioBuffered { call_id, pcm_mulaw }(受信音声10秒を μ-law で受け取る)
    • CallEnded { call_id }(終了通知)

イベント送信元(session 側のタイミング):

  • CallStarted: ACK後、イントロ再生前に送信(session.rs:169)。
  • AudioBuffered: Establishedで10秒貯めたら送信(session.rs:209-229)。
  • CallEnded: BYE/タイムアウトなどで終端時に送信(session.rs:242-257 ほか)。

2) appワーカーのメインループ

// src/app/mod.rs:18-
pub fn spawn_app_worker(call_id: String, rx: UnboundedReceiver<AppEvent>, session_out_tx: UnboundedSender<SessionOut>) {
    let worker = AppWorker::new(call_id, session_out_tx, rx);
    tokio::spawn(async move { worker.run().await });
}

impl AppWorker {
    async fn run(mut self) {
        while let Some(ev) = self.rx.recv().await {
            match ev {
                AppEvent::CallStarted { .. } => { self.active = true; }
                AppEvent::AudioBuffered { pcm_mulaw, .. } => { ... handle_audio_buffer ... }
                AppEvent::CallEnded { .. } => break,
            }
        }
    }
}
  • active フラグが立つまで AudioBuffered は捨てる。
  • 並列処理やキャンセルはなく、1イベントずつ直列で処理。

3) ASR→LLM→TTS の直列処理

// src/app/mod.rs:51-
async fn handle_audio_buffer(&mut self, call_id: &str, pcm_mulaw: Vec<u8>) -> anyhow::Result<()> {
    // ASR: 1チャンク
    let asr_chunks = vec![asr::AsrChunk { pcm_mulaw, end: true }];
    let user_text = match asr::transcribe_chunks(call_id, &asr_chunks).await {
        Ok(t) => t,
        Err(e) => { warn!("ASR failed: {e:?}"); "すみません、聞き取れませんでした。".into() }
    };

    // LLM: 履歴から簡易プロンプトを構築
    let prompt = self.build_prompt(&user_text);
    let answer_text = match llm::generate_answer(&prompt).await {
        Ok(ans) => ans,
        Err(e) => { warn!("LLM failed: {e:?}"); "すみません、うまく答えを用意できませんでした。".into() }
    };
    self.history.push((user_text.clone(), answer_text.clone()));

    // TTS: WAVパスを session に返す
    match tts::synth_to_wav(&answer_text, None).await {
        Ok(bot_wav) => {
            let _ = self.session_out_tx.send(SessionOut::AppSendBotAudioFile { path: bot_wav });
        }
        Err(e) => { warn!("TTS failed: {e:?}"); }
    }
    Ok(())
}
  • ASR/LLM/TTS すべて同期的に await する単線構造。
  • エラー時は定型の謝罪テキストを返すだけで再試行なし。
  • TTS失敗時は無音の返却もなく、発話スキップになる。

4) プロンプト構築(履歴あり)

// src/app/mod.rs:86-
fn build_prompt(&self, latest_user: &str) -> String {
    let mut prompt = String::new();
    for (u, b) in &self.history {
        prompt.push_str("User: "); prompt.push_str(u); prompt.push_str("\nBot: "); prompt.push_str(b); prompt.push('\n');
    }
    prompt.push_str("User: "); prompt.push_str(latest_user);
    prompt
}
  • シンプルに User/Bot の往復を平文で連結するだけ。システムプロンプトや役割指定はなし。
  • 履歴は無制限に伸びる(クリッピングなし)。

5) 現状の割り切り・制約

  • 音声入力は「10秒貯めて1発ASR」。被せ検知・部分ASR・中断はなし。
  • 状態管理は active と履歴のみ。Listening/Thinking/Speaking の分離は未実装。
  • LLM/TTS の失敗時にリトライや「無音でも送る」処理はなし。
  • プロンプトにシステム指示がないため、キャラ付けや禁止事項は未設定。
  • AppEvent/SessionOut は固定型のみ(Barge-in/Cancel/Action 指示などは未導入)。

まとめ(12/21時点の固定点)

  • app層はセッションごとに単一ワーカーを起動し、CallStartedAudioBufferedCallEnded を直列処理。
  • 音声10秒を μ-law チャンクとして ASR→LLM→TTS で同期処理し、生成WAVパスを SessionOut::AppSendBotAudioFile で session に戻す。
  • 状態機械や被せ対応は未実装で、直列パイプラインのみ。エラー時は定型文でフォールバックし、TTS失敗はスキップ。
  • 今後の改善候補: 部分ASR/早口出し、バー ジインで送信停止、履歴クリップ&システムプロンプト導入、TTS失敗時の無音返却やリトライ、LLMアクション拡張。
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?