@co0ontty/wand 4.97.0 → 4.98.0

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 (160) hide show
  1. package/README.md +0 -4
  2. package/dist/agent-dispatch.d.ts +1 -0
  3. package/dist/agent-dispatch.js +13 -2
  4. package/dist/ai-team-types.d.ts +1 -1
  5. package/dist/ai-team-types.js +1 -1
  6. package/dist/build-info.json +3 -3
  7. package/dist/cli.js +2 -0
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +2 -2
  10. package/dist/core-runner.d.ts +0 -4
  11. package/dist/core-runner.js +41 -3
  12. package/dist/decision-expert-employee.js +1 -0
  13. package/dist/distribution-manager.d.ts +29 -4
  14. package/dist/distribution-manager.js +147 -57
  15. package/dist/employee-types.d.ts +2 -1
  16. package/dist/employee-types.js +2 -4
  17. package/dist/feedback-feature-map.d.ts +0 -1
  18. package/dist/feedback-feature-map.js +0 -1
  19. package/dist/feedback.d.ts +0 -1
  20. package/dist/feedback.js +1 -1
  21. package/dist/git-quick-commit.js +13 -52
  22. package/dist/git-tag-generation.d.ts +11 -0
  23. package/dist/git-tag-generation.js +47 -0
  24. package/dist/hr-employee.js +1 -0
  25. package/dist/local-model-setup.d.ts +1 -9
  26. package/dist/local-model-setup.js +52 -166
  27. package/dist/local-model-types.d.ts +2 -5
  28. package/dist/managed-temp.d.ts +1 -1
  29. package/dist/managed-temp.js +1 -1
  30. package/dist/middleware/api-scope.js +2 -4
  31. package/dist/migration-backup-retention.d.ts +17 -0
  32. package/dist/migration-backup-retention.js +91 -0
  33. package/dist/pi-tmux-host.d.ts +28 -0
  34. package/dist/pi-tmux-host.js +189 -0
  35. package/dist/server-employee-routes.d.ts +1 -0
  36. package/dist/server-employee-routes.js +17 -4
  37. package/dist/server-local-model-routes.js +2 -15
  38. package/dist/server-session-input-routes.js +3 -3
  39. package/dist/server-session-lifecycle-routes.js +94 -18
  40. package/dist/server-task-routes.js +18 -13
  41. package/dist/server-update-routes.d.ts +11 -1
  42. package/dist/server-update-routes.js +25 -12
  43. package/dist/server-workspace-routes.js +11 -5
  44. package/dist/server.js +29 -129
  45. package/dist/storage/conversations-repository.d.ts +2 -0
  46. package/dist/storage/conversations-repository.js +4 -0
  47. package/dist/storage/employees-repository.d.ts +2 -2
  48. package/dist/storage/employees-repository.js +12 -15
  49. package/dist/storage/internal.js +2 -2
  50. package/dist/storage/migrations.js +20 -0
  51. package/dist/storage/readonly.js +3 -0
  52. package/dist/storage/sessions-repository.d.ts +2 -0
  53. package/dist/storage/sessions-repository.js +13 -0
  54. package/dist/storage/tasks-repository.d.ts +22 -0
  55. package/dist/storage/tasks-repository.js +52 -3
  56. package/dist/storage/workspaces-repository.d.ts +1 -0
  57. package/dist/storage/workspaces-repository.js +3 -1
  58. package/dist/storage-cleanup.d.ts +37 -2
  59. package/dist/storage-cleanup.js +62 -2
  60. package/dist/storage-schema.js +9 -0
  61. package/dist/structured-claude-adapter.js +5 -1
  62. package/dist/structured-claude-protocol.d.ts +1 -1
  63. package/dist/structured-claude-protocol.js +12 -1
  64. package/dist/structured-runner.d.ts +2 -0
  65. package/dist/structured-session-manager.d.ts +14 -2
  66. package/dist/structured-session-manager.js +87 -14
  67. package/dist/system-ai-call.d.ts +5 -1
  68. package/dist/system-ai-call.js +32 -1
  69. package/dist/system-ai-session-recorder.d.ts +2 -0
  70. package/dist/system-ai-session-recorder.js +35 -3
  71. package/dist/system-cli.js +6 -1
  72. package/dist/system-employee.js +1 -0
  73. package/dist/task-comment-dispatch.d.ts +18 -0
  74. package/dist/task-comment-dispatch.js +121 -0
  75. package/dist/task-types.d.ts +14 -0
  76. package/dist/version-utils.d.ts +31 -0
  77. package/dist/version-utils.js +35 -0
  78. package/dist/web-ui/content/ai-teams.js +10 -10
  79. package/dist/web-ui/content/module-manifest.json +1 -1
  80. package/dist/web-ui/content/modules/02b1ec89c69dc0dd/host.js +30 -0
  81. package/dist/web-ui/content/modules/{e8f0cda6b9ac8a70 → 0434a45abd2c1d15}/chunk.js +1 -1
  82. package/dist/web-ui/content/modules/{e9a01a25ec573294 → 0afa04591224bc23}/library-bridge.js +1 -1
  83. package/dist/web-ui/content/modules/{b33c567946044d83 → 0bf4a78c8be1bdc9}/host.js +1 -1
  84. package/dist/web-ui/content/modules/17f72e0fb014a302/chat-render.js +75 -0
  85. package/dist/web-ui/content/modules/{5903104a2e0e3888 → 183db4802a7d99ed}/host.js +1 -1
  86. package/dist/web-ui/content/modules/23d2006fcfcf3c0c/host-bridge.js +1 -0
  87. package/dist/web-ui/content/modules/{0c96341a022aa360 → 32f151da1a006041}/host.js +1 -1
  88. package/dist/web-ui/content/modules/{3172fe0b46d51f8d → 34a7674efc329383}/chunk.js +1 -1
  89. package/dist/web-ui/content/modules/3541ba4cb2886e29/chunk.js +1 -0
  90. package/dist/web-ui/content/modules/{801e7f686ce7eb28 → 395257bd20cd0098}/chunk.js +1 -1
  91. package/dist/web-ui/content/modules/{3ddcbd8505bfcdf4 → 3fd75af0a6891549}/chunk.js +1 -1
  92. package/dist/web-ui/content/modules/{c41d8ffa27816766 → 428352e422932c40}/host.js +1 -1
  93. package/dist/web-ui/content/modules/485a72b2c83e7680/workspace-window.js +1 -0
  94. package/dist/web-ui/content/modules/489dfe1f7441ef2c/markdown.js +1 -0
  95. package/dist/web-ui/content/modules/4b42b4336458b7b5/runtime.js +550 -0
  96. package/dist/web-ui/content/modules/{de00ab5329b5e05a → 528c5d97e8f9d582}/chunk.js +1 -1
  97. package/dist/web-ui/content/modules/{3086df78444998b3 → 535aad05d34079d2}/host.js +1 -1
  98. package/dist/web-ui/content/modules/62028b9bbf8ffc88/page.js +6 -0
  99. package/dist/web-ui/content/modules/6868f75a5981ad08/host.js +69 -0
  100. package/dist/web-ui/content/modules/6c72c8c166e34de7/host.js +1 -0
  101. package/dist/web-ui/content/modules/6ff6ce370ef26753/host.js +1 -0
  102. package/dist/web-ui/content/modules/{87379801875d1921 → 72bef6889fdafefe}/host.js +1 -1
  103. package/dist/web-ui/content/modules/{5e5eabd9a9366a56 → 763c79f5527d762a}/panel.js +1 -1
  104. package/dist/web-ui/content/modules/{d30ac9af6dc7d555 → 77ea205020273767}/chunk.js +1 -1
  105. package/dist/web-ui/content/modules/{e01e1e25b7fe37e9 → 7fb02c44fd6f877b}/host.js +1 -1
  106. package/dist/web-ui/content/modules/{349ee129eef5a182 → 818d87c1651fc6a1}/chunk.js +1 -1
  107. package/dist/web-ui/content/modules/{66f36cd2c38cfb07 → 8cb5d84a2731789e}/chunk.js +1 -1
  108. package/dist/web-ui/content/modules/{f5b3fb6fedd9b05d → 96e78c7f2cd9acd5}/chunk.js +1 -1
  109. package/dist/web-ui/content/modules/{64c1b0217f5377aa → a60a07ba1abcdc3f}/chunk.js +1 -1
  110. package/dist/web-ui/content/modules/{ee12d3c4421a5c12 → b4bcb9bf5cfb0b3a}/host.js +1 -1
  111. package/dist/web-ui/content/modules/b71cda413318d71f/employee-card.js +1 -0
  112. package/dist/web-ui/content/modules/bb0c299cf006e157/task-board-host.js +17 -0
  113. package/dist/web-ui/content/modules/{42ae495da5903b07 → c8c6adee81d4bd98}/sha256.js +1 -1
  114. package/dist/web-ui/content/modules/{854b0f6dc887923b → cd0cec0ba1887ffd}/chunk.js +1 -1
  115. package/dist/web-ui/content/modules/{3338b55c7767bacb → d673a76becc7d1f5}/chunk.js +1 -1
  116. package/dist/web-ui/content/modules/{2bce1137f402bd70 → d99ea7e921488348}/chunk.js +1 -1
  117. package/dist/web-ui/content/modules/{a9f15bd03bc590e7 → e862ccabc908eb0c}/host.js +1 -1
  118. package/dist/web-ui/content/modules/{9984f6bb05ac55bb → eb46bf62b1ae1bea}/host.js +1 -1
  119. package/dist/web-ui/content/plush-avatar.js +166 -166
  120. package/dist/web-ui/content/styles.css +1 -1
  121. package/dist/web-ui/embedded-assets.d.ts +177 -177
  122. package/dist/web-ui/embedded-assets.js +6 -6
  123. package/dist/web-ui/page-route.d.ts +24 -0
  124. package/dist/web-ui/page-route.js +130 -0
  125. package/dist/web-ui/pty-input-state.d.ts +2 -0
  126. package/dist/web-ui/pty-input-state.js +2 -2
  127. package/dist/web-ui/react/workspaces/types.d.ts +5 -0
  128. package/package.json +1 -3
  129. package/dist/server-speech-routes.d.ts +0 -10
  130. package/dist/server-speech-routes.js +0 -93
  131. package/dist/speech-models.d.ts +0 -24
  132. package/dist/speech-models.js +0 -16
  133. package/dist/speech-polisher-employee.d.ts +0 -6
  134. package/dist/speech-polisher-employee.js +0 -23
  135. package/dist/speech-polisher-identity.d.ts +0 -4
  136. package/dist/speech-polisher-identity.js +0 -4
  137. package/dist/speech-polisher-service.d.ts +0 -33
  138. package/dist/speech-polisher-service.js +0 -110
  139. package/dist/speech-service.d.ts +0 -59
  140. package/dist/speech-service.js +0 -420
  141. package/dist/speech-support.d.ts +0 -6
  142. package/dist/speech-support.js +0 -24
  143. package/dist/speech-types.d.ts +0 -52
  144. package/dist/speech-types.js +0 -3
  145. package/dist/web-ui/content/modules/15e32e85e086f6b4/page.js +0 -6
  146. package/dist/web-ui/content/modules/1e43259bddbc6805/markdown.js +0 -1
  147. package/dist/web-ui/content/modules/271d488d262909c2/host.js +0 -30
  148. package/dist/web-ui/content/modules/43746e7237d61da9/task-board-host.js +0 -17
  149. package/dist/web-ui/content/modules/4485e6c1f0459aa8/chunk.js +0 -1
  150. package/dist/web-ui/content/modules/5b541e0f296a3592/employee-card.js +0 -1
  151. package/dist/web-ui/content/modules/6ed52e6604661678/chat-render.js +0 -75
  152. package/dist/web-ui/content/modules/89888da0972e5033/host.js +0 -50
  153. package/dist/web-ui/content/modules/c11165f843d818f4/host-bridge.js +0 -1
  154. package/dist/web-ui/content/modules/c1829693f91fd25c/host.js +0 -1
  155. package/dist/web-ui/content/modules/cdb882ddab12e325/host.js +0 -1
  156. package/dist/web-ui/content/modules/ea67fd4e96c9f45a/workspace-window.js +0 -1
  157. package/dist/web-ui/content/modules/fd767282c29173b5/runtime.js +0 -539
  158. package/dist/web-ui/speech-audio.d.ts +0 -2
  159. package/dist/web-ui/speech-audio.js +0 -41
  160. package/scripts/install-speech-runtime.js +0 -82
@@ -1,420 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { constants, createReadStream } from "node:fs";
4
- import { access, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { buildChildEnv, systemEnvValue } from "./env-utils.js";
8
- import { SPEECH_MODELS, speechModel, speechModelUrl } from "./speech-models.js";
9
- import { SPEECH_MAX_SECONDS, SPEECH_MAX_WAV_BYTES, SPEECH_SAMPLE_RATE } from "./speech-types.js";
10
- import { configTemporaryRoot, createManagedTemporary, registerTemporaryChild } from "./managed-temp.js";
11
- export const SPEECH_SETTINGS_KEY = "pref:speechRecognition";
12
- export class SpeechError extends Error {
13
- code;
14
- status;
15
- constructor(code, message, status = 400) {
16
- super(message);
17
- this.code = code;
18
- this.status = status;
19
- }
20
- }
21
- export function defaultSpeechSettings() {
22
- return { enabled: false, model: "base", acceleration: "auto", language: "auto", threads: Math.max(1, Math.min(4, os.availableParallelism() - 1)) };
23
- }
24
- export function parseSpeechSettings(value, previous = defaultSpeechSettings()) {
25
- if (!value || typeof value !== "object" || Array.isArray(value))
26
- throw new SpeechError("INVALID_SETTINGS", "语音配置无效。");
27
- const patch = value;
28
- if (Object.keys(patch).some((key) => !["enabled", "model", "acceleration", "language", "threads"].includes(key))) {
29
- throw new SpeechError("INVALID_SETTINGS", "不支持的语音配置项。");
30
- }
31
- const settings = { ...previous, ...patch };
32
- if (typeof settings.enabled !== "boolean" || !speechModel(settings.model)
33
- || !["auto", "cpu", "gpu"].includes(settings.acceleration) || !["auto", "zh", "en"].includes(settings.language)
34
- || !Number.isInteger(settings.threads) || settings.threads < 1 || settings.threads > 16) {
35
- throw new SpeechError("INVALID_SETTINGS", "请选择有效模型、运行设备、语言与 1–16 个 CPU 线程。");
36
- }
37
- return settings;
38
- }
39
- /** Strict canonical WAV; all clients encode this exact shape. No decoder/ffmpeg needed on the host. */
40
- export function validateSpeechWav(audio) {
41
- if (audio.length < 44 + 3200 || audio.length > SPEECH_MAX_WAV_BYTES
42
- || audio.toString("ascii", 0, 4) !== "RIFF" || audio.readUInt32LE(4) !== audio.length - 8
43
- || audio.toString("ascii", 8, 12) !== "WAVE" || audio.toString("ascii", 12, 16) !== "fmt "
44
- || audio.readUInt32LE(16) !== 16 || audio.readUInt16LE(20) !== 1 || audio.readUInt16LE(22) !== 1
45
- || audio.readUInt32LE(24) !== SPEECH_SAMPLE_RATE || audio.readUInt32LE(28) !== SPEECH_SAMPLE_RATE * 2
46
- || audio.readUInt16LE(32) !== 2 || audio.readUInt16LE(34) !== 16
47
- || audio.toString("ascii", 36, 40) !== "data" || audio.readUInt32LE(40) !== audio.length - 44
48
- || (audio.length - 44) % 2 !== 0) {
49
- throw new SpeechError("INVALID_AUDIO", "请上传 0.1–60 秒、16 kHz 单声道 PCM16 WAV 音频。");
50
- }
51
- let peak = 0;
52
- for (let offset = 44; offset < audio.length; offset += 2)
53
- peak = Math.max(peak, Math.abs(audio.readInt16LE(offset)));
54
- return { samples: (audio.length - 44) / 2, silent: peak < 160 };
55
- }
56
- async function fileHash(file) {
57
- const hash = createHash("sha256");
58
- for await (const chunk of createReadStream(file))
59
- hash.update(chunk);
60
- return hash.digest("hex");
61
- }
62
- /** A single bounded job per host avoids CPU/RAM amplification. Audio is temporary, never history. */
63
- export class SpeechService {
64
- storage;
65
- options;
66
- root;
67
- models;
68
- verified = new Map();
69
- verifying = new Map();
70
- download = null;
71
- downloadAbort = null;
72
- active = null;
73
- disposed = false;
74
- maintenance = false;
75
- initialized = new Set();
76
- configurationRevision = 0;
77
- constructor(storage, configDir, options = {}) {
78
- this.storage = storage;
79
- this.options = options;
80
- this.root = path.join(configDir, "speech");
81
- this.models = options.models ?? SPEECH_MODELS;
82
- }
83
- settings() {
84
- try {
85
- return parseSpeechSettings(this.storage.getPreference(SPEECH_SETTINGS_KEY, {}));
86
- }
87
- catch {
88
- return defaultSpeechSettings();
89
- }
90
- }
91
- configure(patch) {
92
- const settings = parseSpeechSettings(patch, this.settings());
93
- this.storage.setPreference(SPEECH_SETTINGS_KEY, settings);
94
- this.configurationRevision += 1;
95
- return settings;
96
- }
97
- settingsRevision() { return this.configurationRevision; }
98
- acquireMaintenance() {
99
- if (this.disposed)
100
- throw new SpeechError("UNAVAILABLE", "语音服务已关闭。", 503);
101
- if (this.active || this.downloadAbort || this.maintenance)
102
- throw new SpeechError("BUSY", "语音识别或模型下载正在进行,请稍后初始化。", 409);
103
- this.maintenance = true;
104
- }
105
- releaseMaintenance() { this.maintenance = false; }
106
- /** Explicit admin health check loads the selected model using synthetic silence, without enabling it. */
107
- async initializeModel(id, signal, acceleration = this.settings().acceleration) {
108
- const model = this.models.find((value) => value.id === id);
109
- if (!model)
110
- throw new SpeechError("INVALID_MODEL", "模型不存在。");
111
- const runtime = await this.runtime();
112
- if (!runtime.executable || !await this.modelReady(model))
113
- throw new SpeechError("NOT_READY", "请先下载模型并安装语音运行时。", 503);
114
- signal.throwIfAborted();
115
- const temporary = createManagedTemporary("speech-check", configTemporaryRoot(path.dirname(this.root)));
116
- const directory = temporary.directory;
117
- try {
118
- const wav = Buffer.alloc(8044);
119
- wav.write("RIFF");
120
- wav.writeUInt32LE(wav.length - 8, 4);
121
- wav.write("WAVEfmt ", 8);
122
- wav.writeUInt32LE(16, 16);
123
- wav.writeUInt16LE(1, 20);
124
- wav.writeUInt16LE(1, 22);
125
- wav.writeUInt32LE(16000, 24);
126
- wav.writeUInt32LE(32000, 28);
127
- wav.writeUInt16LE(2, 32);
128
- wav.writeUInt16LE(16, 34);
129
- wav.write("data", 36);
130
- wav.writeUInt32LE(8000, 40);
131
- const input = path.join(directory, "check.wav"), output = path.join(directory, "check");
132
- await writeFile(input, wav, { mode: 0o600 });
133
- const args = ["-m", this.modelPath(model), "-f", input, "-of", output, "-otxt", "-nt", "-l", "en", "-t", String(this.settings().threads)];
134
- const cpu = acceleration === "cpu" || runtime.backend === "cpu";
135
- try {
136
- await this.run(runtime.executable, [...args, ...(cpu ? ["-ng"] : [])], directory, signal, this.options.timeoutMs ?? 120_000);
137
- }
138
- catch (error) {
139
- if (acceleration !== "auto" || cpu || signal.aborted || !(error instanceof SpeechError) || error.code !== "ENGINE_FAILED")
140
- throw error;
141
- await this.run(runtime.executable, [...args, "-ng"], directory, signal, this.options.timeoutMs ?? 120_000);
142
- }
143
- signal.throwIfAborted();
144
- if (this.disposed)
145
- throw new SpeechError("UNAVAILABLE", "语音服务已关闭。", 503);
146
- this.initialized.add(id);
147
- }
148
- finally {
149
- await temporary.closeAsync();
150
- }
151
- }
152
- modelPath(model) { return path.join(this.root, "models", `ggml-${model.id}.bin`); }
153
- async modelReady(model) {
154
- if (this.verifying.has(model.id))
155
- return this.verifying.get(model.id);
156
- const promise = this.verifyModel(model);
157
- this.verifying.set(model.id, promise);
158
- try {
159
- return await promise;
160
- }
161
- finally {
162
- this.verifying.delete(model.id);
163
- }
164
- }
165
- async verifyModel(model) {
166
- try {
167
- const file = this.modelPath(model);
168
- const info = await stat(file);
169
- if (!info.isFile() || info.size !== model.size)
170
- return false;
171
- const key = `${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
172
- if (this.verified.get(model.id) === key)
173
- return true;
174
- if (await fileHash(file) !== model.sha256)
175
- return false;
176
- this.verified.set(model.id, key);
177
- return true;
178
- }
179
- catch {
180
- return false;
181
- }
182
- }
183
- async runtime() {
184
- let backend = this.options.backend ?? systemEnvValue("WAND_WHISPER_BACKEND");
185
- if (!backend) {
186
- try {
187
- backend = JSON.parse(await readFile(path.join(this.root, "runtime.json"), "utf8")).backend;
188
- }
189
- catch { }
190
- }
191
- if (!["cpu", "metal", "cuda"].includes(backend ?? ""))
192
- backend = process.platform === "darwin" ? "metal" : "cpu";
193
- const name = process.platform === "win32" ? "whisper-cli.exe" : "whisper-cli";
194
- const explicit = this.options.executable ?? systemEnvValue("WAND_WHISPER_BIN");
195
- // Explicit bad paths must not silently run another binary. No shell, no request-provided command.
196
- const candidates = explicit ? [explicit] : [path.join(this.root, "bin", name),
197
- ...(buildChildEnv(true).PATH ?? "").split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, name))];
198
- for (const candidate of candidates) {
199
- if (!path.isAbsolute(candidate))
200
- continue;
201
- try {
202
- await access(candidate, process.platform === "win32" ? constants.F_OK : constants.X_OK);
203
- if ((await stat(candidate)).isFile())
204
- return { executable: candidate, backend: backend };
205
- }
206
- catch { }
207
- }
208
- return { executable: null, backend: backend };
209
- }
210
- async status() {
211
- const runtime = await this.runtime();
212
- const models = await Promise.all(this.models.map(async (model) => ({ id: model.id, label: model.label,
213
- description: model.description, size: model.size, downloaded: await this.modelReady(model) })));
214
- const settings = this.settings();
215
- const reason = this.disposed ? "语音服务已关闭。" : this.maintenance ? "语音运行时正在初始化,请稍后重试。" : !settings.enabled ? "服务端识别未启用,请在管理设置中启用。"
216
- : !runtime.executable ? "服务端尚未安装 whisper.cpp,请在服务器运行语音运行时安装脚本。"
217
- : !models.find((model) => model.id === settings.model)?.downloaded ? "服务端模型尚未下载或校验失败,请在管理设置中下载。"
218
- : settings.acceleration === "gpu" && runtime.backend === "cpu" ? "当前运行时是 CPU 版,请改为自动 / CPU 或安装 GPU 版。" : null;
219
- return { settings, ready: !reason, reason, runtime: { available: !!runtime.executable, backend: runtime.backend,
220
- platform: process.platform, arch: process.arch }, models, download: this.download ? { ...this.download } : null,
221
- maxDurationSeconds: SPEECH_MAX_SECONDS, busy: !!this.active || this.maintenance, initialized: this.initialized.has(settings.model) };
222
- }
223
- cancelDownload() { this.downloadAbort?.abort(); }
224
- async startDownload(id) {
225
- const model = this.models.find((item) => item.id === id);
226
- if (!model)
227
- throw new SpeechError("INVALID_MODEL", "模型不存在。");
228
- if (this.disposed)
229
- throw new SpeechError("UNAVAILABLE", "语音服务已关闭。", 503);
230
- if (this.maintenance)
231
- throw new SpeechError("BUSY", "语音运行时正在初始化,请稍后下载。", 409);
232
- if (this.downloadAbort)
233
- throw new SpeechError("DOWNLOAD_BUSY", "已有模型正在下载。", 409);
234
- if (await this.modelReady(model))
235
- return;
236
- // Recheck after asynchronous verification, before reserving the only download slot.
237
- if (this.disposed)
238
- throw new SpeechError("UNAVAILABLE", "语音服务已关闭。", 503);
239
- if (this.downloadAbort)
240
- throw new SpeechError("DOWNLOAD_BUSY", "已有模型正在下载。", 409);
241
- const abort = new AbortController();
242
- this.downloadAbort = abort;
243
- this.download = { model: model.id, received: 0, total: model.size, phase: "downloading" };
244
- void this.downloadModel(model, abort).finally(() => { if (this.downloadAbort === abort)
245
- this.downloadAbort = null; });
246
- }
247
- async downloadModel(model, abort) {
248
- const destination = this.modelPath(model);
249
- const partial = `${destination}.part`;
250
- const timer = setTimeout(() => abort.abort(), 30 * 60_000);
251
- timer.unref();
252
- try {
253
- await mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
254
- const response = await (this.options.fetch ?? fetch)(speechModelUrl(model), { signal: abort.signal });
255
- if (!response.ok || !response.body)
256
- throw new Error("download");
257
- const declared = response.headers.get("content-length");
258
- if (declared && Number(declared) !== model.size) {
259
- await response.body.cancel();
260
- throw new Error("size");
261
- }
262
- const file = await open(partial, "w", 0o600);
263
- const hash = createHash("sha256");
264
- let received = 0;
265
- try {
266
- for await (const chunk of response.body) {
267
- if (abort.signal.aborted)
268
- throw new Error("cancelled");
269
- received += chunk.length;
270
- if (received > model.size)
271
- throw new Error("size");
272
- hash.update(chunk);
273
- // FileHandle.write may be partial: writeFile writes the entire bounded chunk at current position.
274
- await file.writeFile(chunk);
275
- this.download = { model: model.id, received, total: model.size, phase: "downloading" };
276
- }
277
- }
278
- finally {
279
- await file.close();
280
- }
281
- if (abort.signal.aborted || received !== model.size || hash.digest("hex") !== model.sha256)
282
- throw new Error("integrity");
283
- await rename(partial, destination);
284
- this.verified.delete(model.id);
285
- await this.modelReady(model);
286
- this.download = null;
287
- }
288
- catch {
289
- await rm(partial, { force: true }).catch(() => { });
290
- this.download = { model: model.id, received: 0, total: model.size, phase: "failed", error: "模型下载失败或完整性校验未通过,请检查网络后重试。" };
291
- }
292
- finally {
293
- clearTimeout(timer);
294
- }
295
- }
296
- async transcribe(audio, signal) {
297
- const { silent } = validateSpeechWav(audio);
298
- if (signal?.aborted)
299
- throw new SpeechError("CANCELLED", "识别已取消。", 499);
300
- if (this.disposed)
301
- throw new SpeechError("UNAVAILABLE", "语音服务已关闭。", 503);
302
- if (this.active || this.maintenance)
303
- throw new SpeechError("BUSY", "服务端正在识别或初始化,请稍后重试。", 429);
304
- const abort = new AbortController();
305
- this.active = abort;
306
- const cancel = () => abort.abort();
307
- signal?.addEventListener("abort", cancel, { once: true });
308
- let directory = null;
309
- let temporary = null;
310
- try {
311
- const settings = this.settings();
312
- const model = this.models.find((item) => item.id === settings.model);
313
- const runtime = await this.runtime();
314
- if (!settings.enabled || !runtime.executable || !await this.modelReady(model)) {
315
- throw new SpeechError("NOT_READY", "服务端语音识别未就绪,请先启用并安装运行时、下载模型。", 503);
316
- }
317
- if (settings.acceleration === "gpu" && runtime.backend === "cpu")
318
- throw new SpeechError("NO_GPU", "当前是 CPU 运行时,请改为自动或 CPU。", 503);
319
- if (abort.signal.aborted)
320
- throw new SpeechError("CANCELLED", "识别已取消。", 499);
321
- let backend = settings.acceleration === "cpu" ? "cpu" : runtime.backend;
322
- if (silent)
323
- return { text: "", model: model.id, backend };
324
- temporary = createManagedTemporary("speech-voice", configTemporaryRoot(path.dirname(this.root)));
325
- directory = temporary.directory;
326
- const wav = path.join(directory, "audio.wav");
327
- await writeFile(wav, audio, { mode: 0o600 });
328
- const output = path.join(directory, "transcript");
329
- // Whisper's fixed initial vocabulary biases domain words and Simplified Chinese,
330
- // without rewriting the transcript or reading any private draft/history as context.
331
- const context = settings.language === "en" ? "Wand, code, Git, API."
332
- : "Wand, code, Git, API, 代码, 语音识别, 服务端, 客户端, 任务, 工作区。";
333
- const args = ["-m", this.modelPath(model), "-f", wav, "-of", output, "-otxt", "-nt", "-l", settings.language, "-t", String(settings.threads), "--prompt", context];
334
- const started = Date.now();
335
- const timeout = this.options.timeoutMs ?? 120_000;
336
- try {
337
- await this.run(runtime.executable, [...args, ...(backend === "cpu" ? ["-ng"] : [])], directory, abort.signal, timeout);
338
- }
339
- catch (error) {
340
- if (settings.acceleration !== "auto" || backend === "cpu" || abort.signal.aborted
341
- || !(error instanceof SpeechError) || error.code !== "ENGINE_FAILED")
342
- throw error;
343
- // Only local inference is retried, never a message send. Explicit GPU selection does not fallback.
344
- backend = "cpu";
345
- await rm(`${output}.txt`, { force: true });
346
- const remaining = timeout - (Date.now() - started);
347
- if (remaining <= 0)
348
- throw new SpeechError("TIMEOUT", "服务端识别超时,请选择较小模型。", 504);
349
- await this.run(runtime.executable, [...args, "-ng"], directory, abort.signal, remaining);
350
- }
351
- const resultFile = `${output}.txt`;
352
- if ((await stat(resultFile)).size > 32_768)
353
- throw new SpeechError("INVALID_RESULT", "语音识别结果异常。", 502);
354
- const text = (await readFile(resultFile, "utf8")).trim();
355
- if (text.length > 8_000 || text.includes("\0"))
356
- throw new SpeechError("INVALID_RESULT", "语音识别结果异常。", 502);
357
- if (abort.signal.aborted)
358
- throw new SpeechError("CANCELLED", "识别已取消。", 499);
359
- return { text, model: model.id, backend };
360
- }
361
- catch (error) {
362
- if (error instanceof SpeechError)
363
- throw error;
364
- throw new SpeechError("ENGINE_FAILED", "服务端语音识别失败,请检查运行时、模型与可用磁盘。", 503);
365
- }
366
- finally {
367
- await temporary?.closeAsync();
368
- signal?.removeEventListener("abort", cancel);
369
- if (this.active === abort)
370
- this.active = null;
371
- }
372
- }
373
- run(executable, args, cwd, signal, timeout) {
374
- return new Promise((resolve, reject) => {
375
- if (signal.aborted) {
376
- reject(new SpeechError("CANCELLED", "识别已取消。", 499));
377
- return;
378
- }
379
- const child = spawn(executable, args, { cwd, env: buildChildEnv(false, {
380
- LD_LIBRARY_PATH: systemEnvValue("LD_LIBRARY_PATH"), DYLD_LIBRARY_PATH: systemEnvValue("DYLD_LIBRARY_PATH"),
381
- CUDA_VISIBLE_DEVICES: systemEnvValue("CUDA_VISIBLE_DEVICES"),
382
- }), stdio: "ignore", windowsHide: true });
383
- try {
384
- registerTemporaryChild(cwd, child.pid);
385
- }
386
- catch {
387
- child.on("error", () => { });
388
- child.once("close", () => { reject(new SpeechError("ENGINE_FAILED", "临时文件保护失败。", 503)); });
389
- child.kill("SIGKILL");
390
- return;
391
- }
392
- let failure = null;
393
- let killTimer;
394
- const stop = (error) => {
395
- failure ??= error;
396
- child.kill("SIGTERM");
397
- killTimer ??= setTimeout(() => child.kill("SIGKILL"), 1_000);
398
- killTimer.unref();
399
- };
400
- const cancel = () => stop(new SpeechError("CANCELLED", "识别已取消。", 499));
401
- signal.addEventListener("abort", cancel, { once: true });
402
- const timer = setTimeout(() => stop(new SpeechError("TIMEOUT", "服务端识别超时,请选择较小模型。", 504)), timeout);
403
- timer.unref();
404
- child.once("error", () => { failure = new SpeechError("ENGINE_FAILED", "无法启动服务端语音运行时,请检查安装。", 503); });
405
- child.once("close", (code) => {
406
- signal.removeEventListener("abort", cancel);
407
- clearTimeout(timer);
408
- if (killTimer)
409
- clearTimeout(killTimer);
410
- if (failure)
411
- reject(failure);
412
- else if (code !== 0)
413
- reject(new SpeechError("ENGINE_FAILED", "服务端识别失败,请尝试 CPU 或较小模型。", 502));
414
- else
415
- resolve();
416
- });
417
- });
418
- }
419
- dispose() { this.disposed = true; this.downloadAbort?.abort(); this.active?.abort(); }
420
- }
@@ -1,6 +0,0 @@
1
- export interface SpeechSupport {
2
- supported: boolean;
3
- reason: string | null;
4
- }
5
- /** Fixed, read-only tool probes; never install anything during status polling. */
6
- export declare function speechSupport(runtimeAvailable: boolean): Promise<SpeechSupport>;
@@ -1,24 +0,0 @@
1
- import { systemEnvValue } from "./env-utils.js";
2
- import { runCommand } from "./run-command.js";
3
- function available(command, args = ["--version"]) {
4
- return runCommand(command, args, { timeout: 5000, maxBuffer: 16_384 }).then((result) => result.ok);
5
- }
6
- /** Fixed, read-only tool probes; never install anything during status polling. */
7
- export async function speechSupport(runtimeAvailable) {
8
- if (!["darwin", "linux", "win32"].includes(process.platform)) {
9
- return { supported: false, reason: "服务端语音识别仅支持 macOS、Linux 和 Windows。" };
10
- }
11
- if (runtimeAvailable)
12
- return { supported: true, reason: null };
13
- if (systemEnvValue("WAND_WHISPER_BIN"))
14
- return { supported: false, reason: "配置的语音运行时不可用,请管理员检查 WAND_WHISPER_BIN。" };
15
- const tools = [
16
- ["Git", available("git")], ["CMake", available("cmake")],
17
- ["C++ 编译器", process.platform === "win32" ? available("cl", ["/help"]) : available("c++")],
18
- ...(process.platform === "win32" ? [] : [["Make", available("make")]]),
19
- ];
20
- const results = await Promise.all(tools.map(async ([label, probe]) => await probe ? null : label));
21
- const missing = results.filter(Boolean);
22
- return missing.length ? { supported: false, reason: `缺少可用的 ${missing.join("、")},安装后重新打开此页即可启用。` }
23
- : { supported: true, reason: null };
24
- }
@@ -1,52 +0,0 @@
1
- /** Shared, transport-only speech contract. Never expose executable paths or audio. */
2
- export type SpeechAcceleration = "auto" | "cpu" | "gpu";
3
- export type SpeechBackend = "cpu" | "metal" | "cuda";
4
- export interface SpeechSettings {
5
- enabled: boolean;
6
- model: string;
7
- acceleration: SpeechAcceleration;
8
- language: "auto" | "zh" | "en";
9
- threads: number;
10
- }
11
- export interface SpeechModelStatus {
12
- id: string;
13
- label: string;
14
- description: string;
15
- size: number;
16
- downloaded: boolean;
17
- }
18
- export interface SpeechStatus {
19
- settings: SpeechSettings;
20
- ready: boolean;
21
- reason: string | null;
22
- runtime: {
23
- available: boolean;
24
- backend: SpeechBackend;
25
- platform: string;
26
- arch: string;
27
- };
28
- models: SpeechModelStatus[];
29
- download: {
30
- model: string;
31
- received: number;
32
- total: number;
33
- phase: "downloading" | "failed";
34
- error?: string;
35
- } | null;
36
- maxDurationSeconds: number;
37
- busy: boolean;
38
- initialized?: boolean;
39
- }
40
- export interface SpeechResult {
41
- text: string;
42
- model: string;
43
- backend: SpeechBackend;
44
- originalText?: string;
45
- optimized?: boolean;
46
- optimizationError?: string;
47
- employeeId?: string;
48
- candidate?: number;
49
- }
50
- export declare const SPEECH_SAMPLE_RATE = 16000;
51
- export declare const SPEECH_MAX_SECONDS = 60;
52
- export declare const SPEECH_MAX_WAV_BYTES: number;
@@ -1,3 +0,0 @@
1
- export const SPEECH_SAMPLE_RATE = 16_000;
2
- export const SPEECH_MAX_SECONDS = 60;
3
- export const SPEECH_MAX_WAV_BYTES = 44 + SPEECH_SAMPLE_RATE * 2 * SPEECH_MAX_SECONDS;