Support multiple regeneratable footage analyses

This commit is contained in:
Your Name 2026-10-03 01:24:45 -04:00
parent 5bcf22e458
commit 15deaea19e
20 changed files with 333 additions and 168 deletions

View file

@ -26,3 +26,34 @@
(vals (get-in clip [:symbols :take :nodes]))))))
"and the copy's instance follows its renamed symbol")
(is (empty? (clip/problems clip)))))
(defn- tracked-take [analysis-id]
{:analyses {analysis-id {:id analysis-id :detector "test" :version "1"}}
:subjects {:face-1 {:id :face-1 :analysis analysis-id
:source-subject :face-1 :params {}}}
:features {:face-1/mouth {:id :face-1/mouth :subject :face-1
:symbol :face-1 :area :mouth :nodes [:mouth] :params {}}}
:groups {}
:symbols {:main {:id :main :fps 30 :frames 10
:nodes {:face-1 {:id :face-1 :kind :instance :z "a1"
:source {:symbol :face-1}}}}
:face-1 {:id :face-1 :fps 30 :frames 10
:nodes {:head {:id :head :kind :group :z "a1"
:measured {[:xform :pos]
{:animated? false :value [0 0]}}}
:mouth {:id :mouth :kind :poly :z "a2"}}}}})
(deftest every-import-is-tracked-under-its-unique-symbol-scope
(let [first (bring/take (clip/blank) (tracked-take "analysis-a")
"close up" "footage-a" [0 10])
second (bring/take (:clip first) (tracked-take "analysis-b")
"close up" "footage-b" [0 10])
c (:clip second)]
(is (= #{:close-up.face-1 :close-up-2.face-1} (set (keys (:subjects c)))))
(is (= #{"analysis-a" "analysis-b"} (set (keys (:analyses c)))))
(is (= "analysis-a" (get-in c [:subjects :close-up.face-1 :analysis])))
(is (= "analysis-b" (get-in c [:subjects :close-up-2.face-1 :analysis])))
(is (= :face-1 (get-in c [:subjects :close-up-2.face-1 :source-subject])))
(is (= #{:close-up.face-1/mouth :close-up-2.face-1/mouth}
(set (keys (:features c)))))
(is (empty? (clip/problems c)))))

View file

@ -90,8 +90,17 @@
(is (or (zero? f) (< (clip/shown-frame doc :main (dec f)) n))))))))
(deftest imported-footage-uses-selection-for-picture-and-real-speed-for-audio
(let [{doc :clip sid :sid} (bring/take (clip/set-fps (clip/blank) 12)
footage "take" "video" [15 75])
(let [tracked (-> footage
(assoc :analyses {"a" {:id "a"}}
:subjects {:face-1 {:id :face-1 :analysis "a"
:source-subject :face-1}}
:features {} :groups {}
:symbols {:main {:id :main :fps 30 :frames 60
:nodes {:face-1 {:id :face-1 :kind :instance
:z "a" :source {:symbol :face-1}}}}
:face-1 (get-in footage [:symbols :main])}))
{doc :clip sid :sid} (bring/take (clip/set-fps (clip/blank) 12)
tracked "take" "video" [15 75])
;; A host authored at 12, with a 30fps source starting half a second in.
doc (-> doc (assoc-in [:symbols :main :fps] 12)
(clip/place-symbol store :main sid 6 :insert nil))
@ -99,21 +108,16 @@
draw (clip/resolver doc :main store pal/index-of nil)
[sound] (nest/audio-tracks doc :main)]
(is (= 60 (clip/frames doc sid)))
(is (= 1 (get-in doc [:symbols sid :nodes :sound :time :rate])))
(is (= 1 (get-in doc [:symbols :take.face-1 :nodes :sound :time :rate])))
(is (= [0 24] (:span n)))
(is (empty? (draw 5)))
(is (= 8 (:size (first (draw 9)))))
(is (= 7 (:frame (nest/inside doc store :main [:insert :mark] 9))))
(is (= 7 (:frame (nest/inside doc store :main [:insert :face-1 :mark] 9))))
(is (= [-4 -4 4 4] ((pick/bounds-of doc store :main n) 3)))
(is (= [6 30] (node/placed-span sound)))
(is (= 15 (node/local-frame sound 6)))
(is (= 1 (* (get-in sound [:time :rate]) (/ (:fps doc) (:fps sound)))))
(is (= [6 30] (:span (first (timeline/rows doc :main #{[:insert]})))))
(testing "an off-grid closed mouth can win without modifying the dense source"
(let [pinned (pose/put-cut doc :main :insert :mouth 7 6)
r (clip/resolver pinned :main store pal/index-of nil)]
(is (= 7 (:size (first (r 9)))))
(is (= (get-in doc [:symbols sid]) (get-in pinned [:symbols sid])))))
(testing "a deliberate half speed still retimes audio"
(let [slow (assoc-in doc [:symbols :main :nodes :insert :playback :speed] 0.5)
[track] (nest/audio-tracks slow :main)]
@ -121,7 +125,9 @@
(deftest generated-faces-own-their-footage-sound
(let [face (fn [id] {:id id :fps 30 :frames 20 :nodes {}})
frozen {:fps 30 :subjects {:face-1 {} :face-2 {}}
frozen {:fps 30 :analyses {"a" {:id "a"}}
:subjects {:face-1 {:id :face-1 :analysis "a" :source-subject :face-1}
:face-2 {:id :face-2 :analysis "a" :source-subject :face-2}}
:symbols {:main {:id :main :fps 30 :frames 20
:nodes {:face-1 {:id :face-1 :kind :instance :z "a1"
:source {:symbol :face-1}}
@ -130,11 +136,11 @@
:face-1 (face :face-1)
:face-2 (face :face-2)}}
{doc :clip take :sid} (bring/take (clip/blank) frozen "take" "video" [3 13])
alone (clip/place-symbol doc nil :main :face-1 4 :placed-face nil)]
alone (clip/place-symbol doc nil :main :take.face-1 4 :placed-face nil)]
(is (nil? (get-in doc [:symbols take :nodes :sound]))
"the wrapper does not own a sound the face would lose")
(is (= {:footage "video"}
(get-in doc [:symbols :face-1 :nodes :sound :source])))
(get-in doc [:symbols :take.face-1 :nodes :sound :source])))
(is (= 1 (count (nest/audio-tracks doc take)))
"the same take recording is not mixed once per detected face")
(is (= 1 (count (nest/audio-tracks alone :main)))

View file

@ -45,7 +45,7 @@
(let [ls (leaf/leaves :c7 @take/clip)]
(is (contains? ls "clip/c7/timing"))
(is (contains? ls "clip/c7/stage"))
(is (contains? ls "clip/c7/source"))
(is (contains? ls "clip/c7/analyses"))
;; A TIMELINE ID IS A SEGMENT, which is what lets a symbol's nodes be
;; addressed by the same path shape as the clip's own. `main` is the root.
(is (contains? ls "clip/c7/symbol/main"))

View file

@ -2,6 +2,7 @@
(:require [cljs.test :refer [deftest is testing]]
[clojure.walk :as walk]
[arthur.demo.stage :as stage]
[arthur.domain.bring :as bring]
[arthur.domain.channel :as ch]
[arthur.domain.clip :as clip]
[arthur.domain.params :as params]
@ -29,13 +30,41 @@
(merge take/knobs
{:name "regen" :fps 30 :aspect 1 :stage [320 200]
:expose 1 :head :free
:analysis (get-in @initial [:clip :analysis])}
:analysis (-> @initial :clip :analyses vals first)}
change)
{:face-1 @inputs}))
(defn- channel [entry node path]
(get-in entry [:clip :symbols :face-1 :nodes node :channels path]))
(deftest a-second-import-regenerates-through-its-own-analysis
(let [built (fn [seed]
(let [input {:dense (synth/synth-dense frames {:seed seed})}
analysis (address/analysis
{:detector "synth" :version "mulberry32"
:seed seed :frames frames :fps 30 :aspect 1})]
[(take/build (merge take/knobs
{:name "take" :fps 30 :aspect 1 :stage [320 200]
:expose 1 :head :free :analysis analysis})
{:face-1 input})
input]))
[a input-a] (built 3)
[b input-b] (built 7)
first (bring/take (clip/blank) (:clip a) "shot" "footage-a" [0 frames])
second (bring/take (:clip first) (:clip b) "shot" "footage-b" [0 frames])
entry {:clip (:clip second) :store (merge (:store a) (:store b))
:source-inputs {:subjects {:shot.face-1 input-a
:shot-2.face-1 input-b}}}
fid :shot-2.face-1/mouth
changed (regenerate/change entry
{:scope :feature :id fid :knob :verts :value 10})
analysis-id (get-in changed [:clip :subjects :shot-2.face-1 :analysis])
generated (get-in changed [:clip :symbols :shot-2.face-1 :nodes :mouth
:channels [:geom :pts] :generated])]
(is (= (get-in b [:clip :subjects :face-1 :analysis]) analysis-id))
(is (= analysis-id (:analysis generated)))
(is (= 10 (get-in generated [:params :verts])))))
(deftest eye-rebuild-is-confined-to-the-edited-feature
(let [before @initial
after (regenerate/change before
@ -170,7 +199,7 @@
(defn- part-at [area overrides]
(let [p (merge take/knobs
{:fps 30 :aspect 1 :analysis (get-in @initial [:clip :analysis])}
{:fps 30 :aspect 1 :analysis (-> @initial :clip :analyses vals first)}
overrides)
inputs (assoc @inputs :interior @interior-track)]
(freeze/part :face-1 area p (take/measure-part area p inputs (take/anchor-base p inputs)))))
@ -212,8 +241,8 @@
;; document that cannot be previewed fails in the suite rather than as a slider
;; that silently does nothing in the browser.
(let [entry (staged)]
(is (some? (:analysis (:clip entry)))
"the composed stage keeps the analysis the edit needs")
(is (seq (:analyses (:clip entry)))
"the composed stage keeps the analyses its edits need")
(is (some? (:source-inputs entry)))
(testing "and the features still say which timeline they live in"
(is (every? #(= :face-1 (:symbol %))