@lovelace_lol/polymer 0.0.1

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 (85) hide show
  1. package/README.md +226 -0
  2. package/dist/index.d.ts +2603 -0
  3. package/dist/index.js +1031 -0
  4. package/dist/test.cjs +1519 -0
  5. package/dist/worker/agency-worker.js +972 -0
  6. package/dist/worker/manifest.edn +1 -0
  7. package/package.json +57 -0
  8. package/shadow-cljs.edn +173 -0
  9. package/src/polymer/animation/agency.cljs +84 -0
  10. package/src/polymer/animation/domain.cljs +110 -0
  11. package/src/polymer/animation/planner.cljs +88 -0
  12. package/src/polymer/animation/runtime.cljs +137 -0
  13. package/src/polymer/animation/scheduler.cljs +178 -0
  14. package/src/polymer/animation/service.cljs +981 -0
  15. package/src/polymer/animation/state.cljs +112 -0
  16. package/src/polymer/blink/agency.cljs +131 -0
  17. package/src/polymer/blink/planner.cljs +115 -0
  18. package/src/polymer/blink/scheduler.cljs +90 -0
  19. package/src/polymer/blink/snippet.cljs +50 -0
  20. package/src/polymer/blink/state.cljs +102 -0
  21. package/src/polymer/camera_context/agency.cljs +132 -0
  22. package/src/polymer/camera_context/domain.cljs +186 -0
  23. package/src/polymer/camera_context/planner.cljs +50 -0
  24. package/src/polymer/camera_context/scheduler.cljs +113 -0
  25. package/src/polymer/camera_context/state.cljs +121 -0
  26. package/src/polymer/character.cljs +471 -0
  27. package/src/polymer/conversation/agency.cljs +83 -0
  28. package/src/polymer/conversation/domain.cljs +35 -0
  29. package/src/polymer/conversation/effector.cljs +130 -0
  30. package/src/polymer/conversation/planner.cljs +178 -0
  31. package/src/polymer/conversation/scheduler.cljs +202 -0
  32. package/src/polymer/conversation/service.cljs +329 -0
  33. package/src/polymer/conversation/state.cljs +186 -0
  34. package/src/polymer/core.cljs +172 -0
  35. package/src/polymer/eye_head/agency.cljs +75 -0
  36. package/src/polymer/eye_head/domain.cljs +133 -0
  37. package/src/polymer/eye_head/goap.cljs +106 -0
  38. package/src/polymer/eye_head/scheduler.cljs +176 -0
  39. package/src/polymer/eye_head/service.cljs +259 -0
  40. package/src/polymer/eye_head/snippet.cljs +92 -0
  41. package/src/polymer/eye_head/state.cljs +167 -0
  42. package/src/polymer/gaze/agency.cljs +85 -0
  43. package/src/polymer/gaze/domain.cljs +211 -0
  44. package/src/polymer/gaze/goap.cljs +208 -0
  45. package/src/polymer/gaze/scheduler.cljs +184 -0
  46. package/src/polymer/gaze/state.cljs +202 -0
  47. package/src/polymer/gesture/agency.cljs +185 -0
  48. package/src/polymer/gesture/goap.cljs +461 -0
  49. package/src/polymer/gesture/scheduler.cljs +122 -0
  50. package/src/polymer/gesture/snippet.cljs +198 -0
  51. package/src/polymer/gesture/state.cljs +273 -0
  52. package/src/polymer/lipsync/agency.cljs +593 -0
  53. package/src/polymer/lipsync/articulation/jaw.cljs +258 -0
  54. package/src/polymer/lipsync/articulation/snippet.cljs +605 -0
  55. package/src/polymer/lipsync/articulation/tongue.cljs +418 -0
  56. package/src/polymer/lipsync/articulation/visemes.cljs +429 -0
  57. package/src/polymer/lipsync/goap.cljs +132 -0
  58. package/src/polymer/lipsync/pronunciation/effector.cljs +51 -0
  59. package/src/polymer/lipsync/scheduler.cljs +302 -0
  60. package/src/polymer/lipsync/state.cljs +202 -0
  61. package/src/polymer/nlp/phonemes.cljs +164 -0
  62. package/src/polymer/prosodic/agency.cljs +196 -0
  63. package/src/polymer/prosodic/goap.cljs +122 -0
  64. package/src/polymer/prosodic/scheduler.cljs +99 -0
  65. package/src/polymer/prosodic/snippet.cljs +106 -0
  66. package/src/polymer/prosodic/state.cljs +139 -0
  67. package/src/polymer/runtime/embody.cljs +117 -0
  68. package/src/polymer/runtime/embody_wasm.cljs +177 -0
  69. package/src/polymer/runtime/rust_embody_host.cljs +645 -0
  70. package/src/polymer/society.cljs +239 -0
  71. package/src/polymer/stream.cljs +130 -0
  72. package/src/polymer/transcription/agency.cljs +69 -0
  73. package/src/polymer/transcription/domain.cljs +68 -0
  74. package/src/polymer/transcription/effector.cljs +611 -0
  75. package/src/polymer/transcription/planner.cljs +203 -0
  76. package/src/polymer/transcription/scheduler.cljs +225 -0
  77. package/src/polymer/transcription/service.cljs +209 -0
  78. package/src/polymer/transcription/state.cljs +239 -0
  79. package/src/polymer/tts/agency.cljs +503 -0
  80. package/src/polymer/tts/azure.cljs +290 -0
  81. package/src/polymer/tts/planner.cljs +178 -0
  82. package/src/polymer/tts/runtime.cljs +624 -0
  83. package/src/polymer/tts/scheduler.cljs +162 -0
  84. package/src/polymer/tts/state.cljs +191 -0
  85. package/src/polymer/worker.cljs +70 -0
@@ -0,0 +1,503 @@
1
+ (ns polymer.tts.agency
2
+ (:require [polymer.stream :as stream]
3
+ [polymer.tts.azure :as azure]
4
+ [polymer.tts.planner :as planner]
5
+ [polymer.tts.runtime :refer [azure-cache-key
6
+ azure-voice-name
7
+ backend-url
8
+ callback-event
9
+ cleanup-audio!
10
+ clear-display-media-reference!
11
+ custom-provider
12
+ dispose-playback-resources!
13
+ ensure-web-speech-display-media-reference!
14
+ get-playback-reference-track
15
+ js-error
16
+ load-azure-voices!
17
+ load-web-speech-voices!
18
+ normalize-voices
19
+ now-ms
20
+ plan-failure-message
21
+ play-azure-audio!
22
+ prime-audio!
23
+ remember-cache
24
+ scalar->azure-percent
25
+ scalar->azure-pitch-percent
26
+ speak-web-speech!
27
+ speech-synthesis*
28
+ synthesize-azure!
29
+ stop-web-speech-handle!]]
30
+ [polymer.tts.scheduler :as scheduler]
31
+ [polymer.tts.state :as state]))
32
+
33
+ ;; TTS owns speech-provider side effects and emits plain data facts for LipSync.
34
+ ;; The impure browser/backend/audio calls live in `polymer.tts.runtime`; this
35
+ ;; namespace owns command planning, session state, and callback routing.
36
+
37
+ (defn create-tts-agency
38
+ "Create a TTS agency that owns provider sessions and emits LipSync facts."
39
+ [config]
40
+ (let [initial-config (js->clj config :keywordize-keys true)
41
+ input-stream (stream/create-stream)
42
+ event-stream (stream/create-stream)
43
+ emit-input (:emit input-stream)
44
+ emit-event (:emit event-stream)
45
+ state-atom (atom (state/default-state initial-config))
46
+ resources (atom {:audio nil
47
+ :audioUrl nil
48
+ :audioSource nil
49
+ :webSpeechHandle nil
50
+ :audioContext nil
51
+ :playbackReferenceDestination nil
52
+ :mediaElementSource nil
53
+ :playbackGain nil
54
+ :displayMediaReferenceStream nil
55
+ :displayMediaReferenceTrack nil})
56
+ session-scheduler (scheduler/create-scheduler)
57
+ azure-cache (atom {})
58
+ voice-load-seq (atom 0)
59
+ voice-load-requests (atom {})
60
+ playback-reference-listeners (atom #{})
61
+ disposed? (atom false)]
62
+ (letfn [(session-id []
63
+ ;; Every provider callback captures the session id active when
64
+ ;; it was registered. Replacements/stops increment the token so
65
+ ;; late callbacks cannot mutate state or emit stale LipSync data.
66
+ (:sessionId @state-atom))
67
+
68
+ (active-session? [id]
69
+ (and (not @disposed?) (= id (session-id))))
70
+
71
+ (notify-playback-reference-track! []
72
+ ;; Host-facing handle for barge-in. Stream messages stay plain data;
73
+ ;; MediaStreamTrack is exposed only through this imperative API.
74
+ (let [track (get-playback-reference-track resources)]
75
+ (doseq [listener @playback-reference-listeners]
76
+ (listener track))
77
+ (emit-event {:type "ttsPlaybackReferenceChanged"
78
+ :agency "tts"
79
+ :available (boolean track)
80
+ :at (now-ms)})
81
+ track))
82
+
83
+ (emit-status! []
84
+ ;; Status events are coarse facts, not animation ticks. The
85
+ ;; agency emits word/viseme progress through streams instead of
86
+ ;; making a host poll mutable state.
87
+ (emit-event {:type "ttsStatusChanged"
88
+ :agency "tts"
89
+ :state (state/visible-state @state-atom)}))
90
+
91
+ (emit-error! [message]
92
+ (swap! state-atom state/record-error message)
93
+ (emit-event {:type "error" :agency "tts" :message message})
94
+ (emit-status!))
95
+
96
+ (emit-lipsync! [command]
97
+ ;; This is an event-stream bridge inside Polymer. The agency
98
+ ;; system routes this command to LipSync, which then emits
99
+ ;; animation intent for the Animation agency.
100
+ (emit-event {:type "lipSync.command"
101
+ :agency "tts"
102
+ :command command}))
103
+
104
+ (active-speech? []
105
+ (contains? #{"loading" "speaking"} (:status @state-atom)))
106
+
107
+ (next-voice-load! [engine]
108
+ (let [request-id (swap! voice-load-seq inc)]
109
+ (swap! voice-load-requests assoc engine request-id)
110
+ request-id))
111
+
112
+ (active-voice-load? [engine request-id]
113
+ (and (not @disposed?)
114
+ (= request-id (get @voice-load-requests engine))))
115
+
116
+ (cancel-voice-loads! []
117
+ (swap! voice-load-requests empty))
118
+
119
+ (stop-web-speech! []
120
+ (when-let [handle (:webSpeechHandle @resources)]
121
+ (stop-web-speech-handle! handle))
122
+ (swap! resources assoc :webSpeechHandle nil))
123
+
124
+ (configure-lipsync! [engine command]
125
+ ;; TTS knows provider timing and user speech controls; LipSync knows
126
+ ;; how those controls become mouth/jaw/tongue animation.
127
+ (let [config (:config @state-atom)]
128
+ (emit-lipsync! {:type "configure"
129
+ :config {:intensity (:lipsyncIntensity config)
130
+ :jawScale (:jawScale config)
131
+ :tongueScale (:tongueScale config)
132
+ :speechRate (or (:rate command) (:rate config))
133
+ :visualLeadMs (or (:visualLeadMs command) (:visualLeadMs config))
134
+ :wordDriftThresholdSec (if (= engine "azure")
135
+ (:azureDriftThresholdSec config)
136
+ (:webSpeechDriftThresholdSec config))}})))
137
+
138
+ (stop-session! [reason]
139
+ ((:stop-active-session session-scheduler) reason)
140
+ (stop-web-speech!)
141
+ (cleanup-audio! resources)
142
+ (emit-lipsync! {:type "stop" :reason reason})
143
+ (swap! state-atom state/record-stopped (now-ms))
144
+ (emit-event {:type "ttsSpeechStopped"
145
+ :agency "tts"
146
+ :reason reason
147
+ :stoppedAt (:endedAt @state-atom)})
148
+ (emit-status!))
149
+
150
+ (finish-session! [id]
151
+ (when (active-session? id)
152
+ (stop-session! "completed")
153
+ (emit-event {:type "ttsSpeechEnded"
154
+ :agency "tts"
155
+ :endedAt (:endedAt @state-atom)})))
156
+
157
+ (word-boundary! [id word observed-elapsed-sec host-elapsed-sec]
158
+ (when (and (active-session? id) (pos? (count (or word ""))))
159
+ (let [word-index (:wordIndex @state-atom)]
160
+ (swap! state-atom state/record-word-boundary word-index)
161
+ (emit-lipsync! {:type "wordBoundary"
162
+ :word word
163
+ :wordIndex word-index
164
+ :observedElapsedSec observed-elapsed-sec
165
+ :hostElapsedSec host-elapsed-sec})
166
+ (emit-event {:type "ttsWordBoundary"
167
+ :agency "tts"
168
+ :word word
169
+ :wordIndex word-index
170
+ :observedElapsedSec observed-elapsed-sec
171
+ :hostElapsedSec host-elapsed-sec}))))
172
+
173
+ (start-web-speech! [id command snippet-name]
174
+ (let [config (:config @state-atom)
175
+ text (:text command)
176
+ plan {:text text
177
+ :voiceName (or (:voiceName command) (:voiceName config))
178
+ :rate (or (:rate command) (:rate config))
179
+ :pitch (or (:pitch command) (:pitch config))
180
+ :volume (or (:volume command) (:volume config))
181
+ :onAudioStarted (fn [_event]
182
+ (when (active-session? id)
183
+ (configure-lipsync! "webSpeech" command)
184
+ (emit-lipsync! {:type "startText"
185
+ :name snippet-name
186
+ :text text
187
+ :source "webSpeech"})
188
+ (emit-lipsync! {:type "audioStarted"
189
+ :name snippet-name
190
+ :audioTimeSec 0})
191
+ (swap! state-atom state/record-speaking (now-ms))
192
+ (emit-event {:type "ttsSpeechStarted"
193
+ :agency "tts"
194
+ :engine "webSpeech"
195
+ :name snippet-name
196
+ :startedAt (:startedAt @state-atom)})
197
+ (emit-status!)))
198
+ :onBoundary (fn [event]
199
+ (let [payload (callback-event event)]
200
+ (word-boundary! id
201
+ (:word payload)
202
+ (:observedElapsedSec payload)
203
+ (:hostElapsedSec payload))))
204
+ :onEnd (fn [] (finish-session! id))
205
+ :onError (fn [error] (emit-error! (.-message error)))
206
+ :scheduleStartFallback
207
+ (fn [delay-ms callback]
208
+ ((:schedule-start-fallback session-scheduler) id delay-ms callback))
209
+ :cancelStartFallback
210
+ (fn []
211
+ ((:cancel-start-fallback session-scheduler) id))}]
212
+ (-> (speak-web-speech! config plan)
213
+ (.then (fn [handle]
214
+ (if (active-session? id)
215
+ (swap! resources assoc :webSpeechHandle handle)
216
+ (stop-web-speech-handle! handle))))
217
+ (.catch (fn [error]
218
+ (when (active-session? id)
219
+ (emit-error! (.-message error))))))))
220
+
221
+ (start-azure! [id command snippet-name]
222
+ (let [config (:config @state-atom)
223
+ text (:text command)
224
+ voice-name (azure-voice-name config command)
225
+ style (let [raw (or (:style command) (:azureStyle config))]
226
+ (when (and (string? raw) (pos? (count (.trim raw)))) raw))
227
+ rate (or (:rate command) (:rate config))
228
+ pitch (or (:pitch command) (:pitch config))
229
+ volume (or (:volume command) (:volume config))
230
+ cache-key (azure-cache-key text voice-name style
231
+ (scalar->azure-percent rate)
232
+ (scalar->azure-pitch-percent pitch))
233
+ cached (get @azure-cache cache-key)
234
+ synth-promise (if cached
235
+ (js/Promise.resolve (clj->js cached))
236
+ (synthesize-azure! config (assoc command
237
+ :voiceName voice-name
238
+ :style style
239
+ :rate rate
240
+ :pitch pitch)))]
241
+ ;; Prime synchronously while the click/user gesture is still
242
+ ;; available. Synthesis may finish later, but the element is
243
+ ;; already allowed to play in stricter browsers.
244
+ ;; Prime HTMLAudio for autoplay unlock only. Do not advertise the
245
+ ;; playback-reference track until real Azure audio is routed, or
246
+ ;; barge-in would arm against a silent muted unlock clip.
247
+ (prime-audio! resources volume)
248
+ (-> synth-promise
249
+ (.then (fn [raw]
250
+ ;; Provider/backend shapes are normalized before
251
+ ;; any playback or LipSync command is emitted.
252
+ (let [payload (azure/normalize-azure-synthesis
253
+ (if (map? raw)
254
+ raw
255
+ (js->clj raw :keywordize-keys true)))]
256
+ (when-not cached
257
+ (swap! azure-cache remember-cache cache-key payload (:azureCacheLimit config)))
258
+ ;; Audio is required. Visemes help LipSync but must not
259
+ ;; block playback when Azure returns audio alone.
260
+ (if (pos? (count (str (:audioBase64 payload))))
261
+ payload
262
+ (throw (js-error "Azure TTS returned no usable audio payload"))))))
263
+ (.then (fn [payload]
264
+ (when (active-session? id)
265
+ (play-azure-audio! config resources
266
+ (assoc payload
267
+ :volume volume)))))
268
+ (.then (fn [playback]
269
+ (when (active-session? id)
270
+ (let [payload (if cached cached (get @azure-cache cache-key))
271
+ duration-sec (or (:durationSec payload) (aget playback "durationSec") 0)
272
+ word-timings (:wordTimings payload)]
273
+ (configure-lipsync! "azure" command)
274
+ (emit-lipsync! (azure/azure-synthesis->lipsync-command
275
+ snippet-name
276
+ text
277
+ (assoc payload :durationSec duration-sec)
278
+ config))
279
+ (emit-lipsync! {:type "audioStarted"
280
+ :name snippet-name
281
+ :audioTimeSec 0})
282
+ (swap! state-atom state/record-speaking (now-ms))
283
+ (emit-event {:type "ttsSpeechStarted"
284
+ :agency "tts"
285
+ :engine "azure"
286
+ :name snippet-name
287
+ :startedAt (:startedAt @state-atom)})
288
+ (emit-status!)
289
+ (notify-playback-reference-track!)
290
+ (when-let [clock (or (:clock playback)
291
+ (when (aget playback "clock")
292
+ (js->clj (aget playback "clock") :keywordize-keys true)))]
293
+ ((:schedule-boundaries session-scheduler)
294
+ id
295
+ active-session?
296
+ clock
297
+ word-timings
298
+ (fn [event]
299
+ (word-boundary! id
300
+ (:word event)
301
+ (:observedElapsedSec event)
302
+ nil))))
303
+ (when-let [audio (or (:audio playback) (aget playback "audio"))]
304
+ (set! (.-onended audio) #(finish-session! id)))))))
305
+ (.catch (fn [error]
306
+ (when (active-session? id)
307
+ (emit-error! (.-message error))))))))
308
+
309
+ (speak! [command]
310
+ (let [config (:config @state-atom)
311
+ engine (or (:engine command) (:engine config))
312
+ text (:text command)
313
+ snippet-name (or (:name command) (str "tts_" engine "_" (.now js/Date)))
314
+ ;; Provider planning sees only capability facts. It does not
315
+ ;; receive JS handles or secrets, and it never performs side
316
+ ;; effects.
317
+ world {:backendUrl (backend-url config command)
318
+ :hasAzureSynthesize (boolean (custom-provider config :azureSynthesize))
319
+ :hasWebSpeech (boolean (or (custom-provider config :webSpeechSpeak)
320
+ (speech-synthesis*)))}
321
+ plan (planner/plan-speech (assoc command :engine engine) config world)]
322
+ (swap! state-atom (fn [state]
323
+ (-> state
324
+ state/next-session
325
+ (state/record-plan plan)
326
+ (state/record-loading engine text snippet-name))))
327
+ (emit-event {:type "ttsPlanCreated"
328
+ :agency "tts"
329
+ :plan plan})
330
+ (emit-status!)
331
+ (if-not (:ok plan)
332
+ (emit-error! (plan-failure-message plan))
333
+ (do
334
+ ((:start-session session-scheduler) (session-id) engine snippet-name)
335
+ (case engine
336
+ "azure" (start-azure! (session-id) command snippet-name)
337
+ "webSpeech" (start-web-speech! (session-id) command snippet-name)
338
+ (emit-error! (str "Unsupported TTS engine: " engine)))))))
339
+
340
+ (load-voices! [engine]
341
+ (let [config (:config @state-atom)
342
+ ;; Voice loading has its own plan so missing backend/browser
343
+ ;; support is visible before any provider request is made.
344
+ request-id (next-voice-load! engine)
345
+ world {:backendUrl (:backendUrl config)
346
+ :hasAzureVoices (boolean (custom-provider config :azureVoices))
347
+ :hasWebSpeech (boolean (or (custom-provider config :webSpeechVoices)
348
+ (speech-synthesis*)))}
349
+ plan (planner/plan-voice-load engine world)]
350
+ (emit-event {:type "ttsPlanCreated"
351
+ :agency "tts"
352
+ :plan plan})
353
+ (if-not (:ok plan)
354
+ (do
355
+ (when (= "azure" engine)
356
+ (swap! state-atom
357
+ state/record-azure-status
358
+ "error"
359
+ (plan-failure-message plan)
360
+ []))
361
+ (emit-error! (plan-failure-message plan)))
362
+ (case engine
363
+ "webSpeech"
364
+ (-> (load-web-speech-voices! config)
365
+ (.then (fn [voices]
366
+ (when (active-voice-load? engine request-id)
367
+ (let [normalized (normalize-voices
368
+ (js->clj voices :keywordize-keys true)
369
+ "webSpeech")]
370
+ (swap! state-atom state/record-web-speech-voices normalized)
371
+ (emit-event {:type "ttsVoicesLoaded"
372
+ :agency "tts"
373
+ :engine "webSpeech"
374
+ :voices normalized})
375
+ (emit-status!)))))
376
+ (.catch (fn [error]
377
+ (when (active-voice-load? engine request-id)
378
+ (emit-error! (.-message error))))))
379
+
380
+ "azure"
381
+ (-> (load-azure-voices! config)
382
+ (.then (fn [response]
383
+ (when (active-voice-load? engine request-id)
384
+ (let [data (js->clj response :keywordize-keys true)
385
+ voices (normalize-voices (:voices data) "azure")
386
+ ready? (and (:valid data) (seq voices))
387
+ status (if ready? "ready" "error")
388
+ message (if ready?
389
+ (str "Connected via backend environment (" (count voices) " voices).")
390
+ (or (:message data) "Backend Azure credentials are not configured."))]
391
+ (swap! state-atom state/record-azure-status status message voices)
392
+ (emit-event {:type "ttsVoicesLoaded"
393
+ :agency "tts"
394
+ :engine "azure"
395
+ :voices voices
396
+ :status status
397
+ :message message})
398
+ (emit-status!)))))
399
+ (.catch (fn [error]
400
+ (when (active-voice-load? engine request-id)
401
+ (swap! state-atom state/record-azure-status "error" (.-message error) [])
402
+ (emit-error! (.-message error))))))
403
+
404
+ (emit-error! (str "Unsupported voice provider: " engine))))))
405
+
406
+ (run-action! [action]
407
+ (case (:op action)
408
+ "cancel-voice-loads"
409
+ (cancel-voice-loads!)
410
+
411
+ "configure"
412
+ (do
413
+ (swap! state-atom state/configure (:config action))
414
+ (emit-status!))
415
+
416
+ "load-voices"
417
+ (load-voices! (:engine action))
418
+
419
+ "stop-active-session"
420
+ (when (active-speech?)
421
+ (stop-session! (:reason action)))
422
+
423
+ "advance-session"
424
+ (swap! state-atom state/next-session)
425
+
426
+ "stop-session"
427
+ (stop-session! (:reason action))
428
+
429
+ "speak"
430
+ (speak! (:command action))
431
+
432
+ "reset"
433
+ (do
434
+ (stop-web-speech!)
435
+ ((:stop-all session-scheduler))
436
+ (cleanup-audio! resources)
437
+ (reset! state-atom (state/default-state nil))
438
+ (emit-status!))
439
+
440
+ "error"
441
+ (emit-error! (:message action))
442
+
443
+ (emit-error! (str "Unknown TTS planner action: " (:op action)))))
444
+
445
+ (dispatch! [command]
446
+ (when-not @disposed?
447
+ (let [payload (js->clj command :keywordize-keys true)]
448
+ (emit-input {:type "command"
449
+ :agency "tts"
450
+ :command payload})
451
+ (doseq [action (planner/plan-command @state-atom payload)]
452
+ (run-action! action)))))]
453
+ #js {:dispatch dispatch!
454
+ :snapshot (fn [] (state/visible-state @state-atom))
455
+ :schedulerQueue (fn [] (clj->js ((:queue session-scheduler))))
456
+ :input (stream/writable-port input-stream dispatch!)
457
+ :events (stream/readable-port event-stream)
458
+ :subscribeInput (fn [listener] ((:subscribe input-stream) listener))
459
+ :subscribeEvents (fn [listener] ((:subscribe event-stream) listener))
460
+ :subscribe (fn [listener] ((:subscribe event-stream) listener))
461
+ :subscribeStatus (fn [listener] ((:subscribe event-stream) listener))
462
+ :configure (fn [next-config]
463
+ (dispatch! #js {:type "configure" :config next-config})
464
+ (when (= "none" (get-in @state-atom [:config :webSpeechReferenceMode]))
465
+ (clear-display-media-reference! resources)
466
+ (notify-playback-reference-track!)))
467
+ :loadVoices (fn
468
+ ([] (dispatch! #js {:type "loadVoices"}))
469
+ ([engine] (dispatch! #js {:type "loadVoices" :engine engine})))
470
+ :speak (fn [text] (dispatch! #js {:type "speak" :text text}))
471
+ :stop (fn [] (dispatch! #js {:type "stop"}))
472
+ :reset (fn [] (dispatch! #js {:type "reset"}))
473
+ :getPlaybackReferenceTrack (fn [] (get-playback-reference-track resources))
474
+ :onPlaybackReferenceTrackChange (fn [listener]
475
+ (swap! playback-reference-listeners conj listener)
476
+ (listener (get-playback-reference-track resources))
477
+ (fn []
478
+ (swap! playback-reference-listeners disj listener)))
479
+ :preparePlaybackReference (fn []
480
+ (let [config (:config @state-atom)]
481
+ (cond
482
+ (and (= "webSpeech" (:engine config))
483
+ (= "displayMedia" (:webSpeechReferenceMode config)))
484
+ (-> (ensure-web-speech-display-media-reference! resources)
485
+ (.then (fn [status]
486
+ (notify-playback-reference-track!)
487
+ status)))
488
+
489
+ (get-playback-reference-track resources)
490
+ (js/Promise.resolve "available")
491
+
492
+ :else
493
+ (js/Promise.resolve "unavailable"))))
494
+ :dispose (fn []
495
+ (when-not @disposed?
496
+ (reset! disposed? true)
497
+ (cancel-voice-loads!)
498
+ ((:dispose session-scheduler))
499
+ (stop-web-speech!)
500
+ (dispose-playback-resources! resources)
501
+ (reset! playback-reference-listeners #{})
502
+ ((:dispose input-stream))
503
+ ((:dispose event-stream))))})))