@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,722 @@
1
+ /* Cloning the voice from the UI: the user records the phrases (or uploads an audio file) and the
2
+ studio creates the clone on Fish Audio and Cartesia through the API, in one go, and
3
+ saves the voice profile with both IDs.
4
+
5
+ - Fish: POST /model with all samples in `voices` and each one's transcript
6
+ in `texts` (when we have the text read). train_mode=fast: the voice comes out ready.
7
+ - Cartesia: POST /voices/clone accepts ONE clip of up to 16 MB. The samples become
8
+ a single wav, up to ~60 s (Sonic 3.6 uses up to that). A Cartesia clone
9
+ has one language; the mixed context goes as Portuguese.
10
+
11
+ Samples stay in .clones/<id>/ (outside git) to redo the clone later
12
+ without recording everything again: editing the voice creates or recreates the clone on a single provider
13
+ (`recreateClone`), when it failed, was never made there, or the user wants to redo it.
14
+ None of this goes through the queue: it's network and a short ffmpeg. */
15
+
16
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync, rmSync, readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { ROOT, CONFIG, readJson, run } from "./root";
19
+ import { deleteOldVoice, providerKey, saveProfile, profileNameExists, getProfile } from "./settings";
20
+ import { findCloneContext } from "./clone-phrases";
21
+
22
+ type Resp =
23
+ | { ok: true; output: string; id?: string; warnings?: string[]; failed?: string[] }
24
+ | { ok: false; error: string };
25
+
26
+ const FFMPEG = Bun.which("ffmpeg") ?? "/opt/homebrew/bin/ffmpeg";
27
+ const FFPROBE = Bun.which("ffprobe") ?? "/opt/homebrew/bin/ffprobe";
28
+
29
+ const MIN_TOTAL_S = 30; // the minimum the UI asks for
30
+ const MAX_TOTAL_S = 180; // 3 min: beyond that the gain is small and the upload is heavy
31
+ const MIN_SAMPLE_S = 1; // a click with no speech is dropped
32
+ const MAX_SAMPLES = 20; // Fish's `voices` limit
33
+ const CARTESIA_TARGET_S = 60; // Sonic 3.6 uses up to 60 s of the clip
34
+ const CARTESIA_MAX_BYTES = 16 * 1024 ** 2;
35
+ const TIMEOUT_MS = 120_000;
36
+
37
+ type SampleInput = { text?: unknown; mime?: unknown; data?: unknown };
38
+ type Sample = { file: string; text: string | null; duration: number };
39
+
40
+ const EXT: Record<string, string> = {
41
+ "audio/webm": "webm",
42
+ "video/webm": "webm",
43
+ "audio/ogg": "ogg",
44
+ "audio/mp4": "m4a",
45
+ "audio/x-m4a": "m4a",
46
+ "audio/mpeg": "mp3",
47
+ "audio/wav": "wav",
48
+ "audio/x-wav": "wav",
49
+ "audio/flac": "flac",
50
+ };
51
+ const extOf = (mime: unknown) => EXT[String(mime).split(";")[0].trim().toLowerCase()] ?? "bin";
52
+
53
+ async function durationOf(file: string): Promise<number> {
54
+ const r = await run([FFPROBE, "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", file]);
55
+ const n = parseFloat(r.stdout.trim());
56
+ return Number.isFinite(n) ? n : 0;
57
+ }
58
+
59
+ /* any input becomes mono 44.1 kHz wav: the same format for both providers */
60
+ async function normalizeAudio(raw: string, dest: string): Promise<boolean> {
61
+ const r = await run([
62
+ FFMPEG,
63
+ "-y",
64
+ "-v",
65
+ "error",
66
+ "-i",
67
+ raw,
68
+ "-vn",
69
+ "-ac",
70
+ "1",
71
+ "-ar",
72
+ "44100",
73
+ "-c:a",
74
+ "pcm_s16le",
75
+ dest,
76
+ ]);
77
+ return r.code === 0;
78
+ }
79
+
80
+ function httpError(name: string, status: number, text: string) {
81
+ const body = text.replace(/\s+/g, " ").slice(0, 200);
82
+ let code = "",
83
+ detail = "";
84
+ try {
85
+ const j = JSON.parse(text);
86
+ /* Cartesia: { error_code }; ElevenLabs: { detail: { status, message } } */
87
+ code = String(j?.error_code ?? j?.detail?.status ?? "");
88
+ detail = typeof j?.detail?.message === "string" ? j.detail.message : "";
89
+ } catch {
90
+ /* non-JSON body */
91
+ }
92
+ /* Cartesia only clones through the API on a paid plan; on free, only in the playground */
93
+ if (code === "plan_upgrade_required") {
94
+ return `${name}: o plano free não deixa clonar pela API (${status}). Assine um plano em play.cartesia.ai/subscription e crie a voz de novo em Editar voz → ${name} → Configurar, ou clone no playground da ${name} e cole ali o Voice ID.`;
95
+ }
96
+ /* ElevenLabs: instant clone needs a paid plan (Starter or above), and the key needs Voices: Write */
97
+ if (code === "can_not_use_instant_voice_cloning")
98
+ return `${name}: o plano não deixa clonar voz (${status}). Assine o Starter ou acima em elevenlabs.io/subscription, ou cole o Voice ID de uma voz que já existe.`;
99
+ if (code === "voice_limit_reached")
100
+ return `${name}: a conta chegou ao limite de vozes (${status}). Apague uma voz em elevenlabs.io → Voices e tente de novo.`;
101
+ if (code === "missing_permissions")
102
+ return `${name}: a API key não tem permissão para criar voz (${status}). Gere uma chave com Voices: Write em elevenlabs.io → Developers → API Keys.`;
103
+ if (detail) return `${name} respondeu ${status}: ${detail}`;
104
+ if (status === 401 || status === 403)
105
+ return `${name}: a chave foi recusada (${status}). Confira a API key em Configurações → Voz.`;
106
+ if (status === 402) return `${name}: sem saldo ou plano que permita clonar (${status}). ${body}`;
107
+ if (status === 429) return `${name}: limite de uso atingido (${status}). Tente de novo em alguns minutos.`;
108
+ return `${name} respondeu ${status}: ${body}`;
109
+ }
110
+
111
+ /* ElevenLabs: POST /v1/voices/add (Instant Voice Cloning) with the samples in `files`; the voice
112
+ comes out ready. Up to 25 files; the clone learns best from 1 to 2 min of clean speech. */
113
+ async function cloneOnElevenLabs(key: string, name: string, samples: Sample[]): Promise<string> {
114
+ const form = new FormData();
115
+ form.set("name", name);
116
+ form.set("remove_background_noise", "false");
117
+ for (const a of samples.slice(0, MAX_SAMPLES)) {
118
+ form.append("files", new File([readFileSync(a.file)], a.file.split("/").pop()!, { type: "audio/wav" }));
119
+ }
120
+ const resp = await fetch("https://api.elevenlabs.io/v1/voices/add", {
121
+ method: "POST",
122
+ body: form,
123
+ headers: { "xi-api-key": key },
124
+ signal: AbortSignal.timeout(TIMEOUT_MS),
125
+ });
126
+ const text = await resp.text();
127
+ if (!resp.ok) throw new Error(httpError("ElevenLabs", resp.status, text));
128
+ const id = JSON.parse(text)?.voice_id;
129
+ if (typeof id !== "string" || !/^[A-Za-z0-9]{20}$/.test(id))
130
+ throw new Error(`ElevenLabs criou a voz, mas a resposta veio sem voice_id: ${text.slice(0, 160)}`);
131
+ return id;
132
+ }
133
+
134
+ async function cloneOnFish(key: string, name: string, samples: Sample[]): Promise<string> {
135
+ const form = new FormData();
136
+ form.set("type", "tts");
137
+ form.set("title", name);
138
+ form.set("train_mode", "fast");
139
+ form.set("visibility", "private");
140
+ form.set("enhance_audio_quality", "true");
141
+ const withText = samples.every(a => a.text);
142
+ for (const a of samples.slice(0, MAX_SAMPLES)) {
143
+ form.append("voices", new File([readFileSync(a.file)], a.file.split("/").pop()!, { type: "audio/wav" }));
144
+ if (withText) form.append("texts", a.text!);
145
+ }
146
+ const resp = await fetch("https://api.fish.audio/model", {
147
+ method: "POST",
148
+ body: form,
149
+ headers: { Authorization: `Bearer ${key}` },
150
+ signal: AbortSignal.timeout(TIMEOUT_MS),
151
+ });
152
+ const text = await resp.text();
153
+ if (!resp.ok) throw new Error(httpError("Fish Audio", resp.status, text));
154
+ const id = JSON.parse(text)?._id;
155
+ if (typeof id !== "string" || !/^[0-9a-f]{32}$/i.test(id))
156
+ throw new Error(`Fish Audio criou o modelo, mas a resposta veio sem _id: ${text.slice(0, 160)}`);
157
+ return id;
158
+ }
159
+
160
+ /* --- does the plan allow cloning? ----------------------------------------------------------------
161
+ Cartesia only clones through the API on a paid plan. For the UI to warn before the click, an empty POST
162
+ /voices/clone: on free it returns 402 plan_upgrade_required before looking at the audio; on
163
+ paid it returns 400 (missing clip). Nothing is created. Cached for 1 min, and each real
164
+ clone updates the cache with what Cartesia answered. */
165
+ type Plan = { ok: true } | { ok: false; error: string };
166
+ const PLAN_TTL_MS = 60_000;
167
+ const cartesiaPlan = new Map<string, { at: number; plan: Plan }>();
168
+ const notePlan = (key: string, plan: Plan) => cartesiaPlan.set(key, { at: Date.now(), plan });
169
+
170
+ export async function getClonePlan(): Promise<{ cartesia: Plan | null }> {
171
+ const key = providerKey("cartesia");
172
+ if (!key) return { cartesia: null };
173
+ const cached = cartesiaPlan.get(key);
174
+ if (cached && Date.now() - cached.at < PLAN_TTL_MS) return { cartesia: cached.plan };
175
+ try {
176
+ const resp = await fetch("https://api.cartesia.ai/voices/clone", {
177
+ method: "POST",
178
+ body: new FormData(),
179
+ signal: AbortSignal.timeout(15_000),
180
+ headers: { Authorization: `Bearer ${key}`, "Cartesia-Version": CONFIG.voice.cartesia.api_version },
181
+ });
182
+ const text = await resp.text();
183
+ /* only the plan block and a refused key say something; the "missing clip" 400 is the paid one */
184
+ const plan: Plan =
185
+ resp.status === 402 || resp.status === 401 || resp.status === 403
186
+ ? { ok: false, error: httpError("Cartesia", resp.status, text) }
187
+ : { ok: true };
188
+ notePlan(key, plan);
189
+ return { cartesia: plan };
190
+ } catch {
191
+ return { cartesia: null }; // offline: the UI warns nothing and the click shows the error
192
+ }
193
+ }
194
+
195
+ async function cloneOnCartesia(
196
+ key: string,
197
+ name: string,
198
+ language: string,
199
+ samples: Sample[],
200
+ dir: string,
201
+ ): Promise<string> {
202
+ /* whole samples, in order, up to the 60 s target */
203
+ const used: Sample[] = [];
204
+ let sum = 0;
205
+ for (const a of samples) {
206
+ if (used.length && sum + a.duration > CARTESIA_TARGET_S) break;
207
+ used.push(a);
208
+ sum += a.duration;
209
+ }
210
+ const list = join(dir, "cartesia.txt");
211
+ writeFileSync(list, used.map(a => `file '${a.file.replace(/'/g, "'\\''")}'`).join("\n") + "\n");
212
+ const clip = join(dir, "cartesia.wav");
213
+ const r = await run([
214
+ FFMPEG,
215
+ "-y",
216
+ "-v",
217
+ "error",
218
+ "-f",
219
+ "concat",
220
+ "-safe",
221
+ "0",
222
+ "-i",
223
+ list,
224
+ "-c:a",
225
+ "pcm_s16le",
226
+ clip,
227
+ ]);
228
+ if (r.code !== 0) throw new Error(`Cartesia: não consegui juntar as amostras (${r.stderr.trim().slice(0, 160)})`);
229
+ const bytes = readFileSync(clip);
230
+ if (bytes.length > CARTESIA_MAX_BYTES)
231
+ throw new Error("Cartesia: o áudio passou de 16 MB; grave menos frases ou frases mais curtas");
232
+
233
+ const form = new FormData();
234
+ form.set("name", name);
235
+ form.set("language", language);
236
+ form.set("clip", new File([bytes], "sample.wav", { type: "audio/wav" }));
237
+ const resp = await fetch("https://api.cartesia.ai/voices/clone", {
238
+ method: "POST",
239
+ body: form,
240
+ signal: AbortSignal.timeout(TIMEOUT_MS),
241
+ headers: { Authorization: `Bearer ${key}`, "Cartesia-Version": CONFIG.voice.cartesia.api_version },
242
+ });
243
+ const text = await resp.text();
244
+ if (resp.status === 402 || resp.status === 401 || resp.status === 403) {
245
+ const error = httpError("Cartesia", resp.status, text);
246
+ notePlan(key, { ok: false, error });
247
+ throw new Error(error);
248
+ }
249
+ if (!resp.ok) throw new Error(httpError("Cartesia", resp.status, text));
250
+ notePlan(key, { ok: true });
251
+ const id = JSON.parse(text)?.id;
252
+ if (typeof id !== "string" || !/^[0-9a-f-]{36}$/i.test(id))
253
+ throw new Error(`Cartesia criou a voz, mas a resposta veio sem id: ${text.slice(0, 160)}`);
254
+ return id;
255
+ }
256
+
257
+ /* The voice the user recorded or imported to clone: the samples of the profile's most recent
258
+ clone, joined in .clones/<clone>/original.wav (made once). A voice given only by
259
+ Voice ID has no recording: `ok: false`. */
260
+ export const CLONES_DIR = join(ROOT, ".clones");
261
+ type CloneInfo = {
262
+ name?: string;
263
+ context?: string;
264
+ profile?: string;
265
+ created_at?: string;
266
+ ids?: Record<string, string>;
267
+ samples?: { file: string; duration_s?: number; text?: string | null }[];
268
+ };
269
+
270
+ /* the profile's most recent clone that still has samples on disk */
271
+ function latestCloneOf(profile: string): { name: string; dir: string; info: CloneInfo; samples: Sample[] } | null {
272
+ if (!existsSync(CLONES_DIR)) return null;
273
+ const clones = readdirSync(CLONES_DIR)
274
+ .map(name => ({
275
+ name,
276
+ dir: join(CLONES_DIR, name),
277
+ info: readJson<CloneInfo>(join(CLONES_DIR, name, "clone.json")),
278
+ }))
279
+ .filter((c): c is { name: string; dir: string; info: CloneInfo } => c.info?.profile === profile)
280
+ .sort((a, b) => String(b.info.created_at).localeCompare(String(a.info.created_at)));
281
+ for (const c of clones) {
282
+ const samples = (c.info.samples ?? [])
283
+ .map(a => ({ file: join(c.dir, a.file), text: a.text ?? null, duration: a.duration_s ?? 0 }))
284
+ .filter(a => existsSync(a.file));
285
+ if (samples.length) return { ...c, samples };
286
+ }
287
+ return null;
288
+ }
289
+
290
+ /* for the edit screen: the voice's stored recording, whole (the samples joined), to
291
+ listen and create the clone on another provider */
292
+ export async function profileRecording(
293
+ profile: string,
294
+ ): Promise<{ ok: true; seconds: number; url: string } | { ok: false }> {
295
+ const c = latestCloneOf(profile);
296
+ const o = c ? await originalRecording(profile) : { ok: false as const };
297
+ if (!c || !o.ok) return { ok: false };
298
+ return { ok: true, seconds: +c.samples.reduce((s, a) => s + a.duration, 0).toFixed(1), url: o.url };
299
+ }
300
+
301
+ export async function originalRecording(profile: string): Promise<{ ok: true; url: string } | { ok: false }> {
302
+ const clone = latestCloneOf(profile);
303
+ if (!clone) return { ok: false };
304
+ const dir = clone.dir;
305
+ const url = `/api/voice-original/${clone.name}`;
306
+ if (existsSync(join(dir, "original.wav"))) return { ok: true, url };
307
+ const files = clone.samples.map(a => a.file);
308
+ if (!files.length) return { ok: false };
309
+ const list = join(dir, "original.txt");
310
+ writeFileSync(list, files.map(a => `file '${a.replace(/'/g, "'\\''")}'`).join("\n") + "\n");
311
+ const r = await run([
312
+ FFMPEG,
313
+ "-y",
314
+ "-v",
315
+ "error",
316
+ "-f",
317
+ "concat",
318
+ "-safe",
319
+ "0",
320
+ "-i",
321
+ list,
322
+ "-c:a",
323
+ "pcm_s16le",
324
+ join(dir, "original.wav"),
325
+ ]);
326
+ rmSync(list, { force: true });
327
+ return r.code === 0 ? { ok: true, url } : { ok: false };
328
+ }
329
+
330
+ export type CloneRequest = {
331
+ name?: unknown;
332
+ avatar?: unknown;
333
+ order?: unknown;
334
+ context?: unknown;
335
+ providers?: unknown;
336
+ samples?: unknown;
337
+ consent?: unknown;
338
+ };
339
+
340
+ export async function cloneVoice(p: CloneRequest): Promise<Resp> {
341
+ const name = typeof p.name === "string" ? p.name.trim() : "";
342
+ if (name.length < 3 || name.length > 20) return { ok: false, error: "o nome da voz tem de 3 a 20 caracteres" };
343
+ if (p.consent !== true)
344
+ return { ok: false, error: "confirme que a voz é sua ou que você tem permissão de quem fala" };
345
+ if (profileNameExists(name)) return { ok: false, error: `já existe um perfil chamado “${name}”` };
346
+
347
+ const ctx = findCloneContext(p.context);
348
+ if (!ctx) return { ok: false, error: "contexto de idioma desconhecido" };
349
+ const chosen = (Array.isArray(p.providers) ? p.providers : []).filter(
350
+ (x): x is "fish" | "cartesia" => x === "fish" || x === "cartesia",
351
+ );
352
+ if (!chosen.length) return { ok: false, error: "escolha ao menos um provider (Fish Audio ou Cartesia)" };
353
+ const keys = Object.fromEntries(chosen.map(q => [q, providerKey(q)])) as Record<"fish" | "cartesia", string | null>;
354
+ const withoutKey = chosen.filter(q => !keys[q]);
355
+ if (withoutKey.length)
356
+ return {
357
+ ok: false,
358
+ error: `sem API key da ${withoutKey.map(q => (q === "fish" ? "Fish Audio" : "Cartesia")).join(" e da ")}: grave em Configurações → Voz`,
359
+ };
360
+
361
+ const inputs = (Array.isArray(p.samples) ? p.samples : []) as SampleInput[];
362
+ if (!inputs.length) return { ok: false, error: "nenhuma gravação recebida" };
363
+ if (inputs.length > MAX_SAMPLES) return { ok: false, error: `no máximo ${MAX_SAMPLES} gravações por clone` };
364
+
365
+ const dir = newCloneDir();
366
+ const warnings: string[] = [];
367
+ let keep = false; // with no clone created, the samples don't stay behind
368
+
369
+ try {
370
+ const prepared = await prepareSamples(inputs, dir, warnings);
371
+ if (!prepared.ok) return prepared;
372
+ const { samples, total } = prepared;
373
+
374
+ const ids: { fish?: string; cartesia?: string } = {};
375
+ const tasks = chosen.map(async q => {
376
+ try {
377
+ ids[q] = await cloneOn(q, keys[q]!, name, ctx.language, samples, dir);
378
+ } catch (err: any) {
379
+ warnings.push(errorMessage(q, err));
380
+ }
381
+ });
382
+ await Promise.all(tasks);
383
+ if (!ids.fish && !ids.cartesia)
384
+ return { ok: false, error: warnings.join(" · ") || "nenhum provider criou o clone" };
385
+ keep = true;
386
+
387
+ const profile = saveProfile({
388
+ name,
389
+ avatar: p.avatar,
390
+ order: p.order,
391
+ fish: ids.fish ?? "",
392
+ cartesia: ids.cartesia ?? "",
393
+ });
394
+ if (!profile.ok)
395
+ return {
396
+ ok: false,
397
+ error: `o clone foi criado (${JSON.stringify(ids)}) mas o perfil não gravou: ${profile.error}`,
398
+ };
399
+
400
+ writeFileSync(
401
+ join(dir, "clone.json"),
402
+ JSON.stringify(
403
+ {
404
+ name,
405
+ context: ctx.id,
406
+ created_at: new Date().toISOString(),
407
+ profile: profile.id,
408
+ ids,
409
+ samples: samples.map(a => ({
410
+ file: a.file.split("/").pop(),
411
+ duration_s: +a.duration.toFixed(2),
412
+ text: a.text,
413
+ })),
414
+ },
415
+ null,
416
+ 2,
417
+ ) + "\n",
418
+ );
419
+
420
+ const done = (["fish", "cartesia"] as const).filter(q => ids[q]).map(q => PROVIDER_NAME[q]);
421
+ const failed = chosen.filter(q => !ids[q]);
422
+ return {
423
+ ok: true,
424
+ id: profile.id,
425
+ warnings,
426
+ failed,
427
+ output: `perfil “${name}” criado com a voz clonada na ${done.join(" e na ")} (${total.toFixed(0)} s de fala)${failed.length ? `. Falhou na ${failed.map(q => PROVIDER_NAME[q]).join(" e na ")}: crie de novo em Editar voz → Configurar` : ""}`,
428
+ };
429
+ } finally {
430
+ rmSync(join(dir, "cartesia.txt"), { force: true });
431
+ if (!keep) rmSync(dir, { recursive: true, force: true });
432
+ }
433
+ }
434
+
435
+ /* --- create or recreate the clone of an existing voice, on a single provider ---------------------
436
+ From the edit screen (Fish, Cartesia or ElevenLabs): the clone failed on that provider, was never made there, or the user wants to
437
+ redo it. Without `samples`, it uses the profile's stored recording in .clones/; with them (the voice
438
+ has no recording, only a Voice ID), it saves a new clone for the profile. The new Voice ID goes into the
439
+ profile right away; the old voice stays on the provider's account. */
440
+ export type RecreateRequest = {
441
+ profile?: unknown;
442
+ provider?: unknown;
443
+ samples?: unknown;
444
+ context?: unknown;
445
+ consent?: unknown;
446
+ };
447
+
448
+ export async function recreateClone(p: RecreateRequest): Promise<Resp & { voice_id?: string }> {
449
+ const profile = getProfile(p.profile);
450
+ if (!profile) return { ok: false, error: "voz não encontrada" };
451
+ const q = p.provider;
452
+ if (q !== "fish" && q !== "cartesia" && q !== "elevenlabs")
453
+ return { ok: false, error: "só dá para clonar na Fish Audio, na Cartesia ou na ElevenLabs" };
454
+ const key = providerKey(q);
455
+ if (!key) return { ok: false, error: `sem API key da ${PROVIDER_NAME[q]}: grave a chave aqui mesmo, acima` };
456
+
457
+ const inputs = (Array.isArray(p.samples) ? p.samples : []) as SampleInput[];
458
+ const warnings: string[] = [];
459
+ let dir: string,
460
+ samples: Sample[],
461
+ ctxId: string,
462
+ isNew = false,
463
+ keep = false;
464
+ if (inputs.length) {
465
+ if (p.consent !== true)
466
+ return { ok: false, error: "confirme que a voz é sua ou que você tem permissão de quem fala" };
467
+ if (inputs.length > MAX_SAMPLES) return { ok: false, error: `no máximo ${MAX_SAMPLES} gravações por clone` };
468
+ const ctx = findCloneContext(p.context ?? "mixed");
469
+ if (!ctx) return { ok: false, error: "contexto de idioma desconhecido" };
470
+ ctxId = ctx.id;
471
+ dir = newCloneDir();
472
+ isNew = true;
473
+ const prepared = await prepareSamples(inputs, dir, warnings);
474
+ if (!prepared.ok) {
475
+ rmSync(dir, { recursive: true, force: true });
476
+ return prepared;
477
+ }
478
+ samples = prepared.samples;
479
+ } else {
480
+ const c = latestCloneOf(profile.id);
481
+ if (!c)
482
+ return {
483
+ ok: false,
484
+ error: "esta voz não tem gravação guardada no app: envie um áudio de 30 s a 3 min para criar o clone",
485
+ };
486
+ dir = c.dir;
487
+ samples = c.samples;
488
+ ctxId = c.info.context ?? "mixed";
489
+ }
490
+ const ctx = findCloneContext(ctxId) ?? findCloneContext("mixed")!;
491
+
492
+ try {
493
+ let voiceId: string;
494
+ try {
495
+ voiceId = await cloneOn(q, key, profile.name, ctx.language, samples, dir);
496
+ } catch (err: any) {
497
+ return { ok: false, error: errorMessage(q, err) };
498
+ }
499
+ const order = profile.order.includes(q) ? profile.order : [...profile.order, q];
500
+ const saved = saveProfile({ ...profile, order, [q]: voiceId });
501
+ if (!saved.ok)
502
+ return {
503
+ ok: false,
504
+ error: `o clone foi criado na ${PROVIDER_NAME[q]} (${voiceId}) mas a voz não gravou: ${saved.error}`,
505
+ };
506
+ keep = true;
507
+
508
+ const infoFile = join(dir, "clone.json");
509
+ const info: CloneInfo = isNew
510
+ ? {
511
+ name: profile.name,
512
+ context: ctx.id,
513
+ created_at: new Date().toISOString(),
514
+ profile: profile.id,
515
+ ids: {},
516
+ samples: samples.map(a => ({
517
+ file: a.file.split("/").pop()!,
518
+ duration_s: +a.duration.toFixed(2),
519
+ text: a.text,
520
+ })),
521
+ }
522
+ : (readJson<CloneInfo>(infoFile) ?? {});
523
+ info.ids = { ...(info.ids ?? {}), [q]: voiceId };
524
+ writeFileSync(infoFile, JSON.stringify(info, null, 2) + "\n");
525
+ return { ok: true, voice_id: voiceId, warnings, output: `voz “${profile.name}” clonada na ${PROVIDER_NAME[q]}` };
526
+ } finally {
527
+ rmSync(join(dir, "cartesia.txt"), { force: true });
528
+ if (isNew && !keep) rmSync(dir, { recursive: true, force: true });
529
+ }
530
+ }
531
+
532
+ /* --- redo the whole voice, on all its providers --------------------------------------
533
+ From the edit screen's "Refazer voz": the user sees the same panel as create, with the voice's stored
534
+ recording attached. They can add more recordings to it (`keep`) or delete it and record everything again.
535
+ Underneath it's recreate: the new clone comes from all samples (stored and new) on each
536
+ provider the voice already has, and only after it exists is the old voice deleted on the account. So
537
+ an error midway never leaves the voice without a clone: the provider that failed keeps the old voice.
538
+ Name, avatar and order follow the voice (name and avatar can be changed in the same step). */
539
+ export type RedoRequest = {
540
+ profile?: unknown;
541
+ name?: unknown;
542
+ avatar?: unknown;
543
+ samples?: unknown;
544
+ keep?: unknown;
545
+ context?: unknown;
546
+ consent?: unknown;
547
+ };
548
+
549
+ export async function redoVoice(p: RedoRequest): Promise<Resp> {
550
+ const profile = getProfile(p.profile);
551
+ if (!profile) return { ok: false, error: "voz não encontrada" };
552
+ const name = typeof p.name === "string" && p.name.trim() ? p.name.trim() : profile.name;
553
+ if (name.length < 3 || name.length > Math.max(20, profile.name.length))
554
+ return { ok: false, error: "o nome da voz tem de 3 a 20 caracteres" };
555
+
556
+ const chosen = (["fish", "cartesia", "elevenlabs"] as const).filter(q => profile[q]);
557
+ if (!chosen.length)
558
+ return { ok: false, error: "esta voz não tem clone na Fish Audio, na Cartesia nem na ElevenLabs para refazer" };
559
+ const withoutKey = chosen.filter(q => !providerKey(q));
560
+ if (withoutKey.length)
561
+ return {
562
+ ok: false,
563
+ error: `sem API key da ${withoutKey.map(q => PROVIDER_NAME[q]).join(" e da ")}: grave em Configurações → Voz`,
564
+ };
565
+
566
+ const inputs = (Array.isArray(p.samples) ? p.samples : []) as SampleInput[];
567
+ if (inputs.length && p.consent !== true)
568
+ return { ok: false, error: "confirme que a voz é sua ou que você tem permissão de quem fala" };
569
+ const stored = latestCloneOf(profile.id);
570
+ const previous = p.keep === false ? null : stored;
571
+ if (!inputs.length && !previous)
572
+ return { ok: false, error: "nenhuma gravação: grave ou envie de 30 s a 3 min de fala" };
573
+
574
+ const ctx =
575
+ findCloneContext(
576
+ inputs.length ? (p.context ?? previous?.info.context ?? "mixed") : (previous?.info.context ?? "mixed"),
577
+ ) ?? findCloneContext("mixed")!;
578
+ const dir = newCloneDir();
579
+ const warnings: string[] = [];
580
+ let keep = false;
581
+
582
+ try {
583
+ /* the stored ones come to the new dir, first; the new ones go after them */
584
+ const previousSamples: Sample[] = (previous?.samples ?? []).map((a, i) => {
585
+ const file = join(dir, `sample-${String(i + 1).padStart(2, "0")}.wav`);
586
+ copyFileSync(a.file, file);
587
+ return { ...a, file };
588
+ });
589
+ const prepared = await prepareSamples(inputs, dir, warnings, previousSamples);
590
+ if (!prepared.ok) return prepared;
591
+ const { samples, total } = prepared;
592
+ if (samples.length > MAX_SAMPLES)
593
+ return {
594
+ ok: false,
595
+ error: `no máximo ${MAX_SAMPLES} gravações por clone: apague a gravação anterior e grave de novo`,
596
+ };
597
+
598
+ const created: Partial<Record<ClonableProvider, string>> = {};
599
+ await Promise.all(
600
+ chosen.map(async q => {
601
+ try {
602
+ created[q] = await cloneOn(q, providerKey(q)!, name, ctx.language, samples, dir);
603
+ } catch (err: any) {
604
+ warnings.push(`${errorMessage(q, err)} (a voz de antes continua na ${PROVIDER_NAME[q]})`);
605
+ }
606
+ }),
607
+ );
608
+ const done = chosen.filter(q => created[q]);
609
+ if (!done.length) return { ok: false, error: warnings.join(" · ") || "nenhum provedor criou o clone" };
610
+ keep = true;
611
+
612
+ const saved = saveProfile({ ...profile, name, avatar: p.avatar ?? profile.avatar, ...created });
613
+ if (!saved.ok)
614
+ return {
615
+ ok: false,
616
+ error: `o clone foi criado (${JSON.stringify(created)}) mas a voz não gravou: ${saved.error}`,
617
+ };
618
+
619
+ for (const q of done) {
620
+ const failure = await deleteOldVoice(profile.id, q, profile[q]!);
621
+ if (failure)
622
+ warnings.push(
623
+ `não deu para apagar a voz antiga na ${PROVIDER_NAME[q]} (${profile[q]}): ${failure}. Apague à mão na conta`,
624
+ );
625
+ }
626
+
627
+ writeFileSync(
628
+ join(dir, "clone.json"),
629
+ JSON.stringify(
630
+ {
631
+ name,
632
+ context: ctx.id,
633
+ created_at: new Date().toISOString(),
634
+ profile: profile.id,
635
+ ids: { ...(previous?.info.ids ?? {}), ...created },
636
+ samples: samples.map(a => ({
637
+ file: a.file.split("/").pop(),
638
+ duration_s: +a.duration.toFixed(2),
639
+ text: a.text,
640
+ })),
641
+ },
642
+ null,
643
+ 2,
644
+ ) + "\n",
645
+ );
646
+ /* the old recording is already inside the new dir (or was discarded on purpose) */
647
+ if (stored) rmSync(stored.dir, { recursive: true, force: true });
648
+
649
+ const failed = chosen.filter(q => !created[q]);
650
+ return {
651
+ ok: true,
652
+ id: profile.id,
653
+ warnings,
654
+ failed,
655
+ output: `voz “${name}” refeita na ${done.map(q => PROVIDER_NAME[q]).join(" e na ")} (${total.toFixed(0)} s de fala)${failed.length ? `. Falhou na ${failed.map(q => PROVIDER_NAME[q]).join(" e na ")}: abra Editar voz → Configurar para tentar de novo` : ""}`,
656
+ };
657
+ } finally {
658
+ rmSync(join(dir, "cartesia.txt"), { force: true });
659
+ if (!keep) rmSync(dir, { recursive: true, force: true });
660
+ }
661
+ }
662
+
663
+ const PROVIDER_NAME = { fish: "Fish Audio", cartesia: "Cartesia", elevenlabs: "ElevenLabs" } as const;
664
+ type ClonableProvider = keyof typeof PROVIDER_NAME;
665
+
666
+ const newCloneDir = () => {
667
+ const dir = join(CLONES_DIR, `${new Date().toISOString().slice(0, 10)}-${Math.random().toString(36).slice(2, 8)}`);
668
+ mkdirSync(dir, { recursive: true });
669
+ return dir;
670
+ };
671
+
672
+ const cloneOn = (q: ClonableProvider, key: string, name: string, language: string, samples: Sample[], dir: string) =>
673
+ q === "fish"
674
+ ? cloneOnFish(key, name, samples)
675
+ : q === "elevenlabs"
676
+ ? cloneOnElevenLabs(key, name, samples)
677
+ : cloneOnCartesia(key, name, language, samples, dir);
678
+
679
+ const errorMessage = (q: ClonableProvider, err: any) =>
680
+ err?.name === "TimeoutError"
681
+ ? `${PROVIDER_NAME[q]}: não respondeu em ${TIMEOUT_MS / 1000} s`
682
+ : String(err?.message ?? err);
683
+
684
+ /* the received recordings become normalized wavs in `dir`; checks 30 s to 3 min of speech */
685
+ async function prepareSamples(
686
+ inputs: SampleInput[],
687
+ dir: string,
688
+ warnings: string[],
689
+ previous: Sample[] = [],
690
+ ): Promise<{ ok: true; samples: Sample[]; total: number } | { ok: false; error: string }> {
691
+ /* `previous` (redo) are already in `dir`, first, and count toward the total */
692
+ const samples: Sample[] = [...previous];
693
+ for (const [i, e] of inputs.entries()) {
694
+ if (typeof e.data !== "string" || !e.data) continue;
695
+ const n = previous.length + i + 1;
696
+ const raw = join(dir, `raw-${n}.${extOf(e.mime)}`);
697
+ writeFileSync(raw, Buffer.from(e.data, "base64"));
698
+ const file = join(dir, `sample-${String(n).padStart(2, "0")}.wav`);
699
+ const good = await normalizeAudio(raw, file);
700
+ rmSync(raw, { force: true });
701
+ if (!good) {
702
+ warnings.push(`a gravação ${i + 1} não pôde ser lida e ficou de fora`);
703
+ continue;
704
+ }
705
+ const d = await durationOf(file);
706
+ if (d < MIN_SAMPLE_S) {
707
+ warnings.push(`a gravação ${i + 1} ficou curta demais (${d.toFixed(1)} s) e ficou de fora`);
708
+ continue;
709
+ }
710
+ const text = typeof e.text === "string" && e.text.trim() ? e.text.trim() : null;
711
+ samples.push({ file, text, duration: d });
712
+ }
713
+ const total = samples.reduce((s, a) => s + a.duration, 0);
714
+ if (total < MIN_TOTAL_S)
715
+ return {
716
+ ok: false,
717
+ error: `só ${total.toFixed(0)} s de fala aproveitável: grave pelo menos ${MIN_TOTAL_S} s (o máximo é 3 min)`,
718
+ };
719
+ if (total > MAX_TOTAL_S + 1)
720
+ return { ok: false, error: `${total.toFixed(0)} s de fala: o máximo é 3 min. Tire algum áudio e tente de novo` };
721
+ return { ok: true, samples, total };
722
+ }