dfejza.com
制作物一覧
ML・音声

RadioJP

公開中· ベータ

リアルタイムAI文字起こし機能搭載の日本語ラジオストリーミング。faster-whisperパイプラインがライブ放送を継続的に文字起こしし、MeCabがふりがなを付与、フロントエンドが再生に同期したワードレベルのカラオケハイライトを表示。HLSストリーミング、局検索、お気に入り、地域グループ、RadiKo認証にも対応。

TypeScriptReactNext.jsHLS.jsfaster-whisperPythonMeCabZustand

AI生成: この技術解説はプロジェクトのソースコードからAIが作成したもので、不正確な記述を含む場合があります。

RadioJPは、ライブAIキャプション付きで日本語ラジオをストリーミングします。faster-whisperパイプラインが放送をオンエア中に文字起こしし、MeCabがふりがなを付与、プレイヤーが再生に同期したトランスクリプトを表示するため、聞こえている内容を目で追いながら読めます。文字起こしパイプラインは別のPythonサービスとして動作しており、本稿が扱うのはリポジトリ内のNext.jsフロントエンド — HLS再生、キャプションの同期とゲーティング、Zustandのプレイヤーストア、RadiKo認証です。すでに稼働しており利用可能で、現在はベータ段階です。

アーキテクチャ

フロントエンドはバックエンドサービスと3つのチャネルで通信します。局一覧・ストリームマニフェスト・アーカイブファイル用のHTTP、ライブ音声用のHLSマニフェスト+セグメントストリーム、そしてキャプション用のServer-Sent Eventsストリームです。アプリのレイアウトにマウントされたZustandストアが所有する単一の <audio> 要素が、ページ遷移をまたいで再生を引き継ぎます。ライブ音声はHLS.jsのシングルトンを通り、アーカイブ再生は音声の src を直接設定します。キャプションは再生時刻と照合され、音声が文字起こしを追い越したときはキャプションゲートが音声を抑えます。

flowchart TD
    accTitle: RadioJP フロントエンドの再生・字幕パイプライン
    accDescr: バックエンドサービスが HTTP・HLS・SSE で Zustand 再生ストアに供給します。ストアはライブ音声を HLS.js シングルトンへ、アーカイブ音声をファイル直再生へルーティングし、ライブ経路が放送時刻で照合される SSE 字幕レイヤーを駆動します。
    BE["バックエンドサービス<br/>局一覧 · ライブHLS · アーカイブ · 字幕 (SSE)"]
    STORE["usePlaybackStore (Zustand, app layout)<br/>単一の &lt;audio&gt; 要素 · ライブ/アーカイブ切替<br/>音量・お気に入りを永続化"]
    HLS["HLS.js シングルトン<br/>3段階エラーリカバリ"]
    ARC["audio.src = ファイル<br/>Ogg/Opus · シークオフセット"]
    CAP["useCaptions (SSE)<br/>EXT-X-PROGRAM-DATE-TIME で照合<br/>転写ホライズンで音声をゲート · ふりがな"]
    BE --> STORE
    STORE -->|ライブ| HLS
    STORE -->|アーカイブ| ARC
    HLS --> CAP

ライブ再生とリカバリー

ライブ音声は6秒セグメントのHLSです。プレイヤーはバッファの余裕を確保するため、意図的にライブエッジから約15分後ろに位置し(liveSyncDurationCount: 150)、約25分でジャンプを強制します。HLS.jsはReactステートではなくモジュールレベルのシングルトンとして保持しています。メディア要素を所有しており、再レンダリングをまたいで生存する必要があるためです。

エラーは連続エラーカウンタに基づいて3段階にエスカレーションします。buffering(2以下、無音で自動リトライ)、recovering(3〜5、アンバー表示)、degraded(5超、リトライを停止し「タップでリトライ」を表示)です。録音が存在しない場合は、リトライせず即座にdegradedに短絡します。致命的なネットワークエラーは startLoad(-1) を呼び、メディアエラーは recoverMediaError() を呼び、非致命的なエラーは上書きせずHLS.js自身のバックオフに委ねます。ヘルスビーコンが10秒ごとに状態をログし、ドリフトチェックが再生が壁時計から25分以上遅れた場合にライブへ再同期します(クールダウン60秒)。

「ライブへジャンプ」は、HLS.jsの liveSyncPosition を意図的に信頼しません。浅いマニフェストでは黙ってマニフェスト先頭にクランプされるためです。代わりにターゲットは seekable レンジの終端から設定済みの遅延を引いて算出し、それがロールオフ余白に入る場合はライブエッジ寄りにフォールバックします。タブスリープからの復帰では、タブが前面に戻った後に currentTime が進んでいるかを確認し、停止していれば再同期します。

キャプションの同期とゲーティング

キャプションはSSE経由で個別のセグメントとして到着し、各セグメントはUTCの開始・終了タイムスタンプと、任意でワードレベルのタイミングおよびふりがな読みを持ちます。これらは EXT-X-PROGRAM-DATE-TIME(HLS.jsの playingDate)を介して音声の実際の放送時刻と照合されるため、別途同期エンドポイントは不要です。ライブの照合はUTCミリ秒で2秒の許容差と直近フォールバック付き、アーカイブの照合はファイルオフセット秒で行います。オーバーレイは500msごとにアクティブなキャプションを再評価します。

トランスクライバーはリアルタイム音声より約10〜30秒遅れます(Whisperが音声をチャンクにバッチ処理するため)。ライブエッジ付近では再生が最新キャプションを追い越し、キャプションが音声に対して遅れて見える状態になります。キャプションゲートがこれを解決します。500msのループが音声の放送時刻を地平線(最新セグメントの utc_end)と比較し、地平線の2秒以内で音声を一時停止、キャプションが5秒分追いついたら再開します。ゲートはキャプションをオフに切り替えた時やコンポーネントのアンマウント時にきれいに解除されるため、音声を一時停止のまま放置することはありません。

SSE接続は自己修復します。データが30秒途絶えるとスタールネス監視が再接続し、ブラウザがストリームを閉じると手動再接続が発火し、バックグラウンドのタブを前面に戻すと新規接続を強制します。保持するセグメントはリングバッファで上限を設け(500件にトリム)、プレイヤーの最悪ケースのライブエッジ遅延をカバーできるサイズにしています。

状態と認証

Zustandストアは volumefavorites のみをlocalStorageに永続化し、ランタイム状態(現在の局、再生ステータス、音声要素)はマウント時に再構築します。復元された局は自動再生せず、ユーザーが明示的に再開する必要があります。アーカイブモードは音声の src を直接設定して録音(Ogg/Opus)を読み込み、キャプチャしたシークオフセットを loadedmetadata で適用するため、後続のストア更新が誤った時間帯にシークすることはありません。

RadiKo認証は鍵交換方式です。JWTをメモリに保持して Bearer トークンとして送信し、デスクトップではhttpOnlyクッキーを使用します。このベアラーフォールバックは特にiOS Safari向けに存在します。Intelligent Tracking Preventionがクロスオリジンのクッキーをブロックするためです。フェッチラッパーがトークンを付与し、認証ルート以外で401が返るとログインへリダイレクトします。