@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.
package/lib/text.js CHANGED
@@ -16,11 +16,14 @@ export function blocksToText(content) {
16
16
  export const SPEECH_PHRASES = {
17
17
  en: { codeBlock: 'code block, {n} lines', table: 'table, {n} rows', summaryIntro: 'Summary of the reply.' },
18
18
  ru: { codeBlock: 'блок кода, {n} строк', table: 'таблица, {n} строк', summaryIntro: 'Пересказ ответа.' },
19
+ zh: { codeBlock: '代码块,共 {n} 行', table: '表格,共 {n} 行', summaryIntro: '回复摘要。' },
19
20
  }
20
21
 
21
22
  export function speechPhrases(language) {
22
23
  const lang = String(language || '').toLowerCase()
23
- return lang.startsWith('ru') ? SPEECH_PHRASES.ru : SPEECH_PHRASES.en
24
+ if (lang.startsWith('zh')) return SPEECH_PHRASES.zh
25
+ if (lang.startsWith('ru')) return SPEECH_PHRASES.ru
26
+ return SPEECH_PHRASES.en
24
27
  }
25
28
 
26
29
  // Narration filters run on top of general scrubbing, before pronunciation.
@@ -43,8 +46,10 @@ export function applyNarrationFilters(text, cfg) {
43
46
  // Rough language heuristic: whichever script has more letters wins.
44
47
  export function detectLang(text) {
45
48
  const s = String(text || '')
49
+ const cjk = (s.match(/[\u4E00-\u9FFF]/g) || []).length
46
50
  const cyr = (s.match(/[\u0400-\u04FF]/g) || []).length
47
51
  const lat = (s.match(/[A-Za-z]/g) || []).length
52
+ if (cjk >= cyr && cjk >= lat && cjk > 0) return 'zh'
48
53
  return cyr > lat ? 'ru' : 'en'
49
54
  }
50
55
 
@@ -165,7 +170,14 @@ export function protectAbbreviations(text) {
165
170
  const rx = new RegExp('(?<=^|[\\s"«(])' + escaped + '(?=[\\s"»)\\]]|$)', 'gi')
166
171
  res = res.replace(rx, (m) => m.replace(/\./g, '\u2024'))
167
172
  }
173
+ // Decimal numbers and versions: e.g. 3.14, v0.4.6, 192.168.1.111
168
174
  res = res.replace(/(?<=[vV]?\d+)\.(?=\d+)/g, '\u2024')
175
+ // List numbering at line/word starts: 1. item, 2. item
176
+ res = res.replace(/(?<=^|[\s\n])(\d+)\.(?=\s+[A-Za-zА-Яа-я\u4E00-\u9FFF])/g, '$1\u2024')
177
+ // Common file extensions: e.g. package.json, index.js, script.py
178
+ res = res.replace(/(?<=[a-zA-Z0-9_-])\.(?:js|mjs|cjs|ts|tsx|jsx|json|ya?ml|md|py|sh|tgz|tar|gz|zip|html?|css|rs|go|c|cpp|h|txt)(?=[ \t\n"«'\)\]\},;\.\?!]|$)/gi, (m) => m.replace(/\./g, '\u2024'))
179
+ // Hostnames and domains: e.g. goodandready.app, github.com
180
+ res = res.replace(/(?<=[a-zA-Z0-9])\.(?:com|org|net|io|dev|app|ru|cn|local)(?=[ \t\n/:"«'\)\]\},;\.\?!]|$)/gi, (m) => m.replace(/\./g, '\u2024'))
169
181
  return res
170
182
  }
171
183
 
@@ -186,20 +198,29 @@ export function restoreAbbreviations(text) {
186
198
  */
187
199
  export function splitSentences(text, maxChars) {
188
200
  const limit = Number(maxChars) > 0 ? Number(maxChars) : 320
189
- const min = Math.min(60, Math.floor(limit / 3))
190
201
  const raw = String(text || '').trim()
191
202
  if (!raw) return []
192
203
 
204
+ const isCjk = /[\u4E00-\u9FFF]/.test(raw)
205
+ // CJK characters carry much more meaning per character than Latin/Cyrillic words.
206
+ const min = isCjk ? Math.min(10, Math.floor(limit / 5)) : Math.min(60, Math.floor(limit / 3))
207
+
193
208
  const source = protectAbbreviations(raw)
194
209
  const pieces = []
195
210
  let current = ''
196
- // Boundaries are period, question, exclamation, ellipsis, and newline.
197
- for (const part of source.split(/(?<=[.!?…])\s+|\n+/)) {
211
+ // Boundaries are period, question, exclamation, ellipsis, Chinese full-width punctuation, and newline.
212
+ for (const part of source.split(/(?<=[.!?…\u3002\uFF01\uFF1F])\s*|\n+/)) {
198
213
  const chunk = String(part || '').trim()
199
214
  if (!chunk) continue
200
- if (!current) { current = chunk } else if (current.length < min) { current += ' ' + chunk }
201
- else { pieces.push(current); current = chunk }
202
- // Oversized pieces are split on words: providers choke on walls of text.
215
+ if (!current) {
216
+ current = chunk
217
+ } else if (current.length < min) {
218
+ current += (isCjk ? '' : ' ') + chunk
219
+ } else {
220
+ pieces.push(current)
221
+ current = chunk
222
+ }
223
+ // Oversized pieces are split on words or characters: providers choke on walls of text.
203
224
  while (current.length > limit) {
204
225
  let cut = current.lastIndexOf(' ', limit)
205
226
  if (cut < min) cut = limit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-tts",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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",