@goodandready/dsh-tts 0.3.0 → 0.3.2

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 (2) hide show
  1. package/lib/client.js +201 -37
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -239,6 +239,8 @@ window.__ModuleLoader__.load({
239
239
  }
240
240
 
241
241
  function ChainEditor(props) {
242
+ // Переводчик приходит от карточки: слот отдаёт props.t только ей.
243
+ const t = props.t || ((key) => key)
242
244
  const rows = Array.isArray(props.value) ? props.value : []
243
245
  const change = (i, patch) => {
244
246
  const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
@@ -259,7 +261,7 @@ window.__ModuleLoader__.load({
259
261
  const cred = credOf(props.credentials, row.provider)
260
262
  const draft = (props.keyDrafts && props.keyDrafts[row.provider]) || ''
261
263
  const badge = !cloud ? null
262
- : (!cred.writable ? 'Set in environment' : (cred.configured ? 'Configured' : 'Not set'))
264
+ : (!cred.writable ? t('fromEnv') : (cred.configured ? t('configured') : t('notSet')))
263
265
  return React.createElement('div', { className: 'dts-entry', key: i },
264
266
  React.createElement('div', { className: 'dts-row' },
265
267
  React.createElement('select', {
@@ -274,9 +276,9 @@ window.__ModuleLoader__.load({
274
276
  className: 'dts-model', value: row.voice || '', disabled: !props.writable,
275
277
  placeholder: 'voice', onChange: (e) => change(i, { voice: e.target.value }),
276
278
  }),
277
- React.createElement('button', { type: 'button', className: 'dts-mini', title: 'Up', disabled: !props.writable, onClick: () => move(i, -1) }, '\u2191'),
278
- React.createElement('button', { type: 'button', className: 'dts-mini', title: 'Down', disabled: !props.writable, onClick: () => move(i, 1) }, '\u2193'),
279
- React.createElement('button', { type: 'button', className: 'dts-mini', title: 'Remove', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
279
+ React.createElement('button', { type: 'button', className: 'dts-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '\u2191'),
280
+ React.createElement('button', { type: 'button', className: 'dts-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '\u2193'),
281
+ React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
280
282
  ),
281
283
  cloud ? React.createElement('div', { className: 'dts-row' },
282
284
  React.createElement('input', {
@@ -290,19 +292,121 @@ window.__ModuleLoader__.load({
290
292
  cred.configured && cred.writable ? React.createElement('button', {
291
293
  type: 'button', className: 'dts-link', disabled: !props.writable,
292
294
  onClick: () => props.onClearKey(row.provider),
293
- }, 'Clear') : null,
294
- ) : React.createElement('span', { className: 'dts-sub' }, 'Local / CLI provider — no API key'),
295
+ }, t('clear')) : null,
296
+ ) : React.createElement('span', { className: 'dts-sub' }, t('localProvider')),
295
297
  cloud && cred.ref ? React.createElement('span', { className: 'dts-sub' }, 'Stored as ' + cred.ref) : null,
296
298
  )
297
299
  }),
298
300
  React.createElement('div', { className: 'dts-row' },
299
- React.createElement('button', { type: 'button', className: 'dts-mini', title: 'Add provider', disabled: !props.writable, onClick: add }, '+'),
300
- React.createElement('span', { className: 'dts-sub' }, 'Top to bottom is the fallback order. Cloud keys are saved to the host credentials store, never into plugin settings.'),
301
+ React.createElement('button', { type: 'button', className: 'dts-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
302
+ React.createElement('span', { className: 'dts-sub' }, t('chainHint')),
301
303
  ),
302
304
  )
303
305
  }
304
306
 
305
- function TtsSection() {
307
+ // Строки карточки живут в реестре локалей: так их переводит отдельный
308
+ // пакет, не трогая код этого плагина. Английский — язык по умолчанию,
309
+ // на него же приходится откат, если перевода нет.
310
+ const en = {
311
+ 'general': 'General',
312
+ 'loading': 'Loading\u2026',
313
+ '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.',
314
+ 'speakRepliesHint': 'When on, each finished reply is synthesized on the host and played in this browser. Audio is not sent back to the model.',
315
+ 'chainHint': 'Top to bottom is the fallback order. Cloud keys are saved to the host credentials store, never into plugin settings.',
316
+ 'title': 'Speech',
317
+ 'speakReplies': 'Speak agent replies',
318
+ 'speakAsItGoes': 'Speak as it goes',
319
+ 'speakAsItGoesHint': 'Read each reply the moment it lands, sentence by sentence, instead of waiting for the whole turn.',
320
+ 'rate': 'Playback speed',
321
+ 'rateHint': '0.5 to 2. Synthesis is untouched; the browser plays faster or slower.',
322
+ 'bargeIn': 'Fall silent when you speak',
323
+ 'bargeInHint': 'Stop reading the moment the microphone opens.',
324
+ 'announce': 'Announce approvals',
325
+ 'announceHint': 'Say it out loud and play a chime when the agent stops for an approval or asks a question.',
326
+ 'approvalText': 'Approval wording',
327
+ 'approvalHint': 'Said when an approval is asked for; the tool name is appended.',
328
+ 'questionText': 'Question wording',
329
+ 'questionHint': 'Said when the agent asks a question. Empty disables it.',
330
+ 'chime': 'Chime',
331
+ 'chimeHint': 'ding, beep or none.',
332
+ 'language': 'Language',
333
+ 'languageHint': 'Hint for voices that need a locale (eSpeak, Azure SSML).',
334
+ 'piperModel': 'Piper model path',
335
+ 'piperModelHint': 'Absolute path to an ONNX model. Leave empty to skip Piper.',
336
+ 'piperBin': 'Piper binary',
337
+ 'binHint': 'Looked up in PATH unless absolute.',
338
+ 'espeakBin': 'eSpeak binary',
339
+ 'espeakHint': 'Usually espeak-ng.',
340
+ 'edgeBin': 'edge-tts binary',
341
+ 'edgeHint': 'Python CLI from the edge-tts package.',
342
+ 'azureRegion': 'Azure region',
343
+ 'azureHint': 'Required for the azure provider, e.g. eastus.',
344
+ 'chainTitle': 'Provider chain',
345
+ 'addProvider': 'Add provider',
346
+ 'localProvider': 'Local / CLI provider — no API key',
347
+ 'configured': 'Configured',
348
+ 'notSet': 'Not set',
349
+ 'fromEnv': 'Set in environment',
350
+ 'clear': 'Clear',
351
+ 'remove': 'Remove',
352
+ 'up': 'Up',
353
+ 'down': 'Down',
354
+ 'save': 'Save',
355
+ 'saved': 'Saved',
356
+ 'cardHint': 'Provider chain, voices and playback options.',
357
+ }
358
+ const ru = {
359
+ 'general': 'Общее',
360
+ 'loading': 'Загрузка…',
361
+ 'chainHintCard': 'Выберите провайдера, вставьте ключ и нажмите «Сохранить» (или просто уйдите из поля). Ключ сразу попадает в хранилище учётных данных харнесса; браузер его обратно не читает.',
362
+ 'speakRepliesHint': 'Каждый законченный ответ озвучивается на стороне харнесса и играет в этом браузере. Звук модели не отправляется.',
363
+ 'chainHint': 'Порядок сверху вниз — порядок отката. Ключи облачных провайдеров попадают в хранилище учётных данных харнесса, а не в настройки плагина.',
364
+ 'title': 'Озвучка',
365
+ 'speakReplies': 'Читать ответы агента',
366
+ 'speakAsItGoes': 'Читать по ходу',
367
+ 'speakAsItGoesHint': 'Читать каждый ответ, как только он пришёл, предложениями, не дожидаясь конца хода.',
368
+ 'rate': 'Скорость чтения',
369
+ 'rateHint': 'От 0.5 до 2. Синтез не меняется — быстрее или медленнее играет браузер.',
370
+ 'bargeIn': 'Замолкать, когда вы заговорили',
371
+ 'bargeInHint': 'Прекращать чтение, как только открылся микрофон.',
372
+ 'announce': 'Объявлять запрос подтверждения',
373
+ 'announceHint': 'Говорить вслух и подавать сигнал, когда агент остановился ради подтверждения или задал вопрос.',
374
+ 'approvalText': 'Что говорить о подтверждении',
375
+ 'approvalHint': 'Произносится при запросе подтверждения; имя инструмента добавляется следом.',
376
+ 'questionText': 'Что говорить о вопросе',
377
+ 'questionHint': 'Произносится, когда агент задал вопрос. Пусто — не произносить.',
378
+ 'chime': 'Сигнал',
379
+ 'chimeHint': 'ding, beep или none.',
380
+ 'language': 'Язык',
381
+ 'languageHint': 'Подсказка для голосов, которым нужен язык (eSpeak, Azure SSML).',
382
+ 'piperModel': 'Piper: путь к модели',
383
+ 'piperModelHint': 'Абсолютный путь к модели ONNX. Пусто — Piper пропускается.',
384
+ 'piperBin': 'Piper: бинарь',
385
+ 'binHint': 'Ищется в PATH, если путь не абсолютный.',
386
+ 'espeakBin': 'eSpeak: бинарь',
387
+ 'espeakHint': 'Обычно espeak-ng.',
388
+ 'edgeBin': 'edge-tts: бинарь',
389
+ 'edgeHint': 'Консольная утилита из пакета edge-tts.',
390
+ 'azureRegion': 'Azure: регион',
391
+ 'azureHint': 'Нужен провайдеру azure, например eastus.',
392
+ 'chainTitle': 'Цепочка провайдеров',
393
+ 'addProvider': 'Добавить провайдера',
394
+ 'localProvider': 'Локальный провайдер — ключ не нужен',
395
+ 'configured': 'Ключ задан',
396
+ 'notSet': 'Ключа нет',
397
+ 'fromEnv': 'Задан в окружении',
398
+ 'clear': 'Убрать',
399
+ 'remove': 'Удалить',
400
+ 'up': 'Выше',
401
+ 'down': 'Ниже',
402
+ 'save': 'Сохранить',
403
+ 'saved': 'Сохранено',
404
+ 'cardHint': 'Цепочка провайдеров, голоса и параметры чтения.',
405
+ }
406
+
407
+ function TtsSection(props) {
408
+ // Переводчик приходит от слота, потому что в его записи указан locale.
409
+ const t = (props && props.t) || ((key) => key)
306
410
  const [draft, setDraft] = React.useState(null)
307
411
  const [credentials, setCredentials] = React.useState({})
308
412
  const [keyDrafts, setKeyDrafts] = React.useState({})
@@ -326,7 +430,7 @@ window.__ModuleLoader__.load({
326
430
  return () => { alive = false }
327
431
  }, [])
328
432
 
329
- if (!draft) return React.createElement('div', { className: 'dts-wrap' }, 'Loading\u2026')
433
+ if (!draft) return React.createElement('div', { className: 'dts-wrap' }, t('loading'))
330
434
 
331
435
  const setTop = (key, v) => setDraft((d) => Object.assign({}, d || {}, { [key]: v }))
332
436
  const setDraftKey = (provider, value) => setKeyDrafts((d) => Object.assign({}, d, { [provider]: value }))
@@ -411,22 +515,23 @@ window.__ModuleLoader__.load({
411
515
 
412
516
  return React.createElement('div', { className: 'dts-wrap' },
413
517
  React.createElement('div', { className: 'dts-block' },
414
- React.createElement('div', { className: 'dts-h' }, 'Speech'),
518
+ React.createElement('div', { className: 'dts-h' }, t('title')),
415
519
  React.createElement('label', { className: 'dts-row' },
416
520
  React.createElement('input', {
417
521
  type: 'checkbox', checked: !!(draft && draft.speakReplies), disabled: !writable,
418
522
  onChange: (e) => setTop('speakReplies', e.target.checked),
419
523
  }),
420
- React.createElement('span', null, 'Speak agent replies'),
524
+ React.createElement('span', null, t('speakReplies')),
421
525
  ),
422
526
  React.createElement('div', { className: 'dts-sub' },
423
- 'When on, each finished reply is synthesized on the host and played in this browser. Audio is not sent back to the model.'),
527
+ t('speakRepliesHint')),
424
528
  ),
425
529
  React.createElement('div', { className: 'dts-block' },
426
- React.createElement('div', { className: 'dts-h' }, 'Provider chain'),
530
+ React.createElement('div', { className: 'dts-h' }, t('chainTitle')),
427
531
  React.createElement('div', { className: 'dts-sub' },
428
- '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.'),
532
+ t('chainHintCard')),
429
533
  React.createElement(ChainEditor, {
534
+ t: t,
430
535
  value: draft && draft.chain ? draft.chain : [], writable: writable,
431
536
  credentials: credentials, keyDrafts: keyDrafts,
432
537
  onChange: (v) => setTop('chain', v),
@@ -436,32 +541,58 @@ window.__ModuleLoader__.load({
436
541
  }),
437
542
  ),
438
543
  React.createElement('div', { className: 'dts-block' },
439
- React.createElement('div', { className: 'dts-h' }, 'General'),
440
- boolField('speakAsItGoes', 'Speak as it goes',
441
- 'Read each reply the moment it lands, sentence by sentence, instead of waiting for the whole turn.'),
442
- numberField('rate', 'Playback speed', '0.5 to 2. Synthesis is untouched; the browser plays faster or slower.', 0.1),
443
- boolField('bargeIn', 'Fall silent when you speak',
444
- 'Stop reading the moment the microphone opens.'),
445
- boolField('announceApproval', 'Announce approvals',
446
- 'Say it out loud and play a chime when the agent stops for an approval or asks a question.'),
447
- textField('approvalText', 'Approval wording', 'Said when an approval is asked for; the tool name is appended.'),
448
- textField('questionText', 'Question wording', 'Said when the agent asks a question. Empty disables it.'),
449
- textField('chime', 'Chime', 'ding, beep or none.'),
450
- textField('language', 'Language', 'Hint for voices that need a locale (eSpeak, Azure SSML).'),
451
- textField('piperModel', 'Piper model path', 'Absolute path to an ONNX model. Leave empty to skip Piper.'),
452
- textField('piperBin', 'Piper binary', 'Looked up in PATH unless absolute.'),
453
- textField('espeakBin', 'eSpeak binary', 'Usually espeak-ng.'),
454
- textField('edgeBin', 'edge-tts binary', 'Python CLI from the edge-tts package.'),
455
- textField('azureRegion', 'Azure region', 'Required for the azure provider, e.g. eastus.'),
544
+ React.createElement('div', { className: 'dts-h' }, t('general')),
545
+ boolField('speakAsItGoes', t('speakAsItGoes'),
546
+ t('speakAsItGoesHint')),
547
+ numberField('rate', t('rate'), t('rateHint'), 0.1),
548
+ boolField('bargeIn', t('bargeIn'),
549
+ t('bargeInHint')),
550
+ boolField('announceApproval', t('announce'),
551
+ t('announceHint')),
552
+ textField('approvalText', t('approvalText'), t('approvalHint')),
553
+ textField('questionText', t('questionText'), t('questionHint')),
554
+ textField('chime', t('chime'), t('chimeHint')),
555
+ textField('language', t('language'), t('languageHint')),
556
+ textField('piperModel', t('piperModel'), t('piperModelHint')),
557
+ textField('piperBin', t('piperBin'), t('binHint')),
558
+ textField('espeakBin', t('espeakBin'), t('espeakHint')),
559
+ textField('edgeBin', t('edgeBin'), t('edgeHint')),
560
+ textField('azureRegion', t('azureRegion'), t('azureHint')),
456
561
  ),
457
562
  React.createElement('div', { className: 'dts-row' },
458
- React.createElement('button', { type: 'button', className: 'dts-save', disabled: !writable, onClick: save }, 'Save'),
459
- saved ? React.createElement('span', { className: 'dts-ok' }, 'Saved') : null,
563
+ React.createElement('button', { type: 'button', className: 'dts-save', disabled: !writable, onClick: save }, t('save')),
564
+ saved ? React.createElement('span', { className: 'dts-ok' }, t('saved')) : null,
460
565
  err ? React.createElement('span', { className: 'dts-bad' }, err) : null,
461
566
  ),
462
567
  )
463
568
  }
464
569
 
570
+
571
+ // Карточка настроек во вкладке «Плагины»: заголовок, пояснение и
572
+ // сворачивание рисуем сами — ядро даёт только рамку списка.
573
+ function TtsCard(props) {
574
+ const t = (props && props.t) || ((key) => key)
575
+ const [open, setOpen] = React.useState(false)
576
+ return React.createElement('div',
577
+ {
578
+ className: 'dts-wrap',
579
+ style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', marginBottom: '10px' },
580
+ },
581
+ React.createElement('button',
582
+ {
583
+ type: 'button',
584
+ 'aria-expanded': open ? 'true' : 'false',
585
+ onClick: () => setOpen((v) => !v),
586
+ style: { display: 'flex', width: '100%', gap: '8px', alignItems: 'baseline', background: 'none', border: 'none', padding: '10px 12px', cursor: 'pointer', textAlign: 'left' },
587
+ },
588
+ React.createElement('strong', null, t('title')),
589
+ React.createElement('span', { className: 'dts-sub' }, t('cardHint')),
590
+ React.createElement('span', { style: { marginLeft: 'auto', color: 'var(--dsw-alias-label-secondary)' } }, open ? '\u25be' : '\u25b8'),
591
+ ),
592
+ open ? React.createElement(TtsSection, props) : null,
593
+ )
594
+ }
595
+
465
596
  // Кнопка в строке ввода: видно, что сейчас читается, и можно остановить.
466
597
  function SpeakerControl() {
467
598
  const p = usePlayer()
@@ -491,18 +622,51 @@ window.__ModuleLoader__.load({
491
622
  }
492
623
 
493
624
  function registerSettings(ctx) {
625
+ ctx.effect(() => ctx.locale.register(NS, { en, ru }), 'dsh-tts: словари')
626
+ // Подпись раздела рисует боковой список, а не наш компонент: props.t
627
+ // туда не доходит, поэтому берём переводчик, привязанный к namespace.
628
+ const t = ctx.locale.bind(NS)
629
+ // Штатное место настроек — карточка во вкладке «Настройки плагинов».
630
+ // Вкладка перебирает пространства настроек и ищет слот по entryKey,
631
+ // равному имени пространства: ключ обязан равняться NS, иначе карточка
632
+ // не появится молча, без единой ошибки в журнале.
633
+ let moved = false
634
+ try {
635
+ moved = !!ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
636
+ {
637
+ name: 'settings.plugin.item',
638
+ key: NS,
639
+ locale: NS,
640
+ inject: () => ({ ctx: ctx }),
641
+ },
642
+ TtsCard,
643
+ ))
644
+ } catch (noPluginItemSlot) { moved = false }
645
+ if (moved) return
646
+ // Запасной путь для сборок без settings.plugin.item: прежний раздел
647
+ // в боковом списке, чтобы настройки не пропали.
494
648
  ctx.slots.inject('settings.section', () => ctx.slots.register(
495
- { name: 'settings.section', id: '@goodandready/dsh-tts', order: 32, label: () => 'Speech', inject: () => ({ ctx: ctx }) },
649
+ {
650
+ name: 'settings.section',
651
+ id: '@goodandready/dsh-tts',
652
+ order: 32,
653
+ locale: NS,
654
+ label: () => t('title'),
655
+ inject: () => ({ ctx: ctx }),
656
+ },
496
657
  TtsSection,
497
658
  ))
498
659
  }
499
660
 
500
- exports.inject = ['slots']
661
+ exports.inject = ['slots', 'locale']
501
662
  exports.apply = function apply(ctx) {
502
663
  registerSettings(ctx)
503
664
  registerSpeaker(ctx)
504
665
  ctx.effect(() => listenForVoice(), 'dsh-tts: замолкать, когда человек заговорил')
505
- setInterval(pollPending, 1000)
666
+ ctx.effect(() => {
667
+ const timer = setInterval(pollPending, 1000)
668
+ return () => clearInterval(timer)
669
+ }, 'dsh-tts: опрос готовых озвучек')
506
670
  }
507
671
  return module.exports
508
672
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-tts",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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",