@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,259 @@
|
|
|
1
|
+
(ns polymer.eye-head.service
|
|
2
|
+
(:require [polymer.animation.service :as animation-service]
|
|
3
|
+
[polymer.eye-head.agency :as eye-head-agency]))
|
|
4
|
+
|
|
5
|
+
;; Compatibility adapter for the JavaScript service API LoomLarge already uses.
|
|
6
|
+
;; This is not the Eye/Head agency implementation. Movement work enters
|
|
7
|
+
;; Polymer's Eye/Head agency first; this adapter only preserves the historical
|
|
8
|
+
;; API while routing accepted animation requests into Polymer Animation.
|
|
9
|
+
|
|
10
|
+
(def EYE_AUS
|
|
11
|
+
#js {:BOTH_LOOK_LEFT 61
|
|
12
|
+
:BOTH_LOOK_RIGHT 62
|
|
13
|
+
:BOTH_LOOK_UP 63
|
|
14
|
+
:BOTH_LOOK_DOWN 64
|
|
15
|
+
:BLINK 43
|
|
16
|
+
:WIDE 5
|
|
17
|
+
:SQUINT 7})
|
|
18
|
+
|
|
19
|
+
(def HEAD_AUS
|
|
20
|
+
#js {:TURN_LEFT 51
|
|
21
|
+
:TURN_RIGHT 52
|
|
22
|
+
:TURN_UP 53
|
|
23
|
+
:TURN_DOWN 54
|
|
24
|
+
:TILT_LEFT 55
|
|
25
|
+
:TILT_RIGHT 56})
|
|
26
|
+
|
|
27
|
+
(def DEFAULT_EYE_HEAD_CONFIG
|
|
28
|
+
#js {:gazeMode "engine"
|
|
29
|
+
:eyeTrackingEnabled false
|
|
30
|
+
:headTrackingEnabled false
|
|
31
|
+
:headFollowEyes true
|
|
32
|
+
:eyeIntensity 1.0
|
|
33
|
+
:headIntensity 0.5
|
|
34
|
+
:returnToNeutralEnabled false
|
|
35
|
+
:returnToNeutralDelay 3000
|
|
36
|
+
:returnToNeutralDuration 800
|
|
37
|
+
:useAnimationAgency true})
|
|
38
|
+
|
|
39
|
+
(def DEFAULT_ANIMATION_KEYS
|
|
40
|
+
#js {:EYE_LOOK_LEFT "eyeHeadTracking/eyeLookLeft"
|
|
41
|
+
:EYE_LOOK_RIGHT "eyeHeadTracking/eyeLookRight"
|
|
42
|
+
:EYE_LOOK_UP "eyeHeadTracking/eyeLookUp"
|
|
43
|
+
:EYE_LOOK_DOWN "eyeHeadTracking/eyeLookDown"
|
|
44
|
+
:EYE_BLINK "eyeHeadTracking/eyeBlink"
|
|
45
|
+
:HEAD_TURN_LEFT "eyeHeadTracking/headTurnLeft"
|
|
46
|
+
:HEAD_TURN_RIGHT "eyeHeadTracking/headTurnRight"
|
|
47
|
+
:HEAD_TURN_UP "eyeHeadTracking/headTurnUp"
|
|
48
|
+
:HEAD_TURN_DOWN "eyeHeadTracking/headTurnDown"
|
|
49
|
+
:HEAD_TILT_LEFT "eyeHeadTracking/headTiltLeft"
|
|
50
|
+
:HEAD_TILT_RIGHT "eyeHeadTracking/headTiltRight"})
|
|
51
|
+
|
|
52
|
+
(defn data-map [value]
|
|
53
|
+
(cond
|
|
54
|
+
(map? value) value
|
|
55
|
+
value (js->clj value :keywordize-keys true)
|
|
56
|
+
:else {}))
|
|
57
|
+
|
|
58
|
+
(defn finite-number? [value]
|
|
59
|
+
(and (number? value) (js/isFinite value)))
|
|
60
|
+
|
|
61
|
+
(defn number-or [value fallback]
|
|
62
|
+
(if (finite-number? value) value fallback))
|
|
63
|
+
|
|
64
|
+
(defn js-call [target method-name & args]
|
|
65
|
+
(when target
|
|
66
|
+
(when-let [method (aget target method-name)]
|
|
67
|
+
(when (fn? method)
|
|
68
|
+
(.apply method target (to-array args))))))
|
|
69
|
+
|
|
70
|
+
(defn runtime-prop [value property-name]
|
|
71
|
+
(cond
|
|
72
|
+
(map? value) (get value (keyword property-name))
|
|
73
|
+
value (aget value property-name)
|
|
74
|
+
:else nil))
|
|
75
|
+
|
|
76
|
+
(defn normalize-target [target fallback]
|
|
77
|
+
(let [input (merge {:x 0 :y 0 :z 0} fallback (data-map target))]
|
|
78
|
+
{:x (max -1 (min 1 (number-or (:x input) 0)))
|
|
79
|
+
:y (max -1 (min 1 (number-or (:y input) 0)))
|
|
80
|
+
:z (number-or (:z input) 0)}))
|
|
81
|
+
|
|
82
|
+
(defn service-config [config]
|
|
83
|
+
(let [input (data-map config)
|
|
84
|
+
transition (number-or (:agencyTransitionDuration input)
|
|
85
|
+
(number-or (:transitionDurationMs input) 240))
|
|
86
|
+
neutral-duration (number-or (:returnToNeutralDuration input)
|
|
87
|
+
(number-or (:returnToCenterDurationMs input) 300))]
|
|
88
|
+
{:enabled true
|
|
89
|
+
:eyeTrackingEnabled (boolean (:eyeTrackingEnabled input))
|
|
90
|
+
:headTrackingEnabled (boolean (:headTrackingEnabled input))
|
|
91
|
+
:headFollowEyes (if (contains? input :headFollowEyes)
|
|
92
|
+
(boolean (:headFollowEyes input))
|
|
93
|
+
true)
|
|
94
|
+
:eyeIntensity (number-or (:eyeIntensity input) 1.0)
|
|
95
|
+
:headIntensity (number-or (:headIntensity input) 0.5)
|
|
96
|
+
:transitionDurationMs transition
|
|
97
|
+
:returnToCenterDurationMs neutral-duration
|
|
98
|
+
:coalesceMs (number-or (:coalesceMs input) 0)
|
|
99
|
+
:replaceExisting true}))
|
|
100
|
+
|
|
101
|
+
(defn service-state [state-atom mode-atom]
|
|
102
|
+
(let [snapshot @state-atom
|
|
103
|
+
agency-state (data-map snapshot)
|
|
104
|
+
config (or (:config agency-state) {})
|
|
105
|
+
current-target (or (:currentTarget agency-state) {:x 0 :y 0 :z 0})]
|
|
106
|
+
#js {:eyeStatus (if (get config :eyeTrackingEnabled) "tracking" "idle")
|
|
107
|
+
:headStatus (if (get config :headTrackingEnabled) "tracking" "idle")
|
|
108
|
+
:currentGaze (clj->js current-target)
|
|
109
|
+
:targetGaze (clj->js current-target)
|
|
110
|
+
:eyeIntensity (number-or (:eyeIntensity config) 1.0)
|
|
111
|
+
:lastBlinkTime (:lastBlinkTime agency-state)
|
|
112
|
+
:headIntensity (number-or (:headIntensity config) 0.5)
|
|
113
|
+
:headFollowTimer nil
|
|
114
|
+
:isSpeaking (boolean (:isSpeaking agency-state))
|
|
115
|
+
:isListening (boolean (:isListening agency-state))
|
|
116
|
+
:returnToNeutralTimer nil
|
|
117
|
+
:lastGazeUpdateTime (or (:lastGazeUpdateTime agency-state) 0)
|
|
118
|
+
:mode @mode-atom}))
|
|
119
|
+
|
|
120
|
+
(defn create-blink-snippet []
|
|
121
|
+
(let [name (str "eyeHeadTracking:blink:" (.now js/Date))]
|
|
122
|
+
{:name name
|
|
123
|
+
:curves {"43" [{:time 0.0 :intensity 0.0 :inherit true}
|
|
124
|
+
{:time 0.045 :intensity 1.0}
|
|
125
|
+
{:time 0.13 :intensity 0.0}]}
|
|
126
|
+
:channels [{:target {:type "au" :id 43}
|
|
127
|
+
:keyframes [{:time 0.0 :intensity 0.0 :inherit true}
|
|
128
|
+
{:time 0.045 :intensity 1.0}
|
|
129
|
+
{:time 0.13 :intensity 0.0}]}]
|
|
130
|
+
:maxTime 0.13
|
|
131
|
+
:loop false
|
|
132
|
+
:snippetCategory "eyeHeadTracking"
|
|
133
|
+
:snippetPriority 25
|
|
134
|
+
:snippetBlendMode "additive"
|
|
135
|
+
:snippetPlaybackRate 1.0
|
|
136
|
+
:snippetIntensityScale 1.0}))
|
|
137
|
+
|
|
138
|
+
(defn animation-target [config]
|
|
139
|
+
(or (runtime-prop config "animationAgency")
|
|
140
|
+
(when-let [engine (runtime-prop config "engine")]
|
|
141
|
+
(animation-service/createAnimationService engine))))
|
|
142
|
+
|
|
143
|
+
(defn schedule-animation! [anim snippet options]
|
|
144
|
+
(or (js-call anim "schedule" (clj->js snippet) (clj->js options))
|
|
145
|
+
(js-call anim "scheduleSnippet" (clj->js snippet) (clj->js options))))
|
|
146
|
+
|
|
147
|
+
(defn remove-animation! [anim name]
|
|
148
|
+
(or (js-call anim "remove" name)
|
|
149
|
+
(js-call anim "removeSnippet" name)))
|
|
150
|
+
|
|
151
|
+
(defn createEyeHeadTrackingService
|
|
152
|
+
([]
|
|
153
|
+
(createEyeHeadTrackingService nil nil))
|
|
154
|
+
([config]
|
|
155
|
+
(createEyeHeadTrackingService config nil))
|
|
156
|
+
([config callbacks]
|
|
157
|
+
(let [config-atom (atom (service-config config))
|
|
158
|
+
mode-atom (atom (or (:gazeMode (data-map config)) "manual"))
|
|
159
|
+
state-atom (atom (merge {:isSpeaking false
|
|
160
|
+
:isListening false
|
|
161
|
+
:lastBlinkTime nil
|
|
162
|
+
:lastGazeUpdateTime 0}
|
|
163
|
+
{:config @config-atom
|
|
164
|
+
:currentTarget {:x 0 :y 0 :z 0}}))
|
|
165
|
+
agency (eye-head-agency/create-eye-head-tracking-agency (clj->js @config-atom))
|
|
166
|
+
anim (animation-target config)
|
|
167
|
+
started? (atom false)
|
|
168
|
+
disposed? (atom false)
|
|
169
|
+
unsubscribers (atom [])]
|
|
170
|
+
(letfn [(dispatch! [command]
|
|
171
|
+
(when-not @disposed?
|
|
172
|
+
(js-call agency "dispatch" (clj->js command))))
|
|
173
|
+
(configure! [next-config]
|
|
174
|
+
(swap! config-atom merge (service-config next-config))
|
|
175
|
+
(swap! state-atom assoc :config @config-atom)
|
|
176
|
+
(dispatch! {:type "configure" :config @config-atom}))
|
|
177
|
+
(route-event! [event]
|
|
178
|
+
(let [payload (data-map event)]
|
|
179
|
+
(case (:type payload)
|
|
180
|
+
"animation.requestScheduleSnippet"
|
|
181
|
+
(schedule-animation! anim (:snippet payload) (or (:options payload) {:autoPlay true}))
|
|
182
|
+
|
|
183
|
+
"animation.requestRemoveSnippet"
|
|
184
|
+
(remove-animation! anim (:name payload))
|
|
185
|
+
|
|
186
|
+
"error"
|
|
187
|
+
(js-call callbacks "onError" (js/Error. (or (:message payload)
|
|
188
|
+
"Eye/head tracking error")))
|
|
189
|
+
|
|
190
|
+
nil)
|
|
191
|
+
(when (= "animation.requestScheduleSnippet" (:type payload))
|
|
192
|
+
(swap! state-atom assoc
|
|
193
|
+
:currentTarget (get-in payload [:snippet :metadata :target] (:currentTarget @state-atom))
|
|
194
|
+
:lastGazeUpdateTime (.now js/Date)))))
|
|
195
|
+
(teardown! []
|
|
196
|
+
(doseq [unsubscribe @unsubscribers]
|
|
197
|
+
(unsubscribe))
|
|
198
|
+
(reset! unsubscribers []))]
|
|
199
|
+
(swap! unsubscribers conj (js-call agency "subscribeEvents" route-event!))
|
|
200
|
+
#js {:start (fn []
|
|
201
|
+
(when-not @disposed?
|
|
202
|
+
(reset! started? true)
|
|
203
|
+
(dispatch! {:type "enable"})))
|
|
204
|
+
:stop (fn []
|
|
205
|
+
(when-not @disposed?
|
|
206
|
+
(reset! started? false)
|
|
207
|
+
(dispatch! {:type "cancel" :reason "stopped"})))
|
|
208
|
+
:setGazeTarget (fn [target]
|
|
209
|
+
(let [normalized (normalize-target target nil)]
|
|
210
|
+
(swap! state-atom assoc
|
|
211
|
+
:currentTarget normalized
|
|
212
|
+
:lastGazeUpdateTime (.now js/Date))
|
|
213
|
+
(dispatch! {:type "setTarget"
|
|
214
|
+
:target normalized
|
|
215
|
+
:mode @mode-atom})))
|
|
216
|
+
:resetToNeutral (fn
|
|
217
|
+
([] (dispatch! {:type "reset"}))
|
|
218
|
+
([duration-ms]
|
|
219
|
+
(dispatch! {:type "reset" :durationMs duration-ms})))
|
|
220
|
+
:blink (fn []
|
|
221
|
+
(when-not @disposed?
|
|
222
|
+
(let [snippet (create-blink-snippet)]
|
|
223
|
+
(swap! state-atom assoc :lastBlinkTime (.now js/Date))
|
|
224
|
+
(schedule-animation! anim snippet {:autoPlay true
|
|
225
|
+
:sourceAgency "eyeHeadTracking"}))))
|
|
226
|
+
:setSpeaking (fn [is-speaking]
|
|
227
|
+
(swap! state-atom assoc :isSpeaking (boolean is-speaking))
|
|
228
|
+
(if is-speaking
|
|
229
|
+
(js-call callbacks "onHeadStart")
|
|
230
|
+
(js-call callbacks "onHeadStop")))
|
|
231
|
+
:setListening (fn [is-listening]
|
|
232
|
+
(swap! state-atom assoc :isListening (boolean is-listening))
|
|
233
|
+
(if is-listening
|
|
234
|
+
(js-call callbacks "onEyeStart")
|
|
235
|
+
(js-call callbacks "onEyeStop")))
|
|
236
|
+
:setEyeBlendWeight (fn [_value] nil)
|
|
237
|
+
:setHeadBlendWeight (fn [_value] nil)
|
|
238
|
+
:updateConfig configure!
|
|
239
|
+
:getState (fn [] (service-state state-atom mode-atom))
|
|
240
|
+
:getMachineContext (fn []
|
|
241
|
+
#js {:config (clj->js @config-atom)
|
|
242
|
+
:mode @mode-atom
|
|
243
|
+
:target (clj->js (:currentTarget @state-atom))
|
|
244
|
+
:lastApplied (clj->js (:currentTarget @state-atom))
|
|
245
|
+
:status #js {:eye (if (:eyeTrackingEnabled @config-atom) "tracking" "idle")
|
|
246
|
+
:head (if (:headTrackingEnabled @config-atom) "tracking" "idle")}})
|
|
247
|
+
:getSnippets (fn [] #js {:eye (js/Map.) :head (js/Map.)})
|
|
248
|
+
:setMode (fn [mode]
|
|
249
|
+
(reset! mode-atom mode)
|
|
250
|
+
(dispatch! {:type "configure" :config {:mode mode}}))
|
|
251
|
+
:getMode (fn [] @mode-atom)
|
|
252
|
+
:subscribeToWebcam (fn [_callback] (fn [] nil))
|
|
253
|
+
:getWebcamVideoElement (fn [] nil)
|
|
254
|
+
:isWebcamActive (fn [] false)
|
|
255
|
+
:dispose (fn []
|
|
256
|
+
(when-not @disposed?
|
|
257
|
+
(reset! disposed? true)
|
|
258
|
+
(teardown!)
|
|
259
|
+
(js-call agency "dispose")))}))))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
(ns polymer.eye-head.snippet)
|
|
2
|
+
|
|
3
|
+
;; Eye/Head snippets use typed AU channels. Numeric curve keys remain for
|
|
4
|
+
;; compatibility with older animation builds, while typed channels are the
|
|
5
|
+
;; canonical contract for the current Animation agency.
|
|
6
|
+
|
|
7
|
+
(def eye-yaw-left 61)
|
|
8
|
+
(def eye-yaw-right 62)
|
|
9
|
+
(def eye-pitch-up 63)
|
|
10
|
+
(def eye-pitch-down 64)
|
|
11
|
+
(def head-yaw-left 51)
|
|
12
|
+
(def head-yaw-right 52)
|
|
13
|
+
(def head-pitch-up 53)
|
|
14
|
+
(def head-pitch-down 54)
|
|
15
|
+
(def head-roll-left 55)
|
|
16
|
+
(def head-roll-right 56)
|
|
17
|
+
|
|
18
|
+
(defn seconds
|
|
19
|
+
[duration-ms]
|
|
20
|
+
(/ (max 1 duration-ms) 1000))
|
|
21
|
+
|
|
22
|
+
(defn axis-curves
|
|
23
|
+
[negative-au positive-au value duration-sec]
|
|
24
|
+
(let [value (max -1 (min 1 value))
|
|
25
|
+
negative-value (if (neg? value) (js/Math.abs value) 0)
|
|
26
|
+
positive-value (if (pos? value) value 0)]
|
|
27
|
+
{(str negative-au) [{:time 0 :intensity 0 :inherit true}
|
|
28
|
+
{:time duration-sec :intensity negative-value}]
|
|
29
|
+
(str positive-au) [{:time 0 :intensity 0 :inherit true}
|
|
30
|
+
{:time duration-sec :intensity positive-value}]}))
|
|
31
|
+
|
|
32
|
+
(defn au-channel
|
|
33
|
+
[au-id curve]
|
|
34
|
+
{:target {:type "au" :id au-id}
|
|
35
|
+
:keyframes curve})
|
|
36
|
+
|
|
37
|
+
(defn curves->channels
|
|
38
|
+
[curves]
|
|
39
|
+
(into []
|
|
40
|
+
(map (fn [[au-id curve]]
|
|
41
|
+
(au-channel (js/parseInt au-id 10) curve)))
|
|
42
|
+
curves))
|
|
43
|
+
|
|
44
|
+
(defn merge-curves
|
|
45
|
+
[& curve-maps]
|
|
46
|
+
(apply merge curve-maps))
|
|
47
|
+
|
|
48
|
+
(defn build-eye-curves
|
|
49
|
+
[request]
|
|
50
|
+
(let [target (:target request)
|
|
51
|
+
duration-sec (seconds (:eyeDurationMs request))
|
|
52
|
+
yaw (* (:x target) (:eyeIntensity request))
|
|
53
|
+
pitch (* (:y target) (:eyeIntensity request))]
|
|
54
|
+
(merge-curves
|
|
55
|
+
(axis-curves eye-yaw-left eye-yaw-right yaw duration-sec)
|
|
56
|
+
(axis-curves eye-pitch-down eye-pitch-up pitch duration-sec))))
|
|
57
|
+
|
|
58
|
+
(defn build-head-curves
|
|
59
|
+
[request]
|
|
60
|
+
(let [target (:target request)
|
|
61
|
+
duration-sec (seconds (:headDurationMs request))
|
|
62
|
+
yaw (* (:x target) (:headIntensity request))
|
|
63
|
+
pitch (* (:y target) (:headIntensity request))
|
|
64
|
+
roll (* (:headRoll request) (:headIntensity request))]
|
|
65
|
+
(merge-curves
|
|
66
|
+
(axis-curves head-yaw-left head-yaw-right yaw duration-sec)
|
|
67
|
+
(axis-curves head-pitch-down head-pitch-up pitch duration-sec)
|
|
68
|
+
(axis-curves head-roll-left head-roll-right roll duration-sec))))
|
|
69
|
+
|
|
70
|
+
(defn build-gaze-snippet
|
|
71
|
+
[request config]
|
|
72
|
+
(let [eye-curves (when (:eyeEnabled request) (build-eye-curves request))
|
|
73
|
+
head-curves (when (and (:headEnabled request) (:headFollowEyes request))
|
|
74
|
+
(build-head-curves request))
|
|
75
|
+
curves (merge-curves (or eye-curves {}) (or head-curves {}))
|
|
76
|
+
channels (curves->channels curves)
|
|
77
|
+
max-time (max (seconds (:eyeDurationMs request))
|
|
78
|
+
(seconds (:headDurationMs request)))]
|
|
79
|
+
{:name (str "eyeHeadTracking:gaze:" (:requestId request))
|
|
80
|
+
:curves curves
|
|
81
|
+
:channels channels
|
|
82
|
+
:maxTime max-time
|
|
83
|
+
:loop false
|
|
84
|
+
:mixerClampWhenFinished true
|
|
85
|
+
:snippetPriority (:snippetPriority config)
|
|
86
|
+
:snippetPlaybackRate 1.0
|
|
87
|
+
:snippetIntensityScale 1.0
|
|
88
|
+
:metadata {:agency "eyeHeadTracking"
|
|
89
|
+
:sourceAgency (:sourceAgency request)
|
|
90
|
+
:requestId (:requestId request)
|
|
91
|
+
:mode (:mode request)
|
|
92
|
+
:target (:target request)}}))
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
(ns polymer.eye-head.state
|
|
2
|
+
(:require [polymer.eye-head.domain :as domain]))
|
|
3
|
+
|
|
4
|
+
;; Eye/Head state tracks the local movement schedule. Runtime handles remain
|
|
5
|
+
;; private to the Animation agency; this agency only records plain request and
|
|
6
|
+
;; snippet metadata.
|
|
7
|
+
|
|
8
|
+
(def default-config
|
|
9
|
+
{:enabled true
|
|
10
|
+
:eyeTrackingEnabled true
|
|
11
|
+
:headTrackingEnabled true
|
|
12
|
+
:headFollowEyes true
|
|
13
|
+
:eyeIntensity 1.0
|
|
14
|
+
:headIntensity 0.5
|
|
15
|
+
:headRoll 0
|
|
16
|
+
:eyePriority 20
|
|
17
|
+
:headPriority 15
|
|
18
|
+
:snippetPriority 20
|
|
19
|
+
:transitionDurationMs 240
|
|
20
|
+
:returnToCenterDurationMs 300
|
|
21
|
+
:coalesceMs 0
|
|
22
|
+
:replaceExisting true})
|
|
23
|
+
|
|
24
|
+
(defn input-value
|
|
25
|
+
[input camel-key kebab-key fallback]
|
|
26
|
+
(let [camel (get input camel-key ::missing)
|
|
27
|
+
kebab (get input kebab-key ::missing)]
|
|
28
|
+
(cond
|
|
29
|
+
(not= camel ::missing) camel
|
|
30
|
+
(not= kebab ::missing) kebab
|
|
31
|
+
:else fallback)))
|
|
32
|
+
|
|
33
|
+
(defn normalize-config
|
|
34
|
+
[config]
|
|
35
|
+
(let [input (merge default-config (domain/data-map config))]
|
|
36
|
+
{:enabled (boolean (input-value input :enabled :enabled (:enabled default-config)))
|
|
37
|
+
:eyeTrackingEnabled (boolean (input-value input :eyeTrackingEnabled :eye-tracking-enabled (:eyeTrackingEnabled default-config)))
|
|
38
|
+
:headTrackingEnabled (boolean (input-value input :headTrackingEnabled :head-tracking-enabled (:headTrackingEnabled default-config)))
|
|
39
|
+
:headFollowEyes (boolean (input-value input :headFollowEyes :head-follow-eyes (:headFollowEyes default-config)))
|
|
40
|
+
:eyeIntensity (domain/clamp 0 2
|
|
41
|
+
(domain/number-or (input-value input :eyeIntensity :eye-intensity nil)
|
|
42
|
+
(:eyeIntensity default-config)))
|
|
43
|
+
:headIntensity (domain/clamp 0 2
|
|
44
|
+
(domain/number-or (input-value input :headIntensity :head-intensity nil)
|
|
45
|
+
(:headIntensity default-config)))
|
|
46
|
+
:headRoll (domain/clamp -1 1
|
|
47
|
+
(domain/number-or (input-value input :headRoll :head-roll nil)
|
|
48
|
+
(:headRoll default-config)))
|
|
49
|
+
:eyePriority (int (domain/clamp 0 100
|
|
50
|
+
(domain/number-or (input-value input :eyePriority :eye-priority nil)
|
|
51
|
+
(:eyePriority default-config))))
|
|
52
|
+
:headPriority (int (domain/clamp 0 100
|
|
53
|
+
(domain/number-or (input-value input :headPriority :head-priority nil)
|
|
54
|
+
(:headPriority default-config))))
|
|
55
|
+
:snippetPriority (int (domain/clamp 0 100
|
|
56
|
+
(domain/number-or (input-value input :snippetPriority :snippet-priority nil)
|
|
57
|
+
(:snippetPriority default-config))))
|
|
58
|
+
:transitionDurationMs (int (domain/clamp 50 3000
|
|
59
|
+
(domain/number-or
|
|
60
|
+
(input-value input :transitionDurationMs :transition-duration-ms nil)
|
|
61
|
+
(:transitionDurationMs default-config))))
|
|
62
|
+
:returnToCenterDurationMs (int (domain/clamp 50 3000
|
|
63
|
+
(domain/number-or
|
|
64
|
+
(input-value input :returnToCenterDurationMs :return-to-center-duration-ms nil)
|
|
65
|
+
(:returnToCenterDurationMs default-config))))
|
|
66
|
+
:coalesceMs (int (domain/clamp 0 1000
|
|
67
|
+
(domain/number-or (input-value input :coalesceMs :coalesce-ms nil)
|
|
68
|
+
(:coalesceMs default-config))))
|
|
69
|
+
:replaceExisting (boolean (input-value input :replaceExisting :replace-existing (:replaceExisting default-config)))}))
|
|
70
|
+
|
|
71
|
+
(defn default-state
|
|
72
|
+
[config]
|
|
73
|
+
{:agency "eyeHeadTracking"
|
|
74
|
+
:status "idle"
|
|
75
|
+
:mode "manual"
|
|
76
|
+
:currentTarget domain/zero-target
|
|
77
|
+
:pendingRequest nil
|
|
78
|
+
:lastRequest nil
|
|
79
|
+
:lastSnippet nil
|
|
80
|
+
:activeSnippetNames []
|
|
81
|
+
:lastIgnored nil
|
|
82
|
+
:lastPlan nil
|
|
83
|
+
:scheduledCount 0
|
|
84
|
+
:removedCount 0
|
|
85
|
+
:resetCount 0
|
|
86
|
+
:cancelCount 0
|
|
87
|
+
:ignoredCount 0
|
|
88
|
+
:config (normalize-config config)})
|
|
89
|
+
|
|
90
|
+
(defn config->state
|
|
91
|
+
[config]
|
|
92
|
+
(default-state config))
|
|
93
|
+
|
|
94
|
+
(defn visible-state
|
|
95
|
+
[state]
|
|
96
|
+
(clj->js state))
|
|
97
|
+
|
|
98
|
+
(defn configure
|
|
99
|
+
[state config]
|
|
100
|
+
(assoc state :config (normalize-config (merge (:config state)
|
|
101
|
+
(domain/data-map config)))))
|
|
102
|
+
|
|
103
|
+
(defn set-enabled
|
|
104
|
+
[state enabled]
|
|
105
|
+
(assoc-in state [:config :enabled] (boolean enabled)))
|
|
106
|
+
|
|
107
|
+
(defn record-plan
|
|
108
|
+
[state plan]
|
|
109
|
+
(assoc state :lastPlan plan))
|
|
110
|
+
|
|
111
|
+
(defn record-pending
|
|
112
|
+
[state request]
|
|
113
|
+
(assoc state
|
|
114
|
+
:status "pending"
|
|
115
|
+
:pendingRequest request))
|
|
116
|
+
|
|
117
|
+
(defn record-scheduled
|
|
118
|
+
[state request snippet published-at]
|
|
119
|
+
(-> state
|
|
120
|
+
(assoc :status "tracking"
|
|
121
|
+
:mode (:mode request)
|
|
122
|
+
:currentTarget (:target request)
|
|
123
|
+
:pendingRequest nil
|
|
124
|
+
:lastRequest request
|
|
125
|
+
:lastSnippet snippet
|
|
126
|
+
:activeSnippetNames [(:name snippet)]
|
|
127
|
+
:lastPlan nil
|
|
128
|
+
:lastEvent {:type "animation.requestScheduleSnippet"
|
|
129
|
+
:name (:name snippet)
|
|
130
|
+
:requestId (:requestId request)
|
|
131
|
+
:at published-at})
|
|
132
|
+
(update :scheduledCount inc)))
|
|
133
|
+
|
|
134
|
+
(defn record-removed
|
|
135
|
+
[state name reason removed-at]
|
|
136
|
+
(-> state
|
|
137
|
+
(assoc :activeSnippetNames []
|
|
138
|
+
:lastEvent {:type "animation.requestRemoveSnippet"
|
|
139
|
+
:name name
|
|
140
|
+
:reason reason
|
|
141
|
+
:at removed-at})
|
|
142
|
+
(update :removedCount inc)))
|
|
143
|
+
|
|
144
|
+
(defn record-ignored
|
|
145
|
+
[state ignored]
|
|
146
|
+
(-> state
|
|
147
|
+
(assoc :status "ignored"
|
|
148
|
+
:lastIgnored ignored
|
|
149
|
+
:pendingRequest nil
|
|
150
|
+
:lastEvent ignored)
|
|
151
|
+
(update :ignoredCount inc)))
|
|
152
|
+
|
|
153
|
+
(defn record-reset
|
|
154
|
+
[state request snippet published-at]
|
|
155
|
+
(-> (record-scheduled state request snippet published-at)
|
|
156
|
+
(assoc :status "reset"
|
|
157
|
+
:currentTarget domain/zero-target)
|
|
158
|
+
(update :resetCount inc)))
|
|
159
|
+
|
|
160
|
+
(defn record-cancel
|
|
161
|
+
[state request]
|
|
162
|
+
(-> state
|
|
163
|
+
(assoc :status "cancelled"
|
|
164
|
+
:pendingRequest nil
|
|
165
|
+
:activeSnippetNames []
|
|
166
|
+
:lastEvent request)
|
|
167
|
+
(update :cancelCount inc)))
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
(ns polymer.gaze.agency
|
|
2
|
+
(:require [polymer.gaze.domain :as domain]
|
|
3
|
+
[polymer.gaze.goap :as goap]
|
|
4
|
+
[polymer.gaze.scheduler :as scheduler]
|
|
5
|
+
[polymer.gaze.state :as state]
|
|
6
|
+
[polymer.stream :as stream]))
|
|
7
|
+
|
|
8
|
+
;; Gaze converts attention and camera-relative facts into look-intent requests.
|
|
9
|
+
;; It does not move eyes or head directly. The downstream movement agency owns
|
|
10
|
+
;; the runtime side effects after accepting these request messages.
|
|
11
|
+
|
|
12
|
+
(defn now-ms
|
|
13
|
+
[]
|
|
14
|
+
(.now js/Date))
|
|
15
|
+
|
|
16
|
+
(defn create-gaze-agency
|
|
17
|
+
[config]
|
|
18
|
+
(let [input-stream (stream/create-stream)
|
|
19
|
+
event-stream (stream/create-stream)
|
|
20
|
+
state-atom (atom (state/config->state config))
|
|
21
|
+
disposed? (atom false)
|
|
22
|
+
emit-input (:emit input-stream)
|
|
23
|
+
emit-event (:emit event-stream)
|
|
24
|
+
scheduler-atom (atom nil)]
|
|
25
|
+
(letfn [(dispatch! [command]
|
|
26
|
+
(when-not @disposed?
|
|
27
|
+
(let [payload (domain/data-map command)
|
|
28
|
+
plan (goap/plan-command payload @state-atom (now-ms))]
|
|
29
|
+
(emit-input {:type "command"
|
|
30
|
+
:agency "gaze"
|
|
31
|
+
:command payload})
|
|
32
|
+
(swap! state-atom assoc :lastPlan plan)
|
|
33
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
34
|
+
((:schedule agency-scheduler) plan)))))
|
|
35
|
+
|
|
36
|
+
(dispatch-type! [type]
|
|
37
|
+
(dispatch! #js {:type type}))]
|
|
38
|
+
(let [agency-scheduler (scheduler/create-scheduler
|
|
39
|
+
{:state-atom state-atom
|
|
40
|
+
:emit-event emit-event})]
|
|
41
|
+
(reset! scheduler-atom agency-scheduler)
|
|
42
|
+
#js {:dispatch dispatch!
|
|
43
|
+
:snapshot (fn [] (state/visible-state @state-atom))
|
|
44
|
+
:input (stream/writable-port input-stream dispatch!)
|
|
45
|
+
:events (stream/readable-port event-stream)
|
|
46
|
+
:subscribeInput (fn [listener] ((:subscribe input-stream) listener))
|
|
47
|
+
:subscribeEvents (fn [listener] ((:subscribe event-stream) listener))
|
|
48
|
+
:subscribe (fn [listener] ((:subscribe event-stream) listener))
|
|
49
|
+
:subscribeStatus (fn [listener] ((:subscribe event-stream) listener))
|
|
50
|
+
:setTarget (fn
|
|
51
|
+
([target] (dispatch! #js {:type "setTarget"
|
|
52
|
+
:target target}))
|
|
53
|
+
([target options] (dispatch! #js {:type "setTarget"
|
|
54
|
+
:target target
|
|
55
|
+
:options options})))
|
|
56
|
+
:focusTarget (fn [target] (dispatch! #js {:type "focusTarget"
|
|
57
|
+
:target target}))
|
|
58
|
+
:configure (fn [config] (dispatch! #js {:type "configure"
|
|
59
|
+
:config config}))
|
|
60
|
+
:setMode (fn [mode] (dispatch! #js {:type "setMode"
|
|
61
|
+
:mode mode}))
|
|
62
|
+
:setActive (fn [active] (dispatch! #js {:type "setActive"
|
|
63
|
+
:active active}))
|
|
64
|
+
:enable (fn [] (dispatch-type! "enable"))
|
|
65
|
+
:disable (fn [] (dispatch-type! "disable"))
|
|
66
|
+
:reset (fn
|
|
67
|
+
([] (dispatch-type! "reset"))
|
|
68
|
+
([duration-ms] (dispatch! #js {:type "reset"
|
|
69
|
+
:durationMs duration-ms})))
|
|
70
|
+
:cancel (fn
|
|
71
|
+
([] (dispatch-type! "cancel"))
|
|
72
|
+
([reason] (dispatch! #js {:type "cancel"
|
|
73
|
+
:reason reason})))
|
|
74
|
+
:flush (fn []
|
|
75
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
76
|
+
((:flush agency-scheduler))))
|
|
77
|
+
:queue (fn []
|
|
78
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
79
|
+
(clj->js ((:queue agency-scheduler)))))
|
|
80
|
+
:dispose (fn []
|
|
81
|
+
(when-not @disposed?
|
|
82
|
+
(reset! disposed? true)
|
|
83
|
+
((:dispose agency-scheduler))
|
|
84
|
+
((:dispose input-stream))
|
|
85
|
+
((:dispose event-stream))))}))))
|