@minimalcorp/tsunagi 0.3.3 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
- package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0qjafjt._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_13w5ytx._.js → _01~tp-i._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0kflgfj._.js → _03lm.~7._.js} +3 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0g79jqo._.js → _09br4yx._.js} +3 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0h-u~an._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_139_l.u._.js → _0kkt7a_._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0ts4rpp._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_13ne7ej._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +1 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +3 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0cv326~._.js → node_modules_06jdmu~._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0h.vgyn._.js → node_modules_0ac2-~z._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0sn0lqb._.js → node_modules_0gglkx9._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0tod6y8._.js → node_modules_0h4h_aw._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0q_5qo~._.js → node_modules_0i5v48e._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09d2oqj._.js → node_modules_0k3x6_w._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0oip5a1._.js → node_modules_0q8yuvq._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0npujym._.js → node_modules_0rnsluy._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0krwgny._.js → node_modules_0t-jssg._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0l23gkf._.js → node_modules_0t6ysh_._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_11-z65a._.js → node_modules_0ut2-yw._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_huoqd._.js → node_modules_0vq~hzy._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0a57phw._.js → node_modules_0w6fm3f._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0xp9nhx._.js → node_modules_0yb2sh9._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0q_xfyl._.js → node_modules_0yc~s8p._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0ak_3rk._.js → node_modules_0zi0w9_._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_04pfam-._.js → node_modules_0zzdoud._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0w2dm3r._.js → node_modules_lucide-react_dist_esm_icons_0zerlct._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
- package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/{0l83q95s~tih4.js → 08s3o7outb~0z.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/0a2u28f44p7t..js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0oricprxoxb_d.js → 0ae7zmpe8063z.js} +10 -10
- package/.next/standalone/apps/web/.next/static/chunks/0dgu~u-1e5b7-.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0whl1y16cvn5q.js → 0e8u7rqrqfiiy.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/{0z60ewb40i2ce.js → 0gxhy80~c0gvp.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/0n72w4sxrwoj4.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/0o1bh_qwdvmid.css +4 -0
- package/.next/standalone/apps/web/.next/static/chunks/{17~93fal1fw_d.js → 0uugzgqr-ug3_.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/{149sia28cskw9.js → 0yp~t_.pozr-d.js} +6 -6
- package/.next/standalone/apps/web/.next/static/chunks/0ysgl4vmer~v5.js +6 -0
- package/.next/standalone/apps/web/.next/static/chunks/{053h220fng_68.js → 11u7x4ic4~mp3.js} +10 -10
- package/.next/standalone/apps/web/.next/static/chunks/13_9zaka0bif_.js +1 -0
- package/.next/standalone/apps/web/package.json +1 -3
- package/README.md +1 -1
- package/dist/app.js +3 -4
- package/dist/plugin-lifecycle.d.ts +5 -3
- package/dist/plugin-lifecycle.js +40 -16
- package/dist/prisma/migrations/20261011000000_add_codex_support/migration.sql +20 -0
- package/dist/prisma/schema.prisma +9 -5
- package/dist/server/generated/prisma/browser.d.ts +2 -2
- package/dist/server/generated/prisma/client.d.ts +2 -2
- package/dist/server/generated/prisma/internal/class.d.ts +4 -4
- package/dist/server/generated/prisma/internal/class.js +4 -4
- package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +41 -39
- package/dist/server/generated/prisma/internal/prismaNamespace.js +4 -2
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +5 -3
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +4 -2
- package/dist/server/generated/prisma/models/AgentProfile.d.ts +972 -0
- package/dist/server/generated/prisma/models/Tab.d.ts +36 -1
- package/dist/server/generated/prisma/models.d.ts +1 -1
- package/dist/server/index.js +2 -7
- package/dist/server/lib/agent-profiles.d.ts +44 -0
- package/dist/server/lib/agent-profiles.js +130 -0
- package/dist/server/lib/agents/claude.d.ts +2 -0
- package/dist/server/lib/agents/claude.js +130 -0
- package/dist/server/lib/agents/cli.d.ts +14 -0
- package/dist/server/lib/agents/cli.js +57 -0
- package/dist/server/lib/agents/codex.d.ts +2 -0
- package/dist/server/lib/agents/codex.js +285 -0
- package/dist/server/lib/agents/index.d.ts +11 -0
- package/dist/server/lib/agents/index.js +19 -0
- package/dist/server/lib/agents/login-callback.d.ts +11 -0
- package/dist/server/lib/agents/login-callback.js +47 -0
- package/dist/server/lib/agents/types.d.ts +77 -0
- package/dist/server/lib/agents/types.js +8 -0
- package/dist/server/lib/pty-env-sync.js +1 -1
- package/dist/server/lib/pty-launch.d.ts +8 -10
- package/dist/server/lib/pty-launch.js +20 -58
- package/dist/server/lib/repositories/task.js +1 -0
- package/dist/server/lib/tab-todos.d.ts +5 -0
- package/dist/server/lib/tab-todos.js +13 -0
- package/dist/server/pty-manager.d.ts +7 -2
- package/dist/server/pty-manager.js +13 -0
- package/dist/server/routes/agent-profiles.d.ts +2 -0
- package/dist/server/routes/agent-profiles.js +193 -0
- package/dist/server/routes/env.js +2 -2
- package/dist/server/routes/hooks.d.ts +4 -19
- package/dist/server/routes/hooks.js +83 -73
- package/dist/server/routes/mcp.js +2 -2
- package/dist/server/routes/onboarding.js +19 -10
- package/dist/server/routes/tasks.js +4 -4
- package/dist/server/routes/terminal.js +47 -34
- package/dist/with-plugin.js +3 -3
- package/npm-shrinkwrap.json +37 -24
- package/package.json +1 -1
- package/tsunagi-marketplace/codex/tsunagi-task.md +120 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__11y.qj~._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_03vftq7._.js +0 -7
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hnxatt._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_10gk93.._.js +0 -3
- package/.next/standalone/apps/web/.next/static/chunks/0.gcmzq82~36i.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/06gfh91..uydi.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/0b90ys7wq-p.4.css +0 -4
- package/.next/standalone/apps/web/.next/static/chunks/0crn03y2tj0lu.js +0 -6
- package/.next/standalone/apps/web/.next/static/chunks/0lvl3ec93cab_.js +0 -7
- package/.next/standalone/apps/web/.next/static/chunks/0makl-d3f0_qh.js +0 -2878
- package/.next/standalone/apps/web/.next/static/chunks/0o6ggm7jrim25.js +0 -1
- package/.next/standalone/apps/web/public/vad/ort-wasm-simd-threaded.mjs +0 -59
- package/.next/standalone/apps/web/public/vad/ort-wasm-simd-threaded.wasm +0 -0
- package/.next/standalone/apps/web/public/vad/silero_vad_v5.onnx +0 -0
- package/.next/standalone/apps/web/public/vad/vad.worklet.bundle.min.js +0 -1
- package/dist/server/generated/prisma/models/ClaudeProfile.d.ts +0 -939
- package/dist/server/lib/asr-models.d.ts +0 -7
- package/dist/server/lib/asr-models.js +0 -51
- package/dist/server/lib/claude-profiles.d.ts +0 -49
- package/dist/server/lib/claude-profiles.js +0 -168
- package/dist/server/lib/process-port.d.ts +0 -1
- package/dist/server/lib/process-port.js +0 -28
- package/dist/server/lib/python-venv.d.ts +0 -4
- package/dist/server/lib/python-venv.js +0 -66
- package/dist/server/lib/voice-debug-log.d.ts +0 -9
- package/dist/server/lib/voice-debug-log.js +0 -28
- package/dist/server/lib/whisper-process.d.ts +0 -40
- package/dist/server/lib/whisper-process.js +0 -333
- package/dist/server/routes/claude-profiles.d.ts +0 -2
- package/dist/server/routes/claude-profiles.js +0 -138
- package/dist/server/routes/whisper.d.ts +0 -2
- package/dist/server/routes/whisper.js +0 -103
- package/dist/whisper-server/README.md +0 -31
- package/dist/whisper-server/download_model.py +0 -19
- package/dist/whisper-server/requirements.txt +0 -8
- package/dist/whisper-server/run.sh +0 -62
- package/dist/whisper-server/server.py +0 -147
- /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → vdAs-hr87-L7y2bUSdX3i}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → vdAs-hr87-L7y2bUSdX3i}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → vdAs-hr87-L7y2bUSdX3i}/_ssgManifest.js +0 -0
- /package/dist/server/generated/prisma/models/{ClaudeProfile.js → AgentProfile.js} +0 -0
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
cd "$(dirname "${BASH_SOURCE[0]}")"
|
|
4
|
-
|
|
5
|
-
# venv・モデルキャッシュとも ~/.tsunagi/whisper 配下にまとめる。
|
|
6
|
-
# npmインストール先(apps/whisper-server自体)が書き込み不可な場所でも動くようにする狙いもある。
|
|
7
|
-
TSUNAGI_WHISPER_DIR="${HOME}/.tsunagi/whisper"
|
|
8
|
-
VENV_DIR="${TSUNAGI_WHISPER_DIR}/venv"
|
|
9
|
-
export HF_HOME="${TSUNAGI_WHISPER_DIR}/cache"
|
|
10
|
-
# xet転送は進捗の観測が難しい独自プロトコルのため無効化し、素直なHTTPダウンロードに固定する。
|
|
11
|
-
export HF_HUB_DISABLE_XET=1
|
|
12
|
-
|
|
13
|
-
# requirements.txtでpinしているmlxがrequires-python >=3.10のため、venv作成に使うpython3自体も
|
|
14
|
-
# 3.10以上でなければpip installがそもそも失敗する。`python3`という名前がPATH上のどのバージョンを
|
|
15
|
-
# 指すかは環境依存(古いmacOS付属Python等)なため、候補を新しい順に探して最初に見つかった
|
|
16
|
-
# 3.10以上を使う。
|
|
17
|
-
MIN_PYTHON_VERSION="3.10"
|
|
18
|
-
|
|
19
|
-
find_python() {
|
|
20
|
-
for cand in python3.14 python3.13 python3.12 python3.11 python3.10 python3; do
|
|
21
|
-
if command -v "$cand" >/dev/null 2>&1; then
|
|
22
|
-
ver="$("$cand" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || true)"
|
|
23
|
-
[ -z "$ver" ] && continue
|
|
24
|
-
if [ "$(printf '%s\n%s\n' "$MIN_PYTHON_VERSION" "$ver" | sort -V | head -n1)" = "$MIN_PYTHON_VERSION" ]; then
|
|
25
|
-
echo "$cand"
|
|
26
|
-
return 0
|
|
27
|
-
fi
|
|
28
|
-
fi
|
|
29
|
-
done
|
|
30
|
-
return 1
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if ! PYTHON_BIN="$(find_python)"; then
|
|
34
|
-
echo "音声入力サーバーの起動に失敗: Python ${MIN_PYTHON_VERSION}以上が見つかりません。" >&2
|
|
35
|
-
echo "'brew install python@3.12' 等でインストールしてください。" >&2
|
|
36
|
-
exit 1
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
# requirements.txtの内容、または使用するpython3のバージョンが変わった場合は既存venvごと作り直す。
|
|
40
|
-
# venvは作成時のpythonバイナリに紐づくため、pip installだけでは古いバージョンのまま更新できない。
|
|
41
|
-
REQUIREMENTS_HASH_FILE="${VENV_DIR}/.requirements.sha256"
|
|
42
|
-
CURRENT_HASH="$( { cat requirements.txt; "$PYTHON_BIN" --version; } | shasum -a 256 | awk '{print $1}')"
|
|
43
|
-
|
|
44
|
-
if [ ! -d "$VENV_DIR" ] || [ "$(cat "$REQUIREMENTS_HASH_FILE" 2>/dev/null)" != "$CURRENT_HASH" ]; then
|
|
45
|
-
mkdir -p "$TSUNAGI_WHISPER_DIR"
|
|
46
|
-
rm -rf "$VENV_DIR"
|
|
47
|
-
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
|
48
|
-
"$VENV_DIR/bin/python3" -m pip install -r requirements.txt
|
|
49
|
-
echo "$CURRENT_HASH" > "$REQUIREMENTS_HASH_FILE"
|
|
50
|
-
fi
|
|
51
|
-
|
|
52
|
-
# 読み込むモデル。未指定ならWhisper。Qwen3-ASRを使う場合は例えば
|
|
53
|
-
# TSUNAGI_ASR_ENGINE=qwen3-asr TSUNAGI_ASR_MODEL=mlx-community/Qwen3-ASR-1.7B-8bit ./run.sh
|
|
54
|
-
export TSUNAGI_ASR_ENGINE="${TSUNAGI_ASR_ENGINE:-whisper}"
|
|
55
|
-
export TSUNAGI_ASR_MODEL="${TSUNAGI_ASR_MODEL:-mlx-community/whisper-large-v3-turbo}"
|
|
56
|
-
|
|
57
|
-
# サーバー起動前にモデルを予めダウンロードしておく(初回文字起こしで待たされないように)。
|
|
58
|
-
"$VENV_DIR/bin/python3" download_model.py "$TSUNAGI_ASR_MODEL"
|
|
59
|
-
|
|
60
|
-
# venvを ~/.tsunagi 配下へ移設する場合があるため、絶対パスが焼き込まれる
|
|
61
|
-
# コンソールスクリプト(bin/uvicorn等)ではなく `python3 -m` 経由で呼び出す。
|
|
62
|
-
exec "$VENV_DIR/bin/python3" -m uvicorn server:app --host 127.0.0.1 --port 8765
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
"""ローカル音声認識サーバー (Apple Silicon GPU)。
|
|
2
|
-
|
|
3
|
-
読み込むモデルは起動時の環境変数で切り替える(tsunagiのSettingsで選択したものが渡される)。
|
|
4
|
-
- TSUNAGI_ASR_ENGINE: "whisper"(mlx-whisper) / "qwen3-asr"(mlx-qwen3-asr)
|
|
5
|
-
- TSUNAGI_ASR_MODEL: Hugging FaceのリポジトリID
|
|
6
|
-
|
|
7
|
-
ユーザーが手動でセットアップ・起動する常駐プロセス。モデルをメモリに保持し続け
|
|
8
|
-
リクエスト毎のロード待ちを避ける。tsunagi本体(Fastify)からHTTPでプロキシされる。
|
|
9
|
-
|
|
10
|
-
音声はブラウザ側のVAD(Silero VAD)で発話区間だけを切り出した16kHzモノラルWAVを受け取る。
|
|
11
|
-
デコードにはPyAV(`av`)を使う(WAV以外の形式もそのままデコードできる)。ffmpegの内部コーデックをホイールに同梱しており、
|
|
12
|
-
システムにffmpegバイナリをインストールする必要はない。
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import io
|
|
16
|
-
import os
|
|
17
|
-
import re
|
|
18
|
-
import threading
|
|
19
|
-
from typing import Optional
|
|
20
|
-
|
|
21
|
-
import av
|
|
22
|
-
import numpy as np
|
|
23
|
-
from fastapi import FastAPI, File, Form, UploadFile
|
|
24
|
-
|
|
25
|
-
ENGINE = os.environ.get("TSUNAGI_ASR_ENGINE", "whisper")
|
|
26
|
-
MODEL = os.environ.get("TSUNAGI_ASR_MODEL", "mlx-community/whisper-large-v3-turbo")
|
|
27
|
-
if ENGINE not in ("whisper", "qwen3-asr"):
|
|
28
|
-
raise SystemExit(f"Unknown TSUNAGI_ASR_ENGINE: {ENGINE}")
|
|
29
|
-
|
|
30
|
-
if ENGINE == "whisper":
|
|
31
|
-
import mlx_whisper
|
|
32
|
-
else:
|
|
33
|
-
from mlx_qwen3_asr import Session
|
|
34
|
-
|
|
35
|
-
# Qwen3-ASRはモデルを保持するSessionを起動時に作り、リクエスト毎のロード待ちを避ける。
|
|
36
|
-
# 読み込み完了まで/healthが応答しないため、tsunagi側は読み込み完了を起動完了とみなせる。
|
|
37
|
-
_qwen_session = Session(model=MODEL)
|
|
38
|
-
# 無音・雑音区間で「ご視聴ありがとうございました」等の無関係な文章を自信満々に
|
|
39
|
-
# 生成してしまう(Whisper系モデルで知られたハルシネーション挙動)ことがあるため、
|
|
40
|
-
# no_speech_prob(無音である確率)がこの値を超えるセグメントは出力から除外する。
|
|
41
|
-
NO_SPEECH_THRESHOLD = 0.6
|
|
42
|
-
# ただしno_speech_probは30秒の窓ごとに1回しか計算されず、窓内の全セグメントに同じ値が
|
|
43
|
-
# コピーされる。そのため「発話+末尾の無音」を含む窓では、無音部分から生成された定型文も
|
|
44
|
-
# 発話ありと判定されて素通りする。これを補うため、発話より後ろ(または前)の無音区間に
|
|
45
|
-
# 現れた異常セグメントを読み飛ばすmlx_whisper組込みの判定(要word_timestamps)を有効にする。
|
|
46
|
-
HALLUCINATION_SILENCE_THRESHOLD = 2.0
|
|
47
|
-
# それでも残った場合の最終防衛線。YouTube字幕由来の典型的なハルシネーションで、
|
|
48
|
-
# セグメント全体が(句読点・空白を除いて)これらと完全一致する場合のみ除外する。
|
|
49
|
-
# 部分一致にしないのは、ユーザーが実際にこれらの語を含む文を話すケースを壊さないため。
|
|
50
|
-
HALLUCINATION_PHRASES = {
|
|
51
|
-
"ご視聴ありがとうございました",
|
|
52
|
-
"ご視聴ありがとうございます",
|
|
53
|
-
"最後までご視聴いただきありがとうございました",
|
|
54
|
-
"最後までご視聴ありがとうございました",
|
|
55
|
-
"チャンネル登録お願いします",
|
|
56
|
-
"チャンネル登録をお願いします",
|
|
57
|
-
"チャンネル登録よろしくお願いします",
|
|
58
|
-
"次回もお楽しみに",
|
|
59
|
-
"次回予告",
|
|
60
|
-
"おやすみなさい",
|
|
61
|
-
}
|
|
62
|
-
_NORMALIZE_RE = re.compile(r"[\s、。,.,.!!??・…「」『』()()\[\]〜~♪]+")
|
|
63
|
-
|
|
64
|
-
# MLXの推論は同時実行を想定していないため直列化する。エンドポイント自体は
|
|
65
|
-
# 同期関数にしてスレッドプールで実行させ、推論中も/healthが応答できるようにする
|
|
66
|
-
# (途中経過の表示で文字起こしリクエストが連続しても、起動状態のポーリングが
|
|
67
|
-
# タイムアウトしてUIが「未起動」扱いになるのを防ぐ)。
|
|
68
|
-
_transcribe_lock = threading.Lock()
|
|
69
|
-
|
|
70
|
-
app = FastAPI()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@app.get("/health")
|
|
74
|
-
async def health() -> dict:
|
|
75
|
-
return {"status": "ok", "engine": ENGINE, "model": MODEL}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def decode_to_16k_mono(data: bytes) -> np.ndarray:
|
|
79
|
-
container = av.open(io.BytesIO(data))
|
|
80
|
-
stream = container.streams.audio[0]
|
|
81
|
-
resampler = av.AudioResampler(format="s16", layout="mono", rate=16000)
|
|
82
|
-
|
|
83
|
-
chunks = []
|
|
84
|
-
for frame in container.decode(stream):
|
|
85
|
-
for resampled in resampler.resample(frame):
|
|
86
|
-
chunks.append(resampled.to_ndarray().reshape(-1))
|
|
87
|
-
container.close()
|
|
88
|
-
|
|
89
|
-
if not chunks:
|
|
90
|
-
return np.zeros(0, dtype=np.float32)
|
|
91
|
-
samples = np.concatenate(chunks).astype(np.float32) / 32768.0
|
|
92
|
-
return samples
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def is_hallucination_phrase(text: str) -> bool:
|
|
96
|
-
return _NORMALIZE_RE.sub("", text) in HALLUCINATION_PHRASES
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def transcribe_whisper(audio: np.ndarray, prompt: Optional[str]) -> str:
|
|
100
|
-
# initial_promptは文字起こしのスタイル(表記ゆれ・句読点・固有名詞など)を
|
|
101
|
-
# 誘導するヒントで、tsunagiのSettingsからユーザーが自由に設定できる。
|
|
102
|
-
result = mlx_whisper.transcribe(
|
|
103
|
-
audio,
|
|
104
|
-
path_or_hf_repo=MODEL,
|
|
105
|
-
language="ja",
|
|
106
|
-
initial_prompt=prompt or None,
|
|
107
|
-
# 前の窓の出力を次の窓のプロンプトに引き継ぐと、一度出たハルシネーションが
|
|
108
|
-
# 後続の窓へ連鎖しやすくなるため無効化する。
|
|
109
|
-
condition_on_previous_text=False,
|
|
110
|
-
word_timestamps=True,
|
|
111
|
-
hallucination_silence_threshold=HALLUCINATION_SILENCE_THRESHOLD,
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
# result["text"]はno_speech_prob等のフィルタを経ずに全セグメントを結合した
|
|
115
|
-
# ものなので使わず、セグメント単位でno_speech_probを見て自前で組み立て直す。
|
|
116
|
-
segments = result.get("segments")
|
|
117
|
-
if segments:
|
|
118
|
-
return "".join(
|
|
119
|
-
seg["text"]
|
|
120
|
-
for seg in segments
|
|
121
|
-
if seg.get("no_speech_prob", 0.0) <= NO_SPEECH_THRESHOLD
|
|
122
|
-
and not is_hallucination_phrase(seg["text"])
|
|
123
|
-
)
|
|
124
|
-
text = result.get("text", "")
|
|
125
|
-
return "" if is_hallucination_phrase(text) else text
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def transcribe_qwen3_asr(audio: np.ndarray, prompt: Optional[str]) -> str:
|
|
129
|
-
# contextはシステムプロンプトに差し込まれる語彙ヒント(スペース区切りの単語列を想定)。
|
|
130
|
-
# SettingsのプロンプトをWhisperのinitial_promptと共通で使う。
|
|
131
|
-
result = _qwen_session.transcribe(audio, context=prompt or "", language="Japanese")
|
|
132
|
-
# Whisperのようなno_speech_probは無いため、定型文の完全一致除外のみ行う。
|
|
133
|
-
return "" if is_hallucination_phrase(result.text) else result.text
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
@app.post("/transcribe")
|
|
137
|
-
def transcribe(file: UploadFile = File(...), prompt: Optional[str] = Form(None)) -> dict:
|
|
138
|
-
data = file.file.read()
|
|
139
|
-
audio = decode_to_16k_mono(data)
|
|
140
|
-
if audio.size == 0:
|
|
141
|
-
return {"text": "", "model": MODEL}
|
|
142
|
-
with _transcribe_lock:
|
|
143
|
-
if ENGINE == "whisper":
|
|
144
|
-
text = transcribe_whisper(audio, prompt)
|
|
145
|
-
else:
|
|
146
|
-
text = transcribe_qwen3_asr(audio, prompt)
|
|
147
|
-
return {"text": text.strip(), "model": MODEL}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|