@crievo/omni 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (709) hide show
  1. package/.env.example +41 -0
  2. package/README.md +2 -2
  3. package/bin/omni.mjs +14 -0
  4. package/docs/onboarding/clone-voice.png +0 -0
  5. package/docs/onboarding/getting-started.png +0 -0
  6. package/docs/onboarding/new-video.png +0 -0
  7. package/docs/onboarding/providers.png +0 -0
  8. package/docs/onboarding/publish.png +0 -0
  9. package/docs/onboarding/references.png +0 -0
  10. package/docs/onboarding.md +212 -0
  11. package/omnivideo.config.example.json +437 -0
  12. package/package.json +40 -4
  13. package/reference/captions.md +156 -0
  14. package/reference/carousel-contract.md +91 -0
  15. package/reference/cloud.md +96 -0
  16. package/reference/learnings.md +792 -0
  17. package/reference/omnichat.md +305 -0
  18. package/reference/premiere-mcp.md +91 -0
  19. package/reference/publish.md +305 -0
  20. package/reference/scenes-contract.md +109 -0
  21. package/reference/studio.md +666 -0
  22. package/reference/voicestudio.md +81 -0
  23. package/reference/writing-for-voice.md +67 -0
  24. package/requirements.txt +3 -0
  25. package/studio/api/app.ts +87 -0
  26. package/studio/api/lib/errors.ts +33 -0
  27. package/studio/api/lib/files.ts +41 -0
  28. package/studio/api/lib/hono.ts +9 -0
  29. package/studio/api/lib/result.ts +14 -0
  30. package/studio/api/lib/validate.ts +31 -0
  31. package/studio/api/lib/video.ts +10 -0
  32. package/studio/api/middleware/json-charset.ts +11 -0
  33. package/studio/api/middleware/local-only.ts +32 -0
  34. package/studio/api/middleware/no-timeout.ts +10 -0
  35. package/studio/api/routes/admin.ts +18 -0
  36. package/studio/api/routes/agent-cascade.ts +100 -0
  37. package/studio/api/routes/agent-history.ts +23 -0
  38. package/studio/api/routes/agent.ts +162 -0
  39. package/studio/api/routes/analytics.ts +31 -0
  40. package/studio/api/routes/approval.ts +60 -0
  41. package/studio/api/routes/archive.ts +12 -0
  42. package/studio/api/routes/captions.ts +28 -0
  43. package/studio/api/routes/carousel.ts +38 -0
  44. package/studio/api/routes/clone.ts +27 -0
  45. package/studio/api/routes/create.ts +19 -0
  46. package/studio/api/routes/events.ts +11 -0
  47. package/studio/api/routes/export.ts +14 -0
  48. package/studio/api/routes/media.ts +30 -0
  49. package/studio/api/routes/omnichat.ts +113 -0
  50. package/studio/api/routes/onboarding.ts +14 -0
  51. package/studio/api/routes/publish.ts +40 -0
  52. package/studio/api/routes/queue.ts +14 -0
  53. package/studio/api/routes/references.ts +40 -0
  54. package/studio/api/routes/runner.ts +17 -0
  55. package/studio/api/routes/schedule.ts +47 -0
  56. package/studio/api/routes/services.ts +11 -0
  57. package/studio/api/routes/sessions.ts +27 -0
  58. package/studio/api/routes/settings.ts +102 -0
  59. package/studio/api/routes/sound.ts +45 -0
  60. package/studio/api/routes/thumbnail.ts +18 -0
  61. package/studio/api/routes/usage.ts +13 -0
  62. package/studio/api/routes/videos.ts +41 -0
  63. package/studio/api/routes/voices.ts +71 -0
  64. package/studio/api/services/admin.ts +113 -0
  65. package/studio/api/services/agent-cascade.ts +975 -0
  66. package/studio/api/services/agent-claude.ts +47 -0
  67. package/studio/api/services/agent-history.ts +103 -0
  68. package/studio/api/services/agent-persona.ts +77 -0
  69. package/studio/api/services/agent-tools.ts +2237 -0
  70. package/studio/api/services/agent-ui-tools.ts +259 -0
  71. package/studio/api/services/agent-wait.ts +32 -0
  72. package/studio/api/services/agent.ts +553 -0
  73. package/studio/api/services/analytics.ts +92 -0
  74. package/studio/api/services/approval.ts +134 -0
  75. package/studio/api/services/archive.ts +38 -0
  76. package/studio/api/services/captions.ts +268 -0
  77. package/studio/api/services/carousel.ts +206 -0
  78. package/studio/api/services/classify.ts +196 -0
  79. package/studio/api/services/clone-phrases.ts +75 -0
  80. package/studio/api/services/clone.ts +722 -0
  81. package/studio/api/services/create.ts +421 -0
  82. package/studio/api/services/events.ts +215 -0
  83. package/studio/api/services/media.ts +32 -0
  84. package/studio/api/services/omnichat.ts +624 -0
  85. package/studio/api/services/onboarding.ts +77 -0
  86. package/studio/api/services/publish.ts +287 -0
  87. package/studio/api/services/queue.ts +150 -0
  88. package/studio/api/services/references.ts +269 -0
  89. package/studio/api/services/root.ts +60 -0
  90. package/studio/api/services/services.ts +114 -0
  91. package/studio/api/services/sessions.ts +1007 -0
  92. package/studio/api/services/settings.ts +983 -0
  93. package/studio/api/services/sound.ts +311 -0
  94. package/studio/api/services/thumbnail.ts +133 -0
  95. package/studio/api/services/usage.ts +27 -0
  96. package/studio/api/services/videos.ts +418 -0
  97. package/studio/api/services/voice-pause.ts +98 -0
  98. package/studio/api/services/voices.ts +324 -0
  99. package/studio/runner/runner.ts +244 -0
  100. package/studio/shared/omni-actions.ts +68 -0
  101. package/studio/shared/omni-admin.ts +52 -0
  102. package/studio/shared/omni-farewell.ts +48 -0
  103. package/studio/shared/omni-hotword.ts +20 -0
  104. package/studio/shared/omni-screens.ts +228 -0
  105. package/studio/shared/schemas/admin.ts +15 -0
  106. package/studio/shared/schemas/agent-cascade.ts +58 -0
  107. package/studio/shared/schemas/agent-history.ts +73 -0
  108. package/studio/shared/schemas/agent.ts +50 -0
  109. package/studio/shared/schemas/analytics.ts +6 -0
  110. package/studio/shared/schemas/approval.ts +22 -0
  111. package/studio/shared/schemas/captions.ts +19 -0
  112. package/studio/shared/schemas/carousel.ts +26 -0
  113. package/studio/shared/schemas/clone.ts +40 -0
  114. package/studio/shared/schemas/common.ts +16 -0
  115. package/studio/shared/schemas/omnichat.ts +54 -0
  116. package/studio/shared/schemas/onboarding.ts +5 -0
  117. package/studio/shared/schemas/publish.ts +45 -0
  118. package/studio/shared/schemas/references.ts +20 -0
  119. package/studio/shared/schemas/settings.ts +82 -0
  120. package/studio/shared/schemas/sound.ts +22 -0
  121. package/studio/shared/schemas/videos.ts +38 -0
  122. package/studio/shared/schemas/voices.ts +23 -0
  123. package/studio/shared/types/api.ts +1290 -0
  124. package/studio/studio.ts +148 -0
  125. package/studio/tsconfig.json +15 -0
  126. package/studio/web/assets/logos/cartesia.svg +4 -0
  127. package/studio/web/assets/logos/elevenlabs.svg +12 -0
  128. package/studio/web/assets/logos/fish.svg +24 -0
  129. package/studio/web/assets/logos/omni-black.png +0 -0
  130. package/studio/web/assets/logos/omni-white.png +0 -0
  131. package/studio/web/assets/logos/voicestudio.svg +21 -0
  132. package/studio/web/components/AgentMark.tsx +7 -0
  133. package/studio/web/components/AgentWalking.tsx +11 -0
  134. package/studio/web/components/ApiKeyField.tsx +104 -0
  135. package/studio/web/components/AppShell.tsx +64 -0
  136. package/studio/web/components/ApprovalNotice.tsx +65 -0
  137. package/studio/web/components/AvatarGroupIcon.tsx +22 -0
  138. package/studio/web/components/AvatarNameField.tsx +86 -0
  139. package/studio/web/components/AvatarPicker.tsx +84 -0
  140. package/studio/web/components/BrandLogo.tsx +16 -0
  141. package/studio/web/components/CartesiaPlanSetting.tsx +90 -0
  142. package/studio/web/components/ClaudeWalking.tsx +32 -0
  143. package/studio/web/components/CloneVoiceDialog/GuidedScript.tsx +39 -0
  144. package/studio/web/components/CloneVoiceDialog/LanguageBar.tsx +37 -0
  145. package/studio/web/components/CloneVoiceDialog/LevelMeter.tsx +31 -0
  146. package/studio/web/components/CloneVoiceDialog/ProviderChoice.tsx +81 -0
  147. package/studio/web/components/CloneVoiceDialog/QualityPanel.tsx +62 -0
  148. package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +50 -0
  149. package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +79 -0
  150. package/studio/web/components/CloneVoiceDialog/RedoSummary.tsx +54 -0
  151. package/studio/web/components/CloneVoiceDialog/SaveStep.tsx +91 -0
  152. package/studio/web/components/CloneVoiceDialog/SourceBar.tsx +30 -0
  153. package/studio/web/components/CloneVoiceDialog/index.tsx +141 -0
  154. package/studio/web/components/CloneVoiceDialog/types.ts +28 -0
  155. package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +528 -0
  156. package/studio/web/components/ConnectionSignals.tsx +51 -0
  157. package/studio/web/components/EditVoiceDialog/EditVoiceFooter.tsx +72 -0
  158. package/studio/web/components/EditVoiceDialog/ProviderCards.tsx +81 -0
  159. package/studio/web/components/EditVoiceDialog/index.tsx +256 -0
  160. package/studio/web/components/ErrorBanner.tsx +115 -0
  161. package/studio/web/components/FinishedTask.tsx +41 -0
  162. package/studio/web/components/FishModelSetting.tsx +69 -0
  163. package/studio/web/components/FishPriceSetting.tsx +44 -0
  164. package/studio/web/components/HumanBanner.tsx +64 -0
  165. package/studio/web/components/Lightbox.tsx +41 -0
  166. package/studio/web/components/LogStream.tsx +42 -0
  167. package/studio/web/components/Markdown.tsx +21 -0
  168. package/studio/web/components/MissingDmsPanel/BatchSend.tsx +137 -0
  169. package/studio/web/components/MissingDmsPanel/CommentCard.tsx +168 -0
  170. package/studio/web/components/MissingDmsPanel/CommentFeed.tsx +45 -0
  171. package/studio/web/components/MissingDmsPanel/DmBubble.tsx +38 -0
  172. package/studio/web/components/MissingDmsPanel/ReplyComposer.tsx +63 -0
  173. package/studio/web/components/MissingDmsPanel/index.tsx +98 -0
  174. package/studio/web/components/NotFound.tsx +48 -0
  175. package/studio/web/components/Omni/OmniAvatarPicker.tsx +92 -0
  176. package/studio/web/components/Omni/OmniCards.tsx +202 -0
  177. package/studio/web/components/Omni/OmniGlow.tsx +183 -0
  178. package/studio/web/components/Omni/OmniHistory.tsx +119 -0
  179. package/studio/web/components/Omni/OmniHotword.tsx +40 -0
  180. package/studio/web/components/Omni/OmniMicSelect.tsx +92 -0
  181. package/studio/web/components/Omni/OmniVoiceSample.tsx +81 -0
  182. package/studio/web/components/Omni/OmniWidget.tsx +743 -0
  183. package/studio/web/components/Omni/PendingActions.tsx +200 -0
  184. package/studio/web/components/Omni/Transcript.tsx +127 -0
  185. package/studio/web/components/OmnichatAccountStatus.tsx +87 -0
  186. package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +150 -0
  187. package/studio/web/components/OmnichatRulePanel/RuleDryRun.tsx +15 -0
  188. package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +42 -0
  189. package/studio/web/components/OmnichatRulePanel/RuleSkeleton.tsx +18 -0
  190. package/studio/web/components/OmnichatRulePanel/RuleStatus.tsx +55 -0
  191. package/studio/web/components/OmnichatRulePanel/index.tsx +115 -0
  192. package/studio/web/components/OmnichatRulePanel/rule-steps.ts +67 -0
  193. package/studio/web/components/OmnichatRulePanel/steps/MessageStep.tsx +91 -0
  194. package/studio/web/components/OmnichatRulePanel/steps/RepliesStep.tsx +201 -0
  195. package/studio/web/components/OmnichatRulePanel/steps/TriggerStep.tsx +76 -0
  196. package/studio/web/components/OmnichatRulePanel/steps/types.ts +11 -0
  197. package/studio/web/components/Palette.tsx +17 -0
  198. package/studio/web/components/PlatformCard.tsx +109 -0
  199. package/studio/web/components/PlatformIcon.tsx +19 -0
  200. package/studio/web/components/ProviderCard.tsx +80 -0
  201. package/studio/web/components/ProviderDetail.tsx +367 -0
  202. package/studio/web/components/ProviderLogo.tsx +20 -0
  203. package/studio/web/components/QueuePanel.tsx +77 -0
  204. package/studio/web/components/ReferenceActions.tsx +89 -0
  205. package/studio/web/components/ReferencePicker.tsx +136 -0
  206. package/studio/web/components/ResultMessage.tsx +7 -0
  207. package/studio/web/components/RuleForm.tsx +138 -0
  208. package/studio/web/components/RuleLog.tsx +151 -0
  209. package/studio/web/components/RunningTask.tsx +23 -0
  210. package/studio/web/components/SampleList.tsx +55 -0
  211. package/studio/web/components/SessionPanel.tsx +352 -0
  212. package/studio/web/components/SettingRow.tsx +14 -0
  213. package/studio/web/components/SettingsCategorySkeleton.tsx +25 -0
  214. package/studio/web/components/SidePanel.tsx +77 -0
  215. package/studio/web/components/Sidebar.tsx +289 -0
  216. package/studio/web/components/StageStepper.tsx +87 -0
  217. package/studio/web/components/StudioOffline.tsx +80 -0
  218. package/studio/web/components/TaskName.tsx +17 -0
  219. package/studio/web/components/TopBanners.tsx +30 -0
  220. package/studio/web/components/VideoOmnichatPanel.tsx +287 -0
  221. package/studio/web/components/VoiceAvatar.tsx +33 -0
  222. package/studio/web/components/VoiceCarousel.tsx +196 -0
  223. package/studio/web/components/VoiceCategory.tsx +9 -0
  224. package/studio/web/components/VoiceModal.tsx +39 -0
  225. package/studio/web/components/VoiceProfileCard.tsx +75 -0
  226. package/studio/web/components/VoiceProfilePicker.tsx +107 -0
  227. package/studio/web/components/VoiceProfilesSettings.tsx +150 -0
  228. package/studio/web/components/VoiceProviderCard.tsx +118 -0
  229. package/studio/web/components/VoiceProvidersCard.tsx +26 -0
  230. package/studio/web/components/WaitList.tsx +21 -0
  231. package/studio/web/components/icons.tsx +247 -0
  232. package/studio/web/components/ui/accordion.tsx +47 -0
  233. package/studio/web/components/ui/badge.tsx +18 -0
  234. package/studio/web/components/ui/calendar.tsx +43 -0
  235. package/studio/web/components/ui/chart.tsx +229 -0
  236. package/studio/web/components/ui/checkbox.tsx +26 -0
  237. package/studio/web/components/ui/cx.ts +2 -0
  238. package/studio/web/components/ui/date-time-picker.tsx +84 -0
  239. package/studio/web/components/ui/panel.tsx +23 -0
  240. package/studio/web/components/ui/picker.tsx +58 -0
  241. package/studio/web/components/ui/popover.tsx +24 -0
  242. package/studio/web/components/ui/select.tsx +89 -0
  243. package/studio/web/components/ui/skeleton.tsx +10 -0
  244. package/studio/web/components/ui/slider.tsx +68 -0
  245. package/studio/web/components/ui/switch.tsx +13 -0
  246. package/studio/web/components/ui/tooltip.test.tsx +30 -0
  247. package/studio/web/components/ui/tooltip.tsx +59 -0
  248. package/studio/web/hooks/useAccounts.ts +27 -0
  249. package/studio/web/hooks/useAdmin.ts +12 -0
  250. package/studio/web/hooks/useAnalytics.ts +21 -0
  251. package/studio/web/hooks/useAvatarAudioReactive.ts +87 -0
  252. package/studio/web/hooks/useBrowserConnect.ts +48 -0
  253. package/studio/web/hooks/useClonePlan.ts +14 -0
  254. package/studio/web/hooks/useConnected.ts +7 -0
  255. package/studio/web/hooks/useLiveQuery.ts +29 -0
  256. package/studio/web/hooks/useMenuCollapsed.ts +32 -0
  257. package/studio/web/hooks/useMicDevices.ts +53 -0
  258. package/studio/web/hooks/useMissingDms.ts +211 -0
  259. package/studio/web/hooks/useMissingSetupSteps.ts +26 -0
  260. package/studio/web/hooks/useNow.ts +11 -0
  261. package/studio/web/hooks/useOmniAgent.ts +278 -0
  262. package/studio/web/hooks/useOmniClaude.ts +124 -0
  263. package/studio/web/hooks/useOmniDock.ts +142 -0
  264. package/studio/web/hooks/useOmniGreeting.ts +60 -0
  265. package/studio/web/hooks/useOmniHotword.ts +128 -0
  266. package/studio/web/hooks/useOmnichat.ts +26 -0
  267. package/studio/web/hooks/useOnboarding.ts +27 -0
  268. package/studio/web/hooks/usePost.ts +23 -0
  269. package/studio/web/hooks/useQueue.ts +10 -0
  270. package/studio/web/hooks/useReferences.ts +20 -0
  271. package/studio/web/hooks/useRuleEditor.ts +101 -0
  272. package/studio/web/hooks/useRunner.ts +24 -0
  273. package/studio/web/hooks/useSaveAction.ts +23 -0
  274. package/studio/web/hooks/useSchedule.ts +11 -0
  275. package/studio/web/hooks/useServices.ts +10 -0
  276. package/studio/web/hooks/useSessions.ts +15 -0
  277. package/studio/web/hooks/useSettings.ts +11 -0
  278. package/studio/web/hooks/useSettingsSection.ts +30 -0
  279. package/studio/web/hooks/useSidePanel.ts +23 -0
  280. package/studio/web/hooks/useSound.ts +30 -0
  281. package/studio/web/hooks/useUsage.ts +11 -0
  282. package/studio/web/hooks/useVideoVoice.ts +36 -0
  283. package/studio/web/hooks/useVideos.ts +21 -0
  284. package/studio/web/hooks/useVoiceProfilePreview.ts +52 -0
  285. package/studio/web/hooks/useVoiceProfiles.ts +7 -0
  286. package/studio/web/hooks/useVoiceRunning.ts +10 -0
  287. package/studio/web/hooks/useVoiceStudioClosed.ts +8 -0
  288. package/studio/web/index.html +19 -0
  289. package/studio/web/lib/agent.ts +7 -0
  290. package/studio/web/lib/alerts.ts +210 -0
  291. package/studio/web/lib/analytics-metrics.ts +351 -0
  292. package/studio/web/lib/api.ts +70 -0
  293. package/studio/web/lib/audio.ts +39 -0
  294. package/studio/web/lib/avatars.ts +41 -0
  295. package/studio/web/lib/calendar.ts +218 -0
  296. package/studio/web/lib/caption-position.ts +249 -0
  297. package/studio/web/lib/carousel.ts +24 -0
  298. package/studio/web/lib/chime.ts +42 -0
  299. package/studio/web/lib/clone-voice.ts +55 -0
  300. package/studio/web/lib/export.ts +11 -0
  301. package/studio/web/lib/files.ts +8 -0
  302. package/studio/web/lib/format.ts +93 -0
  303. package/studio/web/lib/media.ts +29 -0
  304. package/studio/web/lib/mic-device.ts +72 -0
  305. package/studio/web/lib/missing-dms.ts +70 -0
  306. package/studio/web/lib/omni-cards.ts +42 -0
  307. package/studio/web/lib/omni-cascade.ts +1067 -0
  308. package/studio/web/lib/omni-greeting-audio.ts +24 -0
  309. package/studio/web/lib/omni-hotword.ts +231 -0
  310. package/studio/web/lib/omni-panel.ts +67 -0
  311. package/studio/web/lib/omni-realtime.ts +811 -0
  312. package/studio/web/lib/omni-ui.ts +402 -0
  313. package/studio/web/lib/omni-wait.ts +174 -0
  314. package/studio/web/lib/omnichat-api.ts +44 -0
  315. package/studio/web/lib/orb.ts +270 -0
  316. package/studio/web/lib/pcm-recorder.ts +115 -0
  317. package/studio/web/lib/platforms.ts +17 -0
  318. package/studio/web/lib/providers.ts +27 -0
  319. package/studio/web/lib/publish-draft.ts +52 -0
  320. package/studio/web/lib/publish-settings.ts +81 -0
  321. package/studio/web/lib/query-client.ts +122 -0
  322. package/studio/web/lib/references.ts +13 -0
  323. package/studio/web/lib/services.ts +40 -0
  324. package/studio/web/lib/sessions.ts +103 -0
  325. package/studio/web/lib/settings-categories.ts +58 -0
  326. package/studio/web/lib/sound-audio.ts +33 -0
  327. package/studio/web/lib/sound-timeline.ts +291 -0
  328. package/studio/web/lib/sse.ts +86 -0
  329. package/studio/web/lib/stage-history.ts +141 -0
  330. package/studio/web/lib/stages.ts +344 -0
  331. package/studio/web/lib/studio-offline.ts +60 -0
  332. package/studio/web/lib/upload.ts +24 -0
  333. package/studio/web/lib/video-tabs.ts +118 -0
  334. package/studio/web/lib/voice-profile-draft.ts +34 -0
  335. package/studio/web/lib/voice-profiles.ts +3 -0
  336. package/studio/web/main.tsx +14 -0
  337. package/studio/web/pages/AdminPage/index.tsx +123 -0
  338. package/studio/web/pages/AgentPage/components/Measurements.tsx +102 -0
  339. package/studio/web/pages/AgentPage/index.tsx +28 -0
  340. package/studio/web/pages/AnalyticsPage/components/About.tsx +52 -0
  341. package/studio/web/pages/AnalyticsPage/components/ChannelCards.tsx +106 -0
  342. package/studio/web/pages/AnalyticsPage/components/ChannelView.tsx +137 -0
  343. package/studio/web/pages/AnalyticsPage/components/CollectionStatus.tsx +46 -0
  344. package/studio/web/pages/AnalyticsPage/components/Comparison.tsx +68 -0
  345. package/studio/web/pages/AnalyticsPage/components/Diagnosis.tsx +50 -0
  346. package/studio/web/pages/AnalyticsPage/components/Highlights.tsx +49 -0
  347. package/studio/web/pages/AnalyticsPage/components/Hint.tsx +11 -0
  348. package/studio/web/pages/AnalyticsPage/components/HookVsBody.tsx +100 -0
  349. package/studio/web/pages/AnalyticsPage/components/HourlyWave.tsx +133 -0
  350. package/studio/web/pages/AnalyticsPage/components/Insights.tsx +70 -0
  351. package/studio/web/pages/AnalyticsPage/components/MetricCard.tsx +46 -0
  352. package/studio/web/pages/AnalyticsPage/components/PlanSection.tsx +40 -0
  353. package/studio/web/pages/AnalyticsPage/components/ProgressionChart.tsx +155 -0
  354. package/studio/web/pages/AnalyticsPage/components/ReelBarsChart.tsx +154 -0
  355. package/studio/web/pages/AnalyticsPage/components/ReelThumb.tsx +19 -0
  356. package/studio/web/pages/AnalyticsPage/components/ReelView.tsx +290 -0
  357. package/studio/web/pages/AnalyticsPage/components/ReelsTable.tsx +269 -0
  358. package/studio/web/pages/AnalyticsPage/components/RequestWithFocus.tsx +38 -0
  359. package/studio/web/pages/AnalyticsPage/components/SegmentedControl.tsx +35 -0
  360. package/studio/web/pages/AnalyticsPage/components/VsMedian.tsx +31 -0
  361. package/studio/web/pages/AnalyticsPage/index.tsx +17 -0
  362. package/studio/web/pages/GettingStartedPage/index.tsx +174 -0
  363. package/studio/web/pages/NewVideoPage/components/DeliverablesPicker.tsx +62 -0
  364. package/studio/web/pages/NewVideoPage/components/FormatPicker.tsx +33 -0
  365. package/studio/web/pages/NewVideoPage/index.tsx +133 -0
  366. package/studio/web/pages/NotFoundPage/index.tsx +9 -0
  367. package/studio/web/pages/OmnichatPage/components/NewFromLink.tsx +59 -0
  368. package/studio/web/pages/OmnichatPage/components/RuleListItem.tsx +64 -0
  369. package/studio/web/pages/OmnichatPage/components/RulePreview.tsx +14 -0
  370. package/studio/web/pages/OmnichatPage/components/RuleView.tsx +88 -0
  371. package/studio/web/pages/OmnichatPage/index.tsx +153 -0
  372. package/studio/web/pages/QueuePage/index.tsx +22 -0
  373. package/studio/web/pages/ReferencePage/components/ReferenceTexts.tsx +131 -0
  374. package/studio/web/pages/ReferencePage/index.tsx +216 -0
  375. package/studio/web/pages/ReferencesPage/components/NewReferenceForm.tsx +150 -0
  376. package/studio/web/pages/ReferencesPage/components/ReferenceCard.tsx +42 -0
  377. package/studio/web/pages/ReferencesPage/components/ReferenceGridSkeleton.tsx +31 -0
  378. package/studio/web/pages/ReferencesPage/index.tsx +67 -0
  379. package/studio/web/pages/SchedulePage/components/CalendarSkeleton.tsx +18 -0
  380. package/studio/web/pages/SchedulePage/components/DayView.tsx +177 -0
  381. package/studio/web/pages/SchedulePage/components/HourPicker.tsx +103 -0
  382. package/studio/web/pages/SchedulePage/components/MonthDayCell.tsx +111 -0
  383. package/studio/web/pages/SchedulePage/components/MonthView.tsx +47 -0
  384. package/studio/web/pages/SchedulePage/components/PlatformIcons.tsx +12 -0
  385. package/studio/web/pages/SchedulePage/components/PostThumb.tsx +15 -0
  386. package/studio/web/pages/SchedulePage/components/WeekColumn.tsx +75 -0
  387. package/studio/web/pages/SchedulePage/components/WeekView.tsx +35 -0
  388. package/studio/web/pages/SchedulePage/index.tsx +285 -0
  389. package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +263 -0
  390. package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +20 -0
  391. package/studio/web/pages/SettingsPage/components/ModelPicker.tsx +49 -0
  392. package/studio/web/pages/SettingsPage/components/ModelsSettings.tsx +138 -0
  393. package/studio/web/pages/SettingsPage/components/PlatformsCategory.tsx +23 -0
  394. package/studio/web/pages/SettingsPage/components/PublishingCategory/AccountStatus.tsx +29 -0
  395. package/studio/web/pages/SettingsPage/components/PublishingCategory/AutoPublishPanel.tsx +90 -0
  396. package/studio/web/pages/SettingsPage/components/PublishingCategory/BrowserConnect.tsx +62 -0
  397. package/studio/web/pages/SettingsPage/components/PublishingCategory/CheckField.tsx +19 -0
  398. package/studio/web/pages/SettingsPage/components/PublishingCategory/InstagramPanel.tsx +113 -0
  399. package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +146 -0
  400. package/studio/web/pages/SettingsPage/components/PublishingCategory/SaveBar.tsx +33 -0
  401. package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +132 -0
  402. package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCard.tsx +47 -0
  403. package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCardGroup.tsx +19 -0
  404. package/studio/web/pages/SettingsPage/components/PublishingCategory/TextPanel.tsx +55 -0
  405. package/studio/web/pages/SettingsPage/components/PublishingCategory/TiktokPanel.tsx +123 -0
  406. package/studio/web/pages/SettingsPage/components/PublishingCategory/YoutubePanel.tsx +100 -0
  407. package/studio/web/pages/SettingsPage/components/PublishingCategory/badges.tsx +15 -0
  408. package/studio/web/pages/SettingsPage/components/PublishingCategory/index.tsx +40 -0
  409. package/studio/web/pages/SettingsPage/components/PublishingCategory/types.ts +10 -0
  410. package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +389 -0
  411. package/studio/web/pages/SettingsPage/components/VoiceProvidersCategory.tsx +97 -0
  412. package/studio/web/pages/SettingsPage/index.tsx +85 -0
  413. package/studio/web/pages/VideoPage/components/AudioPlayer.tsx +113 -0
  414. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/AxisField.tsx +50 -0
  415. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionList.tsx +56 -0
  416. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionListItem.tsx +92 -0
  417. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionMargins.tsx +57 -0
  418. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionStyleFields.tsx +70 -0
  419. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionTextForm.tsx +87 -0
  420. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/ColorField.tsx +37 -0
  421. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/MarginField.tsx +59 -0
  422. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/NumberField.tsx +57 -0
  423. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/PositionControls.tsx +140 -0
  424. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/captionEditor.ts +215 -0
  425. package/studio/web/pages/VideoPage/components/CaptionPositionEditor/useCaptionPosition.ts +154 -0
  426. package/studio/web/pages/VideoPage/components/CarouselChangeRequest.tsx +72 -0
  427. package/studio/web/pages/VideoPage/components/CarouselCompositionStage.tsx +80 -0
  428. package/studio/web/pages/VideoPage/components/CarouselCreate.tsx +63 -0
  429. package/studio/web/pages/VideoPage/components/CarouselMissing.tsx +19 -0
  430. package/studio/web/pages/VideoPage/components/CarouselPublishStage.tsx +173 -0
  431. package/studio/web/pages/VideoPage/components/CarouselScriptStage.tsx +82 -0
  432. package/studio/web/pages/VideoPage/components/CarouselSlideCard.tsx +49 -0
  433. package/studio/web/pages/VideoPage/components/CarouselViewer.tsx +55 -0
  434. package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +53 -0
  435. package/studio/web/pages/VideoPage/components/CompositionStage.tsx +78 -0
  436. package/studio/web/pages/VideoPage/components/CoverPicker.tsx +32 -0
  437. package/studio/web/pages/VideoPage/components/EditorStage.tsx +53 -0
  438. package/studio/web/pages/VideoPage/components/LineTakes.tsx +160 -0
  439. package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +124 -0
  440. package/studio/web/pages/VideoPage/components/LineVoicePicker.tsx +37 -0
  441. package/studio/web/pages/VideoPage/components/MediaImage.tsx +25 -0
  442. package/studio/web/pages/VideoPage/components/MotionScriptStage.tsx +39 -0
  443. package/studio/web/pages/VideoPage/components/NarrationStage.tsx +70 -0
  444. package/studio/web/pages/VideoPage/components/PublishForm.tsx +387 -0
  445. package/studio/web/pages/VideoPage/components/PublishStage.tsx +76 -0
  446. package/studio/web/pages/VideoPage/components/PublishText.tsx +149 -0
  447. package/studio/web/pages/VideoPage/components/ScenesApprovalPanel.tsx +278 -0
  448. package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +72 -0
  449. package/studio/web/pages/VideoPage/components/ScriptApprovalPanel.tsx +189 -0
  450. package/studio/web/pages/VideoPage/components/ScriptStage.tsx +17 -0
  451. package/studio/web/pages/VideoPage/components/SequencePlayer.tsx +146 -0
  452. package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +117 -0
  453. package/studio/web/pages/VideoPage/components/SoundTimeline/AreaSelection.tsx +74 -0
  454. package/studio/web/pages/VideoPage/components/SoundTimeline/BedPanel.tsx +94 -0
  455. package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +45 -0
  456. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionOverlay.tsx +44 -0
  457. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +32 -0
  458. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionsPanel.tsx +22 -0
  459. package/studio/web/pages/VideoPage/components/SoundTimeline/ClipBlock.tsx +111 -0
  460. package/studio/web/pages/VideoPage/components/SoundTimeline/ClipPanel.tsx +76 -0
  461. package/studio/web/pages/VideoPage/components/SoundTimeline/CutsPanel.tsx +44 -0
  462. package/studio/web/pages/VideoPage/components/SoundTimeline/EditorTabBar.tsx +73 -0
  463. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +46 -0
  464. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectItem.tsx +89 -0
  465. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectPanel.tsx +232 -0
  466. package/studio/web/pages/VideoPage/components/SoundTimeline/EffectsRow.tsx +21 -0
  467. package/studio/web/pages/VideoPage/components/SoundTimeline/Lane.tsx +39 -0
  468. package/studio/web/pages/VideoPage/components/SoundTimeline/MarkerRow.tsx +40 -0
  469. package/studio/web/pages/VideoPage/components/SoundTimeline/MuteButton.tsx +36 -0
  470. package/studio/web/pages/VideoPage/components/SoundTimeline/NumberInput.tsx +37 -0
  471. package/studio/web/pages/VideoPage/components/SoundTimeline/PlanWarnings.tsx +19 -0
  472. package/studio/web/pages/VideoPage/components/SoundTimeline/PlayerResizeHandle.tsx +50 -0
  473. package/studio/web/pages/VideoPage/components/SoundTimeline/PreviewControls.tsx +51 -0
  474. package/studio/web/pages/VideoPage/components/SoundTimeline/SceneRow.tsx +54 -0
  475. package/studio/web/pages/VideoPage/components/SoundTimeline/SettingsPanel.tsx +65 -0
  476. package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +99 -0
  477. package/studio/web/pages/VideoPage/components/SoundTimeline/SoundPreview.tsx +82 -0
  478. package/studio/web/pages/VideoPage/components/SoundTimeline/SpeedPanel.tsx +128 -0
  479. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineDock.tsx +56 -0
  480. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +26 -0
  481. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineSkeleton.tsx +21 -0
  482. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineToolbar.tsx +160 -0
  483. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineTracks.tsx +121 -0
  484. package/studio/web/pages/VideoPage/components/SoundTimeline/TrackSidebar.tsx +30 -0
  485. package/studio/web/pages/VideoPage/components/SoundTimeline/VoiceRow.tsx +53 -0
  486. package/studio/web/pages/VideoPage/components/SoundTimeline/Waveform.tsx +97 -0
  487. package/studio/web/pages/VideoPage/components/SoundTimeline/clipActions.ts +50 -0
  488. package/studio/web/pages/VideoPage/components/SoundTimeline/effectActions.ts +161 -0
  489. package/studio/web/pages/VideoPage/components/SoundTimeline/index.tsx +161 -0
  490. package/studio/web/pages/VideoPage/components/SoundTimeline/timelineGeometry.ts +45 -0
  491. package/studio/web/pages/VideoPage/components/SoundTimeline/types.ts +43 -0
  492. package/studio/web/pages/VideoPage/components/SoundTimeline/useAutosave.ts +107 -0
  493. package/studio/web/pages/VideoPage/components/SoundTimeline/useEditorShortcuts.ts +168 -0
  494. package/studio/web/pages/VideoPage/components/SoundTimeline/useFitHeight.ts +40 -0
  495. package/studio/web/pages/VideoPage/components/SoundTimeline/useFollowPlayhead.ts +21 -0
  496. package/studio/web/pages/VideoPage/components/SoundTimeline/useListenMix.ts +43 -0
  497. package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayback.ts +231 -0
  498. package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayerWidth.ts +27 -0
  499. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundData.ts +124 -0
  500. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +511 -0
  501. package/studio/web/pages/VideoPage/components/StageDrawer/CompareVersions.tsx +111 -0
  502. package/studio/web/pages/VideoPage/components/StageDrawer/VersionCard.tsx +81 -0
  503. package/studio/web/pages/VideoPage/components/StageDrawer/VersionMedia.tsx +24 -0
  504. package/studio/web/pages/VideoPage/components/StageDrawer/VersionText.tsx +15 -0
  505. package/studio/web/pages/VideoPage/components/StageDrawer/index.tsx +109 -0
  506. package/studio/web/pages/VideoPage/components/StageProcess.tsx +126 -0
  507. package/studio/web/pages/VideoPage/components/StageResult.tsx +69 -0
  508. package/studio/web/pages/VideoPage/components/SwapReference.tsx +92 -0
  509. package/studio/web/pages/VideoPage/components/ThumbnailPanel.tsx +252 -0
  510. package/studio/web/pages/VideoPage/components/TimingPanel.tsx +89 -0
  511. package/studio/web/pages/VideoPage/components/VideoPlayer.tsx +18 -0
  512. package/studio/web/pages/VideoPage/components/VideoSkeleton.tsx +50 -0
  513. package/studio/web/pages/VideoPage/components/VideoVoiceInfo.tsx +33 -0
  514. package/studio/web/pages/VideoPage/components/VideoVoicePicker.tsx +165 -0
  515. package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +56 -0
  516. package/studio/web/pages/VideoPage/components/WithLogo.tsx +10 -0
  517. package/studio/web/pages/VideoPage/index.tsx +295 -0
  518. package/studio/web/pages/VideosPage/components/PublishedOn.tsx +85 -0
  519. package/studio/web/pages/VideosPage/components/ServicesPanel.tsx +81 -0
  520. package/studio/web/pages/VideosPage/components/SessionsPanel.tsx +62 -0
  521. package/studio/web/pages/VideosPage/components/VideoListSkeleton.tsx +25 -0
  522. package/studio/web/pages/VideosPage/components/VideoRow.tsx +122 -0
  523. package/studio/web/pages/VideosPage/index.tsx +134 -0
  524. package/studio/web/pages/VoicePage/index.tsx +28 -0
  525. package/studio/web/router.tsx +201 -0
  526. package/studio/web/styles/base/elements.css +55 -0
  527. package/studio/web/styles/base/utilities.css +25 -0
  528. package/studio/web/styles/components/badge.css +33 -0
  529. package/studio/web/styles/components/claude-running.css +27 -0
  530. package/studio/web/styles/components/claude-session.css +212 -0
  531. package/studio/web/styles/components/claude-walking.css +92 -0
  532. package/studio/web/styles/components/clone-voice-save.css +136 -0
  533. package/studio/web/styles/components/clone-voice.css +379 -0
  534. package/studio/web/styles/components/controls.css +70 -0
  535. package/studio/web/styles/components/data-and-image.css +54 -0
  536. package/studio/web/styles/components/forms.css +132 -0
  537. package/studio/web/styles/components/human-banner.css +99 -0
  538. package/studio/web/styles/components/lightbox.css +50 -0
  539. package/studio/web/styles/components/markdown.css +68 -0
  540. package/studio/web/styles/components/modal.css +70 -0
  541. package/studio/web/styles/components/omni-cards.css +105 -0
  542. package/studio/web/styles/components/omni.css +859 -0
  543. package/studio/web/styles/components/omnichat.css +538 -0
  544. package/studio/web/styles/components/panel.css +115 -0
  545. package/studio/web/styles/components/platform-card.css +140 -0
  546. package/studio/web/styles/components/provider-card.css +132 -0
  547. package/studio/web/styles/components/queue.css +185 -0
  548. package/studio/web/styles/components/reference-actions.css +133 -0
  549. package/studio/web/styles/components/reference-picker.css +146 -0
  550. package/studio/web/styles/components/results-drawer.css +224 -0
  551. package/studio/web/styles/components/segmented.css +115 -0
  552. package/studio/web/styles/components/side-panel-tab.css +23 -0
  553. package/studio/web/styles/components/status-signals.css +66 -0
  554. package/studio/web/styles/components/stepper.css +208 -0
  555. package/studio/web/styles/components/table.css +19 -0
  556. package/studio/web/styles/components/tabs.css +24 -0
  557. package/studio/web/styles/components/ui/accordion.css +71 -0
  558. package/studio/web/styles/components/ui/chart.css +162 -0
  559. package/studio/web/styles/components/ui/checkbox.css +37 -0
  560. package/studio/web/styles/components/ui/date-picker.css +153 -0
  561. package/studio/web/styles/components/ui/select.css +110 -0
  562. package/studio/web/styles/components/ui/skeleton.css +25 -0
  563. package/studio/web/styles/components/ui/slider.css +81 -0
  564. package/studio/web/styles/components/ui/switch.css +41 -0
  565. package/studio/web/styles/components/ui/tooltip.css +24 -0
  566. package/studio/web/styles/components/voice-carousel.css +255 -0
  567. package/studio/web/styles/components/voice-editor.css +179 -0
  568. package/studio/web/styles/components/voice-grid.css +56 -0
  569. package/studio/web/styles/components/voice-profiles.css +192 -0
  570. package/studio/web/styles/design-system.md +136 -0
  571. package/studio/web/styles/index.css +116 -0
  572. package/studio/web/styles/layout/page.css +41 -0
  573. package/studio/web/styles/layout/sidebar-groups.css +39 -0
  574. package/studio/web/styles/layout/sidebar.css +111 -0
  575. package/studio/web/styles/pages/agent.css +26 -0
  576. package/studio/web/styles/pages/analytics/dashboard.css +614 -0
  577. package/studio/web/styles/pages/analytics/insights.css +48 -0
  578. package/studio/web/styles/pages/getting-started.css +166 -0
  579. package/studio/web/styles/pages/new-video/form.css +32 -0
  580. package/studio/web/styles/pages/not-found.css +59 -0
  581. package/studio/web/styles/pages/references.css +204 -0
  582. package/studio/web/styles/pages/schedule/calendar.css +541 -0
  583. package/studio/web/styles/pages/schedule/chip-states.css +60 -0
  584. package/studio/web/styles/pages/settings/models.css +72 -0
  585. package/studio/web/styles/pages/settings/shell.css +361 -0
  586. package/studio/web/styles/pages/settings/usage.css +63 -0
  587. package/studio/web/styles/pages/studio-offline.css +105 -0
  588. package/studio/web/styles/pages/video/caption-controls.css +194 -0
  589. package/studio/web/styles/pages/video/captions-grid.css +70 -0
  590. package/studio/web/styles/pages/video/captions.css +79 -0
  591. package/studio/web/styles/pages/video/editor-chat.css +103 -0
  592. package/studio/web/styles/pages/video/export.css +50 -0
  593. package/studio/web/styles/pages/video/frames.css +29 -0
  594. package/studio/web/styles/pages/video/fullscreen-stage.css +400 -0
  595. package/studio/web/styles/pages/video/instagram-carousel.css +309 -0
  596. package/studio/web/styles/pages/video/layout.css +285 -0
  597. package/studio/web/styles/pages/video/narration.css +138 -0
  598. package/studio/web/styles/pages/video/preview-overview.css +11 -0
  599. package/studio/web/styles/pages/video/processing-notice.css +34 -0
  600. package/studio/web/styles/pages/video/publish-when.css +76 -0
  601. package/studio/web/styles/pages/video/publish.css +97 -0
  602. package/studio/web/styles/pages/video/result.css +96 -0
  603. package/studio/web/styles/pages/video/scenes.css +167 -0
  604. package/studio/web/styles/pages/video/sound-timeline.css +672 -0
  605. package/studio/web/styles/pages/video/sound-toolbar.css +149 -0
  606. package/studio/web/styles/pages/video/take-player.css +100 -0
  607. package/studio/web/styles/pages/video/thumbnail.css +157 -0
  608. package/studio/web/styles/pages/video/timeline.css +117 -0
  609. package/studio/web/styles/pages/video/voice-picker.css +14 -0
  610. package/studio/web/styles/pages/video-list/active-row.css +27 -0
  611. package/studio/web/styles/pages/video-list/card-link.css +49 -0
  612. package/studio/web/styles/pages/video-list/list.css +86 -0
  613. package/studio/web/styles/theme/identity/details.css +204 -0
  614. package/studio/web/styles/theme/identity/shell.css +249 -0
  615. package/studio/web/styles/theme/identity/surfaces.css +183 -0
  616. package/studio/web/styles/theme/identity/titles-and-buttons.css +120 -0
  617. package/studio/web/styles/theme/no-borders/blocks.css +208 -0
  618. package/studio/web/styles/theme/no-borders/controls.css +256 -0
  619. package/studio/web/styles/tokens.css +245 -0
  620. package/studio/web/types/api.ts +2 -0
  621. package/studio/web/types/assets.d.ts +9 -0
  622. package/templates/brief.md +27 -0
  623. package/templates/captions.html +204 -0
  624. package/templates/carousel-composition.md +47 -0
  625. package/templates/carousel-script.md +60 -0
  626. package/templates/components/voiceavatar/README.md +87 -0
  627. package/templates/components/voiceavatar/demo.html +50 -0
  628. package/templates/components/voiceavatar/voiceavatar.js +415 -0
  629. package/templates/motion.md +80 -0
  630. package/templates/playbook.md +43 -0
  631. package/templates/reference.md +123 -0
  632. package/templates/scene.html +112 -0
  633. package/templates/scenes.example.json +96 -0
  634. package/templates/script.md +63 -0
  635. package/templates/slide.html +71 -0
  636. package/templates/wireframe.html +91 -0
  637. package/tools/analysis.mjs +300 -0
  638. package/tools/approval.mjs +442 -0
  639. package/tools/assemble.mjs +942 -0
  640. package/tools/build-motion.mjs +437 -0
  641. package/tools/captions.mjs +246 -0
  642. package/tools/carousel.mjs +558 -0
  643. package/tools/compose.mjs +307 -0
  644. package/tools/cuts.mjs +74 -0
  645. package/tools/editor.mjs +419 -0
  646. package/tools/epidemic-capture.js +54 -0
  647. package/tools/fanout.mjs +69 -0
  648. package/tools/history.mjs +70 -0
  649. package/tools/history.py +61 -0
  650. package/tools/job-queue.mjs +180 -0
  651. package/tools/job_queue.py +430 -0
  652. package/tools/lib/agents.mjs +412 -0
  653. package/tools/lib/channel.mjs +399 -0
  654. package/tools/lib/claude-settings.mjs +67 -0
  655. package/tools/lib/cli.mjs +9 -0
  656. package/tools/lib/fanout.mjs +234 -0
  657. package/tools/lib/http.mjs +94 -0
  658. package/tools/lib/index.js +14 -0
  659. package/tools/lib/mcp-server.mjs +69 -0
  660. package/tools/lib/metrics/metrics.mjs +204 -0
  661. package/tools/lib/narration.mjs +15 -0
  662. package/tools/lib/oauth-client.mjs +295 -0
  663. package/tools/lib/omnichat/config.mjs +139 -0
  664. package/tools/lib/omnichat/errors.mjs +42 -0
  665. package/tools/lib/omnichat/extract.mjs +82 -0
  666. package/tools/lib/omnichat/gate.mjs +151 -0
  667. package/tools/lib/omnichat/manual.mjs +166 -0
  668. package/tools/lib/omnichat/match.mjs +78 -0
  669. package/tools/lib/omnichat/pass.mjs +393 -0
  670. package/tools/lib/omnichat/record.mjs +208 -0
  671. package/tools/lib/omnichat/text.mjs +315 -0
  672. package/tools/lib/omnichat/webhook.mjs +166 -0
  673. package/tools/lib/pairing.mjs +99 -0
  674. package/tools/lib/paths.mjs +41 -0
  675. package/tools/lib/platforms/instagram.mjs +469 -0
  676. package/tools/lib/platforms/tiktok.mjs +343 -0
  677. package/tools/lib/platforms/youtube.mjs +228 -0
  678. package/tools/lib/publish/publish.mjs +244 -0
  679. package/tools/lib/schedule/schedule.mjs +191 -0
  680. package/tools/lib/service.mjs +134 -0
  681. package/tools/lib/skill-client.mjs +126 -0
  682. package/tools/lib/skills-release.mjs +196 -0
  683. package/tools/lib/testing.mjs +52 -0
  684. package/tools/lib/up.mjs +187 -0
  685. package/tools/metrics.mjs +415 -0
  686. package/tools/models.mjs +134 -0
  687. package/tools/narrate.mjs +257 -0
  688. package/tools/omni.mjs +298 -0
  689. package/tools/omni_paths.py +12 -0
  690. package/tools/omnichat.mjs +1577 -0
  691. package/tools/omnivideo.py +1122 -0
  692. package/tools/package.mjs +130 -0
  693. package/tools/platforms.mjs +236 -0
  694. package/tools/premiere.mjs +533 -0
  695. package/tools/publish.mjs +1496 -0
  696. package/tools/reference.mjs +1408 -0
  697. package/tools/render.mjs +426 -0
  698. package/tools/runner.mjs +156 -0
  699. package/tools/setup.mjs +466 -0
  700. package/tools/skills-release.mjs +93 -0
  701. package/tools/sound.mjs +1323 -0
  702. package/tools/soundtrack.mjs +217 -0
  703. package/tools/thumbnail.mjs +271 -0
  704. package/tools/timing.mjs +108 -0
  705. package/tools/usage.py +628 -0
  706. package/tools/voice-envelope.mjs +185 -0
  707. package/tools/voice.py +448 -0
  708. package/tools/voice_sample.py +75 -0
  709. package/tools/wireframe.mjs +230 -0
@@ -0,0 +1,553 @@
1
+ /* Omni, the studio's voice agent, on Grok Voice (xAI realtime, speech to speech). The persona, the
2
+ greeting, the keyterms and the silence handling live in the agent saved in console.x.ai
3
+ (`agent.xai_agent_id`); the browser connects to it directly over WebSocket. This side mints the
4
+ short-lived token (the API key never leaves the machine), picks the voice (Omni is always Omni:
5
+ the user only chooses a female or a male voice), adds the tools and the session's context, and
6
+ keeps the log that measures it: latency to the first audio, minutes, tool calls and 👍/👎.
7
+
8
+ Config `agent` in omnivideo.config.json:
9
+ { enabled, xai_agent_id, gender, voices: { female, male }, model, language, idle: { nudge_s, hangup_s },
10
+ limits: { daily_voice_min, daily_usd, max_session_min, max_actions, max_tool_calls }, wait_max_min }.
11
+ The persona is agent-persona.ts: the instructions without a console agent, the context message with one.
12
+ Log: .agent/log.jsonl. */
13
+
14
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { AVATAR_SEED } from "../../shared/schemas/agent";
17
+ import { isHotword } from "../../shared/omni-hotword";
18
+ import { OMNI_ENGINES, type OmniEngine, engineOf } from "../../shared/omni-admin";
19
+ import { ROOT, readJson } from "./root";
20
+ import { readEnvKeys } from "./services";
21
+ import { listVideos } from "./videos";
22
+ import { getQueue } from "./queue";
23
+ import { getAnalytics } from "./analytics";
24
+ import { TOOL_DEFS, pendingForUser, registerToolSession } from "./agent-tools";
25
+ import { UI_TOOL_DEFS } from "./agent-ui-tools";
26
+ import { GROK_TOOL_RULES, omniPersona } from "./agent-persona";
27
+
28
+ export const AGENT_DIR = join(ROOT, ".agent");
29
+ const LOG_FILE = join(AGENT_DIR, "log.jsonl");
30
+ const SAMPLES_DIR = join(AGENT_DIR, "samples");
31
+ const XAI = "https://api.x.ai/v1";
32
+
33
+ /* Grok Voice is billed per second of open session ($0.08/min); classification per token (classify.ts) */
34
+ export const VOICE_PRICE_PER_MIN = 0.08;
35
+
36
+ export type AgentGender = "female" | "male";
37
+ /* idle: Grok asks "Ainda tá aí?" after `nudge_s` of silence and the page hangs up after `hangup_s`
38
+ without the user speaking (with a console agent, its own follow-up settings ask first) */
39
+ /* limits: what a runaway conversation can spend or do (enforced here and in agent-tools.ts) */
40
+ /* daily_voice_min: Grok voice minutes per day; past it, Omni goes on in Claude mode (speech → STT →
41
+ the user's own Claude, answering in text), which costs almost nothing */
42
+ export type AgentLimits = {
43
+ daily_voice_min: number;
44
+ daily_usd: number;
45
+ max_session_min: number;
46
+ max_actions: number;
47
+ max_tool_calls: number;
48
+ };
49
+ export type AgentConfig = {
50
+ /* false: Omni is off (the widget hides, no session starts) */
51
+ enabled: boolean;
52
+ xai_agent_id: string | null;
53
+ /* what Omni runs on, for every user (the admin panel, agent.engine): Grok Voice or a cascade mode */
54
+ engine: OmniEngine;
55
+ gender: AgentGender;
56
+ /* Omni's face: the seed of its avatar (the studio's Omni settings) */
57
+ avatar: string;
58
+ voices: Record<AgentGender, string>;
59
+ model: string;
60
+ language: string;
61
+ idle: { nudge_s: number; hangup_s: number };
62
+ limits: AgentLimits;
63
+ /* waiting for the studio's work (wait_for_work, lib/omni-wait.ts): the call sleeps with the
64
+ microphone open and hangs up for good past this many minutes */
65
+ wait_max_min: number;
66
+ };
67
+ const DEFAULTS: AgentConfig = {
68
+ enabled: true,
69
+ xai_agent_id: null,
70
+ engine: "grok",
71
+ gender: "female",
72
+ avatar: "omni-ara",
73
+ voices: { female: "ara", male: "rex" },
74
+ model: "grok-voice-3.0",
75
+ language: "pt-BR",
76
+ idle: { nudge_s: 10, hangup_s: 20 },
77
+ limits: { daily_voice_min: 5, daily_usd: 1, max_session_min: 10, max_actions: 5, max_tool_calls: 40 },
78
+ wait_max_min: 20,
79
+ };
80
+ const num = (v: unknown, fallback: number, min: number, max: number) =>
81
+ typeof v === "number" && Number.isFinite(v) && v >= min && v <= max ? v : fallback;
82
+ const seconds = (v: unknown, fallback: number) => (typeof v === "number" && v >= 5 && v <= 600 ? v : fallback);
83
+ const text = (v: unknown, fallback: string) => (typeof v === "string" && v.trim() ? v.trim() : fallback);
84
+
85
+ /* what a user hears when the admin switched Omni off or moved it to another engine (admin panel) */
86
+ export const OMNI_OFF = "a Omni está desligada pelo admin";
87
+ export const engineMismatch = (engine: OmniEngine) =>
88
+ `o admin mudou a Omni para ${OMNI_ENGINES.find(e => e.id === engine)?.label ?? engine}: abra a conversa de novo`;
89
+
90
+ export function agentConfig(): AgentConfig {
91
+ const cfg = readJson<any>(join(ROOT, "omnivideo.config.json"))?.agent ?? {};
92
+ const L = DEFAULTS.limits;
93
+ return {
94
+ enabled: cfg.enabled !== false,
95
+ xai_agent_id: typeof cfg.xai_agent_id === "string" && cfg.xai_agent_id ? cfg.xai_agent_id : null,
96
+ engine: engineOf(cfg),
97
+ gender: cfg.gender === "male" ? "male" : "female",
98
+ avatar: typeof cfg.avatar === "string" && AVATAR_SEED.test(cfg.avatar) ? cfg.avatar : DEFAULTS.avatar,
99
+ voices: {
100
+ female: text(cfg.voices?.female, DEFAULTS.voices.female),
101
+ male: text(cfg.voices?.male, DEFAULTS.voices.male),
102
+ },
103
+ model: text(cfg.model, DEFAULTS.model),
104
+ language: text(cfg.language, DEFAULTS.language),
105
+ idle: {
106
+ nudge_s: seconds(cfg.idle?.nudge_s, DEFAULTS.idle.nudge_s),
107
+ hangup_s: seconds(cfg.idle?.hangup_s, DEFAULTS.idle.hangup_s),
108
+ },
109
+ limits: {
110
+ daily_voice_min: num(cfg.limits?.daily_voice_min, L.daily_voice_min, 0, 600),
111
+ daily_usd: num(cfg.limits?.daily_usd, L.daily_usd, 0, 100),
112
+ max_session_min: num(cfg.limits?.max_session_min, L.max_session_min, 1, 60),
113
+ max_actions: num(cfg.limits?.max_actions, L.max_actions, 0, 50),
114
+ max_tool_calls: num(cfg.limits?.max_tool_calls, L.max_tool_calls, 1, 500),
115
+ },
116
+ wait_max_min: num(cfg.wait_max_min, DEFAULTS.wait_max_min, 1, 120),
117
+ };
118
+ }
119
+
120
+ function xaiKey(): string {
121
+ const key = readEnvKeys().XAI_API_KEY;
122
+ if (!key) throw new Error("falta XAI_API_KEY no .env");
123
+ return key;
124
+ }
125
+
126
+ async function xai(path: string, body?: unknown): Promise<Response> {
127
+ return fetch(`${XAI}${path}`, {
128
+ method: body === undefined ? "GET" : "POST",
129
+ headers: { Authorization: `Bearer ${xaiKey()}`, "Content-Type": "application/json" },
130
+ body: body === undefined ? undefined : JSON.stringify(body),
131
+ signal: AbortSignal.timeout(20_000),
132
+ });
133
+ }
134
+
135
+ /* --- voice sample ------------------------------------------------------------------------------ */
136
+ const SAMPLE_TEXT = "Oi! Eu sou o Omni. Posso ver seus vídeos, as métricas do canal e pedir alterações por você.";
137
+
138
+ /* what Omni says when the user started a call and said nothing for a while (recorded once, like the sample) */
139
+ const PROMPT_TEXT = "Oi, no que posso ajudar?";
140
+ const CLIPS = { greeting: SAMPLE_TEXT, prompt: PROMPT_TEXT } as const;
141
+ export type VoiceClip = keyof typeof CLIPS;
142
+
143
+ /* a short pt-BR clip of the female or male voice (xAI TTS), cached in .agent/samples: the greeting that
144
+ is also the sample ("Ouvir"), or the "no que posso ajudar?" prompt */
145
+ export async function voiceSample(gender: string, clip: string = "greeting"): Promise<string> {
146
+ if (gender !== "female" && gender !== "male") throw new Error("voz inválida");
147
+ if (!(clip in CLIPS)) throw new Error("trecho inválido");
148
+ /* the greeting the user recorded for Omni (Fish, the cascade voice) wins over the xAI sample, for both genders */
149
+ const chosen = join(SAMPLES_DIR, "greeting.mp3");
150
+ if (clip === "greeting" && existsSync(chosen)) return chosen;
151
+ const config = agentConfig();
152
+ const voice = config.voices[gender];
153
+ const file = join(SAMPLES_DIR, `${voice.toLowerCase()}${clip === "greeting" ? "" : `-${clip}`}.mp3`);
154
+ if (existsSync(file)) return file;
155
+ const r = await xai("/tts", { text: CLIPS[clip as VoiceClip], voice_id: voice, language: config.language });
156
+ if (!r.ok) throw new Error(`xAI ${r.status}: ${(await r.text()).slice(0, 200)}`);
157
+ mkdirSync(SAMPLES_DIR, { recursive: true });
158
+ writeFileSync(file, Buffer.from(await r.arrayBuffer()));
159
+ return file;
160
+ }
161
+
162
+ /* --- session ---------------------------------------------------------------------------------- */
163
+ async function snapshot(): Promise<string> {
164
+ const lines: string[] = [];
165
+ try {
166
+ const videos: any[] = (await listVideos(getQueue())).filter((v: any) => !v.archived);
167
+ videos.sort((a, b) => b.created - a.created);
168
+ lines.push(`Conteúdos ativos: ${videos.length}. Os mais recentes:`);
169
+ for (const v of videos.slice(0, 8)) {
170
+ const next = v.status?.stages?.find((s: any) => !s.ok);
171
+ const published = ["youtube", "instagram", "tiktok"].filter(p => v.publication?.[p]?.url);
172
+ lines.push(
173
+ `- ${v.slug}: "${v.title}" · ${next ? `próxima etapa: ${next.name}` : "pronto"}${published.length ? ` · publicado (${published.join(", ")})` : ""}`,
174
+ );
175
+ }
176
+ } catch {
177
+ lines.push("(não deu para ler a lista de vídeos agora: use list_videos)");
178
+ }
179
+ try {
180
+ const a = await getAnalytics();
181
+ const acc = a?.account?.at?.(-1);
182
+ const m = a?.medians?.current;
183
+ if (acc) lines.push(`Instagram @${acc.username}: ${acc.followers} seguidores.`);
184
+ if (m)
185
+ lines.push(
186
+ `Mediana dos Reels (n=${m.n}): ${m.views} views, ${m.watched_pct}% assistido, ${m.skip_3s_pct}% pulam nos 3 s.`,
187
+ );
188
+ } catch {
189
+ /* metrics are optional */
190
+ }
191
+ /* what waits for the user, so Omni knows it before anyone asks */
192
+ try {
193
+ const w = await pendingForUser();
194
+ lines.push(
195
+ w.count
196
+ ? `Esperando o usuário (${w.count}): ${w.items
197
+ .map(
198
+ i => `${i.label} em ${i.slug}${i.auto_in_min != null ? ` (aprova sozinho em ${i.auto_in_min} min)` : ""}`,
199
+ )
200
+ .join("; ")}. Para abrir, navigate com o slug e a etapa (stage) de pending_approvals.`
201
+ : "Nada esperando aprovação do usuário agora.",
202
+ );
203
+ } catch {
204
+ /* the list is optional: pending_approvals reads it again */
205
+ }
206
+ return lines.join("\n");
207
+ }
208
+
209
+ /* what changes every session: the time and the studio's state, sent as a system message */
210
+ export async function sessionContext(now: Date): Promise<string> {
211
+ return `Agora são ${now.toLocaleString("pt-BR", { timeZone: "America/Sao_Paulo" })} (horário de Brasília).
212
+
213
+ Estado do estúdio no começo da conversa (para o resto, use as ferramentas):
214
+ ${await snapshot()}`;
215
+ }
216
+
217
+ export type AgentSession = {
218
+ id: string;
219
+ token: string;
220
+ expires_at: number;
221
+ url: string;
222
+ /* the session.update sent right after connecting; with a console agent it only adds to it
223
+ (voice, audio format, tools), so the console's persona and silence settings stay */
224
+ session: Record<string, unknown>;
225
+ /* the system message sent after it: the time and the studio's state */
226
+ context: string;
227
+ config: AgentConfig;
228
+ /* what is left of today's voice quota: the page hangs up and switches to Claude mode at zero */
229
+ voice_remaining_s: number;
230
+ };
231
+
232
+ const brl = (v: number) => v.toFixed(2).replace(".", ",");
233
+
234
+ /* Grok sessions minted since the studio started: only these can be resumed after a wait */
235
+ const grokSessions = new Set<string>();
236
+
237
+ /* `resume`: the call slept waiting for the studio's work (wait_for_work) and wakes up. Same session
238
+ id (its tool limits and staged cards go on), a new token and a fresh context; the minutes it slept
239
+ are not voice minutes (each open stretch logs its own "end", see usageToday) */
240
+ export async function startSession(resume?: string): Promise<AgentSession> {
241
+ if (resume && !grokSessions.has(resume)) throw new Error("essa conversa não existe mais: comece outra");
242
+ const config = agentConfig();
243
+ if (!config.enabled) throw new Error(OMNI_OFF);
244
+ if (config.engine !== "grok") throw new Error(engineMismatch(config.engine));
245
+ const remaining = voiceRemainingSeconds(config);
246
+ if (remaining <= 0) throw new QuotaError(config.limits.daily_voice_min);
247
+ const spent = spentToday();
248
+ if (spent >= config.limits.daily_usd)
249
+ throw new Error(
250
+ `limite diário do Omni atingido: US$ ${brl(spent)} de US$ ${brl(config.limits.daily_usd)} (agent.limits.daily_usd)`,
251
+ );
252
+ /* the token only opens the WebSocket: a short life is enough */
253
+ const r = await xai("/realtime/client_secrets", { expires_after: { seconds: 60 } });
254
+ if (!r.ok) throw new Error(`xAI ${r.status}: ${(await r.text()).slice(0, 200)}`);
255
+ const secret: any = await r.json();
256
+ const id =
257
+ resume ?? `${new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "")}-${crypto.randomUUID().slice(0, 6)}`;
258
+ const voice = config.voices[config.gender];
259
+ const audio = {
260
+ input: { format: { type: "audio/pcm", rate: 24000 }, transcription: { language_hint: config.language } },
261
+ output: { format: { type: "audio/pcm", rate: 24000 } },
262
+ };
263
+ /* the tools replace the console's (its end_call included): end_call is in TOOL_DEFS; the UI tools
264
+ (navigate…) run in the browser (lib/omni-ui); web_search is xAI's own, run on their side */
265
+ const tools = [...TOOL_DEFS, ...UI_TOOL_DEFS, { type: "web_search" }];
266
+ const session = config.xai_agent_id
267
+ ? { voice, audio, tools }
268
+ : {
269
+ voice,
270
+ instructions: `${omniPersona(config.gender)}\n\n${GROK_TOOL_RULES}`,
271
+ turn_detection: { type: "server_vad", idle_timeout_ms: config.idle.nudge_s * 1000 },
272
+ audio,
273
+ tools,
274
+ };
275
+ if (resume) logEvent({ session: id, type: "resume" });
276
+ else {
277
+ registerToolSession(id, { max_actions: config.limits.max_actions, max_tool_calls: config.limits.max_tool_calls });
278
+ grokSessions.add(id);
279
+ logEvent({ session: id, type: "start", voice, model: config.model, agent: config.xai_agent_id });
280
+ }
281
+ return {
282
+ id,
283
+ token: secret.value,
284
+ expires_at: secret.expires_at,
285
+ url: config.xai_agent_id
286
+ ? `wss://api.x.ai/v1/realtime?agent_id=${encodeURIComponent(config.xai_agent_id)}`
287
+ : `wss://api.x.ai/v1/realtime?model=${encodeURIComponent(config.model)}`,
288
+ session,
289
+ /* with a console agent, the persona rides in the context message: the console keeps the greeting,
290
+ the speed and the silence settings, and the rules stay the same as the cascade's */
291
+ context: config.xai_agent_id
292
+ ? `${omniPersona(config.gender)}\n\n${GROK_TOOL_RULES}\n\n${await sessionContext(new Date())}`
293
+ : await sessionContext(new Date()),
294
+ config,
295
+ voice_remaining_s: Math.floor(remaining),
296
+ };
297
+ }
298
+
299
+ /* the voice quota for today is used up: the page switches to Claude mode */
300
+ export class QuotaError extends Error {
301
+ constructor(minutes: number) {
302
+ super(`a cota de voz de hoje acabou (${minutes} min): sigo por texto com o Claude`);
303
+ }
304
+ }
305
+
306
+ export function voiceRemainingSeconds(config = agentConfig()): number {
307
+ return config.limits.daily_voice_min * 60 - usageToday().voice_s;
308
+ }
309
+
310
+ /* --- log and stats ----------------------------------------------------------------------------- */
311
+ export type AgentEvent = { session: string; type: string; [k: string]: unknown };
312
+
313
+ export function logEvent(e: AgentEvent) {
314
+ mkdirSync(AGENT_DIR, { recursive: true });
315
+ appendFileSync(LOG_FILE, `${JSON.stringify({ at: new Date().toISOString(), ...e })}\n`);
316
+ }
317
+
318
+ /* a card decided by voice (confirm_action, cancel_action): the same "action" line as the buttons */
319
+ export function logResolved(session: string, output: Record<string, unknown>) {
320
+ const r = output.resolved as { kind: string; slug: string | null; summary: string; state: string } | undefined;
321
+ if (!r) return;
322
+ logEvent({
323
+ session,
324
+ type: "action",
325
+ kind: r.kind,
326
+ slug: r.slug,
327
+ summary: r.summary,
328
+ via: "voice",
329
+ ...(r.state === "canceled"
330
+ ? { canceled: true }
331
+ : { ok: r.state === "confirmed", error: typeof output.error === "string" ? output.error : undefined }),
332
+ });
333
+ }
334
+
335
+ function readLog(): (AgentEvent & { at: string })[] {
336
+ try {
337
+ return readFileSync(LOG_FILE, "utf8")
338
+ .split("\n")
339
+ .filter(Boolean)
340
+ .map(l => JSON.parse(l));
341
+ } catch {
342
+ return [];
343
+ }
344
+ }
345
+
346
+ /* what Omni used today (local day): Grok voice seconds by session time, the sum of its open stretches
347
+ (a call that slept waiting for the studio's work logs an "end" per stretch and a "resume" when it
348
+ wakes; a stretch with no "end", a closed tab, counts until its last logged event; cascade sessions
349
+ don't bill by the minute),
350
+ classification, transcription and the cascade's LLM and paid TTS in dollars */
351
+ export function usageToday(): { voice_s: number; classify_usd: number; stt_usd: number; cascade_usd: number } {
352
+ const today = new Date().toDateString();
353
+ const log = readLog().filter(e => new Date(e.at).toDateString() === today);
354
+ const bySession = new Map<string, { openAt: number | null; last: number; duration: number; voice: boolean }>();
355
+ let classify = 0;
356
+ let stt = 0;
357
+ let cascade = 0;
358
+ for (const e of log) {
359
+ const t = Date.parse(e.at);
360
+ if (e.type === "tool") classify += Number(e.cost_usd) || 0;
361
+ if (e.type === "stt") stt += Number(e.cost_usd) || 0;
362
+ if (e.type === "llm" || e.type === "tts") cascade += Number(e.cost_usd) || 0;
363
+ if (e.session === "-" || e.type === "stt" || e.type === "claude") continue;
364
+ const s = bySession.get(e.session) ?? { openAt: t, last: t, duration: 0, voice: false };
365
+ if (e.type === "start") s.voice = !e.mode || e.mode === "grok";
366
+ if (e.type === "start" || e.type === "resume") s.openAt = t;
367
+ s.last = Math.max(s.last, t);
368
+ if (e.type === "end") {
369
+ s.duration += Number(e.duration_s) || 0;
370
+ s.openAt = null;
371
+ }
372
+ bySession.set(e.session, s);
373
+ }
374
+ let seconds = 0;
375
+ for (const s of bySession.values())
376
+ if (s.voice) seconds += s.duration + (s.openAt != null ? (s.last - s.openAt) / 1000 : 0);
377
+ return { voice_s: seconds, classify_usd: classify, stt_usd: stt, cascade_usd: cascade };
378
+ }
379
+
380
+ export function spentToday(): number {
381
+ const u = usageToday();
382
+ return (u.voice_s / 60) * VOICE_PRICE_PER_MIN + u.classify_usd + u.stt_usd + u.cascade_usd;
383
+ }
384
+
385
+ /* --- Claude mode: speech to text ---------------------------------------------------------------- */
386
+ /* grok-voice-transcribe: $0.10 per hour of audio */
387
+ const STT_PRICE_PER_S = 0.1 / 3600;
388
+
389
+ /* raw PCM16 mono 24 kHz from the page's microphone (base64) → text */
390
+ export async function transcribe(pcmBase64: string, session: string): Promise<{ text: string; duration: number }> {
391
+ const pcm = Buffer.from(pcmBase64, "base64");
392
+ if (pcm.length < 24000 * 2 * 0.3) throw new Error("áudio curto demais");
393
+ if (pcm.length > 24000 * 2 * 120) throw new Error("áudio longo demais (máximo 2 minutos)");
394
+ const form = new FormData();
395
+ form.append("model", "grok-voice-transcribe-2.0");
396
+ form.append("language", agentConfig().language);
397
+ form.append("audio_format", "pcm");
398
+ form.append("sample_rate", "24000");
399
+ form.append("file", new Blob([pcm]), "speech.pcm");
400
+ const r = await fetch(`${XAI}/stt`, {
401
+ method: "POST",
402
+ headers: { Authorization: `Bearer ${xaiKey()}` },
403
+ body: form,
404
+ signal: AbortSignal.timeout(30_000),
405
+ });
406
+ if (!r.ok) throw new Error(`xAI ${r.status}: ${(await r.text()).slice(0, 200)}`);
407
+ const j: any = await r.json();
408
+ const duration = Number(j.duration) || pcm.length / 48000;
409
+ logEvent({ session, type: "stt", duration_s: Math.round(duration * 10) / 10, cost_usd: duration * STT_PRICE_PER_S });
410
+ return { text: String(j.text ?? "").trim(), duration };
411
+ }
412
+
413
+ /* --- "Oi, Omni" -------------------------------------------------------------------------------------
414
+ With the panel closed, the page (lib/omni-hotword.ts) sends the start of each utterance that follows a
415
+ pause; it is transcribed here and checked for Omni's name (shared/omni-hotword.ts). The text is not
416
+ kept nor sent back. A cap on the audio sent per day (`agent.hotword.daily_min`, default 20 min, about
417
+ $0.03) keeps a talkative day cheap; it also counts in daily_usd, like any transcription. */
418
+ const HOTWORD_SESSION = "hotword";
419
+
420
+ function hotwordDailyMin(): number {
421
+ const cfg = readJson<any>(join(ROOT, "omnivideo.config.json"))?.agent?.hotword ?? {};
422
+ return num(cfg.daily_min, 20, 0, 600);
423
+ }
424
+
425
+ function hotwordSecondsToday(): number {
426
+ const today = new Date().toDateString();
427
+ return readLog()
428
+ .filter(e => e.session === HOTWORD_SESSION && e.type === "stt" && new Date(e.at).toDateString() === today)
429
+ .reduce((n, e) => n + (Number(e.duration_s) || 0), 0);
430
+ }
431
+
432
+ export async function checkHotword(pcmBase64: string): Promise<{ called: boolean }> {
433
+ const cfg = agentConfig();
434
+ if (!cfg.enabled) throw new Error(OMNI_OFF);
435
+ const daily = hotwordDailyMin();
436
+ if (hotwordSecondsToday() >= daily * 60) throw new HotwordLimit(daily);
437
+ if (spentToday() >= cfg.limits.daily_usd) throw new HotwordLimit(daily, true);
438
+ const { text } = await transcribe(pcmBase64, HOTWORD_SESSION);
439
+ const called = isHotword(text);
440
+ if (called) logEvent({ session: HOTWORD_SESSION, type: "hotword" });
441
+ return { called };
442
+ }
443
+
444
+ export class HotwordLimit extends Error {
445
+ constructor(minutes: number, spend = false) {
446
+ super(
447
+ spend
448
+ ? "o gasto de hoje da Omni chegou ao limite: o “Oi, Omni” volta amanhã"
449
+ : `o “Oi, Omni” já ouviu ${minutes} min hoje: volta amanhã`,
450
+ );
451
+ }
452
+ }
453
+
454
+ const quantile = (xs: number[], q: number) => {
455
+ if (!xs.length) return null;
456
+ const s = [...xs].sort((a, b) => a - b);
457
+ return s[Math.min(s.length - 1, Math.floor(q * s.length))];
458
+ };
459
+
460
+ export type AgentStats = {
461
+ sessions: number;
462
+ minutes: number;
463
+ turns: number;
464
+ latency_p50_ms: number | null;
465
+ latency_p90_ms: number | null;
466
+ tool_calls: number;
467
+ tool_errors: number;
468
+ classify_cost_usd: number;
469
+ voice_cost_usd: number;
470
+ thumbs_up: number;
471
+ thumbs_down: number;
472
+ by_mode: AgentModeStats[];
473
+ };
474
+
475
+ export function agentStats(): AgentStats {
476
+ const log = readLog();
477
+ const sessions = new Set(log.filter(e => e.type === "start").map(e => e.session));
478
+ /* only Grok bills the open minute: cascade sessions (start with a mode) don't count as voice */
479
+ const cascade = new Set(log.filter(e => e.type === "start" && e.mode && e.mode !== "grok").map(e => e.session));
480
+ const ends = log.filter(e => e.type === "end" && !cascade.has(e.session));
481
+ const seconds = ends.reduce((n, e) => n + (Number(e.duration_s) || 0), 0);
482
+ const latencies = log
483
+ .filter(e => e.type === "turn" && typeof e.latency_ms === "number")
484
+ .map(e => e.latency_ms as number);
485
+ const tools = log.filter(e => e.type === "tool");
486
+ /* the last vote per reply wins (switching 👍 to 👎) */
487
+ const votes = new Map<string, string>();
488
+ for (const e of log.filter(e => e.type === "feedback")) votes.set(`${e.session}:${e.item}`, String(e.value));
489
+ return {
490
+ sessions: sessions.size,
491
+ minutes: Math.round((seconds / 60) * 10) / 10,
492
+ turns: latencies.length,
493
+ latency_p50_ms: quantile(latencies, 0.5),
494
+ latency_p90_ms: quantile(latencies, 0.9),
495
+ tool_calls: tools.length,
496
+ tool_errors: tools.filter(e => e.error).length,
497
+ classify_cost_usd:
498
+ tools.reduce((n, e) => n + (Number(e.cost_usd) || 0), 0) +
499
+ log
500
+ .filter(e => e.type === "stt" || e.type === "llm" || e.type === "tts")
501
+ .reduce((n, e) => n + (Number(e.cost_usd) || 0), 0),
502
+ voice_cost_usd: (seconds / 60) * VOICE_PRICE_PER_MIN,
503
+ thumbs_up: [...votes.values()].filter(v => v === "up").length,
504
+ thumbs_down: [...votes.values()].filter(v => v === "down").length,
505
+ by_mode: statsByMode(log, votes),
506
+ };
507
+ }
508
+
509
+ export type AgentModeStats = {
510
+ mode: "grok" | "stream" | "chunks";
511
+ sessions: number;
512
+ minutes: number;
513
+ turns: number;
514
+ latency_p50_ms: number | null;
515
+ latency_p90_ms: number | null;
516
+ cost_usd: number;
517
+ cost_per_min_usd: number | null;
518
+ thumbs_up: number;
519
+ thumbs_down: number;
520
+ };
521
+
522
+ /* each conversation by how it talked (its start: no mode is Grok, from before the cascade), with what
523
+ it cost: Grok bills its open minutes, the cascade its STT, LLM and paid TTS; tools on top of both */
524
+ function statsByMode(log: (AgentEvent & { at: string })[], votes: Map<string, string>): AgentModeStats[] {
525
+ const modeOf = new Map<string, AgentModeStats["mode"]>();
526
+ for (const e of log)
527
+ if (e.type === "start") modeOf.set(e.session, e.mode === "stream" || e.mode === "chunks" ? e.mode : "grok");
528
+ return (["grok", "stream", "chunks"] as const).map(mode => {
529
+ const mine = (e: AgentEvent) => modeOf.get(e.session) === mode;
530
+ const seconds = log.filter(e => e.type === "end" && mine(e)).reduce((n, e) => n + (Number(e.duration_s) || 0), 0);
531
+ const latencies = log
532
+ .filter(e => e.type === "turn" && mine(e) && typeof e.latency_ms === "number")
533
+ .map(e => e.latency_ms as number);
534
+ const billed = log
535
+ .filter(e => mine(e) && (e.type === "tool" || e.type === "stt" || e.type === "llm" || e.type === "tts"))
536
+ .reduce((n, e) => n + (Number(e.cost_usd) || 0), 0);
537
+ const cost = billed + (mode === "grok" ? (seconds / 60) * VOICE_PRICE_PER_MIN : 0);
538
+ const myVotes = [...votes.entries()].filter(([k]) => modeOf.get(k.split(":")[0]) === mode).map(([, v]) => v);
539
+ const minutes = seconds / 60;
540
+ return {
541
+ mode,
542
+ sessions: [...modeOf.values()].filter(m => m === mode).length,
543
+ minutes: Math.round(minutes * 10) / 10,
544
+ turns: latencies.length,
545
+ latency_p50_ms: quantile(latencies, 0.5),
546
+ latency_p90_ms: quantile(latencies, 0.9),
547
+ cost_usd: cost,
548
+ cost_per_min_usd: minutes > 0 ? cost / minutes : null,
549
+ thumbs_up: myVotes.filter(v => v === "up").length,
550
+ thumbs_down: myVotes.filter(v => v === "down").length,
551
+ };
552
+ });
553
+ }
@@ -0,0 +1,92 @@
1
+ /* Analytics: the metrics of the published Reels and Claude's analysis of
2
+ them. The numbers live in tools/metrics.mjs (the launchd agent collects them
3
+ every 4 hours) and tools/analysis.mjs gathers account, medians, script
4
+ strategy and hook; the studio only shows what it returns, like voice usage.
5
+ Collection is every 4 hours, and each Reel's series comes in its detail.
6
+ The analysis is the av-analysis skill, in a studio session of its own (type
7
+ "analysis", target "channel"): it writes videos/<slug>/analysis.md,
8
+ channel/analyses/<day>.md and updates channel/playbook.md. */
9
+
10
+ import { join } from "node:path";
11
+ import { NODE, ROOT, TOOLS_DIR, run, readText, readJson } from "./root";
12
+ import { videoDirs } from "./videos";
13
+ import { openSession, replyToSession, latestSessionFor, type ApiResult } from "./sessions";
14
+
15
+ const METRICS = join(TOOLS_DIR, "metrics.mjs");
16
+ const ANALYSIS = join(TOOLS_DIR, "analysis.mjs");
17
+ export const CHANNEL_DIR = join(ROOT, "channel");
18
+
19
+ /* account, medians and each Reel; the UI only rereads on the "analytics" notice (events.ts), so
20
+ there is no cache: the file that just changed is what comes back */
21
+ export async function getAnalytics(): Promise<any> {
22
+ const r = await run([NODE, ANALYSIS, "context"]);
23
+ if (r.code !== 0) return { error: r.stderr.trim().split("\n").pop() || `analysis.mjs saiu com código ${r.code}` };
24
+ try {
25
+ const data = JSON.parse(r.stdout);
26
+ const last = data.last_analysis;
27
+ if (last) last.text = readText(join(ROOT, last.file));
28
+ return data;
29
+ } catch {
30
+ return { error: "analysis.mjs devolveu algo que não é JSON" };
31
+ }
32
+ }
33
+
34
+ /* one Reel: what the context has on it, the series of readings (the detail's wave line) and
35
+ analysis.md, if the skill already wrote it */
36
+ export async function getReel(slug: string): Promise<any> {
37
+ const d = await getAnalytics();
38
+ if (d.error) return d;
39
+ const v = d.videos.find((x: any) => x.slug === slug);
40
+ if (!v) return null;
41
+ const dir = videoDirs().get(slug);
42
+ const hourly = dir ? (readJson<any>(join(dir, "metrics.json"))?.instagram?.hourly ?? []) : [];
43
+ return { ...v, hourly, medians: d.medians, analysis_text: dir ? readText(join(dir, "analysis.md")) : null };
44
+ }
45
+
46
+ /* reads the API now (GET only; nothing changes on Instagram) and redoes everyone's current reading */
47
+ export async function collectMetrics(): Promise<ApiResult> {
48
+ const r = await run([NODE, METRICS, "collect", "--force"]);
49
+ if (r.code !== 0) return { ok: false, error: (r.stderr || r.stdout).trim().split("\n").pop() || "a coleta falhou" };
50
+ return { ok: true, output: r.stdout.trim() ? "métricas lidas agora" : "nada para ler" };
51
+ }
52
+
53
+ /* enables and disables the launchd agent (collection every 4 hours): installs with the studio's node */
54
+ export async function setMetricsAgent(enable: unknown): Promise<ApiResult> {
55
+ const r = await run([NODE, METRICS, "agent", enable ? "install" : "uninstall"]);
56
+ if (r.code !== 0) return { ok: false, error: (r.stderr || r.stdout).trim() };
57
+ return { ok: true, output: r.stdout.trim() };
58
+ }
59
+
60
+ /* "Gerar análise": of the channel, or of one Reel (`slug`). With a stopped analysis session, it
61
+ resumes with the new request and remembers the previous analyses; without one, opens one. */
62
+ export function requestAnalysis(body: { slug?: unknown; focus?: unknown }): ApiResult {
63
+ const slug = String(body.slug ?? "").trim();
64
+ if (slug && !videoDirs().has(slug)) return { ok: false, error: "vídeo não encontrado" };
65
+ const focus = String(body.focus ?? "").trim();
66
+ const prompt = [
67
+ `/av-analysis ${slug ? `videos/${slug}` : "channel"}${focus ? ` foco: ${focus}` : ""}`,
68
+ slug
69
+ ? `Pedido pela tela Analytics do studio: analise só o Reel videos/${slug} (o analysis.md dele; o playbook só se ele muda um aprendizado).`
70
+ : "Pedido pela tela Analytics do studio: analise o canal inteiro (cada Reel que mudou, os insights e o plano dos próximos vídeos, e o playbook).",
71
+ "No fim, responda em poucas linhas: o achado principal e os próximos passos. O usuário lê na tela Analytics.",
72
+ ].join("\n");
73
+ const last = latestSessionFor("analysis", "channel");
74
+ if (last) return replyToSession(last.id, prompt, "request");
75
+ return openSession("analysis", "channel", CHANNEL_DIR, "Análise do canal", prompt);
76
+ }
77
+
78
+ /* reply written in the Analytics page's Claude panel */
79
+ export function chatAnalysis(message: unknown): ApiResult {
80
+ const text = String(message ?? "").trim();
81
+ const last = latestSessionFor("analysis", "channel");
82
+ if (last) return replyToSession(last.id, text || "Continue de onde parou.", text ? "user" : "resume");
83
+ if (!text) return { ok: false, error: "diga o que quer saber das métricas" };
84
+ return openSession(
85
+ "analysis",
86
+ "channel",
87
+ CHANNEL_DIR,
88
+ "Análise do canal",
89
+ `Pergunta feita pela tela Analytics do studio sobre as métricas do canal: ${text}\nUse \`node tools/analysis.mjs context\` e o channel/playbook.md; para análise completa, a skill av-analysis.`,
90
+ "user",
91
+ );
92
+ }