@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,605 @@
|
|
|
1
|
+
(ns polymer.lipsync.articulation.snippet
|
|
2
|
+
(:require [clojure.string :as str]
|
|
3
|
+
[polymer.lipsync.articulation.jaw :as jaw]
|
|
4
|
+
[polymer.lipsync.state :as state]
|
|
5
|
+
[polymer.lipsync.articulation.tongue :as tongue]
|
|
6
|
+
[polymer.lipsync.articulation.visemes :as visemes]))
|
|
7
|
+
|
|
8
|
+
;; Snippet construction is pure: it turns a normalized viseme timeline into the
|
|
9
|
+
;; animation data that Polymer Animation can schedule. It does not know about
|
|
10
|
+
;; host UI code, audio playback, LiveKit, Azure credentials, or engine handles.
|
|
11
|
+
|
|
12
|
+
(def jaw-au "26")
|
|
13
|
+
(def labiodental-contact-au "32")
|
|
14
|
+
(def labiodental-press-au "24")
|
|
15
|
+
(def intensity-eps 0.001)
|
|
16
|
+
(def coarticulation-strength 0.52)
|
|
17
|
+
(def envelope-shoulder-ratio 0.55)
|
|
18
|
+
(def envelope-shoulder-intensity 0.62)
|
|
19
|
+
(def lip-total-activation-cap 1.05)
|
|
20
|
+
(def lip-dominant-cap 1)
|
|
21
|
+
(def lip-secondary-ratio 0.30)
|
|
22
|
+
(def lip-secondary-cap 0.22)
|
|
23
|
+
(def closure-dominance-threshold 0.55)
|
|
24
|
+
(def closure-secondary-cap 0.035)
|
|
25
|
+
(def plosive-preclose-sec 0.028)
|
|
26
|
+
(def plosive-hold-sec 0.022)
|
|
27
|
+
(def plosive-release-sec 0.018)
|
|
28
|
+
(def labiodental-contact-peak 0.24)
|
|
29
|
+
(def labiodental-press-peak 0.12)
|
|
30
|
+
|
|
31
|
+
(def vowel-visemes
|
|
32
|
+
#{(:AE visemes/canonical-visemes)
|
|
33
|
+
(:Ah visemes/canonical-visemes)
|
|
34
|
+
(:EE visemes/canonical-visemes)
|
|
35
|
+
(:Er visemes/canonical-visemes)
|
|
36
|
+
(:Ih visemes/canonical-visemes)
|
|
37
|
+
(:Oh visemes/canonical-visemes)})
|
|
38
|
+
|
|
39
|
+
(defn finite-number? [value]
|
|
40
|
+
(and (number? value) (js/isFinite value)))
|
|
41
|
+
|
|
42
|
+
(defn normalize-event [event]
|
|
43
|
+
(let [viseme-id (or (:visemeId event) (:viseme_id event) (:id event))
|
|
44
|
+
offset-ms (or (:offsetMs event) (:offset_ms event) (:offset event) 0)
|
|
45
|
+
duration-ms (or (:durationMs event) (:duration_ms event) (:duration event) 0)
|
|
46
|
+
jaw-activation (or (:jawActivation event)
|
|
47
|
+
(:jaw_activation event)
|
|
48
|
+
(:jaw event)
|
|
49
|
+
(:jawAmount event))]
|
|
50
|
+
(when (and (finite-number? viseme-id)
|
|
51
|
+
(finite-number? offset-ms)
|
|
52
|
+
(finite-number? duration-ms)
|
|
53
|
+
(pos? duration-ms))
|
|
54
|
+
(let [canonical-id (int (state/clamp 0 14 viseme-id))]
|
|
55
|
+
(cond->
|
|
56
|
+
{:visemeId canonical-id
|
|
57
|
+
:offsetMs (max 0 offset-ms)
|
|
58
|
+
:durationMs (max 1 duration-ms)
|
|
59
|
+
:jawActivation (if (finite-number? jaw-activation)
|
|
60
|
+
(state/clamp 0 2 jaw-activation)
|
|
61
|
+
(visemes/jaw-activation-for-viseme canonical-id))}
|
|
62
|
+
(:phoneme event) (assoc :phoneme (:phoneme event))
|
|
63
|
+
(or (:phonemeClass event) (:phoneme_class event))
|
|
64
|
+
(assoc :phonemeClass (or (:phonemeClass event) (:phoneme_class event)))
|
|
65
|
+
(or (:phonemeClasses event) (:phoneme_classes event))
|
|
66
|
+
(assoc :phonemeClasses (or (:phonemeClasses event) (:phoneme_classes event)))
|
|
67
|
+
(:neutral event) (assoc :neutral true))))))
|
|
68
|
+
|
|
69
|
+
(defn normalize-events [events]
|
|
70
|
+
;; Normalize/drop is element-local and transducer-friendly. Sorting is kept as
|
|
71
|
+
;; the next explicit step because ordering is a whole-collection decision.
|
|
72
|
+
(->> (into [] (keep normalize-event) (or events []))
|
|
73
|
+
(sort-by :offsetMs)
|
|
74
|
+
vec))
|
|
75
|
+
|
|
76
|
+
(defn viseme-class [viseme-id]
|
|
77
|
+
(cond
|
|
78
|
+
(= viseme-id (:B_M_P visemes/canonical-visemes)) :bilabial
|
|
79
|
+
(= viseme-id (:W_OO visemes/canonical-visemes)) :glide
|
|
80
|
+
(contains? vowel-visemes viseme-id) :vowel
|
|
81
|
+
(#{(:Ch_J visemes/canonical-visemes)
|
|
82
|
+
(:F_V visemes/canonical-visemes)
|
|
83
|
+
(:S_Z visemes/canonical-visemes)
|
|
84
|
+
(:Th visemes/canonical-visemes)} viseme-id) :fricative
|
|
85
|
+
(#{(:K_G_H_NG visemes/canonical-visemes)
|
|
86
|
+
(:T_L_D_N visemes/canonical-visemes)} viseme-id) :tongue
|
|
87
|
+
(= viseme-id (:R visemes/canonical-visemes)) :liquid
|
|
88
|
+
:else :default))
|
|
89
|
+
|
|
90
|
+
(defn class-name [value]
|
|
91
|
+
(cond
|
|
92
|
+
(keyword? value) (name value)
|
|
93
|
+
(string? value) (str/lower-case value)
|
|
94
|
+
(nil? value) nil
|
|
95
|
+
:else (str/lower-case (str value))))
|
|
96
|
+
|
|
97
|
+
(defn event-classes [event]
|
|
98
|
+
(let [from-event (into []
|
|
99
|
+
(keep class-name)
|
|
100
|
+
(concat (or (:phonemeClasses event) [])
|
|
101
|
+
[(:phonemeClass event)]))
|
|
102
|
+
fallback (into [] (keep class-name) (visemes/viseme-classes (:visemeId event)))]
|
|
103
|
+
(set (if (seq from-event) from-event fallback))))
|
|
104
|
+
|
|
105
|
+
(defn has-class? [event class]
|
|
106
|
+
(contains? (event-classes event) class))
|
|
107
|
+
|
|
108
|
+
(defn normalized-phoneme [event]
|
|
109
|
+
(visemes/normalize-phoneme (:phoneme event)))
|
|
110
|
+
|
|
111
|
+
(defn vowel-event? [event]
|
|
112
|
+
(or (has-class? event "vowel")
|
|
113
|
+
(contains? vowel-visemes (:visemeId event))))
|
|
114
|
+
|
|
115
|
+
(defn bilabial-plosive? [event]
|
|
116
|
+
;; M shares the B_M_P viseme but should not get a hard burst release. Only
|
|
117
|
+
;; known P/B phonemes, or class metadata that says bilabial obstruent without
|
|
118
|
+
;; nasal, get the plosive closure plan.
|
|
119
|
+
(let [phoneme (normalized-phoneme event)
|
|
120
|
+
classes (event-classes event)]
|
|
121
|
+
(and (= (:visemeId event) (:B_M_P visemes/canonical-visemes))
|
|
122
|
+
(not (contains? classes "nasal"))
|
|
123
|
+
(or (contains? #{"P" "B"} phoneme)
|
|
124
|
+
(and (contains? classes "bilabial")
|
|
125
|
+
(contains? classes "obstruent"))))))
|
|
126
|
+
|
|
127
|
+
(defn labiodental-event? [event]
|
|
128
|
+
(or (= (:visemeId event) (:F_V visemes/canonical-visemes))
|
|
129
|
+
(has-class? event "labiodental")))
|
|
130
|
+
|
|
131
|
+
(defn envelope-profile [viseme-id]
|
|
132
|
+
;; The profile keeps closure sounds crisp, vowels rounder, and fricatives
|
|
133
|
+
;; lighter. Those differences matter more than raw intensity for readable
|
|
134
|
+
;; speech animation. The timings are deliberately visual timings, not acoustic
|
|
135
|
+
;; timings: anything below roughly one 60fps frame reads as snapping once the
|
|
136
|
+
;; snippet reaches Three.js, so non-closure sounds get ramps long enough to
|
|
137
|
+
;; interpolate across multiple rendered frames.
|
|
138
|
+
(cond
|
|
139
|
+
(= viseme-id (:W_OO visemes/canonical-visemes)) {:attackSec 0.034 :releaseSec 0.046 :peak 0.98}
|
|
140
|
+
(= viseme-id (:Oh visemes/canonical-visemes)) {:attackSec 0.036 :releaseSec 0.046 :peak 0.96}
|
|
141
|
+
(= viseme-id (:EE visemes/canonical-visemes)) {:attackSec 0.030 :releaseSec 0.038 :peak 0.94}
|
|
142
|
+
(= viseme-id (:Ih visemes/canonical-visemes)) {:attackSec 0.028 :releaseSec 0.036 :peak 0.88}
|
|
143
|
+
(= viseme-id (:F_V visemes/canonical-visemes)) {:attackSec 0.024 :releaseSec 0.034 :peak 0.86}
|
|
144
|
+
(= viseme-id (:Th visemes/canonical-visemes)) {:attackSec 0.024 :releaseSec 0.034 :peak 0.82}
|
|
145
|
+
(= viseme-id (:Ch_J visemes/canonical-visemes)) {:attackSec 0.026 :releaseSec 0.036 :peak 0.84}
|
|
146
|
+
(= viseme-id (:S_Z visemes/canonical-visemes)) {:attackSec 0.022 :releaseSec 0.032 :peak 0.78}
|
|
147
|
+
(= viseme-id (:K_G_H_NG visemes/canonical-visemes)) {:attackSec 0.022 :releaseSec 0.032 :peak 0.68}
|
|
148
|
+
(= viseme-id (:T_L_D_N visemes/canonical-visemes)) {:attackSec 0.026 :releaseSec 0.036 :peak 0.80}
|
|
149
|
+
:else
|
|
150
|
+
(case (viseme-class viseme-id)
|
|
151
|
+
:bilabial {:attackSec 0.004 :releaseSec 0.006 :peak 1.0}
|
|
152
|
+
:vowel {:attackSec 0.032 :releaseSec 0.042 :peak 0.92}
|
|
153
|
+
:fricative {:attackSec 0.024 :releaseSec 0.034 :peak 0.72}
|
|
154
|
+
:tongue {:attackSec 0.026 :releaseSec 0.036 :peak 0.76}
|
|
155
|
+
:liquid {:attackSec 0.030 :releaseSec 0.038 :peak 0.82}
|
|
156
|
+
:glide {:attackSec 0.028 :releaseSec 0.038 :peak 0.84}
|
|
157
|
+
{:attackSec 0.026 :releaseSec 0.034 :peak 0.86})))
|
|
158
|
+
|
|
159
|
+
(defn deduplicate-curve [curve]
|
|
160
|
+
(let [frames (vec curve)]
|
|
161
|
+
(if (<= (count frames) 1)
|
|
162
|
+
frames
|
|
163
|
+
(loop [remaining (rest frames)
|
|
164
|
+
result [(first frames)]]
|
|
165
|
+
(if (empty? remaining)
|
|
166
|
+
(vec result)
|
|
167
|
+
(let [curr (first remaining)
|
|
168
|
+
prev (last result)
|
|
169
|
+
next-frame (second remaining)
|
|
170
|
+
intensity-changed? (> (js/Math.abs (- (:intensity curr) (:intensity prev))) intensity-eps)
|
|
171
|
+
ends-plateau? (and next-frame
|
|
172
|
+
(> (js/Math.abs (- (:intensity next-frame) (:intensity curr))) intensity-eps))
|
|
173
|
+
keep? (or intensity-changed? ends-plateau? (= 1 (count remaining)))]
|
|
174
|
+
(recur (rest remaining) (if keep? (conj result curr) result))))))))
|
|
175
|
+
|
|
176
|
+
(defn scale-lip-intensity [value intensity]
|
|
177
|
+
(let [normalized (state/clamp 0 lip-dominant-cap value)
|
|
178
|
+
scale (max 0 (state/number-or intensity 1))]
|
|
179
|
+
(cond
|
|
180
|
+
(<= (js/Math.abs (- scale 1)) intensity-eps) normalized
|
|
181
|
+
(<= scale 1) (* normalized scale)
|
|
182
|
+
:else (- 1 (js/Math.pow (- 1 normalized) scale)))))
|
|
183
|
+
|
|
184
|
+
(defn scale-curve-intensity [curve intensity]
|
|
185
|
+
(mapv (fn [frame]
|
|
186
|
+
(assoc frame :intensity (scale-lip-intensity (:intensity frame) intensity)))
|
|
187
|
+
curve))
|
|
188
|
+
|
|
189
|
+
(defn build-viseme-curve [viseme-id start-ms duration-ms intensity]
|
|
190
|
+
(let [start-sec (/ start-ms 1000)
|
|
191
|
+
duration-sec (/ duration-ms 1000)
|
|
192
|
+
end-sec (+ start-sec duration-sec)
|
|
193
|
+
profile (envelope-profile viseme-id)
|
|
194
|
+
peak (:peak profile)]
|
|
195
|
+
(if (<= duration-sec 0)
|
|
196
|
+
[]
|
|
197
|
+
(let [attack-sec (min (:attackSec profile) (* duration-sec 0.45))
|
|
198
|
+
release-sec (min (:releaseSec profile) (* duration-sec 0.45))]
|
|
199
|
+
(scale-curve-intensity
|
|
200
|
+
(if (<= duration-sec (+ attack-sec release-sec 0.002))
|
|
201
|
+
[{:time start-sec :intensity 0}
|
|
202
|
+
{:time (+ start-sec (* duration-sec 0.5)) :intensity peak}
|
|
203
|
+
{:time end-sec :intensity 0}]
|
|
204
|
+
(let [ramp-up-end (+ start-sec attack-sec)
|
|
205
|
+
hold-end (- end-sec release-sec)]
|
|
206
|
+
(if (= (viseme-class viseme-id) :bilabial)
|
|
207
|
+
[{:time start-sec :intensity 0}
|
|
208
|
+
{:time ramp-up-end :intensity peak}
|
|
209
|
+
{:time hold-end :intensity peak}
|
|
210
|
+
{:time end-sec :intensity 0}]
|
|
211
|
+
[{:time start-sec :intensity 0}
|
|
212
|
+
{:time (+ start-sec (* attack-sec envelope-shoulder-ratio))
|
|
213
|
+
:intensity (* peak envelope-shoulder-intensity)}
|
|
214
|
+
{:time ramp-up-end :intensity peak}
|
|
215
|
+
{:time hold-end :intensity peak}
|
|
216
|
+
{:time (+ hold-end (* release-sec (- 1 envelope-shoulder-ratio)))
|
|
217
|
+
:intensity (* peak envelope-shoulder-intensity)}
|
|
218
|
+
{:time end-sec :intensity 0}])))
|
|
219
|
+
intensity)))))
|
|
220
|
+
|
|
221
|
+
(defn build-plosive-closure-curve [event next-event intensity]
|
|
222
|
+
;; P/B need a visible close-hold-release phrase, not just a generic envelope.
|
|
223
|
+
;; The close starts slightly early, holds the B_M_P target briefly, then drops
|
|
224
|
+
;; sharply at the following vowel boundary when one is present. Jaw motion is
|
|
225
|
+
;; still separate and comes only from the jaw planner.
|
|
226
|
+
(let [start-sec (/ (:offsetMs event) 1000)
|
|
227
|
+
duration-sec (/ (:durationMs event) 1000)
|
|
228
|
+
end-sec (+ start-sec duration-sec)
|
|
229
|
+
next-start-sec (when next-event (/ (:offsetMs next-event) 1000))
|
|
230
|
+
release-end-sec (if (and next-start-sec (vowel-event? next-event))
|
|
231
|
+
(max start-sec next-start-sec)
|
|
232
|
+
end-sec)
|
|
233
|
+
close-start-sec (max 0 (- start-sec plosive-preclose-sec))
|
|
234
|
+
close-end-sec (min (+ start-sec 0.010) release-end-sec)
|
|
235
|
+
hold-end-sec (max close-end-sec
|
|
236
|
+
(min (- release-end-sec 0.004)
|
|
237
|
+
(+ close-end-sec plosive-hold-sec)
|
|
238
|
+
(- end-sec plosive-release-sec)))
|
|
239
|
+
peak (:peak (envelope-profile (:visemeId event)))]
|
|
240
|
+
(scale-curve-intensity
|
|
241
|
+
(-> [{:time close-start-sec :intensity 0}
|
|
242
|
+
{:time close-end-sec :intensity peak}
|
|
243
|
+
{:time hold-end-sec :intensity peak}
|
|
244
|
+
{:time release-end-sec :intensity 0}]
|
|
245
|
+
deduplicate-curve
|
|
246
|
+
vec)
|
|
247
|
+
intensity)))
|
|
248
|
+
|
|
249
|
+
(defn build-event-viseme-curve [event next-event intensity]
|
|
250
|
+
(cond
|
|
251
|
+
(:neutral event) []
|
|
252
|
+
(bilabial-plosive? event) (build-plosive-closure-curve event next-event intensity)
|
|
253
|
+
:else (build-viseme-curve (:visemeId event)
|
|
254
|
+
(:offsetMs event)
|
|
255
|
+
(:durationMs event)
|
|
256
|
+
intensity)))
|
|
257
|
+
|
|
258
|
+
(defn merge-curves [existing incoming]
|
|
259
|
+
(->> (concat existing incoming)
|
|
260
|
+
(sort-by :time)
|
|
261
|
+
deduplicate-curve
|
|
262
|
+
vec))
|
|
263
|
+
|
|
264
|
+
(defn build-labiodental-au-curve [event peak intensity]
|
|
265
|
+
;; F/V already activate the F_V viseme. This small AU overlay gives the lower
|
|
266
|
+
;; lip a top-teeth contact cue on rigs that expose AU32/AU24, while rigs that
|
|
267
|
+
;; do not expose those AUs can ignore the channels safely.
|
|
268
|
+
(let [start-sec (/ (:offsetMs event) 1000)
|
|
269
|
+
duration-sec (/ (:durationMs event) 1000)
|
|
270
|
+
end-sec (+ start-sec duration-sec)
|
|
271
|
+
attack-sec (min 0.024 (* duration-sec 0.35))
|
|
272
|
+
release-sec (min 0.034 (* duration-sec 0.40))
|
|
273
|
+
hold-end-sec (max (+ start-sec attack-sec)
|
|
274
|
+
(- end-sec release-sec))]
|
|
275
|
+
(scale-curve-intensity
|
|
276
|
+
[{:time start-sec :intensity 0}
|
|
277
|
+
{:time (+ start-sec attack-sec) :intensity peak}
|
|
278
|
+
{:time hold-end-sec :intensity peak}
|
|
279
|
+
{:time end-sec :intensity 0}]
|
|
280
|
+
intensity)))
|
|
281
|
+
|
|
282
|
+
(defn build-labiodental-curves [events intensity]
|
|
283
|
+
(reduce (fn [curves event]
|
|
284
|
+
(if (labiodental-event? event)
|
|
285
|
+
(-> curves
|
|
286
|
+
(update labiodental-contact-au
|
|
287
|
+
#(merge-curves (or % [])
|
|
288
|
+
(build-labiodental-au-curve event
|
|
289
|
+
labiodental-contact-peak
|
|
290
|
+
intensity)))
|
|
291
|
+
(update labiodental-press-au
|
|
292
|
+
#(merge-curves (or % [])
|
|
293
|
+
(build-labiodental-au-curve event
|
|
294
|
+
labiodental-press-peak
|
|
295
|
+
intensity))))
|
|
296
|
+
curves))
|
|
297
|
+
{}
|
|
298
|
+
events))
|
|
299
|
+
|
|
300
|
+
(defn update-last-frame-time [curve f]
|
|
301
|
+
(let [frames (vec curve)
|
|
302
|
+
idx (dec (count frames))]
|
|
303
|
+
(if (neg? idx)
|
|
304
|
+
frames
|
|
305
|
+
(assoc-in frames [idx :time] (f (get-in frames [idx :time]))))))
|
|
306
|
+
|
|
307
|
+
(defn update-first-frame-time [curve f]
|
|
308
|
+
(let [frames (vec curve)]
|
|
309
|
+
(if (empty? frames)
|
|
310
|
+
frames
|
|
311
|
+
(assoc-in frames [0 :time] (f (get-in frames [0 :time]))))))
|
|
312
|
+
|
|
313
|
+
(defn apply-coarticulation [curves events]
|
|
314
|
+
;; Carry and anticipation are conservative. Bilabials keep hard closures;
|
|
315
|
+
;; vowels/liquids/glides get a small overlap so provider timelines do not
|
|
316
|
+
;; look like disconnected mouth poses.
|
|
317
|
+
(loop [index 0
|
|
318
|
+
result curves]
|
|
319
|
+
(if (>= index (dec (count events)))
|
|
320
|
+
result
|
|
321
|
+
(let [current (get events index)
|
|
322
|
+
next-event (get events (inc index))
|
|
323
|
+
current-class (viseme-class (:visemeId current))
|
|
324
|
+
next-class (viseme-class (:visemeId next-event))
|
|
325
|
+
current-end (+ (:offsetMs current) (:durationMs current))
|
|
326
|
+
next-start (:offsetMs next-event)
|
|
327
|
+
gap (- next-start current-end)
|
|
328
|
+
current-key (str (:visemeId current))
|
|
329
|
+
next-key (str (:visemeId next-event))
|
|
330
|
+
blend? (and (< gap 50)
|
|
331
|
+
(> gap -30)
|
|
332
|
+
(not= current-class :bilabial)
|
|
333
|
+
(not= next-class :bilabial))
|
|
334
|
+
carry? (contains? #{:vowel :liquid :glide} current-class)
|
|
335
|
+
anticipate? (contains? #{:vowel :liquid :glide} next-class)]
|
|
336
|
+
(recur (inc index)
|
|
337
|
+
(cond-> result
|
|
338
|
+
(and blend? carry? (get result current-key))
|
|
339
|
+
(update current-key update-last-frame-time
|
|
340
|
+
#(min (max % (/ next-start 1000))
|
|
341
|
+
(+ % (* 0.010 coarticulation-strength))))
|
|
342
|
+
|
|
343
|
+
(and blend? anticipate? (get result next-key))
|
|
344
|
+
(update next-key update-first-frame-time
|
|
345
|
+
#(max 0 (- % (* 0.016 coarticulation-strength))))))))))
|
|
346
|
+
|
|
347
|
+
(defn sample-curve-at [curve time]
|
|
348
|
+
;; The limiter below works on sampled poses instead of raw keyframes. That is
|
|
349
|
+
;; what keeps dense Azure timelines from briefly activating several full lip
|
|
350
|
+
;; shapes at once between keyframes.
|
|
351
|
+
(let [frames (vec curve)]
|
|
352
|
+
(cond
|
|
353
|
+
(empty? frames) 0
|
|
354
|
+
(<= time (:time (first frames))) (:intensity (first frames))
|
|
355
|
+
(>= time (:time (last frames))) (:intensity (last frames))
|
|
356
|
+
:else
|
|
357
|
+
(loop [index 0]
|
|
358
|
+
(if (>= index (dec (count frames)))
|
|
359
|
+
0
|
|
360
|
+
(let [a (get frames index)
|
|
361
|
+
b (get frames (inc index))]
|
|
362
|
+
(if (and (>= time (:time a)) (<= time (:time b)))
|
|
363
|
+
(let [span (max 0.000001 (- (:time b) (:time a)))
|
|
364
|
+
progress (/ (- time (:time a)) span)]
|
|
365
|
+
(+ (:intensity a) (* (- (:intensity b) (:intensity a)) progress)))
|
|
366
|
+
(recur (inc index)))))))))
|
|
367
|
+
|
|
368
|
+
(defn rounded-sample-time [time]
|
|
369
|
+
(/ (js/Math.round (* time 1000)) 1000))
|
|
370
|
+
|
|
371
|
+
(defn collect-lip-sample-times [curves]
|
|
372
|
+
(let [times (atom #{})]
|
|
373
|
+
(doseq [[key curve] curves
|
|
374
|
+
:when (not= key jaw-au)
|
|
375
|
+
frame curve]
|
|
376
|
+
(when (and (finite-number? (:time frame)) (>= (:time frame) 0))
|
|
377
|
+
(swap! times conj (rounded-sample-time (:time frame)))))
|
|
378
|
+
(let [sorted (sort @times)]
|
|
379
|
+
;; Add midpoints across short spans so overlap is capped between envelope
|
|
380
|
+
;; keyframes, where the visual flapping usually appears.
|
|
381
|
+
(doseq [[start end] (map vector sorted (rest sorted))
|
|
382
|
+
:when (and (> end start) (<= (- end start) 0.12))]
|
|
383
|
+
(swap! times conj (rounded-sample-time (/ (+ start end) 2)))))
|
|
384
|
+
(->> @times sort vec)))
|
|
385
|
+
|
|
386
|
+
(defn fit-secondary-activation [active adjusted budget]
|
|
387
|
+
(let [secondary (vec (rest active))
|
|
388
|
+
secondary-sum (transduce (map :value) + 0 secondary)]
|
|
389
|
+
(if (or (empty? secondary) (<= secondary-sum budget))
|
|
390
|
+
adjusted
|
|
391
|
+
(let [scale (/ budget secondary-sum)]
|
|
392
|
+
(reduce (fn [result entry]
|
|
393
|
+
(assoc result (:key entry) (* (:value entry) scale)))
|
|
394
|
+
adjusted
|
|
395
|
+
secondary)))))
|
|
396
|
+
|
|
397
|
+
(defn trim-inactive-padding [curve]
|
|
398
|
+
(let [frames (vec curve)
|
|
399
|
+
first-active (first (keep-indexed (fn [idx frame]
|
|
400
|
+
(when (> (:intensity frame) intensity-eps) idx))
|
|
401
|
+
frames))]
|
|
402
|
+
(if (nil? first-active)
|
|
403
|
+
[]
|
|
404
|
+
(let [last-active (loop [idx (dec (count frames))]
|
|
405
|
+
(cond
|
|
406
|
+
(neg? idx) idx
|
|
407
|
+
(> (:intensity (get frames idx)) intensity-eps) idx
|
|
408
|
+
:else (recur (dec idx))))
|
|
409
|
+
start (max 0 (dec first-active))
|
|
410
|
+
end (min (dec (count frames)) (inc last-active))]
|
|
411
|
+
(subvec frames start (inc end))))))
|
|
412
|
+
|
|
413
|
+
(defn limit-concurrent-lip-activation [curves]
|
|
414
|
+
;; Stable Latticework intentionally does not let every overlapping viseme keep
|
|
415
|
+
;; its full value. The strongest active shape leads, secondary shapes get a
|
|
416
|
+
;; small budget, and closed-mouth bilabials suppress everything else.
|
|
417
|
+
(let [lip-keys (into [] (remove #(= % jaw-au)) (keys curves))]
|
|
418
|
+
(if (<= (count lip-keys) 1)
|
|
419
|
+
curves
|
|
420
|
+
(let [sample-times (collect-lip-sample-times curves)]
|
|
421
|
+
(if (empty? sample-times)
|
|
422
|
+
curves
|
|
423
|
+
(let [normalized (reduce (fn [result time]
|
|
424
|
+
(let [values (mapv (fn [key]
|
|
425
|
+
{:key key
|
|
426
|
+
:visemeId (js/parseInt key 10)
|
|
427
|
+
:value (state/clamp 0 lip-dominant-cap
|
|
428
|
+
(sample-curve-at (get curves key) time))})
|
|
429
|
+
lip-keys)
|
|
430
|
+
active (->> (into [] (filter #(> (:value %) intensity-eps)) values)
|
|
431
|
+
(sort-by :value >)
|
|
432
|
+
vec)
|
|
433
|
+
adjusted-a (into {} (map (fn [entry] [(:key entry) (:value entry)]) values))
|
|
434
|
+
adjusted-b (if (> (count active) 1)
|
|
435
|
+
(let [dominant (first active)]
|
|
436
|
+
(if (and (= (:visemeId dominant) (:B_M_P visemes/canonical-visemes))
|
|
437
|
+
(>= (:value dominant) closure-dominance-threshold))
|
|
438
|
+
(fit-secondary-activation active adjusted-a closure-secondary-cap)
|
|
439
|
+
(let [total (transduce (map :value) + 0 active)]
|
|
440
|
+
(if (> total lip-total-activation-cap)
|
|
441
|
+
(let [budget (max 0
|
|
442
|
+
(min (- lip-total-activation-cap (:value dominant))
|
|
443
|
+
(* (:value dominant) lip-secondary-ratio)
|
|
444
|
+
lip-secondary-cap))]
|
|
445
|
+
(fit-secondary-activation active adjusted-a budget))
|
|
446
|
+
adjusted-a))))
|
|
447
|
+
adjusted-a)]
|
|
448
|
+
(reduce (fn [acc key]
|
|
449
|
+
(update acc key conj {:time time
|
|
450
|
+
:intensity (get adjusted-b key 0)}))
|
|
451
|
+
result
|
|
452
|
+
lip-keys)))
|
|
453
|
+
(into {} (map (fn [key] [key []]) lip-keys))
|
|
454
|
+
sample-times)]
|
|
455
|
+
(into {} (map (fn [key]
|
|
456
|
+
[key (-> normalized
|
|
457
|
+
(get key)
|
|
458
|
+
deduplicate-curve
|
|
459
|
+
trim-inactive-padding
|
|
460
|
+
vec)])
|
|
461
|
+
lip-keys))))))))
|
|
462
|
+
|
|
463
|
+
(defn reduce-curve-keys [viseme-id curve]
|
|
464
|
+
(let [frames (vec curve)]
|
|
465
|
+
(if (<= (count frames) 3)
|
|
466
|
+
frames
|
|
467
|
+
(let [profile (envelope-profile viseme-id)
|
|
468
|
+
reduced (loop [index 1
|
|
469
|
+
result [(first frames)]]
|
|
470
|
+
(if (>= index (dec (count frames)))
|
|
471
|
+
result
|
|
472
|
+
(let [prev (last result)
|
|
473
|
+
curr (get frames index)
|
|
474
|
+
next-frame (get frames (inc index))
|
|
475
|
+
preserves-peak? (>= (:intensity curr) (- (:peak profile) 0.02))
|
|
476
|
+
preserves-closure? (and (= viseme-id (:B_M_P visemes/canonical-visemes))
|
|
477
|
+
(>= (:intensity curr) 0.98))
|
|
478
|
+
near-flat? (and (< (js/Math.abs (- (:intensity curr) (:intensity prev))) 0.015)
|
|
479
|
+
(< (js/Math.abs (- (:intensity next-frame) (:intensity curr))) 0.015))]
|
|
480
|
+
(recur (inc index)
|
|
481
|
+
(if (or (not near-flat?) preserves-peak? preserves-closure?)
|
|
482
|
+
(conj result curr)
|
|
483
|
+
result)))))]
|
|
484
|
+
(conj reduced (last frames))))))
|
|
485
|
+
|
|
486
|
+
(defn reduce-lip-keys [curves]
|
|
487
|
+
(into {}
|
|
488
|
+
(map (fn [[key curve]]
|
|
489
|
+
(let [ordered (->> curve (sort-by :time) deduplicate-curve vec)]
|
|
490
|
+
[key (if (= key jaw-au)
|
|
491
|
+
ordered
|
|
492
|
+
(reduce-curve-keys (js/parseInt key 10) ordered))])))
|
|
493
|
+
curves))
|
|
494
|
+
|
|
495
|
+
(defn build-jaw-curve [events jaw-scale]
|
|
496
|
+
(jaw/build-jaw-curve events jaw-scale))
|
|
497
|
+
|
|
498
|
+
(def snippet-counter (atom 0))
|
|
499
|
+
|
|
500
|
+
(defn next-snippet-name [prefix]
|
|
501
|
+
(str prefix "_" (.now js/Date) "_" (swap! snippet-counter inc)))
|
|
502
|
+
|
|
503
|
+
(defn text-snippet-name [text]
|
|
504
|
+
(let [words (->> (str/split (or text "") #"\s+")
|
|
505
|
+
(take 3)
|
|
506
|
+
(str/join "_")
|
|
507
|
+
str/lower-case
|
|
508
|
+
(#(str/replace % #"[^a-z_]" "")))]
|
|
509
|
+
(next-snippet-name (str "lipSync_" (if (pos? (count words)) words "timeline")))))
|
|
510
|
+
|
|
511
|
+
(defn build-curves [events config]
|
|
512
|
+
(let [raw-curves (reduce (fn [curves index]
|
|
513
|
+
(let [event (get events index)
|
|
514
|
+
next-event (get events (inc index))
|
|
515
|
+
key (str (:visemeId event))
|
|
516
|
+
curve (build-event-viseme-curve event
|
|
517
|
+
next-event
|
|
518
|
+
(:intensity config))]
|
|
519
|
+
(if (empty? curve)
|
|
520
|
+
curves
|
|
521
|
+
(update curves key #(if % (merge-curves % curve) curve)))))
|
|
522
|
+
{}
|
|
523
|
+
(range (count events)))
|
|
524
|
+
articulated (-> raw-curves
|
|
525
|
+
(apply-coarticulation events)
|
|
526
|
+
limit-concurrent-lip-activation
|
|
527
|
+
reduce-lip-keys)
|
|
528
|
+
jaw-curve (build-jaw-curve events (:jawScale config))
|
|
529
|
+
tongue-curves (tongue/build-tongue-curves events (:tongueScale config))
|
|
530
|
+
labiodental-curves (build-labiodental-curves events (:intensity config))
|
|
531
|
+
with-jaw (if (empty? jaw-curve)
|
|
532
|
+
articulated
|
|
533
|
+
(assoc articulated jaw-au jaw-curve))]
|
|
534
|
+
;; Articulator AU curves are added after lip limiting because they are
|
|
535
|
+
;; ordinary AU controls, not viseme slots competing for lip-shape budget.
|
|
536
|
+
(merge with-jaw labiodental-curves tongue-curves)))
|
|
537
|
+
|
|
538
|
+
(defn lipsync-channel-target [curve-key]
|
|
539
|
+
;; LipSync owns its animation namespace explicitly. Numeric keys 0-14 are
|
|
540
|
+
;; canonical viseme slots. The jaw curve is stored internally as "26" because
|
|
541
|
+
;; the planner is conceptually FACS/JALI jaw activation, but it emits to Embody
|
|
542
|
+
;; as a direct JAW bone channel. Emitting AU26 would also drive the CC4
|
|
543
|
+
;; Jaw_Open morph, which can visually cover the separate viseme mouth shapes.
|
|
544
|
+
;; Numeric keys above 14 that are not the jaw curve remain regular AUs for
|
|
545
|
+
;; tongue/labiodental overlays.
|
|
546
|
+
(let [key (str curve-key)
|
|
547
|
+
numeric-id (when (re-matches #"^\d+$" key)
|
|
548
|
+
(js/parseInt key 10))]
|
|
549
|
+
(cond
|
|
550
|
+
(= key jaw-au)
|
|
551
|
+
{:type "bone" :id "JAW" :channel "rz" :maxDegrees 30}
|
|
552
|
+
|
|
553
|
+
(and (some? numeric-id) (<= 0 numeric-id 14))
|
|
554
|
+
{:type "viseme" :id numeric-id}
|
|
555
|
+
|
|
556
|
+
(some? numeric-id)
|
|
557
|
+
{:type "au" :id numeric-id}
|
|
558
|
+
|
|
559
|
+
:else
|
|
560
|
+
{:type "morph" :id key})))
|
|
561
|
+
|
|
562
|
+
(defn curves->channels [curves]
|
|
563
|
+
(into []
|
|
564
|
+
(map (fn [[curve-key curve]]
|
|
565
|
+
{:target (lipsync-channel-target curve-key)
|
|
566
|
+
:keyframes curve}))
|
|
567
|
+
curves))
|
|
568
|
+
|
|
569
|
+
(defn max-snippet-time [events curves]
|
|
570
|
+
(let [event-max (transduce (map #(/ (+ (:offsetMs %) (:durationMs %)) 1000))
|
|
571
|
+
max
|
|
572
|
+
0
|
|
573
|
+
events)
|
|
574
|
+
curve-max (transduce (mapcat (fn [[_ curve]] (map :time curve)))
|
|
575
|
+
max
|
|
576
|
+
0
|
|
577
|
+
curves)]
|
|
578
|
+
(max event-max curve-max)))
|
|
579
|
+
|
|
580
|
+
(defn build-lipsync-snippet
|
|
581
|
+
([events config] (build-lipsync-snippet events config nil))
|
|
582
|
+
([events config name]
|
|
583
|
+
(let [config (state/sanitize-config config)
|
|
584
|
+
normalized-events (normalize-events events)
|
|
585
|
+
snippet-name (or name (next-snippet-name "lipSync"))
|
|
586
|
+
curves (build-curves normalized-events config)
|
|
587
|
+
max-time (max-snippet-time normalized-events curves)]
|
|
588
|
+
{:name snippet-name
|
|
589
|
+
:snippetPriority (:priority config)
|
|
590
|
+
;; Provider/text timing is already baked into the keyframe offsets. Keep
|
|
591
|
+
;; playback at 1.0 so speech rate is not applied twice.
|
|
592
|
+
:snippetPlaybackRate 1
|
|
593
|
+
:snippetIntensityScale 1
|
|
594
|
+
:snippetJawScale (:jawScale config)
|
|
595
|
+
:autoVisemeJaw false
|
|
596
|
+
:loop false
|
|
597
|
+
:maxTime max-time
|
|
598
|
+
:curves curves
|
|
599
|
+
:channels (curves->channels curves)
|
|
600
|
+
:metadata {:agency "lipSync"
|
|
601
|
+
:visemeCount (count normalized-events)
|
|
602
|
+
:tongueCurveCount (count (select-keys curves tongue/tongue-au-keys))}})))
|
|
603
|
+
|
|
604
|
+
(defn build-text-snippet [text events config]
|
|
605
|
+
(build-lipsync-snippet events config (text-snippet-name text)))
|