@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,239 @@
|
|
|
1
|
+
(ns polymer.society)
|
|
2
|
+
|
|
3
|
+
;; Default agencySociety seed for hosts and character profiles.
|
|
4
|
+
;;
|
|
5
|
+
;; Mirrors today's hardcoded polymer.character routing. When character.cljs
|
|
6
|
+
;; gains or drops a route, update this seed in the same change.
|
|
7
|
+
;;
|
|
8
|
+
;; See docs/agency-architecture.md (Agency Society Config) and issues #63/#64.
|
|
9
|
+
|
|
10
|
+
(def agency-names
|
|
11
|
+
["animation"
|
|
12
|
+
"blink"
|
|
13
|
+
"gaze"
|
|
14
|
+
"eyeHeadTracking"
|
|
15
|
+
"gesture"
|
|
16
|
+
"cameraContext"
|
|
17
|
+
"transcription"
|
|
18
|
+
"conversation"
|
|
19
|
+
"tts"
|
|
20
|
+
"lipSync"
|
|
21
|
+
"prosodic"])
|
|
22
|
+
|
|
23
|
+
(def required-agency-names
|
|
24
|
+
#{"animation" "transcription" "tts" "lipSync" "conversation"})
|
|
25
|
+
|
|
26
|
+
(defn- agency-entry [name]
|
|
27
|
+
{:required (contains? required-agency-names name)
|
|
28
|
+
:enabled true
|
|
29
|
+
:priors {}
|
|
30
|
+
:configure {}})
|
|
31
|
+
|
|
32
|
+
(defn- edge
|
|
33
|
+
([id from type to required]
|
|
34
|
+
(edge id from type to required nil))
|
|
35
|
+
([id from type to required match]
|
|
36
|
+
(cond-> {:id id
|
|
37
|
+
:from from
|
|
38
|
+
:type type
|
|
39
|
+
:to to
|
|
40
|
+
:required required
|
|
41
|
+
:enabled true
|
|
42
|
+
:credit 1.0}
|
|
43
|
+
match (assoc :match match))))
|
|
44
|
+
|
|
45
|
+
(def default-edges
|
|
46
|
+
[;; Perception → gaze → motor
|
|
47
|
+
(edge "cameraContext.camera.fact→gaze" "cameraContext" "camera.fact" "gaze" true)
|
|
48
|
+
(edge "cameraContext.camera.stale→gaze" "cameraContext" "camera.stale" "gaze" true)
|
|
49
|
+
(edge "gaze.eyeHeadTracking.requestGaze→eyeHeadTracking" "gaze" "eyeHeadTracking.requestGaze" "eyeHeadTracking" true)
|
|
50
|
+
(edge "gaze.eyeHeadTracking.requestReset→eyeHeadTracking" "gaze" "eyeHeadTracking.requestReset" "eyeHeadTracking" true)
|
|
51
|
+
(edge "gaze.eyeHeadTracking.requestCancel→eyeHeadTracking" "gaze" "eyeHeadTracking.requestCancel" "eyeHeadTracking" true)
|
|
52
|
+
|
|
53
|
+
;; Embodiment → animation apply gate
|
|
54
|
+
(edge "blink.animation.requestScheduleSnippet→animation" "blink" "animation.requestScheduleSnippet" "animation" true)
|
|
55
|
+
(edge "eyeHeadTracking.animation.requestScheduleSnippet→animation" "eyeHeadTracking" "animation.requestScheduleSnippet" "animation" true)
|
|
56
|
+
(edge "eyeHeadTracking.animation.requestRemoveSnippet→animation" "eyeHeadTracking" "animation.requestRemoveSnippet" "animation" true)
|
|
57
|
+
(edge "gesture.animation.requestScheduleSnippet→animation" "gesture" "animation.requestScheduleSnippet" "animation" true)
|
|
58
|
+
(edge "gesture.animation.requestRemoveSnippet→animation" "gesture" "animation.requestRemoveSnippet" "animation" true)
|
|
59
|
+
(edge "lipSync.animation.requestScheduleSnippet→animation" "lipSync" "animation.requestScheduleSnippet" "animation" true)
|
|
60
|
+
(edge "lipSync.animation.requestRemoveSnippet→animation" "lipSync" "animation.requestRemoveSnippet" "animation" true)
|
|
61
|
+
(edge "lipSync.animation.requestSeekSnippet→animation" "lipSync" "animation.requestSeekSnippet" "animation" true)
|
|
62
|
+
(edge "prosodic.animation.requestScheduleSnippet→animation" "prosodic" "animation.requestScheduleSnippet" "animation" true)
|
|
63
|
+
(edge "prosodic.animation.requestRemoveSnippet→animation" "prosodic" "animation.requestRemoveSnippet" "animation" true)
|
|
64
|
+
|
|
65
|
+
;; Blink ↔ prosodic coupling (seeded; not required — may be disabled per character)
|
|
66
|
+
(edge "blink.signal.blink-fast→prosodic" "blink" "signal" "prosodic" false {:signal "blink-fast"})
|
|
67
|
+
|
|
68
|
+
;; Speech discourse (required cores)
|
|
69
|
+
(edge "transcription.transcription.final→conversation" "transcription" "transcription.final" "conversation" true)
|
|
70
|
+
(edge "transcription.transcription.interruption→conversation" "transcription" "transcription.interruption" "conversation" true)
|
|
71
|
+
(edge "transcription.transcription.audioInterruption→conversation" "transcription" "transcription.audioInterruption" "conversation" true)
|
|
72
|
+
(edge "conversation.conversation.userUtterance→prosodic" "conversation" "conversation.userUtterance" "prosodic" false)
|
|
73
|
+
(edge "conversation.conversation.userUtterance→gaze" "conversation" "conversation.userUtterance" "gaze" false)
|
|
74
|
+
(edge "conversation.conversation.agentUtterance→prosodic" "conversation" "conversation.agentUtterance" "prosodic" false)
|
|
75
|
+
(edge "conversation.conversation.agentUtterance→gaze" "conversation" "conversation.agentUtterance" "gaze" false)
|
|
76
|
+
(edge "conversation.conversation.requestResponse→prosodic" "conversation" "conversation.requestResponse" "prosodic" false)
|
|
77
|
+
(edge "conversation.tts.requestSpeak→tts" "conversation" "tts.requestSpeak" "tts" true)
|
|
78
|
+
(edge "conversation.conversation.cancelRequested→tts" "conversation" "conversation.cancelRequested" "tts" true)
|
|
79
|
+
(edge "conversation.conversation.cancelRequested→prosodic" "conversation" "conversation.cancelRequested" "prosodic" false)
|
|
80
|
+
(edge "conversation.conversation.cancelRequested→gaze" "conversation" "conversation.cancelRequested" "gaze" false)
|
|
81
|
+
(edge "tts.lipSync.command→lipSync" "tts" "lipSync.command" "lipSync" true)
|
|
82
|
+
(edge "tts.ttsStatusChanged→conversation" "tts" "ttsStatusChanged" "conversation" true)
|
|
83
|
+
(edge "tts.ttsStatusChanged→transcription" "tts" "ttsStatusChanged" "transcription" true)
|
|
84
|
+
(edge "tts.ttsSpeechStarted→prosodic" "tts" "ttsSpeechStarted" "prosodic" true)
|
|
85
|
+
(edge "tts.ttsSpeechStarted→transcription" "tts" "ttsSpeechStarted" "transcription" true)
|
|
86
|
+
(edge "tts.ttsSpeechStarted→conversation" "tts" "ttsSpeechStarted" "conversation" true)
|
|
87
|
+
(edge "tts.ttsWordBoundary→prosodic" "tts" "ttsWordBoundary" "prosodic" true)
|
|
88
|
+
(edge "tts.ttsSpeechStopped→prosodic" "tts" "ttsSpeechStopped" "prosodic" true)
|
|
89
|
+
(edge "tts.ttsSpeechStopped→transcription" "tts" "ttsSpeechStopped" "transcription" true)
|
|
90
|
+
(edge "tts.ttsSpeechStopped→conversation" "tts" "ttsSpeechStopped" "conversation" true)
|
|
91
|
+
(edge "tts.ttsSpeechEnded→prosodic" "tts" "ttsSpeechEnded" "prosodic" true)
|
|
92
|
+
(edge "tts.ttsSpeechEnded→transcription" "tts" "ttsSpeechEnded" "transcription" true)
|
|
93
|
+
(edge "tts.ttsSpeechEnded→conversation" "tts" "ttsSpeechEnded" "conversation" true)])
|
|
94
|
+
|
|
95
|
+
(def default-agency-society
|
|
96
|
+
{:version 1
|
|
97
|
+
:agencies (into {} (map (fn [name] [name (agency-entry name)]) agency-names))
|
|
98
|
+
:edges default-edges
|
|
99
|
+
:kLines []
|
|
100
|
+
:cb5t {:E 0.5 :N 0.5 :C 0.5 :A 0.5 :O 0.5}
|
|
101
|
+
:personaPackId nil
|
|
102
|
+
:characterRollup {:type "weightedSum"}})
|
|
103
|
+
|
|
104
|
+
(def DEFAULT_AGENCY_SOCIETY
|
|
105
|
+
"JS-facing default agencySociety seed (plain JS object)."
|
|
106
|
+
(clj->js default-agency-society))
|
|
107
|
+
|
|
108
|
+
(defn default-edge-ids
|
|
109
|
+
"Return the set of default edge ids for sync tests."
|
|
110
|
+
[]
|
|
111
|
+
(set (map :id default-edges)))
|
|
112
|
+
|
|
113
|
+
(defn value->clj [value]
|
|
114
|
+
(cond
|
|
115
|
+
(nil? value) nil
|
|
116
|
+
(map? value) value
|
|
117
|
+
:else (js->clj value :keywordize-keys true)))
|
|
118
|
+
|
|
119
|
+
(defn finite-number? [value]
|
|
120
|
+
(and (number? value) (js/isFinite value)))
|
|
121
|
+
|
|
122
|
+
(defn clamp-credit [value]
|
|
123
|
+
(if (finite-number? value)
|
|
124
|
+
(max 0 (min 1 value))
|
|
125
|
+
1.0))
|
|
126
|
+
|
|
127
|
+
(defn normalize-map [value]
|
|
128
|
+
(let [resolved (value->clj value)]
|
|
129
|
+
(if (map? resolved) resolved {})))
|
|
130
|
+
|
|
131
|
+
(defn normalize-agency-entry [name provided]
|
|
132
|
+
(let [base (agency-entry name)
|
|
133
|
+
merged (merge base (normalize-map provided))
|
|
134
|
+
required? (contains? required-agency-names name)]
|
|
135
|
+
(assoc merged
|
|
136
|
+
:required required?
|
|
137
|
+
:enabled (if required? true (not (false? (:enabled merged))))
|
|
138
|
+
:priors (normalize-map (:priors merged))
|
|
139
|
+
:configure (normalize-map (:configure merged)))))
|
|
140
|
+
|
|
141
|
+
(defn valid-edge? [edge]
|
|
142
|
+
(and (string? (:id edge))
|
|
143
|
+
(string? (:from edge))
|
|
144
|
+
(string? (:type edge))
|
|
145
|
+
(string? (:to edge))))
|
|
146
|
+
|
|
147
|
+
(defn normalize-edge [base provided]
|
|
148
|
+
(let [merged (merge base (normalize-map provided))
|
|
149
|
+
required? (true? (:required base))
|
|
150
|
+
requested-credit (clamp-credit (:credit merged))
|
|
151
|
+
credit (if (and required? (not (pos? requested-credit)))
|
|
152
|
+
(clamp-credit (:credit base))
|
|
153
|
+
requested-credit)]
|
|
154
|
+
(assoc merged
|
|
155
|
+
:required required?
|
|
156
|
+
:enabled (if required? true (not (false? (:enabled merged))))
|
|
157
|
+
:credit credit
|
|
158
|
+
:match (normalize-map (:match merged)))))
|
|
159
|
+
|
|
160
|
+
(defn normalize-agency-society
|
|
161
|
+
"Merge a profile society document over the canonical seed.
|
|
162
|
+
|
|
163
|
+
Required agencies and edges cannot be disabled by profile data. Optional
|
|
164
|
+
custom edges are retained when they have a complete data shape."
|
|
165
|
+
[value]
|
|
166
|
+
(let [provided (normalize-map value)
|
|
167
|
+
provided-agencies (normalize-map (:agencies provided))
|
|
168
|
+
provided-edges (->> (or (:edges provided) [])
|
|
169
|
+
(map normalize-map)
|
|
170
|
+
(filter #(string? (:id %))))
|
|
171
|
+
provided-by-id (into {} (map (juxt :id identity) provided-edges))
|
|
172
|
+
default-ids (default-edge-ids)
|
|
173
|
+
normalized-defaults (mapv (fn [base]
|
|
174
|
+
(normalize-edge base (get provided-by-id (:id base))))
|
|
175
|
+
default-edges)
|
|
176
|
+
custom-edges (->> provided-edges
|
|
177
|
+
(remove #(contains? default-ids (:id %)))
|
|
178
|
+
(filter valid-edge?)
|
|
179
|
+
(mapv #(normalize-edge {} %)))]
|
|
180
|
+
(-> default-agency-society
|
|
181
|
+
(merge (select-keys provided [:kLines :cb5t :personaPackId :characterRollup]))
|
|
182
|
+
(assoc :version 1
|
|
183
|
+
:agencies (into {}
|
|
184
|
+
(map (fn [name]
|
|
185
|
+
[name (normalize-agency-entry
|
|
186
|
+
name
|
|
187
|
+
(or (get provided-agencies name)
|
|
188
|
+
(get provided-agencies (keyword name))))])
|
|
189
|
+
agency-names))
|
|
190
|
+
:edges (into normalized-defaults custom-edges)))))
|
|
191
|
+
|
|
192
|
+
(defn agency-entry-for [society name]
|
|
193
|
+
(get-in society [:agencies name]))
|
|
194
|
+
|
|
195
|
+
(defn agency-enabled? [society name]
|
|
196
|
+
(true? (:enabled (agency-entry-for society name))))
|
|
197
|
+
|
|
198
|
+
(defn agency-configure [society name]
|
|
199
|
+
(:configure (agency-entry-for society name)))
|
|
200
|
+
|
|
201
|
+
(defn agency-priors [society name]
|
|
202
|
+
(:priors (agency-entry-for society name)))
|
|
203
|
+
|
|
204
|
+
(defn match-event? [match event]
|
|
205
|
+
(every? (fn [[key expected]] (= expected (get event key))) match))
|
|
206
|
+
|
|
207
|
+
(defn route-context
|
|
208
|
+
"Return the accepted edge metadata for one outgoing domain event.
|
|
209
|
+
|
|
210
|
+
A nil result means the topology, agency enablement, match predicate, or edge
|
|
211
|
+
credit rejected the route before it reached the receiving agency."
|
|
212
|
+
[society from event to]
|
|
213
|
+
(when (and (agency-enabled? society from)
|
|
214
|
+
(agency-enabled? society to))
|
|
215
|
+
(some (fn [edge]
|
|
216
|
+
(when (and (= from (:from edge))
|
|
217
|
+
(= to (:to edge))
|
|
218
|
+
(= (:type event) (:type edge))
|
|
219
|
+
(:enabled edge)
|
|
220
|
+
(pos? (:credit edge))
|
|
221
|
+
(match-event? (:match edge) event))
|
|
222
|
+
{:edgeId (:id edge)
|
|
223
|
+
:from from
|
|
224
|
+
:to to
|
|
225
|
+
:credit (:credit edge)}))
|
|
226
|
+
(:edges society))))
|
|
227
|
+
|
|
228
|
+
(defn agency-config
|
|
229
|
+
"Create one JS agency config without converting runtime/provider handles.
|
|
230
|
+
|
|
231
|
+
Direct constructor options supply runtime objects. Profile `configure`
|
|
232
|
+
values override operational knobs, and `priors` are attached as plain data
|
|
233
|
+
for each agency's local planner."
|
|
234
|
+
[root-config society name]
|
|
235
|
+
(let [direct (when root-config (aget root-config name))
|
|
236
|
+
output (js/Object.assign #js {} (or direct #js {})
|
|
237
|
+
(clj->js (agency-configure society name)))]
|
|
238
|
+
(aset output "priors" (clj->js (agency-priors society name)))
|
|
239
|
+
output))
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
(ns polymer.stream)
|
|
2
|
+
|
|
3
|
+
;; Polymer keeps its stream primitive deliberately small.
|
|
4
|
+
;;
|
|
5
|
+
;; The important architectural point is not this particular implementation.
|
|
6
|
+
;; The important point is the boundary shape: agencies exchange plain immutable
|
|
7
|
+
;; data. A specific agency, such as Animation, may own a runtime side effect,
|
|
8
|
+
;; but a host application should not become the cross-agency router.
|
|
9
|
+
|
|
10
|
+
(defn- default-listener-error-reporter
|
|
11
|
+
[{:keys [error]}]
|
|
12
|
+
(.error js/console "[Polymer stream] listener failed" error))
|
|
13
|
+
|
|
14
|
+
(defn- report-listener-error!
|
|
15
|
+
[reporter listener payload error]
|
|
16
|
+
(try
|
|
17
|
+
(reporter {:type :stream/listener-error
|
|
18
|
+
:listener listener
|
|
19
|
+
:event payload
|
|
20
|
+
:error error})
|
|
21
|
+
(catch :default reporter-error
|
|
22
|
+
;; A broken diagnostic callback must not turn an isolated listener failure
|
|
23
|
+
;; back into a failed delivery pass.
|
|
24
|
+
(.error js/console
|
|
25
|
+
"[Polymer stream] listener error reporter failed"
|
|
26
|
+
reporter-error
|
|
27
|
+
error))))
|
|
28
|
+
|
|
29
|
+
(defn create-stream
|
|
30
|
+
"Create a synchronous in-process stream.
|
|
31
|
+
|
|
32
|
+
Listeners receive JavaScript objects because this is the public package
|
|
33
|
+
boundary. Inside Polymer we keep using Clojure maps so agency code stays
|
|
34
|
+
simple and data-oriented.
|
|
35
|
+
|
|
36
|
+
An optional :on-listener-error callback receives a Clojure map describing a
|
|
37
|
+
failed listener. Errors are isolated from later listeners and are not emitted
|
|
38
|
+
as a generic public stream."
|
|
39
|
+
([]
|
|
40
|
+
(create-stream {}))
|
|
41
|
+
([{:keys [on-listener-error]}]
|
|
42
|
+
(let [listeners (atom [])
|
|
43
|
+
next-listener-id (atom 0)
|
|
44
|
+
pending-events (atom cljs.core/PersistentQueue.EMPTY)
|
|
45
|
+
draining? (atom false)
|
|
46
|
+
reporter (or on-listener-error default-listener-error-reporter)]
|
|
47
|
+
(letfn [(deliver! [event]
|
|
48
|
+
(let [payload (clj->js event)
|
|
49
|
+
;; A listener added during this pass starts with the next
|
|
50
|
+
;; event. Each record still has a live flag so a listener
|
|
51
|
+
;; removed before its turn is skipped immediately.
|
|
52
|
+
delivery-listeners @listeners]
|
|
53
|
+
(doseq [{:keys [listener active?]} delivery-listeners]
|
|
54
|
+
(when @active?
|
|
55
|
+
(try
|
|
56
|
+
(listener payload)
|
|
57
|
+
(catch :default error
|
|
58
|
+
(report-listener-error!
|
|
59
|
+
reporter listener payload error)))))))
|
|
60
|
+
|
|
61
|
+
(drain! []
|
|
62
|
+
;; Reentrant emits only append to the queue. The outer drain loop
|
|
63
|
+
;; completes the current event before processing nested events,
|
|
64
|
+
;; avoiding recursive delivery and preserving FIFO order.
|
|
65
|
+
(when-not @draining?
|
|
66
|
+
(reset! draining? true)
|
|
67
|
+
(try
|
|
68
|
+
(loop []
|
|
69
|
+
(when (seq @pending-events)
|
|
70
|
+
(let [event (peek @pending-events)]
|
|
71
|
+
(swap! pending-events pop)
|
|
72
|
+
(deliver! event)
|
|
73
|
+
(recur))))
|
|
74
|
+
(finally
|
|
75
|
+
(reset! draining? false)))))]
|
|
76
|
+
{:emit
|
|
77
|
+
(fn [event]
|
|
78
|
+
(swap! pending-events conj event)
|
|
79
|
+
(drain!))
|
|
80
|
+
|
|
81
|
+
:subscribe
|
|
82
|
+
(fn [listener]
|
|
83
|
+
;; Subscription records preserve registration order and allow the same
|
|
84
|
+
;; callback to be subscribed independently more than once.
|
|
85
|
+
(let [listener-id (swap! next-listener-id inc)
|
|
86
|
+
active? (atom true)]
|
|
87
|
+
(swap! listeners conj {:id listener-id
|
|
88
|
+
:listener listener
|
|
89
|
+
:active? active?})
|
|
90
|
+
(fn []
|
|
91
|
+
;; compare-and-set! gives each subscription an idempotent cleanup
|
|
92
|
+
;; even when several owners race to tear down the same port.
|
|
93
|
+
(when (compare-and-set! active? true false)
|
|
94
|
+
(swap! listeners
|
|
95
|
+
(fn [current]
|
|
96
|
+
(into []
|
|
97
|
+
(remove #(= listener-id (:id %)))
|
|
98
|
+
current)))))))
|
|
99
|
+
|
|
100
|
+
:dispose
|
|
101
|
+
(fn []
|
|
102
|
+
;; Mark the current delivery snapshot inactive before clearing storage
|
|
103
|
+
;; so disposal during a callback skips later listeners and queued work.
|
|
104
|
+
(doseq [{:keys [active?]} @listeners]
|
|
105
|
+
(reset! active? false))
|
|
106
|
+
(reset! listeners [])
|
|
107
|
+
(reset! pending-events cljs.core/PersistentQueue.EMPTY))}))))
|
|
108
|
+
|
|
109
|
+
(defn readable-port
|
|
110
|
+
"Expose a JS-facing read-only stream port."
|
|
111
|
+
[stream]
|
|
112
|
+
#js {:subscribe (fn [listener] ((:subscribe stream) listener))})
|
|
113
|
+
|
|
114
|
+
(defn writable-port
|
|
115
|
+
"Expose a JS-facing writable stream port.
|
|
116
|
+
|
|
117
|
+
The write function is supplied by the owning agency/system because writing to
|
|
118
|
+
input usually means validating and routing a command, not blindly broadcasting
|
|
119
|
+
data."
|
|
120
|
+
[stream write!]
|
|
121
|
+
#js {:write write!
|
|
122
|
+
:subscribe (fn [listener] ((:subscribe stream) listener))})
|
|
123
|
+
|
|
124
|
+
(defn subscribe-many
|
|
125
|
+
"Subscribe one listener to several streams and return one unsubscriber."
|
|
126
|
+
[streams listener]
|
|
127
|
+
(let [unsubscribers (mapv #((:subscribe %) listener) streams)]
|
|
128
|
+
(fn []
|
|
129
|
+
(doseq [unsubscribe unsubscribers]
|
|
130
|
+
(unsubscribe)))))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
(ns polymer.transcription.agency
|
|
2
|
+
(:require [polymer.stream :as stream]
|
|
3
|
+
[polymer.transcription.domain :as domain]
|
|
4
|
+
[polymer.transcription.effector :as effector]
|
|
5
|
+
[polymer.transcription.planner :as planner]
|
|
6
|
+
[polymer.transcription.scheduler :as scheduler]
|
|
7
|
+
[polymer.transcription.state :as state]))
|
|
8
|
+
|
|
9
|
+
;; Transcription turns provider recognition callbacks into stable speech facts.
|
|
10
|
+
;; Peer agencies only see data: partial/final transcripts, status, diagnostics,
|
|
11
|
+
;; and provider lifecycle requests. Browser work is private to the effector
|
|
12
|
+
;; reached through this agency's planner and scheduler.
|
|
13
|
+
|
|
14
|
+
(defn now-ms
|
|
15
|
+
[]
|
|
16
|
+
(.now js/Date))
|
|
17
|
+
|
|
18
|
+
(defn create-transcription-agency
|
|
19
|
+
([config]
|
|
20
|
+
(create-transcription-agency config nil))
|
|
21
|
+
([config runtime]
|
|
22
|
+
(let [input-stream (stream/create-stream)
|
|
23
|
+
event-stream (stream/create-stream)
|
|
24
|
+
state-atom (atom (state/config->state config))
|
|
25
|
+
disposed? (atom false)
|
|
26
|
+
emit-input (:emit input-stream)
|
|
27
|
+
emit-event (:emit event-stream)
|
|
28
|
+
scheduler-atom (atom nil)]
|
|
29
|
+
(letfn [(dispatch! [command]
|
|
30
|
+
(when-not @disposed?
|
|
31
|
+
(let [payload (domain/data-map command)
|
|
32
|
+
plan (planner/plan-command payload @state-atom (now-ms))]
|
|
33
|
+
(emit-input {:type "command"
|
|
34
|
+
:agency "transcription"
|
|
35
|
+
:command payload})
|
|
36
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
37
|
+
((:schedule agency-scheduler) payload plan)))))
|
|
38
|
+
(dispatch-type! [type]
|
|
39
|
+
(dispatch! #js {:type type}))]
|
|
40
|
+
(let [provider-effector (effector/create-effector
|
|
41
|
+
{:dispatch dispatch!
|
|
42
|
+
:get-state #(deref state-atom)
|
|
43
|
+
:runtime runtime})
|
|
44
|
+
agency-scheduler (scheduler/create-scheduler
|
|
45
|
+
{:state-atom state-atom
|
|
46
|
+
:emit-event emit-event
|
|
47
|
+
:effector provider-effector})]
|
|
48
|
+
(reset! scheduler-atom agency-scheduler)
|
|
49
|
+
#js {:dispatch dispatch!
|
|
50
|
+
:snapshot (fn [] (state/visible-state @state-atom))
|
|
51
|
+
:input (stream/writable-port input-stream dispatch!)
|
|
52
|
+
:events (stream/readable-port event-stream)
|
|
53
|
+
:subscribeInput (fn [listener] ((:subscribe input-stream) listener))
|
|
54
|
+
:subscribeEvents (fn [listener] ((:subscribe event-stream) listener))
|
|
55
|
+
:subscribe (fn [listener] ((:subscribe event-stream) listener))
|
|
56
|
+
:subscribeStatus (fn [listener] ((:subscribe event-stream) listener))
|
|
57
|
+
:start (fn [] (dispatch-type! "start"))
|
|
58
|
+
:stop (fn [] (dispatch-type! "stop"))
|
|
59
|
+
:reset (fn [] (dispatch-type! "reset"))
|
|
60
|
+
:setAgentAudioReferenceTrack
|
|
61
|
+
(fn [track]
|
|
62
|
+
((:set-reference-track agency-scheduler) track))
|
|
63
|
+
:queue (fn [] (clj->js ((:queue agency-scheduler))))
|
|
64
|
+
:dispose (fn []
|
|
65
|
+
(when-not @disposed?
|
|
66
|
+
(reset! disposed? true)
|
|
67
|
+
((:dispose agency-scheduler))
|
|
68
|
+
((:dispose input-stream))
|
|
69
|
+
((:dispose event-stream))))})))))
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
(ns polymer.transcription.domain
|
|
2
|
+
(:require [clojure.string :as str]))
|
|
3
|
+
|
|
4
|
+
;; Pure Transcription transforms live here. Provider/browser work stays outside
|
|
5
|
+
;; this namespace; these helpers only normalize command data and transcript text.
|
|
6
|
+
|
|
7
|
+
(defn data-map
|
|
8
|
+
[value]
|
|
9
|
+
(cond
|
|
10
|
+
(map? value) value
|
|
11
|
+
value (js->clj value :keywordize-keys true)
|
|
12
|
+
:else {}))
|
|
13
|
+
|
|
14
|
+
(defn clean-text
|
|
15
|
+
[value]
|
|
16
|
+
(let [text (str/trim (or value ""))]
|
|
17
|
+
(when (pos? (count text))
|
|
18
|
+
text)))
|
|
19
|
+
|
|
20
|
+
(defn text-or
|
|
21
|
+
[value fallback]
|
|
22
|
+
(or (clean-text value) fallback))
|
|
23
|
+
|
|
24
|
+
(defn transcript-text
|
|
25
|
+
[command]
|
|
26
|
+
(clean-text (or (:text command) (:transcript command))))
|
|
27
|
+
|
|
28
|
+
(defn agent-source?
|
|
29
|
+
[command]
|
|
30
|
+
(contains? #{"agent" "tts" "speaker" "system"}
|
|
31
|
+
(str/lower-case (or (:source command) (:speaker command) ""))))
|
|
32
|
+
|
|
33
|
+
(defn word-tokens
|
|
34
|
+
[text]
|
|
35
|
+
(->> (str/split (str/lower-case (or text "")) #"\s+")
|
|
36
|
+
(remove str/blank?)
|
|
37
|
+
vec))
|
|
38
|
+
|
|
39
|
+
(defn word-set
|
|
40
|
+
[text]
|
|
41
|
+
(set (word-tokens text)))
|
|
42
|
+
|
|
43
|
+
(defn agent-echo?
|
|
44
|
+
[command current-state]
|
|
45
|
+
(let [text (or (transcript-text command) "")
|
|
46
|
+
tokens (word-tokens text)
|
|
47
|
+
words (set tokens)
|
|
48
|
+
agent-words (:agentWords current-state)
|
|
49
|
+
script (:agentScript current-state)
|
|
50
|
+
overlap (count (filter agent-words words))]
|
|
51
|
+
(and (get-in current-state [:config :agentFilteringEnabled])
|
|
52
|
+
(:agentSpeaking current-state)
|
|
53
|
+
(seq tokens)
|
|
54
|
+
(or (every? agent-words tokens)
|
|
55
|
+
(and (seq script) (str/starts-with? script (str/lower-case text)))
|
|
56
|
+
(and (seq words)
|
|
57
|
+
(pos? overlap)
|
|
58
|
+
(>= (/ overlap (count words)) 0.65))))))
|
|
59
|
+
|
|
60
|
+
(defn boundary-events
|
|
61
|
+
[text now-ms]
|
|
62
|
+
(->> (str/split (or text "") #"\s+")
|
|
63
|
+
(remove str/blank?)
|
|
64
|
+
(map-indexed (fn [index word]
|
|
65
|
+
{:word word
|
|
66
|
+
:index index
|
|
67
|
+
:timestamp now-ms
|
|
68
|
+
:speaker "user"}))))
|