@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 +25 -1
- package/lib/index.js +12 -5
- package/lib/text.js +37 -3
- package/package.json +1 -1
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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.
|
|
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",
|