DeciScopeのローカルMVP向け、Go + chi製バックエンドです。
会議API、WebSocketリアルタイム配信、PostgreSQL永続化、 Azure EchoBot向け文字起こし取り込みを 提供します。Teams音声のSTTはVM上のTeams Botが担当し、このAPIはBotから送られる transcript segmentを受け取ります。Azure OpenAIを設定した場合は、会議中ライブ分析と 会議終了時の最終要約も生成します。raw audioのMedia IngressやファイルSTT/ffmpeg処理は 現在のプロダクト範囲には含めていません。
- Go 1.25+
- Docker / Docker Compose
Clean Architectureベースのモジュラーモノリスです。
internal/app
-> internal/adapter, internal/infrastructure
-> internal/application
-> internal/domain
internal/app: 設定読込と具体実装の組み立てinternal/domain: Domain Entity、Error、純粋なRuleinternal/application: Use CaseとOutbound Portinternal/adapter: HTTP、WebSocket、Repository実装internal/infrastructure: DB接続・Migration、Azure OpenAI、Bot制御、 クライアント診断ログ、メール、Firebase
詳細は docs/backend-architecture.md を参照してください。
会議・認証データとEchoBot文字起こし取り込みデータは、いずれもPostgreSQLに保存します。
DECISCOPE_TRANSCRIPT_ONLY=true にすると、会議・認証まわりのcore repositoryは初期化せず、
文字起こし取り込みAPIだけを起動できます(この場合もPostgreSQLへの接続は必要です)。
PostgreSQLの文字起こしテーブルは transcript_segments です。主な列は
event_id, call_id, sequence_no, recognized_at_utc, offset_ticks,
duration_ticks, text, received_at_utc で、event_id と
(call_id, sequence_no) に一意制約があります。同じデータの再送は重複として扱い、
2行目は作りません。
MigrationはAPI起動とは分離しています。同じDockerイメージで次を実行できます。
./deciscope-api migrate
./deciscope-api serveDocker Composeでは migrate serviceが先に成功してから api serviceが起動します。
主な環境変数:
API_PORT: Docker Composeでホストへ公開するAPI port。既定値は9090PORT: APIコンテナ内またはローカル実行時のlisten port。既定値は9090DECISCOPE_BACKEND_ADDR: listen address。設定時はPORTより優先DATABASE_URL: PostgreSQL connection URL。ローカル実行時は必須POSTGRES_DB,POSTGRES_USER,POSTGRES_PASSWORD: Compose PostgreSQL設定DECISCOPE_INGEST_API_KEY: transcript ingest用共有API key。32文字以上、必須DECISCOPE_TRANSCRIPT_ONLY:trueの場合は文字起こし取り込みAPIだけを起動DECISCOPE_WS_ALLOWED_ORIGINS: transcript WebSocketの許可Origin。カンマ区切りDECISCOPE_BOT_CONTROL_URL: Go APIからVM Botへ参加命令を送るURL。Tailscale IPを使いますDECISCOPE_BOT_CONTROL_TOKEN: VM Bot制御API用token。フロントエンドへ渡しませんDECISCOPE_BOT_CONTROL_TIMEOUT_SECONDS: VM Bot制御API呼び出しtimeout。既定値は10FRONTEND_URL,ALLOWED_ORIGINS: CORS設定SESSION_COOKIE_SECURE:trueの場合、セッションCookieにSecure属性を付与AUTH_PROVIDER,FIREBASE_PROJECT_ID,GOOGLE_APPLICATION_CREDENTIALS,FIREBASE_CREDENTIALS_JSON: Firebase認証設定。 詳細は docs/firebase-auth.md を参照してください
完全な例は .env.example を参照してください。.env はGit管理対象外です。
Bot/watchdog、文字起こしヘルス、クライアント診断、AIツリー分類を含む全設定の 用途と既定値は docs/configuration.md を参照してください。
AZURE_OPENAI_ENDPOINT,AZURE_OPENAI_API_KEY,AZURE_OPENAI_DEPLOYMENT: Azure OpenAI接続情報。 いずれか1つでも未設定の場合、AI分析機能全体が自動的に無効化されます(起動時に警告ログを1行出力)。 transcript取り込みや会議終了処理はAI機能の有無に関係なく動作し続けますAZURE_OPENAI_API_VERSION: Azure OpenAI REST APIのバージョン。既定値は2024-10-21AZURE_OPENAI_{LIVE_EXTRACTION,CONTEXT_PLANNER,TREE_AUDIT,TREE_REORGANIZER,FINAL_TREE_REVIEW,FINAL_SUMMARY}_DEPLOYMENT: AI task別のdeployment。未設定のtaskは既存のAZURE_OPENAI_DEPLOYMENTへfallbackします- ライブ抽出は対応deploymentでAzure Structured Outputs(
json_schema,strict: true)を使います。 deploymentがこの形式を明示的に拒否した場合は、同一プロセス中は従来のjson_objectへフォールバックします AI_LIVE_ANALYSIS_ENABLED: 会議中ライブAI分析を行うか。既定値はtrueAI_LIVE_ANALYSIS_INTERVAL_SECONDS: 未分析finalを救済するfallback tick。既定値は10、最小値は5AI_LIVE_ANALYSIS_DEBOUNCE_MILLISECONDS: final確定イベントをまとめるdebounce。既定値は2000AI_LIVE_ANALYSIS_COOLDOWN_SECONDS: 同一sessionのライブAI呼び出し間隔。既定値は8AI_LIVE_ANALYSIS_MAX_WAIT_SECONDS: substantiveな未分析finalの最大待機時間。既定値は18AI_LIVE_ANALYSIS_MIN_CHARS: ライブ分析を実行する最小の新規文字数。既定値は80AI_LIVE_ANALYSIS_MAX_INPUT_CHARS: ライブ分析1回あたりに送る差分transcriptの最大文字数。既定値は4000AI_FINAL_SUMMARY_ENABLED: 会議終了時のAI最終要約生成を行うか。既定値はtrueAI_FINAL_SUMMARY_MAX_INPUT_CHARS: 最終要約に送るtranscriptの最大文字数(超過分は末尾優先で切り詰め)。既定値は12000AI_REQUEST_TIMEOUT_SECONDS: ライブ分析のAzure OpenAI呼び出しtimeout。既定値は20AI_FINAL_SUMMARY_TIMEOUT_SECONDS: 最終要約のAzure OpenAI呼び出しtimeout。既定値は60AI_FINALIZATION_WAIT_TIMEOUT_SECONDS: 実行中分析またはBot通知済み最終sequenceのDB到着を待つ上限。既定値は10AI_FINALIZATION_QUIET_PERIOD_MILLISECONDS: drain情報を送らない旧Bot向けのDB静穏判定。既定値は750、最小値は100AI_FINAL_FLUSH_MAX_ATTEMPTS: 終了時の未処理final抽出の最大試行回数。既定値は3TREE_AUDIT_ENABLED: GPT-5-mini向け議論ツリー監査schedulerを有効化するか。既定値はtrueTREE_AUDIT_INTERVAL_VERSIONS,TREE_AUDIT_INTERVAL_SECONDS,TREE_AUDIT_MIN_INTERVAL_SECONDS: version周期、時間周期、通常triggerのdebounce下限。既定値は順に3,300,300TREE_AUDIT_MAX_RUNS_PER_SESSION,TREE_AUDIT_MAX_RUNS_PER_HOUR: 通常triggerのsession上限・1時間上限。既定値は20,12TREE_AUDIT_HIGH_SEVERITY_MIN_INTERVAL_SECONDS,TREE_AUDIT_HIGH_SEVERITY_MAX_RUNS_PER_HOUR: deterministicな重大異常triggerの別枠debounce・1時間上限。既定値は60,4TREE_AUDIT_UNAPPLIED_WARNING_THRESHOLD: findingを検出したままoperation適用0件が 連続した場合の警告閾値。既定値は3- 入力・頻度・timeout上限の詳細は docs/tree-auditor.md を参照してください
DECISCOPE_TRANSCRIPT_ONLY=true のtranscript-onlyモードでは、AI分析機能は組み込まれません。
Teams会議名を Microsoft Graph の /users/{id}/onlineMeetings から取得するための候補は、
会議作成リクエストの主催者、作成者Microsoft user ID、作成者メールアドレスから組み立てて
Bot join commandへ渡します。ログには値そのものではなく件数とハッシュのみを出します。
PowerShellで .env.example から .env を作り、秘密値を置き換えます。
Copy-Item .env.example .env
notepad .env起動します。postgres はCompose内部ネットワークだけに公開され、PCホストへは公開されません。
docker compose up --build -d
docker compose ps
docker compose logs -f apiMigrationだけを再実行する場合:
docker compose run --rm migrateヘルスチェック:
Invoke-RestMethod http://localhost:9090/healthz
Invoke-RestMethod http://localhost:9090/readyzPostgreSQL内の文字起こしデータ確認:
docker compose exec postgres psql -U deciscope -d deciscopeSELECT call_id, sequence_no, text, offset_ticks, duration_ticks, received_at_utc
FROM transcript_segments
ORDER BY received_at_utc DESC
LIMIT 20;停止します。どちらもnamed volume postgres-data は残ります。
docker compose stop
docker compose downデータも削除したい場合だけ、明示的に -v を付けます。
docker compose down -v既存のC# Bot向けHTTP契約は維持しています。
POST /api/v1/transcript-segments
Content-Type: application/json
X-DeciScope-Api-Key: <DECISCOPE_INGEST_API_KEY>{
"sessionId": "session_...",
"eventId": "06008080-91e3-4b88-a8ff-9af629265ced:1",
"callId": "06008080-91e3-4b88-a8ff-9af629265ced",
"sequenceNo": 1,
"speakerId": "8:orgid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"speakerName": "山田 太郎",
"recognizedAtUtc": "2026-06-25T13:20:01.1234567+00:00",
"offsetTicks": 20300000,
"durationTicks": 18000000,
"text": "本日の会議を開始します。"
}sessionId は任意です。既存のVM Botや手動POSTが sessionId を送らなくても
従来どおり保存できます。会議セッション作成APIから返った sessionId を付けると、
履歴取得とWebSocketで sessionId による絞り込みができます。
speakerId / speakerName も任意です。Botが話者情報を送った場合は
PostgreSQLの speaker_id / speaker_name に保存し、履歴APIとWebSocket配信にも
camelCaseで含めます。
手動テスト:
$apiKey = Read-Host "DeciScope API key"
$headers = @{
"X-DeciScope-Api-Key" = $apiKey
}
$n = Get-Random -Minimum 1000 -Maximum 999999
$body = @{
sessionId = "manual-speaker-session"
eventId = "manual-speaker-test-$n"
callId = "manual-speaker-call"
sequenceNo = $n
recognizedAtUtc = [DateTimeOffset]::UtcNow.ToString("O")
offsetTicks = 0
durationTicks = 10000000
text = "Go APIへの保存テストです。"
speakerId = "manual-speaker-001"
speakerName = "手動テスト太郎"
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:9090/api/v1/transcript-segments" `
-Headers $headers `
-ContentType "application/json; charset=utf-8" `
-Body $bodyDB確認:
SELECT session_id, call_id, sequence_no, speaker_id, speaker_name, text, recognized_at_utc
FROM transcript_segments
ORDER BY recognized_at_utc DESC
LIMIT 10;履歴取得レスポンスにも話者情報が含まれます。
{
"items": [
{
"sessionId": "manual-speaker-session",
"eventId": "manual-speaker-test-1234",
"callId": "manual-speaker-call",
"sequenceNo": 1234,
"speakerId": "manual-speaker-001",
"speakerName": "手動テスト太郎",
"recognizedAtUtc": "2026-06-27T07:00:00Z",
"offsetTicks": 0,
"durationTicks": 10000000,
"text": "Go APIへの保存テストです。",
"receivedAtUtc": "2026-06-27T07:00:01Z"
}
]
}VMから接続する場合、PostgreSQLコンテナや postgres:5432 へ直接接続しません。
VM側の送信先はPCホストのTailscale IPとComposeで公開したAPI portです。
http://100.70.221.61:9090/api/v1/transcript-segments
API_PORT を変えた場合は、上記の 9090 をそのホスト側公開portへ変更してください。
VM BotはWebSocketへ接続しません。従来どおり
POST /api/v1/transcript-segments へHTTP POSTし、Go APIがDB保存に成功した
新規segmentだけを接続中のフロントエンドへbroadcastします。同一内容の再送
duplicate: true は配信しません。
ブラウザはSession Cookieとworkspace所属検査で保護された次のAPIだけを利用します。
WS /v1/workspaces/{workspace_code}/meeting-sessions/{session_id}/transcript-stream
GET /v1/workspaces/{workspace_code}/meeting-sessions/{session_id}/transcript-segments
WebSocket message:
{
"type": "transcript_segment.created",
"sentAtUtc": "2026-06-27T00:00:00Z",
"data": {
"sessionId": "session_...",
"eventId": "09005080-cce6-4132-9404-1e823df47ff9:6",
"callId": "09005080-cce6-4132-9404-1e823df47ff9",
"sequenceNo": 6,
"speakerId": "8:orgid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"speakerName": "山田 太郎",
"recognizedAtUtc": "2026-06-27T00:00:00Z",
"offsetTicks": 287000000,
"durationTicks": 41200000,
"text": "うーんってことは、まあ一旦大丈夫そうかな。",
"duplicate": false
}
}会議セッションの状態変化も同じWebSocketへ配信されます。
{
"type": "meeting_session.status_changed",
"sentAtUtc": "2026-06-27T00:00:00Z",
"data": {
"sessionId": "session_...",
"status": "joined",
"botCallId": "09005080-cce6-4132-9404-1e823df47ff9"
}
}フロントエンドはVM Botを直接叩きません。通常のブラウザUIは認証済みの
POST /v1/workspaces/{workspace_code}/meeting-sessions へTeams会議URLを送信し、
Go APIがTailscale内のVM Bot制御APIへ参加命令を送ります。
/api/v1/meeting-sessions はAPI-keyを使う互換/手動確認用の非workspaceルートです。
VM Bot制御APIの設定:
DECISCOPE_BOT_CONTROL_URL=http://<VM_TAILSCALE_IP>:<PORT>/internal/bot/join
DECISCOPE_BOT_CONTROL_TOKEN=change-me-bot-control-token
DECISCOPE_BOT_CONTROL_TIMEOUT_SECONDS=10Go APIからVM Botへ送るリクエスト:
POST <DECISCOPE_BOT_CONTROL_URL>
Content-Type: application/json
X-DeciScope-Bot-Control-Token: <DECISCOPE_BOT_CONTROL_TOKEN>{
"sessionId": "session_...",
"joinUrl": "https://teams.microsoft.com/l/meetup-join/..."
}会議終了時は、設定URL末尾の /join を除いた同じベースURLへ次を送ります。
POST http://<VM_TAILSCALE_IP>:<PORT>/internal/bot/meeting-sessions/{session_id}/end
Content-Type: application/json
X-DeciScope-Bot-Control-Token: <DECISCOPE_BOT_CONTROL_TOKEN>Go APIホストからBotホストへのfirewall/proxyでは、参加用の
/internal/bot/join と終了用の /internal/bot/meeting-sessions/{session_id}/end
の両方を許可してください。
VM Botが2xxを返すと meeting_sessions.status は command_sent になります。
4xx/5xx/timeoutの場合は failed と last_error を保存します。
joinUrl 全文とtokenはログへ出さず、ログには sessionId と joinUrlHash を使います。
手動テスト:
$body = @{
joinUrl = "https://teams.microsoft.com/l/meetup-join/..."
} | ConvertTo-Json
Invoke-WebRequest `
-Uri "http://localhost:9090/api/v1/meeting-sessions" `
-Method POST `
-ContentType "application/json" `
-Body $body `
-UseBasicParsing状態取得:
Invoke-RestMethod "http://localhost:9090/api/v1/meeting-sessions/<sessionId>"ブラウザUIと同じ経路を確認する場合は、Firebase loginで発行された
deciscope_session Cookieを持つ状態で、workspace-scoped APIを使います。
VM Botからの状態更新:
$apiKey = Read-Host "DeciScope ingest API key"
$body = @{
status = "joined"
botCallId = "09005080-cce6-4132-9404-1e823df47ff9"
message = "joined successfully"
} | ConvertTo-Json
Invoke-RestMethod `
-Method Patch `
-Uri "http://localhost:9090/api/v1/bot/meeting-sessions/<sessionId>/status" `
-Headers @{ "X-DeciScope-Api-Key" = $apiKey } `
-ContentType "application/json" `
-Body $bodyレガシーの履歴GETと共有token WebSocketはルーターから削除されています。
DECISCOPE_INGEST_API_KEY はBotからのPOST専用で、ブラウザへ渡さないでください。
フロントエンドもDocker Composeで起動する場合、フロントエンドコンテナから
Go APIへは http://api:9090 で到達できます。ただしブラウザで実行される
JavaScriptから api:9090 は解決できないため、ブラウザのWebSocket URLは
ホスト公開ポートかフロントエンド側proxyを使います。
手動確認の流れ:
# 認証済みフロントエンドを開き、別shellから既存HTTP POSTでsegmentを送信
$apiKey = Read-Host "DeciScope ingest API key"
$headers = @{ "X-DeciScope-Api-Key" = $apiKey }
$body = @{
eventId = "manual-ws-test:1"
callId = "manual-ws-test"
sequenceNo = 1
recognizedAtUtc = [DateTimeOffset]::UtcNow.ToString("O")
offsetTicks = 0
durationTicks = 10000000
text = "WebSocket配信テストです。"
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:9090/api/v1/transcript-segments" `
-Headers $headers `
-ContentType "application/json; charset=utf-8" `
-Body $bodyGET /v1/healthGET /healthzGET /readyzPOST /api/v1/transcript-segmentsPOST /api/v1/meeting-sessions(互換/手動確認用。API key必須)GET /api/v1/meeting-sessions/{session_id}(互換/手動確認用。API key必須)PATCH /api/v1/bot/meeting-sessions/{session_id}/statusPOST /internal/client-diagnostics(有効時。Session Cookie必須)GET /v1/workspaces/{workspace_code}/meetingsPOST /v1/workspaces/{workspace_code}/meetingsGET /v1/workspaces/{workspace_code}/meeting-sessionsGET /v1/workspaces/{workspace_code}/meeting-sessions/final-summariesPOST /v1/workspaces/{workspace_code}/meeting-sessionsGET /v1/workspaces/{workspace_code}/meeting-sessions/{session_id}POST /v1/workspaces/{workspace_code}/meeting-sessions/{session_id}/endDELETE /v1/workspaces/{workspace_code}/meeting-sessions/{session_id}/PATCH /v1/workspaces/{workspace_code}/meeting-sessions/{session_id}/agenda-progressGET /v1/workspaces/{workspace_code}/meeting-sessions/{session_id}/transcript-segmentsGET /v1/workspaces/{workspace_code}/meeting-sessions/{session_id}/transcript-streamGET /v1/meetings/{meeting_id}GET /v1/meetings/{meeting_id}/events?after_seq=0GET /v1/meetings/{meeting_id}/segments?after_seq=0WS /v1/realtime?meeting_id={meeting_id}
API一覧は docs/api.md、ローカル起動手順は docs/local-dev.md を参照してください。
go test ./...
go vet ./...Repository契約テストはMemoryとPostgreSQLに同じSuiteを実行します。依存方向、 Adapter間依存、環境変数読込の配置もArchitecture Testで検査します。