@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,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))