@goodandready/dsh-tts 0.4.1 → 0.4.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.
@@ -47,7 +47,7 @@
47
47
  React.createElement('button', {
48
48
  type: 'button', className: 'dts-link', style: { flex: 1, textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
49
49
  title: text,
50
- onClick: () => playStandalone(text),
50
+ onClick: () => playStandalone(text), title: text,
51
51
  }, text.slice(0, 80)),
52
52
  React.createElement('button', {
53
53
  type: 'button', className: 'dts-link', title: '★',
@@ -56,7 +56,7 @@
56
56
  )
57
57
  return React.createElement('div', { style: { position: 'relative', display: 'inline-flex', gap: '4px', alignItems: 'center' } },
58
58
  React.createElement('button', {
59
- type: 'button', className: 'dts-link', title: 'Недавние',
59
+ type: 'button', className: 'dts-link', title: t('recent') || 'Recent',
60
60
  onClick: () => setShowList((v) => !v),
61
61
  }, '☰'),
62
62
  showList ? React.createElement('div', {
@@ -67,7 +67,7 @@
67
67
  lists.favs.map((text) => row(text, true)),
68
68
  React.createElement('div', { className: 'dts-h' }, '⟳'),
69
69
  lists.items.filter((x) => !lists.favs.includes(x)).map((text) => row(text, false)),
70
- !lists.items.length ? React.createElement('span', { className: 'dts-sub' }, '—') : null,
70
+ !lists.items.length ? React.createElement('span', { className: 'dts-sub' }, t('recentEmpty') || 'No recent speech yet') : null,
71
71
  ) : null,
72
72
  React.createElement('button', {
73
73
  type: 'button',
@@ -85,11 +85,10 @@
85
85
  }
86
86
 
87
87
  /**
88
- * Запасной перевод дока.
88
+ * Fallback translator for the dock.
89
89
  *
90
- * Ставится при регистрации и живёт до конца работы страницы. Нужен
91
- * потому, что подпись слота рисует ядро: `props.t` туда не доходит в
92
- * принципе, и привязка — единственный способ дать ей язык.
90
+ * Bound at registration for the page lifetime. The core draws the slot label;
91
+ * props.t never reaches it, so the binder is the only way to give it a language.
93
92
  */
94
93
  let fallbackDockText = (key) => key
95
94
 
@@ -124,7 +123,7 @@
124
123
  ctx.effect(() => {
125
124
  const undo = [addLocale('en', en), addLocale('ru', ru)]
126
125
  return () => { for (const off of undo) off() }
127
- }, 'dsh-tts: словари')
126
+ }, 'dsh-tts: locales')
128
127
  // The sidebar draws the section label, not our component: props.t never
129
128
  // reaches it, so bind the translator to the namespace ourselves.
130
129
  const t = ctx.locale.bind(NS)
@@ -168,7 +167,7 @@
168
167
  exports.apply = function apply(ctx) {
169
168
  registerSettings(ctx)
170
169
  registerSpeaker(ctx)
171
- ctx.effect(() => listenForVoice(), 'dsh-tts: замолкать, когда человек заговорил')
170
+ ctx.effect(() => listenForVoice(), 'dsh-tts: barge-in on user speech')
172
171
  ctx.effect(() => {
173
172
  if (typeof window === 'undefined') return () => {}
174
173
  const onKey = (e) => {
@@ -178,47 +177,54 @@
178
177
  }
179
178
  window.addEventListener('keydown', onKey)
180
179
  return () => window.removeEventListener('keydown', onKey)
181
- }, 'dsh-tts: горячие клавиши')
180
+ }, 'dsh-tts: hotkeys')
182
181
  ctx.effect(() => {
183
182
  if (typeof window === 'undefined' || typeof EventSource === 'undefined') return () => {}
184
183
  let es = null
185
- try {
186
- es = new EventSource('/dsh-tts/stream')
187
- es.addEventListener('utterance', (e) => {
188
- try {
189
- const item = JSON.parse(e.data)
190
- if (!item || !item.id) return
191
- if (seenIds.has(item.id)) return
192
- seenIds.add(item.id)
193
- if (seenIds.size > 500) {
194
- const first = seenIds.values().next().value
195
- seenIds.delete(first)
196
- }
197
- player.queue.push(item)
198
- drainQueue()
199
- } catch {}
200
- })
201
- es.addEventListener('chime', (e) => {
202
- try {
203
- const item = JSON.parse(e.data)
204
- if (!item || !item.id) return
205
- if (seenIds.has(item.id)) return
206
- seenIds.add(item.id)
207
- if (seenIds.size > 500) {
208
- const first = seenIds.values().next().value
209
- seenIds.delete(first)
210
- }
211
- player.queue.push(item)
212
- drainQueue()
213
- } catch {}
214
- })
215
- } catch {}
216
- return () => { if (es) es.close() }
217
- }, 'dsh-tts: потоковое получение звука по SSE')
184
+ let closed = false
185
+ let attempt = 0
186
+ let retryTimer = null
187
+ const onItem = (e) => {
188
+ try {
189
+ const item = JSON.parse(e.data)
190
+ if (!item || !item.id) return
191
+ if (seenIds.has(item.id)) return
192
+ seenIds.add(item.id)
193
+ if (seenIds.size > 500) {
194
+ const first = seenIds.values().next().value
195
+ seenIds.delete(first)
196
+ }
197
+ player.queue.push(item)
198
+ drainQueue()
199
+ } catch { /* ignore malformed SSE frame */ }
200
+ }
201
+ const connect = () => {
202
+ if (closed) return
203
+ try {
204
+ es = new EventSource('/dsh-tts/stream')
205
+ es.addEventListener('open', () => { attempt = 0 })
206
+ es.addEventListener('utterance', onItem)
207
+ es.addEventListener('chime', onItem)
208
+ es.onerror = () => {
209
+ try { es.close() } catch { /* already closed */ }
210
+ if (closed) return
211
+ attempt += 1
212
+ const delay = Math.min(15000, 500 * Math.pow(2, Math.min(attempt, 5)))
213
+ retryTimer = setTimeout(connect, delay)
214
+ }
215
+ } catch { /* EventSource constructor failed — poll remains */ }
216
+ }
217
+ connect()
218
+ return () => {
219
+ closed = true
220
+ if (retryTimer) clearTimeout(retryTimer)
221
+ if (es) { try { es.close() } catch { /* already closed */ } }
222
+ }
223
+ }, 'dsh-tts: SSE audio stream')
218
224
  ctx.effect(() => {
219
225
  const timer = setInterval(pollPending, 1000)
220
226
  return () => clearInterval(timer)
221
- }, 'dsh-tts: опрос готовых озвучек')
227
+ }, 'dsh-tts: poll pending audio')
222
228
  }
223
229
  return module.exports
224
230
  },