cozyclay 1.9.0 → 2.0.0

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 (286) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/bin/agent/agent-routes.mjs +74 -70
  3. package/bin/agent/agent-runner.mjs +10 -3
  4. package/bin/agent/motion-runtime.mjs +32 -260
  5. package/bin/agent/providers.mjs +46 -3
  6. package/bin/agent/session-store.mjs +41 -5
  7. package/bin/agent/studio-prompt.mjs +1 -1
  8. package/bin/agent/studio-tools.mjs +48 -11
  9. package/bin/cozyclay.mjs +26 -8
  10. package/bin/live/cli.mjs +22 -3
  11. package/bin/mcp-runtime.mjs +35 -20
  12. package/bin/telemetry-state.mjs +5 -2
  13. package/bin/update-check.mjs +15 -0
  14. package/dist/app/index.html +6 -6
  15. package/dist/assets/analytics-MVZOEW8O.js +1 -0
  16. package/dist/assets/app-C1sdVbg4.css +1 -0
  17. package/dist/assets/app-CA55tcI3.js +4876 -0
  18. package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
  19. package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
  20. package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
  21. package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
  22. package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
  23. package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
  24. package/dist/assets/workflow-DRdwoDBT.css +1 -0
  25. package/dist/assets/workflow-dNPGY9xz.js +179 -0
  26. package/dist/cozyclay-package.json +1 -1
  27. package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
  28. package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
  29. package/dist/fonts/README.md +13 -9
  30. package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  31. package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  32. package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  33. package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  34. package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  35. package/dist/index.html +7 -7
  36. package/dist/privacy/index.html +13 -8
  37. package/dist/sitemap.xml +6 -6
  38. package/dist/workflow/index.html +5 -5
  39. package/mcp/LIVE-PROTOCOL.md +1 -1
  40. package/mcp/live-hub.mjs +35 -93
  41. package/mcp/mesh-file.mjs +48 -0
  42. package/mcp/server.mjs +6 -77
  43. package/mcp/tool-handlers.mjs +224 -377
  44. package/package.json +2 -1
  45. package/src/App.jsx +1273 -9075
  46. package/src/analytics.js +393 -9
  47. package/src/app-context.js +211 -0
  48. package/src/app-stage.jsx +25 -24
  49. package/src/ardy/auto-fix-panel.css +108 -0
  50. package/src/ardy/collision-blockers.js +9 -3
  51. package/src/ardy/fix-collisions.js +10 -66
  52. package/src/ardy/ground.js +4 -4
  53. package/src/ardy/ik-drag.js +101 -0
  54. package/src/ardy/ik-key-json.js +43 -0
  55. package/src/ardy/ik.js +122 -76
  56. package/src/ardy/physics-panel.css +2 -0
  57. package/src/ardy/physics-panel.jsx +17 -5
  58. package/src/ardy/platform-fit-panel.css +64 -0
  59. package/src/ardy/platform-fit-panel.jsx +40 -0
  60. package/src/ardy/platform-fit.js +326 -0
  61. package/src/ardy/playback.js +166 -20
  62. package/src/ardy/range-pin.js +299 -0
  63. package/src/ardy/timeline-coordinates.js +26 -0
  64. package/src/ardy/timeline.css +1199 -0
  65. package/src/ardy/timeline.jsx +149 -41
  66. package/src/ardy/waypoints.js +2 -2
  67. package/src/asset-pane.css +462 -0
  68. package/src/asset-pane.jsx +461 -232
  69. package/src/command-bus.js +349 -0
  70. package/src/commands/ai.js +31 -0
  71. package/src/commands/cast.js +140 -0
  72. package/src/commands/elements/character.js +13 -0
  73. package/src/commands/elements/motion.js +14 -0
  74. package/src/commands/elements/object.js +22 -0
  75. package/src/commands/elements/scene.js +16 -0
  76. package/src/commands/elements/shot.js +9 -0
  77. package/src/commands/elements/stage.js +11 -0
  78. package/src/commands/elements.js +155 -0
  79. package/src/commands/export.js +20 -0
  80. package/src/commands/index.js +40 -0
  81. package/src/commands/motion.js +231 -0
  82. package/src/commands/objects.js +174 -0
  83. package/src/commands/project.js +53 -0
  84. package/src/commands/scene.js +81 -0
  85. package/src/commands/shared.js +19 -0
  86. package/src/commands/shot.js +103 -0
  87. package/src/commands/stage.js +17 -0
  88. package/src/commands/view.js +42 -0
  89. package/src/document-store.js +144 -0
  90. package/src/domains/cast.js +1007 -0
  91. package/src/domains/motion.js +3219 -0
  92. package/src/domains/objects.js +932 -0
  93. package/src/domains/scenes.js +817 -0
  94. package/src/domains/shots.js +404 -0
  95. package/src/domains/stage.js +93 -0
  96. package/src/dualview.jsx +113 -57
  97. package/src/facing-marks.js +3 -0
  98. package/src/first-success-guide.jsx +14 -14
  99. package/src/grid-view.js +18 -5
  100. package/src/hierarchy-model.js +24 -16
  101. package/src/hierarchy-panel.css +386 -0
  102. package/src/hierarchy-panel.jsx +150 -21
  103. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  104. package/src/i2v-motion-studio.jsx +180 -0
  105. package/src/ik-camera.js +3 -0
  106. package/src/main.jsx +6 -0
  107. package/src/motion/generation.js +105 -0
  108. package/src/motion-readiness-ui.jsx +4 -4
  109. package/src/motion-readiness.js +11 -0
  110. package/src/motion-trail.js +366 -9
  111. package/src/object-gizmo.jsx +7 -1
  112. package/src/otio.js +2 -1
  113. package/src/panels/CameraPanel.jsx +60 -0
  114. package/src/panels/CharacterTransformPanel.jsx +48 -0
  115. package/src/panels/EnvironmentPanel.jsx +40 -0
  116. package/src/panels/Foldout.jsx +32 -0
  117. package/src/panels/LightPanel.jsx +24 -0
  118. package/src/panels/ObjectTransformPanel.jsx +554 -0
  119. package/src/panels/PosePanel.jsx +97 -0
  120. package/src/panels/ProjectPanel.jsx +33 -0
  121. package/src/panels/PromptBlocksPanel.jsx +442 -0
  122. package/src/panels/PropsPanel.jsx +70 -0
  123. package/src/panels/ReferenceImageField.jsx +91 -0
  124. package/src/panels/RigControlPanel.jsx +78 -0
  125. package/src/panels/RigPanel.jsx +37 -0
  126. package/src/panels/SubjectBox.jsx +47 -0
  127. package/src/panels/SubjectsPanel.jsx +39 -0
  128. package/src/panels/VideoCapturePanel.jsx +180 -0
  129. package/src/panels/details.css +1171 -0
  130. package/src/panels/motion.css +197 -0
  131. package/src/panels/pose.css +39 -0
  132. package/src/planview.jsx +60 -31
  133. package/src/posestudio.jsx +59 -4
  134. package/src/project-browser.css +1073 -0
  135. package/src/project-browser.jsx +284 -123
  136. package/src/range-pin-object-transform.js +19 -0
  137. package/src/range-pin-panel.css +518 -0
  138. package/src/range-pin-panel.jsx +344 -0
  139. package/src/result-modal.jsx +5 -5
  140. package/src/room.jsx +79 -51
  141. package/src/scene-objects.js +37 -1
  142. package/src/scenes.js +5 -0
  143. package/src/semantic-edit.js +2 -0
  144. package/src/settings-menu.jsx +39 -105
  145. package/src/shell/BottomDock.jsx +450 -0
  146. package/src/shell/DetailsSlot.jsx +513 -0
  147. package/src/shell/LibrarySlot.jsx +86 -0
  148. package/src/shell/MenuBar.jsx +601 -0
  149. package/src/shell/OutlinerSlot.jsx +50 -0
  150. package/src/shell/PreferencesDialog.jsx +538 -0
  151. package/src/shell/PreferencesSlot.jsx +40 -0
  152. package/src/shell/StatusBar.jsx +125 -0
  153. package/src/shell/StudioShell.jsx +92 -0
  154. package/src/shell/TopBar.jsx +129 -0
  155. package/src/shell/ViewportToolbar.jsx +656 -0
  156. package/src/shell/agent-glass.css +296 -0
  157. package/src/shell/dock.css +93 -0
  158. package/src/shell/glass-regions.css +985 -0
  159. package/src/shell/glass.css +687 -0
  160. package/src/shell/log-store.js +52 -0
  161. package/src/shell/mode.css +9 -0
  162. package/src/shell/preferences.css +651 -0
  163. package/src/shell/shell.css +282 -0
  164. package/src/shell/studio-shell-context.js +19 -0
  165. package/src/shell/topbar.css +435 -0
  166. package/src/shell/viewport.css +451 -0
  167. package/src/store/authored-intent.js +22 -0
  168. package/src/store/runtime-adapters.js +92 -0
  169. package/src/store/scene-stage.js +21 -0
  170. package/src/store/use-document-store.js +14 -0
  171. package/src/studio-actions.js +210 -0
  172. package/src/studio-agent-commands.js +41 -271
  173. package/src/studio-agent-context.js +39 -10
  174. package/src/studio-agent-motion.js +122 -373
  175. package/src/studio-agent-protocol.js +67 -31
  176. package/src/studio-app-binding.js +379 -0
  177. package/src/studio-contact-sheet.js +73 -0
  178. package/src/studio-elements.js +58 -51
  179. package/src/styles/themes.css +200 -0
  180. package/src/styles/tokens.css +69 -0
  181. package/src/styles.css +460 -1742
  182. package/src/theme.js +44 -0
  183. package/src/timeline-extent.js +16 -0
  184. package/src/trail-key-conflicts.js +39 -0
  185. package/src/trail-pick.js +59 -0
  186. package/src/ui.jsx +8 -8
  187. package/src/use-case-question.jsx +66 -0
  188. package/src/workflow/AgentPanel.jsx +82 -69
  189. package/src/workflow/agent-client.js +12 -2
  190. package/src/workflow/agent-panel.css +34 -32
  191. package/src/workflow/cozy-scene-node.css +2 -0
  192. package/src/workflow/workflow.css +2 -0
  193. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  194. package/tools/ardy/bridge.mjs +164 -146
  195. package/tools/ardy/visual-qa.mjs +5 -5
  196. package/tools/bench/EXP3.md +61 -0
  197. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  198. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  199. package/tools/bench/cclay_bench_runner.py +42 -0
  200. package/tools/bench/cube-contact.mjs +162 -0
  201. package/tools/bench/exp3.mjs +152 -0
  202. package/tools/bench/extract-bench-lib.mjs +216 -0
  203. package/tools/bench/extract-bench.mjs +302 -0
  204. package/tools/bench/fal-generate.mjs +88 -0
  205. package/tools/bench/fit/README.md +189 -0
  206. package/tools/bench/fit/camera.mjs +59 -0
  207. package/tools/bench/fit/contact.mjs +419 -0
  208. package/tools/bench/fit/footlock.mjs +234 -0
  209. package/tools/bench/fit/motion.mjs +85 -0
  210. package/tools/bench/fit/pin.mjs +27 -0
  211. package/tools/bench/fit/remote.mjs +105 -0
  212. package/tools/bench/fit-bench.mjs +126 -0
  213. package/tools/bench/fit-sanity.mjs +45 -0
  214. package/tools/bench/metrics.mjs +313 -0
  215. package/tools/bench/obs/depth.mjs +258 -0
  216. package/tools/bench/obs/extrinsics.mjs +175 -0
  217. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  218. package/tools/bench/obs/ground.mjs +294 -0
  219. package/tools/bench/obs/heading.mjs +151 -0
  220. package/tools/bench/obs/ladder.mjs +555 -0
  221. package/tools/bench/obs/mannequin-betas.json +124 -0
  222. package/tools/bench/obs/remote.mjs +83 -0
  223. package/tools/bench/obs/rest_joints.py +52 -0
  224. package/tools/bench/obs/ybot-targets.mjs +49 -0
  225. package/tools/bench/obs-bench.mjs +520 -0
  226. package/tools/bench/score.mjs +343 -0
  227. package/tools/bench/summarize.mjs +86 -0
  228. package/tools/dev/pages/privacy.html +13 -8
  229. package/tools/gt-render/browser.mjs +159 -0
  230. package/tools/gt-render/camera-math.mjs +276 -0
  231. package/tools/gt-render/page.mjs +267 -0
  232. package/tools/gt-render/render.mjs +514 -0
  233. package/tools/gt-render/scene-box.mjs +31 -0
  234. package/tools/gt-render/take-transform.mjs +87 -0
  235. package/tools/morphgs/assets/gen_truth.py +39 -0
  236. package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
  237. package/tools/morphgs/assets/playback-check.mjs +30 -0
  238. package/tools/morphgs/demo-gate.sh +26 -0
  239. package/tools/morphgs/fbx2morphgs.mjs +68 -0
  240. package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
  241. package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
  242. package/tools/morphgs/setup-on-cluster.sh +126 -0
  243. package/tools/qa/css-rule-usage.mjs +283 -0
  244. package/tools/qa/studio-control-count.mjs +70 -32
  245. package/tools/run-tests.mjs +134 -12
  246. package/tools/track/DESIGN.md +513 -0
  247. package/tools/track/backfill-provenance.mjs +116 -0
  248. package/tools/track/budget.mjs +8 -0
  249. package/tools/track/check-rig.mjs +209 -0
  250. package/tools/track/diagnostics.schema.json +60 -0
  251. package/tools/track/export-rig.mjs +256 -0
  252. package/tools/track/fallback.mjs +7 -0
  253. package/tools/track/fk-parity-fixture.mjs +161 -0
  254. package/tools/track/gate.mjs +349 -0
  255. package/tools/track/masks.mjs +318 -0
  256. package/tools/track/metrics.mjs +201 -0
  257. package/tools/track/publish-obs.mjs +108 -0
  258. package/tools/track/py/check_env.py +61 -0
  259. package/tools/track/py/eval_lr.py +229 -0
  260. package/tools/track/py/lr_viterbi.py +294 -0
  261. package/tools/track/py/masks.py +405 -0
  262. package/tools/track/py/objective.py +382 -0
  263. package/tools/track/py/rig.py +508 -0
  264. package/tools/track/py/scene.py +326 -0
  265. package/tools/track/py/test_joint_indices.py +123 -0
  266. package/tools/track/py/test_lr_viterbi.py +195 -0
  267. package/tools/track/py/test_masks.py +229 -0
  268. package/tools/track/py/test_rig.py +288 -0
  269. package/tools/track/py/test_scene.py +354 -0
  270. package/tools/track/py/test_track.py +328 -0
  271. package/tools/track/py/track.py +411 -0
  272. package/tools/track/remote.mjs +140 -0
  273. package/tools/track/rig-dump.mjs +227 -0
  274. package/tools/track/run-box-tests.mjs +14 -0
  275. package/tools/track/run-box.mjs +168 -0
  276. package/tools/track/setup-box.sh +59 -0
  277. package/tools/track/study-2d.mjs +728 -0
  278. package/dist/assets/analytics-B1hnH66c.js +0 -1
  279. package/dist/assets/app-BWusbqgO.js +0 -4861
  280. package/dist/assets/app-qKDo4PBX.css +0 -1
  281. package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
  282. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  283. package/dist/assets/workflow-CVwzMhz2.js +0 -179
  284. package/dist/assets/workflow-DsudxKHF.css +0 -1
  285. package/src/fal-motion-studio.jsx +0 -180
  286. package/src/scene-history.js +0 -129
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
- import { frameFromClientX, groupKeyRuns, KEY_RUN_MIN, motionTrimRange, promptMoveStartFrame, shotBlockGeometry } from "./timeline-coordinates.js";
2
+ import { frameFromClientX, groupKeyRuns, KEY_RUN_MIN, motionTrimRange, promptMoveStartFrame, rangePinBandGeometry, shotBlockGeometry } from "./timeline-coordinates.js";
3
3
  import { motionSegmentSpeedForFrames } from "./motion-edit.js";
4
4
  import { createPlaybackClock } from "./playback-clock.js";
5
5
  import { promptResizeFrame } from "./timeline-resize.js";
@@ -8,6 +8,7 @@ import { buildRail, craneHeightAt } from "../camera-follow.js";
8
8
  import { pathMetrics } from "../object-path.js";
9
9
  import { flatTiming, timingIsFlat, envelopeDrag, insertCut, removeCut, CUT_MIN_GAP } from "../speed-envelope.js";
10
10
  import { checkShotAgainstPreset, presetById } from "../model-presets.js";
11
+ import "./timeline.css";
11
12
 
12
13
  /**
13
14
  * ARDY Viser-style animation timeline — the live motion workspace.
@@ -46,10 +47,10 @@ const TRACKS = [
46
47
  "Shots",
47
48
  ];
48
49
  const TRACK_LABELS_KO = {
49
- Prompts: ko("Prompts", "프롬프트"),
50
- "Full-Body": ko("Full-Body", "전신"),
51
- "2D Root": ko("2D Root", "2D 루트"),
52
- Shots: ko("Shots", "샷"),
50
+ Prompts: ko("Prompt", "프롬프트"),
51
+ "Full-Body": ko("Body", "바디"),
52
+ "2D Root": ko("Root path", "루트 경로"),
53
+ Shots: ko("Camera Cuts", "카메라 컷"),
53
54
  };
54
55
 
55
56
  /** IK keys live on the Full-Body lane: one marker per keyed frame, holding
@@ -71,19 +72,6 @@ const framePct = (f, count) => (count > 1 ? f / (count - 1) : 0);
71
72
  // displayed total duration, as it is in an actual frame sequence).
72
73
  const formatTimelineSeconds = (seconds) => `${Math.max(0, Number(seconds) || 0).toFixed(2)}s`;
73
74
 
74
- // Keep related timeline controls visually together without another stylesheet
75
- // dependency. The head is a single flex row, so each group reads like one
76
- // instrument instead of a string of unrelated buttons.
77
- const TL_HEAD_GROUP_STYLE = {
78
- display: "inline-flex",
79
- alignItems: "center",
80
- gap: 5,
81
- padding: "2px 4px",
82
- border: "1px solid var(--line)",
83
- borderRadius: 6,
84
- background: "rgba(0, 0, 0, .12)",
85
- };
86
-
87
75
  const CAMERA_BLOCK_DEFAULTS = {
88
76
  distance: 3,
89
77
  height: 1.6,
@@ -971,6 +959,10 @@ export default function Timeline({
971
959
  // Full-Body lane. Frames are already on the timeline's 24 fps clock.
972
960
  motion = null, // { frames, label } | null
973
961
  ikFrames = [], // sorted full-body key frames
962
+ rangePins = [], // active character's inclusive pin ranges
963
+ previewRanges = [], // amber preview spans shown under the Body track
964
+ selectedPinId = null,
965
+ pendingPinRange = null,
974
966
  footSnap = true, // feet stay planted while the body moves
975
967
  bodyContact = true, // body markers stay above the floor
976
968
  shots = [],
@@ -996,9 +988,9 @@ export default function Timeline({
996
988
  onPromptMove,
997
989
  onPromptRemove,
998
990
  onClearMotion,
999
- onIkToggle,
1000
991
  onIkKeyframeAdd,
1001
992
  onIkKeyframeRemove,
993
+ onPinSelect,
1002
994
  onFootSnapToggle,
1003
995
  onBodyContactToggle,
1004
996
  onCameraMoveSelect,
@@ -1064,7 +1056,7 @@ export default function Timeline({
1064
1056
  // The window key/interval handlers register once; the latest callbacks
1065
1057
  // are read through a ref so they never go stale mid-playback.
1066
1058
  const handlers = useRef({});
1067
- handlers.current = { onScrub, onAdvance, onStep, onPlayToggle, onWaypointToggle, onMarkerSelect, onMarkerRemove, onRootKeyframeAdd, onPromptAdd, onPromptSelect, onPromptChange, onPromptResize, onPromptMove, onPromptRemove, onIkToggle, onIkKeyframeAdd, onIkKeyframeRemove, onFootSnapToggle, onBodyContactToggle, onCameraMoveSelect, onCameraKeyframeAdd, onCameraKeyframeMove, onCameraKeyframeRemove, onCameraBlockSelect, onCameraBlockChange, onCameraPreview, onCameraRailDrawToggle, onCameraRailDelete, onObjectPathDrawToggle, onObjectPathChange, onObjectPathClear, onObjectTimingGestureStart, onObjectTimingGestureEnd, onShotSelect, onShotBoundaryMove, onShotRename, onShotRemove, onShotDuplicate, onShotCut, onShotSplit, onShotMove, onMotionTrim, onMotionTrimReset, onMotionCut, onMotionSpeedChange, onMotionSegmentRemove, onEditGestureStart };
1059
+ handlers.current = { onScrub, onAdvance, onStep, onPlayToggle, onWaypointToggle, onMarkerSelect, onMarkerRemove, onRootKeyframeAdd, onPromptAdd, onPromptSelect, onPromptChange, onPromptResize, onPromptMove, onPromptRemove, onIkKeyframeAdd, onIkKeyframeRemove, onPinSelect, onFootSnapToggle, onBodyContactToggle, onCameraMoveSelect, onCameraKeyframeAdd, onCameraKeyframeMove, onCameraKeyframeRemove, onCameraBlockSelect, onCameraBlockChange, onCameraPreview, onCameraRailDrawToggle, onCameraRailDelete, onObjectPathDrawToggle, onObjectPathChange, onObjectPathClear, onObjectTimingGestureStart, onObjectTimingGestureEnd, onShotSelect, onShotBoundaryMove, onShotRename, onShotRemove, onShotDuplicate, onShotCut, onShotSplit, onShotMove, onMotionTrim, onMotionTrimReset, onMotionCut, onMotionSpeedChange, onMotionSegmentRemove, onEditGestureStart };
1068
1060
 
1069
1061
  // Trackpad/wheel zoom over the FRAME ruler lane only. React registers
1070
1062
  // onWheel as passive, so a synthetic onWheel could never preventDefault —
@@ -1212,6 +1204,10 @@ export default function Timeline({
1212
1204
  // the raw list draws a wall of diamonds. Collapse consecutive frames into
1213
1205
  // runs first; only runs shorter than KEY_RUN_MIN stay diamonds.
1214
1206
  const ikRuns = useMemo(() => groupKeyRuns(ikFrames), [ikFrames]);
1207
+ const pinBands = useMemo(() => rangePins.map((pin) => ({
1208
+ pin,
1209
+ geometry: rangePinBandGeometry(pin, frameCount, displayFrameCount),
1210
+ })).filter((entry) => entry.geometry), [displayFrameCount, frameCount, rangePins]);
1215
1211
  const moveRef = useRef(null);
1216
1212
  const suppressPromptClickRef = useRef(false);
1217
1213
  const resizeRef = useRef(null);
@@ -1718,6 +1714,8 @@ export default function Timeline({
1718
1714
  // The motion-layer track tools (Prompts +, the Full-Body Cut, the trim and
1719
1715
  // retime grips) edit the take, so Scene and Camera never render them.
1720
1716
  const motionTools = workflowMode === "motion";
1717
+ const activeShotForCut = shots[activeShotIdx] ?? null;
1718
+ const canCutShot = Boolean(activeShotForCut && frame > activeShotForCut.startFrame && frame <= activeShotForCut.endFrame);
1721
1719
  // A trim or retime grip edits ONE segment: it belongs to the segment the
1722
1720
  // playhead selects. The trim preview clip carries the live gesture, so it
1723
1721
  // keeps its grips while the pointer is down.
@@ -1728,10 +1726,11 @@ export default function Timeline({
1728
1726
  };
1729
1727
 
1730
1728
  return (
1731
- <section className={"timeline" + (expanded ? "" : " collapsed") + (!shots.length ? " empty-shots" : "")} aria-label={ko("Animation timeline", "애니메이션 타임라인")}>
1729
+ <section className={"timeline v2-sequencer" + (expanded ? "" : " collapsed") + (!shots.length ? " empty-shots" : "")} data-testid="sequencer" aria-label={ko("Animation timeline", "애니메이션 타임라인")}>
1732
1730
  {expanded ? (
1733
1731
  <>
1734
1732
  <div className="tl-head">
1733
+ <strong className="tl-title">{ko("Sequencer", "시퀀서")}</strong>
1735
1734
  <div className="tl-transport" aria-label={ko("Playback transport", "재생 컨트롤")}>
1736
1735
  <button
1737
1736
  type="button"
@@ -1760,11 +1759,74 @@ export default function Timeline({
1760
1759
  >
1761
1760
  ›
1762
1761
  </button>
1763
- <span className="tl-readout" aria-live="polite">
1764
- <b>{frame}</b> / {Math.max(0, frameCount - 1)} · {formatTimelineSeconds(frame / Math.max(1, fps))} / {formatTimelineSeconds(frameCount / Math.max(1, fps))} · {fps} fps · {playbackSpeed.toFixed(2)}×
1762
+ <span className="tl-readout" data-testid="sequencer-frame-readout" aria-live="polite">
1763
+ <b>{String(Math.max(0, frame)).padStart(4, "0")}</b><span> / {String(Math.max(0, frameCount)).padStart(4, "0")} · {fps}fps</span>
1765
1764
  </span>
1766
1765
  </div>
1767
- <div className="tl-head-group tl-view-tools" role="group" aria-label={ko("Timeline view tools", "타임라인 보기 도구")} style={TL_HEAD_GROUP_STYLE}>
1766
+ <div className="tl-sequencer-actions" role="group" aria-label={ko("Sequencer actions", "시퀀서 작업")}>
1767
+ {motionTools ? (<>
1768
+ <button
1769
+ type="button"
1770
+ className="tl-seq-action cut"
1771
+ data-testid="sequencer-cut"
1772
+ disabled={!selectedMotionSegment || frame <= 0 || frame >= motion.frames}
1773
+ title={ko("Cut the Body clip at the playhead", "재생 헤드에서 바디 클립 자르기")}
1774
+ onClick={() => handlers.current.onMotionCut?.()}
1775
+ >
1776
+ {ko("Cut", "컷")}
1777
+ </button>
1778
+ <button
1779
+ type="button"
1780
+ className="tl-seq-action"
1781
+ data-testid="sequencer-add-block"
1782
+ title={ko("Add a prompt block at the playhead", "재생 헤드에 프롬프트 블록 추가")}
1783
+ onClick={() => handlers.current.onPromptAdd?.(frame)}
1784
+ >
1785
+ {ko("+ Block", "+ 블록")}
1786
+ </button>
1787
+ </>) : workflowMode === "pose" || ikMode ? (
1788
+ <button
1789
+ type="button"
1790
+ className="tl-seq-action cut"
1791
+ data-testid="sequencer-cut"
1792
+ disabled={!selectedMotionSegment || frame <= 0 || frame >= (motion?.frames ?? 0)}
1793
+ onClick={() => handlers.current.onMotionCut?.()}
1794
+ >
1795
+ {ko("Cut", "컷")}
1796
+ </button>
1797
+ ) : (<>
1798
+ <button
1799
+ type="button"
1800
+ className="tl-seq-action cut"
1801
+ data-testid="sequencer-cut"
1802
+ disabled={!canCutShot}
1803
+ title={ko("Split the selected Camera Cut at the playhead", "선택한 카메라 컷을 재생 헤드에서 나눕니다")}
1804
+ onClick={() => activeShotForCut && handlers.current.onShotSplit?.(activeShotForCut.id)}
1805
+ >
1806
+ {ko("Cut", "컷")}
1807
+ </button>
1808
+ <button
1809
+ type="button"
1810
+ className="tl-seq-action rail"
1811
+ data-testid="sequencer-draw-rail"
1812
+ title={ko("Draw or redraw the selected camera rail", "선택한 카메라 레일을 그리거나 다시 그립니다")}
1813
+ onClick={() => handlers.current.onCameraRailDrawToggle?.()}
1814
+ >
1815
+ {ko("Draw rail", "레일 그리기")}
1816
+ </button>
1817
+ <button
1818
+ type="button"
1819
+ className="tl-seq-action"
1820
+ data-testid="sequencer-add-track"
1821
+ disabled={shotCutDisabled}
1822
+ title={ko("Add a Camera Cut track block", "카메라 컷 트랙 블록 추가")}
1823
+ onClick={() => handlers.current.onShotCut?.()}
1824
+ >
1825
+ {ko("+ Track", "+ 트랙")}
1826
+ </button>
1827
+ </>) }
1828
+ </div>
1829
+ <div className="tl-head-group tl-view-tools" role="group" aria-label={ko("Timeline view tools", "타임라인 보기 도구")}>
1768
1830
  <button
1769
1831
  type="button"
1770
1832
  className={"tl-btn zoom" + (zoom !== ZOOM_DEFAULT ? " on" : "")}
@@ -1779,28 +1841,18 @@ export default function Timeline({
1779
1841
  className={"tl-btn wp" + (waypointMode ? " on" : "")}
1780
1842
  aria-pressed={waypointMode}
1781
1843
  aria-label={ko("Root path mode", "루트 경로 모드")}
1782
- title={ko("Enable or disable 2D Root path constraints (P)", "2D 루트 경로 제약 켜기/끄기 (P)")}
1844
+ title={ko("Enable or disable root path constraints (P)", "루트 경로 제약 켜기/끄기 (P)")}
1783
1845
  onClick={() => handlers.current.onWaypointToggle?.()}
1784
1846
  >
1785
1847
  {isKo ? `웨이포인트 ${waypointMode ? "켜짐" : "꺼짐"}` : `Waypoint ${waypointMode ? "on" : "off"}`}
1786
1848
  </button>
1787
1849
  </div>
1788
- <div className="tl-head-group tl-pose-tools" role="group" aria-label={ko("Pose correction tools", "포즈 보정 도구")} style={TL_HEAD_GROUP_STYLE}>
1850
+ <div className="tl-head-group tl-pose-tools" role="group" aria-label={ko("Pose correction tools", "포즈 보정 도구")}>
1789
1851
  {/* No rig, no pose editing: the group says why instead of
1790
1852
  offering a button that cannot do anything (R3). */}
1791
1853
  {ikDisabled && !ikMode ? (
1792
1854
  <span className="tl-pose-hint">{ko("Load a rig to edit poses", "리그를 로드하면 포즈를 편집할 수 있어요")}</span>
1793
1855
  ) : (<>
1794
- <button
1795
- type="button"
1796
- className={"tl-btn ik" + (ikMode ? " on" : "")}
1797
- aria-pressed={ikMode}
1798
- aria-label={ko("Inverse kinematics", "역운동학")}
1799
- title={ko("IK mode — drag a wrist / ankle handle; keys land on the Full-Body lane. With a motion loaded, keys correct it layer-style", "IK 모드 — 손목이나 발목 핸들을 드래그하세요. 키는 전신 레인에 찍히며, 모션을 불러온 뒤에는 레이어 방식으로 보정합니다")}
1800
- onClick={() => handlers.current.onIkToggle?.()}
1801
- >
1802
- {isKo ? `IK ${ikMode ? "켜짐" : "꺼짐"}` : `IK ${ikMode ? "on" : "off"}`}
1803
- </button>
1804
1856
  {/* Foot snap and Body contact only reinterpret an IK drag, so
1805
1857
  they live for exactly as long as IK does (R7). Two
1806
1858
  independent bits, two toggles. */}
@@ -1829,7 +1881,7 @@ export default function Timeline({
1829
1881
  </>)}
1830
1882
  </div>
1831
1883
  {(selectedMotionSegment || onClearMotion) && (
1832
- <div className="tl-head-group tl-motion-tools" role="group" aria-label={ko("Motion controls", "모션 컨트롤")} style={TL_HEAD_GROUP_STYLE}>
1884
+ <div className="tl-head-group tl-motion-tools" role="group" aria-label={ko("Motion controls", "모션 컨트롤")}>
1833
1885
  {selectedMotionSegment && (
1834
1886
  <label className="tl-motion-speed-editor">
1835
1887
  <span>{ko(`Segment ${motionSegments.indexOf(selectedMotionSegment) + 1} speed`, `구간 ${motionSegments.indexOf(selectedMotionSegment) + 1} 배율`)}</span>
@@ -1971,9 +2023,23 @@ export default function Timeline({
1971
2023
  onTimingGestureEnd={() => handlers.current.onObjectTimingGestureEnd?.()}
1972
2024
  />
1973
2025
  ) : TRACKS.map((name) => (
1974
- <div className={"tl-track" + (name === "Prompts" ? " prompts" : "") + (name === IK_LANE ? " ik" : "") + (name === SHOTS_LANE ? " shots" : "")} key={name}>
1975
- <span className="tl-track-label">
1976
- {TRACK_LABELS_KO[name]}
2026
+ <div className={"tl-track" + (name === "Prompts" ? " prompts" : "") + (name === IK_LANE ? " ik" : "") + (name === SHOTS_LANE ? " shots" : "")} data-track-id={name} key={name}>
2027
+ <span className="tl-track-label tl-track-tree">
2028
+ {name === IK_LANE ? (
2029
+ <span className="tl-track-label-stack">
2030
+ <span className="tl-track-subrow"><span className="tl-track-chip body" data-lane-label="Body">{ko("Body", "바디")}</span></span>
2031
+ <span className="tl-track-subrow indent"><span className="tl-track-chip pins" data-lane-label="Pins">{ko("Pins", "핀")}</span></span>
2032
+ <span className="tl-track-subrow indent"><span className="tl-track-chip ik" data-lane-label="IK keys">{ko("IK keys", "IK 키")}</span></span>
2033
+ </span>
2034
+ ) : name === SHOTS_LANE ? (
2035
+ <span className="tl-track-label-stack">
2036
+ <span className="tl-track-subrow"><span className="tl-track-chip camera" data-lane-label="Camera Cuts">{ko("Camera Cuts", "카메라 컷")}</span></span>
2037
+ <span className="tl-track-subrow indent"><span className="tl-track-chip rail" data-lane-label="Rail">{ko("Rail", "레일")}</span></span>
2038
+ <span className="tl-track-subrow indent"><span className="tl-track-chip crane" data-lane-label="Crane">{ko("Crane", "크레인")}</span></span>
2039
+ </span>
2040
+ ) : (
2041
+ <span className="tl-track-subrow"><span className={"tl-track-chip " + (name === "2D Root" ? "root" : "prompt")} data-lane-label={TRACK_LABELS_KO[name]}>{TRACK_LABELS_KO[name]}</span></span>
2042
+ )}
1977
2043
  {trackOwner && (name === "Prompts" || name === "2D Root") && <em className="tl-track-owner">{trackOwner}</em>}
1978
2044
  {name === "2D Root" && pathSpeed && (
1979
2045
  <em
@@ -2035,6 +2101,13 @@ export default function Timeline({
2035
2101
  {gridFrames.map((f) => (
2036
2102
  <i key={f} className="tl-grid" style={{ "--tl-f": framePct(f, displayFrameCount) }} aria-hidden="true" />
2037
2103
  ))}
2104
+ {name === IK_LANE && previewRanges.map((range, index) => {
2105
+ const startFrame = Number.isFinite(Number(range?.startFrame)) ? Number(range.startFrame) : Number(range?.start ?? 0);
2106
+ const endFrame = Number.isFinite(Number(range?.endFrame)) ? Number(range.endFrame) : Number(range?.end ?? startFrame);
2107
+ const start = Math.max(0, Math.min(displayFrameCount - 1, Math.round(startFrame)));
2108
+ const end = Math.max(start, Math.min(displayFrameCount - 1, Math.round(endFrame)));
2109
+ return <span key={`preview:${index}`} className="tl-preview-range" style={{ "--tl-f-start": framePct(start, displayFrameCount), "--tl-f-end": framePct(end, displayFrameCount) }} aria-label={ko(`Preview range ${start}–${end}`, `미리보기 범위 ${start}–${end}`)} />;
2110
+ })}
2038
2111
  {name === SHOTS_LANE && shots.length === 0 && (
2039
2112
  <div className="tl-shot-empty">
2040
2113
  <span>{ko("No shots yet — use + Add shot in the lane header to create one.", "아직 샷이 없습니다 — 레인 헤더의 + 샷 추가로 만들어 보세요.")}</span>
@@ -2232,7 +2305,42 @@ export default function Timeline({
2232
2305
  </div>
2233
2306
  );
2234
2307
  })}
2235
- {name === IK_LANE && displayMotionSegments.map((segment, index) => (
2308
+ {name === IK_LANE && pinBands.map(({ pin, geometry }) => {
2309
+ return (
2310
+ <button
2311
+ key={`pin:${pin.id}`}
2312
+ type="button"
2313
+ className={`tl-pin-band${selectedPinId === pin.id ? " selected" : ""}`}
2314
+ style={{
2315
+ "--tl-f-start": geometry.startPct,
2316
+ "--tl-f-end": geometry.endPct,
2317
+ "--tl-pin-ramp-start": geometry.rampStartPct,
2318
+ "--tl-pin-ramp-end": geometry.rampEndPct,
2319
+ "--tl-pin-color": "var(--select, #e8a33d)",
2320
+ }}
2321
+ title={ko(`${pin.track} pin · frames ${pin.startFrame}–${pin.endFrame}`, `${pin.track} 고정 · ${pin.startFrame}–${pin.endFrame}프레임`)}
2322
+ onPointerDown={(event) => {
2323
+ if (event.button !== 0) return;
2324
+ event.stopPropagation();
2325
+ handlers.current.onPinSelect?.(pin.id);
2326
+ }}
2327
+ >
2328
+ <i className="tl-pin-band-ramp start" aria-hidden="true" />
2329
+ <i className="tl-pin-band-core" aria-hidden="true" />
2330
+ <i className="tl-pin-band-ramp end" aria-hidden="true" />
2331
+ </button>
2332
+ );
2333
+ })}
2334
+ {name === IK_LANE && pendingPinRange && (() => {
2335
+ const geometry = rangePinBandGeometry(pendingPinRange, frameCount, displayFrameCount);
2336
+ if (!geometry) return null;
2337
+ return <span
2338
+ className="tl-pin-pending"
2339
+ style={{ "--tl-f-start": geometry.startPct, "--tl-f-end": geometry.endPct }}
2340
+ aria-label={ko(`Pending pin range ${geometry.startFrame}–${geometry.endFrame}`, `대기 중인 고정 범위 ${geometry.startFrame}–${geometry.endFrame}`)}
2341
+ />;
2342
+ })()}
2343
+ {name === IK_LANE && displayMotionSegments.map((segment, index) => (
2236
2344
  <div
2237
2345
  key={segment.id}
2238
2346
  className={"tl-motion-clip" + (trimPreview ? " trimming" : "") + (segment.previewSpeed !== undefined ? " retiming" : "") + (selectedMotionSegment?.id === segment.id ? " selected" : "")}
@@ -344,8 +344,8 @@ export function judgeAuthoredPath(rootPath, fps, clipFrames, { chained = false }
344
344
  if (!chained && clipFrames / fps > PATH_LIMITS.clipMaxS) {
345
345
  errors.push(
346
346
  isKo
347
- ? `이동 경로는 전체 클립을 한 번의 모델 호출로 만들어요. ARDY가 학습한 길이는 ${PATH_LIMITS.clipMaxS}초까지예요 — 클립을 ${PATH_LIMITS.clipMaxS}초로 줄이거나 프롬프트를 블록으로 나눠 이어 만들기를 사용해 주세요`
348
- : `a root path generates the whole clip in one model call, and ARDY's trained window is ${PATH_LIMITS.clipMaxS} s — shorten the clip to ${PATH_LIMITS.clipMaxS} s, or split the prompt into blocks so the path rides a chained rollout`,
347
+ ? `이동 경로는 전체 클립을 한 번의 모델 호출로 만들어요. 모션 모델이 학습한 길이는 ${PATH_LIMITS.clipMaxS}초까지예요 — 클립을 ${PATH_LIMITS.clipMaxS}초로 줄이거나 프롬프트를 블록으로 나눠 이어 만들기를 사용해 주세요`
348
+ : `a root path generates the whole clip in one model call, and the motion model's trained window is ${PATH_LIMITS.clipMaxS} s — shorten the clip to ${PATH_LIMITS.clipMaxS} s, or split the prompt into blocks so the path rides a chained rollout`,
349
349
  );
350
350
  }
351
351
  const tail = (clipFrames - 1 - (ordered[ordered.length - 1]?.frame ?? 0)) / fps;