@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.
- package/README.md +226 -0
- package/dist/index.d.ts +2603 -0
- package/dist/index.js +1031 -0
- package/dist/test.cjs +1519 -0
- package/dist/worker/agency-worker.js +972 -0
- package/dist/worker/manifest.edn +1 -0
- package/package.json +57 -0
- package/shadow-cljs.edn +173 -0
- package/src/polymer/animation/agency.cljs +84 -0
- package/src/polymer/animation/domain.cljs +110 -0
- package/src/polymer/animation/planner.cljs +88 -0
- package/src/polymer/animation/runtime.cljs +137 -0
- package/src/polymer/animation/scheduler.cljs +178 -0
- package/src/polymer/animation/service.cljs +981 -0
- package/src/polymer/animation/state.cljs +112 -0
- package/src/polymer/blink/agency.cljs +131 -0
- package/src/polymer/blink/planner.cljs +115 -0
- package/src/polymer/blink/scheduler.cljs +90 -0
- package/src/polymer/blink/snippet.cljs +50 -0
- package/src/polymer/blink/state.cljs +102 -0
- package/src/polymer/camera_context/agency.cljs +132 -0
- package/src/polymer/camera_context/domain.cljs +186 -0
- package/src/polymer/camera_context/planner.cljs +50 -0
- package/src/polymer/camera_context/scheduler.cljs +113 -0
- package/src/polymer/camera_context/state.cljs +121 -0
- package/src/polymer/character.cljs +471 -0
- package/src/polymer/conversation/agency.cljs +83 -0
- package/src/polymer/conversation/domain.cljs +35 -0
- package/src/polymer/conversation/effector.cljs +130 -0
- package/src/polymer/conversation/planner.cljs +178 -0
- package/src/polymer/conversation/scheduler.cljs +202 -0
- package/src/polymer/conversation/service.cljs +329 -0
- package/src/polymer/conversation/state.cljs +186 -0
- package/src/polymer/core.cljs +172 -0
- package/src/polymer/eye_head/agency.cljs +75 -0
- package/src/polymer/eye_head/domain.cljs +133 -0
- package/src/polymer/eye_head/goap.cljs +106 -0
- package/src/polymer/eye_head/scheduler.cljs +176 -0
- package/src/polymer/eye_head/service.cljs +259 -0
- package/src/polymer/eye_head/snippet.cljs +92 -0
- package/src/polymer/eye_head/state.cljs +167 -0
- package/src/polymer/gaze/agency.cljs +85 -0
- package/src/polymer/gaze/domain.cljs +211 -0
- package/src/polymer/gaze/goap.cljs +208 -0
- package/src/polymer/gaze/scheduler.cljs +184 -0
- package/src/polymer/gaze/state.cljs +202 -0
- package/src/polymer/gesture/agency.cljs +185 -0
- package/src/polymer/gesture/goap.cljs +461 -0
- package/src/polymer/gesture/scheduler.cljs +122 -0
- package/src/polymer/gesture/snippet.cljs +198 -0
- package/src/polymer/gesture/state.cljs +273 -0
- package/src/polymer/lipsync/agency.cljs +593 -0
- package/src/polymer/lipsync/articulation/jaw.cljs +258 -0
- package/src/polymer/lipsync/articulation/snippet.cljs +605 -0
- package/src/polymer/lipsync/articulation/tongue.cljs +418 -0
- package/src/polymer/lipsync/articulation/visemes.cljs +429 -0
- package/src/polymer/lipsync/goap.cljs +132 -0
- package/src/polymer/lipsync/pronunciation/effector.cljs +51 -0
- package/src/polymer/lipsync/scheduler.cljs +302 -0
- package/src/polymer/lipsync/state.cljs +202 -0
- package/src/polymer/nlp/phonemes.cljs +164 -0
- package/src/polymer/prosodic/agency.cljs +196 -0
- package/src/polymer/prosodic/goap.cljs +122 -0
- package/src/polymer/prosodic/scheduler.cljs +99 -0
- package/src/polymer/prosodic/snippet.cljs +106 -0
- package/src/polymer/prosodic/state.cljs +139 -0
- package/src/polymer/runtime/embody.cljs +117 -0
- package/src/polymer/runtime/embody_wasm.cljs +177 -0
- package/src/polymer/runtime/rust_embody_host.cljs +645 -0
- package/src/polymer/society.cljs +239 -0
- package/src/polymer/stream.cljs +130 -0
- package/src/polymer/transcription/agency.cljs +69 -0
- package/src/polymer/transcription/domain.cljs +68 -0
- package/src/polymer/transcription/effector.cljs +611 -0
- package/src/polymer/transcription/planner.cljs +203 -0
- package/src/polymer/transcription/scheduler.cljs +225 -0
- package/src/polymer/transcription/service.cljs +209 -0
- package/src/polymer/transcription/state.cljs +239 -0
- package/src/polymer/tts/agency.cljs +503 -0
- package/src/polymer/tts/azure.cljs +290 -0
- package/src/polymer/tts/planner.cljs +178 -0
- package/src/polymer/tts/runtime.cljs +624 -0
- package/src/polymer/tts/scheduler.cljs +162 -0
- package/src/polymer/tts/state.cljs +191 -0
- package/src/polymer/worker.cljs +70 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
(ns polymer.tts.azure
|
|
2
|
+
(:require [clojure.string :as str]
|
|
3
|
+
[polymer.lipsync.state :as state]
|
|
4
|
+
[polymer.lipsync.articulation.visemes :as visemes]))
|
|
5
|
+
|
|
6
|
+
;; Azure Speech emits SAPI-style viseme IDs 0-21. Embody expects the
|
|
7
|
+
;; canonical 15-slot CC4/ARKit order. This namespace is pure normalization:
|
|
8
|
+
;; provider event data in, Polymer viseme timeline data out.
|
|
9
|
+
;;
|
|
10
|
+
;; This lives under TTS, not LipSync articulation, because Azure IDs are provider
|
|
11
|
+
;; facts. The namespace imports LipSync's canonical viseme definitions only to
|
|
12
|
+
;; output the provider-neutral timeline that LipSync can articulate like any
|
|
13
|
+
;; other text/timeline source.
|
|
14
|
+
|
|
15
|
+
(def azure->canonical
|
|
16
|
+
{0 (:B_M_P visemes/canonical-visemes)
|
|
17
|
+
1 (:AE visemes/canonical-visemes)
|
|
18
|
+
2 (:Ah visemes/canonical-visemes)
|
|
19
|
+
3 (:Oh visemes/canonical-visemes)
|
|
20
|
+
4 (:Ih visemes/canonical-visemes)
|
|
21
|
+
5 (:Er visemes/canonical-visemes)
|
|
22
|
+
6 (:Ih visemes/canonical-visemes)
|
|
23
|
+
7 (:W_OO visemes/canonical-visemes)
|
|
24
|
+
8 (:Oh visemes/canonical-visemes)
|
|
25
|
+
9 (:Ah visemes/canonical-visemes)
|
|
26
|
+
10 (:Oh visemes/canonical-visemes)
|
|
27
|
+
11 (:Ah visemes/canonical-visemes)
|
|
28
|
+
12 (:K_G_H_NG visemes/canonical-visemes)
|
|
29
|
+
13 (:R visemes/canonical-visemes)
|
|
30
|
+
14 (:T_L_D_N visemes/canonical-visemes)
|
|
31
|
+
15 (:S_Z visemes/canonical-visemes)
|
|
32
|
+
16 (:Ch_J visemes/canonical-visemes)
|
|
33
|
+
17 (:Th visemes/canonical-visemes)
|
|
34
|
+
18 (:F_V visemes/canonical-visemes)
|
|
35
|
+
19 (:T_L_D_N visemes/canonical-visemes)
|
|
36
|
+
20 (:K_G_H_NG visemes/canonical-visemes)
|
|
37
|
+
21 (:B_M_P visemes/canonical-visemes)})
|
|
38
|
+
|
|
39
|
+
(def min-duration-ms
|
|
40
|
+
{:silence 0 :bilabial 55 :vowel 120 :fricative 80 :tongue 80 :liquid 90 :glide 100 :default 85})
|
|
41
|
+
|
|
42
|
+
(def max-duration-ms
|
|
43
|
+
{:silence 0 :bilabial 110 :vowel 220 :fricative 150 :tongue 140 :liquid 160 :glide 180 :default 150})
|
|
44
|
+
|
|
45
|
+
(def max-overlap-ms
|
|
46
|
+
{:silence 0 :bilabial 8 :vowel 90 :fricative 45 :tongue 45 :liquid 60 :glide 65 :default 45})
|
|
47
|
+
|
|
48
|
+
(def duplicate-window-ms 35)
|
|
49
|
+
(def diphthong-min-duration-ms 85)
|
|
50
|
+
(def diphthong-secondary-min-ms 38)
|
|
51
|
+
|
|
52
|
+
(def azure-vowels
|
|
53
|
+
#{(:AE visemes/canonical-visemes)
|
|
54
|
+
(:Ah visemes/canonical-visemes)
|
|
55
|
+
(:EE visemes/canonical-visemes)
|
|
56
|
+
(:Er visemes/canonical-visemes)
|
|
57
|
+
(:Ih visemes/canonical-visemes)
|
|
58
|
+
(:Oh visemes/canonical-visemes)
|
|
59
|
+
(:W_OO visemes/canonical-visemes)})
|
|
60
|
+
|
|
61
|
+
(defn normalized-word [word]
|
|
62
|
+
(-> (or word "") str/lower-case (str/replace #"[^a-z]" "")))
|
|
63
|
+
|
|
64
|
+
(defn viseme-class [provider-id canonical-id]
|
|
65
|
+
(cond
|
|
66
|
+
(= provider-id 0) :silence
|
|
67
|
+
(= canonical-id (:B_M_P visemes/canonical-visemes)) :bilabial
|
|
68
|
+
(contains? azure-vowels canonical-id) (if (= canonical-id (:W_OO visemes/canonical-visemes)) :glide :vowel)
|
|
69
|
+
(#{(:Ch_J visemes/canonical-visemes)
|
|
70
|
+
(:F_V visemes/canonical-visemes)
|
|
71
|
+
(:S_Z visemes/canonical-visemes)
|
|
72
|
+
(:Th visemes/canonical-visemes)} canonical-id) :fricative
|
|
73
|
+
(#{(:K_G_H_NG visemes/canonical-visemes)
|
|
74
|
+
(:T_L_D_N visemes/canonical-visemes)} canonical-id) :tongue
|
|
75
|
+
(= canonical-id (:R visemes/canonical-visemes)) :liquid
|
|
76
|
+
:else :default))
|
|
77
|
+
|
|
78
|
+
(defn normalize-provider-viseme [event]
|
|
79
|
+
(let [id (or (:visemeId event) (:viseme_id event) (:id event) 0)
|
|
80
|
+
time (or (:time event) (:audio_offset event) (:audioOffset event) 0)]
|
|
81
|
+
(when (state/finite-number? time)
|
|
82
|
+
{:visemeId id
|
|
83
|
+
:time time})))
|
|
84
|
+
|
|
85
|
+
(defn normalize-provider-visemes [events]
|
|
86
|
+
;; This is the one place a local transducer is useful: normalize each provider
|
|
87
|
+
;; row and drop malformed rows in one pass. Sorting is intentionally outside
|
|
88
|
+
;; the transducer because ordering is a whole-collection operation.
|
|
89
|
+
(->> (into [] (keep normalize-provider-viseme) (or events []))
|
|
90
|
+
(sort-by :time)
|
|
91
|
+
vec))
|
|
92
|
+
|
|
93
|
+
(defn normalize-azure-synthesis
|
|
94
|
+
"Return the pure provider packet TTS uses after Azure synthesis.
|
|
95
|
+
|
|
96
|
+
Audio fields are normalized here for playback. Visemes and word timings stay
|
|
97
|
+
raw until the Azure provider mapper and LipSync agency normalize them into a
|
|
98
|
+
provider-neutral animation timeline."
|
|
99
|
+
[payload]
|
|
100
|
+
{:audioBase64 (or (:audio_base64 payload) (:audioBase64 payload))
|
|
101
|
+
:audioFormat (or (:audio_format payload) (:audioFormat payload))
|
|
102
|
+
:durationSec (state/number-or (or (:durationSec payload)
|
|
103
|
+
(:duration_sec payload)
|
|
104
|
+
(:duration payload))
|
|
105
|
+
0)
|
|
106
|
+
:visemes (:visemes payload)
|
|
107
|
+
:wordTimings (or (:word_boundaries payload)
|
|
108
|
+
(:wordTimings payload))})
|
|
109
|
+
|
|
110
|
+
(defn azure-synthesis->lipsync-command
|
|
111
|
+
"Build the LipSync command from a normalized Azure synthesis packet."
|
|
112
|
+
[snippet-name text payload config]
|
|
113
|
+
{:type "processAzureVisemes"
|
|
114
|
+
:name snippet-name
|
|
115
|
+
:text text
|
|
116
|
+
:source "azure"
|
|
117
|
+
:visemes (:visemes payload)
|
|
118
|
+
:wordTimings (or (:wordTimings payload)
|
|
119
|
+
(:word_timings payload)
|
|
120
|
+
(:word_boundaries payload))
|
|
121
|
+
:totalDurationMs (js/Math.round (* (state/number-or (:durationSec payload) 0) 1000))
|
|
122
|
+
:options {:visualLeadMs (:visualLeadMs config)}})
|
|
123
|
+
|
|
124
|
+
(defn word-start-sec [word]
|
|
125
|
+
(state/number-or (or (:startSec word) (:start word) (:start_time word)) 0))
|
|
126
|
+
|
|
127
|
+
(defn word-end-sec [word]
|
|
128
|
+
(state/number-or (or (:endSec word) (:end word) (:end_time word)) (word-start-sec word)))
|
|
129
|
+
|
|
130
|
+
(defn find-word-at-time [time-sec word-timings]
|
|
131
|
+
(some (fn [word]
|
|
132
|
+
(when (and (>= time-sec (- (word-start-sec word) 0.02))
|
|
133
|
+
(<= time-sec (+ (word-end-sec word) 0.02)))
|
|
134
|
+
word))
|
|
135
|
+
word-timings))
|
|
136
|
+
|
|
137
|
+
(defn long-e-word? [word]
|
|
138
|
+
(or (boolean (re-find #"(?:ee|ea|ie|ei)" word))
|
|
139
|
+
(contains? #{"we" "me" "be" "he" "she" "see"} word)
|
|
140
|
+
(str/ends-with? word "y")))
|
|
141
|
+
|
|
142
|
+
(defn rounded-back? [event-time-sec word]
|
|
143
|
+
(let [text (normalized-word (:word word))]
|
|
144
|
+
(and (pos? (count text))
|
|
145
|
+
(boolean (re-find #"(?:oo|ew|ue|ui|ough|ow|oa|oe|ose|ole|old|own|o)$" text))
|
|
146
|
+
(let [start-sec (word-start-sec word)
|
|
147
|
+
end-sec (word-end-sec word)
|
|
148
|
+
duration-sec (max 0.001 (- end-sec start-sec))
|
|
149
|
+
progress (state/clamp 0 1 (/ (- event-time-sec start-sec) duration-sec))]
|
|
150
|
+
(>= progress 0.35)))))
|
|
151
|
+
|
|
152
|
+
(defn dental-th? [event-time-sec word]
|
|
153
|
+
(let [text (normalized-word (:word word))]
|
|
154
|
+
(and (str/includes? text "th")
|
|
155
|
+
(let [start-sec (word-start-sec word)
|
|
156
|
+
end-sec (word-end-sec word)
|
|
157
|
+
duration-sec (max 0.001 (- end-sec start-sec))
|
|
158
|
+
progress (state/clamp 0 1 (/ (- event-time-sec start-sec) duration-sec))
|
|
159
|
+
positions (loop [index (str/index-of text "th")
|
|
160
|
+
result []]
|
|
161
|
+
(if (nil? index)
|
|
162
|
+
result
|
|
163
|
+
(recur (str/index-of text "th" (inc index)) (conj result index))))]
|
|
164
|
+
(some (fn [index]
|
|
165
|
+
(let [th-progress (if (<= (count text) 2) 0 (/ index (max 1 (- (count text) 2))))
|
|
166
|
+
starts-word? (= index 0)
|
|
167
|
+
ends-word? (>= index (- (count text) 2))]
|
|
168
|
+
(or (and starts-word? (<= progress 0.45))
|
|
169
|
+
(and ends-word? (>= progress 0.55))
|
|
170
|
+
(<= (js/Math.abs (- progress th-progress)) 0.22))))
|
|
171
|
+
positions)))))
|
|
172
|
+
|
|
173
|
+
(defn refine-for-word [provider-id canonical-id event-time-sec word]
|
|
174
|
+
(let [text (normalized-word (:word word))]
|
|
175
|
+
(cond
|
|
176
|
+
(= provider-id 0) nil
|
|
177
|
+
(and (= provider-id 6) (long-e-word? text)) (:EE visemes/canonical-visemes)
|
|
178
|
+
(and (= provider-id 4) (rounded-back? event-time-sec word)) (:W_OO visemes/canonical-visemes)
|
|
179
|
+
(and (= provider-id 19) (dental-th? event-time-sec word)) (:Th visemes/canonical-visemes)
|
|
180
|
+
:else canonical-id)))
|
|
181
|
+
|
|
182
|
+
(defn mapped-events [normalized options]
|
|
183
|
+
(loop [remaining normalized
|
|
184
|
+
result []]
|
|
185
|
+
(if (empty? remaining)
|
|
186
|
+
result
|
|
187
|
+
(let [event (first remaining)
|
|
188
|
+
provider-id (:visemeId event)
|
|
189
|
+
base-canonical (get azure->canonical provider-id (:B_M_P visemes/canonical-visemes))
|
|
190
|
+
word (find-word-at-time (:time event) (:wordTimings options))
|
|
191
|
+
canonical-id (refine-for-word provider-id base-canonical (:time event) word)
|
|
192
|
+
previous (last result)
|
|
193
|
+
duplicate? (and previous
|
|
194
|
+
canonical-id
|
|
195
|
+
(= (:canonicalId previous) canonical-id)
|
|
196
|
+
(< (js/Math.abs (- (* 1000 (:time event))
|
|
197
|
+
(* 1000 (:time previous))))
|
|
198
|
+
duplicate-window-ms))]
|
|
199
|
+
(recur (rest remaining)
|
|
200
|
+
(if (or (nil? canonical-id) duplicate?)
|
|
201
|
+
result
|
|
202
|
+
(conj result (assoc event
|
|
203
|
+
:canonicalId canonical-id
|
|
204
|
+
:className (viseme-class provider-id canonical-id)))))))))
|
|
205
|
+
|
|
206
|
+
(defn clamp-duration [duration-ms min-ms max-ms remaining-ms]
|
|
207
|
+
(let [bounded (min (max duration-ms min-ms) max-ms remaining-ms)]
|
|
208
|
+
(max 0 (js/Math.round bounded))))
|
|
209
|
+
|
|
210
|
+
(defn duration-ms [current next-event total-duration-ms]
|
|
211
|
+
(let [offset-ms (max 0 (js/Math.round (* (:time current) 1000)))
|
|
212
|
+
remaining-ms (if (state/finite-number? total-duration-ms)
|
|
213
|
+
(max 0 (- total-duration-ms offset-ms))
|
|
214
|
+
js/Number.POSITIVE_INFINITY)
|
|
215
|
+
class-name (:className current)
|
|
216
|
+
fallback-ms (if (= class-name :silence) (max-duration-ms :silence) (min-duration-ms class-name))
|
|
217
|
+
raw-span-ms (if next-event
|
|
218
|
+
(max 0 (js/Math.round (* (- (:time next-event) (:time current)) 1000)))
|
|
219
|
+
(min fallback-ms remaining-ms))]
|
|
220
|
+
(if (= class-name :silence)
|
|
221
|
+
(clamp-duration raw-span-ms 0 (max-duration-ms :silence) remaining-ms)
|
|
222
|
+
(let [next-closure? (= (:className next-event) :bilabial)
|
|
223
|
+
overlap-ms (if next-closure? 8 (max-overlap-ms class-name))
|
|
224
|
+
desired-ms (max raw-span-ms (min-duration-ms class-name))
|
|
225
|
+
max-ms (min (max-duration-ms class-name) (+ raw-span-ms overlap-ms))]
|
|
226
|
+
(clamp-duration desired-ms 1 max-ms remaining-ms)))))
|
|
227
|
+
|
|
228
|
+
(defn push-event
|
|
229
|
+
([timeline viseme-id offset-ms duration-ms]
|
|
230
|
+
(push-event timeline viseme-id offset-ms duration-ms nil))
|
|
231
|
+
([timeline viseme-id offset-ms duration-ms extra-classes]
|
|
232
|
+
(if (<= duration-ms 0)
|
|
233
|
+
timeline
|
|
234
|
+
(let [classes (vec (distinct (concat (visemes/viseme-classes viseme-id)
|
|
235
|
+
(or extra-classes []))))]
|
|
236
|
+
(conj timeline {:visemeId viseme-id
|
|
237
|
+
:phonemeClass (visemes/primary-class classes)
|
|
238
|
+
:phonemeClasses classes
|
|
239
|
+
:jawActivation (visemes/jaw-activation-for-viseme viseme-id)
|
|
240
|
+
:offsetMs (max 0 (js/Math.round offset-ms))
|
|
241
|
+
:durationMs (max 1 (js/Math.round duration-ms))})))))
|
|
242
|
+
|
|
243
|
+
(defn diphthong-targets [provider-id]
|
|
244
|
+
(case provider-id
|
|
245
|
+
8 [(:Oh visemes/canonical-visemes) (:W_OO visemes/canonical-visemes)]
|
|
246
|
+
9 [(:Ah visemes/canonical-visemes) (:W_OO visemes/canonical-visemes)]
|
|
247
|
+
10 [(:Oh visemes/canonical-visemes) (:EE visemes/canonical-visemes)]
|
|
248
|
+
11 [(:Ah visemes/canonical-visemes) (:Ih visemes/canonical-visemes)]
|
|
249
|
+
nil))
|
|
250
|
+
|
|
251
|
+
(defn push-expanded-event [timeline provider-id canonical-id offset-ms duration-ms]
|
|
252
|
+
(let [targets (diphthong-targets provider-id)]
|
|
253
|
+
(if (or (nil? targets) (< duration-ms diphthong-min-duration-ms))
|
|
254
|
+
(push-event timeline canonical-id offset-ms duration-ms)
|
|
255
|
+
(let [[first-viseme second-viseme] targets
|
|
256
|
+
second-offset-ms (min (+ offset-ms duration-ms (- diphthong-secondary-min-ms))
|
|
257
|
+
(+ offset-ms (* duration-ms 0.55)))
|
|
258
|
+
first-duration-ms (max diphthong-secondary-min-ms
|
|
259
|
+
(min duration-ms (+ (- second-offset-ms offset-ms)
|
|
260
|
+
(* duration-ms 0.25))))
|
|
261
|
+
second-duration-ms (max diphthong-secondary-min-ms
|
|
262
|
+
(- (+ offset-ms duration-ms) second-offset-ms))]
|
|
263
|
+
;; Azure provider IDs 8-11 represent diphthong motion. Preserve that
|
|
264
|
+
;; fact after expansion so the jaw planner keeps one vocalic jaw arc
|
|
265
|
+
;; while the lips travel through the two canonical targets.
|
|
266
|
+
(-> timeline
|
|
267
|
+
(push-event first-viseme offset-ms first-duration-ms ["diphthong"])
|
|
268
|
+
(push-event second-viseme second-offset-ms second-duration-ms ["diphthong"]))))))
|
|
269
|
+
|
|
270
|
+
(defn azure-visemes->timeline
|
|
271
|
+
([visemes] (azure-visemes->timeline visemes nil nil))
|
|
272
|
+
([visemes total-duration-ms options]
|
|
273
|
+
(let [options (or options {})
|
|
274
|
+
normalized (normalize-provider-visemes visemes)
|
|
275
|
+
mapped (mapped-events normalized options)]
|
|
276
|
+
(->> (map-indexed (fn [index event]
|
|
277
|
+
{:event event
|
|
278
|
+
:next-event (get mapped (inc index))})
|
|
279
|
+
mapped)
|
|
280
|
+
(reduce (fn [timeline {:keys [event next-event]}]
|
|
281
|
+
(let [offset-ms (max 0 (js/Math.round (* (:time event) 1000)))
|
|
282
|
+
event-duration-ms (duration-ms event next-event total-duration-ms)]
|
|
283
|
+
(push-expanded-event timeline
|
|
284
|
+
(:visemeId event)
|
|
285
|
+
(:canonicalId event)
|
|
286
|
+
offset-ms
|
|
287
|
+
event-duration-ms)))
|
|
288
|
+
[])
|
|
289
|
+
(sort-by :offsetMs)
|
|
290
|
+
vec))))
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
(ns polymer.tts.planner)
|
|
2
|
+
|
|
3
|
+
;; The TTS provider planning layer is intentionally small: it produces data plans, not a
|
|
4
|
+
;; planner framework. The agency still executes the plan explicitly.
|
|
5
|
+
;;
|
|
6
|
+
;; In this package "provider plan" means "turn the requested goal plus available world
|
|
7
|
+
;; facts into an auditable action list." It does not mean hidden control flow.
|
|
8
|
+
;; The browser/backend calls remain in polymer.tts.agency, and pure provider
|
|
9
|
+
;; payload cleanup stays beside the provider boundary in the TTS/Azure agencies.
|
|
10
|
+
|
|
11
|
+
(def supported-engines
|
|
12
|
+
;; Keep supported provider names in one set so speech and voice-load planning
|
|
13
|
+
;; reject unknown strings the same way.
|
|
14
|
+
#{"azure" "webSpeech"})
|
|
15
|
+
|
|
16
|
+
(defn present-string?
|
|
17
|
+
"Treat only non-empty strings as configured provider values."
|
|
18
|
+
[value]
|
|
19
|
+
(and (string? value) (pos? (count value))))
|
|
20
|
+
|
|
21
|
+
(defn provider-ready?
|
|
22
|
+
"A provider is ready when the required browser/backend capability exists."
|
|
23
|
+
[world engine]
|
|
24
|
+
(case engine
|
|
25
|
+
"azure" (boolean (or (present-string? (:backendUrl world))
|
|
26
|
+
(:hasAzureSynthesize world)
|
|
27
|
+
(:hasAzureVoices world)))
|
|
28
|
+
"webSpeech" (boolean (:hasWebSpeech world))
|
|
29
|
+
"livekit" false
|
|
30
|
+
false))
|
|
31
|
+
|
|
32
|
+
(defn failure-step
|
|
33
|
+
"Return the first reason a goal cannot run, or nil when it is executable."
|
|
34
|
+
[goal]
|
|
35
|
+
(cond
|
|
36
|
+
(not (contains? supported-engines (:engine goal)))
|
|
37
|
+
{:op "fail" :reason "unsupported-engine" :engine (:engine goal)}
|
|
38
|
+
|
|
39
|
+
(false? (:hasText goal))
|
|
40
|
+
{:op "fail" :reason "missing-text"}
|
|
41
|
+
|
|
42
|
+
(not (:providerReady goal))
|
|
43
|
+
{:op "fail" :reason "provider-not-ready" :engine (:engine goal)}
|
|
44
|
+
|
|
45
|
+
:else nil))
|
|
46
|
+
|
|
47
|
+
(defn provider-goal
|
|
48
|
+
"Turn a speak command into the provider goal the agency must satisfy."
|
|
49
|
+
[command config world]
|
|
50
|
+
(let [engine (or (:engine command) (:engine config) "webSpeech")]
|
|
51
|
+
{:type "speak"
|
|
52
|
+
:engine engine
|
|
53
|
+
:text (:text command)
|
|
54
|
+
:providerReady (provider-ready? world engine)
|
|
55
|
+
:hasText (pos? (count (or (:text command) "")))}))
|
|
56
|
+
|
|
57
|
+
(defn speech-steps
|
|
58
|
+
"Return the ordered action data for a speech goal."
|
|
59
|
+
[goal]
|
|
60
|
+
(cond
|
|
61
|
+
(failure-step goal)
|
|
62
|
+
[(failure-step goal)]
|
|
63
|
+
|
|
64
|
+
(= "azure" (:engine goal))
|
|
65
|
+
[{:op "configure-lipsync"}
|
|
66
|
+
{:op "synthesize-azure"}
|
|
67
|
+
{:op "play-azure-audio"}
|
|
68
|
+
{:op "emit-word-boundaries"}]
|
|
69
|
+
|
|
70
|
+
(= "webSpeech" (:engine goal))
|
|
71
|
+
[{:op "configure-lipsync"}
|
|
72
|
+
{:op "speak-web-speech"}
|
|
73
|
+
{:op "use-web-speech-boundaries"}]
|
|
74
|
+
|
|
75
|
+
:else
|
|
76
|
+
[{:op "fail" :reason "unsupported-engine" :engine (:engine goal)}]))
|
|
77
|
+
|
|
78
|
+
(defn plan-ok?
|
|
79
|
+
"A plan is executable when the first step is not an explicit failure."
|
|
80
|
+
[steps]
|
|
81
|
+
(not= "fail" (:op (first steps))))
|
|
82
|
+
|
|
83
|
+
(defn plan-speech
|
|
84
|
+
"Build an auditable provider plan-style plan for a speak command."
|
|
85
|
+
[command config world]
|
|
86
|
+
(let [goal (provider-goal command config world)
|
|
87
|
+
steps (speech-steps goal)]
|
|
88
|
+
{:goal goal
|
|
89
|
+
:steps steps
|
|
90
|
+
:ok (plan-ok? steps)}))
|
|
91
|
+
|
|
92
|
+
(defn voice-load-goal
|
|
93
|
+
"Turn a load-voices command into a provider availability goal."
|
|
94
|
+
[engine world]
|
|
95
|
+
{:type "loadVoices"
|
|
96
|
+
:engine engine
|
|
97
|
+
:providerReady (provider-ready? world engine)})
|
|
98
|
+
|
|
99
|
+
(defn voice-load-steps
|
|
100
|
+
"Return the ordered action data for loading provider voice choices."
|
|
101
|
+
[goal]
|
|
102
|
+
(cond
|
|
103
|
+
(failure-step goal)
|
|
104
|
+
[(failure-step goal)]
|
|
105
|
+
|
|
106
|
+
(= "azure" (:engine goal))
|
|
107
|
+
[{:op "load-azure-voices"}]
|
|
108
|
+
|
|
109
|
+
(= "webSpeech" (:engine goal))
|
|
110
|
+
[{:op "load-web-speech-voices"}]
|
|
111
|
+
|
|
112
|
+
:else
|
|
113
|
+
[{:op "fail" :reason "unsupported-engine" :engine (:engine goal)}]))
|
|
114
|
+
|
|
115
|
+
(defn plan-voice-load
|
|
116
|
+
"Build a small plan for loading provider voice choices."
|
|
117
|
+
[engine world]
|
|
118
|
+
(let [goal (voice-load-goal engine world)
|
|
119
|
+
steps (voice-load-steps goal)]
|
|
120
|
+
{:goal goal
|
|
121
|
+
:steps steps
|
|
122
|
+
:ok (plan-ok? steps)}))
|
|
123
|
+
|
|
124
|
+
(def active-statuses
|
|
125
|
+
#{"loading" "speaking"})
|
|
126
|
+
|
|
127
|
+
(defn active-speech?
|
|
128
|
+
"Return true when the current state has provider work that must be stopped
|
|
129
|
+
before replacement/reset."
|
|
130
|
+
[tts-state]
|
|
131
|
+
(contains? active-statuses (:status tts-state)))
|
|
132
|
+
|
|
133
|
+
(defn command-action [op extra]
|
|
134
|
+
(merge {:op op} extra))
|
|
135
|
+
|
|
136
|
+
(defn plan-command
|
|
137
|
+
"Plan public TTS dispatch commands into explicit agency actions.
|
|
138
|
+
|
|
139
|
+
This is deliberately separate from provider planning above. `plan-speech` and
|
|
140
|
+
`plan-voice-load` decide whether a provider goal can run. `plan-command`
|
|
141
|
+
decides which local session/voice actions must happen before those provider
|
|
142
|
+
goals are attempted."
|
|
143
|
+
[tts-state payload]
|
|
144
|
+
(case (:type payload)
|
|
145
|
+
;; configure must not cancel in-flight voice loads: hosts dispatch
|
|
146
|
+
;; configure continuously (sliders, engine toggles) and would race the
|
|
147
|
+
;; voice request into a permanent "checking" state. Only reset cancels.
|
|
148
|
+
"configure"
|
|
149
|
+
[(command-action "configure" {:config (:config payload)})]
|
|
150
|
+
|
|
151
|
+
"loadVoices"
|
|
152
|
+
[(command-action "load-voices"
|
|
153
|
+
{:engine (or (:engine payload)
|
|
154
|
+
(get-in tts-state [:config :engine]))})]
|
|
155
|
+
|
|
156
|
+
"speak"
|
|
157
|
+
(cond-> []
|
|
158
|
+
(active-speech? tts-state)
|
|
159
|
+
(conj (command-action "stop-active-session" {:reason "replaced"}))
|
|
160
|
+
|
|
161
|
+
true
|
|
162
|
+
(conj (command-action "speak" {:command payload})))
|
|
163
|
+
|
|
164
|
+
"stop"
|
|
165
|
+
[(command-action "advance-session" {})
|
|
166
|
+
(command-action "stop-session" {:reason "requested"})]
|
|
167
|
+
|
|
168
|
+
"reset"
|
|
169
|
+
(cond-> [(command-action "advance-session" {})
|
|
170
|
+
(command-action "cancel-voice-loads" {})]
|
|
171
|
+
(active-speech? tts-state)
|
|
172
|
+
(conj (command-action "stop-active-session" {:reason "reset"}))
|
|
173
|
+
|
|
174
|
+
true
|
|
175
|
+
(conj (command-action "reset" {})))
|
|
176
|
+
|
|
177
|
+
[(command-action "error"
|
|
178
|
+
{:message (str "Unknown TTS command: " (:type payload))})]))
|