@goodandready/dsh-tts 0.3.4 → 0.3.6

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/lib/client.js CHANGED
@@ -366,6 +366,8 @@ window.__ModuleLoader__.load({
366
366
  'save': 'Save',
367
367
  'saved': 'Saved',
368
368
  'cardHint': 'Provider chain, voices and playback options.',
369
+ 'skipCode': 'Skip code blocks',
370
+ 'skipCodeHint': 'Say a short notice instead of reading code aloud.',
369
371
  }
370
372
  const ru = {
371
373
  'general': 'Общее',
@@ -414,6 +416,8 @@ window.__ModuleLoader__.load({
414
416
  'save': 'Сохранить',
415
417
  'saved': 'Сохранено',
416
418
  'cardHint': 'Цепочка провайдеров, голоса и параметры чтения.',
419
+ 'skipCode': 'Пропускать код',
420
+ 'skipCodeHint': 'Вместо чтения кода вслух — короткая пометка.',
417
421
  }
418
422
 
419
423
  function TtsSection(props) {
@@ -565,6 +569,8 @@ window.__ModuleLoader__.load({
565
569
  t('bargeInHint')),
566
570
  boolField('announceApproval', t('announce'),
567
571
  t('announceHint')),
572
+ boolField('skipCode', t('skipCode'),
573
+ t('skipCodeHint')),
568
574
  textField('approvalText', t('approvalText'), t('approvalHint')),
569
575
  textField('questionText', t('questionText'), t('questionHint')),
570
576
  textField('chime', t('chime'), t('chimeHint')),
@@ -646,7 +652,25 @@ window.__ModuleLoader__.load({
646
652
  }
647
653
 
648
654
  function registerSettings(ctx) {
649
- ctx.effect(() => ctx.locale.register(NS, { en, ru }), 'dsh-tts: словари')
655
+ // Язык может принести не только плагин: словарные пакеты объявляют
656
+ // русский для чужих пространств. Ядро на повторное объявление той же
657
+ // пары «пространство + язык» бросает исключение, и незащищённый вызов
658
+ // уносил с собой весь плагин — в интерфейсе это выглядело как «Failed to
659
+ // load plugins» с перечнем ни в чём не повинных соседей.
660
+ //
661
+ // Поэтому каждый язык объявляется отдельно и по-хорошему: заняли до нас —
662
+ // уступаем, свой английский при этом всё равно встаёт на место.
663
+ const addLocale = (locale, dictionary) => {
664
+ try {
665
+ return ctx.locale.register(NS, locale, dictionary)
666
+ } catch (alreadyTaken) {
667
+ return () => {}
668
+ }
669
+ }
670
+ ctx.effect(() => {
671
+ const undo = [addLocale('en', en), addLocale('ru', ru)]
672
+ return () => { for (const off of undo) off() }
673
+ }, 'dsh-tts: словари')
650
674
  // Подпись раздела рисует боковой список, а не наш компонент: props.t
651
675
  // туда не доходит, поэтому берём переводчик, привязанный к namespace.
652
676
  const t = ctx.locale.bind(NS)
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
3
3
  import { credentialRef } from '@deepseek-ai/dsh-credentials'
4
4
  import { runChain } from './chain.js'
5
5
  import { makeProviders, PROVIDER_KEYS, DEFAULT_MODELS, DEFAULT_VOICES } from './providers.js'
6
- import { assistantText, stripForSpeech, splitSentences } from './text.js'
6
+ import { assistantText, speechPhrases, splitSentences, stripForSpeech } from './text.js'
7
7
  import {
8
8
  CLOUD_PROVIDERS,
9
9
  assertCredentialRef,
@@ -31,6 +31,8 @@ const ChainEntry = z.object({
31
31
  export const Config = z.object({
32
32
  speakReplies: z.boolean().default(false)
33
33
  .description('When on, speak each finished agent reply in the Web UI.'),
34
+ skipCode: z.boolean().default(true)
35
+ .description('Replace fenced code blocks with a short spoken notice instead of reading them aloud.'),
34
36
  language: z.string().default('ru'),
35
37
  speakAsItGoes: z
36
38
  .boolean()
@@ -225,8 +227,13 @@ export function apply(ctx, config) {
225
227
  }
226
228
  }
227
229
 
230
+ // Настройки чистки текста считаем на каждый вызов: конфиг живой.
231
+ function cleanOpts(cfg) {
232
+ return { skipCode: cfg.skipCode !== false, phrases: speechPhrases(cfg.language) }
233
+ }
234
+
228
235
  async function synthesize(rawText, cfg, signal) {
229
- const text = stripForSpeech(rawText, cfg.maxChars)
236
+ const text = stripForSpeech(rawText, cfg.maxChars, cleanOpts(cfg))
230
237
  if (!text) throw new Error('nothing to speak')
231
238
  const models = {}
232
239
  const voices = {}
@@ -320,7 +327,7 @@ export function apply(ctx, config) {
320
327
  // предложениями, и очередь проигрывается по порядку. Ждать конца хода
321
328
  // значит молчать всё время, пока агент работает.
322
329
  if (cfg.speakAsItGoes) {
323
- const ready = stripForSpeech(text, cfg.maxChars)
330
+ const ready = stripForSpeech(text, cfg.maxChars, cleanOpts(cfg))
324
331
  for (const piece of splitSentences(ready, cfg.sentenceChars)) speakPiece(sid, piece, cfg)
325
332
  return
326
333
  }
@@ -335,7 +342,7 @@ export function apply(ctx, config) {
335
342
  const cur = collectors.get(sid)
336
343
  collectors.delete(sid)
337
344
  const raw = cur && Array.isArray(cur.parts) ? cur.parts.join('\n') : ''
338
- const text = stripForSpeech(raw, cfg.maxChars)
345
+ const text = stripForSpeech(raw, cfg.maxChars, cleanOpts(cfg))
339
346
  if (!text) return
340
347
  const id = `u${++utteranceSeq}`
341
348
  const controller = new AbortController()
@@ -502,7 +509,7 @@ export function apply(ctx, config) {
502
509
  }
503
510
  let payload
504
511
  try { payload = JSON.parse(raw.toString('utf8') || '{}') } catch { payload = {} }
505
- const text = stripForSpeech(payload.text, live().maxChars)
512
+ const text = stripForSpeech(payload.text, live().maxChars, cleanOpts(live()))
506
513
  if (!text) { writeJson(res, 400, { ok: false, error: { code: 'no-text', message: 'no text' } }); return }
507
514
  const cfg = live()
508
515
  const controller = new AbortController()
package/lib/text.js CHANGED
@@ -11,10 +11,44 @@ export function blocksToText(content) {
11
11
  return parts.join('\n')
12
12
  }
13
13
 
14
- export function stripForSpeech(raw, maxChars) {
14
+ // Короткие пометки вместо выброшенной разметки. Формулировки выбираются по
15
+ // языку озвучки из настроек; подстановки {n} заполняются числом строк.
16
+ export const SPEECH_PHRASES = {
17
+ en: { codeBlock: 'code block, {n} lines', table: 'table, {n} rows' },
18
+ ru: { codeBlock: 'блок кода, {n} строк', table: 'таблица, {n} строк' },
19
+ }
20
+
21
+ export function speechPhrases(language) {
22
+ const lang = String(language || '').toLowerCase()
23
+ return lang.startsWith('ru') ? SPEECH_PHRASES.ru : SPEECH_PHRASES.en
24
+ }
25
+
26
+ function fill(template, n) {
27
+ return String(template).replace('{n}', String(n))
28
+ }
29
+
30
+ export function stripForSpeech(raw, maxChars, opts) {
31
+ const options = opts || {}
32
+ const skipCode = options.skipCode !== false
33
+ const phrases = options.phrases || SPEECH_PHRASES.en
15
34
  let text = String(raw || '')
16
- text = text.replace(/```[\s\S]*?```/g, ' ')
17
- text = text.replace(/`[^`]+`/g, ' ')
35
+ // Чистка обязана идти до нарезки на предложения, иначе половина блока
36
+ // уедет в первый кусок и будет озвучена.
37
+ if (skipCode) {
38
+ text = text.replace(/```[\s\S]*?```/g, (block) => {
39
+ const inner = block.replace(/^```[^\n]*\n?/, '').replace(/\n?```\s*$/, '')
40
+ const lines = inner ? inner.split('\n').length : 1
41
+ return ' ' + fill(phrases.codeBlock, lines) + ' '
42
+ })
43
+ text = text.replace(/`[^`\n]+`/g, ' ')
44
+ }
45
+ // Таблицы Markdown: подряд идущие строки, начинающиеся с |; разделительная
46
+ // шапка |---| в счёт строк не идёт.
47
+ text = text.replace(/(?:^[ \t]*\|.*\|[ \t]*$\n?)+/gm, (run) => {
48
+ const rows = run.trim().split('\n')
49
+ .filter((line) => !/^\s*\|[\s:|-]+\|\s*$/.test(line)).length
50
+ return fill(phrases.table, Math.max(1, rows)) + '\n'
51
+ })
18
52
  text = text.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
19
53
  text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
20
54
  text = text.replace(/[#*_>]+/g, ' ')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-tts",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Text-to-speech for DeepSeek Harness: speak agent replies in the Web UI with a provider fallback chain (OpenAI, ElevenLabs, Google, Azure, Groq, Deepgram, OpenRouter, Edge, Piper, eSpeak).",
5
5
  "license": "MIT",
6
6
  "type": "module",