@crievo/omni 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (708) hide show
  1. package/.env.example +41 -0
  2. package/README.md +2 -2
  3. package/bin/omni.mjs +18 -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.tsx +59 -0
  247. package/studio/web/hooks/useAccounts.ts +27 -0
  248. package/studio/web/hooks/useAdmin.ts +12 -0
  249. package/studio/web/hooks/useAnalytics.ts +21 -0
  250. package/studio/web/hooks/useAvatarAudioReactive.ts +87 -0
  251. package/studio/web/hooks/useBrowserConnect.ts +48 -0
  252. package/studio/web/hooks/useClonePlan.ts +14 -0
  253. package/studio/web/hooks/useConnected.ts +7 -0
  254. package/studio/web/hooks/useLiveQuery.ts +29 -0
  255. package/studio/web/hooks/useMenuCollapsed.ts +32 -0
  256. package/studio/web/hooks/useMicDevices.ts +53 -0
  257. package/studio/web/hooks/useMissingDms.ts +211 -0
  258. package/studio/web/hooks/useMissingSetupSteps.ts +26 -0
  259. package/studio/web/hooks/useNow.ts +11 -0
  260. package/studio/web/hooks/useOmniAgent.ts +278 -0
  261. package/studio/web/hooks/useOmniClaude.ts +124 -0
  262. package/studio/web/hooks/useOmniDock.ts +142 -0
  263. package/studio/web/hooks/useOmniGreeting.ts +60 -0
  264. package/studio/web/hooks/useOmniHotword.ts +128 -0
  265. package/studio/web/hooks/useOmnichat.ts +26 -0
  266. package/studio/web/hooks/useOnboarding.ts +27 -0
  267. package/studio/web/hooks/usePost.ts +23 -0
  268. package/studio/web/hooks/useQueue.ts +10 -0
  269. package/studio/web/hooks/useReferences.ts +20 -0
  270. package/studio/web/hooks/useRuleEditor.ts +101 -0
  271. package/studio/web/hooks/useRunner.ts +24 -0
  272. package/studio/web/hooks/useSaveAction.ts +23 -0
  273. package/studio/web/hooks/useSchedule.ts +11 -0
  274. package/studio/web/hooks/useServices.ts +10 -0
  275. package/studio/web/hooks/useSessions.ts +15 -0
  276. package/studio/web/hooks/useSettings.ts +11 -0
  277. package/studio/web/hooks/useSettingsSection.ts +30 -0
  278. package/studio/web/hooks/useSidePanel.ts +23 -0
  279. package/studio/web/hooks/useSound.ts +30 -0
  280. package/studio/web/hooks/useUsage.ts +11 -0
  281. package/studio/web/hooks/useVideoVoice.ts +36 -0
  282. package/studio/web/hooks/useVideos.ts +21 -0
  283. package/studio/web/hooks/useVoiceProfilePreview.ts +52 -0
  284. package/studio/web/hooks/useVoiceProfiles.ts +7 -0
  285. package/studio/web/hooks/useVoiceRunning.ts +10 -0
  286. package/studio/web/hooks/useVoiceStudioClosed.ts +8 -0
  287. package/studio/web/index.html +19 -0
  288. package/studio/web/lib/agent.ts +7 -0
  289. package/studio/web/lib/alerts.ts +210 -0
  290. package/studio/web/lib/analytics-metrics.ts +351 -0
  291. package/studio/web/lib/api.ts +70 -0
  292. package/studio/web/lib/audio.ts +39 -0
  293. package/studio/web/lib/avatars.ts +41 -0
  294. package/studio/web/lib/calendar.ts +218 -0
  295. package/studio/web/lib/caption-position.ts +249 -0
  296. package/studio/web/lib/carousel.ts +24 -0
  297. package/studio/web/lib/chime.ts +42 -0
  298. package/studio/web/lib/clone-voice.ts +55 -0
  299. package/studio/web/lib/export.ts +11 -0
  300. package/studio/web/lib/files.ts +8 -0
  301. package/studio/web/lib/format.ts +93 -0
  302. package/studio/web/lib/media.ts +29 -0
  303. package/studio/web/lib/mic-device.ts +72 -0
  304. package/studio/web/lib/missing-dms.ts +70 -0
  305. package/studio/web/lib/omni-cards.ts +42 -0
  306. package/studio/web/lib/omni-cascade.ts +1067 -0
  307. package/studio/web/lib/omni-greeting-audio.ts +24 -0
  308. package/studio/web/lib/omni-hotword.ts +231 -0
  309. package/studio/web/lib/omni-panel.ts +67 -0
  310. package/studio/web/lib/omni-realtime.ts +811 -0
  311. package/studio/web/lib/omni-ui.ts +402 -0
  312. package/studio/web/lib/omni-wait.ts +174 -0
  313. package/studio/web/lib/omnichat-api.ts +44 -0
  314. package/studio/web/lib/orb.ts +270 -0
  315. package/studio/web/lib/pcm-recorder.ts +115 -0
  316. package/studio/web/lib/platforms.ts +17 -0
  317. package/studio/web/lib/providers.ts +27 -0
  318. package/studio/web/lib/publish-draft.ts +52 -0
  319. package/studio/web/lib/publish-settings.ts +81 -0
  320. package/studio/web/lib/query-client.ts +122 -0
  321. package/studio/web/lib/references.ts +13 -0
  322. package/studio/web/lib/services.ts +40 -0
  323. package/studio/web/lib/sessions.ts +103 -0
  324. package/studio/web/lib/settings-categories.ts +58 -0
  325. package/studio/web/lib/sound-audio.ts +33 -0
  326. package/studio/web/lib/sound-timeline.ts +291 -0
  327. package/studio/web/lib/sse.ts +86 -0
  328. package/studio/web/lib/stage-history.ts +141 -0
  329. package/studio/web/lib/stages.ts +344 -0
  330. package/studio/web/lib/studio-offline.ts +60 -0
  331. package/studio/web/lib/upload.ts +24 -0
  332. package/studio/web/lib/video-tabs.ts +118 -0
  333. package/studio/web/lib/voice-profile-draft.ts +34 -0
  334. package/studio/web/lib/voice-profiles.ts +3 -0
  335. package/studio/web/main.tsx +14 -0
  336. package/studio/web/pages/AdminPage/index.tsx +123 -0
  337. package/studio/web/pages/AgentPage/components/Measurements.tsx +102 -0
  338. package/studio/web/pages/AgentPage/index.tsx +28 -0
  339. package/studio/web/pages/AnalyticsPage/components/About.tsx +52 -0
  340. package/studio/web/pages/AnalyticsPage/components/ChannelCards.tsx +106 -0
  341. package/studio/web/pages/AnalyticsPage/components/ChannelView.tsx +137 -0
  342. package/studio/web/pages/AnalyticsPage/components/CollectionStatus.tsx +46 -0
  343. package/studio/web/pages/AnalyticsPage/components/Comparison.tsx +68 -0
  344. package/studio/web/pages/AnalyticsPage/components/Diagnosis.tsx +50 -0
  345. package/studio/web/pages/AnalyticsPage/components/Highlights.tsx +49 -0
  346. package/studio/web/pages/AnalyticsPage/components/Hint.tsx +11 -0
  347. package/studio/web/pages/AnalyticsPage/components/HookVsBody.tsx +100 -0
  348. package/studio/web/pages/AnalyticsPage/components/HourlyWave.tsx +133 -0
  349. package/studio/web/pages/AnalyticsPage/components/Insights.tsx +70 -0
  350. package/studio/web/pages/AnalyticsPage/components/MetricCard.tsx +46 -0
  351. package/studio/web/pages/AnalyticsPage/components/PlanSection.tsx +40 -0
  352. package/studio/web/pages/AnalyticsPage/components/ProgressionChart.tsx +155 -0
  353. package/studio/web/pages/AnalyticsPage/components/ReelBarsChart.tsx +154 -0
  354. package/studio/web/pages/AnalyticsPage/components/ReelThumb.tsx +19 -0
  355. package/studio/web/pages/AnalyticsPage/components/ReelView.tsx +290 -0
  356. package/studio/web/pages/AnalyticsPage/components/ReelsTable.tsx +269 -0
  357. package/studio/web/pages/AnalyticsPage/components/RequestWithFocus.tsx +38 -0
  358. package/studio/web/pages/AnalyticsPage/components/SegmentedControl.tsx +35 -0
  359. package/studio/web/pages/AnalyticsPage/components/VsMedian.tsx +31 -0
  360. package/studio/web/pages/AnalyticsPage/index.tsx +17 -0
  361. package/studio/web/pages/GettingStartedPage/index.tsx +174 -0
  362. package/studio/web/pages/NewVideoPage/components/DeliverablesPicker.tsx +62 -0
  363. package/studio/web/pages/NewVideoPage/components/FormatPicker.tsx +33 -0
  364. package/studio/web/pages/NewVideoPage/index.tsx +133 -0
  365. package/studio/web/pages/NotFoundPage/index.tsx +9 -0
  366. package/studio/web/pages/OmnichatPage/components/NewFromLink.tsx +59 -0
  367. package/studio/web/pages/OmnichatPage/components/RuleListItem.tsx +64 -0
  368. package/studio/web/pages/OmnichatPage/components/RulePreview.tsx +14 -0
  369. package/studio/web/pages/OmnichatPage/components/RuleView.tsx +88 -0
  370. package/studio/web/pages/OmnichatPage/index.tsx +153 -0
  371. package/studio/web/pages/QueuePage/index.tsx +22 -0
  372. package/studio/web/pages/ReferencePage/components/ReferenceTexts.tsx +131 -0
  373. package/studio/web/pages/ReferencePage/index.tsx +216 -0
  374. package/studio/web/pages/ReferencesPage/components/NewReferenceForm.tsx +150 -0
  375. package/studio/web/pages/ReferencesPage/components/ReferenceCard.tsx +42 -0
  376. package/studio/web/pages/ReferencesPage/components/ReferenceGridSkeleton.tsx +31 -0
  377. package/studio/web/pages/ReferencesPage/index.tsx +67 -0
  378. package/studio/web/pages/SchedulePage/components/CalendarSkeleton.tsx +18 -0
  379. package/studio/web/pages/SchedulePage/components/DayView.tsx +177 -0
  380. package/studio/web/pages/SchedulePage/components/HourPicker.tsx +103 -0
  381. package/studio/web/pages/SchedulePage/components/MonthDayCell.tsx +111 -0
  382. package/studio/web/pages/SchedulePage/components/MonthView.tsx +47 -0
  383. package/studio/web/pages/SchedulePage/components/PlatformIcons.tsx +12 -0
  384. package/studio/web/pages/SchedulePage/components/PostThumb.tsx +15 -0
  385. package/studio/web/pages/SchedulePage/components/WeekColumn.tsx +75 -0
  386. package/studio/web/pages/SchedulePage/components/WeekView.tsx +35 -0
  387. package/studio/web/pages/SchedulePage/index.tsx +285 -0
  388. package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +263 -0
  389. package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +20 -0
  390. package/studio/web/pages/SettingsPage/components/ModelPicker.tsx +49 -0
  391. package/studio/web/pages/SettingsPage/components/ModelsSettings.tsx +138 -0
  392. package/studio/web/pages/SettingsPage/components/PlatformsCategory.tsx +23 -0
  393. package/studio/web/pages/SettingsPage/components/PublishingCategory/AccountStatus.tsx +29 -0
  394. package/studio/web/pages/SettingsPage/components/PublishingCategory/AutoPublishPanel.tsx +90 -0
  395. package/studio/web/pages/SettingsPage/components/PublishingCategory/BrowserConnect.tsx +62 -0
  396. package/studio/web/pages/SettingsPage/components/PublishingCategory/CheckField.tsx +19 -0
  397. package/studio/web/pages/SettingsPage/components/PublishingCategory/InstagramPanel.tsx +113 -0
  398. package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +146 -0
  399. package/studio/web/pages/SettingsPage/components/PublishingCategory/SaveBar.tsx +33 -0
  400. package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +132 -0
  401. package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCard.tsx +47 -0
  402. package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCardGroup.tsx +19 -0
  403. package/studio/web/pages/SettingsPage/components/PublishingCategory/TextPanel.tsx +55 -0
  404. package/studio/web/pages/SettingsPage/components/PublishingCategory/TiktokPanel.tsx +123 -0
  405. package/studio/web/pages/SettingsPage/components/PublishingCategory/YoutubePanel.tsx +100 -0
  406. package/studio/web/pages/SettingsPage/components/PublishingCategory/badges.tsx +15 -0
  407. package/studio/web/pages/SettingsPage/components/PublishingCategory/index.tsx +40 -0
  408. package/studio/web/pages/SettingsPage/components/PublishingCategory/types.ts +10 -0
  409. package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +389 -0
  410. package/studio/web/pages/SettingsPage/components/VoiceProvidersCategory.tsx +97 -0
  411. package/studio/web/pages/SettingsPage/index.tsx +85 -0
  412. package/studio/web/pages/VideoPage/components/AudioPlayer.tsx +113 -0
  413. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/AxisField.tsx +50 -0
  414. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionList.tsx +56 -0
  415. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionListItem.tsx +92 -0
  416. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionMargins.tsx +57 -0
  417. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionStyleFields.tsx +70 -0
  418. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionTextForm.tsx +87 -0
  419. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/ColorField.tsx +37 -0
  420. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/MarginField.tsx +59 -0
  421. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/NumberField.tsx +57 -0
  422. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/PositionControls.tsx +140 -0
  423. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/captionEditor.ts +215 -0
  424. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/useCaptionPosition.ts +154 -0
  425. package/studio/web/pages/VideoPage/components/CarouselChangeRequest.tsx +72 -0
  426. package/studio/web/pages/VideoPage/components/CarouselCompositionStage.tsx +80 -0
  427. package/studio/web/pages/VideoPage/components/CarouselCreate.tsx +63 -0
  428. package/studio/web/pages/VideoPage/components/CarouselMissing.tsx +19 -0
  429. package/studio/web/pages/VideoPage/components/CarouselPublishStage.tsx +173 -0
  430. package/studio/web/pages/VideoPage/components/CarouselScriptStage.tsx +82 -0
  431. package/studio/web/pages/VideoPage/components/CarouselSlideCard.tsx +49 -0
  432. package/studio/web/pages/VideoPage/components/CarouselViewer.tsx +55 -0
  433. package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +53 -0
  434. package/studio/web/pages/VideoPage/components/CompositionStage.tsx +78 -0
  435. package/studio/web/pages/VideoPage/components/CoverPicker.tsx +32 -0
  436. package/studio/web/pages/VideoPage/components/EditorStage.tsx +53 -0
  437. package/studio/web/pages/VideoPage/components/LineTakes.tsx +160 -0
  438. package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +124 -0
  439. package/studio/web/pages/VideoPage/components/LineVoicePicker.tsx +37 -0
  440. package/studio/web/pages/VideoPage/components/MediaImage.tsx +25 -0
  441. package/studio/web/pages/VideoPage/components/MotionScriptStage.tsx +39 -0
  442. package/studio/web/pages/VideoPage/components/NarrationStage.tsx +70 -0
  443. package/studio/web/pages/VideoPage/components/PublishForm.tsx +387 -0
  444. package/studio/web/pages/VideoPage/components/PublishStage.tsx +76 -0
  445. package/studio/web/pages/VideoPage/components/PublishText.tsx +149 -0
  446. package/studio/web/pages/VideoPage/components/ScenesApprovalPanel.tsx +278 -0
  447. package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +72 -0
  448. package/studio/web/pages/VideoPage/components/ScriptApprovalPanel.tsx +189 -0
  449. package/studio/web/pages/VideoPage/components/ScriptStage.tsx +17 -0
  450. package/studio/web/pages/VideoPage/components/SequencePlayer.tsx +146 -0
  451. package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +117 -0
  452. package/studio/web/pages/VideoPage/components/SoundTimeline/AreaSelection.tsx +74 -0
  453. package/studio/web/pages/VideoPage/components/SoundTimeline/BedPanel.tsx +94 -0
  454. package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +45 -0
  455. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionOverlay.tsx +44 -0
  456. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +32 -0
  457. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionsPanel.tsx +22 -0
  458. package/studio/web/pages/VideoPage/components/SoundTimeline/ClipBlock.tsx +111 -0
  459. package/studio/web/pages/VideoPage/components/SoundTimeline/ClipPanel.tsx +76 -0
  460. package/studio/web/pages/VideoPage/components/SoundTimeline/CutsPanel.tsx +44 -0
  461. package/studio/web/pages/VideoPage/components/SoundTimeline/EditorTabBar.tsx +73 -0
  462. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +46 -0
  463. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectItem.tsx +89 -0
  464. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectPanel.tsx +232 -0
  465. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectsRow.tsx +21 -0
  466. package/studio/web/pages/VideoPage/components/SoundTimeline/Lane.tsx +39 -0
  467. package/studio/web/pages/VideoPage/components/SoundTimeline/MarkerRow.tsx +40 -0
  468. package/studio/web/pages/VideoPage/components/SoundTimeline/MuteButton.tsx +36 -0
  469. package/studio/web/pages/VideoPage/components/SoundTimeline/NumberInput.tsx +37 -0
  470. package/studio/web/pages/VideoPage/components/SoundTimeline/PlanWarnings.tsx +19 -0
  471. package/studio/web/pages/VideoPage/components/SoundTimeline/PlayerResizeHandle.tsx +50 -0
  472. package/studio/web/pages/VideoPage/components/SoundTimeline/PreviewControls.tsx +51 -0
  473. package/studio/web/pages/VideoPage/components/SoundTimeline/SceneRow.tsx +54 -0
  474. package/studio/web/pages/VideoPage/components/SoundTimeline/SettingsPanel.tsx +65 -0
  475. package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +99 -0
  476. package/studio/web/pages/VideoPage/components/SoundTimeline/SoundPreview.tsx +82 -0
  477. package/studio/web/pages/VideoPage/components/SoundTimeline/SpeedPanel.tsx +128 -0
  478. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineDock.tsx +56 -0
  479. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +26 -0
  480. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineSkeleton.tsx +21 -0
  481. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineToolbar.tsx +160 -0
  482. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineTracks.tsx +121 -0
  483. package/studio/web/pages/VideoPage/components/SoundTimeline/TrackSidebar.tsx +30 -0
  484. package/studio/web/pages/VideoPage/components/SoundTimeline/VoiceRow.tsx +53 -0
  485. package/studio/web/pages/VideoPage/components/SoundTimeline/Waveform.tsx +97 -0
  486. package/studio/web/pages/VideoPage/components/SoundTimeline/clipActions.ts +50 -0
  487. package/studio/web/pages/VideoPage/components/SoundTimeline/effectActions.ts +161 -0
  488. package/studio/web/pages/VideoPage/components/SoundTimeline/index.tsx +161 -0
  489. package/studio/web/pages/VideoPage/components/SoundTimeline/timelineGeometry.ts +45 -0
  490. package/studio/web/pages/VideoPage/components/SoundTimeline/types.ts +43 -0
  491. package/studio/web/pages/VideoPage/components/SoundTimeline/useAutosave.ts +107 -0
  492. package/studio/web/pages/VideoPage/components/SoundTimeline/useEditorShortcuts.ts +168 -0
  493. package/studio/web/pages/VideoPage/components/SoundTimeline/useFitHeight.ts +40 -0
  494. package/studio/web/pages/VideoPage/components/SoundTimeline/useFollowPlayhead.ts +21 -0
  495. package/studio/web/pages/VideoPage/components/SoundTimeline/useListenMix.ts +43 -0
  496. package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayback.ts +231 -0
  497. package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayerWidth.ts +27 -0
  498. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundData.ts +124 -0
  499. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +511 -0
  500. package/studio/web/pages/VideoPage/components/StageDrawer/CompareVersions.tsx +111 -0
  501. package/studio/web/pages/VideoPage/components/StageDrawer/VersionCard.tsx +81 -0
  502. package/studio/web/pages/VideoPage/components/StageDrawer/VersionMedia.tsx +24 -0
  503. package/studio/web/pages/VideoPage/components/StageDrawer/VersionText.tsx +15 -0
  504. package/studio/web/pages/VideoPage/components/StageDrawer/index.tsx +109 -0
  505. package/studio/web/pages/VideoPage/components/StageProcess.tsx +126 -0
  506. package/studio/web/pages/VideoPage/components/StageResult.tsx +69 -0
  507. package/studio/web/pages/VideoPage/components/SwapReference.tsx +92 -0
  508. package/studio/web/pages/VideoPage/components/ThumbnailPanel.tsx +252 -0
  509. package/studio/web/pages/VideoPage/components/TimingPanel.tsx +89 -0
  510. package/studio/web/pages/VideoPage/components/VideoPlayer.tsx +18 -0
  511. package/studio/web/pages/VideoPage/components/VideoSkeleton.tsx +50 -0
  512. package/studio/web/pages/VideoPage/components/VideoVoiceInfo.tsx +33 -0
  513. package/studio/web/pages/VideoPage/components/VideoVoicePicker.tsx +165 -0
  514. package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +56 -0
  515. package/studio/web/pages/VideoPage/components/WithLogo.tsx +10 -0
  516. package/studio/web/pages/VideoPage/index.tsx +295 -0
  517. package/studio/web/pages/VideosPage/components/PublishedOn.tsx +85 -0
  518. package/studio/web/pages/VideosPage/components/ServicesPanel.tsx +81 -0
  519. package/studio/web/pages/VideosPage/components/SessionsPanel.tsx +62 -0
  520. package/studio/web/pages/VideosPage/components/VideoListSkeleton.tsx +25 -0
  521. package/studio/web/pages/VideosPage/components/VideoRow.tsx +122 -0
  522. package/studio/web/pages/VideosPage/index.tsx +134 -0
  523. package/studio/web/pages/VoicePage/index.tsx +28 -0
  524. package/studio/web/router.tsx +201 -0
  525. package/studio/web/styles/base/elements.css +55 -0
  526. package/studio/web/styles/base/utilities.css +25 -0
  527. package/studio/web/styles/components/badge.css +33 -0
  528. package/studio/web/styles/components/claude-running.css +27 -0
  529. package/studio/web/styles/components/claude-session.css +212 -0
  530. package/studio/web/styles/components/claude-walking.css +92 -0
  531. package/studio/web/styles/components/clone-voice-save.css +136 -0
  532. package/studio/web/styles/components/clone-voice.css +379 -0
  533. package/studio/web/styles/components/controls.css +70 -0
  534. package/studio/web/styles/components/data-and-image.css +54 -0
  535. package/studio/web/styles/components/forms.css +132 -0
  536. package/studio/web/styles/components/human-banner.css +99 -0
  537. package/studio/web/styles/components/lightbox.css +50 -0
  538. package/studio/web/styles/components/markdown.css +68 -0
  539. package/studio/web/styles/components/modal.css +70 -0
  540. package/studio/web/styles/components/omni-cards.css +105 -0
  541. package/studio/web/styles/components/omni.css +859 -0
  542. package/studio/web/styles/components/omnichat.css +538 -0
  543. package/studio/web/styles/components/panel.css +115 -0
  544. package/studio/web/styles/components/platform-card.css +140 -0
  545. package/studio/web/styles/components/provider-card.css +132 -0
  546. package/studio/web/styles/components/queue.css +185 -0
  547. package/studio/web/styles/components/reference-actions.css +133 -0
  548. package/studio/web/styles/components/reference-picker.css +146 -0
  549. package/studio/web/styles/components/results-drawer.css +224 -0
  550. package/studio/web/styles/components/segmented.css +115 -0
  551. package/studio/web/styles/components/side-panel-tab.css +23 -0
  552. package/studio/web/styles/components/status-signals.css +66 -0
  553. package/studio/web/styles/components/stepper.css +208 -0
  554. package/studio/web/styles/components/table.css +19 -0
  555. package/studio/web/styles/components/tabs.css +24 -0
  556. package/studio/web/styles/components/ui/accordion.css +71 -0
  557. package/studio/web/styles/components/ui/chart.css +162 -0
  558. package/studio/web/styles/components/ui/checkbox.css +37 -0
  559. package/studio/web/styles/components/ui/date-picker.css +153 -0
  560. package/studio/web/styles/components/ui/select.css +110 -0
  561. package/studio/web/styles/components/ui/skeleton.css +25 -0
  562. package/studio/web/styles/components/ui/slider.css +81 -0
  563. package/studio/web/styles/components/ui/switch.css +41 -0
  564. package/studio/web/styles/components/ui/tooltip.css +24 -0
  565. package/studio/web/styles/components/voice-carousel.css +255 -0
  566. package/studio/web/styles/components/voice-editor.css +179 -0
  567. package/studio/web/styles/components/voice-grid.css +56 -0
  568. package/studio/web/styles/components/voice-profiles.css +192 -0
  569. package/studio/web/styles/design-system.md +136 -0
  570. package/studio/web/styles/index.css +116 -0
  571. package/studio/web/styles/layout/page.css +41 -0
  572. package/studio/web/styles/layout/sidebar-groups.css +39 -0
  573. package/studio/web/styles/layout/sidebar.css +111 -0
  574. package/studio/web/styles/pages/agent.css +26 -0
  575. package/studio/web/styles/pages/analytics/dashboard.css +614 -0
  576. package/studio/web/styles/pages/analytics/insights.css +48 -0
  577. package/studio/web/styles/pages/getting-started.css +166 -0
  578. package/studio/web/styles/pages/new-video/form.css +32 -0
  579. package/studio/web/styles/pages/not-found.css +59 -0
  580. package/studio/web/styles/pages/references.css +204 -0
  581. package/studio/web/styles/pages/schedule/calendar.css +541 -0
  582. package/studio/web/styles/pages/schedule/chip-states.css +60 -0
  583. package/studio/web/styles/pages/settings/models.css +72 -0
  584. package/studio/web/styles/pages/settings/shell.css +361 -0
  585. package/studio/web/styles/pages/settings/usage.css +63 -0
  586. package/studio/web/styles/pages/studio-offline.css +105 -0
  587. package/studio/web/styles/pages/video/caption-controls.css +194 -0
  588. package/studio/web/styles/pages/video/captions-grid.css +70 -0
  589. package/studio/web/styles/pages/video/captions.css +79 -0
  590. package/studio/web/styles/pages/video/editor-chat.css +103 -0
  591. package/studio/web/styles/pages/video/export.css +50 -0
  592. package/studio/web/styles/pages/video/frames.css +29 -0
  593. package/studio/web/styles/pages/video/fullscreen-stage.css +400 -0
  594. package/studio/web/styles/pages/video/instagram-carousel.css +309 -0
  595. package/studio/web/styles/pages/video/layout.css +285 -0
  596. package/studio/web/styles/pages/video/narration.css +138 -0
  597. package/studio/web/styles/pages/video/preview-overview.css +11 -0
  598. package/studio/web/styles/pages/video/processing-notice.css +34 -0
  599. package/studio/web/styles/pages/video/publish-when.css +76 -0
  600. package/studio/web/styles/pages/video/publish.css +97 -0
  601. package/studio/web/styles/pages/video/result.css +96 -0
  602. package/studio/web/styles/pages/video/scenes.css +167 -0
  603. package/studio/web/styles/pages/video/sound-timeline.css +672 -0
  604. package/studio/web/styles/pages/video/sound-toolbar.css +149 -0
  605. package/studio/web/styles/pages/video/take-player.css +100 -0
  606. package/studio/web/styles/pages/video/thumbnail.css +157 -0
  607. package/studio/web/styles/pages/video/timeline.css +117 -0
  608. package/studio/web/styles/pages/video/voice-picker.css +14 -0
  609. package/studio/web/styles/pages/video-list/active-row.css +27 -0
  610. package/studio/web/styles/pages/video-list/card-link.css +49 -0
  611. package/studio/web/styles/pages/video-list/list.css +86 -0
  612. package/studio/web/styles/theme/identity/details.css +204 -0
  613. package/studio/web/styles/theme/identity/shell.css +249 -0
  614. package/studio/web/styles/theme/identity/surfaces.css +183 -0
  615. package/studio/web/styles/theme/identity/titles-and-buttons.css +120 -0
  616. package/studio/web/styles/theme/no-borders/blocks.css +208 -0
  617. package/studio/web/styles/theme/no-borders/controls.css +256 -0
  618. package/studio/web/styles/tokens.css +245 -0
  619. package/studio/web/types/api.ts +2 -0
  620. package/studio/web/types/assets.d.ts +9 -0
  621. package/templates/brief.md +27 -0
  622. package/templates/captions.html +204 -0
  623. package/templates/carousel-composition.md +47 -0
  624. package/templates/carousel-script.md +60 -0
  625. package/templates/components/voiceavatar/README.md +87 -0
  626. package/templates/components/voiceavatar/demo.html +50 -0
  627. package/templates/components/voiceavatar/voiceavatar.js +415 -0
  628. package/templates/motion.md +80 -0
  629. package/templates/playbook.md +43 -0
  630. package/templates/reference.md +123 -0
  631. package/templates/scene.html +112 -0
  632. package/templates/scenes.example.json +96 -0
  633. package/templates/script.md +63 -0
  634. package/templates/slide.html +71 -0
  635. package/templates/wireframe.html +91 -0
  636. package/tools/analysis.mjs +300 -0
  637. package/tools/approval.mjs +442 -0
  638. package/tools/assemble.mjs +942 -0
  639. package/tools/build-motion.mjs +437 -0
  640. package/tools/captions.mjs +246 -0
  641. package/tools/carousel.mjs +558 -0
  642. package/tools/compose.mjs +307 -0
  643. package/tools/cuts.mjs +74 -0
  644. package/tools/editor.mjs +419 -0
  645. package/tools/epidemic-capture.js +54 -0
  646. package/tools/fanout.mjs +69 -0
  647. package/tools/history.mjs +70 -0
  648. package/tools/history.py +61 -0
  649. package/tools/job-queue.mjs +180 -0
  650. package/tools/job_queue.py +430 -0
  651. package/tools/lib/agents.mjs +412 -0
  652. package/tools/lib/channel.mjs +399 -0
  653. package/tools/lib/claude-settings.mjs +67 -0
  654. package/tools/lib/cli.mjs +9 -0
  655. package/tools/lib/fanout.mjs +234 -0
  656. package/tools/lib/http.mjs +94 -0
  657. package/tools/lib/index.js +14 -0
  658. package/tools/lib/mcp-server.mjs +69 -0
  659. package/tools/lib/metrics/metrics.mjs +204 -0
  660. package/tools/lib/narration.mjs +15 -0
  661. package/tools/lib/oauth-client.mjs +295 -0
  662. package/tools/lib/omnichat/config.mjs +139 -0
  663. package/tools/lib/omnichat/errors.mjs +42 -0
  664. package/tools/lib/omnichat/extract.mjs +82 -0
  665. package/tools/lib/omnichat/gate.mjs +151 -0
  666. package/tools/lib/omnichat/manual.mjs +166 -0
  667. package/tools/lib/omnichat/match.mjs +78 -0
  668. package/tools/lib/omnichat/pass.mjs +393 -0
  669. package/tools/lib/omnichat/record.mjs +208 -0
  670. package/tools/lib/omnichat/text.mjs +315 -0
  671. package/tools/lib/omnichat/webhook.mjs +166 -0
  672. package/tools/lib/pairing.mjs +99 -0
  673. package/tools/lib/paths.mjs +41 -0
  674. package/tools/lib/platforms/instagram.mjs +469 -0
  675. package/tools/lib/platforms/tiktok.mjs +343 -0
  676. package/tools/lib/platforms/youtube.mjs +228 -0
  677. package/tools/lib/publish/publish.mjs +244 -0
  678. package/tools/lib/schedule/schedule.mjs +191 -0
  679. package/tools/lib/service.mjs +134 -0
  680. package/tools/lib/skill-client.mjs +126 -0
  681. package/tools/lib/skills-release.mjs +196 -0
  682. package/tools/lib/testing.mjs +52 -0
  683. package/tools/lib/up.mjs +187 -0
  684. package/tools/metrics.mjs +415 -0
  685. package/tools/models.mjs +134 -0
  686. package/tools/narrate.mjs +257 -0
  687. package/tools/omni.mjs +298 -0
  688. package/tools/omni_paths.py +12 -0
  689. package/tools/omnichat.mjs +1577 -0
  690. package/tools/omnivideo.py +1122 -0
  691. package/tools/package.mjs +130 -0
  692. package/tools/platforms.mjs +236 -0
  693. package/tools/premiere.mjs +533 -0
  694. package/tools/publish.mjs +1496 -0
  695. package/tools/reference.mjs +1408 -0
  696. package/tools/render.mjs +426 -0
  697. package/tools/runner.mjs +156 -0
  698. package/tools/setup.mjs +466 -0
  699. package/tools/skills-release.mjs +93 -0
  700. package/tools/sound.mjs +1323 -0
  701. package/tools/soundtrack.mjs +217 -0
  702. package/tools/thumbnail.mjs +271 -0
  703. package/tools/timing.mjs +108 -0
  704. package/tools/usage.py +628 -0
  705. package/tools/voice-envelope.mjs +185 -0
  706. package/tools/voice.py +448 -0
  707. package/tools/voice_sample.py +75 -0
  708. package/tools/wireframe.mjs +230 -0
@@ -0,0 +1,1122 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic stages of omnivideo. Reads and writes videos/<slug>/scenes.json.
3
+
4
+ omnivideo.py status <video> which stage the video is at and which is next
5
+ omnivideo.py voice <video> generates new or changed lines (Fish Audio, Cartesia
6
+ or VoiceStudio, with fallback: tools/voice.py)
7
+ omnivideo.py timing <video> duration of each scene and where each line starts
8
+ omnivideo.py captions <video> time of each word (measured on the audio, no transcription)
9
+ and captions.srt
10
+
11
+ `voice` and `captions` accept `--scene cNN` to handle a single scene (what the
12
+ preview uses). `voice --regenerate-fallback` generates again, with the video's
13
+ provider, the lines that came out of a fallback. With `--scene`, `captions`
14
+ writes the word timings but not captions.srt, which only exists for the whole video.
15
+
16
+ Another reading of a line that came out bad, and the choice between readings:
17
+
18
+ omnivideo.py voice <video> --regenerate c03f1[,c05f1] generates again, even with the same text,
19
+ only those lines (with the video's provider)
20
+ omnivideo.py voice <video> --regenerate c03f1 --provider fish
21
+ generates that line with this provider (the others
22
+ stay as fallback); the choice holds for that line only
23
+ (`provider_pinned`) and survives a general `voice`
24
+ omnivideo.py voice <video> --use c03f1 <version> restores history/voice/c03f1/<version>/
25
+
26
+ The reading that is replaced always goes to history/voice/<line>/ (the most
27
+ recent `history.versions`): none is lost when swapping.
28
+
29
+ `status --json` gives the same diagnosis as data, for the studio and other
30
+ integrations. `voice` and `captions` wait for their turn in the machine queue
31
+ (tools/job_queue.py).
32
+
33
+ Run with the project's Python (.venv, created by node tools/setup.mjs), which has requests, soundfile and numpy:
34
+
35
+ .venv/bin/python tools/omnivideo.py voice videos/<slug>
36
+ """
37
+
38
+ import itertools
39
+ import json
40
+ import math
41
+ import re
42
+ import shutil
43
+ import subprocess
44
+ import sys
45
+ import threading
46
+ import time
47
+ from concurrent.futures import ThreadPoolExecutor
48
+ from datetime import datetime
49
+ from pathlib import Path
50
+
51
+ import usage
52
+ import voice
53
+ from history import keep_previous
54
+ from job_queue import turn
55
+ from omni_paths import DATA_ROOT as ROOT
56
+
57
+ CFG = json.loads((ROOT / "omnivideo.config.json").read_text())
58
+
59
+ WEAK = {
60
+ "de",
61
+ "da",
62
+ "do",
63
+ "das",
64
+ "dos",
65
+ "a",
66
+ "o",
67
+ "as",
68
+ "os",
69
+ "e",
70
+ "em",
71
+ "um",
72
+ "uma",
73
+ "que",
74
+ "com",
75
+ "pra",
76
+ "no",
77
+ "na",
78
+ "nos",
79
+ "nas",
80
+ "por",
81
+ "se",
82
+ "seu",
83
+ "sua",
84
+ "ao",
85
+ "à",
86
+ "às",
87
+ "é",
88
+ "ou",
89
+ "mas",
90
+ }
91
+
92
+ # stale line fields once the audio changes (`asr_similaridade`: legacy, from when captions transcribed)
93
+ STALE_LINE_KEYS = ("start", "end", "words", "asr_similarity", "asr_similaridade")
94
+
95
+
96
+ def fail(msg):
97
+ print(f"erro: {msg}", file=sys.stderr)
98
+ sys.exit(1)
99
+
100
+
101
+ def load(video):
102
+ path = video / "scenes.json"
103
+ if not path.exists():
104
+ fail(f"{path} não existe — a etapa de roteiro de motion ainda não rodou")
105
+ return json.loads(path.read_text())
106
+
107
+
108
+ def narrated(video):
109
+ """False: "Sem narração e voz" (content.json `narration: false`). The video skips the spoken script, the
110
+ narration, the voice and the captions: from the request to the motion script, the composition, the
111
+ render, the assembly and the editor, with each scene as long as its composition says."""
112
+ try:
113
+ return json.loads((Path(video) / "content.json").read_text()).get("narration") is not False
114
+ except (OSError, ValueError):
115
+ return True
116
+
117
+
118
+ def save(video, data):
119
+ tmp = video / "scenes.json.tmp"
120
+ tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
121
+ tmp.replace(video / "scenes.json")
122
+
123
+
124
+ def all_lines(data):
125
+ for scene in data["scenes"]:
126
+ for line in scene.get("lines", []):
127
+ yield scene, line
128
+
129
+
130
+ def norm(word):
131
+ return re.sub(r"[^\w]", "", word.lower())
132
+
133
+
134
+ def frame_ceil(seconds, fps):
135
+ return math.ceil(round(seconds * fps, 6)) / fps
136
+
137
+
138
+ def probe_duration(path):
139
+ out = subprocess.run(
140
+ ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", str(path)],
141
+ capture_output=True,
142
+ text=True,
143
+ )
144
+ try:
145
+ return float(out.stdout.strip())
146
+ except ValueError:
147
+ return None
148
+
149
+
150
+ # --- voice -------------------------------------------------------------------
151
+ # The providers (Fish Audio, Cartesia, VoiceStudio) live in tools/voice.py.
152
+
153
+
154
+ def with_first(chain, provider):
155
+ """The chain with `provider` in front (the others follow as fallback, in the same order).
156
+ Only a provider this video's voice configured is valid: it is the list of the screen's picker."""
157
+ if not provider:
158
+ return chain
159
+ found = next((p for p in chain if p.id == provider), None)
160
+ if found is None:
161
+ fail(
162
+ f"{voice.NAMES.get(provider, provider)} não está entre os providers desta voz: "
163
+ f"{', '.join(voice.NAMES[p.id] for p in chain)}"
164
+ )
165
+ return [found] + [p for p in chain if p is not found]
166
+
167
+
168
+ def pending_lines(video, data, chain, only, regenerate_fallback=False, regenerate=()):
169
+ """Lines without audio, with a different text or instruction, or whose requested
170
+ provider changed. A line generated by a fallback is not redone on its own (the
171
+ default being down would make every run generate everything again): only with
172
+ --regenerate-fallback. Those in `regenerate` (--regenerate) come out again even
173
+ when unchanged: another reading of the same text. With --regenerate, only them:
174
+ changing the video's provider does not drag the other lines along (a line's
175
+ button on the screen generates that line, not the whole video)."""
176
+ pref = chain[0].id
177
+ by_id = {p.id: p for p in chain}
178
+ pending = []
179
+ for scene, line in all_lines(data):
180
+ if only and scene["id"] != only:
181
+ continue
182
+ if regenerate and line["id"] not in regenerate:
183
+ continue
184
+ wav = video / "voice" / f"{line['id']}.wav"
185
+ if line["id"] in regenerate or not wav.exists() or not line.get("hash"):
186
+ pending.append(line)
187
+ continue
188
+ # line from before the providers: VoiceStudio, no provider change
189
+ used = line.get("provider", "voicestudio")
190
+ requested = line.get("provider_requested", used) if "provider" in line else pref
191
+ # `provider_pinned`: the line has its own provider (chosen on the screen), which the video's does not change;
192
+ # what the user chose (provider_pinned) is never redone on its own, not even by --regenerate-fallback
193
+ if not line.get("provider_pinned") and (requested != pref or (regenerate_fallback and used != requested)):
194
+ pending.append(line)
195
+ continue
196
+ d = voice.digest(by_id[used], line) if used in by_id else None
197
+ # VoiceStudio closed: the profile cannot be checked; the audio stays
198
+ if d is not None and d != line["hash"]:
199
+ pending.append(line)
200
+ return pending
201
+
202
+
203
+ def normalize_wav(src, dest):
204
+ """Fish and Cartesia stream WAV, sometimes with the wrong header size:
205
+ ffmpeg rewrites it whole, as 16-bit PCM."""
206
+ r = subprocess.run(
207
+ ["ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-i", str(src), "-c:a", "pcm_s16le", str(dest)],
208
+ capture_output=True,
209
+ text=True,
210
+ )
211
+ if r.returncode != 0:
212
+ raise voice.Failed(f"áudio inválido: {r.stderr.strip()[:200]}", disable=False)
213
+
214
+
215
+ RATE_LIMIT_TRIES = 4
216
+
217
+
218
+ def generate_line(chain, disabled, line, tmp, run=None):
219
+ """Tries the video's provider and then the fallbacks. A provider that fails on
220
+ key, credit or service is dropped for the rest of the run: the following lines
221
+ all come from the same voice. A 429 (too many lines at once, with the voice in
222
+ parallel) is not a failure of the provider: wait and try it again. `run`
223
+ (usage.Run) records what was generated and what failed."""
224
+ for prov in chain:
225
+ if prov.id in disabled:
226
+ continue
227
+ reason = prov.ready()
228
+ if reason:
229
+ print(f"⚠ {voice.NAMES[prov.id]} fora: {reason}")
230
+ if run:
231
+ run.failure(prov.id, line["id"], reason)
232
+ disabled.add(prov.id)
233
+ continue
234
+ for attempt in range(1, RATE_LIMIT_TRIES + 1):
235
+ try:
236
+ audio = prov.generate(line)
237
+ raw = tmp.with_suffix(".raw")
238
+ raw.write_bytes(audio)
239
+ if prov.id == "voicestudio":
240
+ raw.replace(tmp)
241
+ else:
242
+ normalize_wav(raw, tmp)
243
+ raw.unlink(missing_ok=True)
244
+ return prov
245
+ except voice.Failed as exc:
246
+ if exc.rate_limited and attempt < RATE_LIMIT_TRIES:
247
+ wait = 2 * attempt
248
+ print(f"⚠ {line['id']}: {voice.NAMES[prov.id]} pediu calma (429); de novo em {wait}s")
249
+ time.sleep(wait)
250
+ continue
251
+ print(f"⚠ {line['id']}: {exc}" + ("" if prov is chain[-1] else " → tentando o próximo provider"))
252
+ if run:
253
+ run.failure(prov.id, line["id"], exc)
254
+ if exc.disable:
255
+ disabled.add(prov.id)
256
+ break
257
+ fail(f"{line['id']}: nenhum provider gerou a fala ({', '.join(voice.NAMES[p.id] for p in chain)})")
258
+
259
+
260
+ def voice_groups(lines, provider):
261
+ """The lines split into equal groups, one per worker, generated at the same
262
+ time: `voice.parallel.lines_per_worker` lines each (12 lines at 4 each = 3
263
+ workers of 4), up to `max_workers`; when the cap is hit, the groups grow and
264
+ stay even (sizes differ by at most one). VoiceStudio runs on this machine and
265
+ generates one line at a time: one group."""
266
+ cfg = CFG["voice"].get("parallel") or {}
267
+ per = max(1, int(cfg.get("lines_per_worker", 4)))
268
+ cap = max(1, int(cfg.get("max_workers", 4)))
269
+ workers = 1 if provider == "voicestudio" else min(cap, math.ceil(len(lines) / per))
270
+ workers = max(1, min(workers, len(lines)))
271
+ size, extra = divmod(len(lines), workers)
272
+ groups, i = [], 0
273
+ for w in range(workers):
274
+ n = size + (1 if w < extra else 0)
275
+ groups.append(lines[i : i + n])
276
+ i += n
277
+ return groups
278
+
279
+
280
+ def cmd_voice(video, only=None, regenerate_fallback=False, regenerate=(), provider=None):
281
+ if not narrated(video):
282
+ fail("este vídeo é sem narração e voz (content.json): não há voz a gerar")
283
+ import requests
284
+ import soundfile as sf
285
+
286
+ data = load(video)
287
+ ids = {l["id"] for _, l in all_lines(data)}
288
+ if set(regenerate) - ids:
289
+ fail(f"fala que não existe: {', '.join(sorted(set(regenerate) - ids))}")
290
+ chain = with_first(voice.chain(requests, data), provider)
291
+ print(
292
+ f"voz: {voice.NAMES[chain[0].id]}"
293
+ + (f" (fallback: {', '.join(voice.NAMES[p.id] for p in chain[1:])})" if chain[1:] else "")
294
+ )
295
+ (video / "voice").mkdir(exist_ok=True)
296
+ if not pending_lines(video, data, chain, only, regenerate_fallback, regenerate):
297
+ print("todas as falas já estão geradas")
298
+ warn_fallback(data)
299
+ return
300
+
301
+ # generating voice takes the machine (VoiceStudio is local): wait for the turn in
302
+ # the queue. The wait may be long, and scenes.json is re-read on the turn so as
303
+ # not to overwrite what changed meanwhile
304
+ # usage.Run records each line and, on exit (with or without error), reads the platforms' balance
305
+ with turn("voice", video), usage.Run(video.name) as run:
306
+ data = load(video)
307
+ chain = with_first(voice.chain(requests, data), provider)
308
+ pending = pending_lines(video, data, chain, only, regenerate_fallback, regenerate)
309
+ if not pending:
310
+ print("todas as falas já estão geradas")
311
+ else:
312
+ run.before(chain[0].id)
313
+ disabled = set()
314
+ profile_id = (voice.voice_profile(data)[0] or {}).get("id")
315
+ groups = voice_groups(pending, chain[0].id) if pending else []
316
+ if len(groups) > 1:
317
+ print(f"{len(pending)} fala(s) em {len(groups)} grupos ao mesmo tempo: {', '.join(str(len(g)) for g in groups)}")
318
+ # generating runs in parallel; writing (voice/, history/, scenes.json, the usage log) one at a time
319
+ lock = threading.Lock()
320
+ count = itertools.count(1)
321
+
322
+ def one(line):
323
+ wav = video / "voice" / f"{line['id']}.wav"
324
+ tmp = wav.with_name(f".{wav.stem}.new.wav")
325
+ started = time.time()
326
+ prov = generate_line(chain, disabled, line, tmp, run)
327
+ model = getattr(prov, "model", None)
328
+ with lock:
329
+ digest = voice.digest(prov, line)
330
+ # this line's previous voice goes to history/voice/, with the text it
331
+ # said (scenes.json already has the new text)
332
+ sidecar = wav.with_suffix(".json")
333
+ with keep_previous(
334
+ video, "voice", line["id"], [str(wav.relative_to(video)), str(sidecar.relative_to(video))]
335
+ ):
336
+ tmp.replace(wav)
337
+ dur = sf.info(str(wav)).duration
338
+ sidecar.write_text(
339
+ json.dumps(
340
+ {
341
+ "text": line["text"],
342
+ "instruct": line.get("instruct"),
343
+ "hash": digest,
344
+ "provider": prov.id,
345
+ "profile": profile_id,
346
+ "dur": round(dur, 3),
347
+ },
348
+ ensure_ascii=False,
349
+ )
350
+ + "\n"
351
+ )
352
+ line.update(
353
+ wav=str(wav.relative_to(video)),
354
+ dur=round(dur, 3),
355
+ hash=digest,
356
+ provider=prov.id,
357
+ provider_requested=chain[0].id,
358
+ )
359
+ # the voice (profile) that said the line, for the player to show; with no profile registered, none
360
+ if profile_id:
361
+ line["voice_profile"] = profile_id
362
+ else:
363
+ line.pop("voice_profile", None)
364
+ for stale in STALE_LINE_KEYS:
365
+ line.pop(stale, None)
366
+ if line["id"] in regenerate:
367
+ # another reading with a chosen provider: it stays for this line; without a choice, back to the video's
368
+ if provider:
369
+ line["provider_pinned"] = True
370
+ else:
371
+ line.pop("provider_pinned", None)
372
+ save(video, data)
373
+ run.line(prov.id, line, dur, fallback=prov is not chain[0], model=model)
374
+ rate = len(line["text"].split()) / dur if dur else 0
375
+ flag = " ← ritmo estranho, ouvir" if not 2.2 <= rate <= 4.5 else ""
376
+ via = "" if prov is chain[0] else f" · via {voice.NAMES[prov.id]} (fallback)"
377
+ if prov.id == "fish" and not voice.is_free(model):
378
+ via += f" · {model} (pago)"
379
+ print(
380
+ f"[{next(count)}/{len(pending)}] {line['id']}: {dur:.2f}s de áudio em {time.time() - started:.0f}s "
381
+ f"({rate:.1f} palavras/s){via}{flag}",
382
+ flush=True,
383
+ )
384
+
385
+ def group(lines):
386
+ for line in lines:
387
+ one(line)
388
+
389
+ # a line no provider generated ends the run (fail → SystemExit): the other
390
+ # groups finish the line they are on, and the error comes out here
391
+ with ThreadPoolExecutor(max_workers=max(1, len(groups))) as pool:
392
+ for _ in pool.map(group, groups):
393
+ pass
394
+ warn_fallback(data)
395
+
396
+
397
+ def cmd_use(video, line_id, version):
398
+ """Restores a version kept in history/voice/<line>/<version>/ as the line's. The
399
+ one that was active goes to history in its place: swapping back is the same
400
+ command. Only a version that says the current text is valid."""
401
+ data = load(video)
402
+ line = next((l for _, l in all_lines(data) if l["id"] == line_id), None)
403
+ if not line:
404
+ fail(f"a fala {line_id} não existe no scenes.json")
405
+ src = video / "history" / "voice" / line_id / version
406
+ old_wav, old_sidecar = src / f"{line_id}.wav", src / f"{line_id}.json"
407
+ if not old_wav.exists():
408
+ fail(f"não há versão {version} da fala {line_id} em history/voice/")
409
+ if not old_sidecar.exists():
410
+ fail(f"a versão {version} não guardou o texto que dizia ({line_id}.json): não dá para conferir")
411
+ meta = json.loads(old_sidecar.read_text())
412
+ if meta.get("text") != line["text"] or meta.get("instruct") != line.get("instruct"):
413
+ fail(f"a versão {version} diz outro texto: “{meta.get('text')}”. A fala agora é “{line['text']}”")
414
+
415
+ wav = video / "voice" / f"{line_id}.wav"
416
+ sidecar = wav.with_suffix(".json")
417
+ # copy without the old mtime: status sees the export and captions as outdated
418
+ with keep_previous(video, "voice", line_id, [str(wav.relative_to(video)), str(sidecar.relative_to(video))]):
419
+ shutil.copyfile(old_wav, wav)
420
+ shutil.copyfile(old_sidecar, sidecar)
421
+ shutil.rmtree(src, ignore_errors=True)
422
+ dur = meta.get("dur") or probe_duration(wav)
423
+ line.update(
424
+ wav=str(wav.relative_to(video)),
425
+ dur=round(dur, 3),
426
+ hash=meta["hash"],
427
+ provider=meta.get("provider", "voicestudio"),
428
+ provider_requested=voice.preferred(data),
429
+ )
430
+ if meta.get("profile"):
431
+ line["voice_profile"] = meta["profile"]
432
+ else:
433
+ line.pop("voice_profile", None)
434
+ # a chosen version from another provider is a choice, not a fallback: no general `voice` undoes it
435
+ if line["provider"] != line["provider_requested"]:
436
+ line["provider_pinned"] = True
437
+ else:
438
+ line.pop("provider_pinned", None)
439
+ for stale in STALE_LINE_KEYS:
440
+ line.pop(stale, None)
441
+ save(video, data)
442
+ print(
443
+ f"{line_id}: agora usa a versão de {version} ({dur:.2f}s, {voice.NAMES.get(line['provider'], line['provider'])}); "
444
+ "a anterior ficou no historico. Refaça tempo, render, legenda e montagem."
445
+ )
446
+
447
+
448
+ def mixed_voices(data):
449
+ """Different providers nobody chose (fallback). A line with provider_pinned was
450
+ chosen by the user on the screen and is left out: it is not something to unify."""
451
+ used = {}
452
+ for _, line in all_lines(data):
453
+ if line.get("wav") and not line.get("provider_pinned"):
454
+ used.setdefault(line.get("provider", "voicestudio"), []).append(line["id"])
455
+ return used if len(used) > 1 else None
456
+
457
+
458
+ def warn_fallback(data):
459
+ mixed = mixed_voices(data)
460
+ if mixed:
461
+ print(
462
+ "⚠ vozes de providers diferentes neste vídeo: "
463
+ + "; ".join(f"{voice.NAMES.get(p, p)}: {', '.join(ids)}" for p, ids in mixed.items())
464
+ )
465
+ pref = voice.preferred(data)
466
+ fallback = [
467
+ l["id"]
468
+ for _, l in all_lines(data)
469
+ if l.get("wav") and l.get("provider", pref) != pref and not l.get("provider_pinned")
470
+ ]
471
+ if fallback:
472
+ print(
473
+ f"⚠ {len(fallback)} fala(s) saíram de fallback, não do {voice.NAMES[pref]}: {', '.join(fallback)}. "
474
+ f"Com ele de volta: `omnivideo.py voice <video> --regenerate-fallback`"
475
+ )
476
+
477
+
478
+ # --- timing ------------------------------------------------------------------
479
+
480
+
481
+ def cmd_timing_silent(video, data):
482
+ """No voice: a scene lasts as long as its composition says, the larger of `min_duration` and the last
483
+ beat's `t` plus the hold (a beat's `t` is in seconds, there are no words to tie it to)."""
484
+ t = CFG["timing"]
485
+ fps = data["format"]["fps"]
486
+ clock = 0.0
487
+ print(f"{'cena':<6}{'início':>8}{'duração':>9} (sem narração e voz)")
488
+ for scene in data["scenes"]:
489
+ beats = [b["t"] for b in scene.get("beats", []) if isinstance(b.get("t"), (int, float))]
490
+ if not beats and not scene.get("min_duration"):
491
+ fail(f"cena {scene['id']} sem `min_duration` nem beats com `t`: sem voz, é a composição que dá a duração")
492
+ length = max(scene.get("min_duration", 0.0), (max(beats) + scene.get("hold", t["final_hold"])) if beats else 0.0)
493
+ scene["duration"] = round(frame_ceil(length, fps), 4)
494
+ scene["start"] = round(clock, 4)
495
+ clock += scene["duration"]
496
+ print(f"{scene['id']:<6}{scene['start']:>8.2f}{scene['duration']:>9.2f}")
497
+ data["total_duration"] = round(clock, 4)
498
+ save(video, data)
499
+ print(f"total: {clock:.2f}s")
500
+
501
+
502
+ def cmd_timing(video, only=None):
503
+ data = load(video)
504
+ if not narrated(video):
505
+ return cmd_timing_silent(video, data)
506
+ t = CFG["timing"]
507
+ fps = data["format"]["fps"]
508
+ missing = [l["id"] for _, l in all_lines(data) if "dur" not in l]
509
+ if missing:
510
+ fail(f"falas sem áudio: {', '.join(missing)} — rode `omnivideo.py voice` antes")
511
+
512
+ clock = 0.0
513
+ print(f"{'cena':<6}{'início':>8}{'duração':>9}{'voz até':>9} falas")
514
+ for scene in data["scenes"]:
515
+ # a scene may have its own lead_in and hold: a sentence that continues into the
516
+ # next scene must not get the full silence of a cut
517
+ cursor = scene.get("lead_in", t["lead_in"])
518
+ for line in scene.get("lines", []):
519
+ line["start"] = round(frame_ceil(cursor, fps), 4)
520
+ line["end"] = round(line["start"] + line["dur"], 4)
521
+ cursor = line["end"] + t["pause_between_lines"]
522
+ voice_end = scene["lines"][-1]["end"] if scene.get("lines") else 0.0
523
+ length = max(voice_end + scene.get("hold", t["final_hold"]), scene.get("min_duration", 0.0))
524
+ scene["duration"] = round(frame_ceil(length, fps), 4)
525
+ scene["start"] = round(clock, 4)
526
+ clock += scene["duration"]
527
+ print(
528
+ f"{scene['id']:<6}{scene['start']:>8.2f}{scene['duration']:>9.2f}{voice_end:>9.2f} "
529
+ f"{len(scene.get('lines', []))}"
530
+ )
531
+ data["total_duration"] = round(clock, 4)
532
+ save(video, data)
533
+ print(f"total: {clock:.2f}s")
534
+
535
+
536
+ # --- captions ----------------------------------------------------------------
537
+
538
+
539
+ def word_weight(word):
540
+ """How long the word takes, relative to the others: its letters (measured against
541
+ Whisper, letters beat syllables). Numbers and acronyms weigh by what is said."""
542
+ w = norm(word)
543
+ if w.isdigit():
544
+ return 3 * len(w)
545
+ if word.strip(".,;:!?…").isupper() and len(w) > 1:
546
+ return 2.5 * len(w)
547
+ return max(1, len(w))
548
+
549
+
550
+ def voice_spans(wav):
551
+ """Where the voice starts and ends in the file, and the silences inside (≥ 0.12 s)."""
552
+ import numpy as np
553
+ import soundfile as sf
554
+
555
+ audio, sr = sf.read(str(wav), dtype="float32", always_2d=True)
556
+ audio = audio.mean(axis=1)
557
+ step = int(sr * 0.01)
558
+ n = max(1, len(audio) // step)
559
+ rms = np.sqrt(np.mean(audio[: n * step].reshape(n, step) ** 2, axis=1) + 1e-12)
560
+ # start and end with a low threshold (the opening consonant is weak); silence with a higher one
561
+ edge = rms > max(rms.max() * 0.015, np.percentile(rms, 10) * 2)
562
+ voiced = rms > max(rms.max() * 0.04, np.percentile(rms, 10) * 3)
563
+ if not edge.any():
564
+ return 0.0, len(audio) / sr, []
565
+ start, end = int(np.argmax(edge)), int(n - np.argmax(edge[::-1]))
566
+ silences, k = [], start
567
+ while k < end:
568
+ if not voiced[k]:
569
+ j = k
570
+ while j < end and not voiced[j]:
571
+ j += 1
572
+ if j - k >= 12:
573
+ silences.append((k * 0.01, j * 0.01))
574
+ k = j
575
+ else:
576
+ k += 1
577
+ return start * 0.01, end * 0.01, silences
578
+
579
+
580
+ PAUSE = {",": 1.5, ";": 1.5, ":": 1.5, ".": 2.5, "?": 2.5, "!": 2.5, "…": 2.5}
581
+
582
+
583
+ def estimate_words(wav, text):
584
+ """Time of each word without transcribing: the text is known, so all that is
585
+ left is splitting the spoken span of the audio. Each word weighs by its letters,
586
+ and commas and periods weigh a pause. A real silence near punctuation becomes an
587
+ anchor: the word ends where it starts and the next one enters where it ends."""
588
+ words = text.split()
589
+ if not words:
590
+ return []
591
+ start, end, silences = voice_spans(wav)
592
+ weight = [word_weight(w) for w in words]
593
+ pause = [PAUSE.get(w[-1], 0.0) if k < len(words) - 1 else 0.0 for k, w in enumerate(words)]
594
+ total = sum(weight) + sum(pause)
595
+
596
+ # where each pause would fall by the math; the nearest silence (up to 0.4 s), in order, anchors
597
+ anchors, cursor, used = {}, start, 0
598
+ for k in range(len(words)):
599
+ cursor += (end - start) * weight[k] / total
600
+ if pause[k]:
601
+ mid = cursor + (end - start) * pause[k] / total / 2
602
+ near = [(abs((s + e) / 2 - mid), i) for i, (s, e) in enumerate(silences) if i >= used]
603
+ if near and min(near)[0] <= 0.4:
604
+ i = min(near)[1]
605
+ anchors[k] = silences[i]
606
+ used = i + 1
607
+ cursor += (end - start) * pause[k] / total
608
+
609
+ # between anchors, split by weight
610
+ times, a, seg_start = [], 0, start
611
+ for b in sorted(anchors) + [len(words) - 1]:
612
+ seg_end = anchors[b][0] if b in anchors else end
613
+ p = weight[a : b + 1]
614
+ q = pause[a:b] + [0.0]
615
+ total_seg = sum(p) + sum(q)
616
+ t = seg_start
617
+ for k in range(a, b + 1):
618
+ dur = (seg_end - seg_start) * weight[k] / total_seg
619
+ times.append((t, t + dur))
620
+ t += dur + (seg_end - seg_start) * q[k - a] / total_seg
621
+ if b in anchors:
622
+ seg_start = anchors[b][1]
623
+ a = b + 1
624
+ if a >= len(words):
625
+ break
626
+ return [{"w": w, "s": round(s, 3), "e": round(e, 3)} for w, (s, e) in zip(words, times)]
627
+
628
+
629
+ def phrases(words, max_chars):
630
+ """Cuts at sentence ends and commas; joins short comma pieces."""
631
+ length = lambda ws: len(" ".join(w["w"] for w in ws))
632
+ parts, cur = [], []
633
+ for w in words:
634
+ cur.append(w)
635
+ if w["w"].endswith((".", "?", "!", "…", ",", ";", ":")):
636
+ parts.append(cur)
637
+ cur = []
638
+ if cur:
639
+ parts.append(cur)
640
+ merged = []
641
+ for part in parts:
642
+ prev = merged[-1] if merged else None
643
+ if (
644
+ prev
645
+ and prev[-1]["w"].endswith(",")
646
+ and (length(prev) < 12 or length(part) < 8)
647
+ and length(prev + part) <= max_chars
648
+ ):
649
+ merged[-1] = prev + part
650
+ else:
651
+ merged.append(part)
652
+ return merged
653
+
654
+
655
+ def balanced_split(words, max_chars):
656
+ length = lambda ws: len(" ".join(w["w"] for w in ws))
657
+ if length(words) <= max_chars:
658
+ return [words]
659
+ n = math.ceil(length(words) / max_chars)
660
+ best = None
661
+ while best is None or best[0] >= 10_000:
662
+ for cuts in itertools.combinations(range(1, len(words)), n - 1):
663
+ parts = [words[a:b] for a, b in zip((0,) + cuts, cuts + (len(words),))]
664
+ target = length(words) / n
665
+ cost = sum((length(p) - target) ** 2 for p in parts)
666
+ cost += sum(10_000 for p in parts if length(p) > max_chars)
667
+ cost += sum(2_000 for p in parts[:-1] if norm(p[-1]["w"]) in WEAK)
668
+ cost += sum(2_000 for p in parts[:-1] if norm(p[-1]["w"]).isdigit())
669
+ cost += sum(3_000 for p in parts if len(p) == 1)
670
+ if best is None or cost < best[0]:
671
+ best = (cost, parts)
672
+ if best[0] >= 10_000 and n < len(words):
673
+ n += 1
674
+ best = None
675
+ else:
676
+ break
677
+ return best[1]
678
+
679
+
680
+ def srt_time(seconds):
681
+ ms = int(round(seconds * 1000))
682
+ h, ms = divmod(ms, 3_600_000)
683
+ m, ms = divmod(ms, 60_000)
684
+ s, ms = divmod(ms, 1000)
685
+ return f"{h:02}:{m:02}:{s:02},{ms:03}"
686
+
687
+
688
+ def caption_text(words):
689
+ text = " ".join(w["w"] for w in words)
690
+ return text[:-1] if text.endswith((",", ".", ";", ":")) else text
691
+
692
+
693
+ def cmd_captions(video, only=None):
694
+ if not narrated(video):
695
+ fail("este vídeo é sem narração e voz (content.json): não há falas para legendar")
696
+ # what is missing shows up now, not after waiting for the turn
697
+ targets_of(load(video), only)
698
+ # it takes seconds, but goes through the queue: the studio takes the stage's history and log from it
699
+ with turn("captions", video):
700
+ write_captions(video, only)
701
+
702
+
703
+ def targets_of(data, only):
704
+ targets = [(s, l) for s, l in all_lines(data) if not only or s["id"] == only]
705
+ if not targets:
706
+ fail(f"nenhuma fala{' na cena ' + only if only else ''}")
707
+ if any("wav" not in l for _, l in targets):
708
+ fail("há falas sem áudio — rode `omnivideo.py voice` antes")
709
+ if not only and any("start" not in l or "start" not in s for s, l in targets):
710
+ fail("as falas ainda não têm posição — rode `omnivideo.py timing` antes")
711
+ return targets
712
+
713
+
714
+ def apply_texts(data, cues):
715
+ """Text edited on the screen (`captions.texts`, see tools/captions.mjs) goes back to
716
+ the caption the line still generates with the same `original` text: same id,
717
+ else the same line, else any one (the screen's id may have fallen on the
718
+ neighbouring line). An edit with no match is dropped, with a warning: the line changed."""
719
+ cap = data.get("captions") or {}
720
+ left = dict(cap.get("texts") or {})
721
+ if not left:
722
+ return
723
+ line_of = lambda i: i.rsplit(".", 1)[0]
724
+ rules = (lambda c, k: c[3] == k, lambda c, k: line_of(c[3]) == line_of(k), lambda c, k: True)
725
+ matched = {}
726
+ for matches in rules:
727
+ for key, ed in list(left.items()):
728
+ cue = next(
729
+ (c for c in cues if c[3] not in matched and c[2] == ed.get("original") and matches(c, key)), None
730
+ )
731
+ if cue:
732
+ cue[2] = ed["text"]
733
+ matched[cue[3]] = ed
734
+ del left[key]
735
+ for key, ed in left.items():
736
+ print(f"aviso: o texto editado de {key} (“{ed.get('text')}”) saiu: a fala não gera mais “{ed.get('original')}”")
737
+ if matched:
738
+ cap["texts"] = matched
739
+ print(f"{len(matched)} legenda(s) com o texto editado na tela")
740
+ else:
741
+ cap.pop("texts", None)
742
+ if not cap:
743
+ data.pop("captions", None)
744
+
745
+
746
+ def write_captions(video, only):
747
+ data = load(video)
748
+ cc = CFG["captions"]
749
+ targets = targets_of(data, only)
750
+ max_chars = cc["max_chars"][data["format"]["aspect"]]
751
+
752
+ cues = []
753
+ for scene, line in targets:
754
+ words = estimate_words(video / line["wav"], line["text"])
755
+ line["words"] = words
756
+ line.pop("asr_similarity", None) # from when captions transcribed
757
+ line.pop("asr_similaridade", None) # legacy name
758
+ if only:
759
+ continue
760
+ base = scene["start"] + line["start"]
761
+ chunks = [c for part in phrases(words, max_chars) for c in balanced_split(part, max_chars)]
762
+ for n, chunk in enumerate(chunks, 1):
763
+ # id `<line>.<n>`, the same as tools/captions.mjs
764
+ cues.append([base + chunk[0]["s"], base + chunk[-1]["e"], caption_text(chunk), f"{line['id']}.{n}"])
765
+
766
+ for k, cue in enumerate(cues):
767
+ nxt = cues[k + 1][0] if k + 1 < len(cues) else None
768
+ if nxt is not None and nxt - cue[1] < 0.35:
769
+ cue[1] = nxt
770
+ cue[1] = max(cue[1], cue[0] + cc["min_duration"])
771
+ if nxt is not None:
772
+ cue[1] = min(cue[1], nxt)
773
+
774
+ if only:
775
+ save(video, data)
776
+ print(f"{only}: tempo de cada palavra gravado em {len(targets)} fala(s); captions.srt não muda")
777
+ else:
778
+ apply_texts(data, cues)
779
+ srt = "\n".join(f"{i}\n{srt_time(a)} --> {srt_time(b)}\n{t}\n" for i, (a, b, t, _) in enumerate(cues, 1))
780
+ with keep_previous(video, "captions", None, ["captions.srt"]):
781
+ (video / "captions.srt").write_text(srt, encoding="utf-8")
782
+ save(video, data)
783
+ longest = max((len(c[2]) for c in cues), default=0)
784
+ print(f"captions.srt: {len(cues)} legendas, maior com {longest} caracteres (limite {max_chars})")
785
+
786
+
787
+ # --- status ------------------------------------------------------------------
788
+
789
+
790
+ def deadline_passed(a):
791
+ return (
792
+ a.get("mode") == "auto"
793
+ and time.time() >= datetime.fromisoformat(a["deadline"].replace("Z", "+00:00")).timestamp()
794
+ )
795
+
796
+
797
+ def scenes_approval(video, ids):
798
+ """Approval of the motion script's scenes (tools/approval.mjs). None: the video
799
+ has no round (made before it existed). A passed deadline in auto mode counts
800
+ as approved, as in approval.mjs, which is the writer."""
801
+ path = video / "motion" / "approval.json"
802
+ if not path.exists():
803
+ return None
804
+ a = json.loads(path.read_text())
805
+ scenes = a.get("scenes", {})
806
+ # round closed: a scene created afterwards (the composition split one) does not reopen the stage
807
+ if a.get("completed_at"):
808
+ ids = [i for i in ids if i in scenes]
809
+ passed = deadline_passed(a)
810
+ states = [(scenes.get(i) or {}).get("state", "pending") for i in ids]
811
+ states = ["approved" if s == "pending" and passed else s for s in states]
812
+ change = [i for i, s in zip(ids, states) if s == "change_requested"]
813
+ pending = [i for i, s in zip(ids, states) if s == "pending"]
814
+ if change and not pending:
815
+ return {"ok": False, "status": "change_requested", "detail": f"cenas pedem alteração: {', '.join(change)}"}
816
+ if pending:
817
+ who = "contagem pausada: o usuário está editando" if a.get("mode") != "auto" else "aprovam sozinhas no prazo"
818
+ return {"ok": False, "status": "pending", "detail": f"cenas esperando aprovação: {', '.join(pending)} ({who})"}
819
+ return {"ok": True, "status": "approved", "detail": None}
820
+
821
+
822
+ def script_approval(video):
823
+ """Approval of the script (tools/approval.mjs --script, script-approval.json).
824
+ None: the video has no round (made before it existed, or in the chat without one).
825
+ A passed deadline in auto mode counts as approved, as for the scenes."""
826
+ path = video / "script-approval.json"
827
+ if not path.exists():
828
+ return None
829
+ a = json.loads(path.read_text())
830
+ item = a.get("items", {}).get("script") or {}
831
+ state = item.get("state", "pending")
832
+ if state == "pending" and deadline_passed(a):
833
+ state = "approved"
834
+ if state == "change_requested":
835
+ return {"ok": False, "status": "change_requested", "detail": f"pede alteração: «{item.get('comment', '')}»"}
836
+ if state == "pending":
837
+ who = "contagem pausada: o usuário está editando" if a.get("mode") != "auto" else "aprova sozinho no prazo"
838
+ return {"ok": False, "status": "pending", "detail": f"esperando aprovação ({who})"}
839
+ return {"ok": True, "status": "approved", "detail": None}
840
+
841
+
842
+ # PT labels of `changed` values, for the human detail text
843
+ CHANGED_LABEL = {"captions": "legenda", "scenes": "cenas", "motion": "motion", "voice": "voz"}
844
+
845
+
846
+ def diagnose(video):
847
+ """What `status` says, as data: the studio and other integrations read this
848
+ instead of redoing the math."""
849
+ stages = []
850
+
851
+ def stage(id_, number, name, ok, detail=None, **extra):
852
+ stages.append({"id": id_, "number": number, "name": name, "ok": bool(ok), "detail": detail, **extra})
853
+
854
+ silent = not narrated(video)
855
+
856
+ def skipped(id_, number, name):
857
+ """"Sem narração e voz": the stage does not exist for this video; it counts as done and says why."""
858
+ stage(id_, number, name, True, "sem narração e voz", skipped=True)
859
+
860
+ has_script = (video / "script.md").exists()
861
+ script_appr = script_approval(video) if has_script else None
862
+ if silent:
863
+ skipped("script", "1", "roteiro")
864
+ else:
865
+ stage(
866
+ "script",
867
+ "1",
868
+ "roteiro",
869
+ has_script and (script_appr is None or script_appr["ok"]),
870
+ script_appr and script_appr["detail"],
871
+ approval=script_appr and script_appr["status"],
872
+ )
873
+ data = json.loads((video / "scenes.json").read_text()) if (video / "scenes.json").exists() else None
874
+ scenes = data["scenes"] if data else []
875
+ written = (video / "motion.md").exists() and data is not None
876
+ approval = scenes_approval(video, [s["id"] for s in scenes]) if written else None
877
+ stage(
878
+ "motion_script",
879
+ "2",
880
+ "roteiro de motion",
881
+ written and (approval is None or approval["ok"]),
882
+ approval and approval["detail"],
883
+ approval=approval and approval["status"],
884
+ )
885
+ lines = [l for s in scenes for l in s.get("lines", [])]
886
+ # narration and voice apply to the whole video: one scene with lines is not the stage done
887
+ is_narrated = bool(scenes) and all(s.get("lines") for s in scenes) and all(l.get("text") for l in lines)
888
+ voiced = is_narrated and all((video / l.get("wav", "-")).exists() for l in lines)
889
+ stage(
890
+ "motion",
891
+ "3",
892
+ "motion (stills e folha de contatos)",
893
+ bool(scenes) and all((video / s.get("html", f"motion/{s['id']}.html")).exists() for s in scenes),
894
+ )
895
+ if silent:
896
+ skipped("narration", "4", "narração")
897
+ else:
898
+ stage("narration", "4", "narração", is_narrated)
899
+ # the user is swapping voices on the screen: the voice is not decided, and nothing after it runs
900
+ pause_file = video / "voice-pause.json"
901
+ voice_pause = json.loads(pause_file.read_text()) if pause_file.exists() else None
902
+ if silent:
903
+ skipped("voice", "5", "voz")
904
+ else:
905
+ stage(
906
+ "voice",
907
+ "5",
908
+ "voz",
909
+ voiced and not voice_pause,
910
+ "em escolha na tela: o usuário está trocando vozes. Pare aqui; ele segue pelo botão "
911
+ '"Seguir com estas vozes" na etapa Voz'
912
+ if voice_pause
913
+ else None,
914
+ pause=voice_pause,
915
+ )
916
+ stage("timing", "5", "tempo", bool(scenes) and all("duration" in s and "start" in s for s in scenes))
917
+
918
+ shifted = []
919
+
920
+ def rendered(s):
921
+ mp4 = video / s.get("mp4", f"motion/{s['id']}.mp4")
922
+ d = probe_duration(mp4) if mp4.exists() else None
923
+ if d is None or abs(d - s.get("duration", -1)) > 1.5 / data["format"]["fps"]:
924
+ return False
925
+ # background on video time: the MP4 is valid only for the start it was rendered with
926
+ info = video / "motion" / f"{s['id']}.render.json"
927
+ if info.exists():
928
+ r = json.loads(info.read_text())
929
+ if r.get("video_time") and abs(r.get("start", -1) - s.get("start", -2)) > 1e-3:
930
+ shifted.append(s["id"])
931
+ return False
932
+ return True
933
+
934
+ ok_render = bool(scenes) and all(["duration" in s and rendered(s) for s in scenes])
935
+ stage(
936
+ "render",
937
+ "6",
938
+ "render final dos motions",
939
+ ok_render,
940
+ f"início mudou em {', '.join(shifted)}: o fundo pularia no corte" if shifted else None,
941
+ stale=shifted,
942
+ )
943
+ if silent:
944
+ skipped("captions", "7", "legenda")
945
+ else:
946
+ stage("captions", "7", "legenda", (video / "captions.srt").exists() and all("words" in l for l in lines))
947
+ # 8: assemble.mjs (ffmpeg) writes export/assembly.json; a video assembled earlier in
948
+ # Premiere (premiere.json with export) also counts
949
+ assembly = video / "export" / "assembly.json"
950
+ aj = json.loads(assembly.read_text()) if assembly.exists() else {}
951
+ pj = json.loads((video / "premiere.json").read_text()) if (video / "premiere.json").exists() else {}
952
+ # what changed after the export, for the screen to say (and the export-again button to know) what
953
+ inputs = {
954
+ "captions": [video / "captions.srt"],
955
+ "scenes": [video / "scenes.json"],
956
+ "motion": [video / s.get("mp4", f"motion/{s['id']}.mp4") for s in scenes],
957
+ "voice": [video / l["wav"] for l in lines if l.get("wav")],
958
+ }
959
+ if aj:
960
+ ready = (video / aj.get("mp4", "")).exists()
961
+ changed = [
962
+ k
963
+ for k, ps in inputs.items()
964
+ if ready and any(p.exists() and p.stat().st_mtime > assembly.stat().st_mtime for p in ps)
965
+ ]
966
+ outdated = bool(changed)
967
+ stage(
968
+ "export",
969
+ "8",
970
+ "montagem e exportação",
971
+ ready and not outdated,
972
+ f"desatualizada: {', '.join(CHANGED_LABEL[k] for k in changed)} mudou depois; rode assemble.mjs de novo"
973
+ if outdated
974
+ else None,
975
+ outdated=outdated,
976
+ changed=changed,
977
+ )
978
+ else:
979
+ legacy_export = bool(pj.get("export")) and Path(pj["export"]).exists()
980
+ stage(
981
+ "export", "8", "montagem e exportação", legacy_export, "exportado pelo Premiere" if legacy_export else None
982
+ )
983
+
984
+ warnings = []
985
+ if data and scenes:
986
+ library = Path(CFG["references"]["dir"])
987
+ refs = data.get("references") or []
988
+ if not refs:
989
+ warnings.append("scenes.json sem `references`: o estilo não saiu de um estilo visual")
990
+ for ref in refs:
991
+ if not (library / ref).is_dir():
992
+ warnings.append(f"estilo visual “{ref}” não existe em {library}")
993
+ warnings.extend(hard_lines(lines))
994
+ mixed = mixed_voices(data)
995
+ if mixed:
996
+ warnings.append(
997
+ "vozes de providers diferentes: "
998
+ + "; ".join(f"{voice.NAMES.get(p, p)} em {len(ids)} fala(s)" for p, ids in mixed.items())
999
+ )
1000
+ nxt = next((s for s in stages if not s["ok"]), None)
1001
+ return {
1002
+ "video": video.name,
1003
+ "dir": str(video),
1004
+ "stages": stages,
1005
+ "next": {"id": nxt["id"], "name": label(nxt)} if nxt else None,
1006
+ "ready": nxt is None,
1007
+ "total_duration": data["total_duration"] if data and "total_duration" in data else None,
1008
+ "scenes": len(scenes),
1009
+ "lines": len(lines),
1010
+ "warnings": warnings,
1011
+ }
1012
+
1013
+
1014
+ def label(stage):
1015
+ return f"{stage['name']} ({stage['detail']})" if stage["detail"] else stage["name"]
1016
+
1017
+
1018
+ def hard_lines(lines):
1019
+ """Punctuation the voice reads as an intonation break (reference/writing-for-voice.md)."""
1020
+ warnings = []
1021
+ for l in lines:
1022
+ t = l.get("text") or ""
1023
+ reasons = []
1024
+ if ":" in t:
1025
+ reasons.append("dois-pontos")
1026
+ if ";" in t:
1027
+ reasons.append("ponto e vírgula")
1028
+ if any(c in t for c in "—–()/"):
1029
+ reasons.append("travessão, parênteses ou barra")
1030
+ if "..." in t or "…" in t:
1031
+ reasons.append("reticências")
1032
+ if t.count(",") > 1:
1033
+ reasons.append(f"{t.count(',')} vírgulas")
1034
+ if re.search(r"\d+\.\d", t):
1035
+ reasons.append("número com ponto (versão?)")
1036
+ if reasons:
1037
+ warnings.append(
1038
+ f"fala {l.get('id')} difícil para a voz ({', '.join(reasons)}): reescreva (reference/writing-for-voice.md)"
1039
+ )
1040
+ return warnings
1041
+
1042
+
1043
+ def cmd_status(video, as_json=False):
1044
+ d = diagnose(video)
1045
+ if as_json:
1046
+ print(json.dumps(d, ensure_ascii=False, indent=2))
1047
+ return
1048
+ for s in d["stages"]:
1049
+ print(f"{'✓' if s['ok'] else '·'} {label(s)}")
1050
+ if d["total_duration"] is not None:
1051
+ print(f"duração total: {d['total_duration']:.2f}s em {d['scenes']} cenas, {d['lines']} falas")
1052
+ for w in d["warnings"]:
1053
+ print(f"⚠ {w}")
1054
+ print(f"próxima etapa: {d['next']['name']}" if d["next"] else "pronto")
1055
+
1056
+
1057
+ COMMANDS = {"status": cmd_status, "voice": cmd_voice, "timing": cmd_timing, "captions": cmd_captions}
1058
+
1059
+ if __name__ == "__main__":
1060
+ args = sys.argv[1:]
1061
+ as_json = "--json" in args
1062
+ if as_json:
1063
+ args.remove("--json")
1064
+ regenerate_fallback = "--regenerate-fallback" in args
1065
+ if regenerate_fallback:
1066
+ args.remove("--regenerate-fallback")
1067
+ regenerate = ()
1068
+ if "--regenerate" in args:
1069
+ i = args.index("--regenerate")
1070
+ regenerate = tuple(x for x in (args[i + 1] if i + 1 < len(args) else "").split(",") if x)
1071
+ args = args[:i] + args[i + 2 :]
1072
+ if not regenerate:
1073
+ fail("--regenerate precisa dos ids das falas (ex.: --regenerate c03f1,c05f1)")
1074
+ provider = None
1075
+ if "--provider" in args:
1076
+ i = args.index("--provider")
1077
+ provider = args[i + 1] if i + 1 < len(args) else None
1078
+ args = args[:i] + args[i + 2 :]
1079
+ if not provider:
1080
+ fail("--provider precisa do id do provider (ex.: --provider fish)")
1081
+ if not regenerate:
1082
+ fail("--provider só vale com --regenerate: é o provider daquela fala, não do vídeo")
1083
+ use = None
1084
+ if "--use" in args:
1085
+ i = args.index("--use")
1086
+ use = args[i + 1 : i + 3]
1087
+ args = args[:i] + args[i + 3 :]
1088
+ if len(use) != 2:
1089
+ fail("--use precisa da fala e da versão (ex.: --use c03f1 2026-10-12T14-03-11-482Z)")
1090
+ only = None
1091
+ if "--scene" in args:
1092
+ i = args.index("--scene")
1093
+ only = args[i + 1] if i + 1 < len(args) else None
1094
+ args = args[:i] + args[i + 2 :]
1095
+ if not only:
1096
+ fail("--scene precisa do id da cena (ex.: --scene c01)")
1097
+ if len(args) != 2 or args[0] not in COMMANDS:
1098
+ print(__doc__)
1099
+ sys.exit(2)
1100
+ if only and args[0] not in ("voice", "captions"):
1101
+ fail("--scene só vale para voice e captions")
1102
+ if as_json and args[0] != "status":
1103
+ fail("--json só vale para status")
1104
+ if regenerate_fallback and args[0] != "voice":
1105
+ fail("--regenerate-fallback só vale para voice")
1106
+ if (regenerate or use) and args[0] != "voice":
1107
+ fail("--regenerate e --use só valem para voice")
1108
+ if regenerate and use:
1109
+ fail("--regenerate gera outra leitura; --use escolhe uma guardada: um de cada vez")
1110
+ target = Path(args[1]).resolve()
1111
+ if not target.is_dir():
1112
+ fail(f"{target} não é uma pasta de vídeo")
1113
+ if only and not any(s["id"] == only for s in load(target)["scenes"]):
1114
+ fail(f"a cena {only} não existe no scenes.json")
1115
+ if as_json:
1116
+ cmd_status(target, as_json=True)
1117
+ elif args[0] == "voice" and use:
1118
+ cmd_use(target, *use)
1119
+ elif args[0] == "voice":
1120
+ cmd_voice(target, only, regenerate_fallback, regenerate, provider)
1121
+ else:
1122
+ COMMANDS[args[0]](target, only) if only else COMMANDS[args[0]](target)