@minimalcorp/tsunagi 0.1.50 → 0.1.52
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/page.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error/page.js.nft.json +1 -1
- 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.js +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found/page.js.nft.json +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 +20 -19
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +20 -19
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +9 -8
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- 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 +22 -21
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +22 -21
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +9 -8
- 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 +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 +2 -2
- 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 +24 -23
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +24 -23
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +9 -8
- 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 +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js +2 -2
- 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]__02o_btn._.js +4 -4
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__03~.ax1._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0jj9045._.js → [root-of-the-server]__0h2guje._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0qjafjt._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__13yzr3f._.js +4 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_05_5y28._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_07iexqz._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0bru-z.._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +17 -17
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hqrmyz._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0i8ojdw._.js +14 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0m.hvms._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mljs_s._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0o6gl4b._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +3 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +12 -12
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0kuu0i_._.js → node_modules_06gyfuv._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_10vm5h.._.js → node_modules_09e0q95._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0-yxz.k._.js → node_modules_09nusj_._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0br2zvr._.js → node_modules_0_876x4._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0rgnv~i._.js → node_modules_0axuayq._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0-5m30v._.js → node_modules_0c8sp_9._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0azs4bs._.js → node_modules_0h_hear._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_01l-p1x._.js → node_modules_0la1a4x._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0-nk4sk._.js → node_modules_0mt.8za._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0ljm1cj._.js → node_modules_0nji2bx._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0zxpl-b._.js → node_modules_0o9t~hh._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0s5.073._.js → node_modules_0s3mkxs._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0wg2e2t._.js → node_modules_0w8sswp._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_053ofle._.js → node_modules_0zv4e8x._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0g-5xh6._.js → node_modules_10v5lct._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0ne-fkd._.js → node_modules_117mm7f._.js} +1 -1
- 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/00b2u96xq7qxl.css +4 -0
- package/.next/standalone/apps/web/.next/static/chunks/026168y2bx0d~.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/03~3ukpvmjafx.js +12 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0bhssqb0~3~7n.js → 059w2rbrv6jh4.js} +12 -12
- package/.next/standalone/apps/web/.next/static/chunks/08-d-ag2euha9.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/0d-vappn~2d.d.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0f8~7qwyc8ul0.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0gpkw.1e6kcpn.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0hfsoxrzcikz5.js +7 -0
- package/.next/standalone/apps/web/.next/static/chunks/0p3_2uc39mlse.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/0v3hfoggxwsov.js +64 -0
- package/.next/standalone/apps/web/.next/static/chunks/0v6gxm~ldhmma.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0wzu~340lh7o~.js +7 -0
- package/.next/standalone/apps/web/.next/static/chunks/13y6z-vus9yvr.js +5 -0
- package/dist/cli.js +12 -1
- package/dist/server/index.js +6 -5
- package/dist/server/lib/asr-models.d.ts +7 -0
- package/dist/server/lib/asr-models.js +51 -0
- package/dist/server/lib/process-port.js +1 -1
- package/dist/server/lib/update-check.d.ts +7 -0
- package/dist/server/lib/update-check.js +65 -0
- package/dist/server/lib/voice-debug-log.d.ts +4 -4
- package/dist/server/lib/voice-debug-log.js +1 -1
- package/dist/server/lib/whisper-process.d.ts +19 -2
- package/dist/server/lib/whisper-process.js +123 -26
- package/dist/server/routes/version.d.ts +2 -0
- package/dist/server/routes/version.js +7 -0
- package/dist/server/routes/whisper.js +37 -56
- package/dist/whisper-server/README.md +11 -3
- package/dist/whisper-server/download_model.py +8 -3
- package/dist/whisper-server/requirements.txt +1 -0
- package/dist/whisper-server/run.sh +7 -2
- package/dist/whisper-server/server.py +57 -29
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08uda78._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0kpobap._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0m52zsj._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0mo_9ei._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0stter-._.js +0 -4
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_00mhd_n._.js +0 -7
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0i7y5ac._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0iscwmf._.js +0 -14
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0ldtf2a._.js +0 -7
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0ydcfna._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0z8pikg._.js +0 -7
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0zu_05y._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_0-bra0k._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0qttwc4._.js +0 -3
- package/.next/standalone/apps/web/.next/static/chunks/0.ahnhx-ugzbt.js +0 -7
- package/.next/standalone/apps/web/.next/static/chunks/01fuewnm93_l5.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0ekz145m0f5h8.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0n.xodexdkf03.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0ohaa3~lhhv1b.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/0p19bzg3v08hf.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0pg.320d4u8z5.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0plamn018wq.u.css +0 -4
- package/.next/standalone/apps/web/.next/static/chunks/0qbchjvhms-.9.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/0tcznr_934_ai.js +0 -64
- package/.next/standalone/apps/web/.next/static/chunks/0ywpi-uf71k8s.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/12av8cpq.8slk.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/12n4j5ugu5jbn.js +0 -12
- package/.next/standalone/apps/web/.next/static/chunks/16ehoz1y-qf39.js +0 -5
- package/dist/llm-server/README.md +0 -27
- package/dist/llm-server/download_model.py +0 -14
- package/dist/llm-server/requirements.txt +0 -2
- package/dist/llm-server/run.sh +0 -59
- package/dist/server/lib/llm-process.d.ts +0 -29
- package/dist/server/lib/llm-process.js +0 -261
- package/dist/server/routes/llm.d.ts +0 -2
- package/dist/server/routes/llm.js +0 -23
- /package/.next/standalone/apps/web/.next/static/{w7do3hyV84oyognhRCQuA → et2hOuZAP9TEcN1mn6uIG}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{w7do3hyV84oyognhRCQuA → et2hOuZAP9TEcN1mn6uIG}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{w7do3hyV84oyognhRCQuA → et2hOuZAP9TEcN1mn6uIG}/_ssgManifest.js +0 -0
|
@@ -1,261 +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 { killProcessOnPort } from './process-port.js';
|
|
7
|
-
import { findPython, isVenvUpToDate, PYTHON_NOT_FOUND_MESSAGE, writeVenvMarker, } from './python-venv.js';
|
|
8
|
-
const MODEL = 'mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit';
|
|
9
|
-
const LLM_SERVER_URL = process.env.TSUNAGI_LLM_SERVER_URL || 'http://127.0.0.1:8766';
|
|
10
|
-
// run.shが待受けるポート固定値(host.docker.internal経由URLと違い、停止処理は
|
|
11
|
-
// 必ずこのNodeプロセスと同じホスト上のポートを対象にする必要があるため分けて持つ)。
|
|
12
|
-
const LLM_PORT = 8766;
|
|
13
|
-
export function getLlmServerUrl() {
|
|
14
|
-
return LLM_SERVER_URL;
|
|
15
|
-
}
|
|
16
|
-
// venv・モデルキャッシュとも ~/.tsunagi/llm 配下にまとめる(run.shと同じ場所)。
|
|
17
|
-
const TSUNAGI_LLM_DIR = path.join(os.homedir(), '.tsunagi', 'llm');
|
|
18
|
-
const VENV_DIR = path.join(TSUNAGI_LLM_DIR, 'venv');
|
|
19
|
-
const HF_CACHE_DIR = path.join(TSUNAGI_LLM_DIR, 'cache');
|
|
20
|
-
const MODEL_CACHE_DIR = path.join(HF_CACHE_DIR, 'hub', 'models--mlx-community--Qwen3-30B-A3B-Instruct-2507-4bit');
|
|
21
|
-
// 実測値(2026-07時点、4bit量子化された重み一式の合計)。多少の変動はあるがETA計算の目安として使う。
|
|
22
|
-
const EXPECTED_MODEL_BYTES = 17_200_000_000;
|
|
23
|
-
// starting_serverフェーズ(spawn後にhealthyになるまで待つ時間)の上限。
|
|
24
|
-
// mlx_lm.serverは/healthをモデル本体のロード完了を待たずに返す(ロードはバックグラウンド
|
|
25
|
-
// スレッドで進む)ため、この待ち時間の大半はPython起動/import初期化のばらつきに対する
|
|
26
|
-
// マージン。whisperより依存関係が重いぶん長めの90秒を維持する。
|
|
27
|
-
// プロセスが起動途中でクラッシュした場合はこのタイムアウトを待たず即座に検知する。
|
|
28
|
-
const STARTUP_TIMEOUT_MS = 90_000;
|
|
29
|
-
// クラッシュ時にエラーメッセージへ含めるstderrの上限文字数。
|
|
30
|
-
const STDERR_TAIL_MAX_CHARS = 4000;
|
|
31
|
-
// このファイルの位置から llm-server を探す:
|
|
32
|
-
// npm配布物: apps/cli/dist/server/lib → 2階層上 → apps/cli/dist/llm-server
|
|
33
|
-
// dev/build: apps/server/{src,dist}/lib → 3階層上 → apps/llm-server
|
|
34
|
-
export function findLlmServerDir() {
|
|
35
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
-
const candidates = [
|
|
37
|
-
path.join(here, '..', '..', 'llm-server'),
|
|
38
|
-
path.join(here, '..', '..', '..', 'llm-server'),
|
|
39
|
-
];
|
|
40
|
-
return candidates.find((c) => fs.existsSync(path.join(c, 'run.sh'))) ?? null;
|
|
41
|
-
}
|
|
42
|
-
function venvPython() {
|
|
43
|
-
return path.join(VENV_DIR, 'bin', 'python3');
|
|
44
|
-
}
|
|
45
|
-
function isModelReady() {
|
|
46
|
-
const snapshotsDir = path.join(MODEL_CACHE_DIR, 'snapshots');
|
|
47
|
-
if (!fs.existsSync(snapshotsDir))
|
|
48
|
-
return false;
|
|
49
|
-
return fs.readdirSync(snapshotsDir).length > 0;
|
|
50
|
-
}
|
|
51
|
-
let currentStep = 'not_running';
|
|
52
|
-
let downloadProgress;
|
|
53
|
-
let lastError;
|
|
54
|
-
let managedProcess = null;
|
|
55
|
-
let setupPromise = null;
|
|
56
|
-
function sleep(ms) {
|
|
57
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
-
}
|
|
59
|
-
async function checkHealth() {
|
|
60
|
-
try {
|
|
61
|
-
const response = await fetch(`${LLM_SERVER_URL}/health`, {
|
|
62
|
-
signal: AbortSignal.timeout(2000),
|
|
63
|
-
});
|
|
64
|
-
return response.ok;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
export async function getLlmServerStatus() {
|
|
71
|
-
const serverDir = findLlmServerDir();
|
|
72
|
-
// セットアップ/起動フロー進行中はそのステップをそのまま報告する。
|
|
73
|
-
if (setupPromise) {
|
|
74
|
-
return { step: currentStep, serverDir, downloadProgress, error: lastError };
|
|
75
|
-
}
|
|
76
|
-
const healthy = await checkHealth();
|
|
77
|
-
if (healthy) {
|
|
78
|
-
return { step: managedProcess ? 'running' : 'running_external', serverDir };
|
|
79
|
-
}
|
|
80
|
-
return { step: lastError ? 'error' : 'not_running', serverDir, error: lastError };
|
|
81
|
-
}
|
|
82
|
-
function runStep(cmd, args, cwd, env) {
|
|
83
|
-
return new Promise((resolve, reject) => {
|
|
84
|
-
const child = spawn(cmd, args, { cwd, stdio: 'ignore', env: env ?? process.env });
|
|
85
|
-
child.on('exit', (code) => {
|
|
86
|
-
if (code === 0)
|
|
87
|
-
resolve();
|
|
88
|
-
else
|
|
89
|
-
reject(new Error(`${path.basename(cmd)} ${args.join(' ')} exited with code ${code}`));
|
|
90
|
-
});
|
|
91
|
-
child.on('error', reject);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
// モデルDL中(huggingface_hubがblobs/配下に *.incomplete を書き続ける)のファイルサイズを
|
|
95
|
-
// 定期的にポーリングし、進捗(%)と直近の転送速度からのETAを算出する。
|
|
96
|
-
async function trackModelDownload(child) {
|
|
97
|
-
const blobsDir = path.join(MODEL_CACHE_DIR, 'blobs');
|
|
98
|
-
let lastBytes = 0;
|
|
99
|
-
let lastTime = Date.now();
|
|
100
|
-
let stopped = false;
|
|
101
|
-
child.on('exit', () => {
|
|
102
|
-
stopped = true;
|
|
103
|
-
});
|
|
104
|
-
while (!stopped) {
|
|
105
|
-
await sleep(1000);
|
|
106
|
-
let downloaded = 0;
|
|
107
|
-
try {
|
|
108
|
-
for (const f of fs.readdirSync(blobsDir)) {
|
|
109
|
-
if (!f.endsWith('.incomplete'))
|
|
110
|
-
continue;
|
|
111
|
-
downloaded += fs.statSync(path.join(blobsDir, f)).size;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// blobsDir がまだ作られていない場合は0のまま次のポーリングへ
|
|
116
|
-
}
|
|
117
|
-
const now = Date.now();
|
|
118
|
-
const elapsedSec = (now - lastTime) / 1000;
|
|
119
|
-
const bytesPerSec = elapsedSec > 0 ? (downloaded - lastBytes) / elapsedSec : 0;
|
|
120
|
-
const remaining = EXPECTED_MODEL_BYTES - downloaded;
|
|
121
|
-
// 転送が一時的に止まって見える(ポーリング間隔とディスク書き込みのタイミングがずれる)だけで
|
|
122
|
-
// 速度0になることがあるため、その場合は直前のETAを維持し「計算中...」への逆戻りを防ぐ。
|
|
123
|
-
const etaSeconds = bytesPerSec > 0
|
|
124
|
-
? Math.max(0, Math.round(remaining / bytesPerSec))
|
|
125
|
-
: (downloadProgress?.etaSeconds ?? null);
|
|
126
|
-
downloadProgress = {
|
|
127
|
-
downloadedBytes: downloaded,
|
|
128
|
-
totalBytes: EXPECTED_MODEL_BYTES,
|
|
129
|
-
etaSeconds,
|
|
130
|
-
};
|
|
131
|
-
lastBytes = downloaded;
|
|
132
|
-
lastTime = now;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
async function runSetupAndStart(dir) {
|
|
136
|
-
const hfEnv = { ...process.env, HF_HOME: HF_CACHE_DIR, HF_HUB_DISABLE_XET: '1' };
|
|
137
|
-
const requirementsFile = path.join(dir, 'requirements.txt');
|
|
138
|
-
if (!isVenvUpToDate(VENV_DIR, requirementsFile)) {
|
|
139
|
-
currentStep = 'installing_deps';
|
|
140
|
-
const pythonBin = findPython();
|
|
141
|
-
if (!pythonBin)
|
|
142
|
-
throw new Error(PYTHON_NOT_FOUND_MESSAGE);
|
|
143
|
-
fs.mkdirSync(TSUNAGI_LLM_DIR, { recursive: true });
|
|
144
|
-
fs.rmSync(VENV_DIR, { recursive: true, force: true });
|
|
145
|
-
await runStep(pythonBin, ['-m', 'venv', VENV_DIR], dir);
|
|
146
|
-
await runStep(venvPython(), ['-m', 'pip', 'install', '-r', 'requirements.txt'], dir);
|
|
147
|
-
writeVenvMarker(VENV_DIR, requirementsFile);
|
|
148
|
-
}
|
|
149
|
-
if (!isModelReady()) {
|
|
150
|
-
currentStep = 'downloading_model';
|
|
151
|
-
downloadProgress = { downloadedBytes: 0, totalBytes: EXPECTED_MODEL_BYTES, etaSeconds: null };
|
|
152
|
-
await new Promise((resolve, reject) => {
|
|
153
|
-
const child = spawn(venvPython(), ['download_model.py'], {
|
|
154
|
-
cwd: dir,
|
|
155
|
-
stdio: 'ignore',
|
|
156
|
-
env: hfEnv,
|
|
157
|
-
});
|
|
158
|
-
void trackModelDownload(child);
|
|
159
|
-
child.on('exit', (code) => {
|
|
160
|
-
if (code === 0)
|
|
161
|
-
resolve();
|
|
162
|
-
else
|
|
163
|
-
reject(new Error(`download_model.py exited with code ${code}`));
|
|
164
|
-
});
|
|
165
|
-
child.on('error', reject);
|
|
166
|
-
});
|
|
167
|
-
downloadProgress = undefined;
|
|
168
|
-
}
|
|
169
|
-
currentStep = 'starting_server';
|
|
170
|
-
const child = spawn(venvPython(), ['-m', 'mlx_lm.server', '--model', MODEL, '--host', '127.0.0.1', '--port', String(LLM_PORT)], { cwd: dir, stdio: ['ignore', 'ignore', 'pipe'], env: hfEnv });
|
|
171
|
-
let stderrTail = '';
|
|
172
|
-
child.stderr?.on('data', (chunk) => {
|
|
173
|
-
stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_MAX_CHARS);
|
|
174
|
-
});
|
|
175
|
-
// healthポーリングとは別に、プロセスが起動途中で終了したことをタイムアウトを待たず検知する。
|
|
176
|
-
let exitInfo = null;
|
|
177
|
-
child.on('exit', (code, signal) => {
|
|
178
|
-
exitInfo = `exit code=${code} signal=${signal}`;
|
|
179
|
-
if (managedProcess === child)
|
|
180
|
-
managedProcess = null;
|
|
181
|
-
});
|
|
182
|
-
child.on('error', (err) => {
|
|
183
|
-
exitInfo = `spawn error: ${err.message}`;
|
|
184
|
-
if (managedProcess === child)
|
|
185
|
-
managedProcess = null;
|
|
186
|
-
});
|
|
187
|
-
managedProcess = child;
|
|
188
|
-
const start = Date.now();
|
|
189
|
-
while (Date.now() - start < STARTUP_TIMEOUT_MS) {
|
|
190
|
-
if (exitInfo) {
|
|
191
|
-
throw new Error(`llm-server crashed while starting (${exitInfo})${stderrTail ? `\n${stderrTail}` : ''}`);
|
|
192
|
-
}
|
|
193
|
-
if (await checkHealth())
|
|
194
|
-
return;
|
|
195
|
-
await sleep(1000);
|
|
196
|
-
}
|
|
197
|
-
throw new Error(`Server did not become healthy within ${STARTUP_TIMEOUT_MS / 1000}s of starting`);
|
|
198
|
-
}
|
|
199
|
-
export function startLlmServer() {
|
|
200
|
-
if (setupPromise || managedProcess) {
|
|
201
|
-
return { started: false, error: 'Already starting/running' };
|
|
202
|
-
}
|
|
203
|
-
const dir = findLlmServerDir();
|
|
204
|
-
if (!dir) {
|
|
205
|
-
return { started: false, error: 'llm-server directory not found' };
|
|
206
|
-
}
|
|
207
|
-
lastError = undefined;
|
|
208
|
-
setupPromise = runSetupAndStart(dir)
|
|
209
|
-
.catch((error) => {
|
|
210
|
-
currentStep = 'error';
|
|
211
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
212
|
-
})
|
|
213
|
-
.finally(() => {
|
|
214
|
-
setupPromise = null;
|
|
215
|
-
});
|
|
216
|
-
return { started: true };
|
|
217
|
-
}
|
|
218
|
-
export async function stopLlmServer() {
|
|
219
|
-
if (managedProcess) {
|
|
220
|
-
managedProcess.kill();
|
|
221
|
-
managedProcess = null;
|
|
222
|
-
currentStep = 'not_running';
|
|
223
|
-
return { stopped: true };
|
|
224
|
-
}
|
|
225
|
-
// tsunagi外(make llm等)で起動された場合はchild_processのハンドルを
|
|
226
|
-
// 持たないため、ポート番号を手がかりにOS側から見つけて停止する。
|
|
227
|
-
const killed = await killProcessOnPort(LLM_PORT);
|
|
228
|
-
if (!killed) {
|
|
229
|
-
return {
|
|
230
|
-
stopped: false,
|
|
231
|
-
error: `ポート${LLM_PORT}で待ち受けているプロセスが見つかりませんでした`,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
currentStep = 'not_running';
|
|
235
|
-
return { stopped: true };
|
|
236
|
-
}
|
|
237
|
-
// tsunagi本体プロセスの終了時(Ctrl+C等)に、自分が起動したllm-serverも
|
|
238
|
-
// 道連れで停止する。ユーザーが手動で起動したもの(running_external)には触れない。
|
|
239
|
-
export function stopLlmServerOnExit() {
|
|
240
|
-
managedProcess?.kill();
|
|
241
|
-
managedProcess = null;
|
|
242
|
-
}
|
|
243
|
-
// mlx_lm.serverへストリーミングなしで1回だけ問い合わせ、最終テキストのみを返す。
|
|
244
|
-
// 音声入力の文字起こし結果の整形など、対話UIを介さずLLMの出力だけ欲しい場面で使う。
|
|
245
|
-
export async function generateLlmCompletion(messages, maxTokens) {
|
|
246
|
-
const response = await fetch(`${LLM_SERVER_URL}/v1/chat/completions`, {
|
|
247
|
-
method: 'POST',
|
|
248
|
-
headers: { 'content-type': 'application/json' },
|
|
249
|
-
body: JSON.stringify({ messages, stream: false, max_tokens: maxTokens }),
|
|
250
|
-
signal: AbortSignal.timeout(60_000),
|
|
251
|
-
});
|
|
252
|
-
if (!response.ok) {
|
|
253
|
-
throw new Error(`llm-server responded with ${response.status}`);
|
|
254
|
-
}
|
|
255
|
-
const data = (await response.json());
|
|
256
|
-
const content = data.choices?.[0]?.message?.content;
|
|
257
|
-
if (typeof content !== 'string') {
|
|
258
|
-
throw new Error('Unexpected response shape from llm-server');
|
|
259
|
-
}
|
|
260
|
-
return content;
|
|
261
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { getLlmServerStatus, startLlmServer, stopLlmServer } from '../lib/llm-process.js';
|
|
2
|
-
export async function llmRoutes(fastify) {
|
|
3
|
-
// GET /llm/server/status
|
|
4
|
-
fastify.get('/llm/server/status', async (_request, reply) => {
|
|
5
|
-
return reply.status(200).send(await getLlmServerStatus());
|
|
6
|
-
});
|
|
7
|
-
// POST /llm/server/start
|
|
8
|
-
fastify.post('/llm/server/start', async (_request, reply) => {
|
|
9
|
-
const result = startLlmServer();
|
|
10
|
-
if (!result.started) {
|
|
11
|
-
return reply.status(409).send({ error: result.error });
|
|
12
|
-
}
|
|
13
|
-
return reply.status(202).send(await getLlmServerStatus());
|
|
14
|
-
});
|
|
15
|
-
// POST /llm/server/stop
|
|
16
|
-
fastify.post('/llm/server/stop', async (_request, reply) => {
|
|
17
|
-
const result = await stopLlmServer();
|
|
18
|
-
if (!result.stopped) {
|
|
19
|
-
return reply.status(409).send({ error: result.error });
|
|
20
|
-
}
|
|
21
|
-
return reply.status(200).send(await getLlmServerStatus());
|
|
22
|
-
});
|
|
23
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|