@crievo/omni 0.0.0-stage → 0.1.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 (709) hide show
  1. package/.env.example +41 -0
  2. package/README.md +2 -2
  3. package/bin/omni.mjs +14 -0
  4. package/docs/onboarding/clone-voice.png +0 -0
  5. package/docs/onboarding/getting-started.png +0 -0
  6. package/docs/onboarding/new-video.png +0 -0
  7. package/docs/onboarding/providers.png +0 -0
  8. package/docs/onboarding/publish.png +0 -0
  9. package/docs/onboarding/references.png +0 -0
  10. package/docs/onboarding.md +212 -0
  11. package/omnivideo.config.example.json +437 -0
  12. package/package.json +40 -4
  13. package/reference/captions.md +156 -0
  14. package/reference/carousel-contract.md +91 -0
  15. package/reference/cloud.md +96 -0
  16. package/reference/learnings.md +792 -0
  17. package/reference/omnichat.md +305 -0
  18. package/reference/premiere-mcp.md +91 -0
  19. package/reference/publish.md +305 -0
  20. package/reference/scenes-contract.md +109 -0
  21. package/reference/studio.md +666 -0
  22. package/reference/voicestudio.md +81 -0
  23. package/reference/writing-for-voice.md +67 -0
  24. package/requirements.txt +3 -0
  25. package/studio/api/app.ts +87 -0
  26. package/studio/api/lib/errors.ts +33 -0
  27. package/studio/api/lib/files.ts +41 -0
  28. package/studio/api/lib/hono.ts +9 -0
  29. package/studio/api/lib/result.ts +14 -0
  30. package/studio/api/lib/validate.ts +31 -0
  31. package/studio/api/lib/video.ts +10 -0
  32. package/studio/api/middleware/json-charset.ts +11 -0
  33. package/studio/api/middleware/local-only.ts +32 -0
  34. package/studio/api/middleware/no-timeout.ts +10 -0
  35. package/studio/api/routes/admin.ts +18 -0
  36. package/studio/api/routes/agent-cascade.ts +100 -0
  37. package/studio/api/routes/agent-history.ts +23 -0
  38. package/studio/api/routes/agent.ts +162 -0
  39. package/studio/api/routes/analytics.ts +31 -0
  40. package/studio/api/routes/approval.ts +60 -0
  41. package/studio/api/routes/archive.ts +12 -0
  42. package/studio/api/routes/captions.ts +28 -0
  43. package/studio/api/routes/carousel.ts +38 -0
  44. package/studio/api/routes/clone.ts +27 -0
  45. package/studio/api/routes/create.ts +19 -0
  46. package/studio/api/routes/events.ts +11 -0
  47. package/studio/api/routes/export.ts +14 -0
  48. package/studio/api/routes/media.ts +30 -0
  49. package/studio/api/routes/omnichat.ts +113 -0
  50. package/studio/api/routes/onboarding.ts +14 -0
  51. package/studio/api/routes/publish.ts +40 -0
  52. package/studio/api/routes/queue.ts +14 -0
  53. package/studio/api/routes/references.ts +40 -0
  54. package/studio/api/routes/runner.ts +17 -0
  55. package/studio/api/routes/schedule.ts +47 -0
  56. package/studio/api/routes/services.ts +11 -0
  57. package/studio/api/routes/sessions.ts +27 -0
  58. package/studio/api/routes/settings.ts +102 -0
  59. package/studio/api/routes/sound.ts +45 -0
  60. package/studio/api/routes/thumbnail.ts +18 -0
  61. package/studio/api/routes/usage.ts +13 -0
  62. package/studio/api/routes/videos.ts +41 -0
  63. package/studio/api/routes/voices.ts +71 -0
  64. package/studio/api/services/admin.ts +113 -0
  65. package/studio/api/services/agent-cascade.ts +975 -0
  66. package/studio/api/services/agent-claude.ts +47 -0
  67. package/studio/api/services/agent-history.ts +103 -0
  68. package/studio/api/services/agent-persona.ts +77 -0
  69. package/studio/api/services/agent-tools.ts +2237 -0
  70. package/studio/api/services/agent-ui-tools.ts +259 -0
  71. package/studio/api/services/agent-wait.ts +32 -0
  72. package/studio/api/services/agent.ts +553 -0
  73. package/studio/api/services/analytics.ts +92 -0
  74. package/studio/api/services/approval.ts +134 -0
  75. package/studio/api/services/archive.ts +38 -0
  76. package/studio/api/services/captions.ts +268 -0
  77. package/studio/api/services/carousel.ts +206 -0
  78. package/studio/api/services/classify.ts +196 -0
  79. package/studio/api/services/clone-phrases.ts +75 -0
  80. package/studio/api/services/clone.ts +722 -0
  81. package/studio/api/services/create.ts +421 -0
  82. package/studio/api/services/events.ts +215 -0
  83. package/studio/api/services/media.ts +32 -0
  84. package/studio/api/services/omnichat.ts +624 -0
  85. package/studio/api/services/onboarding.ts +77 -0
  86. package/studio/api/services/publish.ts +287 -0
  87. package/studio/api/services/queue.ts +150 -0
  88. package/studio/api/services/references.ts +269 -0
  89. package/studio/api/services/root.ts +60 -0
  90. package/studio/api/services/services.ts +114 -0
  91. package/studio/api/services/sessions.ts +1007 -0
  92. package/studio/api/services/settings.ts +983 -0
  93. package/studio/api/services/sound.ts +311 -0
  94. package/studio/api/services/thumbnail.ts +133 -0
  95. package/studio/api/services/usage.ts +27 -0
  96. package/studio/api/services/videos.ts +418 -0
  97. package/studio/api/services/voice-pause.ts +98 -0
  98. package/studio/api/services/voices.ts +324 -0
  99. package/studio/runner/runner.ts +244 -0
  100. package/studio/shared/omni-actions.ts +68 -0
  101. package/studio/shared/omni-admin.ts +52 -0
  102. package/studio/shared/omni-farewell.ts +48 -0
  103. package/studio/shared/omni-hotword.ts +20 -0
  104. package/studio/shared/omni-screens.ts +228 -0
  105. package/studio/shared/schemas/admin.ts +15 -0
  106. package/studio/shared/schemas/agent-cascade.ts +58 -0
  107. package/studio/shared/schemas/agent-history.ts +73 -0
  108. package/studio/shared/schemas/agent.ts +50 -0
  109. package/studio/shared/schemas/analytics.ts +6 -0
  110. package/studio/shared/schemas/approval.ts +22 -0
  111. package/studio/shared/schemas/captions.ts +19 -0
  112. package/studio/shared/schemas/carousel.ts +26 -0
  113. package/studio/shared/schemas/clone.ts +40 -0
  114. package/studio/shared/schemas/common.ts +16 -0
  115. package/studio/shared/schemas/omnichat.ts +54 -0
  116. package/studio/shared/schemas/onboarding.ts +5 -0
  117. package/studio/shared/schemas/publish.ts +45 -0
  118. package/studio/shared/schemas/references.ts +20 -0
  119. package/studio/shared/schemas/settings.ts +82 -0
  120. package/studio/shared/schemas/sound.ts +22 -0
  121. package/studio/shared/schemas/videos.ts +38 -0
  122. package/studio/shared/schemas/voices.ts +23 -0
  123. package/studio/shared/types/api.ts +1290 -0
  124. package/studio/studio.ts +148 -0
  125. package/studio/tsconfig.json +15 -0
  126. package/studio/web/assets/logos/cartesia.svg +4 -0
  127. package/studio/web/assets/logos/elevenlabs.svg +12 -0
  128. package/studio/web/assets/logos/fish.svg +24 -0
  129. package/studio/web/assets/logos/omni-black.png +0 -0
  130. package/studio/web/assets/logos/omni-white.png +0 -0
  131. package/studio/web/assets/logos/voicestudio.svg +21 -0
  132. package/studio/web/components/AgentMark.tsx +7 -0
  133. package/studio/web/components/AgentWalking.tsx +11 -0
  134. package/studio/web/components/ApiKeyField.tsx +104 -0
  135. package/studio/web/components/AppShell.tsx +64 -0
  136. package/studio/web/components/ApprovalNotice.tsx +65 -0
  137. package/studio/web/components/AvatarGroupIcon.tsx +22 -0
  138. package/studio/web/components/AvatarNameField.tsx +86 -0
  139. package/studio/web/components/AvatarPicker.tsx +84 -0
  140. package/studio/web/components/BrandLogo.tsx +16 -0
  141. package/studio/web/components/CartesiaPlanSetting.tsx +90 -0
  142. package/studio/web/components/ClaudeWalking.tsx +32 -0
  143. package/studio/web/components/CloneVoiceDialog/GuidedScript.tsx +39 -0
  144. package/studio/web/components/CloneVoiceDialog/LanguageBar.tsx +37 -0
  145. package/studio/web/components/CloneVoiceDialog/LevelMeter.tsx +31 -0
  146. package/studio/web/components/CloneVoiceDialog/ProviderChoice.tsx +81 -0
  147. package/studio/web/components/CloneVoiceDialog/QualityPanel.tsx +62 -0
  148. package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +50 -0
  149. package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +79 -0
  150. package/studio/web/components/CloneVoiceDialog/RedoSummary.tsx +54 -0
  151. package/studio/web/components/CloneVoiceDialog/SaveStep.tsx +91 -0
  152. package/studio/web/components/CloneVoiceDialog/SourceBar.tsx +30 -0
  153. package/studio/web/components/CloneVoiceDialog/index.tsx +141 -0
  154. package/studio/web/components/CloneVoiceDialog/types.ts +28 -0
  155. package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +528 -0
  156. package/studio/web/components/ConnectionSignals.tsx +51 -0
  157. package/studio/web/components/EditVoiceDialog/EditVoiceFooter.tsx +72 -0
  158. package/studio/web/components/EditVoiceDialog/ProviderCards.tsx +81 -0
  159. package/studio/web/components/EditVoiceDialog/index.tsx +256 -0
  160. package/studio/web/components/ErrorBanner.tsx +115 -0
  161. package/studio/web/components/FinishedTask.tsx +41 -0
  162. package/studio/web/components/FishModelSetting.tsx +69 -0
  163. package/studio/web/components/FishPriceSetting.tsx +44 -0
  164. package/studio/web/components/HumanBanner.tsx +64 -0
  165. package/studio/web/components/Lightbox.tsx +41 -0
  166. package/studio/web/components/LogStream.tsx +42 -0
  167. package/studio/web/components/Markdown.tsx +21 -0
  168. package/studio/web/components/MissingDmsPanel/BatchSend.tsx +137 -0
  169. package/studio/web/components/MissingDmsPanel/CommentCard.tsx +168 -0
  170. package/studio/web/components/MissingDmsPanel/CommentFeed.tsx +45 -0
  171. package/studio/web/components/MissingDmsPanel/DmBubble.tsx +38 -0
  172. package/studio/web/components/MissingDmsPanel/ReplyComposer.tsx +63 -0
  173. package/studio/web/components/MissingDmsPanel/index.tsx +98 -0
  174. package/studio/web/components/NotFound.tsx +48 -0
  175. package/studio/web/components/Omni/OmniAvatarPicker.tsx +92 -0
  176. package/studio/web/components/Omni/OmniCards.tsx +202 -0
  177. package/studio/web/components/Omni/OmniGlow.tsx +183 -0
  178. package/studio/web/components/Omni/OmniHistory.tsx +119 -0
  179. package/studio/web/components/Omni/OmniHotword.tsx +40 -0
  180. package/studio/web/components/Omni/OmniMicSelect.tsx +92 -0
  181. package/studio/web/components/Omni/OmniVoiceSample.tsx +81 -0
  182. package/studio/web/components/Omni/OmniWidget.tsx +743 -0
  183. package/studio/web/components/Omni/PendingActions.tsx +200 -0
  184. package/studio/web/components/Omni/Transcript.tsx +127 -0
  185. package/studio/web/components/OmnichatAccountStatus.tsx +87 -0
  186. package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +150 -0
  187. package/studio/web/components/OmnichatRulePanel/RuleDryRun.tsx +15 -0
  188. package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +42 -0
  189. package/studio/web/components/OmnichatRulePanel/RuleSkeleton.tsx +18 -0
  190. package/studio/web/components/OmnichatRulePanel/RuleStatus.tsx +55 -0
  191. package/studio/web/components/OmnichatRulePanel/index.tsx +115 -0
  192. package/studio/web/components/OmnichatRulePanel/rule-steps.ts +67 -0
  193. package/studio/web/components/OmnichatRulePanel/steps/MessageStep.tsx +91 -0
  194. package/studio/web/components/OmnichatRulePanel/steps/RepliesStep.tsx +201 -0
  195. package/studio/web/components/OmnichatRulePanel/steps/TriggerStep.tsx +76 -0
  196. package/studio/web/components/OmnichatRulePanel/steps/types.ts +11 -0
  197. package/studio/web/components/Palette.tsx +17 -0
  198. package/studio/web/components/PlatformCard.tsx +109 -0
  199. package/studio/web/components/PlatformIcon.tsx +19 -0
  200. package/studio/web/components/ProviderCard.tsx +80 -0
  201. package/studio/web/components/ProviderDetail.tsx +367 -0
  202. package/studio/web/components/ProviderLogo.tsx +20 -0
  203. package/studio/web/components/QueuePanel.tsx +77 -0
  204. package/studio/web/components/ReferenceActions.tsx +89 -0
  205. package/studio/web/components/ReferencePicker.tsx +136 -0
  206. package/studio/web/components/ResultMessage.tsx +7 -0
  207. package/studio/web/components/RuleForm.tsx +138 -0
  208. package/studio/web/components/RuleLog.tsx +151 -0
  209. package/studio/web/components/RunningTask.tsx +23 -0
  210. package/studio/web/components/SampleList.tsx +55 -0
  211. package/studio/web/components/SessionPanel.tsx +352 -0
  212. package/studio/web/components/SettingRow.tsx +14 -0
  213. package/studio/web/components/SettingsCategorySkeleton.tsx +25 -0
  214. package/studio/web/components/SidePanel.tsx +77 -0
  215. package/studio/web/components/Sidebar.tsx +289 -0
  216. package/studio/web/components/StageStepper.tsx +87 -0
  217. package/studio/web/components/StudioOffline.tsx +80 -0
  218. package/studio/web/components/TaskName.tsx +17 -0
  219. package/studio/web/components/TopBanners.tsx +30 -0
  220. package/studio/web/components/VideoOmnichatPanel.tsx +287 -0
  221. package/studio/web/components/VoiceAvatar.tsx +33 -0
  222. package/studio/web/components/VoiceCarousel.tsx +196 -0
  223. package/studio/web/components/VoiceCategory.tsx +9 -0
  224. package/studio/web/components/VoiceModal.tsx +39 -0
  225. package/studio/web/components/VoiceProfileCard.tsx +75 -0
  226. package/studio/web/components/VoiceProfilePicker.tsx +107 -0
  227. package/studio/web/components/VoiceProfilesSettings.tsx +150 -0
  228. package/studio/web/components/VoiceProviderCard.tsx +118 -0
  229. package/studio/web/components/VoiceProvidersCard.tsx +26 -0
  230. package/studio/web/components/WaitList.tsx +21 -0
  231. package/studio/web/components/icons.tsx +247 -0
  232. package/studio/web/components/ui/accordion.tsx +47 -0
  233. package/studio/web/components/ui/badge.tsx +18 -0
  234. package/studio/web/components/ui/calendar.tsx +43 -0
  235. package/studio/web/components/ui/chart.tsx +229 -0
  236. package/studio/web/components/ui/checkbox.tsx +26 -0
  237. package/studio/web/components/ui/cx.ts +2 -0
  238. package/studio/web/components/ui/date-time-picker.tsx +84 -0
  239. package/studio/web/components/ui/panel.tsx +23 -0
  240. package/studio/web/components/ui/picker.tsx +58 -0
  241. package/studio/web/components/ui/popover.tsx +24 -0
  242. package/studio/web/components/ui/select.tsx +89 -0
  243. package/studio/web/components/ui/skeleton.tsx +10 -0
  244. package/studio/web/components/ui/slider.tsx +68 -0
  245. package/studio/web/components/ui/switch.tsx +13 -0
  246. package/studio/web/components/ui/tooltip.test.tsx +30 -0
  247. package/studio/web/components/ui/tooltip.tsx +59 -0
  248. package/studio/web/hooks/useAccounts.ts +27 -0
  249. package/studio/web/hooks/useAdmin.ts +12 -0
  250. package/studio/web/hooks/useAnalytics.ts +21 -0
  251. package/studio/web/hooks/useAvatarAudioReactive.ts +87 -0
  252. package/studio/web/hooks/useBrowserConnect.ts +48 -0
  253. package/studio/web/hooks/useClonePlan.ts +14 -0
  254. package/studio/web/hooks/useConnected.ts +7 -0
  255. package/studio/web/hooks/useLiveQuery.ts +29 -0
  256. package/studio/web/hooks/useMenuCollapsed.ts +32 -0
  257. package/studio/web/hooks/useMicDevices.ts +53 -0
  258. package/studio/web/hooks/useMissingDms.ts +211 -0
  259. package/studio/web/hooks/useMissingSetupSteps.ts +26 -0
  260. package/studio/web/hooks/useNow.ts +11 -0
  261. package/studio/web/hooks/useOmniAgent.ts +278 -0
  262. package/studio/web/hooks/useOmniClaude.ts +124 -0
  263. package/studio/web/hooks/useOmniDock.ts +142 -0
  264. package/studio/web/hooks/useOmniGreeting.ts +60 -0
  265. package/studio/web/hooks/useOmniHotword.ts +128 -0
  266. package/studio/web/hooks/useOmnichat.ts +26 -0
  267. package/studio/web/hooks/useOnboarding.ts +27 -0
  268. package/studio/web/hooks/usePost.ts +23 -0
  269. package/studio/web/hooks/useQueue.ts +10 -0
  270. package/studio/web/hooks/useReferences.ts +20 -0
  271. package/studio/web/hooks/useRuleEditor.ts +101 -0
  272. package/studio/web/hooks/useRunner.ts +24 -0
  273. package/studio/web/hooks/useSaveAction.ts +23 -0
  274. package/studio/web/hooks/useSchedule.ts +11 -0
  275. package/studio/web/hooks/useServices.ts +10 -0
  276. package/studio/web/hooks/useSessions.ts +15 -0
  277. package/studio/web/hooks/useSettings.ts +11 -0
  278. package/studio/web/hooks/useSettingsSection.ts +30 -0
  279. package/studio/web/hooks/useSidePanel.ts +23 -0
  280. package/studio/web/hooks/useSound.ts +30 -0
  281. package/studio/web/hooks/useUsage.ts +11 -0
  282. package/studio/web/hooks/useVideoVoice.ts +36 -0
  283. package/studio/web/hooks/useVideos.ts +21 -0
  284. package/studio/web/hooks/useVoiceProfilePreview.ts +52 -0
  285. package/studio/web/hooks/useVoiceProfiles.ts +7 -0
  286. package/studio/web/hooks/useVoiceRunning.ts +10 -0
  287. package/studio/web/hooks/useVoiceStudioClosed.ts +8 -0
  288. package/studio/web/index.html +19 -0
  289. package/studio/web/lib/agent.ts +7 -0
  290. package/studio/web/lib/alerts.ts +210 -0
  291. package/studio/web/lib/analytics-metrics.ts +351 -0
  292. package/studio/web/lib/api.ts +70 -0
  293. package/studio/web/lib/audio.ts +39 -0
  294. package/studio/web/lib/avatars.ts +41 -0
  295. package/studio/web/lib/calendar.ts +218 -0
  296. package/studio/web/lib/caption-position.ts +249 -0
  297. package/studio/web/lib/carousel.ts +24 -0
  298. package/studio/web/lib/chime.ts +42 -0
  299. package/studio/web/lib/clone-voice.ts +55 -0
  300. package/studio/web/lib/export.ts +11 -0
  301. package/studio/web/lib/files.ts +8 -0
  302. package/studio/web/lib/format.ts +93 -0
  303. package/studio/web/lib/media.ts +29 -0
  304. package/studio/web/lib/mic-device.ts +72 -0
  305. package/studio/web/lib/missing-dms.ts +70 -0
  306. package/studio/web/lib/omni-cards.ts +42 -0
  307. package/studio/web/lib/omni-cascade.ts +1067 -0
  308. package/studio/web/lib/omni-greeting-audio.ts +24 -0
  309. package/studio/web/lib/omni-hotword.ts +231 -0
  310. package/studio/web/lib/omni-panel.ts +67 -0
  311. package/studio/web/lib/omni-realtime.ts +811 -0
  312. package/studio/web/lib/omni-ui.ts +402 -0
  313. package/studio/web/lib/omni-wait.ts +174 -0
  314. package/studio/web/lib/omnichat-api.ts +44 -0
  315. package/studio/web/lib/orb.ts +270 -0
  316. package/studio/web/lib/pcm-recorder.ts +115 -0
  317. package/studio/web/lib/platforms.ts +17 -0
  318. package/studio/web/lib/providers.ts +27 -0
  319. package/studio/web/lib/publish-draft.ts +52 -0
  320. package/studio/web/lib/publish-settings.ts +81 -0
  321. package/studio/web/lib/query-client.ts +122 -0
  322. package/studio/web/lib/references.ts +13 -0
  323. package/studio/web/lib/services.ts +40 -0
  324. package/studio/web/lib/sessions.ts +103 -0
  325. package/studio/web/lib/settings-categories.ts +58 -0
  326. package/studio/web/lib/sound-audio.ts +33 -0
  327. package/studio/web/lib/sound-timeline.ts +291 -0
  328. package/studio/web/lib/sse.ts +86 -0
  329. package/studio/web/lib/stage-history.ts +141 -0
  330. package/studio/web/lib/stages.ts +344 -0
  331. package/studio/web/lib/studio-offline.ts +60 -0
  332. package/studio/web/lib/upload.ts +24 -0
  333. package/studio/web/lib/video-tabs.ts +118 -0
  334. package/studio/web/lib/voice-profile-draft.ts +34 -0
  335. package/studio/web/lib/voice-profiles.ts +3 -0
  336. package/studio/web/main.tsx +14 -0
  337. package/studio/web/pages/AdminPage/index.tsx +123 -0
  338. package/studio/web/pages/AgentPage/components/Measurements.tsx +102 -0
  339. package/studio/web/pages/AgentPage/index.tsx +28 -0
  340. package/studio/web/pages/AnalyticsPage/components/About.tsx +52 -0
  341. package/studio/web/pages/AnalyticsPage/components/ChannelCards.tsx +106 -0
  342. package/studio/web/pages/AnalyticsPage/components/ChannelView.tsx +137 -0
  343. package/studio/web/pages/AnalyticsPage/components/CollectionStatus.tsx +46 -0
  344. package/studio/web/pages/AnalyticsPage/components/Comparison.tsx +68 -0
  345. package/studio/web/pages/AnalyticsPage/components/Diagnosis.tsx +50 -0
  346. package/studio/web/pages/AnalyticsPage/components/Highlights.tsx +49 -0
  347. package/studio/web/pages/AnalyticsPage/components/Hint.tsx +11 -0
  348. package/studio/web/pages/AnalyticsPage/components/HookVsBody.tsx +100 -0
  349. package/studio/web/pages/AnalyticsPage/components/HourlyWave.tsx +133 -0
  350. package/studio/web/pages/AnalyticsPage/components/Insights.tsx +70 -0
  351. package/studio/web/pages/AnalyticsPage/components/MetricCard.tsx +46 -0
  352. package/studio/web/pages/AnalyticsPage/components/PlanSection.tsx +40 -0
  353. package/studio/web/pages/AnalyticsPage/components/ProgressionChart.tsx +155 -0
  354. package/studio/web/pages/AnalyticsPage/components/ReelBarsChart.tsx +154 -0
  355. package/studio/web/pages/AnalyticsPage/components/ReelThumb.tsx +19 -0
  356. package/studio/web/pages/AnalyticsPage/components/ReelView.tsx +290 -0
  357. package/studio/web/pages/AnalyticsPage/components/ReelsTable.tsx +269 -0
  358. package/studio/web/pages/AnalyticsPage/components/RequestWithFocus.tsx +38 -0
  359. package/studio/web/pages/AnalyticsPage/components/SegmentedControl.tsx +35 -0
  360. package/studio/web/pages/AnalyticsPage/components/VsMedian.tsx +31 -0
  361. package/studio/web/pages/AnalyticsPage/index.tsx +17 -0
  362. package/studio/web/pages/GettingStartedPage/index.tsx +174 -0
  363. package/studio/web/pages/NewVideoPage/components/DeliverablesPicker.tsx +62 -0
  364. package/studio/web/pages/NewVideoPage/components/FormatPicker.tsx +33 -0
  365. package/studio/web/pages/NewVideoPage/index.tsx +133 -0
  366. package/studio/web/pages/NotFoundPage/index.tsx +9 -0
  367. package/studio/web/pages/OmnichatPage/components/NewFromLink.tsx +59 -0
  368. package/studio/web/pages/OmnichatPage/components/RuleListItem.tsx +64 -0
  369. package/studio/web/pages/OmnichatPage/components/RulePreview.tsx +14 -0
  370. package/studio/web/pages/OmnichatPage/components/RuleView.tsx +88 -0
  371. package/studio/web/pages/OmnichatPage/index.tsx +153 -0
  372. package/studio/web/pages/QueuePage/index.tsx +22 -0
  373. package/studio/web/pages/ReferencePage/components/ReferenceTexts.tsx +131 -0
  374. package/studio/web/pages/ReferencePage/index.tsx +216 -0
  375. package/studio/web/pages/ReferencesPage/components/NewReferenceForm.tsx +150 -0
  376. package/studio/web/pages/ReferencesPage/components/ReferenceCard.tsx +42 -0
  377. package/studio/web/pages/ReferencesPage/components/ReferenceGridSkeleton.tsx +31 -0
  378. package/studio/web/pages/ReferencesPage/index.tsx +67 -0
  379. package/studio/web/pages/SchedulePage/components/CalendarSkeleton.tsx +18 -0
  380. package/studio/web/pages/SchedulePage/components/DayView.tsx +177 -0
  381. package/studio/web/pages/SchedulePage/components/HourPicker.tsx +103 -0
  382. package/studio/web/pages/SchedulePage/components/MonthDayCell.tsx +111 -0
  383. package/studio/web/pages/SchedulePage/components/MonthView.tsx +47 -0
  384. package/studio/web/pages/SchedulePage/components/PlatformIcons.tsx +12 -0
  385. package/studio/web/pages/SchedulePage/components/PostThumb.tsx +15 -0
  386. package/studio/web/pages/SchedulePage/components/WeekColumn.tsx +75 -0
  387. package/studio/web/pages/SchedulePage/components/WeekView.tsx +35 -0
  388. package/studio/web/pages/SchedulePage/index.tsx +285 -0
  389. package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +263 -0
  390. package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +20 -0
  391. package/studio/web/pages/SettingsPage/components/ModelPicker.tsx +49 -0
  392. package/studio/web/pages/SettingsPage/components/ModelsSettings.tsx +138 -0
  393. package/studio/web/pages/SettingsPage/components/PlatformsCategory.tsx +23 -0
  394. package/studio/web/pages/SettingsPage/components/PublishingCategory/AccountStatus.tsx +29 -0
  395. package/studio/web/pages/SettingsPage/components/PublishingCategory/AutoPublishPanel.tsx +90 -0
  396. package/studio/web/pages/SettingsPage/components/PublishingCategory/BrowserConnect.tsx +62 -0
  397. package/studio/web/pages/SettingsPage/components/PublishingCategory/CheckField.tsx +19 -0
  398. package/studio/web/pages/SettingsPage/components/PublishingCategory/InstagramPanel.tsx +113 -0
  399. package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +146 -0
  400. package/studio/web/pages/SettingsPage/components/PublishingCategory/SaveBar.tsx +33 -0
  401. package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +132 -0
  402. package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCard.tsx +47 -0
  403. package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCardGroup.tsx +19 -0
  404. package/studio/web/pages/SettingsPage/components/PublishingCategory/TextPanel.tsx +55 -0
  405. package/studio/web/pages/SettingsPage/components/PublishingCategory/TiktokPanel.tsx +123 -0
  406. package/studio/web/pages/SettingsPage/components/PublishingCategory/YoutubePanel.tsx +100 -0
  407. package/studio/web/pages/SettingsPage/components/PublishingCategory/badges.tsx +15 -0
  408. package/studio/web/pages/SettingsPage/components/PublishingCategory/index.tsx +40 -0
  409. package/studio/web/pages/SettingsPage/components/PublishingCategory/types.ts +10 -0
  410. package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +389 -0
  411. package/studio/web/pages/SettingsPage/components/VoiceProvidersCategory.tsx +97 -0
  412. package/studio/web/pages/SettingsPage/index.tsx +85 -0
  413. package/studio/web/pages/VideoPage/components/AudioPlayer.tsx +113 -0
  414. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/AxisField.tsx +50 -0
  415. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionList.tsx +56 -0
  416. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionListItem.tsx +92 -0
  417. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionMargins.tsx +57 -0
  418. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionStyleFields.tsx +70 -0
  419. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionTextForm.tsx +87 -0
  420. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/ColorField.tsx +37 -0
  421. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/MarginField.tsx +59 -0
  422. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/NumberField.tsx +57 -0
  423. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/PositionControls.tsx +140 -0
  424. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/captionEditor.ts +215 -0
  425. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/useCaptionPosition.ts +154 -0
  426. package/studio/web/pages/VideoPage/components/CarouselChangeRequest.tsx +72 -0
  427. package/studio/web/pages/VideoPage/components/CarouselCompositionStage.tsx +80 -0
  428. package/studio/web/pages/VideoPage/components/CarouselCreate.tsx +63 -0
  429. package/studio/web/pages/VideoPage/components/CarouselMissing.tsx +19 -0
  430. package/studio/web/pages/VideoPage/components/CarouselPublishStage.tsx +173 -0
  431. package/studio/web/pages/VideoPage/components/CarouselScriptStage.tsx +82 -0
  432. package/studio/web/pages/VideoPage/components/CarouselSlideCard.tsx +49 -0
  433. package/studio/web/pages/VideoPage/components/CarouselViewer.tsx +55 -0
  434. package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +53 -0
  435. package/studio/web/pages/VideoPage/components/CompositionStage.tsx +78 -0
  436. package/studio/web/pages/VideoPage/components/CoverPicker.tsx +32 -0
  437. package/studio/web/pages/VideoPage/components/EditorStage.tsx +53 -0
  438. package/studio/web/pages/VideoPage/components/LineTakes.tsx +160 -0
  439. package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +124 -0
  440. package/studio/web/pages/VideoPage/components/LineVoicePicker.tsx +37 -0
  441. package/studio/web/pages/VideoPage/components/MediaImage.tsx +25 -0
  442. package/studio/web/pages/VideoPage/components/MotionScriptStage.tsx +39 -0
  443. package/studio/web/pages/VideoPage/components/NarrationStage.tsx +70 -0
  444. package/studio/web/pages/VideoPage/components/PublishForm.tsx +387 -0
  445. package/studio/web/pages/VideoPage/components/PublishStage.tsx +76 -0
  446. package/studio/web/pages/VideoPage/components/PublishText.tsx +149 -0
  447. package/studio/web/pages/VideoPage/components/ScenesApprovalPanel.tsx +278 -0
  448. package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +72 -0
  449. package/studio/web/pages/VideoPage/components/ScriptApprovalPanel.tsx +189 -0
  450. package/studio/web/pages/VideoPage/components/ScriptStage.tsx +17 -0
  451. package/studio/web/pages/VideoPage/components/SequencePlayer.tsx +146 -0
  452. package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +117 -0
  453. package/studio/web/pages/VideoPage/components/SoundTimeline/AreaSelection.tsx +74 -0
  454. package/studio/web/pages/VideoPage/components/SoundTimeline/BedPanel.tsx +94 -0
  455. package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +45 -0
  456. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionOverlay.tsx +44 -0
  457. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +32 -0
  458. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionsPanel.tsx +22 -0
  459. package/studio/web/pages/VideoPage/components/SoundTimeline/ClipBlock.tsx +111 -0
  460. package/studio/web/pages/VideoPage/components/SoundTimeline/ClipPanel.tsx +76 -0
  461. package/studio/web/pages/VideoPage/components/SoundTimeline/CutsPanel.tsx +44 -0
  462. package/studio/web/pages/VideoPage/components/SoundTimeline/EditorTabBar.tsx +73 -0
  463. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +46 -0
  464. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectItem.tsx +89 -0
  465. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectPanel.tsx +232 -0
  466. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectsRow.tsx +21 -0
  467. package/studio/web/pages/VideoPage/components/SoundTimeline/Lane.tsx +39 -0
  468. package/studio/web/pages/VideoPage/components/SoundTimeline/MarkerRow.tsx +40 -0
  469. package/studio/web/pages/VideoPage/components/SoundTimeline/MuteButton.tsx +36 -0
  470. package/studio/web/pages/VideoPage/components/SoundTimeline/NumberInput.tsx +37 -0
  471. package/studio/web/pages/VideoPage/components/SoundTimeline/PlanWarnings.tsx +19 -0
  472. package/studio/web/pages/VideoPage/components/SoundTimeline/PlayerResizeHandle.tsx +50 -0
  473. package/studio/web/pages/VideoPage/components/SoundTimeline/PreviewControls.tsx +51 -0
  474. package/studio/web/pages/VideoPage/components/SoundTimeline/SceneRow.tsx +54 -0
  475. package/studio/web/pages/VideoPage/components/SoundTimeline/SettingsPanel.tsx +65 -0
  476. package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +99 -0
  477. package/studio/web/pages/VideoPage/components/SoundTimeline/SoundPreview.tsx +82 -0
  478. package/studio/web/pages/VideoPage/components/SoundTimeline/SpeedPanel.tsx +128 -0
  479. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineDock.tsx +56 -0
  480. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +26 -0
  481. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineSkeleton.tsx +21 -0
  482. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineToolbar.tsx +160 -0
  483. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineTracks.tsx +121 -0
  484. package/studio/web/pages/VideoPage/components/SoundTimeline/TrackSidebar.tsx +30 -0
  485. package/studio/web/pages/VideoPage/components/SoundTimeline/VoiceRow.tsx +53 -0
  486. package/studio/web/pages/VideoPage/components/SoundTimeline/Waveform.tsx +97 -0
  487. package/studio/web/pages/VideoPage/components/SoundTimeline/clipActions.ts +50 -0
  488. package/studio/web/pages/VideoPage/components/SoundTimeline/effectActions.ts +161 -0
  489. package/studio/web/pages/VideoPage/components/SoundTimeline/index.tsx +161 -0
  490. package/studio/web/pages/VideoPage/components/SoundTimeline/timelineGeometry.ts +45 -0
  491. package/studio/web/pages/VideoPage/components/SoundTimeline/types.ts +43 -0
  492. package/studio/web/pages/VideoPage/components/SoundTimeline/useAutosave.ts +107 -0
  493. package/studio/web/pages/VideoPage/components/SoundTimeline/useEditorShortcuts.ts +168 -0
  494. package/studio/web/pages/VideoPage/components/SoundTimeline/useFitHeight.ts +40 -0
  495. package/studio/web/pages/VideoPage/components/SoundTimeline/useFollowPlayhead.ts +21 -0
  496. package/studio/web/pages/VideoPage/components/SoundTimeline/useListenMix.ts +43 -0
  497. package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayback.ts +231 -0
  498. package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayerWidth.ts +27 -0
  499. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundData.ts +124 -0
  500. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +511 -0
  501. package/studio/web/pages/VideoPage/components/StageDrawer/CompareVersions.tsx +111 -0
  502. package/studio/web/pages/VideoPage/components/StageDrawer/VersionCard.tsx +81 -0
  503. package/studio/web/pages/VideoPage/components/StageDrawer/VersionMedia.tsx +24 -0
  504. package/studio/web/pages/VideoPage/components/StageDrawer/VersionText.tsx +15 -0
  505. package/studio/web/pages/VideoPage/components/StageDrawer/index.tsx +109 -0
  506. package/studio/web/pages/VideoPage/components/StageProcess.tsx +126 -0
  507. package/studio/web/pages/VideoPage/components/StageResult.tsx +69 -0
  508. package/studio/web/pages/VideoPage/components/SwapReference.tsx +92 -0
  509. package/studio/web/pages/VideoPage/components/ThumbnailPanel.tsx +252 -0
  510. package/studio/web/pages/VideoPage/components/TimingPanel.tsx +89 -0
  511. package/studio/web/pages/VideoPage/components/VideoPlayer.tsx +18 -0
  512. package/studio/web/pages/VideoPage/components/VideoSkeleton.tsx +50 -0
  513. package/studio/web/pages/VideoPage/components/VideoVoiceInfo.tsx +33 -0
  514. package/studio/web/pages/VideoPage/components/VideoVoicePicker.tsx +165 -0
  515. package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +56 -0
  516. package/studio/web/pages/VideoPage/components/WithLogo.tsx +10 -0
  517. package/studio/web/pages/VideoPage/index.tsx +295 -0
  518. package/studio/web/pages/VideosPage/components/PublishedOn.tsx +85 -0
  519. package/studio/web/pages/VideosPage/components/ServicesPanel.tsx +81 -0
  520. package/studio/web/pages/VideosPage/components/SessionsPanel.tsx +62 -0
  521. package/studio/web/pages/VideosPage/components/VideoListSkeleton.tsx +25 -0
  522. package/studio/web/pages/VideosPage/components/VideoRow.tsx +122 -0
  523. package/studio/web/pages/VideosPage/index.tsx +134 -0
  524. package/studio/web/pages/VoicePage/index.tsx +28 -0
  525. package/studio/web/router.tsx +201 -0
  526. package/studio/web/styles/base/elements.css +55 -0
  527. package/studio/web/styles/base/utilities.css +25 -0
  528. package/studio/web/styles/components/badge.css +33 -0
  529. package/studio/web/styles/components/claude-running.css +27 -0
  530. package/studio/web/styles/components/claude-session.css +212 -0
  531. package/studio/web/styles/components/claude-walking.css +92 -0
  532. package/studio/web/styles/components/clone-voice-save.css +136 -0
  533. package/studio/web/styles/components/clone-voice.css +379 -0
  534. package/studio/web/styles/components/controls.css +70 -0
  535. package/studio/web/styles/components/data-and-image.css +54 -0
  536. package/studio/web/styles/components/forms.css +132 -0
  537. package/studio/web/styles/components/human-banner.css +99 -0
  538. package/studio/web/styles/components/lightbox.css +50 -0
  539. package/studio/web/styles/components/markdown.css +68 -0
  540. package/studio/web/styles/components/modal.css +70 -0
  541. package/studio/web/styles/components/omni-cards.css +105 -0
  542. package/studio/web/styles/components/omni.css +859 -0
  543. package/studio/web/styles/components/omnichat.css +538 -0
  544. package/studio/web/styles/components/panel.css +115 -0
  545. package/studio/web/styles/components/platform-card.css +140 -0
  546. package/studio/web/styles/components/provider-card.css +132 -0
  547. package/studio/web/styles/components/queue.css +185 -0
  548. package/studio/web/styles/components/reference-actions.css +133 -0
  549. package/studio/web/styles/components/reference-picker.css +146 -0
  550. package/studio/web/styles/components/results-drawer.css +224 -0
  551. package/studio/web/styles/components/segmented.css +115 -0
  552. package/studio/web/styles/components/side-panel-tab.css +23 -0
  553. package/studio/web/styles/components/status-signals.css +66 -0
  554. package/studio/web/styles/components/stepper.css +208 -0
  555. package/studio/web/styles/components/table.css +19 -0
  556. package/studio/web/styles/components/tabs.css +24 -0
  557. package/studio/web/styles/components/ui/accordion.css +71 -0
  558. package/studio/web/styles/components/ui/chart.css +162 -0
  559. package/studio/web/styles/components/ui/checkbox.css +37 -0
  560. package/studio/web/styles/components/ui/date-picker.css +153 -0
  561. package/studio/web/styles/components/ui/select.css +110 -0
  562. package/studio/web/styles/components/ui/skeleton.css +25 -0
  563. package/studio/web/styles/components/ui/slider.css +81 -0
  564. package/studio/web/styles/components/ui/switch.css +41 -0
  565. package/studio/web/styles/components/ui/tooltip.css +24 -0
  566. package/studio/web/styles/components/voice-carousel.css +255 -0
  567. package/studio/web/styles/components/voice-editor.css +179 -0
  568. package/studio/web/styles/components/voice-grid.css +56 -0
  569. package/studio/web/styles/components/voice-profiles.css +192 -0
  570. package/studio/web/styles/design-system.md +136 -0
  571. package/studio/web/styles/index.css +116 -0
  572. package/studio/web/styles/layout/page.css +41 -0
  573. package/studio/web/styles/layout/sidebar-groups.css +39 -0
  574. package/studio/web/styles/layout/sidebar.css +111 -0
  575. package/studio/web/styles/pages/agent.css +26 -0
  576. package/studio/web/styles/pages/analytics/dashboard.css +614 -0
  577. package/studio/web/styles/pages/analytics/insights.css +48 -0
  578. package/studio/web/styles/pages/getting-started.css +166 -0
  579. package/studio/web/styles/pages/new-video/form.css +32 -0
  580. package/studio/web/styles/pages/not-found.css +59 -0
  581. package/studio/web/styles/pages/references.css +204 -0
  582. package/studio/web/styles/pages/schedule/calendar.css +541 -0
  583. package/studio/web/styles/pages/schedule/chip-states.css +60 -0
  584. package/studio/web/styles/pages/settings/models.css +72 -0
  585. package/studio/web/styles/pages/settings/shell.css +361 -0
  586. package/studio/web/styles/pages/settings/usage.css +63 -0
  587. package/studio/web/styles/pages/studio-offline.css +105 -0
  588. package/studio/web/styles/pages/video/caption-controls.css +194 -0
  589. package/studio/web/styles/pages/video/captions-grid.css +70 -0
  590. package/studio/web/styles/pages/video/captions.css +79 -0
  591. package/studio/web/styles/pages/video/editor-chat.css +103 -0
  592. package/studio/web/styles/pages/video/export.css +50 -0
  593. package/studio/web/styles/pages/video/frames.css +29 -0
  594. package/studio/web/styles/pages/video/fullscreen-stage.css +400 -0
  595. package/studio/web/styles/pages/video/instagram-carousel.css +309 -0
  596. package/studio/web/styles/pages/video/layout.css +285 -0
  597. package/studio/web/styles/pages/video/narration.css +138 -0
  598. package/studio/web/styles/pages/video/preview-overview.css +11 -0
  599. package/studio/web/styles/pages/video/processing-notice.css +34 -0
  600. package/studio/web/styles/pages/video/publish-when.css +76 -0
  601. package/studio/web/styles/pages/video/publish.css +97 -0
  602. package/studio/web/styles/pages/video/result.css +96 -0
  603. package/studio/web/styles/pages/video/scenes.css +167 -0
  604. package/studio/web/styles/pages/video/sound-timeline.css +672 -0
  605. package/studio/web/styles/pages/video/sound-toolbar.css +149 -0
  606. package/studio/web/styles/pages/video/take-player.css +100 -0
  607. package/studio/web/styles/pages/video/thumbnail.css +157 -0
  608. package/studio/web/styles/pages/video/timeline.css +117 -0
  609. package/studio/web/styles/pages/video/voice-picker.css +14 -0
  610. package/studio/web/styles/pages/video-list/active-row.css +27 -0
  611. package/studio/web/styles/pages/video-list/card-link.css +49 -0
  612. package/studio/web/styles/pages/video-list/list.css +86 -0
  613. package/studio/web/styles/theme/identity/details.css +204 -0
  614. package/studio/web/styles/theme/identity/shell.css +249 -0
  615. package/studio/web/styles/theme/identity/surfaces.css +183 -0
  616. package/studio/web/styles/theme/identity/titles-and-buttons.css +120 -0
  617. package/studio/web/styles/theme/no-borders/blocks.css +208 -0
  618. package/studio/web/styles/theme/no-borders/controls.css +256 -0
  619. package/studio/web/styles/tokens.css +245 -0
  620. package/studio/web/types/api.ts +2 -0
  621. package/studio/web/types/assets.d.ts +9 -0
  622. package/templates/brief.md +27 -0
  623. package/templates/captions.html +204 -0
  624. package/templates/carousel-composition.md +47 -0
  625. package/templates/carousel-script.md +60 -0
  626. package/templates/components/voiceavatar/README.md +87 -0
  627. package/templates/components/voiceavatar/demo.html +50 -0
  628. package/templates/components/voiceavatar/voiceavatar.js +415 -0
  629. package/templates/motion.md +80 -0
  630. package/templates/playbook.md +43 -0
  631. package/templates/reference.md +123 -0
  632. package/templates/scene.html +112 -0
  633. package/templates/scenes.example.json +96 -0
  634. package/templates/script.md +63 -0
  635. package/templates/slide.html +71 -0
  636. package/templates/wireframe.html +91 -0
  637. package/tools/analysis.mjs +300 -0
  638. package/tools/approval.mjs +442 -0
  639. package/tools/assemble.mjs +942 -0
  640. package/tools/build-motion.mjs +437 -0
  641. package/tools/captions.mjs +246 -0
  642. package/tools/carousel.mjs +558 -0
  643. package/tools/compose.mjs +307 -0
  644. package/tools/cuts.mjs +74 -0
  645. package/tools/editor.mjs +419 -0
  646. package/tools/epidemic-capture.js +54 -0
  647. package/tools/fanout.mjs +69 -0
  648. package/tools/history.mjs +70 -0
  649. package/tools/history.py +61 -0
  650. package/tools/job-queue.mjs +180 -0
  651. package/tools/job_queue.py +430 -0
  652. package/tools/lib/agents.mjs +412 -0
  653. package/tools/lib/channel.mjs +399 -0
  654. package/tools/lib/claude-settings.mjs +67 -0
  655. package/tools/lib/cli.mjs +9 -0
  656. package/tools/lib/fanout.mjs +234 -0
  657. package/tools/lib/http.mjs +94 -0
  658. package/tools/lib/index.js +14 -0
  659. package/tools/lib/mcp-server.mjs +69 -0
  660. package/tools/lib/metrics/metrics.mjs +204 -0
  661. package/tools/lib/narration.mjs +15 -0
  662. package/tools/lib/oauth-client.mjs +295 -0
  663. package/tools/lib/omnichat/config.mjs +139 -0
  664. package/tools/lib/omnichat/errors.mjs +42 -0
  665. package/tools/lib/omnichat/extract.mjs +82 -0
  666. package/tools/lib/omnichat/gate.mjs +151 -0
  667. package/tools/lib/omnichat/manual.mjs +166 -0
  668. package/tools/lib/omnichat/match.mjs +78 -0
  669. package/tools/lib/omnichat/pass.mjs +393 -0
  670. package/tools/lib/omnichat/record.mjs +208 -0
  671. package/tools/lib/omnichat/text.mjs +315 -0
  672. package/tools/lib/omnichat/webhook.mjs +166 -0
  673. package/tools/lib/pairing.mjs +99 -0
  674. package/tools/lib/paths.mjs +41 -0
  675. package/tools/lib/platforms/instagram.mjs +469 -0
  676. package/tools/lib/platforms/tiktok.mjs +343 -0
  677. package/tools/lib/platforms/youtube.mjs +228 -0
  678. package/tools/lib/publish/publish.mjs +244 -0
  679. package/tools/lib/schedule/schedule.mjs +191 -0
  680. package/tools/lib/service.mjs +134 -0
  681. package/tools/lib/skill-client.mjs +126 -0
  682. package/tools/lib/skills-release.mjs +196 -0
  683. package/tools/lib/testing.mjs +52 -0
  684. package/tools/lib/up.mjs +187 -0
  685. package/tools/metrics.mjs +415 -0
  686. package/tools/models.mjs +134 -0
  687. package/tools/narrate.mjs +257 -0
  688. package/tools/omni.mjs +298 -0
  689. package/tools/omni_paths.py +12 -0
  690. package/tools/omnichat.mjs +1577 -0
  691. package/tools/omnivideo.py +1122 -0
  692. package/tools/package.mjs +130 -0
  693. package/tools/platforms.mjs +236 -0
  694. package/tools/premiere.mjs +533 -0
  695. package/tools/publish.mjs +1496 -0
  696. package/tools/reference.mjs +1408 -0
  697. package/tools/render.mjs +426 -0
  698. package/tools/runner.mjs +156 -0
  699. package/tools/setup.mjs +466 -0
  700. package/tools/skills-release.mjs +93 -0
  701. package/tools/sound.mjs +1323 -0
  702. package/tools/soundtrack.mjs +217 -0
  703. package/tools/thumbnail.mjs +271 -0
  704. package/tools/timing.mjs +108 -0
  705. package/tools/usage.py +628 -0
  706. package/tools/voice-envelope.mjs +185 -0
  707. package/tools/voice.py +448 -0
  708. package/tools/voice_sample.py +75 -0
  709. package/tools/wireframe.mjs +230 -0
@@ -0,0 +1,1408 @@
1
+ #!/usr/bin/env node
2
+ /* ============================================================================
3
+ Biblioteca de estilos visuais (antes "referências"): vídeos e fotos que dirigem o visual de um vídeo novo.
4
+
5
+ node tools/reference.mjs list
6
+ node tools/reference.mjs measure <video|url> <nome> [--out <pasta>]
7
+ node tools/reference.mjs excerpt <nome> <t0> <t1> [--fps 15]
8
+ node tools/reference.mjs frame <nome> <t> [<t> …]
9
+ node tools/reference.mjs color <nome> <t> <x>,<y> [<x>,<y> …]
10
+ node tools/reference.mjs curve <nome> <t0> <t1> --box <x>,<y>,<w>,<h> [--color #hex] [--background #hex]
11
+ node tools/reference.mjs compare <nome> <proof.html> <tRef>[:<tProva>] … [--out <png>]
12
+
13
+ Cada estilo visual é uma pasta em `references.dir` (config) com o vídeo, um
14
+ GUIDE.md escrito a partir das medições e as folhas que embasam o guia.
15
+
16
+ `measure` faz o que dá para medir sem olhar: cortes, duração dos planos,
17
+ quanto da tela muda a cada instante (onde a peça corre e onde segura),
18
+ paleta com área de cada cor, superfície (indício de desenho chapado ou de
19
+ imagem filmada) e, se houver som, volume e pulso. Grava
20
+ measurements.json, sheets/contacts.png e sheets/cuts.png, e um GUIDE.md a
21
+ partir de templates/reference.md se a pasta ainda não tem guia.
22
+
23
+ O resto (tipografia, layout, curvas) sai de ler as folhas com Read:
24
+ `excerpt` monta uma folha quadro a quadro de um intervalo, `frame` extrai
25
+ quadros em resolução cheia para medir corpo de fonte, e `color` lê o hex de
26
+ pontos de um quadro (coordenadas do quadro em resolução cheia).
27
+
28
+ Curva e duração também se medem: `curve` acompanha o que está numa caixa
29
+ do quadro e devolve, por canal (posição, tamanho, presença), início,
30
+ duração, o cubic-bezier ajustado, as curvas nomeadas mais perto (CSS e
31
+ GSAP), overshoot e steps(). `compare` renderiza uma cena de prova
32
+ (proof/proof.html, a partir de templates/scene.html) nos tempos pedidos e
33
+ monta referência | prova | diferença, com SSIM e ΔE: é o teste de que o
34
+ guia reproduz a peça, e não só a descreve.
35
+
36
+ Ritmo não se sente em quadro parado, mas se mede: é o que este script dá.
37
+ Requer ffmpeg/ffprobe; yt-dlp para baixar de link.
38
+ ============================================================================ */
39
+
40
+ import {
41
+ readFileSync,
42
+ writeFileSync,
43
+ existsSync,
44
+ mkdirSync,
45
+ readdirSync,
46
+ copyFileSync,
47
+ statSync,
48
+ rmSync,
49
+ } from "node:fs";
50
+ import { spawnSync } from "node:child_process";
51
+ import { join, resolve, dirname, basename, extname, relative } from "node:path";
52
+ import { homedir } from "node:os";
53
+ import { fileURLToPath } from "node:url";
54
+ import { CODE_ROOT, DATA_ROOT as ROOT } from "./lib/paths.mjs";
55
+
56
+ const CFG = JSON.parse(readFileSync(join(ROOT, "omnivideo.config.json"), "utf8"));
57
+ const LIBRARY = resolve(ROOT, CFG.references.dir.replace(/^~(?=\/|$)/, homedir()));
58
+ const VIDEO_EXT = /\.(mp4|mov|webm|mkv|m4v)$/i;
59
+
60
+ function fail(msg) {
61
+ console.error(`erro: ${msg}`);
62
+ process.exit(1);
63
+ }
64
+ function run(bin, args, { binary = false } = {}) {
65
+ const r = spawnSync(bin, args, { maxBuffer: 1 << 29, encoding: binary ? "buffer" : "utf8" });
66
+ if (r.error) fail(`${bin}: ${r.error.message}`);
67
+ if (r.status !== 0) fail(`${bin} saiu com ${r.status}: ${String(r.stderr).slice(-600)}`);
68
+ return r.stdout;
69
+ }
70
+
71
+ /* --- video ------------------------------------------------------------------- */
72
+ function probe(file) {
73
+ const j = JSON.parse(run("ffprobe", ["-v", "error", "-print_format", "json", "-show_streams", "-show_format", file]));
74
+ const v = j.streams.find(s => s.codec_type === "video");
75
+ if (!v) fail(`${file} não tem vídeo`);
76
+ const [n, d] = (v.avg_frame_rate !== "0/0" ? v.avg_frame_rate : v.r_frame_rate).split("/").map(Number);
77
+ return {
78
+ duration: +(+j.format.duration).toFixed(3),
79
+ w: v.width,
80
+ h: v.height,
81
+ fps: +(n / (d || 1)).toFixed(3),
82
+ audio: j.streams.some(s => s.codec_type === "audio"),
83
+ };
84
+ }
85
+
86
+ /* dir and video of a reference by name. A path (with "/") also works: a
87
+ loose dir, or the video itself (reels/, one video among several), and
88
+ then the sheets go to its dir. */
89
+ function openRef(name) {
90
+ if (!name.includes("/")) {
91
+ const dir = join(LIBRARY, name);
92
+ return { dir, file: videoInDir(dir) };
93
+ }
94
+ const p = resolve(name);
95
+ if (!existsSync(p)) fail(`${p} não existe`);
96
+ return statSync(p).isFile() ? { dir: dirname(p), file: p } : { dir: p, file: videoInDir(p) };
97
+ }
98
+
99
+ function videoInDir(dir) {
100
+ const m = existsSync(join(dir, "measurements.json"))
101
+ ? JSON.parse(readFileSync(join(dir, "measurements.json"), "utf8"))
102
+ : null;
103
+ if (m?.video && existsSync(join(dir, m.video))) return join(dir, m.video);
104
+ const v = readdirSync(dir)
105
+ .filter(f => VIDEO_EXT.test(f))
106
+ .sort()[0];
107
+ if (!v) fail(`${dir} não tem vídeo na raiz`);
108
+ return join(dir, v);
109
+ }
110
+
111
+ /* --- measurements ---------------------------------------------------------------- */
112
+
113
+ /* cuts: scene difference of each frame vs the previous one. A cut is the
114
+ strong jump (≥ 0.3) or the local peak: ≥ 0.06 and 5× the median of ±0.5 s
115
+ around it. The fixed threshold alone missed cuts between near-black frames
116
+ (the hero changes, but the whole image barely does): on dark-vibrant-elements
117
+ it found 0 of 10 cuts, the local peak finds 7; on pure-dark, 3 of 3. */
118
+ function cuts(file, duration) {
119
+ const out = run("ffmpeg", [
120
+ "-hide_banner",
121
+ "-nostats",
122
+ "-i",
123
+ file,
124
+ "-an",
125
+ "-vf",
126
+ "scale=320:-2,select='gte(scene,0)',metadata=print:file=-",
127
+ "-f",
128
+ "null",
129
+ "-",
130
+ ]);
131
+ const pts = [];
132
+ let t = null;
133
+ for (const line of out.split("\n")) {
134
+ const mt = /pts_time:([\d.]+)/.exec(line);
135
+ if (mt) t = +mt[1];
136
+ const ms = /lavfi\.scene_score=([\d.]+)/.exec(line);
137
+ if (ms && t != null) pts.push([t, +ms[1]]);
138
+ }
139
+ const win = Math.max(3, Math.round((pts.length / (duration || 1)) * 0.5));
140
+ const res = [];
141
+ pts.forEach(([t, s], i) => {
142
+ /* the first frames have no "previous shot", and a 0.1 s shot at the end is not a shot */
143
+ if (t < 0.2 || t > duration - 0.1 || s < 0.06) return;
144
+ const near = [...pts.slice(Math.max(0, i - win), Math.max(0, i - 1)), ...pts.slice(i + 2, i + win + 1)].map(
145
+ p => p[1],
146
+ );
147
+ if (s >= 0.3 || s >= 5 * Math.max(pct(near, 0.5), 0.004)) res.push({ t: +t.toFixed(3), strength: +s.toFixed(2) });
148
+ });
149
+ /* two "cuts" less than 0.1 s apart are a single few-frame transition
150
+ (flash, motion blur, smear): not a 0.03 s shot */
151
+ const merged = [];
152
+ for (const c of res) {
153
+ const prev = merged.at(-1);
154
+ if (prev && c.t - prev.t < 0.1) {
155
+ prev.transition = true;
156
+ prev.strength = Math.max(prev.strength, c.strength);
157
+ } else merged.push({ ...c });
158
+ }
159
+ return merged;
160
+ }
161
+
162
+ /* motion: mean luminance of the difference between neighbouring frames,
163
+ normalized to 30 fps (at 60 fps each difference covers half the time) */
164
+ function motion(file, fps) {
165
+ const out = run("ffmpeg", [
166
+ "-hide_banner",
167
+ "-nostats",
168
+ "-i",
169
+ file,
170
+ "-an",
171
+ "-vf",
172
+ "scale=180:-2,tblend=all_mode=difference,signalstats,metadata=print:key=lavfi.signalstats.YAVG:file=-",
173
+ "-f",
174
+ "null",
175
+ "-",
176
+ ]);
177
+ const pts = [];
178
+ let t = null;
179
+ for (const line of out.split("\n")) {
180
+ const mt = /pts_time:([\d.]+)/.exec(line);
181
+ if (mt) t = +mt[1];
182
+ const my = /YAVG=([\d.]+)/.exec(line);
183
+ if (my && t != null) pts.push([t, (+my[1] * fps) / 30]);
184
+ }
185
+ return pts;
186
+ }
187
+
188
+ const BLOCKS = "▁▂▃▄▅▆▇█";
189
+ const strip = (vals, max) => vals.map(v => BLOCKS[Math.min(7, Math.floor((v / (max || 1)) * 7.999))]).join("");
190
+ const pct = (arr, p) => {
191
+ const s = [...arr].sort((a, b) => a - b);
192
+ return s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
193
+ };
194
+
195
+ function rhythm(pts, duration) {
196
+ const step = Math.max(0.25, Math.ceil((duration / 60) * 4) / 4);
197
+ const n = Math.ceil(duration / step);
198
+ const sum = Array(n).fill(0),
199
+ count = Array(n).fill(0);
200
+ for (const [t, e] of pts) {
201
+ const i = Math.min(n - 1, Math.floor(t / step));
202
+ sum[i] += e;
203
+ count[i]++;
204
+ }
205
+ const energy = sum.map((s, i) => (count[i] ? s / count[i] : 0));
206
+ const max = Math.max(...energy);
207
+ const t = i => +Math.min(duration, i * step).toFixed(2);
208
+
209
+ /* Holding is not standing still: particles and slow camera keep alive in
210
+ the hold. What defines a hold is motion staying STABLE (nothing new
211
+ arrives on screen) for at least 1 s. */
212
+ const stable = energy.map((e, i) => i > 0 && Math.abs(e - energy[i - 1]) <= 0.12 * Math.max(e, 0.5));
213
+ const holds = [];
214
+ let start = null;
215
+ for (let i = 0; i <= n; i++) {
216
+ if (i < n && stable[i]) {
217
+ if (start == null) start = i;
218
+ continue;
219
+ }
220
+ if (start != null && (i - start) * step >= 1) holds.push([t(start), t(i)]);
221
+ start = null;
222
+ }
223
+
224
+ /* Change: a sharp rise over the previous half second. Opening already in
225
+ strong motion counts too. */
226
+ const median = pct(energy, 0.5);
227
+ const changes = [];
228
+ energy.forEach((e, i) => {
229
+ const before = i === 0 ? null : Math.min(energy[i - 1], energy[i - 2] ?? energy[i - 1]);
230
+ const rises = before == null ? e > 1.5 * median : e - before > 0.25 + 0.3 * before;
231
+ if (rises && !(changes.length && t(i) - changes.at(-1) < 0.5)) changes.push(t(i));
232
+ });
233
+
234
+ /* Truly frozen: almost nothing moves (below 12 % of p90). */
235
+ const threshold = Math.max(0.15, 0.12 * pct(energy, 0.9));
236
+ const frozen = energy.filter(e => e < threshold).length * step;
237
+ const holdTime = holds.reduce((s, [a, b]) => s + (b - a), 0);
238
+ return {
239
+ step,
240
+ strip: strip(energy, max),
241
+ energy: energy.map(e => +e.toFixed(2)),
242
+ changes,
243
+ holds,
244
+ hold_pct: Math.round((100 * holdTime) / duration),
245
+ frozen_pct: Math.round((100 * frozen) / duration),
246
+ };
247
+ }
248
+
249
+ /* palette: thumbnail frames at 2 fps, color quantized and grouped in OKLab */
250
+ const lin = c => {
251
+ c /= 255;
252
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
253
+ };
254
+ function oklab(r, g, b) {
255
+ const [R, G, B] = [lin(r), lin(g), lin(b)];
256
+ const l = Math.cbrt(0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B);
257
+ const m = Math.cbrt(0.2119034982 * R + 0.6806995451 * G + 0.1073969566 * B);
258
+ const s = Math.cbrt(0.0883024619 * R + 0.2817188376 * G + 0.6299787005 * B);
259
+ return [
260
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
261
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
262
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
263
+ ];
264
+ }
265
+ const hex = (r, g, b) =>
266
+ "#" +
267
+ [r, g, b]
268
+ .map(v => Math.round(v).toString(16).padStart(2, "0"))
269
+ .join("")
270
+ .toUpperCase();
271
+
272
+ function palette(file) {
273
+ const buf = run(
274
+ "ffmpeg",
275
+ [
276
+ "-hide_banner",
277
+ "-loglevel",
278
+ "error",
279
+ "-i",
280
+ file,
281
+ "-an",
282
+ "-vf",
283
+ "fps=2,scale=48:-2:flags=area",
284
+ "-f",
285
+ "rawvideo",
286
+ "-pix_fmt",
287
+ "rgb24",
288
+ "-",
289
+ ],
290
+ { binary: true },
291
+ );
292
+ const bins = new Map();
293
+ for (let i = 0; i + 2 < buf.length; i += 3) {
294
+ const k = ((buf[i] >> 4) << 8) | ((buf[i + 1] >> 4) << 4) | (buf[i + 2] >> 4);
295
+ const c = bins.get(k) ?? { n: 0, r: 0, g: 0, b: 0 };
296
+ c.n++;
297
+ c.r += buf[i];
298
+ c.g += buf[i + 1];
299
+ c.b += buf[i + 2];
300
+ bins.set(k, c);
301
+ }
302
+ const total = buf.length / 3;
303
+ const groups = [];
304
+ for (const c of [...bins.values()].sort((a, b) => b.n - a.n)) {
305
+ const lab = oklab(c.r / c.n, c.g / c.n, c.b / c.n);
306
+ const g = groups.find(g => Math.hypot(g.lab[0] - lab[0], g.lab[1] - lab[1], g.lab[2] - lab[2]) < 0.04);
307
+ if (g) {
308
+ g.n += c.n;
309
+ g.r += c.r;
310
+ g.g += c.g;
311
+ g.b += c.b;
312
+ } else groups.push({ ...c, lab });
313
+ }
314
+ return groups
315
+ .filter(g => g.n / total >= 0.002)
316
+ .slice(0, 8)
317
+ .map(g => colorOf(g, total));
318
+ }
319
+
320
+ function colorOf(g, total) {
321
+ const [L, a, b] = oklab(g.r / g.n, g.g / g.n, g.b / g.n);
322
+ return {
323
+ hex: hex(g.r / g.n, g.g / g.n, g.b / g.n),
324
+ oklch: `${L.toFixed(3)} ${Math.hypot(a, b).toFixed(3)} ${Math.round(((Math.atan2(b, a) * 180) / Math.PI + 360) % 360)}`,
325
+ area_pct: +((100 * g.n) / total).toFixed(2),
326
+ };
327
+ }
328
+
329
+ /* Accent and text take little area and vanish in the thumbnail average. Here
330
+ the sample is per point (no averaging) and only counts what has chroma
331
+ ≥ 0.08 or is far from the dominant background: where accent, text and
332
+ highlights live. */
333
+ function highlights(file, bgHex) {
334
+ const buf = run(
335
+ "ffmpeg",
336
+ [
337
+ "-hide_banner",
338
+ "-loglevel",
339
+ "error",
340
+ "-i",
341
+ file,
342
+ "-an",
343
+ "-vf",
344
+ "fps=2,scale=135:-2:flags=neighbor",
345
+ "-f",
346
+ "rawvideo",
347
+ "-pix_fmt",
348
+ "rgb24",
349
+ "-",
350
+ ],
351
+ { binary: true },
352
+ );
353
+ const f = oklab(...[1, 3, 5].map(i => parseInt(bgHex.slice(i, i + 2), 16)));
354
+ const bins = new Map();
355
+ for (let i = 0; i + 2 < buf.length; i += 3) {
356
+ const lab = oklab(buf[i], buf[i + 1], buf[i + 2]);
357
+ const chroma = Math.hypot(lab[1], lab[2]);
358
+ if (chroma < 0.08 && Math.hypot(lab[0] - f[0], lab[1] - f[1], lab[2] - f[2]) < 0.5) continue;
359
+ const k = ((buf[i] >> 4) << 8) | ((buf[i + 1] >> 4) << 4) | (buf[i + 2] >> 4);
360
+ const c = bins.get(k) ?? { n: 0, r: 0, g: 0, b: 0 };
361
+ c.n++;
362
+ c.r += buf[i];
363
+ c.g += buf[i + 1];
364
+ c.b += buf[i + 2];
365
+ bins.set(k, c);
366
+ }
367
+ const total = buf.length / 3;
368
+ const groups = [];
369
+ for (const c of [...bins.values()].sort((a, b) => b.n - a.n)) {
370
+ const lab = oklab(c.r / c.n, c.g / c.n, c.b / c.n);
371
+ const g = groups.find(g => Math.hypot(g.lab[0] - lab[0], g.lab[1] - lab[1], g.lab[2] - lab[2]) < 0.06);
372
+ if (g) {
373
+ g.n += c.n;
374
+ g.r += c.r;
375
+ g.g += c.g;
376
+ g.b += c.b;
377
+ } else groups.push({ ...c, lab });
378
+ }
379
+ return groups
380
+ .filter(g => g.n / total >= 0.0001)
381
+ .slice(0, 6)
382
+ .map(g => colorOf(g, total));
383
+ }
384
+
385
+ /* surface: hint that the piece is flat drawing (shape, type, UI) or filmed
386
+ image (photo, b-roll, grain). How many 4-bit colors cover 90 % of the
387
+ screen, and how many neighbours are equal (flat area), almost equal
388
+ (gradient, grain, photo) or edge. The library's shape references give 1 to
389
+ 23 colors and 83 to 95 % flat. A hint for reading the sheets, not a
390
+ verdict: a dark photo also has few colors. */
391
+ function surface(file) {
392
+ const buf = run(
393
+ "ffmpeg",
394
+ [
395
+ "-hide_banner",
396
+ "-loglevel",
397
+ "error",
398
+ "-i",
399
+ file,
400
+ "-an",
401
+ "-vf",
402
+ "fps=2,scale=96:-2:flags=area",
403
+ "-f",
404
+ "rawvideo",
405
+ "-pix_fmt",
406
+ "rgb24",
407
+ "-",
408
+ ],
409
+ { binary: true },
410
+ );
411
+ const n = buf.length / 3;
412
+ const bins = new Map();
413
+ let equal = 0,
414
+ smooth = 0;
415
+ for (let i = 0; i + 2 < buf.length; i += 3) {
416
+ const k = ((buf[i] >> 4) << 8) | ((buf[i + 1] >> 4) << 4) | (buf[i + 2] >> 4);
417
+ bins.set(k, (bins.get(k) ?? 0) + 1);
418
+ if (i < 3) continue;
419
+ const d = Math.abs(buf[i] - buf[i - 3]) + Math.abs(buf[i + 1] - buf[i - 2]) + Math.abs(buf[i + 2] - buf[i - 1]);
420
+ if (d <= 6) equal++;
421
+ else if (d <= 40) smooth++;
422
+ }
423
+ let sum = 0,
424
+ colors_90 = 0;
425
+ for (const v of [...bins.values()].sort((a, b) => b - a)) {
426
+ sum += v;
427
+ colors_90++;
428
+ if (sum >= 0.9 * n) break;
429
+ }
430
+ return { colors_90, flat_pct: Math.round((100 * equal) / n), smooth_pct: Math.round((100 * smooth) / n) };
431
+ }
432
+
433
+ /* sound: volume every 0.5 s and pulse by autocorrelation of the onsets */
434
+ function sound(file, duration) {
435
+ const raw = run(
436
+ "ffmpeg",
437
+ ["-hide_banner", "-loglevel", "error", "-i", file, "-vn", "-ac", "1", "-ar", "8000", "-f", "f32le", "-"],
438
+ { binary: true },
439
+ );
440
+ const x = new Float32Array(raw.buffer, raw.byteOffset, Math.floor(raw.length / 4));
441
+ const HOP = 80; /* 10 ms */
442
+ const e = [];
443
+ for (let i = 0; i + HOP <= x.length; i += HOP) {
444
+ let s = 0;
445
+ for (let j = i; j < i + HOP; j++) s += x[j] * x[j];
446
+ e.push(s / HOP);
447
+ }
448
+ const db = v => 10 * Math.log10(v + 1e-10);
449
+ const vol = [];
450
+ for (let i = 0; i < e.length; i += 50) {
451
+ const slice = e.slice(i, i + 50);
452
+ vol.push(db(slice.reduce((a, b) => a + b, 0) / slice.length));
453
+ }
454
+ const floor = Math.min(...vol);
455
+ const onsets = e.map((v, i) => Math.max(0, db(v) - db(e[i - 1] ?? v)));
456
+ const mean = onsets.reduce((a, b) => a + b, 0) / onsets.length;
457
+ const o = onsets.map(v => v - mean);
458
+ const auto = L => {
459
+ let s = 0;
460
+ for (let i = L; i < o.length; i++) s += o[i] * o[i - L];
461
+ return s;
462
+ };
463
+ const r0 = auto(0) || 1;
464
+ let best = { L: 0, r: -Infinity };
465
+ for (let L = 33; L <= 100; L++) {
466
+ const r = auto(L);
467
+ if (r > best.r) best = { L, r };
468
+ }
469
+ const strength = best.r / r0;
470
+ return {
471
+ volume_db: vol.map(v => +v.toFixed(1)),
472
+ strip: strip(
473
+ vol.map(v => v - floor),
474
+ Math.max(...vol) - floor,
475
+ ),
476
+ start_db: +vol
477
+ .slice(0, 4)
478
+ .reduce((a, b, _, s) => a + b / s.length, 0)
479
+ .toFixed(1),
480
+ end_db: +vol
481
+ .slice(-4)
482
+ .reduce((a, b, _, s) => a + b / s.length, 0)
483
+ .toFixed(1),
484
+ pulse:
485
+ strength >= 0.1
486
+ ? { bpm: Math.round(6000 / best.L), every_s: best.L / 100, strength: +strength.toFixed(2) }
487
+ : null,
488
+ };
489
+ }
490
+
491
+ /* --- sheets -------------------------------------------------------------------- */
492
+ function frameWidth(info) {
493
+ return info.h > info.w ? 180 : 320;
494
+ }
495
+ function columns(info) {
496
+ return info.h > info.w ? 6 : 4;
497
+ }
498
+
499
+ function contactSheet(file, info, out) {
500
+ const step = info.duration <= 20 ? 0.5 : info.duration <= 45 ? 1 : Math.ceil(info.duration / 45);
501
+ const n = Math.floor(info.duration / step) + 1;
502
+ const cols = columns(info),
503
+ rows = Math.ceil(n / cols);
504
+ run("ffmpeg", [
505
+ "-y",
506
+ "-hide_banner",
507
+ "-loglevel",
508
+ "error",
509
+ "-i",
510
+ file,
511
+ "-an",
512
+ "-vf",
513
+ `fps=1/${step}:round=near,scale=${frameWidth(info)}:-2,tile=${cols}x${rows}:padding=4:margin=4:color=white`,
514
+ "-frames:v",
515
+ "1",
516
+ out,
517
+ ]);
518
+ return { step, n, cols };
519
+ }
520
+
521
+ function cutsSheet(file, info, list, out) {
522
+ const used = list.slice(0, 12);
523
+ if (!used.length) return 0;
524
+ const frames = used.flatMap(c => {
525
+ const n = Math.round(c.t * info.fps);
526
+ return [Math.max(0, n - 1), n];
527
+ });
528
+ const sel = frames.map(n => `eq(n,${n})`).join("+");
529
+ run("ffmpeg", [
530
+ "-y",
531
+ "-hide_banner",
532
+ "-loglevel",
533
+ "error",
534
+ "-i",
535
+ file,
536
+ "-an",
537
+ "-vf",
538
+ `select='${sel}',scale=${frameWidth(info)}:-2,tile=2x${used.length}:padding=4:margin=4:color=white`,
539
+ "-fps_mode",
540
+ "vfr",
541
+ "-frames:v",
542
+ "1",
543
+ out,
544
+ ]);
545
+ return used.length;
546
+ }
547
+
548
+ /* --- commands ------------------------------------------------------------------ */
549
+ function measure(source, name, outOpt) {
550
+ if (!source || !name) fail("uso: reference.mjs measure <video|url> <nome> [--out <pasta>]");
551
+ const dir = resolve(outOpt ?? join(LIBRARY, name));
552
+ mkdirSync(join(dir, "sheets"), { recursive: true });
553
+
554
+ let file;
555
+ if (/^https?:\/\//.test(source)) {
556
+ file = join(dir, "video.mp4");
557
+ if (!existsSync(file)) {
558
+ console.log("baixando com yt-dlp…");
559
+ run("yt-dlp", [
560
+ "-q",
561
+ "--no-playlist",
562
+ "-f",
563
+ "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b",
564
+ "--merge-output-format",
565
+ "mp4",
566
+ "-o",
567
+ file,
568
+ source,
569
+ ]);
570
+ }
571
+ } else {
572
+ const origin = resolve(source);
573
+ if (!existsSync(origin)) fail(`${origin} não existe`);
574
+ file = dirname(origin) === dir ? origin : join(dir, `video${extname(origin).toLowerCase()}`);
575
+ if (file !== origin) copyFileSync(origin, file);
576
+ }
577
+
578
+ const info = probe(file);
579
+ console.log(
580
+ `${name} · ${info.duration}s · ${info.w}×${info.h} · ${info.fps} fps${info.audio ? " · com som" : " · sem som"}`,
581
+ );
582
+
583
+ const cutList = cuts(file, info.duration);
584
+ const shots = [0, ...cutList.map(c => c.t), info.duration]
585
+ .map((t, i, a) => (i ? +(t - a[i - 1]).toFixed(2) : null))
586
+ .slice(1);
587
+ const rhy = rhythm(motion(file, info.fps), info.duration);
588
+ const colors = palette(file);
589
+ const accents = highlights(file, colors[0].hex);
590
+ const surf = surface(file);
591
+ const audio = info.audio ? sound(file, info.duration) : null;
592
+ const contacts = contactSheet(file, info, join(dir, "sheets", "contacts.png"));
593
+ const nCuts = cutsSheet(file, info, cutList, join(dir, "sheets", "cuts.png"));
594
+
595
+ const measurements = {
596
+ name,
597
+ source,
598
+ video: basename(file),
599
+ measured_at: new Date().toISOString().slice(0, 10),
600
+ ...info,
601
+ cuts: cutList,
602
+ shots: {
603
+ count: shots.length,
604
+ mean_s: +(info.duration / shots.length).toFixed(2),
605
+ min_s: Math.min(...shots),
606
+ max_s: Math.max(...shots),
607
+ list_s: shots,
608
+ },
609
+ rhythm: rhy,
610
+ palette: colors,
611
+ highlights: accents,
612
+ surface: surf,
613
+ sound: audio,
614
+ sheets: { contacts: { file: "sheets/contacts.png", ...contacts }, cuts: nCuts ? "sheets/cuts.png" : null },
615
+ };
616
+ writeFileSync(join(dir, "measurements.json"), JSON.stringify(measurements, null, 1) + "\n");
617
+
618
+ console.log(
619
+ `planos: ${shots.length}${shots.length === 1 ? " (um plano só, sem corte)" : ` · média ${measurements.shots.mean_s}s · de ${measurements.shots.min_s} a ${measurements.shots.max_s}s`}`,
620
+ );
621
+ if (cutList.length)
622
+ console.log(
623
+ `cortes em: ${cutList.map(c => `${c.t}s${c.transition ? " (transição de poucos quadros)" : ""}`).join(", ")}`,
624
+ );
625
+ console.log(`movimento (cada caractere = ${rhy.step}s): ${rhy.strip}`);
626
+ console.log(`mudanças em: ${rhy.changes.join(", ")}s`);
627
+ console.log(
628
+ `segura ${rhy.hold_pct}% do tempo: ${rhy.holds.map(([a, b]) => `${a}–${b}s`).join(", ") || "nunca por 1 s"}` +
629
+ (rhy.frozen_pct ? ` · congelado de verdade ${rhy.frozen_pct}% (somando trechos curtos)` : " · nunca congela"),
630
+ );
631
+ console.log("paleta por área:");
632
+ for (const c of colors) console.log(` ${c.hex} oklch ${c.oklch.padEnd(16)} ${String(c.area_pct).padStart(6)}%`);
633
+ console.log("destaques (acento, texto, brilho):");
634
+ for (const c of accents) console.log(` ${c.hex} oklch ${c.oklch.padEnd(16)} ${String(c.area_pct).padStart(6)}%`);
635
+ const filmed = surf.colors_90 > 60 || surf.flat_pct < 60;
636
+ console.log(
637
+ `superfície: ${surf.colors_90} cores cobrem 90% · ${surf.flat_pct}% chapado · ${surf.smooth_pct}% gradiente/grão → ` +
638
+ (filmed
639
+ ? "indício de imagem filmada (foto, b-roll, textura): confira nas folhas e diga no Tipo do guia o que o código não reproduz"
640
+ : "indício de desenho chapado (shape, tipo, UI): reproduzível em código"),
641
+ );
642
+ if (audio) {
643
+ console.log(`som (cada caractere = 0.5s): ${audio.strip} de ${audio.start_db} a ${audio.end_db} dB`);
644
+ console.log(
645
+ audio.pulse
646
+ ? `pulso: ≈ ${audio.pulse.bpm} BPM, um a cada ${audio.pulse.every_s}s (força ${audio.pulse.strength})`
647
+ : "pulso: nenhum claro",
648
+ );
649
+ }
650
+ console.log(
651
+ `folhas: ${join(dir, "sheets", "contacts.png")} (um quadro a cada ${contacts.step}s, ${contacts.cols} por linha)` +
652
+ (nCuts ? ` · ${join(dir, "sheets", "cuts.png")} (antes | depois de cada corte)` : ""),
653
+ );
654
+
655
+ const guides = readdirSync(dir).filter(f => /\.md$/i.test(f));
656
+ if (!guides.length) {
657
+ const summary = `${info.duration} s · ${info.w}×${info.h} · ${info.fps} fps · ${shots.length === 1 ? "um plano só" : `${shots.length} planos`} · ${info.audio ? "com som" : "sem som"}`;
658
+ const tpl = readFileSync(join(CODE_ROOT, "templates", "reference.md"), "utf8")
659
+ .replace("<name>", () => name)
660
+ .replace("<source>", () => source)
661
+ .replace("<summary>", () => summary);
662
+ writeFileSync(join(dir, "GUIDE.md"), tpl);
663
+ console.log(`GUIDE.md criado a partir do modelo: leia as folhas e preencha → ${join(dir, "GUIDE.md")}`);
664
+ }
665
+ }
666
+
667
+ function list() {
668
+ if (!existsSync(LIBRARY)) fail(`${LIBRARY} não existe`);
669
+ const all = readdirSync(LIBRARY)
670
+ .filter(d => statSync(join(LIBRARY, d)).isDirectory())
671
+ .sort();
672
+ /* disabled in the studio (a `.disabled` in the dir): out of the choice */
673
+ const disabled = all.filter(d => existsSync(join(LIBRARY, d, ".disabled")));
674
+ const dirs = all.filter(d => !disabled.includes(d));
675
+ for (const d of dirs) {
676
+ const dir = join(LIBRARY, d);
677
+ const md = readdirSync(dir)
678
+ .filter(f => /\.md$/i.test(f))
679
+ .sort((a, b) => (b === "GUIDE.md") - (a === "GUIDE.md"))[0];
680
+ const text = md ? readFileSync(join(dir, md), "utf8") : "";
681
+ const title = /^#\s+(.+)$/m.exec(text)?.[1] ?? "(sem guia)";
682
+ /* GUIDE.md headings stay PT (parsed content) */
683
+ const essence = /##\s+Ess[êe]ncia\s*\n+([\s\S]*?)(?:\n\n|\n#)/.exec(text)?.[1]?.replace(/\s+/g, " ").trim();
684
+ const m = existsSync(join(dir, "measurements.json"))
685
+ ? JSON.parse(readFileSync(join(dir, "measurements.json"), "utf8"))
686
+ : null;
687
+ const videos = readdirSync(dir).filter(f => VIDEO_EXT.test(f));
688
+ console.log(`${d} · ${title}`);
689
+ if (essence) console.log(` ${essence.length > 220 ? essence.slice(0, 217) + "…" : essence}`);
690
+ const type = /##\s+Tipo\s*\n+([^\n]+)/.exec(text)?.[1]?.replace(/\*\*/g, "").trim();
691
+ console.log(
692
+ ` tipo: ${type ?? "não classificado (guia sem a seção Tipo: releia com a skill av-reference)"}` +
693
+ (existsSync(join(dir, "proof", "proof.html")) ? " · com prova em código" : " · sem prova"),
694
+ );
695
+ if (m)
696
+ console.log(
697
+ ` ${m.duration}s ${m.w}×${m.h} · ${m.shots.count} plano(s) · segura ${m.rhythm.hold_pct}% · ${m.sound?.pulse ? `${m.sound.pulse.bpm} BPM` : "sem pulso"} · movimento ${m.rhythm.strip.slice(0, 60)}`,
698
+ );
699
+ console.log(
700
+ ` guia: ${md ? join(dir, md) : "falta"}${m ? "" : " · sem measurements.json"}${videos.length ? "" : " · vídeo fora da raiz"}`,
701
+ );
702
+ }
703
+ if (disabled.length) console.log(`\ndesligadas (não use): ${disabled.join(", ")}`);
704
+ }
705
+
706
+ function excerpt(name, t0, t1, fps) {
707
+ if (!name || t0 == null || t1 == null) fail("uso: reference.mjs excerpt <nome> <t0> <t1> [--fps 15]");
708
+ const { dir, file } = openRef(name);
709
+ const info = probe(file);
710
+ const n = Math.ceil((t1 - t0) * fps);
711
+ const cols = columns(info);
712
+ const out = join(dir, "sheets", `excerpt-${t0}-${t1}.png`);
713
+ mkdirSync(dirname(out), { recursive: true });
714
+ run("ffmpeg", [
715
+ "-y",
716
+ "-hide_banner",
717
+ "-loglevel",
718
+ "error",
719
+ "-ss",
720
+ String(t0),
721
+ "-to",
722
+ String(t1),
723
+ "-i",
724
+ file,
725
+ "-an",
726
+ "-vf",
727
+ `fps=${fps},scale=${frameWidth(info)}:-2,tile=${cols}x${Math.ceil(n / cols)}:padding=4:margin=4:color=white`,
728
+ "-frames:v",
729
+ "1",
730
+ out,
731
+ ]);
732
+ console.log(`${n} quadros de ${t0}s a ${t1}s (um a cada ${(1 / fps).toFixed(3)}s, ${cols} por linha) → ${out}`);
733
+ }
734
+
735
+ function frame(name, times) {
736
+ if (!name || !times.length) fail("uso: reference.mjs frame <nome> <t> [<t> …]");
737
+ const { dir, file } = openRef(name);
738
+ mkdirSync(join(dir, "sheets"), { recursive: true });
739
+ for (const t of times) {
740
+ const out = join(dir, "sheets", `frame-${t}.png`);
741
+ run("ffmpeg", ["-y", "-hide_banner", "-loglevel", "error", "-ss", String(t), "-i", file, "-frames:v", "1", out]);
742
+ console.log(`t = ${t}s → ${out}`);
743
+ }
744
+ }
745
+
746
+ /* color: the point's pixel and the 5×5 mean around it. The mean removes
747
+ compression noise on flat areas; on thin strokes (letters, edges) the pixel counts. */
748
+ function colorAt(name, t, points) {
749
+ if (!name || !Number.isFinite(t) || !points.length) fail("uso: reference.mjs color <nome> <t> <x>,<y> [<x>,<y> …]");
750
+ const { file } = openRef(name);
751
+ const { w, h } = probe(file);
752
+ const buf = run(
753
+ "ffmpeg",
754
+ [
755
+ "-hide_banner",
756
+ "-loglevel",
757
+ "error",
758
+ "-ss",
759
+ String(t),
760
+ "-i",
761
+ file,
762
+ "-frames:v",
763
+ "1",
764
+ "-f",
765
+ "rawvideo",
766
+ "-pix_fmt",
767
+ "rgb24",
768
+ "-",
769
+ ],
770
+ { binary: true },
771
+ );
772
+ if (buf.length < w * h * 3) fail(`quadro em ${t}s veio com ${buf.length} bytes, esperava ${w * h * 3}`);
773
+ const px = (x, y) => {
774
+ const i = (Math.min(h - 1, Math.max(0, y)) * w + Math.min(w - 1, Math.max(0, x))) * 3;
775
+ return [buf[i], buf[i + 1], buf[i + 2]];
776
+ };
777
+ for (const p of points) {
778
+ const [x, y] = p.split(",").map(Number);
779
+ if (!Number.isInteger(x) || !Number.isInteger(y)) fail(`ponto "${p}" não é <x>,<y>`);
780
+ const mean = { n: 0, r: 0, g: 0, b: 0 };
781
+ for (let dy = -2; dy <= 2; dy++)
782
+ for (let dx = -2; dx <= 2; dx++) {
783
+ const [r, g, b] = px(x + dx, y + dy);
784
+ mean.n++;
785
+ mean.r += r;
786
+ mean.g += g;
787
+ mean.b += b;
788
+ }
789
+ const [r, g, b] = px(x, y);
790
+ const point = colorOf({ n: 1, r, g, b }, 1),
791
+ area = colorOf(mean, mean.n);
792
+ console.log(
793
+ `(${x}, ${y}) pixel ${point.hex} oklch ${point.oklch.padEnd(16)} média 5×5 ${area.hex} oklch ${area.oklch}`,
794
+ );
795
+ }
796
+ }
797
+
798
+ /* --- curve: measured duration and easing ---------------------------------------- */
799
+
800
+ /* CSS cubic-bezier: progress at `s` (fraction of time). x(u) is monotonic
801
+ for x1 and x2 in [0,1], so bisection finds u. */
802
+ function bezier([x1, y1, x2, y2], s) {
803
+ let lo = 0,
804
+ hi = 1,
805
+ u = s;
806
+ for (let i = 0; i < 22; i++) {
807
+ const x = 3 * (1 - u) * (1 - u) * u * x1 + 3 * (1 - u) * u * u * x2 + u * u * u;
808
+ if (x < s) lo = u;
809
+ else hi = u;
810
+ u = (lo + hi) / 2;
811
+ }
812
+ return 3 * (1 - u) * (1 - u) * u * y1 + 3 * (1 - u) * u * u * y2 + u * u * u;
813
+ }
814
+
815
+ /* CSS and GSAP names (power1 = quad … power4 = quint), as cubic-bezier */
816
+ const CURVES = {
817
+ linear: [0, 0, 1, 1],
818
+ ease: [0.25, 0.1, 0.25, 1],
819
+ "ease-out": [0, 0, 0.58, 1],
820
+ "ease-in": [0.42, 0, 1, 1],
821
+ "ease-in-out": [0.42, 0, 0.58, 1],
822
+ "sine.out": [0.61, 1, 0.88, 1],
823
+ "quad.out · power1.out": [0.5, 1, 0.89, 1],
824
+ "cubic.out · power2.out": [0.33, 1, 0.68, 1],
825
+ "quart.out · power3.out": [0.25, 1, 0.5, 1],
826
+ "quint.out · power4.out": [0.22, 1, 0.36, 1],
827
+ "expo.out": [0.16, 1, 0.3, 1],
828
+ "circ.out": [0, 0.55, 0.45, 1],
829
+ "back.out": [0.34, 1.56, 0.64, 1],
830
+ "sine.in": [0.12, 0, 0.39, 0],
831
+ "quad.in · power1.in": [0.11, 0, 0.5, 0],
832
+ "cubic.in · power2.in": [0.32, 0, 0.67, 0],
833
+ "quart.in · power3.in": [0.5, 0, 0.75, 0],
834
+ "quint.in · power4.in": [0.64, 0, 0.78, 0],
835
+ "expo.in": [0.7, 0, 0.84, 0],
836
+ "back.in": [0.36, 0, 0.66, -0.56],
837
+ "sine.inOut": [0.37, 0, 0.63, 1],
838
+ "quad.inOut · power1.inOut": [0.45, 0, 0.55, 1],
839
+ "cubic.inOut · power2.inOut": [0.65, 0, 0.35, 1],
840
+ "quart.inOut · power3.inOut": [0.76, 0, 0.24, 1],
841
+ "quint.inOut · power4.inOut": [0.83, 0, 0.17, 1],
842
+ "expo.inOut": [0.87, 0, 0.13, 1],
843
+ "back.inOut": [0.68, -0.6, 0.32, 1.6],
844
+ };
845
+
846
+ /* RMS error of a whole animation against the frames: still at 0 until
847
+ `start`, the curve until `end`, still at 1 after. Start, end and curve are
848
+ fitted together: the "visible" end of an ease-out arrives long before the
849
+ real end (a 700 ms power4.out seems to end at 400), and trimming before
850
+ fitting changes the curve and shortens the duration. */
851
+ function animError(c, start, end, pts) {
852
+ let s = 0;
853
+ for (const [t, p] of pts) {
854
+ const q = t <= start ? 0 : t >= end ? 1 : bezier(c, (t - start) / (end - start));
855
+ s += (q - p) ** 2;
856
+ }
857
+ return Math.sqrt(s / pts.length);
858
+ }
859
+
860
+ /* each named curve with the best start and end on a half-frame grid; then
861
+ the best three seed a coordinate descent over the four bezier points,
862
+ the start and the end */
863
+ function fitAnim(pts, startDet, endDet, fps) {
864
+ const half = 1 / (2 * fps);
865
+ const starts = [],
866
+ ends = [];
867
+ for (let t = startDet - 0.4; t <= startDet + half; t += half) starts.push(t);
868
+ for (let t = endDet - half; t <= endDet + 0.8; t += half) ends.push(t);
869
+ const named = Object.entries(CURVES)
870
+ .map(([name, c]) => {
871
+ let m = { name, c, e: Infinity };
872
+ for (const start of starts)
873
+ for (const end of ends) {
874
+ if (end - start < 2 * half) continue;
875
+ const e = animError(c, start, end, pts);
876
+ if (e < m.e) m = { name, c, start, end, e };
877
+ }
878
+ return m;
879
+ })
880
+ .sort((a, b) => a.e - b.e);
881
+
882
+ let best = null;
883
+ for (const seed of named.slice(0, 3)) {
884
+ let m = { c: [...seed.c], start: seed.start, end: seed.end, e: seed.e };
885
+ for (const step of [0.05, 0.02, 0.01]) {
886
+ let changed = true;
887
+ while (changed) {
888
+ changed = false;
889
+ for (let k = 0; k < 6; k++)
890
+ for (const sign of [-1, 1]) {
891
+ const c = [...m.c];
892
+ let { start, end } = m;
893
+ if (k < 4) {
894
+ c[k] = +(c[k] + sign * step).toFixed(2);
895
+ if ((k === 0 || k === 2) && (c[k] < 0 || c[k] > 1)) continue;
896
+ } else if (k === 4) start += sign * half * step * 20;
897
+ else end += sign * half * step * 20;
898
+ if (end - start < 2 * half) continue;
899
+ const e = animError(c, start, end, pts);
900
+ if (e < m.e - 1e-6) {
901
+ m = { c, start, end, e };
902
+ changed = true;
903
+ }
904
+ }
905
+ }
906
+ }
907
+ if (!best || m.e < best.e) best = m;
908
+ }
909
+ return { free: best, named: named.slice(0, 3) };
910
+ }
911
+
912
+ /* Tracks what is inside a box of the frame (full-resolution coordinates)
913
+ and measures, frame by frame, position, size and presence (area × opacity).
914
+ The channel that changes most becomes progress 0→1, and from it come start,
915
+ duration, curve and overshoot. The box must hold only the element, start to end. */
916
+ function curve(name, t0, t1, boxTxt, colorTxt, bgTxt) {
917
+ if (!name || !Number.isFinite(t0) || !Number.isFinite(t1) || t1 <= t0 || !boxTxt) {
918
+ fail("uso: reference.mjs curve <nome> <t0> <t1> --box <x>,<y>,<w>,<h> [--color #hex] [--background #hex]");
919
+ }
920
+ const { dir, file } = openRef(name);
921
+ const info = probe(file);
922
+ const [bx, by, bw, bh] = boxTxt.split(",").map(Number);
923
+ if (![bx, by, bw, bh].every(Number.isInteger) || bw < 4 || bh < 4 || bx + bw > info.w || by + bh > info.h) {
924
+ fail(`caixa "${boxTxt}" fora do quadro ${info.w}×${info.h} (x,y,w,h em px inteiros)`);
925
+ }
926
+ const sw = Math.min(160, bw) & ~1,
927
+ sh = Math.max(2, Math.round((bh * sw) / bw) & ~1);
928
+ const scale = bw / sw;
929
+ const buf = run(
930
+ "ffmpeg",
931
+ [
932
+ "-hide_banner",
933
+ "-loglevel",
934
+ "error",
935
+ "-ss",
936
+ String(t0),
937
+ "-to",
938
+ String(t1),
939
+ "-i",
940
+ file,
941
+ "-an",
942
+ "-vf",
943
+ `crop=${bw}:${bh}:${bx}:${by},scale=${sw}:${sh}:flags=area`,
944
+ "-f",
945
+ "rawvideo",
946
+ "-pix_fmt",
947
+ "rgb24",
948
+ "-",
949
+ ],
950
+ { binary: true },
951
+ );
952
+ const size = sw * sh * 3,
953
+ nf = Math.floor(buf.length / size);
954
+ if (nf < 4) fail(`só ${nf} quadros entre ${t0}s e ${t1}s`);
955
+ const labOf = h => oklab(...[1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16)));
956
+
957
+ /* background: the requested one, or the most common color on the box border over the range */
958
+ let bg;
959
+ if (bgTxt) bg = labOf(bgTxt);
960
+ else {
961
+ const cx = new Map();
962
+ for (let q = 0; q < nf; q++)
963
+ for (let y = 0; y < sh; y++)
964
+ for (let x = 0; x < sw; x++) {
965
+ if (x > 0 && x < sw - 1 && y > 0 && y < sh - 1) continue;
966
+ const i = q * size + (y * sw + x) * 3;
967
+ const k = ((buf[i] >> 4) << 8) | ((buf[i + 1] >> 4) << 4) | (buf[i + 2] >> 4);
968
+ const c = cx.get(k) ?? { n: 0, r: 0, g: 0, b: 0 };
969
+ c.n++;
970
+ c.r += buf[i];
971
+ c.g += buf[i + 1];
972
+ c.b += buf[i + 2];
973
+ cx.set(k, c);
974
+ }
975
+ const c = [...cx.values()].sort((a, b) => b.n - a.n)[0];
976
+ bg = oklab(c.r / c.n, c.g / c.n, c.b / c.n);
977
+ bgTxt = hex(c.r / c.n, c.g / c.n, c.b / c.n);
978
+ }
979
+ const target = colorTxt ? labOf(colorTxt) : null;
980
+ const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
981
+
982
+ /* weight of each pixel: with --color, closeness to it; without, distance
983
+ from the background, scaled to what the element reaches in the range (so a fade shows) */
984
+ const d = new Float32Array(nf * sw * sh);
985
+ for (let p = 0; p < d.length; p++) {
986
+ const lab = oklab(buf[p * 3], buf[p * 3 + 1], buf[p * 3 + 2]);
987
+ d[p] = target ? Math.max(0, 1 - Math.max(0, dist(lab, target) - 0.06) / 0.1) : dist(lab, bg);
988
+ }
989
+ let ref = 1;
990
+ if (!target) {
991
+ const strong = [...d].filter(v => v > 0.05);
992
+ if (!strong.length) fail(`nada se distingue do fundo ${bgTxt} na caixa: aumente a caixa ou passe --color`);
993
+ ref = Math.max(0.08, pct(strong, 0.9));
994
+ }
995
+ const series = [];
996
+ for (let q = 0; q < nf; q++) {
997
+ let P = 0,
998
+ X = 0,
999
+ Y = 0,
1000
+ XX = 0,
1001
+ YY = 0;
1002
+ for (let y = 0; y < sh; y++)
1003
+ for (let x = 0; x < sw; x++) {
1004
+ const v = d[q * sw * sh + y * sw + x];
1005
+ const w = target ? v : Math.min(1, Math.max(0, (v - 0.03) / (ref - 0.03)));
1006
+ if (!w) continue;
1007
+ P += w;
1008
+ X += w * x;
1009
+ Y += w * y;
1010
+ XX += w * x * x;
1011
+ YY += w * y * y;
1012
+ }
1013
+ const empty = P < 0.002 * sw * sh;
1014
+ const mx = X / P,
1015
+ my = Y / P;
1016
+ series.push({
1017
+ t: +(t0 + q / info.fps).toFixed(3),
1018
+ presence: P / (sw * sh),
1019
+ x: empty ? null : bx + (mx + 0.5) * scale,
1020
+ y: empty ? null : by + (my + 0.5) * scale,
1021
+ size: empty ? null : Math.sqrt(Math.max(0, XX / P - mx * mx) + Math.max(0, YY / P - my * my)) * scale,
1022
+ });
1023
+ }
1024
+
1025
+ /* each channel: where it started, where it went, how far it moved (relative) */
1026
+ const channels = ["x", "y", "size", "presence"]
1027
+ .map(k => {
1028
+ const vals = series.map(s => s[k]);
1029
+ const ok = vals.map((v, i) => [i, v]).filter(([, v]) => v != null);
1030
+ if (ok.length < 4) return null;
1031
+ const v0 = ok[0][1],
1032
+ v1 = ok.at(-1)[1];
1033
+ const span = k === "x" ? bw : k === "y" ? bh : Math.max(...ok.map(([, v]) => v)) || 1;
1034
+ return { k, vals, v0, v1, rel: Math.abs(v1 - v0) / span };
1035
+ /* presence goes last: whatever enters from nothing goes 0 to 100 % even when
1036
+ what matters is the path (position) or the growth (size) */
1037
+ })
1038
+ .filter(c => c && c.rel >= 0.05)
1039
+ .sort((a, b) => (a.k === "presence") - (b.k === "presence") || b.rel - a.rel);
1040
+ if (!channels.length) fail("nada muda na caixa entre t0 e t1 (menos de 5 % em posição, tamanho e presença)");
1041
+
1042
+ const result = channels.map(c => {
1043
+ const { k, vals, v0, v1 } = c;
1044
+ const prog = vals.map(v => (v == null ? (k === "presence" ? 0 : null) : (v - v0) / (v1 - v0)));
1045
+ const valid = prog.map((p, i) => [i, p]).filter(([, p]) => p != null);
1046
+ const first = valid.find(([, p]) => Math.abs(p) > 0.03)?.[0] ?? 0;
1047
+ const last = [...valid].reverse().find(([, p]) => Math.abs(p - 1) > 0.03)?.[0] ?? nf - 1;
1048
+ const a = Math.max(0, first - 1),
1049
+ b = Math.min(nf - 1, last + 1);
1050
+ const tq = i => t0 + i / info.fps;
1051
+ const span = valid.filter(([i]) => i >= a && i <= b);
1052
+ const max = Math.max(...span.map(([, p]) => p)),
1053
+ min = Math.min(...span.map(([, p]) => p));
1054
+ const ms = s => Math.round(s * 1000);
1055
+ const unit = k === "presence" ? "" : "px";
1056
+ const fmt = v => (k === "presence" ? `${(v * 100).toFixed(1)}% da caixa` : `${Math.round(v)}${unit}`);
1057
+ const base = {
1058
+ channel: k,
1059
+ from: fmt(v0),
1060
+ to: fmt(v1),
1061
+ overshoot: max > 1.03 ? +(max - 1).toFixed(2) : 0,
1062
+ anticipation: min < -0.03 ? +(-min).toFixed(2) : 0,
1063
+ strip: strip(
1064
+ span.map(([, p]) => Math.max(0, p - Math.min(0, min))),
1065
+ Math.max(1, max) - Math.min(0, min),
1066
+ ),
1067
+ };
1068
+
1069
+ /* steps: most intervals still and a few big jumps. In CSS, steps(n)
1070
+ (jump-end) holds the whole first step: the real start is one spacing
1071
+ before the first jump, and the duration is n spacings. */
1072
+ const deltas = span.slice(1).map(([i, p], j) => [i, p - span[j][1]]);
1073
+ const jumps = deltas.filter(([, x]) => Math.abs(x) >= 0.04);
1074
+ const still = deltas.filter(([, x]) => Math.abs(x) < 0.01).length;
1075
+ if (
1076
+ deltas.length >= 4 &&
1077
+ still / deltas.length >= 0.5 &&
1078
+ jumps.length >= 2 &&
1079
+ jumps.length <= 12 &&
1080
+ jumps.every(([, x]) => Math.abs(x) >= 0.5 / jumps.length)
1081
+ ) {
1082
+ const n = jumps.length;
1083
+ const spacing = (tq(jumps.at(-1)[0]) - tq(jumps[0][0])) / (n - 1);
1084
+ return {
1085
+ ...base,
1086
+ steps: n,
1087
+ start_s: +(tq(jumps[0][0]) - spacing).toFixed(3),
1088
+ dur_ms: ms(spacing * n),
1089
+ spacing_ms: ms(spacing),
1090
+ };
1091
+ }
1092
+
1093
+ const pts = valid.map(([i, p]) => [tq(i), p]);
1094
+ const { free, named } = fitAnim(pts, tq(a), tq(b), info.fps);
1095
+ return {
1096
+ ...base,
1097
+ steps: null,
1098
+ start_s: +free.start.toFixed(3),
1099
+ dur_ms: ms(free.end - free.start),
1100
+ bezier: free.c.map(v => +v.toFixed(2)),
1101
+ error: +free.e.toFixed(3),
1102
+ named: named.map(x => ({
1103
+ name: x.name,
1104
+ bezier: x.c,
1105
+ start_s: +x.start.toFixed(3),
1106
+ dur_ms: ms(x.end - x.start),
1107
+ error: +x.e.toFixed(3),
1108
+ })),
1109
+ };
1110
+ });
1111
+
1112
+ const frameMs = 1000 / info.fps;
1113
+ console.log(
1114
+ `${name} · ${t0}–${t1}s · caixa ${boxTxt} · ${nf} quadros a ${info.fps} fps (±${Math.round(frameMs)} ms nas pontas) · ${target ? `cor ${colorTxt}` : `fundo ${bgTxt}`}`,
1115
+ );
1116
+ for (const r of result) {
1117
+ console.log(`\n${r.channel}: ${r.from} → ${r.to}`);
1118
+ console.log(` progresso ${r.strip}`);
1119
+ if (r.steps) {
1120
+ console.log(
1121
+ ` anda em degraus: steps(${r.steps}), um salto a cada ${r.spacing_ms} ms · começa em ${r.start_s}s · ${r.dur_ms} ms (CSS steps, jump-end)`,
1122
+ );
1123
+ continue;
1124
+ }
1125
+ console.log(
1126
+ ` ajuste: cubic-bezier(${r.bezier.join(", ")}) começa em ${r.start_s}s · ${r.dur_ms} ms · erro ${r.error}`,
1127
+ );
1128
+ for (const n of r.named)
1129
+ console.log(
1130
+ ` perto de: ${n.name.padEnd(28)} cubic-bezier(${n.bezier.join(", ")}) começa em ${n.start_s}s · ${n.dur_ms} ms · erro ${n.error}`,
1131
+ );
1132
+ if (r.overshoot) console.log(` overshoot: passa ${Math.round(r.overshoot * 100)}% do destino e volta`);
1133
+ if (r.anticipation) console.log(` antecipação: recua ${Math.round(r.anticipation * 100)}% antes de ir`);
1134
+ }
1135
+ console.log(
1136
+ `\nerro < 0,03: é essa curva, com essa duração (curvas parecidas com durações diferentes dão o mesmo movimento; prefira a nomeada se o erro dela for quase o mesmo). De 0,03 a 0,06, aproximação. Acima, a caixa pegou mais de um elemento ou o movimento tem fases: meça cada uma.`,
1137
+ );
1138
+
1139
+ const mf = join(dir, "measurements.json");
1140
+ if (existsSync(mf)) {
1141
+ const m = JSON.parse(readFileSync(mf, "utf8"));
1142
+ const key = `${t0}-${t1}@${boxTxt}${colorTxt ? `:${colorTxt}` : ""}`;
1143
+ m.curves = [
1144
+ ...(m.curves ?? []).filter(c => c.key !== key),
1145
+ { key, t0, t1, box: boxTxt, color: colorTxt ?? null, channels: result },
1146
+ ];
1147
+ writeFileSync(mf, JSON.stringify(m, null, 1) + "\n");
1148
+ console.log(`gravado em measurements.json → curves["${key}"]`);
1149
+ }
1150
+ }
1151
+
1152
+ /* --- compare: the proof scene next to the reference ------------------------------- */
1153
+
1154
+ /* frame → rgb24 pixels at a fixed size */
1155
+ function pixels(png, w, h) {
1156
+ return run(
1157
+ "ffmpeg",
1158
+ [
1159
+ "-hide_banner",
1160
+ "-loglevel",
1161
+ "error",
1162
+ "-i",
1163
+ png,
1164
+ "-vf",
1165
+ `scale=${w}:${h}:flags=area`,
1166
+ "-frames:v",
1167
+ "1",
1168
+ "-f",
1169
+ "rawvideo",
1170
+ "-pix_fmt",
1171
+ "rgb24",
1172
+ "-",
1173
+ ],
1174
+ { binary: true },
1175
+ );
1176
+ }
1177
+
1178
+ /* palette of one frame: colors grouped in OKLab, with area fraction */
1179
+ function paletteOf(buf) {
1180
+ const groups = [];
1181
+ for (let i = 0; i + 2 < buf.length; i += 3) {
1182
+ const lab = oklab(buf[i], buf[i + 1], buf[i + 2]);
1183
+ const g = groups.find(g => Math.hypot(g.lab[0] - lab[0], g.lab[1] - lab[1], g.lab[2] - lab[2]) < 0.06);
1184
+ if (g) {
1185
+ g.n++;
1186
+ g.r += buf[i];
1187
+ g.g += buf[i + 1];
1188
+ g.b += buf[i + 2];
1189
+ } else groups.push({ lab, n: 1, r: buf[i], g: buf[i + 1], b: buf[i + 2] });
1190
+ }
1191
+ const total = buf.length / 3;
1192
+ return groups
1193
+ .filter(g => g.n / total >= 0.002)
1194
+ .map(g => ({ lab: g.lab, f: g.n / total, hex: hex(g.r / g.n, g.g / g.n, g.b / g.n) }));
1195
+ }
1196
+
1197
+ /* Palette regardless of layout: each color on one side goes to the nearest
1198
+ on the other, both ways (× 100, like ΔE). Weight is the square root of the
1199
+ area, otherwise the accent (1 % of the screen) vanishes behind the
1200
+ background. A reference color with no match in the proof (ΔE > 8) is
1201
+ "missing"; a proof color with no match in the reference is "extra". */
1202
+ function paletteDistance(a, b) {
1203
+ const nearest = (c, q) =>
1204
+ Math.min(...q.map(d => Math.hypot(c.lab[0] - d.lab[0], c.lab[1] - d.lab[1], c.lab[2] - d.lab[2])));
1205
+ const side = (p, q) =>
1206
+ p.reduce((s, c) => s + Math.sqrt(c.f) * nearest(c, q), 0) / p.reduce((s, c) => s + Math.sqrt(c.f), 0);
1207
+ const unmatched = (p, q) => p.filter(c => nearest(c, q) > 0.08).map(c => `${c.hex} ${(100 * c.f).toFixed(1)}%`);
1208
+ return { de: +(50 * (side(a, b) + side(b, a))).toFixed(1), missing: unmatched(a, b), extra: unmatched(b, a) };
1209
+ }
1210
+
1211
+ /* The proof is a seek-safe scene (templates/scene.html) that recreates an
1212
+ excerpt of the reference at its size. `compare` renders the proof at the
1213
+ requested times, grabs the reference frames and builds one sheet row per
1214
+ pair: reference | proof | difference. Three numbers per pair: structure
1215
+ SSIM (1 = equal), per-point color ΔE (layout and color) and palette ΔE
1216
+ (colors only, wherever they are). The numbers track the proof improving
1217
+ from one round to the next; reading the sheet decides if it looks alike. */
1218
+ async function compare(name, scene, pairs, outOpt) {
1219
+ if (!name || !scene || !pairs.length)
1220
+ fail(
1221
+ "uso: reference.mjs compare <nome> <cena.html> <tRef>[:<tCena>] [<tRef>[:<tCena>] …] [--out sheets/comparison.png]",
1222
+ );
1223
+ const { dir, file } = openRef(name);
1224
+ const info = probe(file);
1225
+ const html = resolve(scene);
1226
+ if (!existsSync(html)) fail(`${html} não existe`);
1227
+ const list = pairs.map(p => {
1228
+ const [r, c] = p.split(":").map(Number);
1229
+ return { ref: r, scene: Number.isFinite(c) ? c : r };
1230
+ });
1231
+ if (list.some(p => !Number.isFinite(p.ref))) fail(`pares: <tRef>[:<tCena>], em segundos (recebi ${pairs.join(" ")})`);
1232
+
1233
+ const tmp = join(dir, "sheets", `.compare-${process.pid}`);
1234
+ mkdirSync(tmp, { recursive: true });
1235
+ const r = spawnSync(
1236
+ process.execPath,
1237
+ [
1238
+ join(CODE_ROOT, "tools", "render.mjs"),
1239
+ html,
1240
+ "--frames",
1241
+ join(tmp, "proof"),
1242
+ "--at",
1243
+ list.map(p => p.scene).join(","),
1244
+ "--w",
1245
+ String(info.w),
1246
+ "--h",
1247
+ String(info.h),
1248
+ ],
1249
+ { encoding: "utf8" },
1250
+ );
1251
+ if (r.status !== 0) fail(`render.mjs saiu com ${r.status}: ${(r.stderr || r.stdout).slice(-600)}`);
1252
+
1253
+ const lw = 300,
1254
+ lh = Math.round((info.h * lw) / info.w / 2) * 2;
1255
+ const mw = 36,
1256
+ mh = Math.max(2, Math.round((info.h * mw) / info.w));
1257
+ const rows = [];
1258
+ for (const [i, p] of list.entries()) {
1259
+ const refPng = join(tmp, `ref${i}.png`),
1260
+ proofPng = join(tmp, "proof", `q${String(i).padStart(3, "0")}.png`);
1261
+ run("ffmpeg", [
1262
+ "-y",
1263
+ "-hide_banner",
1264
+ "-loglevel",
1265
+ "error",
1266
+ "-ss",
1267
+ String(p.ref),
1268
+ "-i",
1269
+ file,
1270
+ "-frames:v",
1271
+ "1",
1272
+ refPng,
1273
+ ]);
1274
+ if (!existsSync(proofPng)) fail(`o render não gerou ${proofPng}`);
1275
+ const ssimOut = spawnSync(
1276
+ "ffmpeg",
1277
+ [
1278
+ "-hide_banner",
1279
+ "-nostats",
1280
+ "-i",
1281
+ refPng,
1282
+ "-i",
1283
+ proofPng,
1284
+ "-lavfi",
1285
+ `[0]scale=${lw}:${lh},format=gray[a];[1]scale=${lw}:${lh},format=gray[b];[a][b]ssim`,
1286
+ "-f",
1287
+ "null",
1288
+ "-",
1289
+ ],
1290
+ { encoding: "utf8" },
1291
+ ).stderr;
1292
+ const ssim = +(/All:([\d.]+)/.exec(ssimOut)?.[1] ?? NaN);
1293
+ const a = pixels(refPng, mw, mh),
1294
+ b = pixels(proofPng, mw, mh);
1295
+ let de = 0;
1296
+ for (let k = 0; k + 2 < a.length; k += 3) {
1297
+ const la = oklab(a[k], a[k + 1], a[k + 2]),
1298
+ lb = oklab(b[k], b[k + 1], b[k + 2]);
1299
+ de += Math.hypot(la[0] - lb[0], la[1] - lb[1], la[2] - lb[2]);
1300
+ }
1301
+ const pa = paletteOf(pixels(refPng, 96, Math.round((info.h * 96) / info.w))),
1302
+ pb = paletteOf(pixels(proofPng, 96, Math.round((info.h * 96) / info.w)));
1303
+ const pal = paletteDistance(pa, pb);
1304
+ rows.push({
1305
+ ref: p.ref,
1306
+ scene: p.scene,
1307
+ ssim: +ssim.toFixed(3),
1308
+ delta_e: +((100 * de) / (a.length / 3)).toFixed(1),
1309
+ palette_de: pal.de,
1310
+ missing: pal.missing,
1311
+ extra: pal.extra,
1312
+ refPng,
1313
+ proofPng,
1314
+ });
1315
+ }
1316
+
1317
+ const out = resolve(dir, outOpt ?? join("sheets", "comparison.png"));
1318
+ mkdirSync(dirname(out), { recursive: true });
1319
+ const inputs = rows.flatMap(l => ["-i", l.refPng, "-i", l.proofPng]);
1320
+ const filter =
1321
+ rows
1322
+ .map(
1323
+ (_, i) =>
1324
+ `[${2 * i}]scale=${lw}:${lh},format=rgb24,split[r${i}][rd${i}];[${2 * i + 1}]scale=${lw}:${lh},format=rgb24,split[p${i}][pd${i}];` +
1325
+ `[rd${i}][pd${i}]blend=all_mode=difference,lutrgb=r='min(255,val*3)':g='min(255,val*3)':b='min(255,val*3)'[d${i}];` +
1326
+ `[r${i}]pad=iw+4:ih:0:0:white[rr${i}];[p${i}]pad=iw+4:ih:0:0:white[pp${i}];[rr${i}][pp${i}][d${i}]hstack=3,pad=iw+8:ih+8:4:4:white[l${i}]`,
1327
+ )
1328
+ .join(";") +
1329
+ (rows.length > 1 ? `;${rows.map((_, i) => `[l${i}]`).join("")}vstack=${rows.length}[out]` : ";[l0]null[out]");
1330
+ run("ffmpeg", [
1331
+ "-y",
1332
+ "-hide_banner",
1333
+ "-loglevel",
1334
+ "error",
1335
+ ...inputs,
1336
+ "-filter_complex",
1337
+ filter,
1338
+ "-map",
1339
+ "[out]",
1340
+ "-frames:v",
1341
+ "1",
1342
+ out,
1343
+ ]);
1344
+ rmSync(tmp, { recursive: true, force: true });
1345
+
1346
+ console.log(`${name} × ${basename(html)} · ${info.w}×${info.h}`);
1347
+ console.log("t ref → t prova SSIM ΔE cor ΔE paleta");
1348
+ for (const l of rows) {
1349
+ console.log(
1350
+ `${String(l.ref).padStart(6)}s → ${String(l.scene).padEnd(7)}s ${l.ssim.toFixed(3)} ${String(l.delta_e).padStart(6)} ${String(l.palette_de).padStart(9)}`,
1351
+ );
1352
+ if (l.missing.length) console.log(` falta na prova: ${l.missing.join(" · ")}`);
1353
+ if (l.extra.length) console.log(` sobra na prova: ${l.extra.join(" · ")}`);
1354
+ }
1355
+ const mean = k => +(rows.reduce((s, l) => s + l[k], 0) / rows.length).toFixed(k === "ssim" ? 3 : 1);
1356
+ console.log(
1357
+ `média ${mean("ssim").toFixed(3)} ${String(mean("delta_e")).padStart(6)} ${String(mean("palette_de")).padStart(9)}`,
1358
+ );
1359
+ console.log(`folha: ${out} (cada linha: estilo visual | prova | diferença ×3). Leia com Read.`);
1360
+ console.log(
1361
+ "ΔE paleta perto de 1: as mesmas cores; o que diz o que corrigir é a lista do que falta e do que sobra. SSIM e ΔE cor caem com layout diferente, mesmo no estilo certo: compare de uma volta para outra.",
1362
+ );
1363
+
1364
+ const mf = join(dir, "measurements.json");
1365
+ if (existsSync(mf)) {
1366
+ const m = JSON.parse(readFileSync(mf, "utf8"));
1367
+ const round = {
1368
+ at: new Date().toISOString().slice(0, 16),
1369
+ scene: relative(dir, html),
1370
+ sheet: relative(dir, out),
1371
+ pairs: rows.map(({ refPng, proofPng, ...l }) => l),
1372
+ mean: { ssim: mean("ssim"), delta_e: mean("delta_e"), palette_de: mean("palette_de") },
1373
+ };
1374
+ m.proofs = [...(m.proofs ?? []), round].slice(-10);
1375
+ writeFileSync(mf, JSON.stringify(m, null, 1) + "\n");
1376
+ console.log(`gravado em measurements.json → proofs (volta ${m.proofs.length})`);
1377
+ }
1378
+ }
1379
+
1380
+ /* --- cli ------------------------------------------------------------------------ */
1381
+ const [cmd, ...rest] = process.argv.slice(2);
1382
+ const opt = (n, d) => {
1383
+ const i = rest.indexOf(`--${n}`);
1384
+ return i < 0 ? d : rest.splice(i, 2)[1];
1385
+ };
1386
+ if (cmd === "list") list();
1387
+ else if (cmd === "measure") {
1388
+ const out = opt("out");
1389
+ measure(rest[0], rest[1], out);
1390
+ } else if (cmd === "excerpt") {
1391
+ const fps = +opt("fps", 15);
1392
+ excerpt(rest[0], +rest[1], +rest[2], fps);
1393
+ } else if (cmd === "frame") frame(rest[0], rest.slice(1).map(Number));
1394
+ else if (cmd === "color") colorAt(rest[0], +rest[1], rest.slice(2));
1395
+ else if (cmd === "curve") {
1396
+ const box = opt("box"),
1397
+ color = opt("color"),
1398
+ bg = opt("background");
1399
+ curve(rest[0], +rest[1], +rest[2], box, color, bg);
1400
+ } else if (cmd === "compare") {
1401
+ const out = opt("out");
1402
+ await compare(rest[0], rest[1], rest.slice(2), out);
1403
+ } else {
1404
+ /* usage = the header banner */
1405
+ const lines = readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n");
1406
+ console.log(lines.slice(1, lines.findIndex(l => l.includes("=== */")) + 1).join("\n"));
1407
+ process.exit(2);
1408
+ }