@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,983 @@
1
+ /* Settings editable from the /settings screen. They live in two places:
2
+ - omnivideo.config.json: voice_id, Fish model, price, Claude models, the agent accounts
3
+ (agents: N Claude dirs and N Codex homes, never ~/.claude);
4
+ - .env (outside git): the API keys. A key never goes back whole to the
5
+ UI, only masked.
6
+ tools/voice.py rereads both on every `voice`; the studio rereads the config on every session. */
7
+
8
+ import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
9
+ import { createHash } from "node:crypto";
10
+ import { homedir } from "node:os";
11
+ import { join, resolve } from "node:path";
12
+ import { AVATAR_SEED } from "../../shared/schemas/agent";
13
+ import { ROOT, PYTHON, TOOLS_DIR, readJson, readText, run } from "./root";
14
+ import { forgetUsage } from "./usage";
15
+ import { forgetAccounts } from "./publish";
16
+ import { MODELS, EFFORTS, validate, inSkill, applyModels } from "../../../tools/models.mjs";
17
+ import {
18
+ accountsOf,
19
+ claudeBin,
20
+ cleanAccounts,
21
+ clearLimit,
22
+ codexBin,
23
+ codexModels,
24
+ codexSettings,
25
+ envFor,
26
+ expandHome,
27
+ limitedUntil,
28
+ loginCommand,
29
+ loginOf,
30
+ pickAccount,
31
+ tildify,
32
+ } from "../../../tools/lib/agents.mjs";
33
+
34
+ type Resp = { ok: true; output: string } | { ok: false; error: string };
35
+
36
+ const CONFIG_FILE = join(ROOT, "omnivideo.config.json");
37
+ const ENV_FILE = join(ROOT, ".env");
38
+
39
+ /* like the file is written by hand: short lists and objects on a single line */
40
+ const json = (v: unknown) =>
41
+ JSON.stringify(v, null, 2)
42
+ .replace(/\[\n\s+([^[\]{}]*?)\n\s*\]/g, (all, items: string) => {
43
+ const line = `[${items.split(/,\n\s+/).join(", ")}]`;
44
+ return line.length <= 72 ? line : all;
45
+ })
46
+ .replace(/\{\n\s+([^[\]{}]*?)\n\s*\}/g, (all, items: string) => {
47
+ const line = `{ ${items.split(/,\n\s+/).join(", ")} }`;
48
+ return line.length <= 90 ? line : all;
49
+ });
50
+
51
+ export const readConfig = () => readJson<any>(CONFIG_FILE);
52
+
53
+ export function writeConfig(cfg: unknown) {
54
+ const tmp = `${CONFIG_FILE}.${process.pid}.tmp`;
55
+ writeFileSync(tmp, json(cfg) + "\n");
56
+ renameSync(tmp, CONFIG_FILE);
57
+ forgetUsage();
58
+ }
59
+
60
+ /* replaces (or adds) a key in the object without changing the order of the others */
61
+ function withKey(obj: Record<string, unknown>, k: string, v: unknown, after?: string) {
62
+ if (k in obj || !after || !(after in obj)) return { ...obj, [k]: v };
63
+ const next: Record<string, unknown> = {};
64
+ for (const [kk, vv] of Object.entries(obj)) {
65
+ next[kk] = vv;
66
+ if (kk === after) next[k] = v;
67
+ }
68
+ return next;
69
+ }
70
+
71
+ /* --- .env ------------------------------------------------------------------------ */
72
+ const PUBLISH_ENV_KEYS = ["YOUTUBE_CLIENT_ID", "YOUTUBE_CLIENT_SECRET", "TIKTOK_CLIENT_KEY", "TIKTOK_CLIENT_SECRET"];
73
+ export const ENV_KEYS = [
74
+ "FISH_API_KEY",
75
+ "CARTESIA_API_KEY",
76
+ "CARTESIA_ADMIN_API_KEY",
77
+ "ELEVENLABS_API_KEY",
78
+ ...PUBLISH_ENV_KEYS,
79
+ ];
80
+
81
+ function readEnv(): Record<string, string> {
82
+ const vals: Record<string, string> = {};
83
+ for (const line of (readText(ENV_FILE) ?? "").split("\n")) {
84
+ const l = line.trim();
85
+ if (!l || l.startsWith("#") || !l.includes("=")) continue;
86
+ const i = l.indexOf("=");
87
+ vals[l.slice(0, i).trim()] = l
88
+ .slice(i + 1)
89
+ .trim()
90
+ .replace(/^["']|["']$/g, "");
91
+ }
92
+ return vals;
93
+ }
94
+
95
+ const mask = (v: string) => (v.length <= 12 ? "•".repeat(v.length) : `${v.slice(0, 7)}…${v.slice(-4)}`);
96
+
97
+ /* voice.py uses the environment variable before the .env one: flag when it hides the file */
98
+ function keyStatus(name: string, env: Record<string, string>) {
99
+ const fromFile = env[name] || "";
100
+ const fromEnv = process.env[name] || "";
101
+ return {
102
+ name,
103
+ masked: fromFile ? mask(fromFile) : null,
104
+ in_env: !!fromEnv && fromEnv !== fromFile,
105
+ };
106
+ }
107
+
108
+ export function saveKey(name: unknown, value: unknown): Resp {
109
+ if (typeof name !== "string" || !ENV_KEYS.includes(name)) return { ok: false, error: `chave desconhecida: ${name}` };
110
+ const v = typeof value === "string" ? value.trim() : "";
111
+ if (/[\s"'#]/.test(v)) return { ok: false, error: "a chave não pode ter espaço, aspas nem #" };
112
+ const lines = (readText(ENV_FILE) ?? "").split("\n");
113
+ const i = lines.findIndex(l => new RegExp(`^\\s*${name}\\s*=`).test(l));
114
+ if (i >= 0) lines[i] = `${name}=${v}`;
115
+ else {
116
+ while (lines.length && lines.at(-1) === "") lines.pop();
117
+ lines.push(`${name}=${v}`, "");
118
+ }
119
+ const tmp = `${ENV_FILE}.${process.pid}.tmp`;
120
+ writeFileSync(tmp, lines.join("\n"), { mode: 0o600 });
121
+ renameSync(tmp, ENV_FILE);
122
+ forgetUsage();
123
+ const isPublish = PUBLISH_ENV_KEYS.includes(name);
124
+ if (isPublish) forgetAccounts();
125
+ return { ok: true, output: v ? `gravada: vale para a próxima ${isPublish ? "conexão" : "voz"}` : "apagada" };
126
+ }
127
+
128
+ /* --- voice --------------------------------------------------------------------------- */
129
+ export const FISH_MODELS = ["s2.1-pro-free", "s2.1-pro"];
130
+
131
+ /* the free one (`s2.1-pro-free`, no cost) or the paid one (`s2.1-pro`, charged to the API balance).
132
+ The fallback is tried when the first fails; null = no fallback. */
133
+ export function setFishModel(model: unknown, fallback: unknown): Resp {
134
+ if (typeof model !== "string" || !FISH_MODELS.includes(model))
135
+ return { ok: false, error: `modelo desconhecido: ${model}` };
136
+ if (fallback !== null && (typeof fallback !== "string" || !FISH_MODELS.includes(fallback) || fallback === model))
137
+ return { ok: false, error: `fallback inválido: ${fallback}` };
138
+ const cfg = readConfig();
139
+ if (!cfg?.voice?.fish) return { ok: false, error: "omnivideo.config.json sem voice.fish" };
140
+ /* same key order, the fallback right after the model */
141
+ const fish: Record<string, unknown> = {};
142
+ for (const [k, v] of Object.entries(cfg.voice.fish)) {
143
+ if (k === "fallback_model") continue;
144
+ fish[k] = k === "model" ? model : v;
145
+ if (k === "model" && fallback) fish.fallback_model = fallback;
146
+ }
147
+ cfg.voice.fish = fish;
148
+ writeConfig(cfg);
149
+ return { ok: true, output: fallback ? `${model}, com ${fallback} se falhar` : `${model}, sem fallback` };
150
+ }
151
+
152
+ /* Default voice: the first of `voice.order`. The others stay as fallback, in the order
153
+ they were. Applies to new videos and to those that didn't choose their own
154
+ (`voice.provider` in scenes.json); one that already has lines doesn't regenerate. */
155
+ export const VOICE_PROVIDERS = ["elevenlabs", "fish", "cartesia", "voicestudio"];
156
+
157
+ export function setDefaultVoice(provider: unknown): Resp {
158
+ const p = String(provider);
159
+ if (!VOICE_PROVIDERS.includes(p)) return { ok: false, error: `provider desconhecido: ${provider}` };
160
+ const cfg = readConfig();
161
+ const order: string[] = cfg?.voice?.order ?? VOICE_PROVIDERS;
162
+ if (order[0] === p) return { ok: true, output: "já era a padrão" };
163
+ cfg.voice.order = [p, ...order.filter(x => x !== p), ...VOICE_PROVIDERS.filter(x => x !== p && !order.includes(x))];
164
+ writeConfig(cfg);
165
+ return {
166
+ ok: true,
167
+ output: `gravado: ${cfg.voice.order.join(" → ")}. Vale para vídeo novo e para o que não escolheu a voz`,
168
+ };
169
+ }
170
+
171
+ /* cloned voice: on Fish the voice model's reference_id (32 hex), on Cartesia the voice id (UUID),
172
+ on ElevenLabs the voice_id (20 alphanumeric chars) */
173
+ const VOICE_ID_FORMAT: Record<string, { re: RegExp; name: string }> = {
174
+ fish: { re: /^[0-9a-f]{32}$/i, name: "32 caracteres hexadecimais" },
175
+ cartesia: { re: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, name: "um UUID" },
176
+ elevenlabs: { re: /^[A-Za-z0-9]{20}$/, name: "20 letras e números" },
177
+ };
178
+ const VOICE_ID_PROVIDER_NAME: Record<string, string> = { fish: "Fish", cartesia: "Cartesia", elevenlabs: "ElevenLabs" };
179
+
180
+ export function setVoiceId(provider: unknown, id: unknown): Resp {
181
+ const format = VOICE_ID_FORMAT[String(provider)];
182
+ if (!format) return { ok: false, error: `provider desconhecido: ${provider}` };
183
+ const v = typeof id === "string" ? id.trim() : "";
184
+ if (!format.re.test(v))
185
+ return { ok: false, error: `o voice_id da ${VOICE_ID_PROVIDER_NAME[String(provider)]} é ${format.name}` };
186
+ const cfg = readConfig();
187
+ const p = cfg?.voice?.[provider as string];
188
+ if (!p) return { ok: false, error: `omnivideo.config.json sem voice.${provider}` };
189
+ if (p.voice_id === v) return { ok: true, output: "já era este" };
190
+ cfg.voice[provider as string] = withKey(p, "voice_id", v, "key_env");
191
+ writeConfig(cfg);
192
+ return { ok: true, output: "gravado: vale para a próxima voz" };
193
+ }
194
+
195
+ /* --- voice profiles --------------------------------------------------------------------
196
+ `voice.profiles`: each profile is a voice with a name, the providers it has (the Fish
197
+ voice_id, the Cartesia one, the VoiceStudio profile name) and the attempt `order`
198
+ among them: the first is preferred, the others are fallback. `voice.default_profile`
199
+ is the id of the active voice, the one preselected on a new video. A video picks its own in
200
+ `voice.voice_profile` of scenes.json (tools/voice.py resolves it). Without any profile, each
201
+ provider's voice_id and `voice.order` apply. */
202
+ export type VoiceProfile = {
203
+ id: string;
204
+ name: string;
205
+ order: string[];
206
+ fish: string | null;
207
+ cartesia: string | null;
208
+ voicestudio: string | null;
209
+ elevenlabs: string | null;
210
+ /* the avatar seed (`h<hue>-<suffix>`), chosen when cloning; without it the id is used */
211
+ avatar: string | null;
212
+ };
213
+
214
+ const AVATAR_RE = /^h\d{1,3}-[a-z0-9]{3,12}$/;
215
+
216
+ const profileSlug = (name: string) =>
217
+ name
218
+ .normalize("NFD")
219
+ .replace(/[̀-ͯ]/g, "")
220
+ .toLowerCase()
221
+ .replace(/[^a-z0-9]+/g, "-")
222
+ .replace(/^-+|-+$/g, "")
223
+ .slice(0, 32);
224
+
225
+ /* the profile's order only has providers it configured; configured ones that are missing
226
+ go at the end, in the default order */
227
+ function orderOf(
228
+ profile: Pick<VoiceProfile, "fish" | "cartesia" | "voicestudio" | "elevenlabs">,
229
+ requested: unknown,
230
+ ): string[] {
231
+ const has = (p: string) => !!profile[p as "fish" | "cartesia" | "voicestudio" | "elevenlabs"];
232
+ const given = Array.isArray(requested)
233
+ ? requested.map(String).filter(p => VOICE_PROVIDERS.includes(p) && has(p))
234
+ : [];
235
+ const unique = given.filter((p, i) => given.indexOf(p) === i);
236
+ return [...unique, ...VOICE_PROVIDERS.filter(p => has(p) && !unique.includes(p))];
237
+ }
238
+
239
+ const profilesOf = (cfg: any): VoiceProfile[] =>
240
+ ((cfg?.voice?.profiles ?? []) as any[]).map(p => {
241
+ const base = {
242
+ fish: p.fish ?? null,
243
+ cartesia: p.cartesia ?? null,
244
+ voicestudio: p.voicestudio ?? null,
245
+ elevenlabs: p.elevenlabs ?? null,
246
+ };
247
+ const avatar = typeof p.avatar === "string" && AVATAR_RE.test(p.avatar) ? p.avatar : null;
248
+ return { id: String(p.id), name: String(p.name ?? p.id), order: orderOf(base, p.order), ...base, avatar };
249
+ });
250
+
251
+ export const getProfile = (id: unknown): VoiceProfile | undefined => profilesOf(readConfig()).find(p => p.id === id);
252
+
253
+ const toConfig = (p: VoiceProfile) => ({
254
+ id: p.id,
255
+ name: p.name,
256
+ order: p.order,
257
+ ...(p.fish ? { fish: p.fish } : {}),
258
+ ...(p.cartesia ? { cartesia: p.cartesia } : {}),
259
+ ...(p.voicestudio ? { voicestudio: p.voicestudio } : {}),
260
+ ...(p.elevenlabs ? { elevenlabs: p.elevenlabs } : {}),
261
+ ...(p.avatar ? { avatar: p.avatar } : {}),
262
+ });
263
+
264
+ /* one profile at a time: empty id creates (new id from the name), existing id edits */
265
+ export function saveProfile(data: {
266
+ id?: unknown;
267
+ name?: unknown;
268
+ order?: unknown;
269
+ fish?: unknown;
270
+ cartesia?: unknown;
271
+ voicestudio?: unknown;
272
+ elevenlabs?: unknown;
273
+ avatar?: unknown;
274
+ }): Resp & { id?: string } {
275
+ const name = typeof data.name === "string" ? data.name.trim() : "";
276
+ if (!name || name.length > 40) return { ok: false, error: "dê um nome à voz (até 40 caracteres)" };
277
+ const str = (v: unknown) => (typeof v === "string" ? v.trim() : "");
278
+ const fish = str(data.fish),
279
+ cartesia = str(data.cartesia),
280
+ voicestudio = str(data.voicestudio),
281
+ elevenlabs = str(data.elevenlabs);
282
+ if (!fish && !cartesia && !voicestudio && !elevenlabs)
283
+ return { ok: false, error: "escolha ao menos um provedor e informe a voz dele" };
284
+ if (fish && !VOICE_ID_FORMAT.fish.re.test(fish))
285
+ return { ok: false, error: `o Voice ID da Fish é ${VOICE_ID_FORMAT.fish.name}` };
286
+ if (cartesia && !VOICE_ID_FORMAT.cartesia.re.test(cartesia))
287
+ return { ok: false, error: `o Voice ID da Cartesia é ${VOICE_ID_FORMAT.cartesia.name}` };
288
+ if (elevenlabs && !VOICE_ID_FORMAT.elevenlabs.re.test(elevenlabs))
289
+ return { ok: false, error: `o Voice ID da ElevenLabs é ${VOICE_ID_FORMAT.elevenlabs.name}` };
290
+ if (voicestudio.length > 80)
291
+ return { ok: false, error: "o nome do perfil no VoiceStudio é curto demais para ser real (até 80 caracteres)" };
292
+
293
+ const cfg = readConfig();
294
+ if (!cfg?.voice) return { ok: false, error: "omnivideo.config.json sem voice" };
295
+ const profiles = profilesOf(cfg);
296
+ const requested = str(data.id);
297
+ const existing = requested ? profiles.find(p => p.id === requested) : undefined;
298
+ if (requested && !existing) return { ok: false, error: `a voz “${requested}” não existe` };
299
+ if (profiles.some(p => p.id !== requested && p.name.toLowerCase() === name.toLowerCase()))
300
+ return { ok: false, error: `já existe uma voz chamada “${name}”` };
301
+
302
+ let id = existing?.id ?? "";
303
+ if (!id) {
304
+ const base = profileSlug(name) || "voz";
305
+ id = base;
306
+ for (let n = 2; profiles.some(p => p.id === id); n++) id = `${base}-${n}`;
307
+ }
308
+ const voiceIds = {
309
+ fish: fish || null,
310
+ cartesia: cartesia || null,
311
+ voicestudio: voicestudio || null,
312
+ elevenlabs: elevenlabs || null,
313
+ };
314
+ /* without an avatar in the request (the editor doesn't send one), the voice keeps what it had */
315
+ const avatar =
316
+ typeof data.avatar === "string" && AVATAR_RE.test(data.avatar) ? data.avatar : (existing?.avatar ?? null);
317
+ const item: VoiceProfile = { id, name, order: orderOf(voiceIds, data.order), ...voiceIds, avatar };
318
+ const list = existing ? profiles.map(p => (p.id === id ? item : p)) : [...profiles, item];
319
+ cfg.voice = withKey(cfg.voice, "profiles", list.map(toConfig), "order");
320
+ /* the first voice created is already the active one: without this a new video would fall back to the loose voice_id */
321
+ if (!cfg.voice.default_profile || !list.some(p => p.id === cfg.voice.default_profile))
322
+ cfg.voice = withKey(cfg.voice, "default_profile", id, "profiles");
323
+ writeConfig(cfg);
324
+ generateSamples(item);
325
+ return {
326
+ ok: true,
327
+ id,
328
+ output: existing ? "gravado: vale para a próxima voz dos vídeos que usam esta voz" : "voz criada",
329
+ };
330
+ }
331
+
332
+ /* "Ouvir voz": a short phrase in the voice of each of the profile's providers (tools/voice_sample.py).
333
+ One sample per provider, in .voice-samples/<profile>-<provider>-<hash of voice_id and phrase>.wav:
334
+ name, order and avatar are not part of the key, so only changing that provider's
335
+ voice_id (or the phrase) makes a new one, and the same voice is never billed twice. The
336
+ samples are generated in the background when the voice is created or edited; playing only regenerates
337
+ if generation failed. */
338
+ export const SAMPLES_DIR = join(ROOT, ".voice-samples");
339
+ const PROVIDER_NAME: Record<string, string> = {
340
+ fish: "Fish Audio",
341
+ cartesia: "Cartesia",
342
+ voicestudio: "VoiceStudio",
343
+ elevenlabs: "ElevenLabs",
344
+ };
345
+ const SAMPLE_PHRASE = "Esta é a minha voz. É assim que eu vou narrar os seus vídeos.";
346
+ type Sample = { ok: true; url: string; provider: string } | { ok: false; error: string };
347
+ const inProgress = new Map<string, Promise<{ ok: true } | { ok: false; error: string }>>();
348
+
349
+ const sampleFile = (profile: VoiceProfile, provider: string) => {
350
+ const key = createHash("sha1")
351
+ .update(JSON.stringify({ provider, voice: (profile as any)[provider], phrase: SAMPLE_PHRASE }))
352
+ .digest("hex")
353
+ .slice(0, 10);
354
+ return `${profile.id}-${provider}-${key}.wav`;
355
+ };
356
+
357
+ /* removes the profile's old samples (of `provider`, or all), except `keep` */
358
+ function clearSamples(id: string, provider?: string, keep?: string) {
359
+ if (!existsSync(SAMPLES_DIR)) return;
360
+ const re = new RegExp(`^${id}-(?:(?:${provider ?? VOICE_PROVIDERS.join("|")})-)?[0-9a-f]{10}\\.wav$`);
361
+ for (const file of readdirSync(SAMPLES_DIR)) {
362
+ if (file !== keep && re.test(file)) rmSync(join(SAMPLES_DIR, file), { force: true });
363
+ }
364
+ }
365
+
366
+ async function providerSample(profile: VoiceProfile, provider: string): Promise<Sample> {
367
+ if (!(profile as any)[provider])
368
+ return { ok: false, error: `a voz “${profile.name}” não tem voz no ${PROVIDER_NAME[provider] ?? provider}` };
369
+ const file = sampleFile(profile, provider);
370
+ const dest = join(SAMPLES_DIR, file);
371
+ const done: Sample = { ok: true, url: `/api/voice-sample/${file}`, provider };
372
+ if (existsSync(dest)) return done;
373
+ /* two requests for the same sample (the click and the background generation) wait on the same process */
374
+ let p = inProgress.get(file);
375
+ if (!p) {
376
+ p = (async () => {
377
+ mkdirSync(SAMPLES_DIR, { recursive: true });
378
+ const r = await run([
379
+ PYTHON,
380
+ join(TOOLS_DIR, "voice_sample.py"),
381
+ profile.id,
382
+ dest,
383
+ "--provider",
384
+ provider,
385
+ "--phrase",
386
+ SAMPLE_PHRASE,
387
+ ]);
388
+ if (r.code !== 0)
389
+ return { ok: false as const, error: r.stderr.trim().split("\n").pop() || "não consegui gerar a amostra" };
390
+ clearSamples(profile.id, provider, file);
391
+ return { ok: true as const };
392
+ })().finally(() => inProgress.delete(file));
393
+ inProgress.set(file, p);
394
+ }
395
+ const r = await p;
396
+ return r.ok ? done : r;
397
+ }
398
+
399
+ /* empty `provider`: the voice as a video would use it, the first ready sample in the profile's order
400
+ or, with none, the first that comes out (with fallback). With `provider`: only that one. */
401
+ export async function voiceSample(
402
+ id: unknown,
403
+ provider?: unknown,
404
+ ): Promise<Resp & { url?: string; provider?: string }> {
405
+ const profile = profilesOf(readConfig()).find(p => p.id === id);
406
+ if (!profile) return { ok: false, error: "voz não encontrada" };
407
+ if (provider !== undefined && provider !== null && provider !== "") {
408
+ if (typeof provider !== "string" || !VOICE_PROVIDERS.includes(provider))
409
+ return { ok: false, error: "provider desconhecido" };
410
+ const r = await providerSample(profile, provider);
411
+ return r.ok ? { ok: true, output: PROVIDER_NAME[provider], url: r.url, provider: r.provider } : r;
412
+ }
413
+ const withVoice = profile.order.filter(p => (profile as any)[p]);
414
+ const ready = withVoice.find(p => existsSync(join(SAMPLES_DIR, sampleFile(profile, p))));
415
+ const errors: string[] = [];
416
+ for (const p of ready ? [ready] : withVoice) {
417
+ const r = await providerSample(profile, p);
418
+ if (r.ok) return { ok: true, output: PROVIDER_NAME[p], url: r.url, provider: p };
419
+ errors.push(`${PROVIDER_NAME[p]}: ${r.error}`);
420
+ }
421
+ return { ok: false, error: errors.join(" · ") || "esta voz não tem nenhum provedor" };
422
+ }
423
+
424
+ /* in the background, one provider at a time: one without a key or not open fails right away and is left
425
+ for the click */
426
+ function generateSamples(profile: VoiceProfile) {
427
+ void (async () => {
428
+ for (const p of profile.order) {
429
+ if (!(profile as any)[p]) continue;
430
+ const r = await providerSample(profile, p).catch(e => ({ ok: false as const, error: String(e?.message ?? e) }));
431
+ if (!r.ok) console.error(`amostra da voz “${profile.name}” (${p}): ${r.error}`);
432
+ }
433
+ })();
434
+ }
435
+
436
+ export function setDefaultProfile(id: unknown): Resp {
437
+ const cfg = readConfig();
438
+ const profile = profilesOf(cfg).find(p => p.id === id);
439
+ if (!profile) return { ok: false, error: "voz não encontrada" };
440
+ if (cfg.voice.default_profile === profile.id) return { ok: true, output: "já era a voz ativa" };
441
+ cfg.voice = withKey(cfg.voice, "default_profile", profile.id, "profiles");
442
+ writeConfig(cfg);
443
+ return { ok: true, output: `“${profile.name}” é a voz ativa: já vem marcada no vídeo novo` };
444
+ }
445
+
446
+ /* Deletes the voice on the provider's account. 404 counts as deleted (it wasn't there). Returns the error, or null. */
447
+ async function deleteOnProvider(
448
+ provider: "fish" | "cartesia" | "elevenlabs",
449
+ voiceId: string,
450
+ cfg: any,
451
+ ): Promise<string | null> {
452
+ const key = providerKey(provider);
453
+ if (!key) return "sem API key";
454
+ const request: Record<typeof provider, [string, Record<string, string>]> = {
455
+ fish: [`https://api.fish.audio/model/${voiceId}`, { Authorization: `Bearer ${key}` }],
456
+ cartesia: [
457
+ `https://api.cartesia.ai/voices/${voiceId}`,
458
+ { Authorization: `Bearer ${key}`, "Cartesia-Version": cfg?.voice?.cartesia?.api_version ?? "2026-08-14" },
459
+ ],
460
+ elevenlabs: [`https://api.elevenlabs.io/v1/voices/${voiceId}`, { "xi-api-key": key }],
461
+ };
462
+ const [url, headers] = request[provider];
463
+ try {
464
+ const r = await fetch(url, { method: "DELETE", headers, signal: AbortSignal.timeout(30_000) });
465
+ if (r.ok || r.status === 404) return null;
466
+ return `respondeu ${r.status}: ${(await r.text()).replace(/\s+/g, " ").slice(0, 160)}`;
467
+ } catch (e: any) {
468
+ return e?.name === "TimeoutError" ? "não respondeu em 30 s" : String(e?.message ?? e);
469
+ }
470
+ }
471
+
472
+ /* Redoing a voice: deletes the old one on the provider's account, after the new one is created. A Voice
473
+ ID that another voice also uses, or that is the provider's loose voice_id in the config, stays. Returns the
474
+ error (to delete by hand), or null when deleted or kept. */
475
+ export async function deleteOldVoice(
476
+ profileId: string,
477
+ provider: "fish" | "cartesia" | "elevenlabs",
478
+ voiceId: string,
479
+ ): Promise<string | null> {
480
+ const cfg = readConfig();
481
+ const others = profilesOf(cfg).filter(p => p.id !== profileId);
482
+ if (others.some(o => o[provider] === voiceId) || cfg?.voice?.[provider]?.voice_id === voiceId) return null;
483
+ return deleteOnProvider(provider, voiceId, cfg);
484
+ }
485
+
486
+ /* Removing the voice takes it out of the app and deletes it on the providers' accounts (Fish, Cartesia, ElevenLabs).
487
+ VoiceStudio stays: its profile is local and serves other voices. A Voice ID that another voice
488
+ also uses, or that is the provider's loose voice_id in the config, is not deleted. A provider failure
489
+ doesn't block the removal: it goes in the result, to delete by hand. */
490
+ export async function removeProfile(id: unknown): Promise<Resp> {
491
+ const cfg = readConfig();
492
+ const profiles = profilesOf(cfg);
493
+ const profile = profiles.find(p => p.id === id);
494
+ if (!profile) return { ok: false, error: "voz não encontrada" };
495
+ const rest = profiles.filter(p => p.id !== profile.id);
496
+
497
+ const deleted: string[] = [],
498
+ kept: string[] = [],
499
+ failures: string[] = [];
500
+ for (const p of ["fish", "cartesia", "elevenlabs"] as const) {
501
+ const voiceId = profile[p];
502
+ if (!voiceId) continue;
503
+ if (rest.some(o => o[p] === voiceId) || cfg?.voice?.[p]?.voice_id === voiceId) {
504
+ kept.push(PROVIDER_NAME[p]);
505
+ continue;
506
+ }
507
+ const error = await deleteOnProvider(p, voiceId, cfg);
508
+ if (error) failures.push(`${PROVIDER_NAME[p]} (${voiceId}): ${error}`);
509
+ else deleted.push(PROVIDER_NAME[p]);
510
+ }
511
+
512
+ cfg.voice = withKey(cfg.voice, "profiles", rest.map(toConfig), "order");
513
+ if (cfg.voice.default_profile === profile.id)
514
+ cfg.voice = withKey(cfg.voice, "default_profile", rest[0]?.id ?? null, "profiles");
515
+ writeConfig(cfg);
516
+ clearSamples(profile.id);
517
+ const parts = [`“${profile.name}” removida`];
518
+ if (deleted.length) parts.push(`apagada na ${deleted.join(", na ")}`);
519
+ if (kept.length) parts.push(`mantida na ${kept.join(", na ")}, porque outra voz usa o mesmo Voice ID`);
520
+ if (failures.length) parts.push(`não deu para apagar na ${failures.join("; na ")}: apague à mão na conta`);
521
+ return {
522
+ ok: true,
523
+ output: `${parts.join(". ")}. Vídeo que usava esta voz precisa escolher outra antes de gerar as falas`,
524
+ };
525
+ }
526
+
527
+ /* Fish price used in the cost estimate (usage.fish.usd_per_million_bytes) */
528
+ export function setFishPrice(value: unknown): Resp {
529
+ const n = typeof value === "number" ? value : Number(String(value).replace(",", "."));
530
+ if (!Number.isFinite(n) || n < 0 || n > 10_000) return { ok: false, error: "preço inválido" };
531
+ const cfg = readConfig();
532
+ cfg.usage = cfg.usage ?? {};
533
+ cfg.usage.fish = withKey(cfg.usage.fish ?? {}, "usd_per_million_bytes", n);
534
+ writeConfig(cfg);
535
+ return { ok: true, output: `US$ ${n} por milhão de bytes` };
536
+ }
537
+
538
+ /* Cartesia plan (usage.cartesia): the API only returns usage, never the plan, so the quota
539
+ is the chosen plan's (tools/usage.py CARTESIA_PLANS) or, for "other", the one the user typed */
540
+ const CARTESIA_PLANS: Record<string, number> = { free: 20_000, pro: 100_000, startup: 1_250_000, scale: 8_000_000 };
541
+ export function setCartesiaPlan(plan: unknown, quota: unknown, day: unknown): Resp {
542
+ const p = String(plan);
543
+ if (!(p in CARTESIA_PLANS) && p !== "other") return { ok: false, error: "plano desconhecido" };
544
+ const d = Number(day);
545
+ if (!Number.isInteger(d) || d < 1 || d > 28) return { ok: false, error: "o dia da renovação é de 1 a 28" };
546
+ const q = p === "other" ? Number(String(quota).replace(/\./g, "").replace(",", ".")) : CARTESIA_PLANS[p];
547
+ if (!Number.isFinite(q) || q <= 0 || q > 1_000_000_000) return { ok: false, error: "cota inválida" };
548
+ const cfg = readConfig();
549
+ cfg.usage = cfg.usage ?? {};
550
+ const current = cfg.usage.cartesia ?? {};
551
+ cfg.usage.cartesia = withKey(
552
+ withKey(withKey(current, "plan", p), "monthly_quota", Math.round(q), "plan"),
553
+ "cycle_day",
554
+ d,
555
+ );
556
+ writeConfig(cfg);
557
+ return {
558
+ ok: true,
559
+ output: `${p === "other" ? "Outro plano" : `Plano ${p}`}: ${Math.round(q).toLocaleString("pt-BR")} créditos por mês, renova no dia ${d}`,
560
+ };
561
+ }
562
+
563
+ /* --- agent accounts --------------------------------------------------------------------
564
+ `agents.accounts` (N Claude config dirs, N Codex homes) and `agents.codex` (model, effort,
565
+ sandbox); the rotation and the limits live in tools/lib/agents.mjs. ~/.claude is refused there. */
566
+ const SANDBOXES = ["auto", "full"];
567
+
568
+ function agentsSettings(cfg: any) {
569
+ const accounts = accountsOf(cfg);
570
+ const logins = loginRuns();
571
+ return {
572
+ accounts: accounts.map(a => ({
573
+ id: a.id,
574
+ provider: a.provider,
575
+ dir: tildify(a.dir),
576
+ label: a.label,
577
+ enabled: a.enabled,
578
+ login: loginOf(a),
579
+ limited_until: limitedUntil(ROOT, a.id) || null,
580
+ login_command: loginCommand(a, cfg),
581
+ logging_in: logins[a.id] ?? null,
582
+ })),
583
+ /* who takes the next new session */
584
+ next: pickAccount(ROOT, cfg, { advance: false })?.id ?? null,
585
+ codex: {
586
+ ...codexSettings(cfg),
587
+ bin: codexBin(cfg),
588
+ installed: existsSync(codexBin(cfg)) || !!Bun.which(codexBin(cfg)),
589
+ options: { models: codexModels(accounts), sandboxes: SANDBOXES },
590
+ },
591
+ };
592
+ }
593
+
594
+ export function saveAgents(body: {
595
+ accounts?: unknown;
596
+ codex?: { model?: string; effort?: string; sandbox?: string };
597
+ }): Resp {
598
+ const cfg = readConfig();
599
+ const agents = { ...(cfg.agents ?? {}) };
600
+ if (body.accounts !== undefined) {
601
+ const c = cleanAccounts(body.accounts);
602
+ if ("error" in c) return { ok: false, error: c.error as string };
603
+ for (const a of c.accounts) mkdirSync(expandHome(a.dir), { recursive: true });
604
+ agents.accounts = c.accounts;
605
+ }
606
+ if (body.codex) {
607
+ const next = { ...(agents.codex ?? {}) };
608
+ if (body.codex.sandbox !== undefined) {
609
+ if (!SANDBOXES.includes(body.codex.sandbox))
610
+ return { ok: false, error: `sandbox desconhecida: ${body.codex.sandbox}` };
611
+ next.sandbox = body.codex.sandbox;
612
+ }
613
+ if (body.codex.model !== undefined) {
614
+ if (!/^[\w.:-]+$/.test(body.codex.model)) return { ok: false, error: "modelo inválido" };
615
+ next.model = body.codex.model;
616
+ }
617
+ if (body.codex.effort !== undefined) {
618
+ if (body.codex.effort && !/^[a-z]+$/.test(body.codex.effort)) return { ok: false, error: "effort inválido" };
619
+ if (body.codex.effort) next.effort = body.codex.effort;
620
+ else delete next.effort;
621
+ }
622
+ agents.codex = next;
623
+ }
624
+ cfg.agents = agents;
625
+ writeConfig(cfg);
626
+ const n = accountsOf(cfg).length;
627
+ return { ok: true, output: `${n} conta${n === 1 ? "" : "s"} de agente salva${n === 1 ? "" : "s"}` };
628
+ }
629
+
630
+ /* `claude auth login` / `codex login` for one account: each opens the browser and waits for the
631
+ callback; the screen rereads the settings until the login shows up */
632
+ type LoginRun = { state: "waiting" | "ok" | "error"; url: string | null; output: string; at: number };
633
+ const logins: Record<string, { run: LoginRun; proc: ReturnType<typeof Bun.spawn> }> = {};
634
+ const loginRuns = () => Object.fromEntries(Object.entries(logins).map(([id, l]) => [id, l.run]));
635
+
636
+ export async function loginAgent(id: string): Promise<Resp & { url?: string | null }> {
637
+ const cfg = readConfig();
638
+ const a = accountsOf(cfg).find(x => x.id === id);
639
+ if (!a) return { ok: false, error: "conta não encontrada" };
640
+ logins[id]?.proc.kill();
641
+ mkdirSync(a.dir, { recursive: true });
642
+ const cmd = a.provider === "claude" ? [claudeBin(cfg), "auth", "login"] : [codexBin(cfg), "login"];
643
+ const proc = Bun.spawn(cmd, { cwd: ROOT, stdin: "ignore", stdout: "pipe", stderr: "pipe", env: envFor(a) });
644
+ const entry = { run: { state: "waiting", url: null, output: "", at: Date.now() } as LoginRun, proc };
645
+ logins[id] = entry;
646
+ const read = async (stream: ReadableStream<Uint8Array>) => {
647
+ const dec = new TextDecoder();
648
+ for await (const chunk of stream) {
649
+ entry.run.output = (entry.run.output + dec.decode(chunk)).slice(-4000);
650
+ entry.run.url ??= /https:\/\/\S+/.exec(entry.run.output)?.[0] ?? null;
651
+ }
652
+ };
653
+ void read(proc.stdout as ReadableStream<Uint8Array>);
654
+ void read(proc.stderr as ReadableStream<Uint8Array>);
655
+ proc.exited.then(code => {
656
+ if (logins[id] === entry) entry.run.state = code === 0 && loginOf(a) ? "ok" : "error";
657
+ });
658
+ /* the link shows up in a second or two; the browser usually opens by itself */
659
+ for (let i = 0; i < 20 && !entry.run.url && entry.run.state === "waiting"; i++) await Bun.sleep(200);
660
+ return {
661
+ ok: true,
662
+ output: entry.run.url ? "abri o login no navegador: entre e volte aqui" : "login iniciado: siga no navegador",
663
+ url: entry.run.url,
664
+ };
665
+ }
666
+
667
+ export function clearAgentLimit(id: string): Resp {
668
+ return clearLimit(ROOT, id)
669
+ ? { ok: true, output: "conta liberada: volta para o rodízio" }
670
+ : { ok: false, error: "essa conta não está no limite" };
671
+ }
672
+
673
+ /* model and effort of a studio session: the orchestrator (video) or the reference */
674
+ export function sessionModel(type: string): { model?: string; effort?: string } | null {
675
+ const m = readConfig()?.models;
676
+ return (
677
+ (type === "reference"
678
+ ? m?.reference
679
+ : type === "edit"
680
+ ? (m?.edit ?? m?.orchestrator)
681
+ : type === "omni"
682
+ ? (m?.omni ?? m?.orchestrator)
683
+ : m?.orchestrator) ?? null
684
+ );
685
+ }
686
+
687
+ /* --- model and effort per stage ---------------------------------------------------------
688
+ `models` in the config; the stages go to the skills' frontmatter (tools/models.mjs apply) */
689
+ type ModelChoice = { model: string; effort?: string };
690
+
691
+ export function saveModels(body: any): Resp {
692
+ const cfg = readConfig();
693
+ const current = cfg.models ?? { stages: {} };
694
+ const clean = (m: any): ModelChoice | string => {
695
+ const e: ModelChoice = { model: String(m?.model ?? "") };
696
+ if (m?.effort) e.effort = String(m.effort);
697
+ return validate(e) ?? e;
698
+ };
699
+ const next: any = { ...current, stages: { ...(current.stages ?? {}) } };
700
+ for (const k of ["orchestrator", "reference"]) {
701
+ if (body?.[k] == null) continue;
702
+ const e = clean(body[k]);
703
+ if (typeof e === "string") return { ok: false, error: `${k}: ${e}` };
704
+ next[k] = e;
705
+ }
706
+ for (const [stage, m] of Object.entries(body?.stages ?? {})) {
707
+ if (!(stage in next.stages)) return { ok: false, error: `etapa desconhecida: ${stage}` };
708
+ const e = clean(m);
709
+ if (typeof e === "string") return { ok: false, error: `${stage}: ${e}` };
710
+ next.stages[stage] = e;
711
+ }
712
+ cfg.models = next;
713
+ writeConfig(cfg);
714
+ try {
715
+ const changed = applyModels();
716
+ return { ok: true, output: changed.length ? `gravado; skills atualizadas: ${changed.join(", ")}` : "gravado" };
717
+ } catch (e: any) {
718
+ return { ok: false, error: `a config foi gravada, mas as skills não: ${e.message}` };
719
+ }
720
+ }
721
+
722
+ /* --- publishing ---------------------------------------------------------------------
723
+ `publish` in the config: publish.mjs rereads it on every use. Each field has its
724
+ validator; what the UI sends that isn't here is refused. */
725
+ const valid = (v: unknown) => ({ v });
726
+ const invalid = (e: string) => ({ e });
727
+ type Validator = (x: unknown) => { v: unknown } | { e: string };
728
+ const boolField =
729
+ (name: string): Validator =>
730
+ x =>
731
+ typeof x === "boolean" ? valid(x) : invalid(`${name}: precisa ser true ou false`);
732
+ const textField =
733
+ (name: string, max: number): Validator =>
734
+ x =>
735
+ typeof x === "string" && x.length <= max ? valid(x.trim()) : invalid(`${name}: texto de até ${max} caracteres`);
736
+ const PLATFORMS = ["youtube", "instagram", "tiktok"];
737
+ const TIKTOK_PRIVACIES = ["SELF_ONLY", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "PUBLIC_TO_EVERYONE"];
738
+
739
+ const PUBLISH_SECTIONS: Record<string, Record<string, Validator>> = {
740
+ auto: {
741
+ approved: boolField("approved"),
742
+ platforms: x =>
743
+ Array.isArray(x) && x.every(p => PLATFORMS.includes(p))
744
+ ? valid([...new Set(x)])
745
+ : invalid("platforms: youtube, instagram e/ou tiktok"),
746
+ },
747
+ text: {
748
+ footer_youtube: textField("footer_youtube", 2000),
749
+ footer_instagram: textField("footer_instagram", 2000),
750
+ footer_tiktok: textField("footer_tiktok", 2000),
751
+ hashtags: x => {
752
+ if (!Array.isArray(x) || x.length > 30) return invalid("hashtags: até 30");
753
+ const l = x.map(h => String(h).trim().replace(/^#/, ""));
754
+ return l.every(h => /^[\p{L}\p{N}_]+$/u.test(h)) ? valid(l) : invalid("hashtags: só letras, números e _");
755
+ },
756
+ },
757
+ youtube: {
758
+ privacy: x =>
759
+ ["private", "unlisted", "public"].includes(String(x))
760
+ ? valid(x)
761
+ : invalid("privacy: private, unlisted ou public"),
762
+ audited: boolField("audited"),
763
+ category: x =>
764
+ /^\d{1,3}$/.test(String(x)) ? valid(String(x)) : invalid("category: o número da categoria do YouTube"),
765
+ language: x =>
766
+ /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(String(x)) ? valid(x) : invalid("language: algo como pt-BR"),
767
+ made_for_kids: boolField("made_for_kids"),
768
+ synthetic_content: boolField("synthetic_content"),
769
+ },
770
+ instagram: {
771
+ api_version: x => (/^v\d{1,2}\.\d$/.test(String(x)) ? valid(x) : invalid("api_version: algo como v23.0")),
772
+ share_to_feed: boolField("share_to_feed"),
773
+ cover_at_s: x => {
774
+ if (x === null || x === "") return valid(null);
775
+ const n = Number(String(x).replace(",", "."));
776
+ return Number.isFinite(n) && n >= 0 && n <= 900 ? valid(n) : invalid("cover_at_s: segundos de 0 a 900, ou vazio");
777
+ },
778
+ },
779
+ tiktok: {
780
+ mode: x => (["direct", "draft"].includes(String(x)) ? valid(x) : invalid("mode: direct ou draft")),
781
+ privacy: x =>
782
+ TIKTOK_PRIVACIES.includes(String(x)) ? valid(x) : invalid(`privacy: ${TIKTOK_PRIVACIES.join(", ")}`),
783
+ audited: boolField("audited"),
784
+ comments: boolField("comments"),
785
+ duet: boolField("duet"),
786
+ stitch: boolField("stitch"),
787
+ ai_content: boolField("ai_content"),
788
+ cover_at_s: x => {
789
+ if (x === null || x === "") return valid(null);
790
+ const n = Number(String(x).replace(",", "."));
791
+ return Number.isFinite(n) && n >= 0 && n <= 600 ? valid(n) : invalid("cover_at_s: segundos de 0 a 600, ou vazio");
792
+ },
793
+ },
794
+ schedule: {
795
+ max_delay_h: x => {
796
+ const n = Number(String(x).replace(",", "."));
797
+ return Number.isFinite(n) && n >= 0.25 && n <= 168 ? valid(n) : invalid("max_delay_h: horas de 0,25 a 168");
798
+ },
799
+ attempts: x => {
800
+ const n = Number(x);
801
+ return Number.isInteger(n) && n >= 1 && n <= 10 ? valid(n) : invalid("attempts: de 1 a 10");
802
+ },
803
+ interval_min: x => {
804
+ const n = Number(x);
805
+ return Number.isInteger(n) && n >= 1 && n <= 120 ? valid(n) : invalid("interval_min: minutos de 1 a 120");
806
+ },
807
+ },
808
+ };
809
+
810
+ export function savePublishSettings(section: unknown, values: unknown): Resp {
811
+ const fields = PUBLISH_SECTIONS[String(section)];
812
+ if (!fields) return { ok: false, error: `seção desconhecida: ${section}` };
813
+ if (!values || typeof values !== "object" || Array.isArray(values)) return { ok: false, error: "valores inválidos" };
814
+ const cfg = readConfig();
815
+ cfg.publish = cfg.publish ?? {};
816
+ let obj: Record<string, unknown> = { ...(cfg.publish[section as string] ?? {}) };
817
+ const changed: string[] = [];
818
+ for (const [k, x] of Object.entries(values as Record<string, unknown>)) {
819
+ const check = fields[k];
820
+ if (!check) return { ok: false, error: `campo desconhecido: ${section}.${k}` };
821
+ const r = check(x);
822
+ if ("e" in r) return { ok: false, error: r.e };
823
+ if (JSON.stringify(obj[k]) !== JSON.stringify(r.v)) changed.push(k);
824
+ obj = withKey(obj, k, r.v);
825
+ }
826
+ if (section === "auto" && obj.approved === true && !(obj.platforms as string[] | undefined)?.length)
827
+ return { ok: false, error: "escolha ao menos uma plataforma para a publicação automática" };
828
+ if (!changed.length) return { ok: true, output: "já era assim" };
829
+ cfg.publish[section as string] = obj;
830
+ writeConfig(cfg);
831
+ forgetAccounts();
832
+ return { ok: true, output: `gravado: ${changed.join(", ")}` };
833
+ }
834
+
835
+ /* omnichat.instagram: enable the rule when the Reel goes up (the permanent approval to reply without
836
+ the enable click) and whether a new rule quotes the Reel in the DM. Same defaults as omnichat.mjs */
837
+ const OMNICHAT_FIELDS: Record<string, Validator> = {
838
+ enable_on_publish: boolField("enable_on_publish"),
839
+ quote_post: boolField("quote_post"),
840
+ };
841
+
842
+ export function saveOmnichatSettings(values: unknown): Resp {
843
+ if (!values || typeof values !== "object" || Array.isArray(values)) return { ok: false, error: "valores inválidos" };
844
+ const cfg = readConfig();
845
+ cfg.omnichat = cfg.omnichat ?? {};
846
+ let obj: Record<string, unknown> = { ...(cfg.omnichat.instagram ?? {}) };
847
+ const changed: string[] = [];
848
+ for (const [k, x] of Object.entries(values as Record<string, unknown>)) {
849
+ const check = OMNICHAT_FIELDS[k];
850
+ if (!check) return { ok: false, error: `campo desconhecido: omnichat.${k}` };
851
+ const r = check(x);
852
+ if ("e" in r) return { ok: false, error: r.e };
853
+ if (JSON.stringify(obj[k]) !== JSON.stringify(r.v)) changed.push(k);
854
+ obj = withKey(obj, k, r.v);
855
+ }
856
+ if (!changed.length) return { ok: true, output: "já era assim" };
857
+ cfg.omnichat.instagram = obj;
858
+ writeConfig(cfg);
859
+ return { ok: true, output: `gravado: ${changed.join(", ")}` };
860
+ }
861
+
862
+ /* Omni's voice (the agent page): the female or the male one; the voices themselves are
863
+ `agent.voices` in the config */
864
+ export function setAgentGender(gender: unknown): Resp {
865
+ if (gender !== "female" && gender !== "male") return { ok: false, error: "escolha voz feminina ou masculina" };
866
+ const cfg = readConfig();
867
+ if ((cfg.agent?.gender ?? "female") === gender) return { ok: true, output: "já era assim" };
868
+ cfg.agent = { ...(cfg.agent ?? {}), gender };
869
+ writeConfig(cfg);
870
+ return { ok: true, output: gender === "male" ? "voz masculina" : "voz feminina" };
871
+ }
872
+
873
+ /* Omni's face: the seed of its avatar (the same kind a voice has), drawn by the studio */
874
+ export function setAgentAvatar(avatar: unknown): Resp {
875
+ if (typeof avatar !== "string" || !AVATAR_SEED.test(avatar)) return { ok: false, error: "avatar inválido" };
876
+ const cfg = readConfig();
877
+ if (cfg.agent?.avatar === avatar) return { ok: true, output: "já era assim" };
878
+ cfg.agent = { ...(cfg.agent ?? {}), avatar };
879
+ writeConfig(cfg);
880
+ return { ok: true, output: "avatar do Omni gravado" };
881
+ }
882
+
883
+ /* a provider's key as voice.py resolves it: the environment before .env */
884
+ export function providerKey(provider: "fish" | "cartesia" | "elevenlabs"): string | null {
885
+ const fallback = { fish: "FISH_API_KEY", cartesia: "CARTESIA_API_KEY", elevenlabs: "ELEVENLABS_API_KEY" }[provider];
886
+ const name = readConfig()?.voice?.[provider]?.key_env ?? fallback;
887
+ return process.env[name] || readEnv()[name] || null;
888
+ }
889
+
890
+ export const profileNameExists = (name: string) =>
891
+ profilesOf(readConfig()).some(p => p.name.toLowerCase() === name.trim().toLowerCase());
892
+
893
+ /* --- read for the UI ------------------------------------------------------------ */
894
+ export function getSettings() {
895
+ const cfg = readConfig() ?? {};
896
+ const env = readEnv();
897
+ const fish = cfg.voice?.fish ?? {};
898
+ const cartesia = cfg.voice?.cartesia ?? {};
899
+ const elevenlabs = cfg.voice?.elevenlabs ?? {};
900
+ const pub = cfg.publish ?? {};
901
+ /* requires_approved_preview belonged to the preview stage, which no longer exists: left out so that
902
+ saving the section doesn't send it back (it would be an unknown field) */
903
+ const { requires_approved_preview: _dropped, ...auto } = pub.auto ?? {};
904
+ return {
905
+ voice: {
906
+ order: (cfg.voice?.order ?? VOICE_PROVIDERS) as string[],
907
+ profiles: profilesOf(cfg),
908
+ voicestudio: { profile: cfg.voicestudio?.profile ?? null },
909
+ default_profile: (cfg.voice?.default_profile ?? null) as string | null,
910
+ fish: {
911
+ key: keyStatus(fish.key_env ?? "FISH_API_KEY", env),
912
+ voice_id: fish.voice_id ?? null,
913
+ model: fish.model ?? "s2.1-pro-free",
914
+ fallback_model: fish.fallback_model ?? null,
915
+ usd_per_million_bytes: cfg.usage?.fish?.usd_per_million_bytes ?? null,
916
+ },
917
+ cartesia: {
918
+ key: keyStatus(cartesia.key_env ?? "CARTESIA_API_KEY", env),
919
+ admin_key: keyStatus(cfg.usage?.cartesia?.admin_key_env ?? "CARTESIA_ADMIN_API_KEY", env),
920
+ voice_id: cartesia.voice_id ?? null,
921
+ plan: (cfg.usage?.cartesia?.plan ?? "free") as string,
922
+ monthly_quota: (cfg.usage?.cartesia?.monthly_quota ?? 20000) as number,
923
+ cycle_day: (cfg.usage?.cartesia?.cycle_day ?? 1) as number,
924
+ },
925
+ elevenlabs: {
926
+ key: keyStatus(elevenlabs.key_env ?? "ELEVENLABS_API_KEY", env),
927
+ voice_id: elevenlabs.voice_id || null,
928
+ model: elevenlabs.model ?? "eleven_multilingual_v2",
929
+ },
930
+ },
931
+ /* the same defaults as publish.mjs */
932
+ publish: {
933
+ auto: { approved: false, platforms: [], ...auto },
934
+ text: { footer_youtube: "", footer_instagram: "", footer_tiktok: "", hashtags: [], ...(pub.text ?? {}) },
935
+ youtube: {
936
+ privacy: "public",
937
+ audited: false,
938
+ category: "22",
939
+ language: "pt-BR",
940
+ made_for_kids: false,
941
+ synthetic_content: false,
942
+ ...(pub.youtube ?? {}),
943
+ },
944
+ instagram: { api_version: "v23.0", share_to_feed: true, cover_at_s: null, ...(pub.instagram ?? {}) },
945
+ tiktok: {
946
+ mode: "direct",
947
+ privacy: "PUBLIC_TO_EVERYONE",
948
+ audited: false,
949
+ comments: true,
950
+ duet: false,
951
+ stitch: false,
952
+ ai_content: true,
953
+ cover_at_s: null,
954
+ ...(pub.tiktok ?? {}),
955
+ },
956
+ schedule: { max_delay_h: 12, attempts: 3, interval_min: 10, ...(pub.schedule ?? {}) },
957
+ keys: {
958
+ youtube_client_id: keyStatus("YOUTUBE_CLIENT_ID", env),
959
+ youtube_client_secret: keyStatus("YOUTUBE_CLIENT_SECRET", env),
960
+ youtube_connected: !!env.YOUTUBE_REFRESH_TOKEN,
961
+ instagram_connected: !!env.INSTAGRAM_TOKEN,
962
+ tiktok_client_key: keyStatus("TIKTOK_CLIENT_KEY", env),
963
+ tiktok_client_secret: keyStatus("TIKTOK_CLIENT_SECRET", env),
964
+ tiktok_connected: !!env.TIKTOK_REFRESH_TOKEN,
965
+ },
966
+ },
967
+ omnichat: {
968
+ enable_on_publish: cfg.omnichat?.instagram?.enable_on_publish === true,
969
+ quote_post: cfg.omnichat?.instagram?.quote_post !== false,
970
+ },
971
+ agents: agentsSettings(cfg),
972
+ models: {
973
+ orchestrator: cfg.models?.orchestrator ?? null,
974
+ reference: cfg.models?.reference ?? null,
975
+ stages: Object.entries(cfg.models?.stages ?? {}).map(([stage, m]: [string, any]) => ({
976
+ stage,
977
+ ...m,
978
+ in_skill: inSkill(stage),
979
+ })),
980
+ options: { models: MODELS, efforts: EFFORTS },
981
+ },
982
+ };
983
+ }