dsh-line-bot 0.10.0 → 0.10.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.10.1
2
+
3
+ - 語音:自然語言語氣(instruct)改用 CosyVoice GGUF;MLX 的 instruct 模式會把語氣指令一起念出來,已避免。
4
+ - 語音:MLX 合成加上 `NUMBA_CACHE_DIR`,避免新版 Python 環境第一次執行就失敗。
5
+ - 面板與文件更新語音引擎說明。
6
+
1
7
  ## 0.10.0
2
8
 
3
9
  - 新增語音回覆(CosyVoice3-0.5B):角色可以在回覆最後加 `[[voice]]`,插件會把文字合成語音並用 LINE 語音訊息送出;合成失敗或 reply token 過期時自動改用文字/push。
package/README.en.md CHANGED
@@ -230,6 +230,7 @@ The persona can attach a sticker to a text reply, or reply with just an image. I
230
230
  - The persona can put `[[voice]]` at the end of a reply; the plugin synthesizes the text with CosyVoice3-0.5B and sends a LINE audio message (m4a).
231
231
  - On Apple Silicon macOS it synthesizes locally with MLX; on Windows (or without MLX) it uses CosyVoice GGUF (CrispASR). Press "Set up environment" once in the panel (downloads roughly 1–2 GB).
232
232
  - The "Voice replies" card toggles the feature, requires confirming that you have the rights to the voice, lets you upload a reference clip (the default is a 12–16 s cut of the provided sample), choose instruct/zero-shot mode, and preview the audio.
233
+ - Natural-language style (instruct) uses CosyVoice GGUF; MLX is only used for zero-shot, so the instruction is never read aloud.
233
234
  - Synthesis takes about 20–40 s; if the reply token expires it falls back to push. Audio is stored under `<DSH_HOME>/line-bot/media/` and served at `/media/<token>` through the tunnel.
234
235
  - Requires `ffmpeg`; MLX mode needs Python 3.10+.
235
236
 
package/README.ja.md CHANGED
@@ -233,6 +233,9 @@ Bot がグループ(または複数人のルーム)に追加されたとき
233
233
  - 合成には約 20–40 秒かかります。reply token の期限が切れた場合は push に切り替わります。音声は `<DSH_HOME>/line-bot/media/` に保存され、tunnel の `/media/<token>` で配信されます。
234
234
  - `ffmpeg` が必要です。MLX モードは Python 3.10+ が必要です。
235
235
 
236
+ - 自然言語の話し方(instruct)は CosyVoice GGUF を使用します。MLX はゼロショット専用で、指示が読み上げられるのを防ぎます。
237
+
238
+
236
239
  ## 診断
237
240
  - パネルの「詳細設定 → 診断を実行」でサーバー、Tunnel、公開 URL、webhook、dsh-meme 図庫、画像変換ツール、モデル、最近のエラーを確認できます。「安全な修復」は tunnel 再接続と webhook 再設定だけを行います。
238
241
  - CLI:`npx dsh-line-bot doctor`、`doctor --fix`、`doctor --json`。
package/README.ko.md CHANGED
@@ -231,6 +231,9 @@ Bot이 그룹(또는 다인 룸)에 추가되면:
231
231
  - 합성에는 약 20–40초가 걸립니다. reply token이 만료되면 push로 자동 전환됩니다. 음성은 `<DSH_HOME>/line-bot/media/`에 저장되고 tunnel의 `/media/<token>`으로 제공됩니다.
232
232
  - `ffmpeg`가 필요합니다. MLX 모드는 Python 3.10+가 필요합니다.
233
233
 
234
+ - 자연어 말투(instruct)는 CosyVoice GGUF를 사용합니다. MLX는 제로샷 전용이라 지시문이 읽히지 않습니다.
235
+
236
+
234
237
  ## 진단
235
238
  - 패널의 "고급 설정 → 진단 실행"에서 서버, Tunnel, 공개 URL, webhook, dsh-meme 라이브러리, 이미지 변환 도구, 모델, 최근 오류를 확인할 수 있습니다. "안전 복구"는 tunnel 재연결과 webhook 재설정만 수행합니다.
236
239
  - CLI: `npx dsh-line-bot doctor`, `doctor --fix`, `doctor --json`.
package/README.md CHANGED
@@ -230,6 +230,7 @@ Bot 被加進群組(或多人聊天室)後:
230
230
  - 角色可以在回覆最後加 `[[voice]]`,插件會用 CosyVoice3-0.5B 把文字合成語音,並以 LINE 語音訊息送出(m4a)。
231
231
  - macOS(Apple Silicon)在本機用 MLX 合成;Windows/沒有 MLX 時用 CosyVoice GGUF(CrispASR)。第一次要先在面板按「一鍵配置環境」(會下載模型,約 1–2GB)。
232
232
  - 面板「語音回覆」可開關、確認聲音使用權、上傳參考音(預設用指定音檔的 12–16 秒片段)、選推理模式與語氣指令、試聽。
233
+ - 自然語言語氣(instruct)使用 CosyVoice GGUF;MLX 只用於零樣本模式,避免語氣指令被念出來。
233
234
  - 合成需要約 20–40 秒;若 reply token 過期會自動改用 push。語音檔存在 `<DSH_HOME>/line-bot/media/`,透過 tunnel 的 `/media/<token>` 提供。
234
235
  - 需要 `ffmpeg`;MLX 模式需要 Python 3.10+。
235
236
 
package/README.zh-CN.md CHANGED
@@ -230,6 +230,7 @@ Bot 被加入群组(或多人聊天室)后:
230
230
  - 角色可以在回复最后加 `[[voice]]`,插件会用 CosyVoice3-0.5B 把文字合成语音,并以 LINE 语音消息发送(m4a)。
231
231
  - macOS(Apple Silicon)在本机用 MLX 合成;Windows/没有 MLX 时用 CosyVoice GGUF(CrispASR)。第一次要先在面板按「一键配置环境」(会下载模型,约 1–2GB)。
232
232
  - 面板「语音回复」可开关、确认声音使用权、上传参考音频(默认用指定音频的 12–16 秒片段)、选择推理模式与语气指令、试听。
233
+ - 自然语言语气(instruct)使用 CosyVoice GGUF;MLX 只用于零样本模式,避免语气指令被念出来。
233
234
  - 合成需要约 20–40 秒;若 reply token 过期会自动改用 push。语音文件存在 `<DSH_HOME>/line-bot/media/`,通过 tunnel 的 `/media/<token>` 提供。
234
235
  - 需要 `ffmpeg`;MLX 模式需要 Python 3.10+。
235
236
 
@@ -38,6 +38,9 @@ npx dsh-line-bot doctor --fix # 安全修復:重連 tunnel、重設 webhook
38
38
  - 合成約需 20–40 秒;reply token 過期時會自動改用 push。日誌看 `語音回覆`、`語音合成失敗`。
39
39
  - 模型只有在語言上下文有語音提示時才會加 `[[voice]]`;可先用「測試 Agent 回覆」確認。
40
40
 
41
+ ### 語音把語氣指令念出來
42
+ - 舊版 MLX 的 instruct 模式會把語氣描述一起念;0.10.1 起 instruct 一律走 CosyVoice GGUF。升級後重新產生一次語音即可,舊音檔不會變。
43
+
41
44
  ### APNG 不會動
42
45
  - LINE 不播放 APNG,只顯示第一幀。保持 `stickers.apng=false` 即可。
43
46
 
package/client.js CHANGED
@@ -1415,10 +1415,10 @@ window.__ModuleLoader__.load({
1415
1415
  h('div', { style: S.row },
1416
1416
  h(Badge, { ok: voiceStatus.ready === true }, voiceStatus.ready ? '語音環境就緒' : '尚未就緒'),
1417
1417
  h(Badge, { ok: voiceCfg.consent === true }, voiceCfg.consent ? '已確認使用權' : '未確認使用權'),
1418
- h(Badge, { ok: true }, voiceStatus.mlx?.ok ? 'macOS MLX' : voiceStatus.gguf?.ok ? 'GGUF' : '未安裝引擎'),
1418
+ h(Badge, { ok: true }, voiceStatus.effectiveProvider === 'gguf' ? 'GGUF' : voiceStatus.effectiveProvider === 'mlx' ? 'MLX' : '未安裝引擎'),
1419
1419
  h(Badge, { ok: !!voiceStatus.reference?.path }, voiceStatus.reference?.path ? ('參考音 ' + (voiceStatus.reference.seconds || 0) + ' 秒') : '沒有參考音'),
1420
1420
  ),
1421
- h('div', { style: S.hint }, 'macOS 會用 MLX(較快);Windows 或沒有 MLX 時用 CosyVoice GGUF。合成一次約需 20–40 秒,送出時若 reply token 過期會自動改用 push。'),
1421
+ h('div', { style: S.hint }, '自然語言語氣(instruct)一律用 CosyVoice GGUF,避免 MLX 把語氣指令念出來;零樣本模式可用 MLX。合成一次約需 20–40 秒,reply token 過期會自動改用 push。'),
1422
1422
  h(Toggle, { checked: voiceCfg.enabled === true, onChange: (value) => update({ voice: { enabled: value } }), label: '開啟語音回覆', hint: '開啟後角色可以自己決定用語音;關閉時完全不合成。' }),
1423
1423
  h(Toggle, { checked: voiceCfg.consent === true, onChange: (value) => update({ voice: { consent: value } }), label: '我確認有使用此聲音的權利', hint: '使用他人聲音做克隆前請先取得同意;確認後才會合成。' }),
1424
1424
  h('div', { style: S.row },
package/lib/host.mjs CHANGED
@@ -51,7 +51,7 @@ const { createVision } = visionMod
51
51
  const { createTts } = ttsMod
52
52
  const { chunkText, clip, inQuietWindow, jsonFail, jsonOk, maskSecret, readJsonBody, safeArray, sleep, readJsonSafe } = utilMod
53
53
 
54
- export const VERSION = '0.10.0'
54
+ export const VERSION = '0.10.1'
55
55
 
56
56
  // 環境變數提供的 Cloudflare API Token(方便用 shell 注入、不用把 token 寫進設定檔)。
57
57
  // 設定檔裡的 token 優先;兩者都沒有時才用環境變數。
package/lib/tts.mjs CHANGED
@@ -218,13 +218,25 @@ export function createTts({ dir, logger, getConfig }) {
218
218
  },
219
219
  reference: ref,
220
220
  setup: state.setup,
221
- ready: (preferred === 'mlx' ? mlxReady() : ggufReady()) && !!ref.path,
221
+ ready: (mlxReady() || ggufReady()) && !!ref.path,
222
222
  }
223
223
  }
224
224
 
225
+ function providerFor(cfg = cfgVoice()) {
226
+ const detected = detect()
227
+ const instruct = String(cfg.cloneMode || 'instruct') !== 'zero-shot'
228
+ if (instruct) return detected.gguf.ok ? 'gguf' : ''
229
+ return detected.mlx.ok ? 'mlx' : detected.gguf.ok ? 'gguf' : ''
230
+ }
231
+
225
232
  function resolveProvider(cfg = cfgVoice()) {
226
233
  const detected = detect()
227
234
  const wanted = String(cfg.provider || 'auto')
235
+ const instruct = String(cfg.cloneMode || 'instruct') !== 'zero-shot'
236
+ if (instruct) {
237
+ if (detected.gguf.ok) return 'gguf'
238
+ throw new Error('自然語言語氣(instruct)模式需要 GGUF 引擎;MLX 的 instruct 會把指示念出來。請按「一鍵配置環境」。')
239
+ }
228
240
  if (wanted === 'mlx') {
229
241
  if (!detected.mlx.ok) throw new Error('MLX 語音環境還沒安裝完成,請先按「一鍵配置環境」')
230
242
  return 'mlx'
@@ -293,7 +305,7 @@ export function createTts({ dir, logger, getConfig }) {
293
305
  const pipBase = uv ? [uv, 'pip', 'install', '--python', paths.mlxPython] : [paths.mlxPython, '-m', 'pip', 'install']
294
306
  setSetup('pip', '安裝 MLX 語音套件(mlx-audio-plus)')
295
307
  const packages = ['mlx-audio-plus==0.1.7', 'mlx-lm==0.29.1', 'librosa', 'pyloudnorm', 'soundfile']
296
- await run(pipBase[0], [...pipBase.slice(1), ...packages], { timeout: 1800000, env: { ...process.env, HF_HOME: paths.hfHome } })
308
+ await run(pipBase[0], [...pipBase.slice(1), ...packages], { timeout: 1800000, env: { ...process.env, HF_HOME: paths.hfHome, NUMBA_CACHE_DIR: path.join(paths.root, 'numba') } })
297
309
 
298
310
  if (!mlxModelCached()) {
299
311
  setSetup('model', '下載 CosyVoice3-0.5B 模型(約 1.4GB,第一次會比較久)')
@@ -303,7 +315,7 @@ export function createTts({ dir, logger, getConfig }) {
303
315
  ].join('\n')
304
316
  await run(paths.mlxPython, ['-c', downloader], {
305
317
  timeout: 3600000,
306
- env: { ...process.env, HF_HOME: paths.hfHome },
318
+ env: { ...process.env, HF_HOME: paths.hfHome, NUMBA_CACHE_DIR: path.join(paths.root, 'numba') },
307
319
  })
308
320
  } else {
309
321
  setSetup('model', 'MLX 模型已下載')
@@ -398,10 +410,18 @@ export function createTts({ dir, logger, getConfig }) {
398
410
  }
399
411
  await ensureDefaultReference()
400
412
  const env = { ...process.env, HF_HOME: paths.hfHome }
401
- setSetup('test', '測試語音合成')
402
- await synthesize('主人,語音功能測試成功。', { forceProvider: provider, env, skipConsent: true })
403
- setSetup('done', `環境已就緒(${provider === 'mlx' ? 'macOS MLX' : 'CosyVoice GGUF'})`)
404
- return { started: true, provider }
413
+ if (detected.platform === 'darwin' && provider !== 'gguf') {
414
+ try {
415
+ await installMacGguf()
416
+ } catch (error) {
417
+ logger.warn(`GGUF 備援引擎安裝失敗:${String(error?.message || error).slice(0, 200)}`)
418
+ }
419
+ }
420
+ setSetup('test', '測試語音合成(instruct 模式會用 GGUF 引擎)')
421
+ await synthesize('主人,語音功能測試成功。', { env, skipConsent: true })
422
+ const finalProvider = providerFor() || provider
423
+ setSetup('done', `環境已就緒(${finalProvider === 'mlx' ? 'macOS MLX' : 'CosyVoice GGUF'})`)
424
+ return { started: true, provider: finalProvider }
405
425
  } catch (error) {
406
426
  setSetup('error', '環境安裝失敗', error)
407
427
  throw error
@@ -423,6 +443,11 @@ export function createTts({ dir, logger, getConfig }) {
423
443
  const cfg = cfgVoice()
424
444
  if (!skipConsent && cfg.consent !== true) throw new Error('尚未確認聲音使用權利(面板:語音回覆 → 我確認有使用權)')
425
445
  if (!skipConsent && cfg.enabled === false) throw new Error('語音功能未開啟')
446
+ if (String(cfg.cloneMode || 'instruct') !== 'zero-shot' && !ggufReady()) {
447
+ setSetup('engine', '下載 GGUF 引擎(自然語言語氣模式需要)')
448
+ if (isWin()) await installWinGguf()
449
+ else await installMacGguf()
450
+ }
426
451
  const provider = forceProvider || resolveProvider(cfg)
427
452
  const ref = referencePath()
428
453
  if (!ref) throw new Error('尚未設定參考音')
@@ -451,7 +476,7 @@ export function createTts({ dir, logger, getConfig }) {
451
476
  fs.writeFileSync(jobFile, JSON.stringify(payload), { mode: 0o600 })
452
477
  const { stdout } = await run(paths.mlxPython, [paths.mlxScript, jobFile], {
453
478
  timeout: 600000,
454
- env: { ...env, HF_HOME: paths.hfHome },
479
+ env: { ...env, HF_HOME: paths.hfHome, NUMBA_CACHE_DIR: path.join(paths.root, 'numba') },
455
480
  })
456
481
  const line = String(stdout || '').split(/\r?\n/).reverse().find((item) => item.startsWith('RESULT:'))
457
482
  if (!line) throw new Error(`MLX 沒有回傳結果:${String(stdout).slice(-300)}`)
@@ -524,6 +549,7 @@ export function createTts({ dir, logger, getConfig }) {
524
549
  installing: state.installing,
525
550
  last: state.last,
526
551
  lastError: state.lastError,
552
+ effectiveProvider: (() => { try { return providerFor() } catch { return '' } })(),
527
553
  mlxScript: fileExists(paths.mlxScript),
528
554
  lastSample: (() => {
529
555
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-line-bot",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "把 LINE 官方帳號(Messaging API)接進 DeepSeek Harness 的插件:內建 Cloudflare Tunnel、人設選擇(大肥魚或插件自訂)、主動傳訊與完整設定面板。",
5
5
  "type": "module",
6
6
  "main": "index.js",