@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,1496 @@
1
+ #!/usr/bin/env node
2
+ /* ============================================================================
3
+ Publicação: sobe o vídeo exportado no YouTube, no Instagram (Reels) e no TikTok.
4
+
5
+ node tools/publish.mjs status [--json] as contas estão conectadas?
6
+ node tools/publish.mjs auth youtube login no Google pelo navegador (uma vez)
7
+ node tools/publish.mjs auth tiktok login no TikTok pelo navegador (uma vez)
8
+ --browser Dia abre em outro app em vez do navegador padrão
9
+ --no-open só imprime o link, para colar à mão
10
+ node tools/publish.mjs auth instagram <token> grava o token gerado no painel da Meta
11
+ node tools/publish.mjs text videos/<slug> [--redo] [--json]
12
+ título, descrição, legenda e tags a partir do script.md (o Claude escreve;
13
+ sem ele, tirados do roteiro). Só preenche o que está vazio; --redo troca tudo
14
+ node tools/publish.mjs cover videos/<slug> qual thumbnail vai de capa (sem publicar nada)
15
+ node tools/publish.mjs videos/<slug> --youtube --instagram --tiktok --confirm
16
+ --privacy private|unlisted|public no YouTube (padrão: publish.youtube.privacy)
17
+ --tiktok-privacy SELF_ONLY|… no TikTok (padrão: publish.tiktok.privacy)
18
+ --again sobe outra vez onde já foi publicado
19
+ node tools/publish.mjs carousel videos/<slug> --confirm [--again]
20
+ o carrossel da pasta (carousel/export/NN.jpg, exportado na hora se faltar) no
21
+ Instagram, com a legenda de carousel/publication.json (ou a do carousel.json);
22
+ o resultado volta para carousel/publication.json
23
+ node tools/publish.mjs videos/<slug> --auto
24
+ o Claude, no fim do pipeline: nas plataformas de publish.auto, se
25
+ publish.auto.approved for true e o vídeo estiver pronto
26
+
27
+ node tools/publish.mjs schedule videos/<slug> --when "2026-10-14 18:00" --youtube --tiktok --confirm
28
+ o mesmo pedido, para o dia e a hora (hora desta máquina); aceita as
29
+ mesmas --privacy, --tiktok-privacy e --again
30
+ node tools/publish.mjs schedule videos/<slug> --reschedule --when "2026-10-15 09:30" --confirm
31
+ outra hora para o mesmo agendamento (plataformas, privacidade e texto ficam)
32
+ node tools/publish.mjs schedule videos/<slug> --cancel
33
+ node tools/publish.mjs scheduler [--json] o que está agendado e se o agente está ligado
34
+ node tools/publish.mjs scheduler install|uninstall o agente do launchd que confere a cada minuto
35
+ node tools/publish.mjs scheduler run publica o que venceu (o agente chama)
36
+ node tools/publish.mjs cloud push credenciais do YouTube e do TikTok e os agendamentos
37
+ pendentes para a cloud (com cloud.schedule)
38
+
39
+ Com cloud.schedule no config, a cloud (cloud/omnichat) publica na hora: o
40
+ `schedule` prepara aqui o que precisa da máquina (texto, ffprobe, a capa em JPG, a regra do
41
+ omnichat), sobe o MP4 e a capa para a cloud e grava o pedido lá; remarcar e cancelar vão para
42
+ ela; o agente daqui só lê o resultado depois da hora e escreve no publication.json.
43
+
44
+ Publicar é para fora: só com pedido do usuário (o botão do studio, ou ele
45
+ pedindo com todas as letras, com --confirm) ou com a aprovação permanente
46
+ da config (--auto). Sem um dos dois, não sobe nada. Agendar é o mesmo
47
+ pedido, feito agora para subir depois.
48
+
49
+ O que sobe é o MP4 do export/assembly.json. Título, descrição e legenda (a
50
+ mesma no Instagram e no TikTok) vêm de videos/<slug>/publication.json; vazio,
51
+ o `text` escreve a partir do roteiro antes de subir. O rodapé de cada
52
+ plataforma e as hashtags de publish.text entram no fim. O resultado de
53
+ cada plataforma (id, link, quando, qual MP4) volta para o mesmo arquivo, e
54
+ é ele que impede publicar duas vezes.
55
+
56
+ A capa é a thumbnail selecionada (thumbnail/thumbnail.json `selected`), lida
57
+ na hora de subir: vai em JPG para o YouTube (thumbnails.set) e o Instagram
58
+ (cover_url). O TikTok não aceita imagem pela API. Capa que falha não derruba
59
+ o vídeo: fica `cover: { version, applied: false, error }` no resultado.
60
+
61
+ Credenciais no .env (fora do git): YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET,
62
+ YOUTUBE_REFRESH_TOKEN; INSTAGRAM_TOKEN, INSTAGRAM_USER_ID,
63
+ INSTAGRAM_TOKEN_EXPIRES; TIKTOK_CLIENT_KEY, TIKTOK_CLIENT_SECRET e os tokens
64
+ TIKTOK_* que o auth grava. Passo a passo em reference/publish.md.
65
+ ============================================================================ */
66
+
67
+ /* The platform clients (tools/lib/platforms/), the texts and per-platform checks (tools/lib/publish/)
68
+ and the schedule decisions (tools/lib/schedule/) are the core; this file is the CLI: files, flags,
69
+ the desktop login, the local tunnel for Instagram, ffmpeg/ffprobe, `claude` and the launchd agent. */
70
+
71
+ import {
72
+ readFileSync,
73
+ writeFileSync,
74
+ renameSync,
75
+ existsSync,
76
+ statSync,
77
+ createReadStream,
78
+ readdirSync,
79
+ mkdirSync,
80
+ openSync,
81
+ writeSync,
82
+ closeSync,
83
+ unlinkSync,
84
+ } from "node:fs";
85
+ import { createServer } from "node:http";
86
+ import https from "node:https";
87
+ import { randomBytes, createHash } from "node:crypto";
88
+ import { spawn, spawnSync } from "node:child_process";
89
+ import { homedir, tmpdir } from "node:os";
90
+ import { join, resolve, dirname, basename, sep } from "node:path";
91
+ import { fileURLToPath } from "node:url";
92
+ import {
93
+ env,
94
+ envStore,
95
+ writeEnv,
96
+ pushInstagramToken,
97
+ pushPlatformEnv,
98
+ pullTiktokEnv,
99
+ cloudSettings,
100
+ cloudApi,
101
+ sleep,
102
+ instagramToken,
103
+ instagramAccount,
104
+ igClient,
105
+ } from "./platforms.mjs";
106
+ import { instagramAuthValues, publishReel, publishCarouselPost } from "./lib/platforms/instagram.mjs";
107
+ import { askTextSync } from "./lib/agents.mjs";
108
+ import {
109
+ googleClient as coreGoogleClient,
110
+ youtubeAccessToken,
111
+ youtubeAuthUrl,
112
+ exchangeGoogleCode,
113
+ youtubeChannel,
114
+ uploadYoutube as coreUploadYoutube,
115
+ } from "./lib/platforms/youtube.mjs";
116
+ import {
117
+ tiktokApp,
118
+ tiktokAccessToken,
119
+ fetchTiktokToken,
120
+ tiktokAuthUrl,
121
+ tiktokAccount,
122
+ uploadTiktok as coreUploadTiktok,
123
+ } from "./lib/platforms/tiktok.mjs";
124
+ import {
125
+ publishConfig,
126
+ PLATFORMS,
127
+ PRIVACIES,
128
+ TIKTOK_PRIVACIES,
129
+ TEXT_FIELDS,
130
+ withFixed as coreWithFixed,
131
+ instagramCaptionProblem,
132
+ metadata as coreMetadata,
133
+ defaultTitle as coreDefaultTitle,
134
+ textFromScript as coreTextFromScript,
135
+ TEXT_PROMPT,
136
+ parseModelText,
137
+ missingText,
138
+ platformGate,
139
+ publishedEntry,
140
+ failedEntry,
141
+ previousOf,
142
+ } from "./lib/publish/publish.mjs";
143
+ import {
144
+ parseWhen as coreParseWhen,
145
+ newSchedule,
146
+ reschedule,
147
+ dropPlatform,
148
+ dueAction,
149
+ markMissed,
150
+ startAttempt,
151
+ failureReason,
152
+ attemptOutcome,
153
+ } from "./lib/schedule/schedule.mjs";
154
+ import { CODE_ROOT, homeEnvPlist, DATA_ROOT as ROOT } from "./lib/paths.mjs";
155
+
156
+ const CFG = JSON.parse(readFileSync(join(ROOT, "omnivideo.config.json"), "utf8"));
157
+ const P = publishConfig(CFG.publish ?? {});
158
+
159
+ const args = process.argv.slice(2);
160
+ const has = n => args.includes(`--${n}`);
161
+ const option = n => {
162
+ const i = args.indexOf(`--${n}`);
163
+ return i < 0 ? undefined : args[i + 1];
164
+ };
165
+ function fail(msg) {
166
+ console.error(`erro: ${msg}`);
167
+ process.exit(1);
168
+ }
169
+ const readJson = file => {
170
+ try {
171
+ return JSON.parse(readFileSync(file, "utf8"));
172
+ } catch {
173
+ return null;
174
+ }
175
+ };
176
+ const log = line => console.log(line);
177
+ const warn = line => console.error(line);
178
+ const MB = 1024 * 1024;
179
+
180
+ /* Desktop-app login (YouTube and TikTok): the platform sends the login back to a
181
+ server on 127.0.0.1 on a free port. The link opens in the default browser;
182
+ `--browser Dia` opens another app, and `--no-open` only prints the link (line
183
+ `url: …`) to paste in any browser on this machine: the callback goes to
184
+ 127.0.0.1, so the login must happen here. Waits up to 10 min. */
185
+ async function localLogin({ name, who, path = "/", buildUrl }) {
186
+ const state = randomBytes(16).toString("hex");
187
+ let redirect = "";
188
+ const code = await new Promise((ok, reject) => {
189
+ const srv = createServer((req, res) => {
190
+ const u = new URL(req.url, "http://127.0.0.1");
191
+ if (u.pathname !== path) {
192
+ res.writeHead(404).end();
193
+ return;
194
+ }
195
+ const c = u.searchParams.get("code");
196
+ const valid = c && u.searchParams.get("state") === state;
197
+ const reason =
198
+ u.searchParams.get("error_description") ??
199
+ u.searchParams.get("error") ??
200
+ "resposta sem code ou com state errado";
201
+ res
202
+ .writeHead(200, { "content-type": "text/html; charset=utf-8" })
203
+ .end(valid ? `<p>${name} conectado ao OmniVideo. Pode fechar esta aba.</p>` : `<p>Não deu: ${reason}.</p>`);
204
+ srv.close();
205
+ valid ? ok(c) : reject(new Error(`login ${who}: ${reason}`));
206
+ });
207
+ srv.listen(0, "127.0.0.1", () => {
208
+ redirect = `http://127.0.0.1:${srv.address().port}${path === "/" ? "" : path}`;
209
+ const url = buildUrl(redirect, state);
210
+ const app = option("browser");
211
+ if (has("no-open"))
212
+ console.log(
213
+ `abra o link abaixo num navegador desta máquina e escolha a conta ${who === "do Google" ? "do canal" : `do ${name}`}:`,
214
+ );
215
+ else {
216
+ console.log(`abrindo o login ${who} ${app ? `no ${app}` : "no navegador"}…`);
217
+ const p = spawnSync("open", [...(app ? ["-a", app] : []), url], { encoding: "utf8" });
218
+ if (p.status !== 0)
219
+ console.log(`não abriu${app ? ` o ${app}` : ""}: ${(p.stderr || "").trim()}. Cole o link à mão.`);
220
+ }
221
+ /* the last line with the link: the studio reads this one to show and copy it */
222
+ console.log(`url: ${url}`);
223
+ });
224
+ setTimeout(() => {
225
+ srv.close();
226
+ reject(new Error("10 min sem resposta do navegador"));
227
+ }, 600_000).unref();
228
+ });
229
+ return { code, redirect };
230
+ }
231
+
232
+ /* --- YouTube --------------------------------------------------------------------------- */
233
+ const youtubeToken = () => youtubeAccessToken({ store: envStore });
234
+
235
+ async function authYoutube() {
236
+ const { id, secret } = coreGoogleClient(envStore);
237
+ const verifier = randomBytes(32).toString("base64url");
238
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
239
+ const { code, redirect } = await localLogin({
240
+ name: "YouTube",
241
+ who: "do Google",
242
+ buildUrl: (redirect, state) => youtubeAuthUrl({ clientId: id, redirect, state, challenge }),
243
+ });
244
+ const j = await exchangeGoogleCode({ clientId: id, secret, code, redirect, verifier });
245
+ if (!j.refresh_token)
246
+ fail(
247
+ "o Google não devolveu refresh_token; tire o acesso do app em myaccount.google.com/permissions e rode de novo",
248
+ );
249
+ writeEnv({ YOUTUBE_REFRESH_TOKEN: j.refresh_token });
250
+ await pushPlatformEnv({ YOUTUBE_REFRESH_TOKEN: j.refresh_token });
251
+ console.log(`YouTube conectado: ${(await youtubeChannel(j.access_token)).account}`);
252
+ }
253
+
254
+ /* The cover is the thumbnail selected in the studio (tools/thumbnail.mjs choose), read at
255
+ upload time: a scheduled post takes whichever is selected when the scheduler runs.
256
+ It goes as a JPG up to 2 MB, YouTube's limit (Instagram accepts it too). */
257
+ const COVER_LIMIT = 2 * 1024 * 1024;
258
+ function videoCover(dir) {
259
+ const t = readJson(join(dir, "thumbnail", "thumbnail.json"));
260
+ const png = join(dir, "thumbnail", "thumbnail.png");
261
+ if (!t?.selected || !existsSync(png)) return null;
262
+ const jpg = join(tmpdir(), `cover-${process.pid}-${randomBytes(4).toString("hex")}.jpg`);
263
+ for (const q of [2, 4, 6, 9, 13]) {
264
+ const r = spawnSync("ffmpeg", ["-y", "-loglevel", "error", "-i", png, "-q:v", String(q), jpg], {
265
+ encoding: "utf8",
266
+ });
267
+ if (r.status !== 0) {
268
+ console.error(`aviso: capa: o ffmpeg não converteu thumbnail.png: ${(r.stderr ?? "").trim().slice(-200)}`);
269
+ return null;
270
+ }
271
+ if (statSync(jpg).size <= COVER_LIMIT) return { version: t.selected, jpg };
272
+ }
273
+ console.error("aviso: capa: thumbnail.png não coube em 2 MB nem em JPG de qualidade baixa; sobe sem capa");
274
+ return null;
275
+ }
276
+
277
+ async function uploadYoutube(mp4, meta, privacy, video, cover) {
278
+ const token = await youtubeToken();
279
+ return coreUploadYoutube({
280
+ token,
281
+ bytes: readFileSync(mp4),
282
+ meta,
283
+ privacy,
284
+ video,
285
+ settings: P.youtube,
286
+ cover: cover ? { version: cover.version, jpeg: readFileSync(cover.jpg) } : null,
287
+ log,
288
+ });
289
+ }
290
+
291
+ /* --- Instagram (Instagram API with Instagram login) --------------------------------- */
292
+ async function authInstagram(token) {
293
+ if (!token)
294
+ fail("passe o token: node tools/publish.mjs auth instagram <token> (reference/publish.md, Instagram passo 4)");
295
+ const c = await instagramAccount(token);
296
+ if (c.account_type && !["BUSINESS", "MEDIA_CREATOR"].includes(c.account_type)) {
297
+ fail(`@${c.username} é conta ${c.account_type}: a API só publica em conta profissional (Criador ou Empresa)`);
298
+ }
299
+ /* the token generated in the dashboard is already the 60-day one */
300
+ const values = instagramAuthValues({ token, account: c, now: Date.now() });
301
+ writeEnv(values);
302
+ await pushInstagramToken(values);
303
+ console.log(`Instagram conectado: @${c.username}`);
304
+ }
305
+
306
+ /* does the URL answer (206) when resolved through Cloudflare's DNS (1.1.1.1)? */
307
+ async function reachableFromOutside(url, host) {
308
+ let ip;
309
+ try {
310
+ const r = await fetch(`https://1.1.1.1/dns-query?name=${host}&type=A`, {
311
+ headers: { accept: "application/dns-json" },
312
+ signal: AbortSignal.timeout(5000),
313
+ });
314
+ ip = (await r.json()).Answer?.find(a => a.type === 1)?.data;
315
+ } catch {
316
+ return false;
317
+ }
318
+ if (!ip) return false;
319
+ return new Promise(ok => {
320
+ const req = https.get(
321
+ url,
322
+ {
323
+ headers: { range: "bytes=0-0" },
324
+ lookup: (_h, opts, cb) => (opts?.all ? cb(null, [{ address: ip, family: 4 }]) : cb(null, ip, 4)),
325
+ timeout: 8000,
326
+ },
327
+ res => {
328
+ res.resume();
329
+ ok(res.statusCode === 206 || res.statusCode === 200);
330
+ },
331
+ );
332
+ req.on("timeout", () => {
333
+ req.destroy();
334
+ ok(false);
335
+ });
336
+ req.on("error", () => ok(false));
337
+ });
338
+ }
339
+
340
+ /* Files at a public URL for a few minutes: a server on 127.0.0.1 that only answers
341
+ /<token>.<ext> for each one (with Range) and a cloudflared quick tunnel in front
342
+ (https://<random>.trycloudflare.com). `urls` in file order; `close()` tears both down.
343
+ Local only: in the cloud the files are already at a public URL (object storage). */
344
+ const TYPES = { ".mp4": "video/mp4", ".jpg": "image/jpeg" };
345
+ async function servePublic(files) {
346
+ const served = new Map(
347
+ files.map(file => {
348
+ const ext = file.slice(file.lastIndexOf("."));
349
+ return [
350
+ `/${randomBytes(16).toString("hex")}${ext}`,
351
+ { file, size: statSync(file).size, type: TYPES[ext] ?? "application/octet-stream" },
352
+ ];
353
+ }),
354
+ );
355
+ const srv = createServer((req, res) => {
356
+ const s = served.get(req.url);
357
+ if (!s || !["GET", "HEAD"].includes(req.method)) {
358
+ res.writeHead(404).end();
359
+ return;
360
+ }
361
+ const { file, size } = s;
362
+ let start = 0,
363
+ end = size - 1,
364
+ code = 200;
365
+ const m = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range ?? "");
366
+ if (m && (m[1] || m[2])) {
367
+ code = 206;
368
+ if (m[1]) {
369
+ start = +m[1];
370
+ if (m[2]) end = Math.min(+m[2], size - 1);
371
+ } else start = Math.max(0, size - +m[2]);
372
+ if (start > end) {
373
+ res.writeHead(416, { "content-range": `bytes */${size}` }).end();
374
+ return;
375
+ }
376
+ }
377
+ res.writeHead(code, {
378
+ "content-type": s.type,
379
+ "content-length": String(end - start + 1),
380
+ "accept-ranges": "bytes",
381
+ ...(code === 206 ? { "content-range": `bytes ${start}-${end}/${size}` } : {}),
382
+ });
383
+ if (req.method === "HEAD") res.end();
384
+ else createReadStream(file, { start, end }).pipe(res);
385
+ });
386
+ await new Promise(ok => srv.listen(0, "127.0.0.1", ok));
387
+ const tunnel = spawn(
388
+ P.instagram.cloudflared ?? "cloudflared",
389
+ ["tunnel", "--no-autoupdate", "--url", `http://127.0.0.1:${srv.address().port}`],
390
+ { stdio: ["ignore", "pipe", "pipe"] },
391
+ );
392
+ const close = () => {
393
+ try {
394
+ tunnel.kill();
395
+ } catch {
396
+ /* already exited */
397
+ }
398
+ srv.close();
399
+ };
400
+ try {
401
+ /* the address shows up in the output right away; the registered connection a few seconds later */
402
+ let output = "";
403
+ const waitFor = (re, label) =>
404
+ new Promise((ok, reject) => {
405
+ const check = () => {
406
+ const m = re.exec(output);
407
+ if (m) ok(m[0]);
408
+ };
409
+ tunnel.stdout.on("data", check);
410
+ tunnel.stderr.on("data", check);
411
+ check();
412
+ tunnel.on("error", e => reject(new Error(`cloudflared: ${e.message} (instale com brew install cloudflared)`)));
413
+ tunnel.on("exit", c => reject(new Error(`o cloudflared saiu com ${c}: ${output.trim().slice(-300)}`)));
414
+ setTimeout(() => reject(new Error(`o cloudflared não ${label} em 30 s`)), 30_000).unref();
415
+ });
416
+ tunnel.stdout.on("data", b => {
417
+ output += b;
418
+ });
419
+ tunnel.stderr.on("data", b => {
420
+ output += b;
421
+ });
422
+ const base = await waitFor(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/, "abriu o túnel");
423
+ await waitFor(/Registered tunnel connection/, "conectou o túnel");
424
+ /* The new name takes one to two minutes to answer. The proof is downloading 1 byte from
425
+ the public URL, with the IP taken from a public DNS: macOS's DNS caches the "does not
426
+ exist" of the first attempt and would delay everything. */
427
+ const urls = [...served.keys()].map(path => `${base}${path}`);
428
+ const host = new URL(base).hostname;
429
+ const deadline = Date.now() + 150_000;
430
+ for (;;) {
431
+ if (await reachableFromOutside(urls[0], host)) break;
432
+ if (Date.now() > deadline) throw new Error(`o túnel abriu, mas ${host} não respondeu em 2,5 min`);
433
+ await sleep(3000);
434
+ }
435
+ return { urls, close };
436
+ } catch (e) {
437
+ close();
438
+ throw e;
439
+ }
440
+ }
441
+
442
+ /* With Instagram login, Meta does not accept the file: it downloads from a public
443
+ video_url (resumable upload is only for apps with Facebook login). The link comes from
444
+ servePublic and stays up until Instagram finishes processing. The cover goes the same
445
+ way, as cover_url, instead of the cover_at_s frame. */
446
+ async function uploadInstagram(mp4, caption, cover) {
447
+ const { token, id } = await instagramToken();
448
+ console.log(
449
+ `Instagram: abrindo um link temporário para o Instagram baixar o vídeo (${(statSync(mp4).size / 1e6).toFixed(1)} MB)…`,
450
+ );
451
+ const link = await servePublic(cover ? [mp4, cover.jpg] : [mp4]);
452
+ const [videoUrl, coverUrl] = link.urls;
453
+ return publishReel({
454
+ client: igClient(token),
455
+ userId: id,
456
+ videoUrl,
457
+ coverUrl,
458
+ cover,
459
+ caption,
460
+ shareToFeed: !!P.instagram.share_to_feed,
461
+ coverAtS: P.instagram.cover_at_s,
462
+ release: link.close,
463
+ log,
464
+ warn,
465
+ });
466
+ }
467
+
468
+ /* --- TikTok (Content Posting API, Login Kit for desktop) -------------------------------
469
+ The access token lasts 24 h and the refresh one year: each use renews whatever expired
470
+ and writes both back to .env (lib/platforms/tiktok.mjs). */
471
+ /* with the cloud schedule, TikTok's newest tokens come down first (TikTok renews the refresh token too) */
472
+ const tiktokToken = async () => {
473
+ await pullTiktokEnv();
474
+ return tiktokAccessToken({ store: envStore });
475
+ };
476
+
477
+ async function authTiktok() {
478
+ const { key } = tiktokApp(envStore);
479
+ /* TikTok desktop PKCE: the challenge is the SHA-256 in hex, not base64url */
480
+ const verifier = randomBytes(48).toString("base64url");
481
+ const challenge = createHash("sha256").update(verifier).digest("hex");
482
+ const { code, redirect } = await localLogin({
483
+ name: "TikTok",
484
+ who: "do TikTok",
485
+ path: "/callback/",
486
+ buildUrl: (redirect, state) => tiktokAuthUrl({ clientKey: key, redirect, state, challenge }),
487
+ });
488
+ const token = await fetchTiktokToken({
489
+ store: envStore,
490
+ body: { code, grant_type: "authorization_code", redirect_uri: redirect, code_verifier: verifier },
491
+ label: "token do TikTok",
492
+ });
493
+ console.log(`TikTok conectado: ${(await tiktokAccount(token)).account}`);
494
+ }
495
+
496
+ async function uploadTiktok(mp4, caption, video, requestedPrivacy) {
497
+ const token = await tiktokToken();
498
+ return coreUploadTiktok({
499
+ token,
500
+ bytes: readFileSync(mp4),
501
+ caption,
502
+ video,
503
+ requestedPrivacy,
504
+ settings: P.tiktok,
505
+ log,
506
+ });
507
+ }
508
+
509
+ /* --- status ------------------------------------------------------------------------------ */
510
+ async function status() {
511
+ const r = {};
512
+ const yt = { privacy: P.youtube.privacy, audited: !!P.youtube.audited };
513
+ try {
514
+ r.youtube = { ready: true, ...(await youtubeChannel(await youtubeToken())), ...yt };
515
+ } catch (e) {
516
+ r.youtube = { ready: false, reason: e.message, ...yt };
517
+ }
518
+ try {
519
+ const { token } = await instagramToken();
520
+ const c = await instagramAccount(token);
521
+ r.instagram = {
522
+ ready: true,
523
+ account: `@${c.username}`,
524
+ username: `@${c.username}`,
525
+ name: c.name ?? null,
526
+ avatar: c.profile_picture_url ?? null,
527
+ expires: env("INSTAGRAM_TOKEN_EXPIRES") || null,
528
+ };
529
+ } catch (e) {
530
+ r.instagram = { ready: false, reason: e.message };
531
+ }
532
+ const tt = { mode: P.tiktok.mode, privacy: P.tiktok.privacy, audited: !!P.tiktok.audited };
533
+ try {
534
+ r.tiktok = {
535
+ ready: true,
536
+ ...(await tiktokAccount(await tiktokToken())),
537
+ expires: env("TIKTOK_REFRESH_EXPIRES") || null,
538
+ ...tt,
539
+ };
540
+ } catch (e) {
541
+ r.tiktok = { ready: false, reason: e.message, ...tt };
542
+ }
543
+ r.auto = { approved: !!P.auto.approved, platforms: P.auto.platforms };
544
+ r.scheduler = agentStatus();
545
+ if (has("json")) {
546
+ console.log(JSON.stringify(r));
547
+ return;
548
+ }
549
+ for (const name of PLATFORMS) {
550
+ const s = r[name];
551
+ console.log(`${s.ready ? "✓" : "·"} ${name}: ${s.ready ? s.account : s.reason}`);
552
+ }
553
+ console.log(
554
+ ` automático: ${P.auto.approved ? `ligado (${P.auto.platforms.join(", ")})` : "desligado (publish.auto.approved)"}`,
555
+ );
556
+ console.log(
557
+ ` agenda: ${r.scheduler.loaded ? "agente ligado, confere a cada minuto" : "agente desligado (node tools/publish.mjs scheduler install)"}`,
558
+ );
559
+ if (r.youtube.ready && !P.youtube.audited)
560
+ console.log(
561
+ " YouTube: projeto sem auditoria, o vídeo fica privado até você abrir no Studio (publish.youtube.audited)",
562
+ );
563
+ if (r.instagram.expires)
564
+ console.log(
565
+ ` Instagram: token vale até ${new Date(r.instagram.expires).toLocaleDateString("pt-BR")} (renova sozinho a cada publicação)`,
566
+ );
567
+ if (r.tiktok.ready && P.tiktok.mode === "draft")
568
+ console.log(" TikTok: modo rascunho, o vídeo vai para a caixa de entrada do app e você termina a postagem lá");
569
+ else if (r.tiktok.ready && !P.tiktok.audited)
570
+ console.log(' TikTok: app sem auditoria, o vídeo sobe como "só eu" (publish.tiktok.audited)');
571
+ }
572
+
573
+ /* --- text from the script ------------------------------------------------------------- */
574
+ function readScript(dir) {
575
+ try {
576
+ return readFileSync(join(dir, "script.md"), "utf8");
577
+ } catch {
578
+ return "";
579
+ }
580
+ }
581
+
582
+ const defaultTitle = dir =>
583
+ coreDefaultTitle({
584
+ scenesTitle: readJson(join(dir, "scenes.json"))?.title,
585
+ script: readScript(dir),
586
+ dirName: dir.split("/").pop(),
587
+ });
588
+
589
+ /* without Claude: the script's title, the thesis as description and the hook (first line) as caption */
590
+ const textFromScript = dir => coreTextFromScript(readScript(dir), defaultTitle(dir));
591
+
592
+ /* one short answer with the script on stdin, on the agent accounts in rotation (tools/lib/agents.mjs):
593
+ one that hits its limit is set aside and the next answers; ~/.claude is never used */
594
+ function textFromClaude(dir) {
595
+ const script = readScript(dir);
596
+ if (!script.trim()) throw new Error("sem script.md");
597
+ const { text } = askTextSync(
598
+ {
599
+ root: ROOT,
600
+ cfg: CFG,
601
+ prompt: TEXT_PROMPT,
602
+ claudeModel: CFG.publish?.text?.model ?? CFG.models?.orchestrator?.model ?? "sonnet",
603
+ tmpDir: tmpdir(),
604
+ },
605
+ (bin, args, env) =>
606
+ spawnSync(bin, args, { cwd: ROOT, input: script, encoding: "utf8", env, timeout: 180_000, maxBuffer: 4 * MB }),
607
+ );
608
+ return parseModelText(text);
609
+ }
610
+
611
+ /* fills what is empty in publication.json (everything, with redo) and writes it */
612
+ function writeText(dir, redo) {
613
+ const file = join(dir, "publication.json");
614
+ const pub = readJson(file) ?? {};
615
+ const missing = missingText(pub, redo);
616
+ if (!missing.length) return { pub, by: null, warning: null };
617
+ let fresh,
618
+ by = "claude",
619
+ warning = null;
620
+ try {
621
+ fresh = textFromClaude(dir);
622
+ } catch (e) {
623
+ fresh = textFromScript(dir);
624
+ by = "script";
625
+ warning = `o agente não escreveu o texto (${e.message}); usei o título, a tese e o gancho do roteiro`;
626
+ }
627
+ for (const k of missing) pub[k] = fresh[k];
628
+ pub.text_generated = { at: new Date().toISOString(), by };
629
+ writePub(file, pub);
630
+ return { pub, by, warning };
631
+ }
632
+
633
+ function text(dir) {
634
+ const { pub, by, warning } = writeText(dir, has("redo"));
635
+ if (warning) console.error(`aviso: ${warning}`);
636
+ const data = Object.fromEntries(TEXT_FIELDS.map(k => [k, pub[k] ?? (k === "tags" ? [] : "")]));
637
+ if (has("json")) {
638
+ console.log(JSON.stringify({ ...data, by, warning }));
639
+ return;
640
+ }
641
+ if (!by) console.log("o texto já estava escrito (--redo troca)");
642
+ for (const k of TEXT_FIELDS) console.log(`${k}: ${Array.isArray(data[k]) ? data[k].join(", ") : data[k]}`);
643
+ }
644
+
645
+ /* --- publish one video ----------------------------------------------------------------------- */
646
+ function exported(dir) {
647
+ const m = readJson(join(dir, "export", "assembly.json"));
648
+ if (!m?.mp4) fail("sem export/assembly.json: monte o vídeo antes (node tools/assemble.mjs)");
649
+ const mp4 = join(dir, m.mp4);
650
+ if (!existsSync(mp4)) fail(`${m.mp4} não existe: monte o vídeo de novo`);
651
+ return { mp4, rel: m.mp4, mtime: Math.round(statSync(mp4).mtimeMs), w: m.w, h: m.h, duration: m.duration };
652
+ }
653
+
654
+ /* the footer and the fixed hashtags of publish.text go at the end, without repeating what the text has */
655
+ const withFixed = (text, footer) => coreWithFixed(text, footer, P.text.hashtags);
656
+
657
+ /* each platform's limits, checked before uploading anything */
658
+ const metadata = (dir, pub) => coreMetadata({ pub, defaultTitle: () => defaultTitle(dir), text: P.text });
659
+
660
+ /* only a change is written: every write is a studio notice (video, omnichat, analytics), and the
661
+ scheduler rereads the cloud every minute while a job runs */
662
+ function writePub(file, pub) {
663
+ const text = JSON.stringify(pub, null, 2) + "\n";
664
+ try {
665
+ if (readFileSync(file, "utf8") === text) return;
666
+ } catch {
667
+ /* no file yet */
668
+ }
669
+ const tmp = `${file}.${process.pid}.tmp`;
670
+ writeFileSync(tmp, text);
671
+ renameSync(tmp, file);
672
+ }
673
+
674
+ /* --auto only publishes a video that finished the pipeline: every stage ✓, export up to date,
675
+ and an assembly without warnings */
676
+ function checkAuto(dir) {
677
+ if (!P.auto.approved) fail("publish.auto.approved está false: publicar só com pedido do usuário (--confirm)");
678
+ for (const n of [...PLATFORMS, "privacy", "tiktok-privacy", "again", "confirm"]) {
679
+ if (has(n)) fail(`--auto usa as plataformas e a privacidade da config; não combina com --${n}`);
680
+ }
681
+ const py =
682
+ [join(CODE_ROOT, ".venv", "bin", "python"), CFG.voicestudio?.python ?? ""].find(p => p && existsSync(p)) ??
683
+ "python3";
684
+ const r = spawnSync(py, [join(CODE_ROOT, "tools", "omnivideo.py"), "status", dir, "--json"], { encoding: "utf8" });
685
+ if (r.status !== 0) fail(`status do vídeo: ${(r.stderr || r.stdout).slice(-400)}`);
686
+ const st = JSON.parse(r.stdout);
687
+ const stage = id => st.stages?.find(e => e.id === id);
688
+ const problems = [];
689
+ if (!st.ready) problems.push(`o vídeo não terminou o pipeline (próxima: ${st.next?.name ?? "?"})`);
690
+ if (stage("export")?.outdated)
691
+ problems.push("o export está desatualizado: cenas, voz ou legenda mudaram depois da montagem");
692
+ const m = readJson(join(dir, "export", "assembly.json"));
693
+ if (m?.warnings?.length) problems.push(`a montagem deixou avisos: ${m.warnings.join("; ")}`);
694
+ if (problems.length) fail("publicação automática recusada:\n " + problems.join("\n "));
695
+ }
696
+
697
+ /* where and how: the request's flags (or the config, with --auto) */
698
+ function publishRequest(auto) {
699
+ const platforms = PLATFORMS.filter(p => (auto ? P.auto.platforms.includes(p) : has(p)));
700
+ if (!platforms.length)
701
+ fail(auto ? "publish.auto.platforms está vazio" : "diga onde: --youtube, --instagram, --tiktok (uma ou mais)");
702
+ const privacy = (auto ? null : option("privacy")) ?? P.youtube.privacy;
703
+ if (!PRIVACIES.includes(privacy)) fail(`privacidade: ${PRIVACIES.join(", ")}`);
704
+ const tiktokPrivacy = (auto ? null : option("tiktok-privacy")) ?? P.tiktok.privacy;
705
+ if (!TIKTOK_PRIVACIES.includes(tiktokPrivacy)) fail(`privacidade do TikTok: ${TIKTOK_PRIVACIES.join(", ")}`);
706
+ return { platforms, privacy, tiktokPrivacy };
707
+ }
708
+
709
+ /* without a title, the text comes from the script before uploading */
710
+ function ensureText(dir) {
711
+ if (String(readJson(join(dir, "publication.json"))?.title ?? "").trim()) return;
712
+ const { by, warning } = writeText(dir, false);
713
+ if (warning) console.error(`aviso: ${warning}`);
714
+ if (by)
715
+ console.log(
716
+ `texto escrito a partir do roteiro (${by === "claude" ? "pelo Claude" : "tirado do roteiro"}): publication.json`,
717
+ );
718
+ }
719
+
720
+ const FROM_SCHEDULER = process.env.OMNIVIDEO_ORIGIN === "schedule";
721
+
722
+ /* The Reel is live: the video's omnichat replies on it. `omnichat.mjs published` says whether
723
+ the rule was already enabled and, with omnichat.instagram.enable_on_publish, enables the
724
+ missing one (no rule: from the narration's "comenta X que eu te mando"). Nothing from it
725
+ brings down the publication. */
726
+ function omnichatOnPublish(dir) {
727
+ const r = spawnSync(process.execPath, [join(CODE_ROOT, "tools", "omnichat.mjs"), "published", dir], {
728
+ encoding: "utf8",
729
+ timeout: 60_000,
730
+ });
731
+ const output = [r.stdout, r.stderr]
732
+ .map(s => (s ?? "").trim())
733
+ .filter(Boolean)
734
+ .join("\n");
735
+ if (r.status === 0) {
736
+ if (output) console.log(output);
737
+ } else
738
+ console.error(
739
+ `aviso: omnichat: o Reel subiu, mas a regra não foi conferida: ${output.replace(/^erro: /, "") || r.error?.message || `saiu com ${r.status}`}`,
740
+ );
741
+ }
742
+
743
+ const longDate = iso => new Date(iso).toLocaleString("pt-BR");
744
+
745
+ /* the uploaded MP4 as the platforms see it: ffprobe, or assembly.json when it fails */
746
+ function probeVideo(exp) {
747
+ const probeRun = spawnSync(
748
+ "ffprobe",
749
+ [
750
+ "-v",
751
+ "error",
752
+ "-select_streams",
753
+ "v:0",
754
+ "-show_entries",
755
+ "stream=width,height:format=duration",
756
+ "-of",
757
+ "json",
758
+ exp.mp4,
759
+ ],
760
+ { encoding: "utf8" },
761
+ );
762
+ const probe = probeRun.status === 0 ? JSON.parse(probeRun.stdout) : null;
763
+ return {
764
+ w: probe?.streams?.[0]?.width ?? exp.w,
765
+ h: probe?.streams?.[0]?.height ?? exp.h,
766
+ duration: +(probe?.format?.duration ?? exp.duration),
767
+ };
768
+ }
769
+
770
+ async function publish(dir) {
771
+ const auto = has("auto");
772
+ if (auto) checkAuto(dir);
773
+ else if (!has("confirm"))
774
+ fail("publicar é para fora: só com pedido do usuário (--confirm) ou com publish.auto.approved (--auto)");
775
+ const { platforms, privacy, tiktokPrivacy } = publishRequest(auto);
776
+
777
+ const file = join(dir, "publication.json");
778
+ const sch = readJson(file)?.schedule;
779
+ if (!FROM_SCHEDULER && sch?.state === "publishing" && alive(sch.pid))
780
+ fail("a agenda está publicando este vídeo agora: espere terminar");
781
+ ensureText(dir);
782
+ const pub = readJson(file) ?? {};
783
+ const exp = exported(dir);
784
+ const meta = metadata(dir, pub);
785
+ if (meta.problems.length) fail("não dá para publicar:\n " + meta.problems.join("\n "));
786
+ const video = probeVideo(exp);
787
+
788
+ const cover = platforms.some(p => p !== "tiktok") ? videoCover(dir) : null;
789
+ if (cover) console.log(`capa: thumbnail ${cover.version} (YouTube e Instagram)`);
790
+ if (cover && platforms.includes("tiktok"))
791
+ console.log("aviso: tiktok: a API não aceita capa própria; vai o quadro de publish.tiktok.cover_at_s");
792
+
793
+ let failed = false;
794
+ for (const plat of platforms) {
795
+ const before = pub[plat];
796
+ const gate = platformGate({ platform: plat, before, video, again: has("again"), format: longDate });
797
+ if (gate.action === "skip") {
798
+ console.log(gate.message);
799
+ continue;
800
+ }
801
+ for (const w of gate.warnings) console.log(w);
802
+ if (gate.action === "refuse") {
803
+ console.error(gate.message);
804
+ failed = true;
805
+ continue;
806
+ }
807
+ try {
808
+ const r =
809
+ plat === "youtube"
810
+ ? await uploadYoutube(exp.mp4, meta, privacy, video, cover)
811
+ : plat === "instagram"
812
+ ? await uploadInstagram(exp.mp4, meta.caption, cover)
813
+ : await uploadTiktok(exp.mp4, meta.tiktokCaption, video, tiktokPrivacy);
814
+ pub[plat] = publishedEntry({
815
+ result: r,
816
+ before,
817
+ mp4: exp.rel,
818
+ mp4Mtime: exp.mtime,
819
+ scheduledFor: FROM_SCHEDULER ? process.env.OMNIVIDEO_SCHEDULED_FOR : null,
820
+ now: Date.now(),
821
+ });
822
+ /* published by hand where it was scheduled: the scheduler does not upload it again */
823
+ if (!FROM_SCHEDULER && dropPlatform(pub, plat)) console.log(`${plat}: saiu da agenda (publicado agora)`);
824
+ writePub(file, pub);
825
+ console.log(
826
+ `${plat}: ${r.draft ? "enviado para a caixa de entrada do app (termine a postagem no TikTok)" : `publicado → ${r.url ?? r.id}`}`,
827
+ );
828
+ if (r.cover?.applied) console.log(` capa ${r.cover.version} aplicada`);
829
+ else if (r.cover)
830
+ console.log(
831
+ `aviso: ${plat}: o vídeo subiu, mas a capa ${r.cover.version} não foi: ${r.cover.error}${r.studio ? `. Troque no Studio: ${r.studio}` : ""}`,
832
+ );
833
+ if (plat === "youtube" && r.privacy !== "private" && !P.youtube.audited) {
834
+ console.log(
835
+ ` o projeto do Google ainda não passou pela auditoria: o YouTube trava o vídeo como privado. Mude no Studio: ${r.studio}`,
836
+ );
837
+ }
838
+ if (plat === "instagram") omnichatOnPublish(dir);
839
+ } catch (e) {
840
+ console.error(`${plat}: ${e.message}`);
841
+ pub[plat] = failedEntry(before, e.message, Date.now());
842
+ writePub(file, pub);
843
+ failed = true;
844
+ }
845
+ }
846
+ if (cover)
847
+ try {
848
+ unlinkSync(cover.jpg);
849
+ } catch {
850
+ /* already gone */
851
+ }
852
+ if (failed) process.exit(1);
853
+ }
854
+
855
+ /* --- carousel -------------------------------------------------------------------------------
856
+ Instagram only. With Instagram login, images do not go up as files either: each JPEG goes
857
+ through a public image_url (the same servePublic as the Reel), becomes a child container
858
+ (is_carousel_item), and the CAROUSEL container joins the children with the caption. The
859
+ caption is carousel/publication.json's (the studio writes the edited text there) or
860
+ carousel.json's, with publish.text's footer and hashtags. The result goes back to
861
+ carousel/publication.json. */
862
+ async function publishCarousel(videoDir) {
863
+ if (!has("confirm")) fail("publicar é para fora: só com pedido do usuário (--confirm)");
864
+ const { diagnosisOf, exportOf, LIMITS } = await import("./carousel.mjs");
865
+ const dir = join(videoDir, "carousel");
866
+ let d = diagnosisOf(videoDir);
867
+ if (!d.exists) fail("a pasta não tem carousel/carousel.json");
868
+ const comp = d.stages.find(e => e.id === "composition");
869
+ if (!comp?.ok) fail(`a composição do carrossel não está pronta: ${comp?.missing ?? "rode o build"}`);
870
+ if (!d.stages.find(e => e.id === "export")?.ok) {
871
+ exportOf(videoDir);
872
+ d = diagnosisOf(videoDir);
873
+ }
874
+ const exp = readJson(join(dir, "export", "export.json"));
875
+ const jpgs = (exp?.slides ?? []).map(s => join(dir, s.jpg));
876
+ if (jpgs.length < 2) fail("o carrossel precisa de pelo menos 2 slides");
877
+ if (jpgs.length > LIMITS.publish)
878
+ fail(`${jpgs.length} slides: a publicação aceita até ${LIMITS.publish} (carousel.slides.publish)`);
879
+ if (jpgs.some(j => !existsSync(j))) fail("faltam JPEG em carousel/export: exporte de novo");
880
+ /* the API accepts images from 4:5 to 1.91:1 (the app's 3:4 does not go through it yet) */
881
+ const f = exp.format ?? {};
882
+ if (f.w && f.h && f.w / f.h < 0.8 - 1e-3)
883
+ fail(`formato ${f.ratio ?? `${f.w}×${f.h}`}: a API do Instagram aceita imagem de 4:5 a 1,91:1`);
884
+
885
+ const file = join(dir, "publication.json");
886
+ const pub = readJson(file) ?? {};
887
+ if (pub.instagram?.id && !has("again")) {
888
+ console.log(
889
+ `instagram: já publicado em ${longDate(pub.instagram.published_at)}: ${pub.instagram.url ?? pub.instagram.id} (--again sobe outra vez)`,
890
+ );
891
+ return;
892
+ }
893
+ const text = String(pub.caption ?? readJson(join(dir, "carousel.json"))?.caption ?? "").trim();
894
+ if (!text)
895
+ fail("o carrossel não tem legenda: escreva na etapa Publicar do carrossel (ou em caption no carousel.json)");
896
+ const caption = withFixed(text, P.text.footer_instagram);
897
+ const captionProblem = instagramCaptionProblem(caption);
898
+ if (captionProblem) fail(captionProblem);
899
+
900
+ try {
901
+ const { token, id } = await instagramToken();
902
+ console.log(`Instagram: abrindo um link temporário para o Instagram baixar os ${jpgs.length} slides…`);
903
+ const link = await servePublic(jpgs);
904
+ const done = await publishCarouselPost({
905
+ client: igClient(token),
906
+ userId: id,
907
+ imageUrls: link.urls,
908
+ caption,
909
+ release: link.close,
910
+ log,
911
+ });
912
+ const previous = previousOf(pub.instagram);
913
+ pub.instagram = {
914
+ id: done.id,
915
+ url: done.url,
916
+ type: "CAROUSEL_ALBUM",
917
+ slides: jpgs.length,
918
+ published_at: new Date().toISOString(),
919
+ export: exp.exported_at,
920
+ ...(previous?.length ? { previous } : {}),
921
+ };
922
+ writePub(file, pub);
923
+ console.log(`instagram: carrossel publicado → ${done.url ?? done.id}`);
924
+ omnichatOnPublish(dir);
925
+ } catch (e) {
926
+ pub.instagram = failedEntry(pub.instagram, e.message, Date.now());
927
+ writePub(file, pub);
928
+ throw e;
929
+ }
930
+ }
931
+
932
+ /* --- schedule -------------------------------------------------------------------------------
933
+ Scheduling is the --confirm request made now to upload later: the day, the time and the
934
+ platforms live in publication.json (`schedule`). The uploader is `scheduler run`, which
935
+ the launchd agent (`scheduler install`) calls every minute, with the studio open or not:
936
+ each due video goes up through a child publish.mjs with --confirm, same as the button.
937
+ The due / missed / retry decisions are lib/schedule/schedule.mjs.
938
+
939
+ The agent does not wake the Mac. Asleep or off at the time, the video goes up when it
940
+ comes back, up to publish.schedule.max_delay_h later; past that, it is "missed" and does
941
+ not go up. A failure on one platform retries only there (publish.schedule.attempts,
942
+ every interval_min) and then stays "failed", waiting for you. */
943
+ /* own dir: the queue reads every .json in .queue/ as a ticket */
944
+ const SCHEDULE_DIR = join(ROOT, ".schedule");
945
+ const INDEX_FILE = join(SCHEDULE_DIR, "dirs.json"); /* videos outside videos/ that were scheduled */
946
+ const LOCK_FILE = join(SCHEDULE_DIR, "running.lock");
947
+ const LOG_FILE = join(SCHEDULE_DIR, "schedule.log");
948
+ const AGENT = "com.omnivideo.schedule";
949
+ const PLIST = join(homedir(), "Library", "LaunchAgents", `${AGENT}.plist`);
950
+ const SELF = fileURLToPath(import.meta.url);
951
+
952
+ const alive = pid => {
953
+ if (!pid) return false;
954
+ try {
955
+ process.kill(pid, 0);
956
+ return true;
957
+ } catch (e) {
958
+ return e.code === "EPERM";
959
+ }
960
+ };
961
+ const dayTime = iso =>
962
+ new Date(iso).toLocaleString("pt-BR", {
963
+ weekday: "short",
964
+ day: "2-digit",
965
+ month: "2-digit",
966
+ hour: "2-digit",
967
+ minute: "2-digit",
968
+ });
969
+ const nowIso = () => new Date().toISOString();
970
+
971
+ /* "2026-10-14 18:00" or "2026-10-14T18:00" is this machine's time; with Z or an offset, the text's */
972
+ function parseWhen(s) {
973
+ try {
974
+ return coreParseWhen(s, { now: Date.now(), format: dayTime });
975
+ } catch (e) {
976
+ fail(e.message);
977
+ }
978
+ }
979
+
980
+ async function schedule(dir) {
981
+ const file = join(dir, "publication.json");
982
+ const current = readJson(file)?.schedule;
983
+ if (current?.state === "publishing" && alive(current.pid)) fail("a publicação agendada já começou: espere terminar");
984
+ if (has("cancel")) {
985
+ const pub = readJson(file) ?? {};
986
+ if (!pub.schedule) {
987
+ console.log("não havia agendamento");
988
+ return;
989
+ }
990
+ if (pub.schedule.cloud) await cloudApi("DELETE", `/api/schedule/${basename(dir)}`);
991
+ delete pub.schedule;
992
+ writePub(file, pub);
993
+ console.log("agendamento cancelado");
994
+ return;
995
+ }
996
+ if (!has("confirm")) fail("agendar é publicar para fora, só que depois: só com pedido do usuário (--confirm)");
997
+ const when = parseWhen(option("when"));
998
+ /* another time for the same request (the studio calendar, dragging): platforms,
999
+ privacy and text stay; the attempt count restarts */
1000
+ if (has("reschedule")) {
1001
+ const pub = readJson(file) ?? {};
1002
+ if (!pub.schedule) fail("este vídeo não tem publicação agendada para remarcar");
1003
+ const before = pub.schedule.when;
1004
+ if (pub.schedule.cloud) {
1005
+ const r = await cloudApi("POST", `/api/schedule/${basename(dir)}/reschedule`, { when: when.toISOString() });
1006
+ if (!r.ok) fail("a cloud não achou este agendamento ativo: agende de novo");
1007
+ }
1008
+ pub.schedule = { ...reschedule(pub.schedule, when, Date.now()), ...(pub.schedule.cloud ? { cloud: true } : {}) };
1009
+ writePub(file, pub);
1010
+ console.log(`remarcado: de ${dayTime(before)} para ${dayTime(when)}`);
1011
+ return;
1012
+ }
1013
+ const { platforms, privacy, tiktokPrivacy } = publishRequest(false);
1014
+ exported(dir);
1015
+ /* the text is written and checked now, so you see what will go up */
1016
+ ensureText(dir);
1017
+ const pub = readJson(file) ?? {};
1018
+ const meta = metadata(dir, pub);
1019
+ if (meta.problems.length) fail("não dá para agendar:\n " + meta.problems.join("\n "));
1020
+ const already = platforms.filter(p => pub[p]?.id);
1021
+ if (already.length && !has("again")) fail(`já publicado em ${already.join(", ")}: --again para subir outra vez`);
1022
+
1023
+ pub.schedule = newSchedule({
1024
+ when,
1025
+ platforms,
1026
+ privacy,
1027
+ tiktokPrivacy,
1028
+ republish: has("again"),
1029
+ now: Date.now(),
1030
+ origin: process.env.OMNIVIDEO_ORIGIN ?? "terminal",
1031
+ });
1032
+ writePub(file, pub);
1033
+ if (cloudSchedules()) {
1034
+ try {
1035
+ await scheduleToCloud(dir);
1036
+ } catch (e) {
1037
+ const back = readJson(file) ?? {};
1038
+ delete back.schedule;
1039
+ writePub(file, back);
1040
+ fail(`a cloud não recebeu o agendamento (nada ficou agendado): ${e.message}`);
1041
+ }
1042
+ console.log(`agendado na cloud: ${platforms.join(", ")} em ${dayTime(when)}`);
1043
+ return;
1044
+ }
1045
+ if (!dir.startsWith(join(ROOT, "videos") + sep)) {
1046
+ mkdirSync(SCHEDULE_DIR, { recursive: true });
1047
+ const index = new Set(readJson(INDEX_FILE) ?? []);
1048
+ index.add(dir);
1049
+ writeFileSync(INDEX_FILE, JSON.stringify([...index], null, 2) + "\n");
1050
+ }
1051
+ console.log(`agendado: ${platforms.join(", ")} em ${dayTime(when)}`);
1052
+ if (!agentStatus().loaded)
1053
+ console.log(
1054
+ "aviso: o agente da agenda está desligado e nada sobe na hora: node tools/publish.mjs scheduler install",
1055
+ );
1056
+ }
1057
+
1058
+ /* --- the cloud schedule (cloud.schedule) ------------------------------------------------------
1059
+ What needs this machine is done when scheduling: the text, ffprobe, the cover JPEG and the omnichat
1060
+ rule the Reel will have. The MP4 and the cover go to the cloud's storage, and the job carries the
1061
+ rest; the cloud publishes at the time and keeps each platform's entry, which `scheduler run` (the
1062
+ agent) reads after the time and writes here. */
1063
+ const cloudSchedules = () => cloudSettings().owns.schedule;
1064
+
1065
+ async function cloudUpload(slug, name, file) {
1066
+ const c = cloudSettings();
1067
+ const r = await fetch(`${c.url}/api/schedule/${slug}/files/${name}`, {
1068
+ method: "PUT",
1069
+ headers: { authorization: `Bearer ${env("OMNICHAT_CLOUD_SECRET")}` },
1070
+ body: readFileSync(file),
1071
+ });
1072
+ const j = await r.json().catch(() => ({}));
1073
+ if (!r.ok || !j.key) throw new Error(`subir ${name}: ${r.status} ${j.error ?? ""}`.trim());
1074
+ return j.key;
1075
+ }
1076
+
1077
+ /* the rule the Reel will have, from `omnichat.mjs published --plan` (null: none goes live) */
1078
+ function omnichatPlan(dir) {
1079
+ const r = spawnSync(process.execPath, [join(CODE_ROOT, "tools", "omnichat.mjs"), "published", dir, "--plan"], {
1080
+ encoding: "utf8",
1081
+ timeout: 60_000,
1082
+ });
1083
+ try {
1084
+ return JSON.parse(r.stdout.trim().split("\n").pop() ?? "null");
1085
+ } catch {
1086
+ return null;
1087
+ }
1088
+ }
1089
+
1090
+ async function scheduleToCloud(dir) {
1091
+ const file = join(dir, "publication.json");
1092
+ const pub = readJson(file) ?? {};
1093
+ const sch = pub.schedule;
1094
+ const exp = exported(dir);
1095
+ const meta = metadata(dir, pub);
1096
+ const video = probeVideo(exp);
1097
+ const cover = sch.platforms.some(p => p !== "tiktok") ? videoCover(dir) : null;
1098
+ const slug = basename(dir);
1099
+ console.log(`subindo o vídeo para a cloud (${(statSync(exp.mp4).size / 1e6).toFixed(1)} MB)…`);
1100
+ try {
1101
+ const media = { video: await cloudUpload(slug, "video.mp4", exp.mp4), cover: null };
1102
+ if (cover) media.cover = await cloudUpload(slug, "cover.jpg", cover.jpg);
1103
+ const plan = sch.platforms.includes("instagram") ? omnichatPlan(dir) : null;
1104
+ await cloudApi("PUT", `/api/schedule/${slug}`, {
1105
+ job: {
1106
+ when: sch.when,
1107
+ platforms: sch.platforms,
1108
+ privacy: sch.privacy,
1109
+ tiktok_privacy: sch.tiktok_privacy,
1110
+ republish: !!sch.republish,
1111
+ meta,
1112
+ video,
1113
+ cover: cover ? { version: cover.version } : null,
1114
+ media,
1115
+ mp4: { rel: exp.rel, mtime: exp.mtime },
1116
+ before: Object.fromEntries(sch.platforms.map(p => [p, pub[p] ?? null])),
1117
+ settings: {
1118
+ youtube: P.youtube,
1119
+ tiktok: P.tiktok,
1120
+ instagram: { share_to_feed: !!P.instagram.share_to_feed, cover_at_s: P.instagram.cover_at_s ?? null },
1121
+ schedule: P.schedule,
1122
+ },
1123
+ omnichat: plan?.rule ? { rule: plan.rule } : null,
1124
+ },
1125
+ });
1126
+ } finally {
1127
+ if (cover)
1128
+ try {
1129
+ unlinkSync(cover.jpg);
1130
+ } catch {
1131
+ /* already gone */
1132
+ }
1133
+ }
1134
+ const now = readJson(file) ?? {};
1135
+ now.schedule = { ...now.schedule, cloud: true };
1136
+ writePub(file, now);
1137
+ }
1138
+
1139
+ /* After the time: the cloud's outcome into publication.json, as a local run would write it */
1140
+ async function syncFromCloud(dir) {
1141
+ const file = join(dir, "publication.json");
1142
+ const pub = readJson(file) ?? {};
1143
+ const sch = pub.schedule;
1144
+ const name = basename(dir);
1145
+ let j;
1146
+ try {
1147
+ j = await cloudApi("GET", `/api/schedule/${name}`);
1148
+ } catch (e) {
1149
+ console.log(`${nowIso()} ${name}: não deu para ler a cloud: ${e.message}`);
1150
+ return;
1151
+ }
1152
+ const steps = j.steps ?? {};
1153
+ for (const p of sch.platforms) if (steps[p]?.entry) pub[p] = steps[p].entry;
1154
+ const igLive = steps.instagram?.state === "done" && !sch.synced_instagram;
1155
+ if (j.state === "done") {
1156
+ delete pub.schedule;
1157
+ console.log(`${nowIso()} ${name}: publicado pela cloud`);
1158
+ } else if (j.state === "failed" || j.state === "canceled" || j.state === "missed") {
1159
+ const missing = sch.platforms.filter(p => steps[p]?.state !== "done");
1160
+ pub.schedule = {
1161
+ ...sch,
1162
+ platforms: missing,
1163
+ state: j.state === "canceled" ? "failed" : j.state,
1164
+ last_error: j.last_error ?? null,
1165
+ };
1166
+ console.log(`${nowIso()} ${name}: a cloud não publicou ${missing.join(", ")}: ${j.last_error?.message ?? j.state}`);
1167
+ } else {
1168
+ pub.schedule = {
1169
+ ...sch,
1170
+ state: "publishing",
1171
+ last_error: j.last_error ?? null,
1172
+ ...(igLive ? { synced_instagram: true } : {}),
1173
+ };
1174
+ }
1175
+ writePub(file, pub);
1176
+ /* the Reel went up: the local rule matches the one the cloud turned on (and goes up again, the same) */
1177
+ if (igLive) omnichatOnPublish(dir);
1178
+ }
1179
+
1180
+ /* cloud push: the credentials, the switch, and what was scheduled here before the switch */
1181
+ async function cloudPush() {
1182
+ if (!cloudSettings().ready) fail("cloud não configurada: cloud.url no config e OMNICHAT_CLOUD_SECRET no .env");
1183
+ const keys = [
1184
+ "YOUTUBE_CLIENT_ID",
1185
+ "YOUTUBE_CLIENT_SECRET",
1186
+ "YOUTUBE_REFRESH_TOKEN",
1187
+ "TIKTOK_CLIENT_KEY",
1188
+ "TIKTOK_CLIENT_SECRET",
1189
+ "TIKTOK_ACCESS_TOKEN",
1190
+ "TIKTOK_ACCESS_EXPIRES",
1191
+ "TIKTOK_REFRESH_TOKEN",
1192
+ "TIKTOK_REFRESH_EXPIRES",
1193
+ "TIKTOK_OPEN_ID",
1194
+ "TIKTOK_SCOPES",
1195
+ ];
1196
+ const values = Object.fromEntries(keys.map(k => [k, env(k)]).filter(([, v]) => v));
1197
+ await cloudApi("PUT", "/api/platform-env", { values });
1198
+ const { token, id } = await instagramToken();
1199
+ await cloudApi("PUT", "/api/account", { id, token, expires: env("INSTAGRAM_TOKEN_EXPIRES") || null });
1200
+ await cloudApi("PUT", "/api/schedule/active", { active: cloudSchedules() });
1201
+ console.log(`credenciais na cloud: ${Object.keys(values).length ? Object.keys(values).join(", ") : "nenhuma"}`);
1202
+ if (!cloudSchedules()) {
1203
+ console.log("a cloud recebeu, mas quem publica a agenda ainda é esta máquina (cloud.schedule: true para virar)");
1204
+ return;
1205
+ }
1206
+ for (const { dir, sch } of scheduled()) {
1207
+ if (sch.cloud || sch.state !== "scheduled") continue;
1208
+ await scheduleToCloud(dir);
1209
+ console.log(` ${basename(dir)}: agendamento de ${dayTime(sch.when)} foi para a cloud`);
1210
+ }
1211
+ console.log("a cloud publica a agenda a partir de agora (cloud.schedule)");
1212
+ }
1213
+
1214
+ function scheduled() {
1215
+ const base = join(ROOT, "videos");
1216
+ let names = [];
1217
+ try {
1218
+ names = readdirSync(base);
1219
+ } catch {
1220
+ /* no videos/ */
1221
+ }
1222
+ const dirs = new Set([...names.map(n => join(base, n)), ...(readJson(INDEX_FILE) ?? [])]);
1223
+ return [...dirs]
1224
+ .map(dir => ({ dir, sch: readJson(join(dir, "publication.json"))?.schedule }))
1225
+ .filter(x => x.sch)
1226
+ .sort((a, b) => a.sch.when.localeCompare(b.sch.when));
1227
+ }
1228
+
1229
+ function listSchedule() {
1230
+ const items = scheduled().map(({ dir, sch }) => ({ slug: basename(dir), dir, ...sch }));
1231
+ const agent = agentStatus();
1232
+ if (has("json")) {
1233
+ console.log(JSON.stringify({ agent, items }));
1234
+ return;
1235
+ }
1236
+ console.log(
1237
+ `agente: ${agent.loaded ? "ligado, confere a cada minuto" : "desligado (scheduler install)"}${agent.installed && !agent.node_ok ? " · o node do agente sumiu: rode scheduler install de novo" : ""}`,
1238
+ );
1239
+ if (!items.length) console.log("nada agendado");
1240
+ for (const i of items) {
1241
+ const error = i.last_error ? ` · ${i.last_error.message}` : "";
1242
+ console.log(`${dayTime(i.when)} ${i.state.padEnd(10)} ${i.platforms.join(", ").padEnd(26)} ${i.slug}${error}`);
1243
+ }
1244
+ }
1245
+
1246
+ /* one `scheduler run` at a time: the every-minute agent and someone in the terminal don't upload the same video twice */
1247
+ function lock() {
1248
+ mkdirSync(SCHEDULE_DIR, { recursive: true });
1249
+ for (let i = 0; i < 2; i++) {
1250
+ try {
1251
+ const fd = openSync(LOCK_FILE, "wx");
1252
+ writeSync(fd, String(process.pid));
1253
+ closeSync(fd);
1254
+ return () => {
1255
+ try {
1256
+ unlinkSync(LOCK_FILE);
1257
+ } catch {
1258
+ /* already gone */
1259
+ }
1260
+ };
1261
+ } catch (e) {
1262
+ if (e.code !== "EEXIST") throw e;
1263
+ if (alive(+readFileSync(LOCK_FILE, "utf8"))) return null;
1264
+ try {
1265
+ unlinkSync(LOCK_FILE);
1266
+ } catch {
1267
+ /* someone else took it */
1268
+ }
1269
+ }
1270
+ }
1271
+ return null;
1272
+ }
1273
+
1274
+ async function runScheduler() {
1275
+ const release = lock();
1276
+ if (!release) return;
1277
+ try {
1278
+ for (const { dir, sch } of scheduled()) {
1279
+ if (cloudSchedules()) {
1280
+ /* the cloud publishes: here only its outcome is read, and only after the time */
1281
+ if (sch.cloud && ["scheduled", "publishing"].includes(sch.state) && Date.parse(sch.when) <= Date.now())
1282
+ await syncFromCloud(dir);
1283
+ else if (!sch.cloud && sch.state === "scheduled")
1284
+ console.log(`${nowIso()} ${basename(dir)}: agendado antes da cloud: node tools/publish.mjs cloud push`);
1285
+ continue;
1286
+ }
1287
+ if (sch.cloud) continue;
1288
+ runOne(dir);
1289
+ }
1290
+ } finally {
1291
+ release();
1292
+ }
1293
+ }
1294
+
1295
+ function runOne(dir) {
1296
+ const file = join(dir, "publication.json");
1297
+ const pub = readJson(file);
1298
+ const sch = pub?.schedule;
1299
+ const name = basename(dir);
1300
+ const due = dueAction(sch, { now: Date.now(), alive, settings: P.schedule });
1301
+ if (due.action === "skip") return;
1302
+ if (due.action === "missed") {
1303
+ markMissed(sch, due.message, Date.now());
1304
+ writePub(file, pub);
1305
+ console.log(`${nowIso()} ${name}: perdido, ${sch.last_error.message}`);
1306
+ return;
1307
+ }
1308
+
1309
+ const startedAt = nowIso();
1310
+ startAttempt(sch, { pid: process.pid, startedAt });
1311
+ writePub(file, pub);
1312
+ console.log(
1313
+ `${startedAt} ${name}: publicando ${sch.platforms.join(", ")} (agendado para ${dayTime(sch.when)}, tentativa ${sch.attempts})`,
1314
+ );
1315
+ const r = spawnSync(
1316
+ process.execPath,
1317
+ [
1318
+ SELF,
1319
+ dir,
1320
+ ...sch.platforms.map(p => `--${p}`),
1321
+ "--privacy",
1322
+ sch.privacy,
1323
+ "--tiktok-privacy",
1324
+ sch.tiktok_privacy,
1325
+ "--confirm",
1326
+ ...(sch.republish ? ["--again"] : []),
1327
+ ],
1328
+ {
1329
+ cwd: ROOT,
1330
+ encoding: "utf8",
1331
+ timeout: 45 * 60_000,
1332
+ maxBuffer: 16 * MB,
1333
+ env: { ...process.env, OMNIVIDEO_ORIGIN: "schedule", OMNIVIDEO_SCHEDULED_FOR: sch.when },
1334
+ },
1335
+ );
1336
+ process.stdout.write(r.stdout ?? "");
1337
+ process.stderr.write(r.stderr ?? "");
1338
+
1339
+ const after = readJson(file) ?? {};
1340
+ const outcome = attemptOutcome({
1341
+ sch,
1342
+ after,
1343
+ startedAt,
1344
+ reason: () => failureReason({ stderr: r.stderr, error: r.error, status: r.status }),
1345
+ now: Date.now(),
1346
+ settings: P.schedule,
1347
+ });
1348
+ if (outcome.done) {
1349
+ delete after.schedule;
1350
+ writePub(file, after);
1351
+ console.log(`${nowIso()} ${name}: publicado`);
1352
+ return;
1353
+ }
1354
+ after.schedule = outcome.schedule;
1355
+ writePub(file, after);
1356
+ const { missing, giveUp, reason } = outcome;
1357
+ console.log(
1358
+ `${nowIso()} ${name}: ${giveUp ? `falhou em ${missing.join(", ")}, desisti depois de ${outcome.schedule.attempts} tentativas` : `falhou em ${missing.join(", ")}, tento de novo em ${P.schedule.interval_min} min`}: ${reason}`,
1359
+ );
1360
+ }
1361
+
1362
+ /* --- the launchd agent ---------------------------------------------------------------
1363
+ Runs `scheduler run` every 60 s with the node that installed it. PATH goes along because
1364
+ launchd starts with a minimal PATH, without ffprobe, cloudflared or claude. */
1365
+ const domain = () => `gui/${process.getuid()}`;
1366
+ const launchctl = (...a) => spawnSync("launchctl", a, { encoding: "utf8" });
1367
+
1368
+ function agentStatus() {
1369
+ const installed = existsSync(PLIST);
1370
+ let node = null;
1371
+ if (installed) {
1372
+ try {
1373
+ node =
1374
+ /<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]+)<\/string>/.exec(readFileSync(PLIST, "utf8"))?.[1] ??
1375
+ null;
1376
+ } catch {
1377
+ /* unreadable */
1378
+ }
1379
+ }
1380
+ return {
1381
+ installed,
1382
+ loaded: installed && launchctl("print", `${domain()}/${AGENT}`).status === 0,
1383
+ node_ok: !!node && existsSync(node),
1384
+ log: LOG_FILE,
1385
+ };
1386
+ }
1387
+
1388
+ function installAgent() {
1389
+ const x = s => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1390
+ const path = [
1391
+ ...new Set([
1392
+ dirname(process.execPath),
1393
+ "/opt/homebrew/bin",
1394
+ "/usr/local/bin",
1395
+ ...(process.env.PATH ?? "").split(":"),
1396
+ "/usr/bin",
1397
+ "/bin",
1398
+ "/usr/sbin",
1399
+ "/sbin",
1400
+ ]),
1401
+ ]
1402
+ .filter(Boolean)
1403
+ .join(":");
1404
+ mkdirSync(SCHEDULE_DIR, { recursive: true });
1405
+ mkdirSync(dirname(PLIST), { recursive: true });
1406
+ writeFileSync(
1407
+ PLIST,
1408
+ `<?xml version="1.0" encoding="UTF-8"?>
1409
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1410
+ <plist version="1.0">
1411
+ <dict>
1412
+ <key>Label</key><string>${AGENT}</string>
1413
+ <key>ProgramArguments</key>
1414
+ <array>
1415
+ <string>${x(process.execPath)}</string>
1416
+ <string>${x(SELF)}</string>
1417
+ <string>scheduler</string>
1418
+ <string>run</string>
1419
+ </array>
1420
+ <key>WorkingDirectory</key><string>${x(ROOT)}</string>
1421
+ <key>EnvironmentVariables</key>
1422
+ <dict><key>PATH</key><string>${x(path)}</string>${homeEnvPlist(x)}</dict>
1423
+ <key>StartInterval</key><integer>60</integer>
1424
+ <key>RunAtLoad</key><true/>
1425
+ <key>StandardOutPath</key><string>${x(LOG_FILE)}</string>
1426
+ <key>StandardErrorPath</key><string>${x(LOG_FILE)}</string>
1427
+ </dict>
1428
+ </plist>
1429
+ `,
1430
+ );
1431
+ /* reinstalling replaces what was loaded; launchd sometimes takes a moment to release the old one */
1432
+ launchctl("bootout", `${domain()}/${AGENT}`);
1433
+ let r;
1434
+ for (let i = 0; i < 6; i++) {
1435
+ r = launchctl("bootstrap", domain(), PLIST);
1436
+ if (r.status === 0) break;
1437
+ spawnSync("sleep", ["0.5"]);
1438
+ }
1439
+ if (r.status !== 0) fail(`launchctl bootstrap: ${(r.stderr || r.stdout).trim()}`);
1440
+ console.log(
1441
+ `agente da agenda ligado: confere a cada minuto, com o studio aberto ou não (log em .schedule/schedule.log)`,
1442
+ );
1443
+ }
1444
+
1445
+ function uninstallAgent() {
1446
+ launchctl("bootout", `${domain()}/${AGENT}`);
1447
+ try {
1448
+ unlinkSync(PLIST);
1449
+ } catch {
1450
+ /* was not there */
1451
+ }
1452
+ const n = scheduled().filter(i => ["scheduled", "publishing"].includes(i.sch.state)).length;
1453
+ console.log(`agente da agenda desligado${n ? `: ${n} agendamento(s) ficam parados até você ligar de novo` : ""}`);
1454
+ }
1455
+
1456
+ /* ============================================================================ */
1457
+ const [cmd, sub] = args;
1458
+ const isVideo = p => p && !p.startsWith("--") && existsSync(join(resolve(p), "scenes.json"));
1459
+ try {
1460
+ if (cmd === "status") await status();
1461
+ else if (cmd === "auth" && sub === "youtube") await authYoutube();
1462
+ else if (cmd === "auth" && sub === "tiktok") await authTiktok();
1463
+ else if (cmd === "auth" && sub === "instagram") await authInstagram(args[2] ?? process.env.OMNIVIDEO_IG_TOKEN);
1464
+ else if (cmd === "text" && isVideo(sub)) text(resolve(sub));
1465
+ else if (cmd === "cover" && isVideo(sub)) {
1466
+ const c = videoCover(resolve(sub));
1467
+ console.log(
1468
+ c
1469
+ ? `capa: thumbnail ${c.version} (${(statSync(c.jpg).size / 1024).toFixed(0)} KB em JPG) vai para o YouTube e o Instagram; o TikTok leva o quadro de cover_at_s`
1470
+ : "sem thumbnail selecionada: cada plataforma usa um quadro do vídeo",
1471
+ );
1472
+ if (c) unlinkSync(c.jpg);
1473
+ } else if (cmd === "schedule" && isVideo(sub)) await schedule(resolve(sub));
1474
+ else if (cmd === "carousel" && sub && existsSync(join(resolve(sub), "carousel", "carousel.json")))
1475
+ await publishCarousel(resolve(sub));
1476
+ else if (cmd === "scheduler" && (!sub || sub === "--json")) listSchedule();
1477
+ else if (cmd === "scheduler" && sub === "run") await runScheduler();
1478
+ else if (cmd === "cloud" && sub === "push") await cloudPush();
1479
+ else if (cmd === "scheduler" && sub === "install") installAgent();
1480
+ else if (cmd === "scheduler" && sub === "uninstall") uninstallAgent();
1481
+ else if (isVideo(cmd)) await publish(resolve(cmd));
1482
+ else {
1483
+ const source = readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n");
1484
+ console.log(
1485
+ source
1486
+ .slice(
1487
+ 1,
1488
+ source.findIndex((l, i) => i > 1 && /^\s*=+\s*\*\/\s*$/.test(l)),
1489
+ )
1490
+ .join("\n"),
1491
+ );
1492
+ process.exit(2);
1493
+ }
1494
+ } catch (e) {
1495
+ fail(e.message);
1496
+ }