Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ CAPTION_RELAY_STT_USAGE_PATH=
CAPTION_RELAY_AI_TIMEOUT_SECONDS=15
# Optional absolute path; empty/unset uses the user's data directory.
CAPTION_RELAY_DICTIONARY_PATH=
CAPTION_RELAY_ROOMS_PATH=
CAPTION_RELAY_UDTALK_URL=
28 changes: 28 additions & 0 deletions .impeccable/surfaces/frontend-src-views-home-tsx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
version: 1
slug: "frontend-src-views-home-tsx"
primary_target: "frontend/src/views/home.tsx"
related_targets: ["frontend/src/views/room.tsx"]
---

# ホームとルームの操作

Mode: Operate

## Direction contract

THESIS: 運用者が以前使ったルームへ戻り、新しいルームを登録し、次のトークのために字幕だけを消せるようにする。

OWN-WORLD: 既存の白い作業面、灰色の細い区切り、黒い主操作、青いフォーカス、Lucideの線画を引き継ぐ。余白は8・16・24・32pxを基準にし、ホームの本文は16px、行間24pxとする。削除を確定するボタンは白文字と暗い赤で区別する。

STORY: ホームで名前と最終利用日時からルームを選ぶ。追加時は名前・配信先・必要な参加リンクを入れる。接続後は字幕の確認と訂正に集中し、リセットと離脱は対象と下書きの有無を確かめる。

FIRST VIEWPORT: 上部にCaption Relayと新しいルームへの接続。下は区切り線のある一列のルーム一覧。各行の左に名前と接続方法・最終利用日時、右に接続設定と接続を置く。狭い画面では操作を次の行の右へ回す。ルーム画面は既存の操作画面の上に名前、ホーム、リセットを置く。

FORM: 既存のOperate画面の拡張。新しい世界や構成案の選択は行わず、受け入れ済みのホーム・ルーム操作を既存の部品と配色で実装する。

FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance

## Constraints

同時に入力するのは一つのルーム。字幕と背景を混ぜない。参加リンクは履歴のAPI応答へ返さない。リセットは確認後にCaption Relayの字幕と下書きだけを消し、UDトークの字幕とルーム設定を残す。Rootは一つとし、Viewは通知、Mediatorは判断、Adapterは通信を担当する。
2 changes: 1 addition & 1 deletion .impeccable/surfaces/operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ FINISH: unreviewed and undocumented is unfinished; this build ends with the fini

## Constraints

人の修正は明示的に保存する。外部AIへの接続は後の段階に行い、今回は選択・振り分けと結果の受け取りまで。画面はRoot配下のPassive Viewとし、操作をChain経由で状態機械のMediatorへ渡す。認証・永続化・会場実機の試験は現行PoCの対象外。
人の修正は明示的に保存する。外部AIへの接続は後の段階に行い、今回は選択・振り分けと結果の受け取りまで。画面はRoot配下のPassive Viewとし、操作をChain経由で状態機械のMediatorへ渡す。認証・字幕の永続化・会場実機の試験は現行PoCの対象外。ルームの設定は保存する。

INTERACTION: 右ペインの境界は24pxの操作領域でドラッグでき、右端で収納する。幅と未保存入力を保持して再表示し、キーボードでも操作できる。
9 changes: 7 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# システム構成

実装確認日:2026-10-11
実装確認日:2026-10-25

Caption Relay は、単一の FastAPI プロセスが字幕の状態を持つローカル PoC。React の操作画面は HTTP で取得・更新し、観客画面は SSE で公開字幕を受け取る。現在の振る舞いは[現行 PoC の仕様](docs/current-behavior.md)、起動方法は[開発手順](docs/development.md)を参照する。

Expand All @@ -9,6 +9,7 @@ Caption Relay は、単一の FastAPI プロセスが字幕の状態を持つロ
| 場所 | 責務 |
|---|---|
| [backend/app/main.py](backend/app/main.py) | HTTP ルート、エラー応答、SSE、ビルド済み画面の配信 |
| [backend/app/rooms.py](backend/app/rooms.py)・[room_store.py](backend/app/room_store.py) | 一つの入力接続とルームごとのWorkflow、設定の保存・復元、リセットと古い操作の拒否 |
| [backend/app/relay.py](backend/app/relay.py) | セッション状態、入力の改版・再送判定、手修正、公開可否、購読キュー |
| [backend/app/workflow.py](backend/app/workflow.py) | 入力、AI処理の開始・取消、訂正先への送信、保存時の改版照合 |
| [backend/app/connections.py](backend/app/connections.py) | 接続モードと入力の開始・終了、接続変更と保存の直列化 |
Expand All @@ -33,6 +34,8 @@ Caption Relay は、単一の FastAPI プロセスが字幕の状態を持つロ

## データの流れ

ホームは `Rooms` の接続履歴を読み、ルームごとの `CaptionWorkflow` を選ぶ。同時に入力するのは一つのルームだけで、切り替え時に前の入力とAI要求を終了する。字幕は各Relayのメモリ、名前・接続先・背景・AI・表示・公開設定は `RoomStore` のファイルに保持する。単語辞書は全ルームで共有する。保存範囲と利用手順は[ルームの接続と字幕リセット](docs/rooms.md)を参照する。

```mermaid
flowchart LR
ud[UDトーク] -->|Zoom形式のPOST| api
Expand All @@ -56,7 +59,7 @@ flowchart LR

FastAPIはPydanticモデルで入出力を検証し、入力と訂正保存を `CaptionWorkflow` へ渡す。状態遷移と公開可否は `CaptionRelay`、外部接続の寿命と送信先は `Connections` が所有する。AI要求はバックグラウンドで実行し、元の発話の更新とモード変更で終了する。保存と接続変更は同時に実行しない。訂正先への送信前後に原文・表示文の改版を照合し、古い原文で新しい発話を上書きしない。存在しない字幕と改版競合はAPI層で404・409に変換する。

`create_app()`ごとに独立した字幕状態を作る。字幕状態を共有するデータベースはなく、再起動で消えるため、複数ワーカーで運用しない。字幕・接続・AI設定・測定結果をこのプロセスに保持する。各AI提供元のキーは環境変数で読み、画面へ返さない。通常起動では単語辞書とマイク認識の試験使用量を再起動をまたぐファイルに保存する。テスト用の `create_app()` は辞書を注入しない限りメモリ上に保持する。詳細は[単語辞書](docs/dictionary.md)を参照する。
`create_app()`ごとに独立したルーム管理を作る。字幕状態を共有するデータベースはなく、再起動で消えるため、複数ワーカーで運用しない。字幕とAIの測定結果はこのプロセスに保持する。各AI提供元のキーは環境変数で読み、画面へ返さない。通常起動ではルームの設定、単語辞書、マイク認識の試験使用量をファイルに保存する。テスト用の `create_app()` は保存先を注入しない限りメモリ上に保持する。詳細は[ルーム](docs/rooms.md)と[単語辞書](docs/dictionary.md)を参照する。

接続の初期値は独立配信。Zoom形式の送信先はプロセス内で生成し、再起動と接続変更で更新する。従来のWeb入力Adapterは `CAPTION_RELAY_UDTALK_URL` を設定した場合に開始する。編集参加へ切り替えると両方の入力を止め、有効な編集用URLの参加を開始する。入力の変換は各Adapter、公開と手修正の判断はRelayが所有する。経路・根拠・制約は[UDトークの入力接続](docs/udtalk-input.md)を参照する。

Expand All @@ -68,6 +71,8 @@ FastAPIはPydanticモデルで入出力を検証し、入力と訂正保存を `

## API と公開データの境界

操作APIは `X-Caption-Relay-Room` と `X-Caption-Relay-Generation` で対象とリセットの世代を固定する。書き込みは現在接続中のルームだけに許可し、古い世代には409を返す。観客APIの `room_id` とSSEは取得開始時のRelayを保持し、ルーム切り替えで別の字幕へ切り替えない。リセットは保存・外部送信と直列化し、AI要求の取消完了後に字幕を消す。字幕の改版番号を巻き戻さず、リセット済みの発話IDを再送でも採用しない。

Zoom受信先の資格情報は操作画面用の接続状態だけに返す。編集用URLは適用時だけ送信し、応答・字幕ID・ログへ含めない。AIキーは提供元の認証ヘッダーで送り、画面には設定済みかどうかだけを返す。アクセスログはURLの資格情報を記録しないよう起動スクリプトで無効にする。アプリ全体の認証はなく、loopbackでのローカル利用を前提とする。

バックエンドは FastAPI と Pydantic v2 を使用する。入力・応答と内部の状態をモデルで型付けし、`backend/pyproject.toml` の pyright 厳格モードを通す。基底の `Model` は余分な項目と暗黙の型変換を拒否し、代入も検証する。
Expand Down
2 changes: 1 addition & 1 deletion PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ web
- 「字幕を隠す」は観客への表示を止める操作。入力と編集は継続する。「字幕を表示」では新しい発話から再開し、非表示期間の発話を自動公開しない。
- 観客には公開を許可した字幕だけを送り、原文や管理情報を送らない。観客画面は初期設定で公開字幕を残しながら末尾へスクロールする。過去の字幕を読み返し、「最新へ」で追従を再開できる。背景・文字・書体・サイズ・行間を調整でき、直近の発話だけの表示にも切り替えられる。
- Gemini・Claude・OpenAIのAPIによる校正と、費用・実測速度に基づく振り分けを設定できる。AIは初期値オフで、キーを設定した提供元へ次の確定字幕から依頼する。GPTの実生成と、Apple認識からGPT-5.4 Miniによる自動校正・編集・配信を合成音声で確認した。[AIの確認記録](docs/ai-routing.md#実接続での確認)を参照する。各社の品質・速度比較と請求確定額は未確認。
- 認証と字幕の永続化は未実装。字幕と画面の設定は再起動で消え、単語辞書は保存先に残る。アプリはローカルの単一プロセスで使う。独立配信はUDトークのZoom連携から受信する。編集参加は試験接続として提供し、純正への手動訂正・AI訂正の反映を合成音声で確認した。[実接続の範囲](docs/udtalk-input.md#復旧後の実接続確認)を参照する。
- 認証と字幕の永続化は未実装。字幕は再起動で消え、ルームの接続履歴・設定と単語辞書は保存先に残る。アプリはローカルの単一プロセスで使う。独立配信はUDトークのZoom連携から受信する。編集参加は試験接続として提供し、純正への手動訂正・AI訂正の反映を合成音声で確認した。[実接続の範囲](docs/udtalk-input.md#復旧後の実接続確認)を参照する。

振る舞いの詳細、テストとの対応、その他の未実装事項は [現行 PoC の仕様](docs/current-behavior.md)を参照する。将来要件を含む [v0.2 の設計案](docs/design-v0.2.md)を、現行機能の説明に使わない。

Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ UDトークまたはマイク認識の字幕を、AIと手動で訂正して観

UDトークがない場合は、独立配信の入力元を「マイク」にし、OpenAIまたは接続先MacのApple認識を選べます。準備と開始・停止は[マイク入力](docs/microphone-input.md)を参照してください。両方とも合成音声で実認識を確認しています。物理マイクと会場での認識は未確認です。

AIは初期値オフです。サーバーにGemini・Claude・OpenAIのAPIキーを設定すると、次の確定字幕から校正を実行します。APIキー未設定時と失敗時は原文を保持します。認証と字幕の永続化はなく、ローカルの単一プロセスで使用します。再起動すると字幕と画面の設定は消えます。単語辞書は保存先に残ります。
AIは初期値オフです。サーバーにGemini・Claude・OpenAIのAPIキーを設定すると、次の確定字幕から校正を実行します。APIキー未設定時と失敗時は原文を保持します。認証と字幕の永続化はなく、ローカルの単一プロセスで使用します。字幕は再起動で消えます。ルームの接続履歴・設定と単語辞書は保存先に残ります。

## 起動

Expand All @@ -23,13 +23,15 @@ bash scripts/dev.sh

| 画面 | URL |
|---|---|
| 操作画面 | <http://127.0.0.1:24681/> |
| 観客画面 | <http://127.0.0.1:24681/view/demo> |
| ホーム | <http://127.0.0.1:24681/> |
| 観客画面 | 接続後の操作画面のモニターアイコンから開きます |

`setup.sh` は依存関係を導入し、画面をビルドします。終了時はサーバーを Ctrl+C で止めます。フロントエンドの変更を即時確認する手順は[開発手順](docs/development.md)、Codex Cloud と SSH 接続は[リモート開発](docs/remote-development.md)に記載しています。

## 試す

ホームからルームへ接続します。初回は「独立配信」を選ぶか、「新しいルームに接続」で名前と接続先を登録します。ホームへの帰還と字幕リセットはルーム画面の上部にあります。詳しくは[ルームの接続と字幕リセット](docs/rooms.md)を参照してください。

1. 接続アイコンから「字幕入力の接続設定」を開き、「独立配信」の送信先をUDトークのZoom連携に設定します。実機を使わない画面確認は「準備」の「テスト字幕を送る」で行えます。
2. 表示文をクリックして訂正欄を開き、修正して「保存して公開」を押します。未保存の下書きは観客へ出ません。Ctrl / Command + Enterでも保存できます。
3. 「新着の公開」で自動公開・確認して公開を選びます。「字幕を隠す」は表示を止め、「字幕を表示」は新しい発話から再開します。
Expand Down
18 changes: 12 additions & 6 deletions backend/app/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,20 @@ def __init__(self, accept: Callable[[SourceEvent, bool], Caption | SourceIgnored

def start(self) -> None:
self._running = True
self.web.start()
if self.mode == "host" and self.input_source == "udtalk":
self.web.start()

async def stop(self) -> None:
await self.microphone.stop()
await self.web.stop()
if self.editor is not None:
await self.editor.stop()
self._running = False
async with self._changes:
await self.microphone.stop()
await self.web.stop()
if self.editor is not None:
await self.editor.stop()
self._running = False

async def reset_captions(self, reset: Callable[[], Awaitable[None]]) -> None:
async with self._changes:
await reset()

def status(self) -> ConnectionStatus:
if self.editor is not None:
Expand Down
Loading
Loading