@goodandready/dsh-tts 0.4.6 → 0.4.8

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.
@@ -263,7 +263,8 @@
263
263
  onChange: (e) => set(r, { chime: e.target.value }),
264
264
  }),
265
265
  React.createElement('input', {
266
- className: 'dts-input dts-grow', placeholder: 'ssml',
266
+ className: 'dts-input dts-grow', placeholder: t('ssmlStyle') || 'ssml',
267
+ title: t('ssmlStyleHint') || 'SSML tone style',
267
268
  value: (value[r] && value[r].ssmlStyle) || '', disabled: !props.writable,
268
269
  onChange: (e) => set(r, { ssmlStyle: e.target.value }),
269
270
  }),
@@ -293,6 +293,7 @@
293
293
  ),
294
294
  React.createElement('div', { className: 'dts-block' },
295
295
  React.createElement('div', { className: 'dts-h' }, t('rolesTitle')),
296
+ boolField('autoDetectSubagent', t('autoDetectSubagent'), t('autoDetectSubagent')),
296
297
  React.createElement(RolesEditor, {
297
298
  t: t, writable: writable, onPreview: onPreview,
298
299
  value: draft && draft.roles ? draft.roles : {},
@@ -53,6 +53,10 @@
53
53
  type: 'button', className: 'dts-link', title: '★',
54
54
  onClick: () => { recent.toggleFav(text); force() },
55
55
  }, starred ? '★' : '☆'),
56
+ React.createElement('button', {
57
+ type: 'button', className: 'dts-link', title: t('exportAudio') || 'Export',
58
+ onClick: () => exportAudioClip(text),
59
+ }, '⤓'),
56
60
  )
57
61
  return React.createElement('div', { style: { position: 'relative', display: 'inline-flex', gap: '4px', alignItems: 'center' } },
58
62
  React.createElement('button', {
@@ -81,6 +85,12 @@
81
85
  title: t('dockStop'),
82
86
  onClick: stopPlayback,
83
87
  }, '■'),
88
+ React.createElement('button', {
89
+ type: 'button',
90
+ className: 'dts-link',
91
+ title: t('exportAudio') || 'Export clip',
92
+ onClick: () => exportAudioClip(player.lastText),
93
+ }, '⤓'),
84
94
  )
85
95
  }
86
96
 
@@ -121,7 +131,7 @@
121
131
  }
122
132
  }
123
133
  ctx.effect(() => {
124
- const undo = [addLocale('en', en), addLocale('ru', ru)]
134
+ const undo = [addLocale('en', en), addLocale('zh', zh)]
125
135
  return () => { for (const off of undo) off() }
126
136
  }, 'dsh-tts: locales')
127
137
  // The sidebar draws the section label, not our component: props.t never
package/lib/client.js CHANGED
@@ -280,6 +280,7 @@ window.__ModuleLoader__.load({
280
280
  const { audio, url } = item.prepared
281
281
  audio.playbackRate = Math.max(0.5, Math.min(2, player.rate || 1))
282
282
  player.audio = audio
283
+ if (item.text) player.lastText = item.text
283
284
  playerChanged()
284
285
  prebufferNext()
285
286
  let doneCalled = false
@@ -461,11 +462,57 @@ window.__ModuleLoader__.load({
461
462
  .catch(() => { /* chime fetch failed or not found */ })
462
463
  }
463
464
 
465
+ function exportAudioClip(text) {
466
+ let phrase = text || player.lastText
467
+ if (!phrase) {
468
+ const list = recent.list()
469
+ if (list && list.items && list.items.length) phrase = list.items[0]
470
+ }
471
+ if (!phrase) return
472
+ fetch('/dsh-tts/speak', {
473
+ method: 'POST',
474
+ headers: { 'Content-Type': 'application/json' },
475
+ body: JSON.stringify({ text: phrase }),
476
+ })
477
+ .then((r) => r.json())
478
+ .then((d) => {
479
+ if (!d || !d.ok || !d.audioBase64) return
480
+ const bin = atob(d.audioBase64)
481
+ const bytes = new Uint8Array(bin.length)
482
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
483
+ const mime = d.mime || 'audio/mpeg'
484
+ const blob = new Blob([bytes], { type: mime })
485
+ const url = URL.createObjectURL(blob)
486
+ const a = document.createElement('a')
487
+ a.href = url
488
+ a.download = 'dsh-tts-' + Date.now() + (mime.indexOf('wav') !== -1 ? '.wav' : '.mp3')
489
+ document.body.appendChild(a)
490
+ a.click()
491
+ document.body.removeChild(a)
492
+ setTimeout(() => { try { URL.revokeObjectURL(url) } catch {} }, 2000)
493
+ })
494
+ .catch(() => {})
495
+ }
496
+
464
497
  function credOf(map, provider) {
465
498
  return (map && map[provider]) || { configured: false, writable: true, ref: '' }
466
499
  }
467
500
 
468
501
  const en = {
502
+ 'dockLabel': 'Speech',
503
+ 'dockPause': 'Pause',
504
+ 'dockResume': 'Resume speech',
505
+ 'dockStop': 'Stop speech',
506
+ 'dockCurrent': 'Now speaking',
507
+ 'dockNone': 'Waiting for speech',
508
+ 'dockQueue': 'Queue',
509
+ 'dockClearQueue': 'Clear queue',
510
+ 'dockReplay': 'Replay',
511
+ 'dockFavorite': 'Favorite',
512
+ 'dockUnfavorite': 'Unfavorite',
513
+ 'dockFavorites': 'Favorite clips',
514
+ 'exportAudio': 'Export audio clip',
515
+ 'exportSuccess': 'Audio exported',
469
516
  'statusOnline': 'Host online',
470
517
  'recent': 'Recent',
471
518
  'recentEmpty': 'No recent speech yet',
@@ -484,12 +531,10 @@ window.__ModuleLoader__.load({
484
531
  'queueLen': 'Queue',
485
532
  'sseState': 'SSE',
486
533
  'currentVoice': 'Voice',
487
-
488
534
  'statusOffline': 'Host unreachable',
489
535
  'cacheStats': 'Cache',
490
-
491
536
  'general': 'General',
492
- 'loading': 'Loading\u2026',
537
+ 'loading': 'Loading…',
493
538
  'chainHintCard': 'Pick a provider, paste its API key, and Save (or leave the key field). The key is written to the host credentials store immediately. The browser never reads it back.',
494
539
  'speakRepliesHint': 'When on, each finished reply is synthesized on the host and played in this browser. Audio is not sent back to the model.',
495
540
  'chainHint': 'Top to bottom is the fallback order. Cloud keys are saved to the host credentials store, never into plugin settings.',
@@ -571,6 +616,8 @@ window.__ModuleLoader__.load({
571
616
  'roleReply': 'Reply',
572
617
  'roleApproval': 'Approval',
573
618
  'roleError': 'Error',
619
+ 'ssmlStyle': 'SSML tone style',
620
+ 'ssmlStyleHint': 'Expressive tone (e.g. cheerful, serious, empathetic).',
574
621
  'longReply': 'Long replies',
575
622
  'longReplyHint': 'truncate: cut at the limit; summarize: short retelling; full: read everything.',
576
623
  'summaryModel': 'Summary model',
@@ -583,9 +630,8 @@ window.__ModuleLoader__.load({
583
630
  'statHits': 'Cache hits',
584
631
  'statErrors': 'Errors',
585
632
  'statReset': 'Reset stats',
586
-
587
633
  'autoDetect': 'Auto language per piece',
588
- 'autoDetectHint': 'Guess ru/en per spoken piece instead of the language setting.',
634
+ 'autoDetectHint': 'Guess zh/en per spoken piece instead of the language setting.',
589
635
  'narrateQuotesOnly': 'Only quotes',
590
636
  'narrateQuotesHint': 'Read only «quoted» fragments of a reply.',
591
637
  'skipActions': 'Skip *actions*',
@@ -596,7 +642,6 @@ window.__ModuleLoader__.load({
596
642
  'customBaseUrlHint': 'OpenAI-compatible origin, e.g. http://localhost:8880/v1; empty = off.',
597
643
  'customKeyEnv': 'Custom key env',
598
644
  'customKeyEnvHint': 'Credential name for the custom endpoint bearer key.',
599
-
600
645
  'localEnginesTitle': 'Local Engines (Offline)',
601
646
  'localEnginesHint': 'Run speech synthesis 100% offline without API keys.',
602
647
  'enableLocalEngines': 'Enable local engines',
@@ -613,7 +658,6 @@ window.__ModuleLoader__.load({
613
658
  'installKokoro': 'Install (350 MB)',
614
659
  'installF5': 'Install (2.1 GB)',
615
660
  'deleteModel': 'Delete',
616
-
617
661
  'voiceDuplexTitle': 'Voice Duplex (Voice-to-Voice)',
618
662
  'voiceDuplexHint': 'Interactive voice conversation integrated with dsh-voice.',
619
663
  'voiceNotInstalledTitle': 'Required plugin @goodandready/dsh-voice is not installed',
@@ -624,208 +668,207 @@ window.__ModuleLoader__.load({
624
668
  'vadBargeInHint': 'Mute speech synthesis immediately when user voice is detected.',
625
669
  'newSubagentRole': 'Subagent name (e.g. coder, reviewer)',
626
670
  'addSubagentRole': 'Add Subagent',
671
+ 'autoDetectSubagent': 'Auto-detect subagent roles',
627
672
  'role_coder': 'Coder',
628
673
  'role_reviewer': 'Reviewer',
629
674
  'role_planner': 'Planner',
630
-
631
675
  'enableItDictionary': 'Built-in IT dictionary',
632
676
  'enableItDictionaryHint': 'Auto-correct pronunciation for SQL, Nginx, K8s, Docker, API, JSON, YAML, etc.',
633
677
  'loadItDictionary': 'Populate IT terms',
634
678
  'previewRule': 'Listen',
635
-
636
679
  'messengerIntegrationTitle': 'Messenger Integration (Telegram, Discord)',
637
680
  'messengerIntegrationHint': 'Voice replies and notes sent through @goodandready/dsh-messenger-gateway.',
638
681
  'messengerNotInstalledTitle': 'Required plugin @goodandready/dsh-messenger-gateway is not installed',
639
- 'messengerInstallHint': 'To enable voice notes in messengers, install the plugin with',
682
+ 'messengerInstallHint': 'To enable voice replies in messengers, install the plugin with',
640
683
  'messengerTtsEnabled': 'Voice replies in messengers',
641
- 'messengerTtsEnabledHint': 'Allow messenger bridge to synthesize voice notes for agent replies.',
642
-
643
- 'dockLabel': 'Speech',
644
- 'dockPause': 'Pause',
645
- 'dockResume': 'Resume speech',
646
- 'dockStop': 'Stop speech',
647
- }
648
- const ru = {
649
- 'statusOnline': 'Хост на связи',
650
- 'recent': 'Недавние',
651
- 'recentEmpty': 'Пока нет реплик',
652
- 'retry': 'Повторить',
653
- 'advancedCollapsed': 'Дополнительно',
654
- 'confirmClearCache': 'Очистить кэш синтеза? Действие необратимо.',
655
- 'confirmDeleteModel': 'Удалить скачанные веса движка?',
656
- 'autoplayBlocked': 'Звук заблокирован — один раз кликните или коснитесь экрана',
657
- 'enableSound': 'Включить звук',
658
- 'runtimeNotBundled': 'Рантайм нейросинтеза не входит в пакет',
659
- 'runtimeNotBundledHint': 'Установка весов не включает речь. Для offline используйте Edge, Piper или eSpeak.',
660
- 'testProvider': 'Проверить провайдера',
661
- 'breakerOpen': 'Переключатель открыт',
662
- 'lastError': 'Последняя ошибка',
663
- 'activeProvider': 'Активный провайдер',
664
- 'queueLen': 'Очередь',
665
- 'sseState': 'SSE',
666
- 'currentVoice': 'Голос',
667
-
668
- 'statusOffline': 'Хост недоступен',
669
- 'cacheStats': 'Кэш',
670
-
671
- 'dockLabel': 'Чтение',
672
- 'dockPause': 'Пауза',
673
- 'dockResume': 'Продолжить чтение',
674
- 'dockStop': 'Прекратить чтение',
675
-
676
- 'general': 'Общее',
677
- 'loading': 'Загрузка…',
678
- 'chainHintCard': 'Выберите провайдера, вставьте ключ и нажмите «Сохранить» (или просто уйдите из поля). Ключ сразу попадает в хранилище учётных данных харнесса; браузер его обратно не читает.',
679
- 'speakRepliesHint': 'Каждый законченный ответ озвучивается на стороне харнесса и играет в этом браузере. Звук модели не отправляется.',
680
- 'chainHint': 'Порядок сверху вниз — порядок отката. Ключи облачных провайдеров попадают в хранилище учётных данных харнесса, а не в настройки плагина.',
681
- 'title': 'Озвучка',
682
- 'speakReplies': 'Читать ответы агента',
683
- 'speakAsItGoes': 'Читать по ходу',
684
- 'speakAsItGoesHint': 'Читать каждый ответ, как только он пришёл, предложениями, не дожидаясь конца хода.',
685
- 'rate': 'Скорость чтения',
686
- 'rateHint': 'От 0.5 до 2. Синтез не меняется — быстрее или медленнее играет браузер.',
687
- 'bargeIn': 'Замолкать, когда вы заговорили',
688
- 'bargeInHint': 'Прекращать чтение, как только открылся микрофон.',
689
- 'announce': 'Объявлять запрос подтверждения',
690
- 'announceHint': 'Говорить вслух и подавать сигнал, когда агент остановился ради подтверждения или задал вопрос.',
691
- 'approvalText': 'Что говорить о подтверждении',
692
- 'approvalHint': 'Произносится при запросе подтверждения; имя инструмента добавляется следом.',
693
- 'questionText': 'Что говорить о вопросе',
694
- 'questionHint': 'Произносится, когда агент задал вопрос. Пусто — не произносить.',
695
- 'chime': 'Сигнал',
696
- 'chimeHint': 'ding, beep или none.',
697
- 'language': 'Язык',
698
- 'languageHint': 'Подсказка для голосов, которым нужен язык (eSpeak, Azure SSML).',
699
- 'piperModel': 'Piper: путь к модели',
700
- 'piperModelHint': 'Абсолютный путь к модели ONNX. Пусто — Piper пропускается.',
701
- 'piperBin': 'Piper: бинарь',
702
- 'binHint': 'Ищется в PATH, если путь не абсолютный.',
703
- 'espeakBin': 'eSpeak: бинарь',
704
- 'espeakHint': 'Обычно espeak-ng.',
705
- 'edgeBin': 'edge-tts: бинарь',
706
- 'edgeHint': 'Консольная утилита из пакета edge-tts.',
707
- 'azureRegion': 'Azure: регион',
708
- 'azureHint': 'Нужен провайдеру azure, например eastus.',
709
- 'openaiBaseUrl': 'OpenAI base URL',
710
- 'openaiBaseUrlHint': 'OpenAI-совместимый origin для провайдера openai.',
711
- 'mimoBaseUrl': 'MiMo base URL',
712
- 'mimoBaseUrlHint': 'Корень API MiMo; синтез идёт через chat endpoint.',
713
- 'mimoFormat': 'Формат MiMo',
714
- 'mimoFormatHint': 'mp3 или wav.',
715
- 'minimaxBin': 'MiniMax CLI',
716
- 'minimaxBinHint': 'Ищется в PATH, если путь не абсолютный; вход в CLI — отдельно.',
717
- 'advancedTitle': 'Дополнительно',
718
- 'advancedHint': 'Лимиты и override endpoint. API-ключи — в редакторе цепочки; имена credential-слотов (*KeyEnv) — только в конфиге.',
719
- 'maxChars': 'Максимум символов',
720
- 'maxCharsHint': 'Жёсткий лимит до обработки длинных ответов; 0 — без лимита.',
721
- 'sentenceChars': 'Размер куска (символы)',
722
- 'sentenceCharsHint': 'Максимум символов в streaming-куске предложения.',
723
- 'timeoutMs': 'Таймаут провайдера (мс)',
724
- 'timeoutMsHint': 'Прервать попытку провайдера через столько миллисекунд.',
725
- 'maxQueue': 'Длина очереди',
726
- 'maxQueueHint': 'Отбрасывать самые старые куски сверх этого числа.',
727
- 'chainTitle': 'Цепочка провайдеров',
728
- 'addProvider': 'Добавить провайдера',
729
- 'localProvider': 'Локальный провайдер — ключ не нужен',
730
- 'configured': 'Ключ задан',
731
- 'notSet': 'Ключа нет',
732
- 'fromEnv': 'Задан в окружении',
733
- 'clear': 'Убрать',
734
- 'remove': 'Удалить',
735
- 'up': 'Выше',
736
- 'down': 'Ниже',
737
- 'save': 'Сохранить',
738
- 'saved': 'Сохранено',
739
- 'cardHint': 'Цепочка провайдеров, голоса и параметры чтения.',
740
- 'skipCode': 'Пропускать код',
741
- 'skipCodeHint': 'Вместо чтения кода вслух — короткая пометка.',
742
- 'cache': 'Кэш синтеза',
743
- 'cacheHint': 'Повторяющиеся фразы не синтезируются заново.',
744
- 'cacheMaxMb': 'Лимит кэша, МБ',
745
- 'cacheMaxMbHint': 'Сначала вытесняется самое давнее по использованию.',
746
- 'clearCache': 'Очистить кэш',
747
- 'pronTitle': 'Произношение',
748
- 'pronHint': 'Правила работают сверху вниз; /…/ в левой части — регулярка; Язк ограничивает правило.',
749
- 'pronFrom': 'Заменить',
750
- 'pronTo': 'На',
751
- 'pronWhole': 'Целое слово',
752
- 'pronLang': 'Язк',
753
- 'addRule': 'Добавить правило',
754
- 'rolesTitle': 'Голоса ролей',
755
- 'rolesHint': 'Пустые поля наследуют основную цепочку.',
756
- 'roleReply': 'Ответ',
757
- 'roleApproval': 'Подтверждение',
758
- 'roleError': 'Ошибка',
759
- 'longReply': 'Длинные ответы',
760
- 'longReplyHint': 'truncate: обрезать; summarize: короткий пересказ; full: читать целиком.',
761
- 'summaryModel': 'Модель пересказа',
762
- 'summaryModelHint': 'provider/model, пусто — как у разговора.',
763
- 'summarySentences': 'Длина пересказа',
764
- 'summarySentencesHint': 'Предложений в озвучиваемом пересказе.',
765
- 'preview': 'Проба',
766
- 'statsTitle': 'Статистика',
767
- 'statTotal': 'Всего',
768
- 'statHits': 'Кэш-хиты',
769
- 'statErrors': 'Ошибки',
770
- 'statReset': 'Сбросить',
771
-
772
- 'autoDetect': 'Автоязык по куску',
773
- 'autoDetectHint': 'Подсказывать провайдеру ru/en по каждому куску вместо общей настройки.',
774
- 'narrateQuotesOnly': 'Только цитаты',
775
- 'narrateQuotesHint': 'Читать только фрагменты в кавычках.',
776
- 'skipActions': 'Пропускать *действия*',
777
- 'skipActionsHint': 'Вырезать блоки в звёздочках.',
778
- 'removeRegex': 'Регулярка вырезания',
779
- 'removeRegexHint': 'Глобальная регулярка: совпадения удаляются перед синтезом.',
780
- 'customBaseUrl': 'Свой TTS endpoint',
781
- 'customBaseUrlHint': 'OpenAI-compatible base URL, e.g. http://localhost:8880/v1; empty disables it.',
782
- 'customKeyEnv': 'Имя ключа для endpoint',
783
- 'customKeyEnvHint': 'Имя credential для Bearer-ключа своего endpointа.',
784
-
785
- 'localEnginesTitle': 'Локальные движки (Offline)',
786
- 'localEnginesHint': 'Автономный синтез без API-ключей и интернета.',
787
- 'enableLocalEngines': 'Включить локальные движки',
788
- 'enableLocalEnginesHint': 'Использовать Kokoro или F5-TTS при наличии моделей.',
789
- 'streamingEnabled': 'Мгновенный потоковый звук (< 300 мс)',
790
- 'streamingEnabledHint': 'Воспроизводить звук через AudioWorklet и SSE без задержек.',
791
- 'useKokoro': 'Использовать Kokoro-82M (CPU)',
792
- 'useKokoroHint': 'Быстрый нейросинтез на процессоре.',
793
- 'useF5': 'Использовать F5-TTS (GPU)',
794
- 'useF5Hint': 'Высокоточный синтез на видеокарте NVIDIA.',
795
- 'installed': 'Установлена',
796
- 'notInstalled': 'Не установлена',
797
- 'downloading': 'Загрузка',
798
- 'installKokoro': 'Установить (350 МБ)',
799
- 'installF5': 'Установить (2.1 ГБ)',
800
- 'deleteModel': 'Удалить',
801
-
802
- 'voiceDuplexTitle': 'Голосовой диалог (Voice-to-Voice)',
803
- 'voiceDuplexHint': 'Интерактивный голосовой разговор при наличии плагина dsh-voice.',
804
- 'voiceNotInstalledTitle': 'Плагин @goodandready/dsh-voice не установлен',
805
- 'voiceInstallHint': 'Для включения голосового диалога установите плагин командой',
806
- 'voiceDuplex': 'Voice-to-Voice режим',
807
- 'voiceDuplexHintToggle': 'Автоматически отвечать голосом после завершения голосовой реплики.',
808
- 'vadBargeIn': 'VAD Barge-in',
809
- 'vadBargeInHint': 'Мгновенно глушить речь ассистента при обнаружении голоса пользователя.',
810
- 'newSubagentRole': 'Имя субагента (например, coder, reviewer)',
811
- 'addSubagentRole': 'Добавить субагента',
812
- 'role_coder': 'Кодер',
813
- 'role_reviewer': 'Ревьюер',
814
- 'role_planner': 'Планировщик',
815
-
816
- 'enableItDictionary': 'Предустановленный словарь IT-терминов',
817
- 'enableItDictionaryHint': 'Автоматически заменять SQL, Nginx, K8s, Docker, API, JSON, YAML и др. на правильное произношение.',
818
- 'loadItDictionary': 'Вставить IT-термины в таблицу',
819
- 'previewRule': 'Прослушать',
820
-
821
- 'messengerIntegrationTitle': 'Интеграция с мессенджерами (Telegram, Discord)',
822
- 'messengerIntegrationHint': 'Озвучивание ответов и голосовые заметки через @goodandready/dsh-messenger-gateway.',
823
- 'messengerNotInstalledTitle': 'Плагин @goodandready/dsh-messenger-gateway не установлен',
824
- 'messengerInstallHint': 'Для отправки голосовых сообщений в мессенджеры установите плагин командой',
825
- 'messengerTtsEnabled': 'Голосовые ответы в мессенджеры',
826
- 'messengerTtsEnabledHint': 'Разрешить мосту мессенджеров озвучивать ответы голосовыми заметками.',
684
+ 'messengerTtsEnabledHint': 'Allow the messenger gateway to deliver replies as voice notes.',
827
685
  }
828
686
 
687
+ const zh = {
688
+ 'dockLabel': '语音朗读',
689
+ 'dockPause': '暂停朗读',
690
+ 'dockResume': '继续朗读',
691
+ 'dockStop': '停止朗读',
692
+ 'dockCurrent': '当前朗读',
693
+ 'dockNone': '等待语音',
694
+ 'dockQueue': '队列',
695
+ 'dockClearQueue': '清空队列',
696
+ 'dockReplay': '重新朗读',
697
+ 'dockFavorite': '收藏',
698
+ 'dockUnfavorite': '取消收藏',
699
+ 'dockFavorites': '收藏语音',
700
+ 'exportAudio': '导出语音片段',
701
+ 'exportSuccess': '音频已导出',
702
+ 'statusOnline': '服务在线',
703
+ 'recent': '最近朗读',
704
+ 'recentEmpty': '暂无朗读记录',
705
+ 'retry': '重试',
706
+ 'advancedCollapsed': '高级设置',
707
+ 'confirmClearCache': '确定清除语音合成缓存吗?此操作无法撤销。',
708
+ 'confirmDeleteModel': '确定删除该引擎已下载的模型权重吗?',
709
+ 'autoplayBlocked': '声音已受系统限制,请点击任意位置以激活音频',
710
+ 'enableSound': '开启声音',
711
+ 'runtimeNotBundled': '未捆绑神经推理运行时',
712
+ 'runtimeNotBundledHint': '仅下载权重无法直接合成。建议使用 Edge、Piper 或 eSpeak 进行离线语音合成。',
713
+ 'testProvider': '测试服务商',
714
+ 'breakerOpen': '熔断中',
715
+ 'lastError': '最近错误',
716
+ 'activeProvider': '当前服务商',
717
+ 'queueLen': '队列长度',
718
+ 'sseState': 'SSE状态',
719
+ 'currentVoice': '发音人',
720
+ 'statusOffline': '服务不可达',
721
+ 'cacheStats': '缓存统计',
722
+ 'general': '常规设置',
723
+ 'loading': '加载中…',
724
+ 'chainHintCard': '选择服务商,粘贴 API Key 并保存。密钥将直接写入服务端安全凭证库,前端不会回显。',
725
+ 'speakRepliesHint': '启用后,智能体回复将在服务端合成语音并在当前浏览器中播放。音频不会回传至模型。',
726
+ 'chainHint': '从上到下为降级候选顺序。云端密钥直接保存在服务端凭证中心,绝不存入插件公开设置中。',
727
+ 'title': '语音朗读 (TTS)',
728
+ 'speakReplies': '朗读智能体回复',
729
+ 'speakAsItGoes': '流式逐句朗读',
730
+ 'speakAsItGoesHint': '模型输出时按句子即时流式朗读,无需等待整段内容生成完毕。',
731
+ 'rate': '播放语速',
732
+ 'rateHint': '0.5 到 2.0 倍速。服务端按标准速度合成,由浏览器调整播放速率。',
733
+ 'bargeIn': '打断静音 (Barge-in)',
734
+ 'bargeInHint': '当麦克风开启或检测到说话时立即中断朗读。',
735
+ 'announce': '重要事件播报',
736
+ 'announceHint': '当智能体等待用户审批或提出问题时,播放提示音并语音播报。',
737
+ 'approvalText': '审批提示语',
738
+ 'approvalHint': '请求审批时播报的短语,会自动附带工具名称。',
739
+ 'questionText': '提问提示语',
740
+ 'questionHint': '智能体提问时播报的前导短语。留空则直接读出问题。',
741
+ 'chime': '提示音效',
742
+ 'chimeHint': '可选 ding、beep 或无。',
743
+ 'language': '语言选择',
744
+ 'languageHint': '针对需要指定语言的服务商(如 eSpeak 或 Azure SSML)。',
745
+ 'piperModel': 'Piper 模型路径',
746
+ 'piperModelHint': 'ONNX 模型的绝对路径。留空则跳过 Piper。',
747
+ 'piperBin': 'Piper 可执行文件',
748
+ 'binHint': '默认从 PATH 查找,除非指定绝对路径。',
749
+ 'espeakBin': 'eSpeak 可执行文件',
750
+ 'espeakHint': '通常为 espeak-ng。',
751
+ 'edgeBin': 'edge-tts 可执行文件',
752
+ 'edgeHint': 'Python edge-tts 命令行工具。',
753
+ 'azureRegion': 'Azure 区域',
754
+ 'azureHint': 'Azure 语音服务所在区域,例如 eastus。',
755
+ 'openaiBaseUrl': 'OpenAI API 地址',
756
+ 'openaiBaseUrlHint': '兼容 OpenAI 的 TTS 接口地址。',
757
+ 'mimoBaseUrl': 'MiMo API 地址',
758
+ 'mimoBaseUrlHint': 'MiMo 接口根地址,通过其对话接口提取语音。',
759
+ 'mimoFormat': 'MiMo 音频格式',
760
+ 'mimoFormatHint': 'mp3 或 wav。',
761
+ 'minimaxBin': 'MiniMax CLI 路径',
762
+ 'minimaxBinHint': '默认从 PATH 查找,需提前安装并登录。',
763
+ 'advancedTitle': '高级参数',
764
+ 'advancedHint': '字符上限与接口重定向。API 密钥在候选链中维护,凭证环境变量仅在后台生效。',
765
+ 'maxChars': '最大字符上限',
766
+ 'maxCharsHint': '触发长文本截断或摘要的硬性上限;0 表示无限制。',
767
+ 'sentenceChars': '单句切片大小',
768
+ 'sentenceCharsHint': '流式语音合成中单句的最大字符长度。',
769
+ 'timeoutMs': '服务商超时 (毫秒)',
770
+ 'timeoutMsHint': '单次语音合成请求超时时间,超时后自动切换下一服务商。',
771
+ 'maxQueue': '最大队列长度',
772
+ 'maxQueueHint': '超出该长度时丢弃最旧的未播放语音片段。',
773
+ 'chainTitle': '服务商降级链 (Fallback Chain)',
774
+ 'addProvider': '添加服务商',
775
+ 'localProvider': '本地 / CLI 服务商(无需 API 密钥)',
776
+ 'configured': '已配置',
777
+ 'notSet': '未设置',
778
+ 'fromEnv': '来自环境变量',
779
+ 'clear': '清空',
780
+ 'remove': '移除',
781
+ 'up': '上移',
782
+ 'down': '下移',
783
+ 'save': '保存设置',
784
+ 'saved': '已保存',
785
+ 'cardHint': '配置服务商降级顺序、发音人与朗读参数。',
786
+ 'skipCode': '跳过代码块',
787
+ 'skipCodeHint': '遇到代码块时朗读简短提示,而不逐行朗读代码字符。',
788
+ 'cache': '合成音频缓存',
789
+ 'cacheHint': '重复短语直接从本地缓存读取,无需重复合成。',
790
+ 'cacheMaxMb': '缓存上限 (MB)',
791
+ 'cacheMaxMbHint': '超出容量时优先淘汰最近最少使用 (LRU) 的音频。',
792
+ 'clearCache': '清空缓存',
793
+ 'pronTitle': '发音纠正字典',
794
+ 'pronHint': '规则自上而下匹配。左侧支持 /…/ 正则表达式;支持按语言过滤。',
795
+ 'pronFrom': '原词',
796
+ 'pronTo': '替换为',
797
+ 'pronWhole': '全词匹配',
798
+ 'pronLang': '语言',
799
+ 'addRule': '添加规则',
800
+ 'rolesTitle': '角色专属音色',
801
+ 'rolesHint': '留空项将自动继承主服务商配置。',
802
+ 'roleReply': '普通回复',
803
+ 'roleApproval': '操作审批',
804
+ 'roleError': '系统错误',
805
+ 'ssmlStyle': '情感语调 (SSML)',
806
+ 'ssmlStyleHint': '情绪语调风格(如 cheerful、serious、empathetic 等)。',
807
+ 'longReply': '长回复处理',
808
+ 'longReplyHint': 'truncate: 截断超长部分; summarize: 智能生成简述; full: 完整朗读全文。',
809
+ 'summaryModel': '摘要生成模型',
810
+ 'summaryModelHint': 'provider/model 格式,留空则跟随当前对话模型。',
811
+ 'summarySentences': '摘要句数',
812
+ 'summarySentencesHint': '语音朗读所采用的摘要句子数量。',
813
+ 'preview': '试听',
814
+ 'statsTitle': '实时统计',
815
+ 'statTotal': '总请求数',
816
+ 'statHits': '缓存命中',
817
+ 'statErrors': '合成失败',
818
+ 'statReset': '重置统计',
819
+ 'autoDetect': '按句自动识别语种',
820
+ 'autoDetectHint': '每句根据字符特征自动推测语言,而非强制使用全局设定。',
821
+ 'narrateQuotesOnly': '仅朗读引用内容',
822
+ 'narrateQuotesHint': '仅朗读正文中用引号括起来的引用短语。',
823
+ 'skipActions': '跳过 *星号动作*',
824
+ 'skipActionsHint': '自动剔除 *动作说明* 文本。',
825
+ 'removeRegex': '自定义正则过滤',
826
+ 'removeRegexHint': '在合成前由全局正则表达式匹配并剔除匹配文本。',
827
+ 'customBaseUrl': '自定义 TTS API 地址',
828
+ 'customBaseUrlHint': '兼容 OpenAI 的接口地址,如 http://localhost:8880/v1;留空禁用。',
829
+ 'customKeyEnv': '自定义接口凭据名',
830
+ 'customKeyEnvHint': '用于存储自定义接口 Bearer Token 的凭证槽位名称。',
831
+ 'localEnginesTitle': '本地离线引擎 (Offline)',
832
+ 'localEnginesHint': '在本地离线运行语音合成,无需 API 密钥与互联网。',
833
+ 'enableLocalEngines': '启用本地引擎',
834
+ 'enableLocalEnginesHint': '检测并调用本地 Kokoro 或 F5-TTS 模型。',
835
+ 'streamingEnabled': '毫秒级低延迟流式 (< 300 ms)',
836
+ 'streamingEnabledHint': '利用 AudioWorklet 和 SSE 实时输出流式音频。',
837
+ 'useKokoro': '启用 Kokoro-82M (CPU)',
838
+ 'useKokoroHint': '基于 CPU 的轻量级快速神经语音合成。',
839
+ 'useF5': '启用 F5-TTS (GPU)',
840
+ 'useF5Hint': '基于 NVIDIA GPU 的高保真零样本克隆语音合成。',
841
+ 'installed': '已就绪',
842
+ 'notInstalled': '未安装',
843
+ 'downloading': '下载中',
844
+ 'installKokoro': '安装权重 (350 MB)',
845
+ 'installF5': '安装权重 (2.1 GB)',
846
+ 'deleteModel': '删除模型',
847
+ 'voiceDuplexTitle': '双工语音对话 (Voice-to-Voice)',
848
+ 'voiceDuplexHint': '与 dsh-voice 协同提供全双工交互式对话。',
849
+ 'voiceNotInstalledTitle': '依赖插件 @goodandready/dsh-voice 尚未安装',
850
+ 'voiceInstallHint': '如需启用语音对话,请通过以下命令安装该插件:',
851
+ 'voiceDuplex': '语音双工模式',
852
+ 'voiceDuplexHintToggle': '用户语音输入结束后,自动使用语音朗读回复。',
853
+ 'vadBargeIn': 'VAD 语音打断',
854
+ 'vadBargeInHint': '检测到人类语音时立即终止智能体当前语音输出。',
855
+ 'newSubagentRole': '子智能体标识 (例如 coder, reviewer)',
856
+ 'addSubagentRole': '添加子智能体',
857
+ 'autoDetectSubagent': '自动感应会话中的子智能体角色',
858
+ 'role_coder': '编码助手 (Coder)',
859
+ 'role_reviewer': '代码评审 (Reviewer)',
860
+ 'role_planner': '架构规划 (Planner)',
861
+ 'enableItDictionary': '内置 IT 术语发音校正',
862
+ 'enableItDictionaryHint': '自动纠正 SQL、Nginx、K8s、Docker、API、JSON、YAML 等专有名词发音。',
863
+ 'loadItDictionary': '填入常用 IT 术语',
864
+ 'previewRule': '试听发音',
865
+ 'messengerIntegrationTitle': '即时通讯联动 (Telegram, Discord)',
866
+ 'messengerIntegrationHint': '通过 @goodandready/dsh-messenger-gateway 向聊天群组发送语音回复。',
867
+ 'messengerNotInstalledTitle': '依赖插件 @goodandready/dsh-messenger-gateway 尚未安装',
868
+ 'messengerInstallHint': '如需向聊天软件发送语音回复,请运行:',
869
+ 'messengerTtsEnabled': '群组语音回复',
870
+ 'messengerTtsEnabledHint': '允许网关将回复以语音便签 (Voice Note) 形式发送。',
871
+ }
829
872
  function ChainEditor(props) {
830
873
  // Translator comes from the card: the slot passes props.t only to it.
831
874
  const t = props.t || ((key) => key)
@@ -1091,7 +1134,8 @@ window.__ModuleLoader__.load({
1091
1134
  onChange: (e) => set(r, { chime: e.target.value }),
1092
1135
  }),
1093
1136
  React.createElement('input', {
1094
- className: 'dts-input dts-grow', placeholder: 'ssml',
1137
+ className: 'dts-input dts-grow', placeholder: t('ssmlStyle') || 'ssml',
1138
+ title: t('ssmlStyleHint') || 'SSML tone style',
1095
1139
  value: (value[r] && value[r].ssmlStyle) || '', disabled: !props.writable,
1096
1140
  onChange: (e) => set(r, { ssmlStyle: e.target.value }),
1097
1141
  }),
@@ -1511,6 +1555,7 @@ window.__ModuleLoader__.load({
1511
1555
  ),
1512
1556
  React.createElement('div', { className: 'dts-block' },
1513
1557
  React.createElement('div', { className: 'dts-h' }, t('rolesTitle')),
1558
+ boolField('autoDetectSubagent', t('autoDetectSubagent'), t('autoDetectSubagent')),
1514
1559
  React.createElement(RolesEditor, {
1515
1560
  t: t, writable: writable, onPreview: onPreview,
1516
1561
  value: draft && draft.roles ? draft.roles : {},
@@ -1669,6 +1714,10 @@ window.__ModuleLoader__.load({
1669
1714
  type: 'button', className: 'dts-link', title: '★',
1670
1715
  onClick: () => { recent.toggleFav(text); force() },
1671
1716
  }, starred ? '★' : '☆'),
1717
+ React.createElement('button', {
1718
+ type: 'button', className: 'dts-link', title: t('exportAudio') || 'Export',
1719
+ onClick: () => exportAudioClip(text),
1720
+ }, '⤓'),
1672
1721
  )
1673
1722
  return React.createElement('div', { style: { position: 'relative', display: 'inline-flex', gap: '4px', alignItems: 'center' } },
1674
1723
  React.createElement('button', {
@@ -1697,6 +1746,12 @@ window.__ModuleLoader__.load({
1697
1746
  title: t('dockStop'),
1698
1747
  onClick: stopPlayback,
1699
1748
  }, '■'),
1749
+ React.createElement('button', {
1750
+ type: 'button',
1751
+ className: 'dts-link',
1752
+ title: t('exportAudio') || 'Export clip',
1753
+ onClick: () => exportAudioClip(player.lastText),
1754
+ }, '⤓'),
1700
1755
  )
1701
1756
  }
1702
1757
 
@@ -1737,7 +1792,7 @@ window.__ModuleLoader__.load({
1737
1792
  }
1738
1793
  }
1739
1794
  ctx.effect(() => {
1740
- const undo = [addLocale('en', en), addLocale('ru', ru)]
1795
+ const undo = [addLocale('en', en), addLocale('zh', zh)]
1741
1796
  return () => { for (const off of undo) off() }
1742
1797
  }, 'dsh-tts: locales')
1743
1798
  // The sidebar draws the section label, not our component: props.t never
package/lib/index.js CHANGED
@@ -100,6 +100,8 @@ export const Config = z.object({
100
100
  .description('Mute speech synthesis immediately when voice activity is detected.'),
101
101
  messengerTtsEnabled: z.boolean().default(false)
102
102
  .description('Send synthesized voice notes to messengers via @goodandready/dsh-messenger-gateway.'),
103
+ autoDetectSubagent: z.boolean().default(true)
104
+ .description('Auto-detect subagent roles in multi-agent sessions and route to dedicated voice profiles.'),
103
105
  language: z.string().default('ru'),
104
106
  speakAsItGoes: z
105
107
  .boolean()
@@ -519,8 +521,12 @@ export function apply(ctx, config) {
519
521
 
520
522
  if (event.type === 'assistant/message') {
521
523
  const text = assistantText(event.data && event.data.message)
522
- const agentName = (event.data && (event.data.agent || (event.data.message && event.data.message.agent) || (event.data.message && event.data.message.author && event.data.message.author.name))) || (session && session.agent) || ''
523
- const activeRole = (agentName && cfg.roles && cfg.roles[agentName] && cfg.roles[agentName].provider) ? agentName : 'reply'
524
+ const agentName = (event.data && (
525
+ event.data.subagent ||
526
+ event.data.agent ||
527
+ (event.data.message && (event.data.message.subagent || event.data.message.agent || event.data.message.sender || (event.data.message.author && event.data.message.author.name)))
528
+ )) || (session && (session.subagent || session.agent)) || ''
529
+ const activeRole = (agentName && cfg.roles && cfg.roles[agentName] && (cfg.roles[agentName].provider || cfg.roles[agentName].voice)) ? agentName : 'reply'
524
530
 
525
531
  // Speak as it goes: each incoming chunk is spoken immediately
526
532
  // in sentence pieces; waiting for turn end means silence while the agent works.