@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,333 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import * as fs from 'node:fs';
3
- import * as os from 'node:os';
4
- import * as path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { ASR_MODELS, findAsrModel, findAsrModelByRepo, getVoiceInputSettings, saveVoiceInputSettings, } from './asr-models.js';
7
- import { killProcessOnPort } from './process-port.js';
8
- import { findPython, isVenvUpToDate, PYTHON_NOT_FOUND_MESSAGE, writeVenvMarker, } from './python-venv.js';
9
- const WHISPER_SERVER_URL = process.env.TSUNAGI_WHISPER_SERVER_URL || 'http://127.0.0.1:8765';
10
- // run.shが待受けるポート固定値(host.docker.internal経由URLと違い、停止処理は
11
- // 必ずこのNodeプロセスと同じホスト上のポートを対象にする必要があるため分けて持つ)。
12
- const WHISPER_PORT = 8765;
13
- // venv・モデルキャッシュとも ~/.tsunagi/whisper 配下にまとめる(run.shと同じ場所)。
14
- const TSUNAGI_WHISPER_DIR = path.join(os.homedir(), '.tsunagi', 'whisper');
15
- const VENV_DIR = path.join(TSUNAGI_WHISPER_DIR, 'venv');
16
- const HF_CACHE_DIR = path.join(TSUNAGI_WHISPER_DIR, 'cache');
17
- // starting_serverフェーズ(spawn後にhealthyになるまで待つ時間)の上限。
18
- // 遅いマシンでのPython起動/import/Metal初期化に加え、Qwen3-ASRは起動時にモデル(最大約4GB)を
19
- // メモリへ読み込むため、そのばらつきを吸収できるよう120秒に設定。
20
- // プロセスが起動途中でクラッシュした場合はこのタイムアウトを待たず即座に検知する。
21
- const STARTUP_TIMEOUT_MS = 120_000;
22
- // モデル切り替え時、停止したサーバーのポートが空くまで待つ時間の上限。
23
- const SHUTDOWN_TIMEOUT_MS = 15_000;
24
- // クラッシュ時にエラーメッセージへ含めるstderrの上限文字数。
25
- const STDERR_TAIL_MAX_CHARS = 4000;
26
- // このファイルの位置から whisper-server を探す:
27
- // npm配布物: apps/cli/dist/server/lib → 2階層上 → apps/cli/dist/whisper-server
28
- // dev/build: apps/server/{src,dist}/lib → 3階層上 → apps/whisper-server
29
- export function findWhisperServerDir() {
30
- const here = path.dirname(fileURLToPath(import.meta.url));
31
- const candidates = [
32
- path.join(here, '..', '..', 'whisper-server'),
33
- path.join(here, '..', '..', '..', 'whisper-server'),
34
- ];
35
- return candidates.find((c) => fs.existsSync(path.join(c, 'run.sh'))) ?? null;
36
- }
37
- function venvPython() {
38
- return path.join(VENV_DIR, 'bin', 'python3');
39
- }
40
- // huggingface_hubのキャッシュ構成(hub/models--{owner}--{name})に合わせたモデルの保存先。
41
- function modelCacheDir(repo) {
42
- return path.join(HF_CACHE_DIR, 'hub', `models--${repo.replace('/', '--')}`);
43
- }
44
- function isModelReady(repo) {
45
- const snapshotsDir = path.join(modelCacheDir(repo), 'snapshots');
46
- if (!fs.existsSync(snapshotsDir))
47
- return false;
48
- return fs.readdirSync(snapshotsDir).length > 0;
49
- }
50
- let currentStep = 'not_running';
51
- let downloadProgress;
52
- let lastError;
53
- let managedProcess = null;
54
- let setupPromise = null;
55
- // セットアップ中・tsunagi管理で起動中のモデル
56
- let targetModel = null;
57
- function sleep(ms) {
58
- return new Promise((resolve) => setTimeout(resolve, ms));
59
- }
60
- // 起動していれば読み込み中のモデルのリポジトリIDを返す(未起動ならnull)。
61
- // tsunagi外で起動されたサーバーでも、どのモデルで動いているかを知るために/healthから取る。
62
- async function checkHealth() {
63
- try {
64
- const response = await fetch(`${WHISPER_SERVER_URL}/health`, {
65
- signal: AbortSignal.timeout(2000),
66
- });
67
- if (!response.ok)
68
- return null;
69
- const body = (await response.json().catch(() => ({})));
70
- return { repo: typeof body.model === 'string' ? body.model : null };
71
- }
72
- catch {
73
- return null;
74
- }
75
- }
76
- export async function getWhisperServerStatus() {
77
- const serverDir = findWhisperServerDir();
78
- // セットアップ/起動フロー進行中はそのステップをそのまま報告する。
79
- if (setupPromise) {
80
- return {
81
- step: currentStep,
82
- serverDir,
83
- modelId: targetModel?.id ?? null,
84
- downloadProgress,
85
- error: lastError,
86
- };
87
- }
88
- const health = await checkHealth();
89
- if (health) {
90
- return {
91
- step: managedProcess ? 'running' : 'running_external',
92
- serverDir,
93
- modelId: (health.repo && findAsrModelByRepo(health.repo)?.id) ?? null,
94
- };
95
- }
96
- return {
97
- step: lastError ? 'error' : 'not_running',
98
- serverDir,
99
- modelId: null,
100
- error: lastError,
101
- };
102
- }
103
- function runStep(cmd, args, cwd, env) {
104
- return new Promise((resolve, reject) => {
105
- const child = spawn(cmd, args, { cwd, stdio: 'ignore', env: env ?? process.env });
106
- child.on('exit', (code) => {
107
- if (code === 0)
108
- resolve();
109
- else
110
- reject(new Error(`${path.basename(cmd)} ${args.join(' ')} exited with code ${code}`));
111
- });
112
- child.on('error', reject);
113
- });
114
- }
115
- // モデルDL中(huggingface_hubがblobs/配下に *.incomplete を書き続ける)のファイルサイズを
116
- // 定期的にポーリングし、進捗(%)と直近の転送速度からのETAを算出する。
117
- async function trackModelDownload(child, model) {
118
- const blobsDir = path.join(modelCacheDir(model.repo), 'blobs');
119
- let lastBytes = 0;
120
- let lastTime = Date.now();
121
- let stopped = false;
122
- child.on('exit', () => {
123
- stopped = true;
124
- });
125
- while (!stopped) {
126
- await sleep(1000);
127
- let downloaded = 0;
128
- try {
129
- for (const f of fs.readdirSync(blobsDir)) {
130
- if (!f.endsWith('.incomplete'))
131
- continue;
132
- downloaded += fs.statSync(path.join(blobsDir, f)).size;
133
- }
134
- }
135
- catch {
136
- // blobsDir がまだ作られていない場合は0のまま次のポーリングへ
137
- }
138
- const now = Date.now();
139
- const elapsedSec = (now - lastTime) / 1000;
140
- const bytesPerSec = elapsedSec > 0 ? (downloaded - lastBytes) / elapsedSec : 0;
141
- const remaining = model.expectedBytes - downloaded;
142
- // 転送が一時的に止まって見える(ポーリング間隔とディスク書き込みのタイミングがずれる)だけで
143
- // 速度0になることがあるため、その場合は直前のETAを維持し「計算中...」への逆戻りを防ぐ。
144
- const etaSeconds = bytesPerSec > 0
145
- ? Math.max(0, Math.round(remaining / bytesPerSec))
146
- : (downloadProgress?.etaSeconds ?? null);
147
- downloadProgress = {
148
- downloadedBytes: downloaded,
149
- totalBytes: model.expectedBytes,
150
- etaSeconds,
151
- };
152
- lastBytes = downloaded;
153
- lastTime = now;
154
- }
155
- }
156
- async function runSetupAndStart(dir, model) {
157
- const hfEnv = {
158
- ...process.env,
159
- HF_HOME: HF_CACHE_DIR,
160
- HF_HUB_DISABLE_XET: '1',
161
- TSUNAGI_ASR_ENGINE: model.engine,
162
- TSUNAGI_ASR_MODEL: model.repo,
163
- };
164
- const requirementsFile = path.join(dir, 'requirements.txt');
165
- if (!isVenvUpToDate(VENV_DIR, requirementsFile)) {
166
- currentStep = 'installing_deps';
167
- const pythonBin = findPython();
168
- if (!pythonBin)
169
- throw new Error(PYTHON_NOT_FOUND_MESSAGE);
170
- fs.mkdirSync(TSUNAGI_WHISPER_DIR, { recursive: true });
171
- fs.rmSync(VENV_DIR, { recursive: true, force: true });
172
- await runStep(pythonBin, ['-m', 'venv', VENV_DIR], dir);
173
- await runStep(venvPython(), ['-m', 'pip', 'install', '-r', 'requirements.txt'], dir);
174
- writeVenvMarker(VENV_DIR, requirementsFile);
175
- }
176
- if (!isModelReady(model.repo)) {
177
- currentStep = 'downloading_model';
178
- downloadProgress = { downloadedBytes: 0, totalBytes: model.expectedBytes, etaSeconds: null };
179
- await new Promise((resolve, reject) => {
180
- const child = spawn(venvPython(), ['download_model.py', model.repo], {
181
- cwd: dir,
182
- stdio: 'ignore',
183
- env: hfEnv,
184
- });
185
- void trackModelDownload(child, model);
186
- child.on('exit', (code) => {
187
- if (code === 0)
188
- resolve();
189
- else
190
- reject(new Error(`download_model.py exited with code ${code}`));
191
- });
192
- child.on('error', reject);
193
- });
194
- downloadProgress = undefined;
195
- }
196
- currentStep = 'starting_server';
197
- const child = spawn(venvPython(), ['-m', 'uvicorn', 'server:app', '--host', '127.0.0.1', '--port', '8765'], { cwd: dir, stdio: ['ignore', 'ignore', 'pipe'], env: hfEnv });
198
- let stderrTail = '';
199
- child.stderr?.on('data', (chunk) => {
200
- stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_MAX_CHARS);
201
- });
202
- // healthポーリングとは別に、プロセスが起動途中で終了したことをタイムアウトを待たず検知する。
203
- let exitInfo = null;
204
- child.on('exit', (code, signal) => {
205
- exitInfo = `exit code=${code} signal=${signal}`;
206
- if (managedProcess === child)
207
- managedProcess = null;
208
- });
209
- child.on('error', (err) => {
210
- exitInfo = `spawn error: ${err.message}`;
211
- if (managedProcess === child)
212
- managedProcess = null;
213
- });
214
- managedProcess = child;
215
- const start = Date.now();
216
- while (Date.now() - start < STARTUP_TIMEOUT_MS) {
217
- if (exitInfo) {
218
- throw new Error(`whisper-server crashed while starting (${exitInfo})${stderrTail ? `\n${stderrTail}` : ''}`);
219
- }
220
- if (await checkHealth())
221
- return;
222
- await sleep(1000);
223
- }
224
- throw new Error(`Server did not become healthy within ${STARTUP_TIMEOUT_MS / 1000}s of starting`);
225
- }
226
- // 設定で選択中のモデルでセットアップ・起動する。
227
- export async function startWhisperServer() {
228
- if (setupPromise || managedProcess) {
229
- return { started: false, error: 'Already starting/running' };
230
- }
231
- const dir = findWhisperServerDir();
232
- if (!dir) {
233
- return { started: false, error: 'whisper-server directory not found' };
234
- }
235
- const { modelId } = await getVoiceInputSettings();
236
- // getVoiceInputSettingsが一覧にあるIDだけを返すため必ず見つかる
237
- const model = findAsrModel(modelId);
238
- // 設定の読み込みを待つ間に別のリクエストで起動が始まった場合に備えて再確認する
239
- if (setupPromise || managedProcess) {
240
- return { started: false, error: 'Already starting/running' };
241
- }
242
- lastError = undefined;
243
- targetModel = model;
244
- setupPromise = runSetupAndStart(dir, model)
245
- .catch((error) => {
246
- currentStep = 'error';
247
- lastError = error instanceof Error ? error.message : String(error);
248
- })
249
- .finally(() => {
250
- setupPromise = null;
251
- });
252
- return { started: true };
253
- }
254
- export async function stopWhisperServer() {
255
- if (managedProcess) {
256
- managedProcess.kill();
257
- managedProcess = null;
258
- targetModel = null;
259
- currentStep = 'not_running';
260
- return { stopped: true };
261
- }
262
- // tsunagi外(make whisper等)で起動された場合はchild_processのハンドルを
263
- // 持たないため、ポート番号を手がかりにOS側から見つけて停止する。
264
- const killed = await killProcessOnPort(WHISPER_PORT);
265
- if (!killed) {
266
- return {
267
- stopped: false,
268
- error: `ポート${WHISPER_PORT}で待ち受けているプロセスが見つかりませんでした`,
269
- };
270
- }
271
- currentStep = 'not_running';
272
- return { stopped: true };
273
- }
274
- // tsunagi本体プロセスの終了時(Ctrl+C等)に、自分が起動したwhisper-serverも
275
- // 道連れで停止する。ユーザーが手動で起動したもの(running_external)には触れない。
276
- export function stopWhisperServerOnExit() {
277
- managedProcess?.kill();
278
- managedProcess = null;
279
- }
280
- async function waitUntilStopped() {
281
- const start = Date.now();
282
- while (Date.now() - start < SHUTDOWN_TIMEOUT_MS) {
283
- if (!(await checkHealth()))
284
- return true;
285
- await sleep(500);
286
- }
287
- return false;
288
- }
289
- export async function listAsrModels() {
290
- const { modelId } = await getVoiceInputSettings();
291
- return {
292
- models: ASR_MODELS.map((m) => ({ ...m, installed: isModelReady(m.repo) })),
293
- selectedModelId: modelId,
294
- };
295
- }
296
- // 使用するモデルを切り替える。サーバーが別のモデルで起動中なら、停止して
297
- // 新しいモデルで起動し直す(未ダウンロードならダウンロードから行う)。
298
- // 停止中の場合は設定の保存のみ行い、次回起動時にそのモデルを読み込む。
299
- export async function selectAsrModel(modelId) {
300
- if (!findAsrModel(modelId))
301
- return { ok: false, error: `Unknown model: ${modelId}` };
302
- if (setupPromise) {
303
- return { ok: false, error: 'サーバーのセットアップ中はモデルを切り替えられません' };
304
- }
305
- await saveVoiceInputSettings({ modelId });
306
- const status = await getWhisperServerStatus();
307
- const running = status.step === 'running' || status.step === 'running_external';
308
- if (!running || status.modelId === modelId)
309
- return { ok: true, restarted: false };
310
- const stopped = await stopWhisperServer();
311
- if (!stopped.stopped)
312
- return { ok: false, error: stopped.error ?? 'サーバーを停止できませんでした' };
313
- if (!(await waitUntilStopped())) {
314
- return { ok: false, error: 'サーバーの停止を確認できませんでした' };
315
- }
316
- const started = await startWhisperServer();
317
- if (!started.started)
318
- return { ok: false, error: started.error ?? 'サーバーを起動できませんでした' };
319
- return { ok: true, restarted: true };
320
- }
321
- // ダウンロード済みのモデルを削除してディスク容量を空ける。
322
- // 使用中(セットアップ中・起動中)のモデルは削除できない。
323
- export async function deleteAsrModel(modelId) {
324
- const model = findAsrModel(modelId);
325
- if (!model)
326
- return { ok: false, error: `Unknown model: ${modelId}` };
327
- const status = await getWhisperServerStatus();
328
- if (status.modelId === modelId) {
329
- return { ok: false, error: '使用中のモデルは削除できません。先にサーバーを停止してください' };
330
- }
331
- fs.rmSync(modelCacheDir(model.repo), { recursive: true, force: true });
332
- return { ok: true };
333
- }
@@ -1,2 +0,0 @@
1
- import type { FastifyInstance } from 'fastify';
2
- export declare function claudeProfilesRoutes(fastify: FastifyInstance): Promise<void>;
@@ -1,138 +0,0 @@
1
- import * as os from 'os';
2
- import { ptyManager } from '../pty-manager.js';
3
- import { prisma } from '../lib/db.js';
4
- import { deleteEnv, getEnv, setEnv } from '../lib/repositories/environment.js';
5
- import { CLAUDE_PROFILE_ENV_KEY, DEFAULT_CLAUDE_PROFILE, buildClaudeLoginCommand, claudeLoginPtyEnv, claudeProfileExists, createClaudeProfile, deleteClaudeProfile, getClaudeAuthStatus, listClaudeProfiles, } from '../lib/claude-profiles.js';
6
- import { notifyEnvChanged } from '../lib/pty-env-sync.js';
7
- /** ログイン用 PTY の sessionId の接頭辞(タブの PTY と区別する) */
8
- const LOGIN_SESSION_PREFIX = 'claude-login-';
9
- function validateScope(scope, owner, repo) {
10
- if (scope !== 'global' && scope !== 'owner' && scope !== 'repo')
11
- return 'Invalid scope';
12
- if (scope === 'owner' && !owner)
13
- return 'owner is required for scope=owner';
14
- if (scope === 'repo' && (!owner || !repo))
15
- return 'owner and repo are required for scope=repo';
16
- return null;
17
- }
18
- async function getAssignment(scope, owner, repo) {
19
- const row = await prisma.environmentVariable.findFirst({
20
- where: {
21
- key: CLAUDE_PROFILE_ENV_KEY,
22
- scope,
23
- ...(scope !== 'global' && { owner }),
24
- ...(scope === 'repo' && { repo }),
25
- },
26
- });
27
- const merged = await getEnv(scope, owner, repo);
28
- const assignment = {
29
- assigned: row?.value ?? null,
30
- effective: merged[CLAUDE_PROFILE_ENV_KEY] ?? DEFAULT_CLAUDE_PROFILE,
31
- };
32
- return assignment;
33
- }
34
- function syncPtyEnv(fastify) {
35
- notifyEnvChanged().catch((err) => fastify.log.warn(err, 'Failed to sync PTY env'));
36
- }
37
- export async function claudeProfilesRoutes(fastify) {
38
- // GET /claude-profiles - システム既定 + 登録済みプロファイルとログイン状態
39
- fastify.get('/claude-profiles', async (_request, reply) => {
40
- const profiles = await listClaudeProfiles();
41
- const data = await Promise.all([
42
- getClaudeAuthStatus(DEFAULT_CLAUDE_PROFILE).then((status) => ({
43
- slug: DEFAULT_CLAUDE_PROFILE,
44
- name: 'System default',
45
- // CLAUDE_CONFIG_DIR を付けない(普段ターミナルで claude を使うときと同じ設定)
46
- configDir: null,
47
- status,
48
- })),
49
- ...profiles.map(async (profile) => ({
50
- ...profile,
51
- status: await getClaudeAuthStatus(profile.slug),
52
- })),
53
- ]);
54
- return reply.send({ data: { profiles: data } });
55
- });
56
- // GET /claude-profiles/:slug/status - ログイン状態(ログイン用ダイアログで完了を確認する)
57
- fastify.get('/claude-profiles/:slug/status', async (request, reply) => {
58
- const { slug } = request.params;
59
- if (!(await claudeProfileExists(slug))) {
60
- return reply.status(404).send({ error: 'Profile not found' });
61
- }
62
- return reply.send({ data: { status: await getClaudeAuthStatus(slug) } });
63
- });
64
- // POST /claude-profiles - プロファイル作成
65
- fastify.post('/claude-profiles', async (request, reply) => {
66
- try {
67
- const profile = await createClaudeProfile(request.body?.name ?? '');
68
- return reply.status(201).send({ data: { profile } });
69
- }
70
- catch (err) {
71
- return reply.status(400).send({ error: err instanceof Error ? err.message : String(err) });
72
- }
73
- });
74
- // DELETE /claude-profiles/:slug - プロファイル削除(logout してディレクトリごと消す)
75
- fastify.delete('/claude-profiles/:slug', async (request, reply) => {
76
- const { slug } = request.params;
77
- if (slug === DEFAULT_CLAUDE_PROFILE || !(await claudeProfileExists(slug))) {
78
- return reply.status(404).send({ error: 'Profile not found' });
79
- }
80
- await deleteClaudeProfile(slug);
81
- syncPtyEnv(fastify);
82
- return reply.status(204).send();
83
- });
84
- // POST /claude-profiles/:slug/login - ログイン用 PTY を起動し `claude auth login` を実行する
85
- fastify.post('/claude-profiles/:slug/login', async (request, reply) => {
86
- const { slug } = request.params;
87
- if (!(await claudeProfileExists(slug))) {
88
- return reply.status(404).send({ error: 'Profile not found' });
89
- }
90
- // 同じプロファイルのログイン用 PTY が残っていれば閉じる
91
- const prefix = `${LOGIN_SESSION_PREFIX}${slug}-`;
92
- for (const id of ptyManager.listSessions()) {
93
- if (id.startsWith(prefix))
94
- ptyManager.deleteSession(id);
95
- }
96
- const sessionId = `${prefix}${crypto.randomUUID()}`;
97
- const { env, unsetKeys } = claudeLoginPtyEnv(slug);
98
- const session = ptyManager.createSession(sessionId, os.homedir(), env, unsetKeys);
99
- // シェルの初期化(プロンプト表示)を待つため少し遅延させて書き込む
100
- setTimeout(() => session.pty.write(`${buildClaudeLoginCommand()}\n`), 300);
101
- return reply.status(201).send({ data: { sessionId } });
102
- });
103
- // DELETE /claude-profiles/login/:sessionId - ログイン用 PTY を閉じる
104
- fastify.delete('/claude-profiles/login/:sessionId', async (request, reply) => {
105
- const { sessionId } = request.params;
106
- if (!sessionId.startsWith(LOGIN_SESSION_PREFIX)) {
107
- return reply.status(400).send({ error: 'Not a login session' });
108
- }
109
- ptyManager.deleteSession(sessionId);
110
- return reply.status(204).send();
111
- });
112
- // GET /claude-profiles/assignment - スコープのプロファイル割り当て
113
- fastify.get('/claude-profiles/assignment', async (request, reply) => {
114
- const { scope, owner, repo } = request.query;
115
- const error = validateScope(scope, owner, repo);
116
- if (error)
117
- return reply.status(400).send({ error });
118
- return reply.send({ data: { assignment: await getAssignment(scope, owner, repo) } });
119
- });
120
- // PUT /claude-profiles/assignment - スコープのプロファイル割り当てを変更する
121
- fastify.put('/claude-profiles/assignment', async (request, reply) => {
122
- const { scope, owner, repo, slug } = request.body ?? {};
123
- const error = validateScope(scope, owner, repo);
124
- if (error)
125
- return reply.status(400).send({ error });
126
- if (slug !== null && !(await claudeProfileExists(slug))) {
127
- return reply.status(400).send({ error: 'Profile not found' });
128
- }
129
- if (slug === null) {
130
- await deleteEnv(CLAUDE_PROFILE_ENV_KEY, scope, owner, repo);
131
- }
132
- else {
133
- await setEnv(CLAUDE_PROFILE_ENV_KEY, slug, scope, owner, repo);
134
- }
135
- syncPtyEnv(fastify);
136
- return reply.send({ data: { assignment: await getAssignment(scope, owner, repo) } });
137
- });
138
- }
@@ -1,2 +0,0 @@
1
- import type { FastifyInstance } from 'fastify';
2
- export declare function whisperRoutes(fastify: FastifyInstance): Promise<void>;
@@ -1,103 +0,0 @@
1
- import { deleteAsrModel, getWhisperServerStatus, listAsrModels, selectAsrModel, startWhisperServer, stopWhisperServer, } from '../lib/whisper-process.js';
2
- import { appendVoiceDebugLog } from '../lib/voice-debug-log.js';
3
- // ローカル常駐の音声認識サーバー(apps/whisper-server)を指す。
4
- // 読み込むモデル(Whisper / Qwen3-ASR)は起動時に whisper-process 側で指定する。
5
- const WHISPER_SERVER_URL = process.env.TSUNAGI_WHISPER_SERVER_URL || 'http://127.0.0.1:8765';
6
- export async function whisperRoutes(fastify) {
7
- // GET /whisper/server/status
8
- fastify.get('/whisper/server/status', async (_request, reply) => {
9
- return reply.status(200).send(await getWhisperServerStatus());
10
- });
11
- // POST /whisper/server/start
12
- fastify.post('/whisper/server/start', async (_request, reply) => {
13
- const result = await startWhisperServer();
14
- if (!result.started) {
15
- return reply.status(409).send({ error: result.error });
16
- }
17
- return reply.status(202).send(await getWhisperServerStatus());
18
- });
19
- // POST /whisper/server/stop
20
- fastify.post('/whisper/server/stop', async (_request, reply) => {
21
- const result = await stopWhisperServer();
22
- if (!result.stopped) {
23
- return reply.status(409).send({ error: result.error });
24
- }
25
- return reply.status(200).send(await getWhisperServerStatus());
26
- });
27
- // GET /whisper/models
28
- fastify.get('/whisper/models', async (_request, reply) => {
29
- return reply.status(200).send({ data: await listAsrModels() });
30
- });
31
- // PUT /whisper/models/selected
32
- fastify.put('/whisper/models/selected', async (request, reply) => {
33
- const modelId = request.body?.modelId;
34
- if (typeof modelId !== 'string') {
35
- return reply.status(400).send({ error: 'modelId is required' });
36
- }
37
- const result = await selectAsrModel(modelId);
38
- if (!result.ok) {
39
- return reply.status(409).send({ error: result.error });
40
- }
41
- return reply.status(200).send({ data: await listAsrModels() });
42
- });
43
- // DELETE /whisper/models/:modelId
44
- fastify.delete('/whisper/models/:modelId', async (request, reply) => {
45
- const result = await deleteAsrModel(request.params.modelId);
46
- if (!result.ok) {
47
- return reply.status(409).send({ error: result.error });
48
- }
49
- return reply.status(200).send({ data: await listAsrModels() });
50
- });
51
- // POST /whisper/transcribe
52
- fastify.post('/whisper/transcribe', async (request, reply) => {
53
- const file = await request.file();
54
- if (!file) {
55
- return reply.status(400).send({ error: 'No audio file provided' });
56
- }
57
- // 音声ファイルと同じmultipartペイロード内の`prompt`フィールド(あれば)を
58
- // whisper-serverのinitial_promptとしてそのまま転送する。
59
- const promptField = file.fields.prompt;
60
- const prompt = typeof promptField?.value === 'string' ? promptField.value : undefined;
61
- // 発話中の途中経過表示用のリクエスト。確定前の暫定結果なので、
62
- // デバッグログはスキップする。
63
- const interimField = file.fields.interim;
64
- const interim = interimField?.value === 'true';
65
- const buffer = await file.toBuffer();
66
- const body = new FormData();
67
- body.append('file', new Blob([buffer]), file.filename);
68
- if (prompt)
69
- body.append('prompt', prompt);
70
- let whisperText;
71
- let model;
72
- const startedAt = Date.now();
73
- try {
74
- const response = await fetch(`${WHISPER_SERVER_URL}/transcribe`, {
75
- method: 'POST',
76
- body,
77
- });
78
- if (!response.ok) {
79
- throw new Error(`whisper-server responded with ${response.status}`);
80
- }
81
- const result = (await response.json());
82
- whisperText = result.text;
83
- model = result.model;
84
- }
85
- catch (error) {
86
- fastify.log.error(error, 'Whisper transcription error');
87
- return reply.status(502).send({
88
- error: error instanceof Error
89
- ? error.message
90
- : 'Transcription failed. Is apps/whisper-server running?',
91
- });
92
- }
93
- if (!interim) {
94
- appendVoiceDebugLog({
95
- model,
96
- elapsedMs: Date.now() - startedAt,
97
- whisperPrompt: prompt,
98
- whisperText,
99
- });
100
- }
101
- return reply.status(200).send({ text: whisperText });
102
- });
103
- }
@@ -1,31 +0,0 @@
1
- # whisper-server
2
-
3
- ローカル音声入力用サーバー。Apple Silicon GPU (MLX) で文字起こしする。
4
- モデルは起動時の環境変数で切り替える(tsunagiのSettingsで選択したものが渡される)。
5
-
6
- | `TSUNAGI_ASR_ENGINE` | `TSUNAGI_ASR_MODEL` の例 | 実装 |
7
- | -------------------- | ------------------------------------------------------------------ | ------------- |
8
- | `whisper` (既定) | `mlx-community/whisper-large-v3-turbo` | mlx-whisper |
9
- | `qwen3-asr` | `mlx-community/Qwen3-ASR-1.7B-8bit`, `neosophie/Qwen3-ASR-1.7B-JA` | mlx-qwen3-asr |
10
-
11
- 選択肢の一覧は `apps/server/src/lib/asr-models.ts` で定義する。
12
- tsunagi本体はこのサーバーにHTTPでプロキシするだけで、セットアップ・起動は行わない。
13
-
14
- ## 要件
15
-
16
- - macOS (Apple Silicon: M1/M2/M3/M4)
17
- - Python 3.9+ (Xcode Command Line Tools または Homebrew 経由で入手可能)
18
-
19
- ffmpeg等の外部バイナリは不要(音声はブラウザ側のVADで発話区間だけ切り出し、16kHzモノラルWAVで送られる)。
20
-
21
- ## セットアップ・起動
22
-
23
- ```bash
24
- cd apps/whisper-server
25
- ./run.sh
26
- ```
27
-
28
- 初回はvenvを作成し`requirements.txt`の依存関係(mlx-whisper, mlx-qwen3-asr, fastapi, uvicorn等)をインストールしてから起動する。
29
- `http://127.0.0.1:8765`で待受。初回起動時に指定モデルを自動ダウンロードする。
30
-
31
- 起動している間、tsunagiのSettingsで音声入力を有効にすると利用できる。停止するとtsunagi側の音声入力はエラーになる。
@@ -1,19 +0,0 @@
1
- """音声認識モデルの重みを事前ダウンロードするだけのスクリプト。
2
-
3
- 使い方: python3 download_model.py <Hugging FaceのリポジトリID>
4
- (省略時はWhisper large-v3-turbo)
5
-
6
- サーバー起動(server.py)より前に呼ぶことで、初回の文字起こしリクエストで
7
- 突然数分待たされる、という事態を避ける。tsunagi(Node)側からはこのプロセスの
8
- 実行中、キャッシュディレクトリ内の.incompleteファイルのサイズを定期的に見て
9
- 進捗を計算する想定なので、ここでは特別な進捗出力はしない。
10
- """
11
-
12
- import sys
13
-
14
- from huggingface_hub import snapshot_download
15
-
16
- DEFAULT_MODEL_REPO = "mlx-community/whisper-large-v3-turbo"
17
-
18
- if __name__ == "__main__":
19
- snapshot_download(repo_id=sys.argv[1] if len(sys.argv) > 1 else DEFAULT_MODEL_REPO)
@@ -1,8 +0,0 @@
1
- mlx-whisper==0.4.3
2
- mlx-qwen3-asr==0.4.4
3
- mlx==0.32.0
4
- fastapi
5
- uvicorn[standard]
6
- python-multipart
7
- numpy
8
- av