@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,196 @@
|
|
|
1
|
+
(ns polymer.prosodic.agency
|
|
2
|
+
(:require [polymer.prosodic.goap :as goap]
|
|
3
|
+
[polymer.prosodic.scheduler :as scheduler]
|
|
4
|
+
[polymer.prosodic.snippet :as snippet]
|
|
5
|
+
[polymer.prosodic.state :as state]
|
|
6
|
+
[polymer.stream :as stream]))
|
|
7
|
+
|
|
8
|
+
;; Prosodic Expression consumes speech/blink/conversation facts and emits
|
|
9
|
+
;; animation intent. It does not own TTS, LipSync, host UI state, storage, or
|
|
10
|
+
;; the animation runtime. The character network routes peer facts here, then
|
|
11
|
+
;; routes Prosodic's animation requests to Polymer Animation.
|
|
12
|
+
|
|
13
|
+
(defn js-command [type value]
|
|
14
|
+
#js {:type type :value value})
|
|
15
|
+
|
|
16
|
+
(defn plan-error-message [plan]
|
|
17
|
+
(let [step (first (:steps plan))]
|
|
18
|
+
(case (:reason step)
|
|
19
|
+
"unsupported-command" (str "Prosodic plan failed: unsupported command " (:commandType step))
|
|
20
|
+
(str "Prosodic plan failed: " (:reason step)))))
|
|
21
|
+
|
|
22
|
+
(defn should-emit-plan? [plan]
|
|
23
|
+
;; Ignore/no-op plans are routine for word boundaries. Emitting every one
|
|
24
|
+
;; would create noisy high-frequency diagnostics without changing behavior.
|
|
25
|
+
(or (false? (:ok plan))
|
|
26
|
+
(:gesture plan)
|
|
27
|
+
(some #{"record-conversation-fact" "remove-active-gestures"}
|
|
28
|
+
(map :op (:steps plan)))))
|
|
29
|
+
|
|
30
|
+
(defn create-prosodic-agency [config]
|
|
31
|
+
(let [input-stream (stream/create-stream)
|
|
32
|
+
event-stream (stream/create-stream)
|
|
33
|
+
state-atom (atom (state/config->state config))
|
|
34
|
+
disposed? (atom false)
|
|
35
|
+
emit-input (:emit input-stream)
|
|
36
|
+
emit-event (:emit event-stream)
|
|
37
|
+
agency-scheduler (scheduler/create-scheduler {:emit-event emit-event})]
|
|
38
|
+
(letfn [(emit-plan! [plan]
|
|
39
|
+
(when (should-emit-plan? plan)
|
|
40
|
+
(emit-event {:type "prosodicPlanCreated"
|
|
41
|
+
:agency "prosodic"
|
|
42
|
+
:plan plan})))
|
|
43
|
+
|
|
44
|
+
(schedule-gesture! [gesture-kind context]
|
|
45
|
+
(when-let [snippet-data (snippet/build-gesture-snippet gesture-kind
|
|
46
|
+
(:config @state-atom)
|
|
47
|
+
context)]
|
|
48
|
+
((:schedule-gesture agency-scheduler) snippet-data {:autoPlay true})
|
|
49
|
+
(let [scheduled-at (state/now-ms)]
|
|
50
|
+
(swap! state-atom state/record-schedule (:name snippet-data) gesture-kind scheduled-at)
|
|
51
|
+
(emit-event {:type "prosodicGestureScheduled"
|
|
52
|
+
:agency "prosodic"
|
|
53
|
+
:gesture gesture-kind
|
|
54
|
+
:name (:name snippet-data)
|
|
55
|
+
:word (:word context)
|
|
56
|
+
:wordIndex (:wordIndex context)
|
|
57
|
+
:scheduledAt scheduled-at}))
|
|
58
|
+
snippet-data))
|
|
59
|
+
|
|
60
|
+
(remove-active! [reason]
|
|
61
|
+
(let [active (:activeSnippets @state-atom)
|
|
62
|
+
removed-at (state/now-ms)]
|
|
63
|
+
((:remove-many agency-scheduler) active reason)
|
|
64
|
+
(doseq [name active]
|
|
65
|
+
(swap! state-atom state/record-remove name reason removed-at))))
|
|
66
|
+
|
|
67
|
+
(stop-local! [reason]
|
|
68
|
+
(remove-active! reason)
|
|
69
|
+
(let [stopped-at (state/now-ms)]
|
|
70
|
+
(swap! state-atom state/clear-active reason stopped-at)
|
|
71
|
+
(emit-event {:type "prosodicStopped"
|
|
72
|
+
:agency "prosodic"
|
|
73
|
+
:reason reason
|
|
74
|
+
:stoppedAt stopped-at})))
|
|
75
|
+
|
|
76
|
+
(execute-steps! [payload plan]
|
|
77
|
+
(doseq [step (:steps plan)]
|
|
78
|
+
(case (:op step)
|
|
79
|
+
"apply-config"
|
|
80
|
+
(do
|
|
81
|
+
(swap! state-atom state/configure (:config payload))
|
|
82
|
+
(emit-event {:type "prosodicConfigChanged"
|
|
83
|
+
:agency "prosodic"
|
|
84
|
+
:state @state-atom}))
|
|
85
|
+
|
|
86
|
+
"clear-conversation-suppress"
|
|
87
|
+
(swap! state-atom state/clear-conversation-suppress)
|
|
88
|
+
|
|
89
|
+
"record-speech-start"
|
|
90
|
+
(let [started-at (state/now-ms)]
|
|
91
|
+
(swap! state-atom state/record-speech-start started-at (:name payload))
|
|
92
|
+
(emit-event {:type "prosodicSpeechStarted"
|
|
93
|
+
:agency "prosodic"
|
|
94
|
+
:name (:name payload)
|
|
95
|
+
:startedAt started-at}))
|
|
96
|
+
|
|
97
|
+
"record-speech-stop"
|
|
98
|
+
(let [stopped-at (state/now-ms)
|
|
99
|
+
reason (or (:reason payload) "requested")]
|
|
100
|
+
(swap! state-atom state/clear-active reason stopped-at)
|
|
101
|
+
(emit-event {:type "prosodicStopped"
|
|
102
|
+
:agency "prosodic"
|
|
103
|
+
:reason reason
|
|
104
|
+
:stoppedAt stopped-at}))
|
|
105
|
+
|
|
106
|
+
"record-word-boundary"
|
|
107
|
+
(let [word (state/clean-word (:word payload))
|
|
108
|
+
word-index (int (state/number-or (:wordIndex payload)
|
|
109
|
+
(:wordIndex @state-atom)))
|
|
110
|
+
observed-at (state/now-ms)]
|
|
111
|
+
(swap! state-atom state/record-word-boundary word word-index observed-at)
|
|
112
|
+
(emit-event {:type "prosodicWordBoundary"
|
|
113
|
+
:agency "prosodic"
|
|
114
|
+
:word word
|
|
115
|
+
:wordIndex word-index
|
|
116
|
+
:observedAt observed-at}))
|
|
117
|
+
|
|
118
|
+
"record-blink-fast-cue"
|
|
119
|
+
(swap! state-atom state/record-blink-fast-cue (state/now-ms))
|
|
120
|
+
|
|
121
|
+
"record-conversation-fact"
|
|
122
|
+
(let [observed-at (state/now-ms)]
|
|
123
|
+
(swap! state-atom state/record-conversation-fact payload observed-at)
|
|
124
|
+
(emit-event {:type "prosodicConversationFact"
|
|
125
|
+
:agency "prosodic"
|
|
126
|
+
:conversationType (:type payload)
|
|
127
|
+
:text (:text payload)
|
|
128
|
+
:turnId (:turnId payload)
|
|
129
|
+
:observedAt observed-at}))
|
|
130
|
+
|
|
131
|
+
"build-gesture-snippet"
|
|
132
|
+
nil
|
|
133
|
+
|
|
134
|
+
"schedule-animation"
|
|
135
|
+
(when-let [gesture (:gesture plan)]
|
|
136
|
+
(schedule-gesture! gesture
|
|
137
|
+
{:trigger (or (:type payload) "plan")
|
|
138
|
+
:word (state/clean-word (:word payload))
|
|
139
|
+
:wordIndex (int (state/number-or (:wordIndex payload)
|
|
140
|
+
(:wordIndex @state-atom)))}))
|
|
141
|
+
|
|
142
|
+
"remove-active-gestures"
|
|
143
|
+
(remove-active! (or (:reason payload) "requested"))
|
|
144
|
+
|
|
145
|
+
"reset-state"
|
|
146
|
+
(do
|
|
147
|
+
(stop-local! "reset")
|
|
148
|
+
(reset! state-atom (state/config->state nil))
|
|
149
|
+
(emit-event {:type "prosodicConfigChanged"
|
|
150
|
+
:agency "prosodic"
|
|
151
|
+
:state @state-atom}))
|
|
152
|
+
|
|
153
|
+
("ignore" "fail")
|
|
154
|
+
nil
|
|
155
|
+
|
|
156
|
+
nil)))
|
|
157
|
+
|
|
158
|
+
(dispatch! [command]
|
|
159
|
+
(when-not @disposed?
|
|
160
|
+
(let [payload (js->clj command :keywordize-keys true)
|
|
161
|
+
plan (goap/plan-command payload @state-atom)]
|
|
162
|
+
(emit-input {:type "command"
|
|
163
|
+
:agency "prosodic"
|
|
164
|
+
:command payload})
|
|
165
|
+
(emit-plan! plan)
|
|
166
|
+
(if (false? (:ok plan))
|
|
167
|
+
(emit-event {:type "error"
|
|
168
|
+
:agency "prosodic"
|
|
169
|
+
:message (plan-error-message plan)})
|
|
170
|
+
(execute-steps! payload plan)))))]
|
|
171
|
+
#js {:dispatch dispatch!
|
|
172
|
+
:snapshot (fn [] (state/visible-state @state-atom))
|
|
173
|
+
:schedulerQueue (fn [] (clj->js ((:queue agency-scheduler))))
|
|
174
|
+
:input (stream/writable-port input-stream dispatch!)
|
|
175
|
+
:events (stream/readable-port event-stream)
|
|
176
|
+
:subscribeInput (fn [listener] ((:subscribe input-stream) listener))
|
|
177
|
+
:subscribeEvents (fn [listener] ((:subscribe event-stream) listener))
|
|
178
|
+
:subscribe (fn [listener] ((:subscribe event-stream) listener))
|
|
179
|
+
:subscribeStatus (fn [listener] ((:subscribe event-stream) listener))
|
|
180
|
+
:configure (fn [next-config] (dispatch! #js {:type "configure" :config next-config}))
|
|
181
|
+
:speechStarted (fn [name] (dispatch! #js {:type "speechStarted" :name name}))
|
|
182
|
+
:wordBoundary (fn
|
|
183
|
+
([word] (dispatch! #js {:type "wordBoundary" :word word}))
|
|
184
|
+
([word word-index]
|
|
185
|
+
(dispatch! #js {:type "wordBoundary"
|
|
186
|
+
:word word
|
|
187
|
+
:wordIndex word-index})))
|
|
188
|
+
:blinkFast (fn [] (dispatch! #js {:type "blinkFast"}))
|
|
189
|
+
:stop (fn [] (dispatch! #js {:type "stop"}))
|
|
190
|
+
:reset (fn [] (dispatch! #js {:type "reset"}))
|
|
191
|
+
:dispose (fn []
|
|
192
|
+
(when-not @disposed?
|
|
193
|
+
(reset! disposed? true)
|
|
194
|
+
((:dispose agency-scheduler))
|
|
195
|
+
((:dispose input-stream))
|
|
196
|
+
((:dispose event-stream))))})))
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
(ns polymer.prosodic.goap
|
|
2
|
+
(:require [polymer.prosodic.state :as state]))
|
|
3
|
+
|
|
4
|
+
;; Prosodic GOAP stays intentionally local. It answers whether a speech/blink
|
|
5
|
+
;; or conversation fact should become a gesture plan. It does not build
|
|
6
|
+
;; snippets, mutate state, or talk to Animation.
|
|
7
|
+
|
|
8
|
+
(def supported-command-types
|
|
9
|
+
#{"configure"
|
|
10
|
+
"speechStarted"
|
|
11
|
+
"speechStopped"
|
|
12
|
+
"wordBoundary"
|
|
13
|
+
"blinkFast"
|
|
14
|
+
"conversation.userUtterance"
|
|
15
|
+
"conversation.agentUtterance"
|
|
16
|
+
"conversation.requestResponse"
|
|
17
|
+
"conversation.cancelRequested"
|
|
18
|
+
"stop"
|
|
19
|
+
"reset"})
|
|
20
|
+
|
|
21
|
+
(defn command-goal [command world]
|
|
22
|
+
(let [type (:type command)
|
|
23
|
+
word (state/clean-word (:word command))
|
|
24
|
+
word-index (int (state/number-or (:wordIndex command) (:wordIndex world)))
|
|
25
|
+
last-conversation (:lastConversationFact world)]
|
|
26
|
+
{:type type
|
|
27
|
+
:enabled (get-in world [:config :enabled])
|
|
28
|
+
:speaking (:speaking world)
|
|
29
|
+
:word word
|
|
30
|
+
:wordIndex word-index
|
|
31
|
+
:hasWord (boolean word)
|
|
32
|
+
:blinkFastReady (state/blink-fast-cue-ready? world (state/now-ms))
|
|
33
|
+
:lastConversationType (:type last-conversation)
|
|
34
|
+
:conversationSuppressed? (= "conversation.cancelRequested"
|
|
35
|
+
(:type last-conversation))}))
|
|
36
|
+
|
|
37
|
+
(defn word-gesture-kind [goal config]
|
|
38
|
+
;; Local policy: speech cadence, biased by recent conversation context.
|
|
39
|
+
;; Cancel suppresses gestures until speechStarted clears the fact.
|
|
40
|
+
(when-not (:conversationSuppressed? goal)
|
|
41
|
+
(let [cycle (:speechGestureEvery config)
|
|
42
|
+
mod-value (mod (:wordIndex goal) cycle)
|
|
43
|
+
conv (:lastConversationType goal)]
|
|
44
|
+
(cond
|
|
45
|
+
(and (= "conversation.userUtterance" conv)
|
|
46
|
+
(or (= mod-value 0) (= mod-value 1)))
|
|
47
|
+
"emphasis"
|
|
48
|
+
|
|
49
|
+
(= "conversation.requestResponse" conv)
|
|
50
|
+
(when (or (= mod-value 0) (= mod-value 4)) "contemplate")
|
|
51
|
+
|
|
52
|
+
(= mod-value 0) "emphasis"
|
|
53
|
+
(= mod-value 3) "nod"
|
|
54
|
+
(= mod-value 4) "contemplate"
|
|
55
|
+
:else nil))))
|
|
56
|
+
|
|
57
|
+
(defn failure-step [goal]
|
|
58
|
+
(cond
|
|
59
|
+
(not (contains? supported-command-types (:type goal)))
|
|
60
|
+
{:op "fail" :reason "unsupported-command" :commandType (:type goal)}
|
|
61
|
+
|
|
62
|
+
(not (:enabled goal))
|
|
63
|
+
{:op "ignore" :reason "disabled"}
|
|
64
|
+
|
|
65
|
+
(and (= "wordBoundary" (:type goal)) (not (:hasWord goal)))
|
|
66
|
+
{:op "ignore" :reason "missing-word"}
|
|
67
|
+
|
|
68
|
+
(and (= "blinkFast" (:type goal)) (not (:blinkFastReady goal)))
|
|
69
|
+
{:op "ignore" :reason "cooldown"}
|
|
70
|
+
|
|
71
|
+
:else nil))
|
|
72
|
+
|
|
73
|
+
(defn command-steps [goal config]
|
|
74
|
+
(if-let [failure (failure-step goal)]
|
|
75
|
+
[failure]
|
|
76
|
+
(case (:type goal)
|
|
77
|
+
"configure" [{:op "apply-config"}]
|
|
78
|
+
"speechStarted" [{:op "clear-conversation-suppress"}
|
|
79
|
+
{:op "record-speech-start"}]
|
|
80
|
+
"speechStopped" [{:op "remove-active-gestures"}
|
|
81
|
+
{:op "record-speech-stop"}]
|
|
82
|
+
"stop" [{:op "remove-active-gestures"}
|
|
83
|
+
{:op "record-speech-stop"}]
|
|
84
|
+
"reset" [{:op "remove-active-gestures"}
|
|
85
|
+
{:op "reset-state"}]
|
|
86
|
+
"blinkFast" [{:op "record-blink-fast-cue"}
|
|
87
|
+
{:op "build-gesture-snippet" :gesture "blink-fast"}
|
|
88
|
+
{:op "schedule-animation"}]
|
|
89
|
+
"conversation.userUtterance"
|
|
90
|
+
[{:op "record-conversation-fact"}
|
|
91
|
+
{:op "build-gesture-snippet" :gesture "contemplate"}
|
|
92
|
+
{:op "schedule-animation"}]
|
|
93
|
+
"conversation.agentUtterance"
|
|
94
|
+
[{:op "record-conversation-fact"}
|
|
95
|
+
{:op "build-gesture-snippet" :gesture "nod"}
|
|
96
|
+
{:op "schedule-animation"}]
|
|
97
|
+
"conversation.requestResponse"
|
|
98
|
+
[{:op "record-conversation-fact"}]
|
|
99
|
+
"conversation.cancelRequested" [{:op "record-conversation-fact"}
|
|
100
|
+
{:op "remove-active-gestures"}]
|
|
101
|
+
"wordBoundary" (if-let [gesture (word-gesture-kind goal config)]
|
|
102
|
+
[{:op "record-word-boundary"}
|
|
103
|
+
{:op "build-gesture-snippet" :gesture gesture}
|
|
104
|
+
{:op "schedule-animation"}]
|
|
105
|
+
[{:op "record-word-boundary"}
|
|
106
|
+
{:op "ignore" :reason (if (:conversationSuppressed? goal)
|
|
107
|
+
"conversation-cancelled"
|
|
108
|
+
"no-gesture-for-word")}]))))
|
|
109
|
+
|
|
110
|
+
(defn plan-ok? [steps]
|
|
111
|
+
(not= "fail" (:op (first steps))))
|
|
112
|
+
|
|
113
|
+
(defn scheduled-gesture [steps]
|
|
114
|
+
(:gesture (some #(when (= "build-gesture-snippet" (:op %)) %) steps)))
|
|
115
|
+
|
|
116
|
+
(defn plan-command [command world]
|
|
117
|
+
(let [goal (command-goal command world)
|
|
118
|
+
steps (command-steps goal (:config world))]
|
|
119
|
+
{:goal goal
|
|
120
|
+
:steps steps
|
|
121
|
+
:ok (plan-ok? steps)
|
|
122
|
+
:gesture (scheduled-gesture steps)}))
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
(ns polymer.prosodic.scheduler)
|
|
2
|
+
|
|
3
|
+
;; Prosodic has no provider, audio, DOM, or runtime side effect. Its scheduler
|
|
4
|
+
;; still owns the ordered effector queue for this agency: schedule a gesture
|
|
5
|
+
;; request, remove active gesture requests on stop/reset, and expose that queue
|
|
6
|
+
;; for diagnostics/tests. Polymer Animation performs the runtime effect later.
|
|
7
|
+
|
|
8
|
+
(defn now-ms []
|
|
9
|
+
(.now js/Date))
|
|
10
|
+
|
|
11
|
+
(defn effector-target [channel]
|
|
12
|
+
(let [target (:target channel)]
|
|
13
|
+
(case (:type target)
|
|
14
|
+
"au" (case (:id target)
|
|
15
|
+
53 "head"
|
|
16
|
+
54 "head"
|
|
17
|
+
55 "head"
|
|
18
|
+
"face")
|
|
19
|
+
(:type target))))
|
|
20
|
+
|
|
21
|
+
(def snippet-effectors-xf
|
|
22
|
+
;; This is a small, valid transducer use: pure channel -> effector labels.
|
|
23
|
+
;; It keeps queue diagnostics data-oriented without hiding control flow.
|
|
24
|
+
(comp
|
|
25
|
+
(map effector-target)
|
|
26
|
+
(remove nil?)
|
|
27
|
+
(distinct)))
|
|
28
|
+
|
|
29
|
+
(defn snippet-effectors [snippet]
|
|
30
|
+
(into [] snippet-effectors-xf (:channels snippet)))
|
|
31
|
+
|
|
32
|
+
(defn queue-entry [effect queue-index]
|
|
33
|
+
(assoc effect
|
|
34
|
+
:queueIndex queue-index
|
|
35
|
+
:queuedAt (now-ms)))
|
|
36
|
+
|
|
37
|
+
(defn create-scheduler [{:keys [emit-event]}]
|
|
38
|
+
(let [queue (atom [])
|
|
39
|
+
disposed? (atom false)]
|
|
40
|
+
(letfn [(enqueue! [effect]
|
|
41
|
+
(let [entry (queue-entry effect (count @queue))]
|
|
42
|
+
(swap! queue conj entry)
|
|
43
|
+
entry))
|
|
44
|
+
|
|
45
|
+
(schedule-animation! [snippet-data options]
|
|
46
|
+
(let [effectors (snippet-effectors snippet-data)
|
|
47
|
+
entry (enqueue! {:type "scheduleAnimation"
|
|
48
|
+
:agency "prosodic"
|
|
49
|
+
:requestId (:name snippet-data)
|
|
50
|
+
:snippetName (:name snippet-data)
|
|
51
|
+
:effectors effectors})]
|
|
52
|
+
(emit-event {:type "animation.requestScheduleSnippet"
|
|
53
|
+
:agency "prosodic"
|
|
54
|
+
:requestId (:requestId entry)
|
|
55
|
+
:snippet snippet-data
|
|
56
|
+
:options options
|
|
57
|
+
:effectors effectors
|
|
58
|
+
:queueIndex (:queueIndex entry)
|
|
59
|
+
:queuedAt (:queuedAt entry)})
|
|
60
|
+
entry))
|
|
61
|
+
|
|
62
|
+
(remove-animation! [name reason]
|
|
63
|
+
(let [entry (enqueue! {:type "removeAnimation"
|
|
64
|
+
:agency "prosodic"
|
|
65
|
+
:requestId name
|
|
66
|
+
:name name
|
|
67
|
+
:reason reason})]
|
|
68
|
+
(emit-event {:type "animation.requestRemoveSnippet"
|
|
69
|
+
:agency "prosodic"
|
|
70
|
+
:requestId name
|
|
71
|
+
:name name
|
|
72
|
+
:reason reason
|
|
73
|
+
:queueIndex (:queueIndex entry)
|
|
74
|
+
:queuedAt (:queuedAt entry)})
|
|
75
|
+
entry))]
|
|
76
|
+
{:schedule-gesture
|
|
77
|
+
(fn [snippet-data options]
|
|
78
|
+
(when-not @disposed?
|
|
79
|
+
(schedule-animation! snippet-data options)))
|
|
80
|
+
|
|
81
|
+
:remove
|
|
82
|
+
(fn [name reason]
|
|
83
|
+
(when-not @disposed?
|
|
84
|
+
(remove-animation! name reason)))
|
|
85
|
+
|
|
86
|
+
:remove-many
|
|
87
|
+
(fn [names reason]
|
|
88
|
+
(when-not @disposed?
|
|
89
|
+
(doseq [name names]
|
|
90
|
+
(remove-animation! name reason))))
|
|
91
|
+
|
|
92
|
+
:queue
|
|
93
|
+
(fn []
|
|
94
|
+
@queue)
|
|
95
|
+
|
|
96
|
+
:dispose
|
|
97
|
+
(fn []
|
|
98
|
+
(reset! disposed? true)
|
|
99
|
+
(reset! queue []))})))
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
(ns polymer.prosodic.snippet
|
|
2
|
+
(:require [polymer.prosodic.state :as state]))
|
|
3
|
+
|
|
4
|
+
;; Prosodic snippet construction is pure data mapping. The agency scheduler
|
|
5
|
+
;; emits the resulting map to Polymer Animation; only Animation touches a
|
|
6
|
+
;; runtime-specific effector.
|
|
7
|
+
|
|
8
|
+
(defn au-channel [au-id curve]
|
|
9
|
+
{:target {:type "au" :id au-id}
|
|
10
|
+
:keyframes curve})
|
|
11
|
+
|
|
12
|
+
(defn base-snippet [name curves max-time priority intensity metadata]
|
|
13
|
+
{:name name
|
|
14
|
+
:curves curves
|
|
15
|
+
:channels (mapv (fn [[au-id curve]]
|
|
16
|
+
(au-channel (js/parseInt (str au-id) 10) curve))
|
|
17
|
+
curves)
|
|
18
|
+
:maxTime max-time
|
|
19
|
+
:loop false
|
|
20
|
+
:snippetPriority priority
|
|
21
|
+
:snippetPlaybackRate 1
|
|
22
|
+
:snippetIntensityScale intensity
|
|
23
|
+
:metadata (merge {:agency "prosodic"} metadata)})
|
|
24
|
+
|
|
25
|
+
(defn gesture-name [gesture-kind now]
|
|
26
|
+
(str "polymer:prosodic:" gesture-kind ":" now))
|
|
27
|
+
|
|
28
|
+
(defn emphasis-snippet [now config context]
|
|
29
|
+
(base-snippet
|
|
30
|
+
(gesture-name "emphasis" now)
|
|
31
|
+
{"1" [{:time 0.0 :intensity 0}
|
|
32
|
+
{:time 0.15 :intensity 0.35}
|
|
33
|
+
{:time 0.45 :intensity 0.45}
|
|
34
|
+
{:time 0.75 :intensity 0}]
|
|
35
|
+
"2" [{:time 0.0 :intensity 0}
|
|
36
|
+
{:time 0.15 :intensity 0.25}
|
|
37
|
+
{:time 0.45 :intensity 0.35}
|
|
38
|
+
{:time 0.75 :intensity 0}]
|
|
39
|
+
"55" [{:time 0.0 :intensity 0}
|
|
40
|
+
{:time 0.25 :intensity 0.15}
|
|
41
|
+
{:time 0.65 :intensity 0.15}
|
|
42
|
+
{:time 0.95 :intensity 0}]}
|
|
43
|
+
0.95
|
|
44
|
+
(:priority config)
|
|
45
|
+
(* 0.8 (:intensity config))
|
|
46
|
+
(merge {:gesture "emphasis"} context)))
|
|
47
|
+
|
|
48
|
+
(defn nod-snippet [now config context]
|
|
49
|
+
(base-snippet
|
|
50
|
+
(gesture-name "nod" now)
|
|
51
|
+
{"53" [{:time 0.0 :intensity 0}
|
|
52
|
+
{:time 0.15 :intensity 0.4}
|
|
53
|
+
{:time 0.4 :intensity 0.5}
|
|
54
|
+
{:time 0.75 :intensity 0}]}
|
|
55
|
+
0.75
|
|
56
|
+
(:priority config)
|
|
57
|
+
(* 0.9 (:intensity config))
|
|
58
|
+
(merge {:gesture "nod"} context)))
|
|
59
|
+
|
|
60
|
+
(defn contemplate-snippet [now config context]
|
|
61
|
+
(base-snippet
|
|
62
|
+
(gesture-name "contemplate" now)
|
|
63
|
+
{"4" [{:time 0.0 :intensity 0}
|
|
64
|
+
{:time 0.2 :intensity 0.18}
|
|
65
|
+
{:time 0.6 :intensity 0.22}
|
|
66
|
+
{:time 1.0 :intensity 0}]
|
|
67
|
+
"1" [{:time 0.0 :intensity 0}
|
|
68
|
+
{:time 0.25 :intensity 0.12}
|
|
69
|
+
{:time 0.65 :intensity 0.12}
|
|
70
|
+
{:time 1.0 :intensity 0}]}
|
|
71
|
+
1.0
|
|
72
|
+
(:priority config)
|
|
73
|
+
(* 0.6 (:intensity config))
|
|
74
|
+
(merge {:gesture "contemplate"} context)))
|
|
75
|
+
|
|
76
|
+
(defn blink-fast-snippet [now config context]
|
|
77
|
+
;; Fast blinking gets a small downward head cue. It is no longer hard-coded
|
|
78
|
+
;; in the character network; Blink emits a signal and Prosodic decides how to
|
|
79
|
+
;; express that signal.
|
|
80
|
+
(base-snippet
|
|
81
|
+
(gesture-name "blink-fast" now)
|
|
82
|
+
{"1" [{:time 0 :intensity 0}
|
|
83
|
+
{:time 0.12 :intensity 0.26}
|
|
84
|
+
{:time 0.42 :intensity 0.34}
|
|
85
|
+
{:time 0.72 :intensity 0}]
|
|
86
|
+
"2" [{:time 0 :intensity 0}
|
|
87
|
+
{:time 0.12 :intensity 0.2}
|
|
88
|
+
{:time 0.42 :intensity 0.28}
|
|
89
|
+
{:time 0.72 :intensity 0}]
|
|
90
|
+
"54" [{:time 0 :intensity 0}
|
|
91
|
+
{:time 0.16 :intensity 0.16}
|
|
92
|
+
{:time 0.44 :intensity 0.2}
|
|
93
|
+
{:time 0.72 :intensity 0}]}
|
|
94
|
+
0.72
|
|
95
|
+
(max (:priority config) 35)
|
|
96
|
+
(* 0.75 (:intensity config))
|
|
97
|
+
(merge {:gesture "blink-fast" :trigger "blink-fast"} context)))
|
|
98
|
+
|
|
99
|
+
(defn build-gesture-snippet [gesture-kind config context]
|
|
100
|
+
(let [now (state/now-ms)]
|
|
101
|
+
(case gesture-kind
|
|
102
|
+
"emphasis" (emphasis-snippet now config context)
|
|
103
|
+
"nod" (nod-snippet now config context)
|
|
104
|
+
"contemplate" (contemplate-snippet now config context)
|
|
105
|
+
"blink-fast" (blink-fast-snippet now config context)
|
|
106
|
+
nil)))
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
(ns polymer.prosodic.state)
|
|
2
|
+
|
|
3
|
+
;; Prosodic state is a small local ledger. It records speech/prosody facts and
|
|
4
|
+
;; active snippet names, but it never stores runtime handles. Runtime-specific
|
|
5
|
+
;; handles are owned by Polymer Animation after Prosodic requests a gesture.
|
|
6
|
+
|
|
7
|
+
(def default-config
|
|
8
|
+
{:enabled true
|
|
9
|
+
:intensity 1
|
|
10
|
+
:priority 30
|
|
11
|
+
:speechGestureEvery 6
|
|
12
|
+
:blinkFastCooldownMs 1200})
|
|
13
|
+
|
|
14
|
+
(def default-state
|
|
15
|
+
{:agency "prosodic"
|
|
16
|
+
:speaking false
|
|
17
|
+
:wordIndex 0
|
|
18
|
+
:currentWord nil
|
|
19
|
+
:activeSnippets []
|
|
20
|
+
:scheduledCount 0
|
|
21
|
+
:removedCount 0
|
|
22
|
+
:conversationFactCount 0
|
|
23
|
+
:lastConversationFact nil
|
|
24
|
+
:lastGesture nil
|
|
25
|
+
:lastBlinkFastCueAt 0
|
|
26
|
+
:config default-config
|
|
27
|
+
:lastEvent nil})
|
|
28
|
+
|
|
29
|
+
(defn now-ms []
|
|
30
|
+
(.now js/Date))
|
|
31
|
+
|
|
32
|
+
(defn finite-number? [value]
|
|
33
|
+
(and (number? value) (js/isFinite value)))
|
|
34
|
+
|
|
35
|
+
(defn number-or [value fallback]
|
|
36
|
+
(if (finite-number? value) value fallback))
|
|
37
|
+
|
|
38
|
+
(defn clamp [lo hi value]
|
|
39
|
+
(-> value (max lo) (min hi)))
|
|
40
|
+
|
|
41
|
+
(defn js-config [config]
|
|
42
|
+
(if config (js->clj config :keywordize-keys true) {}))
|
|
43
|
+
|
|
44
|
+
(defn sanitize-config [config]
|
|
45
|
+
;; Clamp all public knobs in one pure function so commands from JS, workers,
|
|
46
|
+
;; or other agencies share the same agency contract.
|
|
47
|
+
(let [merged (merge default-config config)]
|
|
48
|
+
{:enabled (boolean (:enabled merged))
|
|
49
|
+
:intensity (clamp 0 2 (number-or (:intensity merged) (:intensity default-config)))
|
|
50
|
+
:priority (int (clamp -1000 1000 (number-or (:priority merged) (:priority default-config))))
|
|
51
|
+
:speechGestureEvery (int (clamp 2 32 (number-or (:speechGestureEvery merged)
|
|
52
|
+
(:speechGestureEvery default-config))))
|
|
53
|
+
:blinkFastCooldownMs (clamp 0 10000 (number-or (:blinkFastCooldownMs merged)
|
|
54
|
+
(:blinkFastCooldownMs default-config)))}))
|
|
55
|
+
|
|
56
|
+
(defn config->state [config]
|
|
57
|
+
(assoc default-state :config (sanitize-config (js-config config))))
|
|
58
|
+
|
|
59
|
+
(defn configure [state config]
|
|
60
|
+
(update state :config #(sanitize-config (merge % (js-config config)))))
|
|
61
|
+
|
|
62
|
+
(defn clean-word [word]
|
|
63
|
+
(when word
|
|
64
|
+
(let [value (.trim (str word))]
|
|
65
|
+
(when (pos? (count value)) value))))
|
|
66
|
+
|
|
67
|
+
(defn record-speech-start [state started-at name]
|
|
68
|
+
(-> state
|
|
69
|
+
(assoc :speaking true
|
|
70
|
+
:currentWord nil
|
|
71
|
+
:wordIndex 0
|
|
72
|
+
:lastEvent {:type "prosodicSpeechStarted"
|
|
73
|
+
:name name
|
|
74
|
+
:at started-at})))
|
|
75
|
+
|
|
76
|
+
(defn record-word-boundary [state word word-index observed-at]
|
|
77
|
+
(-> state
|
|
78
|
+
(assoc :currentWord word
|
|
79
|
+
:wordIndex (inc (or word-index (:wordIndex state) 0))
|
|
80
|
+
:lastEvent {:type "prosodicWordBoundary"
|
|
81
|
+
:word word
|
|
82
|
+
:wordIndex word-index
|
|
83
|
+
:at observed-at})))
|
|
84
|
+
|
|
85
|
+
(defn record-conversation-fact [state fact observed-at]
|
|
86
|
+
;; Conversation facts are local planning context. GOAP may schedule a gesture
|
|
87
|
+
;; from the same fact and later word boundaries can bias off lastConversationFact.
|
|
88
|
+
(-> state
|
|
89
|
+
(assoc :lastConversationFact (assoc fact :observedAt observed-at)
|
|
90
|
+
:lastEvent {:type "prosodicConversationFact"
|
|
91
|
+
:conversationType (:type fact)
|
|
92
|
+
:text (:text fact)
|
|
93
|
+
:at observed-at})
|
|
94
|
+
(update :conversationFactCount inc)))
|
|
95
|
+
|
|
96
|
+
(defn clear-conversation-suppress [state]
|
|
97
|
+
;; speechStarted ends cancel-driven gesture suppression.
|
|
98
|
+
(cond-> state
|
|
99
|
+
(= "conversation.cancelRequested" (get-in state [:lastConversationFact :type]))
|
|
100
|
+
(assoc :lastConversationFact nil)))
|
|
101
|
+
|
|
102
|
+
(defn record-schedule [state snippet-name gesture-kind scheduled-at]
|
|
103
|
+
(-> state
|
|
104
|
+
(update :activeSnippets #(vec (distinct (conj % snippet-name))))
|
|
105
|
+
(update :scheduledCount inc)
|
|
106
|
+
(assoc :lastGesture gesture-kind
|
|
107
|
+
:lastEvent {:type "prosodicGestureScheduled"
|
|
108
|
+
:name snippet-name
|
|
109
|
+
:gesture gesture-kind
|
|
110
|
+
:at scheduled-at})))
|
|
111
|
+
|
|
112
|
+
(defn record-remove [state snippet-name reason removed-at]
|
|
113
|
+
(-> state
|
|
114
|
+
(update :activeSnippets #(vec (remove #{snippet-name} %)))
|
|
115
|
+
(update :removedCount inc)
|
|
116
|
+
(assoc :lastEvent {:type "prosodicGestureRemoved"
|
|
117
|
+
:name snippet-name
|
|
118
|
+
:reason reason
|
|
119
|
+
:at removed-at})))
|
|
120
|
+
|
|
121
|
+
(defn clear-active [state reason stopped-at]
|
|
122
|
+
(-> state
|
|
123
|
+
(assoc :speaking false
|
|
124
|
+
:currentWord nil
|
|
125
|
+
:wordIndex 0
|
|
126
|
+
:activeSnippets []
|
|
127
|
+
:lastEvent {:type "prosodicStopped"
|
|
128
|
+
:reason reason
|
|
129
|
+
:at stopped-at})))
|
|
130
|
+
|
|
131
|
+
(defn blink-fast-cue-ready? [state now]
|
|
132
|
+
(let [cooldown (get-in state [:config :blinkFastCooldownMs])]
|
|
133
|
+
(>= (- now (:lastBlinkFastCueAt state)) cooldown)))
|
|
134
|
+
|
|
135
|
+
(defn record-blink-fast-cue [state now]
|
|
136
|
+
(assoc state :lastBlinkFastCueAt now))
|
|
137
|
+
|
|
138
|
+
(defn visible-state [state]
|
|
139
|
+
(clj->js state))
|