Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

talk-with-local-llm-with-web-streams-api

kbaba1001
December 04, 2024

 talk-with-local-llm-with-web-streams-api

kbaba1001

December 04, 2024
Tweet

More Decks by kbaba1001

Other Decks in Programming

Transcript

  1. 概要 何か話す ブラウザで音声デー タにする WhisperStreamingで 音声をテキスト化 音声バイナリを WebStreamsで サーバーに送る フロントエンド

    バックエンド 会話テキストを WebStreamsでポー リング PostgreSQL から データ取得 テキスト表示 Notify 「AIを呼び出す」 ボタンを押下 Ollamaに会話データを 入力して出力をNotifyし テーブルにも保存する Notify Listen Post Request
  2. 概要 何か話す ブラウザで音声デー タにする WhisperStreamingで 音声をテキスト化 音声バイナリを WebStreamsで サーバーに送る フロントエンド

    バックエンド 会話テキストを WebStreamsでポー リング PostgreSQL から データ取得 テキスト表示 Notify 「AIを呼び出す」 ボタンを押下 Ollamaに会話データを 入力して出力をNotifyし テーブルにも保存する Notify Listen Post Request まずここの話
  3. 音声データの取得 const startRecording = async () => { // マイクへのアクセスをリクエスト

    const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // MediaRecorderの作成 const options = { mimeType: "audio/webm; codecs=opus" , // サポートされているmimeTypeを指定 }; const mediaRecorder = new MediaRecorder(stream, options); mediaRecorderRef.current = mediaRecorder; // ReadableStreamの作成 const audioStream = new ReadableStream<Uint8Array>({ start(controller) { // コントローラを保存して、後でデータをエンキューする streamControllerRef.current = controller; }, });
  4. 音声データの取得 // dataavailableイベントのハンドリング mediaRecorder.addEventListener("dataavailable", async (event) => { if (event.data

    && event.data.size > 0) { // BlobをArrayBufferに変換 const arrayBuffer = await event.data.arrayBuffer(); // ArrayBufferをUint8Arrayに変換 const chunk = new Uint8Array(arrayBuffer); // ReadableStreamにチャンクをエンキュー streamControllerRef .current?.enqueue(chunk); } }); // stopイベントのハンドリング mediaRecorder.addEventListener("stop", () => { // ReadableStreamをクローズ streamControllerRef .current?.close(); }); // 録音開始(100msごとにデータを収集) mediaRecorder.start(100);
  5. 音声のバイナリデータをサーバーにStreaming送信 // 先程の関数の中で次を実行する mutate(audioStream) // mutate の中身 const { mutate,

    isPending } = useMutation({ mutationFn: async (audioStream: ReadableStream<Uint8Array>) => { return await httpClient({ jwtToken }) // ky のラッパー .post(`talks/${talkId}/stream`, { body: audioStream, // ReadableStream をそのままBodyに入れてリクエストする timeout: false, }) .text(); } });
  6. Whisper Streaming • Faster Whisper などをバックにして Stream 処理でテキスト化してくれる • whisper_online_server.py

    を使えばサーバー 化できる • これはTCPで動作するので Deno.connect で 接続できる • 入力できる音声データのサンプリングレートな どに指定がある(のでffmpegでの変換が必要)
  7. バックエンド:ReadableStreamの受け取り app.post("/:id/stream", permissionChecker("talks"), async (c) => { const stream =

    c.req.raw.body as ReadableStream; const talkId = Number(c.req.param("id")); const user = c.get("currentUser");
  8. ffmpeg に pipe で音声データを渡す // FFmpegコマンドを設定 const ffmpeg = new

    Deno.Command("ffmpeg", { // Deno.Command も Stream を返す args: [ "-i", "pipe:0", "-ar", "16000", "-ac", "1", "-sample_fmt", "s16", "-f", "wav", "pipe:1", ], stdin: "piped", stdout: "piped", stderr: "piped", }); const ffmpegProcess = ffmpeg.spawn(); stream.pipeTo(ffmpegProcess.stdin); // Web Streams の Pipe で音声データをそのままffmpeg にわたす
  9. Whisper Streaming に pipe でデータを渡す const convertedAudioStream = ffmpegProcess.stdout; //

    Deno.connect も Stream を返す const whisper = await Deno.connect({ hostname: Deno.env.get("WHISPER_HOST") || "localhost", port: Number(Deno.env.get("WHISPER_PORT")) || 43001, }); convertedAudioStream.pipeTo(whisper.writable);
  10. Deno の場合、以下がすべてWeb Streams で扱える • HTTP の Request Body •

    ファイルの読み書き • コマンドの実行 • Deno.connect や fetch による他サーバーとの通信 便利!
  11. Streamからテキストデータの取得 const reader = whisper.readable.getReader(); const decoder = new TextDecoder("utf-8");

    while (true) { const { done, value } = await reader.read(); if (done) { break; } const text = decoder.decode(value); // text を DB に保存したりクライアントに返送したりすればいい }
  12. 概要 何か話す ブラウザで音声デー タにする WhisperStreamingで 音声をテキスト化 音声バイナリを WebStreamsで サーバーに送る フロントエンド

    バックエンド 会話テキストを WebStreamsでポー リング PostgreSQL から データ取得 テキスト表示 Notify 「AIを呼び出す」 ボタンを押下 Ollamaに会話データを 入力して出力をNotifyし テーブルにも保存する Notify Listen Post Request ここの話おわり
  13. 概要 何か話す ブラウザで音声デー タにする WhisperStreamingで 音声をテキスト化 音声バイナリを WebStreamsで サーバーに送る フロントエンド

    バックエンド 会話テキストを WebStreamsでポー リング PostgreSQL から データ取得 テキスト表示 Notify 「AIを呼び出す」 ボタンを押下 Ollamaに会話データを 入力して出力をNotifyし テーブルにも保存する Notify Listen Post Request 次はここの話
  14. PostgreSQL の Channel • Notify/Listen という機能がある • LISTEN foo_channel; •

    • NOTIFY foo_channel, 'Hello!'; • • ; • -- Asynchronous notification "foo_channel" with payload "Hello!" received from server process with PID 5963.
  15. ブラウザ システムへの応用 GET /talks/${talkId}/stream DB中で Listen talks_1; して おく。 通知を受け取ったら

    Stream でフロントにデータを送る POST /talks/${talkId}/stream Notify talks_1 ‘{“a”: 1}’ 通知を送る
  16. DenoにおけるNotify/Listenの実装 export async function notify(channel: string, obj: object) { return

    await sql`select pg_notify(${channel}, ${JSON.stringify(obj)})`.execute( db, ); } export async function listen( listenChannel: string, callback: (msg: { channel: string; payload: string }) => void, ) { const pgClient = await pool.connect(); pgClient.on("notification", callback); return await pgClient.query(listenChannel); }
  17. 概要 何か話す ブラウザで音声デー タにする WhisperStreamingで 音声をテキスト化 音声バイナリを WebStreamsで サーバーに送る フロントエンド

    バックエンド 会話テキストを WebStreamsでポー リング PostgreSQL から データ取得 テキスト表示 Notify 「AIを呼び出す」 ボタンを押下 Ollamaに会話データを 入力して出力をNotifyし テーブルにも保存する Notify Listen Post Request ここの話おわり
  18. 概要 何か話す ブラウザで音声デー タにする WhisperStreamingで 音声をテキスト化 音声バイナリを WebStreamsで サーバーに送る フロントエンド

    バックエンド 会話テキストを WebStreamsでポー リング PostgreSQL から データ取得 テキスト表示 Notify 「AIを呼び出す」 ボタンを押下 Ollamaに会話データを 入力して出力をNotifyし テーブルにも保存する Notify Listen Post Request 次はここの話
  19. ローカルLLMサーバー • Ollama を使用 ◦ Ollama はローカルLLMを実行しやすくするツール ◦ ChatGPT互換APIなどを提供 •

    モデルは Llama-3-ELYZA-JP-8B を使用 ◦ Meta社の llama-3.0 を Elyza 社が日本語化したもの
  20. Text to Speech について • 現状ブラウザ(OS) の機能で読み上げてるだけ function speak(text: string)

    { // SpeechSynthesisUtteranceのインスタンスを作成 const utterance = new SpeechSynthesisUtterance(); utterance.text = text; utterance.lang = "ja-JP"; // 音声を再生 window.speechSynthesis.speak(utterance); }
  21. ブラウザでの Text to Speech の課題 • 棒読み • 声を選ぶなど自由度が少ない •

    フロントの環境依存 ◦ フロント側で日本語の読み上げ機能が有効になっている必要がある • リアルタイム性に欠ける ◦ 現状は10文字ずつ読み上げてるだけ
  22. まとめ • Web Streams API を活用することで同期処理で音声→テキスト、テキスト→音声 の変換処理を行えるようにした • DenoはWeb標準を大切にしているので Web

    Streams API と相性が良い • PostgreSQL の Notify/Listen を活用することでサーバーのプロセスから分離され た Channel を扱うことができる • 音声の読み上げはブラウザだけでも可能