@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.
Files changed (182) hide show
  1. package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  2. package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
  3. package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +2 -2
  14. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  15. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  17. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  21. package/.next/standalone/apps/web/.next/server/app/index.rsc +3 -3
  22. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  23. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +3 -3
  24. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  25. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  26. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  27. package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
  28. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  30. package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  31. package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
  32. package/.next/standalone/apps/web/.next/server/app/settings.rsc +3 -3
  33. package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +3 -3
  34. package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  35. package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
  36. package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  37. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
  38. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  39. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  40. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0qjafjt._.js +1 -1
  42. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_13w5ytx._.js → _01~tp-i._.js} +2 -2
  43. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0kflgfj._.js → _03lm.~7._.js} +3 -3
  44. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0g79jqo._.js → _09br4yx._.js} +3 -3
  45. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +2 -2
  46. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0h-u~an._.js +7 -0
  47. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_139_l.u._.js → _0kkt7a_._.js} +2 -2
  48. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0ts4rpp._.js +3 -0
  49. package/.next/standalone/apps/web/.next/server/chunks/ssr/_13ne7ej._.js +3 -0
  50. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +1 -3
  51. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +3 -3
  52. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0cv326~._.js → node_modules_06jdmu~._.js} +1 -1
  53. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0h.vgyn._.js → node_modules_0ac2-~z._.js} +1 -1
  54. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0sn0lqb._.js → node_modules_0gglkx9._.js} +1 -1
  55. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0tod6y8._.js → node_modules_0h4h_aw._.js} +1 -1
  56. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0q_5qo~._.js → node_modules_0i5v48e._.js} +1 -1
  57. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09d2oqj._.js → node_modules_0k3x6_w._.js} +1 -1
  58. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0oip5a1._.js → node_modules_0q8yuvq._.js} +1 -1
  59. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0npujym._.js → node_modules_0rnsluy._.js} +1 -1
  60. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0krwgny._.js → node_modules_0t-jssg._.js} +1 -1
  61. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0l23gkf._.js → node_modules_0t6ysh_._.js} +1 -1
  62. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_11-z65a._.js → node_modules_0ut2-yw._.js} +1 -1
  63. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_huoqd._.js → node_modules_0vq~hzy._.js} +1 -1
  64. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0a57phw._.js → node_modules_0w6fm3f._.js} +2 -2
  65. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0xp9nhx._.js → node_modules_0yb2sh9._.js} +1 -1
  66. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0q_xfyl._.js → node_modules_0yc~s8p._.js} +1 -1
  67. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0ak_3rk._.js → node_modules_0zi0w9_._.js} +1 -1
  68. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_04pfam-._.js → node_modules_0zzdoud._.js} +1 -1
  69. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0w2dm3r._.js → node_modules_lucide-react_dist_esm_icons_0zerlct._.js} +2 -2
  70. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  71. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  72. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  73. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  74. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  75. package/.next/standalone/apps/web/.next/static/chunks/{0l83q95s~tih4.js → 08s3o7outb~0z.js} +1 -1
  76. package/.next/standalone/apps/web/.next/static/chunks/0a2u28f44p7t..js +1 -0
  77. package/.next/standalone/apps/web/.next/static/chunks/{0oricprxoxb_d.js → 0ae7zmpe8063z.js} +10 -10
  78. package/.next/standalone/apps/web/.next/static/chunks/0dgu~u-1e5b7-.js +5 -0
  79. package/.next/standalone/apps/web/.next/static/chunks/{0whl1y16cvn5q.js → 0e8u7rqrqfiiy.js} +1 -1
  80. package/.next/standalone/apps/web/.next/static/chunks/{0z60ewb40i2ce.js → 0gxhy80~c0gvp.js} +1 -1
  81. package/.next/standalone/apps/web/.next/static/chunks/0n72w4sxrwoj4.js +5 -0
  82. package/.next/standalone/apps/web/.next/static/chunks/0o1bh_qwdvmid.css +4 -0
  83. package/.next/standalone/apps/web/.next/static/chunks/{17~93fal1fw_d.js → 0uugzgqr-ug3_.js} +1 -1
  84. package/.next/standalone/apps/web/.next/static/chunks/{149sia28cskw9.js → 0yp~t_.pozr-d.js} +6 -6
  85. package/.next/standalone/apps/web/.next/static/chunks/0ysgl4vmer~v5.js +6 -0
  86. package/.next/standalone/apps/web/.next/static/chunks/{053h220fng_68.js → 11u7x4ic4~mp3.js} +10 -10
  87. package/.next/standalone/apps/web/.next/static/chunks/13_9zaka0bif_.js +1 -0
  88. package/.next/standalone/apps/web/package.json +1 -3
  89. package/README.md +1 -1
  90. package/dist/app.js +3 -4
  91. package/dist/plugin-lifecycle.d.ts +5 -3
  92. package/dist/plugin-lifecycle.js +40 -16
  93. package/dist/prisma/migrations/20261011000000_add_codex_support/migration.sql +20 -0
  94. package/dist/prisma/schema.prisma +9 -5
  95. package/dist/server/generated/prisma/browser.d.ts +2 -2
  96. package/dist/server/generated/prisma/client.d.ts +2 -2
  97. package/dist/server/generated/prisma/internal/class.d.ts +4 -4
  98. package/dist/server/generated/prisma/internal/class.js +4 -4
  99. package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +41 -39
  100. package/dist/server/generated/prisma/internal/prismaNamespace.js +4 -2
  101. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +5 -3
  102. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +4 -2
  103. package/dist/server/generated/prisma/models/AgentProfile.d.ts +972 -0
  104. package/dist/server/generated/prisma/models/Tab.d.ts +36 -1
  105. package/dist/server/generated/prisma/models.d.ts +1 -1
  106. package/dist/server/index.js +2 -7
  107. package/dist/server/lib/agent-profiles.d.ts +44 -0
  108. package/dist/server/lib/agent-profiles.js +130 -0
  109. package/dist/server/lib/agents/claude.d.ts +2 -0
  110. package/dist/server/lib/agents/claude.js +130 -0
  111. package/dist/server/lib/agents/cli.d.ts +14 -0
  112. package/dist/server/lib/agents/cli.js +57 -0
  113. package/dist/server/lib/agents/codex.d.ts +2 -0
  114. package/dist/server/lib/agents/codex.js +285 -0
  115. package/dist/server/lib/agents/index.d.ts +11 -0
  116. package/dist/server/lib/agents/index.js +19 -0
  117. package/dist/server/lib/agents/login-callback.d.ts +11 -0
  118. package/dist/server/lib/agents/login-callback.js +47 -0
  119. package/dist/server/lib/agents/types.d.ts +77 -0
  120. package/dist/server/lib/agents/types.js +8 -0
  121. package/dist/server/lib/pty-env-sync.js +1 -1
  122. package/dist/server/lib/pty-launch.d.ts +8 -10
  123. package/dist/server/lib/pty-launch.js +20 -58
  124. package/dist/server/lib/repositories/task.js +1 -0
  125. package/dist/server/lib/tab-todos.d.ts +5 -0
  126. package/dist/server/lib/tab-todos.js +13 -0
  127. package/dist/server/pty-manager.d.ts +7 -2
  128. package/dist/server/pty-manager.js +13 -0
  129. package/dist/server/routes/agent-profiles.d.ts +2 -0
  130. package/dist/server/routes/agent-profiles.js +193 -0
  131. package/dist/server/routes/env.js +2 -2
  132. package/dist/server/routes/hooks.d.ts +4 -19
  133. package/dist/server/routes/hooks.js +83 -73
  134. package/dist/server/routes/mcp.js +2 -2
  135. package/dist/server/routes/onboarding.js +19 -10
  136. package/dist/server/routes/tasks.js +4 -4
  137. package/dist/server/routes/terminal.js +47 -34
  138. package/dist/with-plugin.js +3 -3
  139. package/npm-shrinkwrap.json +37 -24
  140. package/package.json +1 -1
  141. package/tsunagi-marketplace/codex/tsunagi-task.md +120 -0
  142. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__11y.qj~._.js +0 -3
  143. package/.next/standalone/apps/web/.next/server/chunks/ssr/_03vftq7._.js +0 -7
  144. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hnxatt._.js +0 -3
  145. package/.next/standalone/apps/web/.next/server/chunks/ssr/_10gk93.._.js +0 -3
  146. package/.next/standalone/apps/web/.next/static/chunks/0.gcmzq82~36i.js +0 -1
  147. package/.next/standalone/apps/web/.next/static/chunks/06gfh91..uydi.js +0 -5
  148. package/.next/standalone/apps/web/.next/static/chunks/0b90ys7wq-p.4.css +0 -4
  149. package/.next/standalone/apps/web/.next/static/chunks/0crn03y2tj0lu.js +0 -6
  150. package/.next/standalone/apps/web/.next/static/chunks/0lvl3ec93cab_.js +0 -7
  151. package/.next/standalone/apps/web/.next/static/chunks/0makl-d3f0_qh.js +0 -2878
  152. package/.next/standalone/apps/web/.next/static/chunks/0o6ggm7jrim25.js +0 -1
  153. package/.next/standalone/apps/web/public/vad/ort-wasm-simd-threaded.mjs +0 -59
  154. package/.next/standalone/apps/web/public/vad/ort-wasm-simd-threaded.wasm +0 -0
  155. package/.next/standalone/apps/web/public/vad/silero_vad_v5.onnx +0 -0
  156. package/.next/standalone/apps/web/public/vad/vad.worklet.bundle.min.js +0 -1
  157. package/dist/server/generated/prisma/models/ClaudeProfile.d.ts +0 -939
  158. package/dist/server/lib/asr-models.d.ts +0 -7
  159. package/dist/server/lib/asr-models.js +0 -51
  160. package/dist/server/lib/claude-profiles.d.ts +0 -49
  161. package/dist/server/lib/claude-profiles.js +0 -168
  162. package/dist/server/lib/process-port.d.ts +0 -1
  163. package/dist/server/lib/process-port.js +0 -28
  164. package/dist/server/lib/python-venv.d.ts +0 -4
  165. package/dist/server/lib/python-venv.js +0 -66
  166. package/dist/server/lib/voice-debug-log.d.ts +0 -9
  167. package/dist/server/lib/voice-debug-log.js +0 -28
  168. package/dist/server/lib/whisper-process.d.ts +0 -40
  169. package/dist/server/lib/whisper-process.js +0 -333
  170. package/dist/server/routes/claude-profiles.d.ts +0 -2
  171. package/dist/server/routes/claude-profiles.js +0 -138
  172. package/dist/server/routes/whisper.d.ts +0 -2
  173. package/dist/server/routes/whisper.js +0 -103
  174. package/dist/whisper-server/README.md +0 -31
  175. package/dist/whisper-server/download_model.py +0 -19
  176. package/dist/whisper-server/requirements.txt +0 -8
  177. package/dist/whisper-server/run.sh +0 -62
  178. package/dist/whisper-server/server.py +0 -147
  179. /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → vdAs-hr87-L7y2bUSdX3i}/_buildManifest.js +0 -0
  180. /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → vdAs-hr87-L7y2bUSdX3i}/_clientMiddlewareManifest.js +0 -0
  181. /package/.next/standalone/apps/web/.next/static/{7x81GlNKt9yqIkeI7U1BX → vdAs-hr87-L7y2bUSdX3i}/_ssgManifest.js +0 -0
  182. /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}