@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.
- package/.env.example +41 -0
- package/README.md +2 -2
- package/bin/omni.mjs +14 -0
- package/docs/onboarding/clone-voice.png +0 -0
- package/docs/onboarding/getting-started.png +0 -0
- package/docs/onboarding/new-video.png +0 -0
- package/docs/onboarding/providers.png +0 -0
- package/docs/onboarding/publish.png +0 -0
- package/docs/onboarding/references.png +0 -0
- package/docs/onboarding.md +212 -0
- package/omnivideo.config.example.json +437 -0
- package/package.json +40 -4
- package/reference/captions.md +156 -0
- package/reference/carousel-contract.md +91 -0
- package/reference/cloud.md +96 -0
- package/reference/learnings.md +792 -0
- package/reference/omnichat.md +305 -0
- package/reference/premiere-mcp.md +91 -0
- package/reference/publish.md +305 -0
- package/reference/scenes-contract.md +109 -0
- package/reference/studio.md +666 -0
- package/reference/voicestudio.md +81 -0
- package/reference/writing-for-voice.md +67 -0
- package/requirements.txt +3 -0
- package/studio/api/app.ts +87 -0
- package/studio/api/lib/errors.ts +33 -0
- package/studio/api/lib/files.ts +41 -0
- package/studio/api/lib/hono.ts +9 -0
- package/studio/api/lib/result.ts +14 -0
- package/studio/api/lib/validate.ts +31 -0
- package/studio/api/lib/video.ts +10 -0
- package/studio/api/middleware/json-charset.ts +11 -0
- package/studio/api/middleware/local-only.ts +32 -0
- package/studio/api/middleware/no-timeout.ts +10 -0
- package/studio/api/routes/admin.ts +18 -0
- package/studio/api/routes/agent-cascade.ts +100 -0
- package/studio/api/routes/agent-history.ts +23 -0
- package/studio/api/routes/agent.ts +162 -0
- package/studio/api/routes/analytics.ts +31 -0
- package/studio/api/routes/approval.ts +60 -0
- package/studio/api/routes/archive.ts +12 -0
- package/studio/api/routes/captions.ts +28 -0
- package/studio/api/routes/carousel.ts +38 -0
- package/studio/api/routes/clone.ts +27 -0
- package/studio/api/routes/create.ts +19 -0
- package/studio/api/routes/events.ts +11 -0
- package/studio/api/routes/export.ts +14 -0
- package/studio/api/routes/media.ts +30 -0
- package/studio/api/routes/omnichat.ts +113 -0
- package/studio/api/routes/onboarding.ts +14 -0
- package/studio/api/routes/publish.ts +40 -0
- package/studio/api/routes/queue.ts +14 -0
- package/studio/api/routes/references.ts +40 -0
- package/studio/api/routes/runner.ts +17 -0
- package/studio/api/routes/schedule.ts +47 -0
- package/studio/api/routes/services.ts +11 -0
- package/studio/api/routes/sessions.ts +27 -0
- package/studio/api/routes/settings.ts +102 -0
- package/studio/api/routes/sound.ts +45 -0
- package/studio/api/routes/thumbnail.ts +18 -0
- package/studio/api/routes/usage.ts +13 -0
- package/studio/api/routes/videos.ts +41 -0
- package/studio/api/routes/voices.ts +71 -0
- package/studio/api/services/admin.ts +113 -0
- package/studio/api/services/agent-cascade.ts +975 -0
- package/studio/api/services/agent-claude.ts +47 -0
- package/studio/api/services/agent-history.ts +103 -0
- package/studio/api/services/agent-persona.ts +77 -0
- package/studio/api/services/agent-tools.ts +2237 -0
- package/studio/api/services/agent-ui-tools.ts +259 -0
- package/studio/api/services/agent-wait.ts +32 -0
- package/studio/api/services/agent.ts +553 -0
- package/studio/api/services/analytics.ts +92 -0
- package/studio/api/services/approval.ts +134 -0
- package/studio/api/services/archive.ts +38 -0
- package/studio/api/services/captions.ts +268 -0
- package/studio/api/services/carousel.ts +206 -0
- package/studio/api/services/classify.ts +196 -0
- package/studio/api/services/clone-phrases.ts +75 -0
- package/studio/api/services/clone.ts +722 -0
- package/studio/api/services/create.ts +421 -0
- package/studio/api/services/events.ts +215 -0
- package/studio/api/services/media.ts +32 -0
- package/studio/api/services/omnichat.ts +624 -0
- package/studio/api/services/onboarding.ts +77 -0
- package/studio/api/services/publish.ts +287 -0
- package/studio/api/services/queue.ts +150 -0
- package/studio/api/services/references.ts +269 -0
- package/studio/api/services/root.ts +60 -0
- package/studio/api/services/services.ts +114 -0
- package/studio/api/services/sessions.ts +1007 -0
- package/studio/api/services/settings.ts +983 -0
- package/studio/api/services/sound.ts +311 -0
- package/studio/api/services/thumbnail.ts +133 -0
- package/studio/api/services/usage.ts +27 -0
- package/studio/api/services/videos.ts +418 -0
- package/studio/api/services/voice-pause.ts +98 -0
- package/studio/api/services/voices.ts +324 -0
- package/studio/runner/runner.ts +244 -0
- package/studio/shared/omni-actions.ts +68 -0
- package/studio/shared/omni-admin.ts +52 -0
- package/studio/shared/omni-farewell.ts +48 -0
- package/studio/shared/omni-hotword.ts +20 -0
- package/studio/shared/omni-screens.ts +228 -0
- package/studio/shared/schemas/admin.ts +15 -0
- package/studio/shared/schemas/agent-cascade.ts +58 -0
- package/studio/shared/schemas/agent-history.ts +73 -0
- package/studio/shared/schemas/agent.ts +50 -0
- package/studio/shared/schemas/analytics.ts +6 -0
- package/studio/shared/schemas/approval.ts +22 -0
- package/studio/shared/schemas/captions.ts +19 -0
- package/studio/shared/schemas/carousel.ts +26 -0
- package/studio/shared/schemas/clone.ts +40 -0
- package/studio/shared/schemas/common.ts +16 -0
- package/studio/shared/schemas/omnichat.ts +54 -0
- package/studio/shared/schemas/onboarding.ts +5 -0
- package/studio/shared/schemas/publish.ts +45 -0
- package/studio/shared/schemas/references.ts +20 -0
- package/studio/shared/schemas/settings.ts +82 -0
- package/studio/shared/schemas/sound.ts +22 -0
- package/studio/shared/schemas/videos.ts +38 -0
- package/studio/shared/schemas/voices.ts +23 -0
- package/studio/shared/types/api.ts +1290 -0
- package/studio/studio.ts +148 -0
- package/studio/tsconfig.json +15 -0
- package/studio/web/assets/logos/cartesia.svg +4 -0
- package/studio/web/assets/logos/elevenlabs.svg +12 -0
- package/studio/web/assets/logos/fish.svg +24 -0
- package/studio/web/assets/logos/omni-black.png +0 -0
- package/studio/web/assets/logos/omni-white.png +0 -0
- package/studio/web/assets/logos/voicestudio.svg +21 -0
- package/studio/web/components/AgentMark.tsx +7 -0
- package/studio/web/components/AgentWalking.tsx +11 -0
- package/studio/web/components/ApiKeyField.tsx +104 -0
- package/studio/web/components/AppShell.tsx +64 -0
- package/studio/web/components/ApprovalNotice.tsx +65 -0
- package/studio/web/components/AvatarGroupIcon.tsx +22 -0
- package/studio/web/components/AvatarNameField.tsx +86 -0
- package/studio/web/components/AvatarPicker.tsx +84 -0
- package/studio/web/components/BrandLogo.tsx +16 -0
- package/studio/web/components/CartesiaPlanSetting.tsx +90 -0
- package/studio/web/components/ClaudeWalking.tsx +32 -0
- package/studio/web/components/CloneVoiceDialog/GuidedScript.tsx +39 -0
- package/studio/web/components/CloneVoiceDialog/LanguageBar.tsx +37 -0
- package/studio/web/components/CloneVoiceDialog/LevelMeter.tsx +31 -0
- package/studio/web/components/CloneVoiceDialog/ProviderChoice.tsx +81 -0
- package/studio/web/components/CloneVoiceDialog/QualityPanel.tsx +62 -0
- package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +50 -0
- package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +79 -0
- package/studio/web/components/CloneVoiceDialog/RedoSummary.tsx +54 -0
- package/studio/web/components/CloneVoiceDialog/SaveStep.tsx +91 -0
- package/studio/web/components/CloneVoiceDialog/SourceBar.tsx +30 -0
- package/studio/web/components/CloneVoiceDialog/index.tsx +141 -0
- package/studio/web/components/CloneVoiceDialog/types.ts +28 -0
- package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +528 -0
- package/studio/web/components/ConnectionSignals.tsx +51 -0
- package/studio/web/components/EditVoiceDialog/EditVoiceFooter.tsx +72 -0
- package/studio/web/components/EditVoiceDialog/ProviderCards.tsx +81 -0
- package/studio/web/components/EditVoiceDialog/index.tsx +256 -0
- package/studio/web/components/ErrorBanner.tsx +115 -0
- package/studio/web/components/FinishedTask.tsx +41 -0
- package/studio/web/components/FishModelSetting.tsx +69 -0
- package/studio/web/components/FishPriceSetting.tsx +44 -0
- package/studio/web/components/HumanBanner.tsx +64 -0
- package/studio/web/components/Lightbox.tsx +41 -0
- package/studio/web/components/LogStream.tsx +42 -0
- package/studio/web/components/Markdown.tsx +21 -0
- package/studio/web/components/MissingDmsPanel/BatchSend.tsx +137 -0
- package/studio/web/components/MissingDmsPanel/CommentCard.tsx +168 -0
- package/studio/web/components/MissingDmsPanel/CommentFeed.tsx +45 -0
- package/studio/web/components/MissingDmsPanel/DmBubble.tsx +38 -0
- package/studio/web/components/MissingDmsPanel/ReplyComposer.tsx +63 -0
- package/studio/web/components/MissingDmsPanel/index.tsx +98 -0
- package/studio/web/components/NotFound.tsx +48 -0
- package/studio/web/components/Omni/OmniAvatarPicker.tsx +92 -0
- package/studio/web/components/Omni/OmniCards.tsx +202 -0
- package/studio/web/components/Omni/OmniGlow.tsx +183 -0
- package/studio/web/components/Omni/OmniHistory.tsx +119 -0
- package/studio/web/components/Omni/OmniHotword.tsx +40 -0
- package/studio/web/components/Omni/OmniMicSelect.tsx +92 -0
- package/studio/web/components/Omni/OmniVoiceSample.tsx +81 -0
- package/studio/web/components/Omni/OmniWidget.tsx +743 -0
- package/studio/web/components/Omni/PendingActions.tsx +200 -0
- package/studio/web/components/Omni/Transcript.tsx +127 -0
- package/studio/web/components/OmnichatAccountStatus.tsx +87 -0
- package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +150 -0
- package/studio/web/components/OmnichatRulePanel/RuleDryRun.tsx +15 -0
- package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +42 -0
- package/studio/web/components/OmnichatRulePanel/RuleSkeleton.tsx +18 -0
- package/studio/web/components/OmnichatRulePanel/RuleStatus.tsx +55 -0
- package/studio/web/components/OmnichatRulePanel/index.tsx +115 -0
- package/studio/web/components/OmnichatRulePanel/rule-steps.ts +67 -0
- package/studio/web/components/OmnichatRulePanel/steps/MessageStep.tsx +91 -0
- package/studio/web/components/OmnichatRulePanel/steps/RepliesStep.tsx +201 -0
- package/studio/web/components/OmnichatRulePanel/steps/TriggerStep.tsx +76 -0
- package/studio/web/components/OmnichatRulePanel/steps/types.ts +11 -0
- package/studio/web/components/Palette.tsx +17 -0
- package/studio/web/components/PlatformCard.tsx +109 -0
- package/studio/web/components/PlatformIcon.tsx +19 -0
- package/studio/web/components/ProviderCard.tsx +80 -0
- package/studio/web/components/ProviderDetail.tsx +367 -0
- package/studio/web/components/ProviderLogo.tsx +20 -0
- package/studio/web/components/QueuePanel.tsx +77 -0
- package/studio/web/components/ReferenceActions.tsx +89 -0
- package/studio/web/components/ReferencePicker.tsx +136 -0
- package/studio/web/components/ResultMessage.tsx +7 -0
- package/studio/web/components/RuleForm.tsx +138 -0
- package/studio/web/components/RuleLog.tsx +151 -0
- package/studio/web/components/RunningTask.tsx +23 -0
- package/studio/web/components/SampleList.tsx +55 -0
- package/studio/web/components/SessionPanel.tsx +352 -0
- package/studio/web/components/SettingRow.tsx +14 -0
- package/studio/web/components/SettingsCategorySkeleton.tsx +25 -0
- package/studio/web/components/SidePanel.tsx +77 -0
- package/studio/web/components/Sidebar.tsx +289 -0
- package/studio/web/components/StageStepper.tsx +87 -0
- package/studio/web/components/StudioOffline.tsx +80 -0
- package/studio/web/components/TaskName.tsx +17 -0
- package/studio/web/components/TopBanners.tsx +30 -0
- package/studio/web/components/VideoOmnichatPanel.tsx +287 -0
- package/studio/web/components/VoiceAvatar.tsx +33 -0
- package/studio/web/components/VoiceCarousel.tsx +196 -0
- package/studio/web/components/VoiceCategory.tsx +9 -0
- package/studio/web/components/VoiceModal.tsx +39 -0
- package/studio/web/components/VoiceProfileCard.tsx +75 -0
- package/studio/web/components/VoiceProfilePicker.tsx +107 -0
- package/studio/web/components/VoiceProfilesSettings.tsx +150 -0
- package/studio/web/components/VoiceProviderCard.tsx +118 -0
- package/studio/web/components/VoiceProvidersCard.tsx +26 -0
- package/studio/web/components/WaitList.tsx +21 -0
- package/studio/web/components/icons.tsx +247 -0
- package/studio/web/components/ui/accordion.tsx +47 -0
- package/studio/web/components/ui/badge.tsx +18 -0
- package/studio/web/components/ui/calendar.tsx +43 -0
- package/studio/web/components/ui/chart.tsx +229 -0
- package/studio/web/components/ui/checkbox.tsx +26 -0
- package/studio/web/components/ui/cx.ts +2 -0
- package/studio/web/components/ui/date-time-picker.tsx +84 -0
- package/studio/web/components/ui/panel.tsx +23 -0
- package/studio/web/components/ui/picker.tsx +58 -0
- package/studio/web/components/ui/popover.tsx +24 -0
- package/studio/web/components/ui/select.tsx +89 -0
- package/studio/web/components/ui/skeleton.tsx +10 -0
- package/studio/web/components/ui/slider.tsx +68 -0
- package/studio/web/components/ui/switch.tsx +13 -0
- package/studio/web/components/ui/tooltip.test.tsx +30 -0
- package/studio/web/components/ui/tooltip.tsx +59 -0
- package/studio/web/hooks/useAccounts.ts +27 -0
- package/studio/web/hooks/useAdmin.ts +12 -0
- package/studio/web/hooks/useAnalytics.ts +21 -0
- package/studio/web/hooks/useAvatarAudioReactive.ts +87 -0
- package/studio/web/hooks/useBrowserConnect.ts +48 -0
- package/studio/web/hooks/useClonePlan.ts +14 -0
- package/studio/web/hooks/useConnected.ts +7 -0
- package/studio/web/hooks/useLiveQuery.ts +29 -0
- package/studio/web/hooks/useMenuCollapsed.ts +32 -0
- package/studio/web/hooks/useMicDevices.ts +53 -0
- package/studio/web/hooks/useMissingDms.ts +211 -0
- package/studio/web/hooks/useMissingSetupSteps.ts +26 -0
- package/studio/web/hooks/useNow.ts +11 -0
- package/studio/web/hooks/useOmniAgent.ts +278 -0
- package/studio/web/hooks/useOmniClaude.ts +124 -0
- package/studio/web/hooks/useOmniDock.ts +142 -0
- package/studio/web/hooks/useOmniGreeting.ts +60 -0
- package/studio/web/hooks/useOmniHotword.ts +128 -0
- package/studio/web/hooks/useOmnichat.ts +26 -0
- package/studio/web/hooks/useOnboarding.ts +27 -0
- package/studio/web/hooks/usePost.ts +23 -0
- package/studio/web/hooks/useQueue.ts +10 -0
- package/studio/web/hooks/useReferences.ts +20 -0
- package/studio/web/hooks/useRuleEditor.ts +101 -0
- package/studio/web/hooks/useRunner.ts +24 -0
- package/studio/web/hooks/useSaveAction.ts +23 -0
- package/studio/web/hooks/useSchedule.ts +11 -0
- package/studio/web/hooks/useServices.ts +10 -0
- package/studio/web/hooks/useSessions.ts +15 -0
- package/studio/web/hooks/useSettings.ts +11 -0
- package/studio/web/hooks/useSettingsSection.ts +30 -0
- package/studio/web/hooks/useSidePanel.ts +23 -0
- package/studio/web/hooks/useSound.ts +30 -0
- package/studio/web/hooks/useUsage.ts +11 -0
- package/studio/web/hooks/useVideoVoice.ts +36 -0
- package/studio/web/hooks/useVideos.ts +21 -0
- package/studio/web/hooks/useVoiceProfilePreview.ts +52 -0
- package/studio/web/hooks/useVoiceProfiles.ts +7 -0
- package/studio/web/hooks/useVoiceRunning.ts +10 -0
- package/studio/web/hooks/useVoiceStudioClosed.ts +8 -0
- package/studio/web/index.html +19 -0
- package/studio/web/lib/agent.ts +7 -0
- package/studio/web/lib/alerts.ts +210 -0
- package/studio/web/lib/analytics-metrics.ts +351 -0
- package/studio/web/lib/api.ts +70 -0
- package/studio/web/lib/audio.ts +39 -0
- package/studio/web/lib/avatars.ts +41 -0
- package/studio/web/lib/calendar.ts +218 -0
- package/studio/web/lib/caption-position.ts +249 -0
- package/studio/web/lib/carousel.ts +24 -0
- package/studio/web/lib/chime.ts +42 -0
- package/studio/web/lib/clone-voice.ts +55 -0
- package/studio/web/lib/export.ts +11 -0
- package/studio/web/lib/files.ts +8 -0
- package/studio/web/lib/format.ts +93 -0
- package/studio/web/lib/media.ts +29 -0
- package/studio/web/lib/mic-device.ts +72 -0
- package/studio/web/lib/missing-dms.ts +70 -0
- package/studio/web/lib/omni-cards.ts +42 -0
- package/studio/web/lib/omni-cascade.ts +1067 -0
- package/studio/web/lib/omni-greeting-audio.ts +24 -0
- package/studio/web/lib/omni-hotword.ts +231 -0
- package/studio/web/lib/omni-panel.ts +67 -0
- package/studio/web/lib/omni-realtime.ts +811 -0
- package/studio/web/lib/omni-ui.ts +402 -0
- package/studio/web/lib/omni-wait.ts +174 -0
- package/studio/web/lib/omnichat-api.ts +44 -0
- package/studio/web/lib/orb.ts +270 -0
- package/studio/web/lib/pcm-recorder.ts +115 -0
- package/studio/web/lib/platforms.ts +17 -0
- package/studio/web/lib/providers.ts +27 -0
- package/studio/web/lib/publish-draft.ts +52 -0
- package/studio/web/lib/publish-settings.ts +81 -0
- package/studio/web/lib/query-client.ts +122 -0
- package/studio/web/lib/references.ts +13 -0
- package/studio/web/lib/services.ts +40 -0
- package/studio/web/lib/sessions.ts +103 -0
- package/studio/web/lib/settings-categories.ts +58 -0
- package/studio/web/lib/sound-audio.ts +33 -0
- package/studio/web/lib/sound-timeline.ts +291 -0
- package/studio/web/lib/sse.ts +86 -0
- package/studio/web/lib/stage-history.ts +141 -0
- package/studio/web/lib/stages.ts +344 -0
- package/studio/web/lib/studio-offline.ts +60 -0
- package/studio/web/lib/upload.ts +24 -0
- package/studio/web/lib/video-tabs.ts +118 -0
- package/studio/web/lib/voice-profile-draft.ts +34 -0
- package/studio/web/lib/voice-profiles.ts +3 -0
- package/studio/web/main.tsx +14 -0
- package/studio/web/pages/AdminPage/index.tsx +123 -0
- package/studio/web/pages/AgentPage/components/Measurements.tsx +102 -0
- package/studio/web/pages/AgentPage/index.tsx +28 -0
- package/studio/web/pages/AnalyticsPage/components/About.tsx +52 -0
- package/studio/web/pages/AnalyticsPage/components/ChannelCards.tsx +106 -0
- package/studio/web/pages/AnalyticsPage/components/ChannelView.tsx +137 -0
- package/studio/web/pages/AnalyticsPage/components/CollectionStatus.tsx +46 -0
- package/studio/web/pages/AnalyticsPage/components/Comparison.tsx +68 -0
- package/studio/web/pages/AnalyticsPage/components/Diagnosis.tsx +50 -0
- package/studio/web/pages/AnalyticsPage/components/Highlights.tsx +49 -0
- package/studio/web/pages/AnalyticsPage/components/Hint.tsx +11 -0
- package/studio/web/pages/AnalyticsPage/components/HookVsBody.tsx +100 -0
- package/studio/web/pages/AnalyticsPage/components/HourlyWave.tsx +133 -0
- package/studio/web/pages/AnalyticsPage/components/Insights.tsx +70 -0
- package/studio/web/pages/AnalyticsPage/components/MetricCard.tsx +46 -0
- package/studio/web/pages/AnalyticsPage/components/PlanSection.tsx +40 -0
- package/studio/web/pages/AnalyticsPage/components/ProgressionChart.tsx +155 -0
- package/studio/web/pages/AnalyticsPage/components/ReelBarsChart.tsx +154 -0
- package/studio/web/pages/AnalyticsPage/components/ReelThumb.tsx +19 -0
- package/studio/web/pages/AnalyticsPage/components/ReelView.tsx +290 -0
- package/studio/web/pages/AnalyticsPage/components/ReelsTable.tsx +269 -0
- package/studio/web/pages/AnalyticsPage/components/RequestWithFocus.tsx +38 -0
- package/studio/web/pages/AnalyticsPage/components/SegmentedControl.tsx +35 -0
- package/studio/web/pages/AnalyticsPage/components/VsMedian.tsx +31 -0
- package/studio/web/pages/AnalyticsPage/index.tsx +17 -0
- package/studio/web/pages/GettingStartedPage/index.tsx +174 -0
- package/studio/web/pages/NewVideoPage/components/DeliverablesPicker.tsx +62 -0
- package/studio/web/pages/NewVideoPage/components/FormatPicker.tsx +33 -0
- package/studio/web/pages/NewVideoPage/index.tsx +133 -0
- package/studio/web/pages/NotFoundPage/index.tsx +9 -0
- package/studio/web/pages/OmnichatPage/components/NewFromLink.tsx +59 -0
- package/studio/web/pages/OmnichatPage/components/RuleListItem.tsx +64 -0
- package/studio/web/pages/OmnichatPage/components/RulePreview.tsx +14 -0
- package/studio/web/pages/OmnichatPage/components/RuleView.tsx +88 -0
- package/studio/web/pages/OmnichatPage/index.tsx +153 -0
- package/studio/web/pages/QueuePage/index.tsx +22 -0
- package/studio/web/pages/ReferencePage/components/ReferenceTexts.tsx +131 -0
- package/studio/web/pages/ReferencePage/index.tsx +216 -0
- package/studio/web/pages/ReferencesPage/components/NewReferenceForm.tsx +150 -0
- package/studio/web/pages/ReferencesPage/components/ReferenceCard.tsx +42 -0
- package/studio/web/pages/ReferencesPage/components/ReferenceGridSkeleton.tsx +31 -0
- package/studio/web/pages/ReferencesPage/index.tsx +67 -0
- package/studio/web/pages/SchedulePage/components/CalendarSkeleton.tsx +18 -0
- package/studio/web/pages/SchedulePage/components/DayView.tsx +177 -0
- package/studio/web/pages/SchedulePage/components/HourPicker.tsx +103 -0
- package/studio/web/pages/SchedulePage/components/MonthDayCell.tsx +111 -0
- package/studio/web/pages/SchedulePage/components/MonthView.tsx +47 -0
- package/studio/web/pages/SchedulePage/components/PlatformIcons.tsx +12 -0
- package/studio/web/pages/SchedulePage/components/PostThumb.tsx +15 -0
- package/studio/web/pages/SchedulePage/components/WeekColumn.tsx +75 -0
- package/studio/web/pages/SchedulePage/components/WeekView.tsx +35 -0
- package/studio/web/pages/SchedulePage/index.tsx +285 -0
- package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +263 -0
- package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +20 -0
- package/studio/web/pages/SettingsPage/components/ModelPicker.tsx +49 -0
- package/studio/web/pages/SettingsPage/components/ModelsSettings.tsx +138 -0
- package/studio/web/pages/SettingsPage/components/PlatformsCategory.tsx +23 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/AccountStatus.tsx +29 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/AutoPublishPanel.tsx +90 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/BrowserConnect.tsx +62 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/CheckField.tsx +19 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/InstagramPanel.tsx +113 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +146 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SaveBar.tsx +33 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +132 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCard.tsx +47 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCardGroup.tsx +19 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/TextPanel.tsx +55 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/TiktokPanel.tsx +123 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/YoutubePanel.tsx +100 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/badges.tsx +15 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/index.tsx +40 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/types.ts +10 -0
- package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +389 -0
- package/studio/web/pages/SettingsPage/components/VoiceProvidersCategory.tsx +97 -0
- package/studio/web/pages/SettingsPage/index.tsx +85 -0
- package/studio/web/pages/VideoPage/components/AudioPlayer.tsx +113 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/AxisField.tsx +50 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionList.tsx +56 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionListItem.tsx +92 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionMargins.tsx +57 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionStyleFields.tsx +70 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionTextForm.tsx +87 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/ColorField.tsx +37 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/MarginField.tsx +59 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/NumberField.tsx +57 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/PositionControls.tsx +140 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/captionEditor.ts +215 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/useCaptionPosition.ts +154 -0
- package/studio/web/pages/VideoPage/components/CarouselChangeRequest.tsx +72 -0
- package/studio/web/pages/VideoPage/components/CarouselCompositionStage.tsx +80 -0
- package/studio/web/pages/VideoPage/components/CarouselCreate.tsx +63 -0
- package/studio/web/pages/VideoPage/components/CarouselMissing.tsx +19 -0
- package/studio/web/pages/VideoPage/components/CarouselPublishStage.tsx +173 -0
- package/studio/web/pages/VideoPage/components/CarouselScriptStage.tsx +82 -0
- package/studio/web/pages/VideoPage/components/CarouselSlideCard.tsx +49 -0
- package/studio/web/pages/VideoPage/components/CarouselViewer.tsx +55 -0
- package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +53 -0
- package/studio/web/pages/VideoPage/components/CompositionStage.tsx +78 -0
- package/studio/web/pages/VideoPage/components/CoverPicker.tsx +32 -0
- package/studio/web/pages/VideoPage/components/EditorStage.tsx +53 -0
- package/studio/web/pages/VideoPage/components/LineTakes.tsx +160 -0
- package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +124 -0
- package/studio/web/pages/VideoPage/components/LineVoicePicker.tsx +37 -0
- package/studio/web/pages/VideoPage/components/MediaImage.tsx +25 -0
- package/studio/web/pages/VideoPage/components/MotionScriptStage.tsx +39 -0
- package/studio/web/pages/VideoPage/components/NarrationStage.tsx +70 -0
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +387 -0
- package/studio/web/pages/VideoPage/components/PublishStage.tsx +76 -0
- package/studio/web/pages/VideoPage/components/PublishText.tsx +149 -0
- package/studio/web/pages/VideoPage/components/ScenesApprovalPanel.tsx +278 -0
- package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +72 -0
- package/studio/web/pages/VideoPage/components/ScriptApprovalPanel.tsx +189 -0
- package/studio/web/pages/VideoPage/components/ScriptStage.tsx +17 -0
- package/studio/web/pages/VideoPage/components/SequencePlayer.tsx +146 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +117 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/AreaSelection.tsx +74 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedPanel.tsx +94 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +45 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionOverlay.tsx +44 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +32 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionsPanel.tsx +22 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/ClipBlock.tsx +111 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/ClipPanel.tsx +76 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CutsPanel.tsx +44 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EditorTabBar.tsx +73 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +46 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectItem.tsx +89 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectPanel.tsx +232 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectsRow.tsx +21 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/Lane.tsx +39 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/MarkerRow.tsx +40 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/MuteButton.tsx +36 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/NumberInput.tsx +37 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/PlanWarnings.tsx +19 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/PlayerResizeHandle.tsx +50 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/PreviewControls.tsx +51 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SceneRow.tsx +54 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SettingsPanel.tsx +65 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +99 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SoundPreview.tsx +82 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SpeedPanel.tsx +128 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineDock.tsx +56 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +26 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineSkeleton.tsx +21 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineToolbar.tsx +160 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineTracks.tsx +121 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TrackSidebar.tsx +30 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/VoiceRow.tsx +53 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/Waveform.tsx +97 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/clipActions.ts +50 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/effectActions.ts +161 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/index.tsx +161 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/timelineGeometry.ts +45 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/types.ts +43 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useAutosave.ts +107 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useEditorShortcuts.ts +168 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useFitHeight.ts +40 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useFollowPlayhead.ts +21 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useListenMix.ts +43 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayback.ts +231 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayerWidth.ts +27 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundData.ts +124 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +511 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/CompareVersions.tsx +111 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/VersionCard.tsx +81 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/VersionMedia.tsx +24 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/VersionText.tsx +15 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/index.tsx +109 -0
- package/studio/web/pages/VideoPage/components/StageProcess.tsx +126 -0
- package/studio/web/pages/VideoPage/components/StageResult.tsx +69 -0
- package/studio/web/pages/VideoPage/components/SwapReference.tsx +92 -0
- package/studio/web/pages/VideoPage/components/ThumbnailPanel.tsx +252 -0
- package/studio/web/pages/VideoPage/components/TimingPanel.tsx +89 -0
- package/studio/web/pages/VideoPage/components/VideoPlayer.tsx +18 -0
- package/studio/web/pages/VideoPage/components/VideoSkeleton.tsx +50 -0
- package/studio/web/pages/VideoPage/components/VideoVoiceInfo.tsx +33 -0
- package/studio/web/pages/VideoPage/components/VideoVoicePicker.tsx +165 -0
- package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +56 -0
- package/studio/web/pages/VideoPage/components/WithLogo.tsx +10 -0
- package/studio/web/pages/VideoPage/index.tsx +295 -0
- package/studio/web/pages/VideosPage/components/PublishedOn.tsx +85 -0
- package/studio/web/pages/VideosPage/components/ServicesPanel.tsx +81 -0
- package/studio/web/pages/VideosPage/components/SessionsPanel.tsx +62 -0
- package/studio/web/pages/VideosPage/components/VideoListSkeleton.tsx +25 -0
- package/studio/web/pages/VideosPage/components/VideoRow.tsx +122 -0
- package/studio/web/pages/VideosPage/index.tsx +134 -0
- package/studio/web/pages/VoicePage/index.tsx +28 -0
- package/studio/web/router.tsx +201 -0
- package/studio/web/styles/base/elements.css +55 -0
- package/studio/web/styles/base/utilities.css +25 -0
- package/studio/web/styles/components/badge.css +33 -0
- package/studio/web/styles/components/claude-running.css +27 -0
- package/studio/web/styles/components/claude-session.css +212 -0
- package/studio/web/styles/components/claude-walking.css +92 -0
- package/studio/web/styles/components/clone-voice-save.css +136 -0
- package/studio/web/styles/components/clone-voice.css +379 -0
- package/studio/web/styles/components/controls.css +70 -0
- package/studio/web/styles/components/data-and-image.css +54 -0
- package/studio/web/styles/components/forms.css +132 -0
- package/studio/web/styles/components/human-banner.css +99 -0
- package/studio/web/styles/components/lightbox.css +50 -0
- package/studio/web/styles/components/markdown.css +68 -0
- package/studio/web/styles/components/modal.css +70 -0
- package/studio/web/styles/components/omni-cards.css +105 -0
- package/studio/web/styles/components/omni.css +859 -0
- package/studio/web/styles/components/omnichat.css +538 -0
- package/studio/web/styles/components/panel.css +115 -0
- package/studio/web/styles/components/platform-card.css +140 -0
- package/studio/web/styles/components/provider-card.css +132 -0
- package/studio/web/styles/components/queue.css +185 -0
- package/studio/web/styles/components/reference-actions.css +133 -0
- package/studio/web/styles/components/reference-picker.css +146 -0
- package/studio/web/styles/components/results-drawer.css +224 -0
- package/studio/web/styles/components/segmented.css +115 -0
- package/studio/web/styles/components/side-panel-tab.css +23 -0
- package/studio/web/styles/components/status-signals.css +66 -0
- package/studio/web/styles/components/stepper.css +208 -0
- package/studio/web/styles/components/table.css +19 -0
- package/studio/web/styles/components/tabs.css +24 -0
- package/studio/web/styles/components/ui/accordion.css +71 -0
- package/studio/web/styles/components/ui/chart.css +162 -0
- package/studio/web/styles/components/ui/checkbox.css +37 -0
- package/studio/web/styles/components/ui/date-picker.css +153 -0
- package/studio/web/styles/components/ui/select.css +110 -0
- package/studio/web/styles/components/ui/skeleton.css +25 -0
- package/studio/web/styles/components/ui/slider.css +81 -0
- package/studio/web/styles/components/ui/switch.css +41 -0
- package/studio/web/styles/components/ui/tooltip.css +24 -0
- package/studio/web/styles/components/voice-carousel.css +255 -0
- package/studio/web/styles/components/voice-editor.css +179 -0
- package/studio/web/styles/components/voice-grid.css +56 -0
- package/studio/web/styles/components/voice-profiles.css +192 -0
- package/studio/web/styles/design-system.md +136 -0
- package/studio/web/styles/index.css +116 -0
- package/studio/web/styles/layout/page.css +41 -0
- package/studio/web/styles/layout/sidebar-groups.css +39 -0
- package/studio/web/styles/layout/sidebar.css +111 -0
- package/studio/web/styles/pages/agent.css +26 -0
- package/studio/web/styles/pages/analytics/dashboard.css +614 -0
- package/studio/web/styles/pages/analytics/insights.css +48 -0
- package/studio/web/styles/pages/getting-started.css +166 -0
- package/studio/web/styles/pages/new-video/form.css +32 -0
- package/studio/web/styles/pages/not-found.css +59 -0
- package/studio/web/styles/pages/references.css +204 -0
- package/studio/web/styles/pages/schedule/calendar.css +541 -0
- package/studio/web/styles/pages/schedule/chip-states.css +60 -0
- package/studio/web/styles/pages/settings/models.css +72 -0
- package/studio/web/styles/pages/settings/shell.css +361 -0
- package/studio/web/styles/pages/settings/usage.css +63 -0
- package/studio/web/styles/pages/studio-offline.css +105 -0
- package/studio/web/styles/pages/video/caption-controls.css +194 -0
- package/studio/web/styles/pages/video/captions-grid.css +70 -0
- package/studio/web/styles/pages/video/captions.css +79 -0
- package/studio/web/styles/pages/video/editor-chat.css +103 -0
- package/studio/web/styles/pages/video/export.css +50 -0
- package/studio/web/styles/pages/video/frames.css +29 -0
- package/studio/web/styles/pages/video/fullscreen-stage.css +400 -0
- package/studio/web/styles/pages/video/instagram-carousel.css +309 -0
- package/studio/web/styles/pages/video/layout.css +285 -0
- package/studio/web/styles/pages/video/narration.css +138 -0
- package/studio/web/styles/pages/video/preview-overview.css +11 -0
- package/studio/web/styles/pages/video/processing-notice.css +34 -0
- package/studio/web/styles/pages/video/publish-when.css +76 -0
- package/studio/web/styles/pages/video/publish.css +97 -0
- package/studio/web/styles/pages/video/result.css +96 -0
- package/studio/web/styles/pages/video/scenes.css +167 -0
- package/studio/web/styles/pages/video/sound-timeline.css +672 -0
- package/studio/web/styles/pages/video/sound-toolbar.css +149 -0
- package/studio/web/styles/pages/video/take-player.css +100 -0
- package/studio/web/styles/pages/video/thumbnail.css +157 -0
- package/studio/web/styles/pages/video/timeline.css +117 -0
- package/studio/web/styles/pages/video/voice-picker.css +14 -0
- package/studio/web/styles/pages/video-list/active-row.css +27 -0
- package/studio/web/styles/pages/video-list/card-link.css +49 -0
- package/studio/web/styles/pages/video-list/list.css +86 -0
- package/studio/web/styles/theme/identity/details.css +204 -0
- package/studio/web/styles/theme/identity/shell.css +249 -0
- package/studio/web/styles/theme/identity/surfaces.css +183 -0
- package/studio/web/styles/theme/identity/titles-and-buttons.css +120 -0
- package/studio/web/styles/theme/no-borders/blocks.css +208 -0
- package/studio/web/styles/theme/no-borders/controls.css +256 -0
- package/studio/web/styles/tokens.css +245 -0
- package/studio/web/types/api.ts +2 -0
- package/studio/web/types/assets.d.ts +9 -0
- package/templates/brief.md +27 -0
- package/templates/captions.html +204 -0
- package/templates/carousel-composition.md +47 -0
- package/templates/carousel-script.md +60 -0
- package/templates/components/voiceavatar/README.md +87 -0
- package/templates/components/voiceavatar/demo.html +50 -0
- package/templates/components/voiceavatar/voiceavatar.js +415 -0
- package/templates/motion.md +80 -0
- package/templates/playbook.md +43 -0
- package/templates/reference.md +123 -0
- package/templates/scene.html +112 -0
- package/templates/scenes.example.json +96 -0
- package/templates/script.md +63 -0
- package/templates/slide.html +71 -0
- package/templates/wireframe.html +91 -0
- package/tools/analysis.mjs +300 -0
- package/tools/approval.mjs +442 -0
- package/tools/assemble.mjs +942 -0
- package/tools/build-motion.mjs +437 -0
- package/tools/captions.mjs +246 -0
- package/tools/carousel.mjs +558 -0
- package/tools/compose.mjs +307 -0
- package/tools/cuts.mjs +74 -0
- package/tools/editor.mjs +419 -0
- package/tools/epidemic-capture.js +54 -0
- package/tools/fanout.mjs +69 -0
- package/tools/history.mjs +70 -0
- package/tools/history.py +61 -0
- package/tools/job-queue.mjs +180 -0
- package/tools/job_queue.py +430 -0
- package/tools/lib/agents.mjs +412 -0
- package/tools/lib/channel.mjs +399 -0
- package/tools/lib/claude-settings.mjs +67 -0
- package/tools/lib/cli.mjs +9 -0
- package/tools/lib/fanout.mjs +234 -0
- package/tools/lib/http.mjs +94 -0
- package/tools/lib/index.js +14 -0
- package/tools/lib/mcp-server.mjs +69 -0
- package/tools/lib/metrics/metrics.mjs +204 -0
- package/tools/lib/narration.mjs +15 -0
- package/tools/lib/oauth-client.mjs +295 -0
- package/tools/lib/omnichat/config.mjs +139 -0
- package/tools/lib/omnichat/errors.mjs +42 -0
- package/tools/lib/omnichat/extract.mjs +82 -0
- package/tools/lib/omnichat/gate.mjs +151 -0
- package/tools/lib/omnichat/manual.mjs +166 -0
- package/tools/lib/omnichat/match.mjs +78 -0
- package/tools/lib/omnichat/pass.mjs +393 -0
- package/tools/lib/omnichat/record.mjs +208 -0
- package/tools/lib/omnichat/text.mjs +315 -0
- package/tools/lib/omnichat/webhook.mjs +166 -0
- package/tools/lib/pairing.mjs +99 -0
- package/tools/lib/paths.mjs +41 -0
- package/tools/lib/platforms/instagram.mjs +469 -0
- package/tools/lib/platforms/tiktok.mjs +343 -0
- package/tools/lib/platforms/youtube.mjs +228 -0
- package/tools/lib/publish/publish.mjs +244 -0
- package/tools/lib/schedule/schedule.mjs +191 -0
- package/tools/lib/service.mjs +134 -0
- package/tools/lib/skill-client.mjs +126 -0
- package/tools/lib/skills-release.mjs +196 -0
- package/tools/lib/testing.mjs +52 -0
- package/tools/lib/up.mjs +187 -0
- package/tools/metrics.mjs +415 -0
- package/tools/models.mjs +134 -0
- package/tools/narrate.mjs +257 -0
- package/tools/omni.mjs +298 -0
- package/tools/omni_paths.py +12 -0
- package/tools/omnichat.mjs +1577 -0
- package/tools/omnivideo.py +1122 -0
- package/tools/package.mjs +130 -0
- package/tools/platforms.mjs +236 -0
- package/tools/premiere.mjs +533 -0
- package/tools/publish.mjs +1496 -0
- package/tools/reference.mjs +1408 -0
- package/tools/render.mjs +426 -0
- package/tools/runner.mjs +156 -0
- package/tools/setup.mjs +466 -0
- package/tools/skills-release.mjs +93 -0
- package/tools/sound.mjs +1323 -0
- package/tools/soundtrack.mjs +217 -0
- package/tools/thumbnail.mjs +271 -0
- package/tools/timing.mjs +108 -0
- package/tools/usage.py +628 -0
- package/tools/voice-envelope.mjs +185 -0
- package/tools/voice.py +448 -0
- package/tools/voice_sample.py +75 -0
- package/tools/wireframe.mjs +230 -0
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
/* Omni in cascade: speech → text (xAI STT) → a fast LLM with Omni's tools (OpenRouter) → speech
|
|
2
|
+
(Fish Audio, the free model first). The cheap alternative to Grok Voice, which bills every second
|
|
3
|
+
the session is open: here only speech is billed (STT by the second of audio, the LLM by token,
|
|
4
|
+
Fish free under fair use, the paid model only when the free one fails).
|
|
5
|
+
|
|
6
|
+
Two ways to hear the user, picked in the widget:
|
|
7
|
+
- "stream": the browser streams the microphone to xAI's STT WebSocket (wss://api.x.ai/v1/stt, with
|
|
8
|
+
an ephemeral token minted here); Smart Turn says when the user finished. $0.20 per hour streamed.
|
|
9
|
+
- "chunks": the browser records each utterance (its own pause detection) and posts it to
|
|
10
|
+
/api/agent/stt (REST, $0.10 per hour of speech only).
|
|
11
|
+
Both post the text to runTurn, which streams NDJSON back: the reply's text, the tools called, the
|
|
12
|
+
staged actions and the audio (PCM16 mono 24 kHz), sentence by sentence, so the first sentence
|
|
13
|
+
plays while the LLM is still writing the rest. The tools are the same as Grok's (agent-tools.ts)
|
|
14
|
+
and run here, with the same guardrails: actions only stage a card for "Confirmar".
|
|
15
|
+
|
|
16
|
+
Config `agent.cascade` in omnivideo.config.json (all optional):
|
|
17
|
+
{ llm, llm_fallback, fish_model, fish_fallback, voice, speed, voices: { female, male },
|
|
18
|
+
stt: { smart_turn, smart_turn_timeout_ms, endpointing_ms }, chunk_pause_ms }. */
|
|
19
|
+
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { ROOT, readJson } from "./root";
|
|
23
|
+
import { readEnvKeys } from "./services";
|
|
24
|
+
import {
|
|
25
|
+
AGENT_DIR,
|
|
26
|
+
OMNI_OFF,
|
|
27
|
+
agentConfig,
|
|
28
|
+
engineMismatch,
|
|
29
|
+
logEvent,
|
|
30
|
+
logResolved,
|
|
31
|
+
sessionContext,
|
|
32
|
+
spentToday,
|
|
33
|
+
} from "./agent";
|
|
34
|
+
import { omniPersona } from "./agent-persona";
|
|
35
|
+
import {
|
|
36
|
+
cancelSessionActions,
|
|
37
|
+
isActionTool,
|
|
38
|
+
isLoggedTool,
|
|
39
|
+
noteUserTurn,
|
|
40
|
+
registerToolSession,
|
|
41
|
+
runTool,
|
|
42
|
+
TOOL_DEFS,
|
|
43
|
+
} from "./agent-tools";
|
|
44
|
+
import { checkUiCall, isUiTool, UI_TOOL_DEFS } from "./agent-ui-tools";
|
|
45
|
+
import { WAIT_TOOL_NAME } from "./agent-wait";
|
|
46
|
+
import { FAREWELL_REPLY, isFarewell } from "../../shared/omni-farewell";
|
|
47
|
+
import { describePath } from "../../shared/omni-screens";
|
|
48
|
+
import type { CascadeMode } from "../../shared/schemas/agent-cascade";
|
|
49
|
+
|
|
50
|
+
const XAI = "https://api.x.ai/v1";
|
|
51
|
+
const OPENROUTER = "https://openrouter.ai/api/v1/chat/completions";
|
|
52
|
+
const FISH = "https://api.fish.audio/v1/tts";
|
|
53
|
+
/* Fish paid models: $15 per million UTF-8 bytes of text */
|
|
54
|
+
const FISH_PAID_PER_BYTE = 15 / 1e6;
|
|
55
|
+
const RATE = 24000;
|
|
56
|
+
|
|
57
|
+
export type CascadeConfig = {
|
|
58
|
+
llm: string;
|
|
59
|
+
llm_fallback: string;
|
|
60
|
+
fish_model: string;
|
|
61
|
+
fish_fallback: string;
|
|
62
|
+
/* one Fish voice for Omni whatever agent.gender says; without it, voices[gender] */
|
|
63
|
+
voice: string | null;
|
|
64
|
+
/* Fish prosody speed (1 = normal) */
|
|
65
|
+
speed: number;
|
|
66
|
+
voices: { female: string; male: string };
|
|
67
|
+
stt: { smart_turn: number; smart_turn_timeout_ms: number; endpointing_ms: number };
|
|
68
|
+
chunk_pause_ms: number;
|
|
69
|
+
/* silence: Omni asks "Ainda tá aí?" at nudge_s of quiet and says goodbye and hangs up at close_s
|
|
70
|
+
(both counted from when the silence began, like the widget's 6 s / 10 s for Grok) */
|
|
71
|
+
idle: { nudge_s: number; close_s: number };
|
|
72
|
+
};
|
|
73
|
+
/* voices from Fish's public library, chosen for conversation in pt-BR and not imitating anyone:
|
|
74
|
+
"Mulher comum" and "Linn" */
|
|
75
|
+
const DEFAULTS: CascadeConfig = {
|
|
76
|
+
llm: "anthropic/claude-haiku-5.5",
|
|
77
|
+
llm_fallback: "google/gemini-3.1-flash-lite",
|
|
78
|
+
fish_model: "s2.1-pro-free",
|
|
79
|
+
fish_fallback: "s2.1-pro",
|
|
80
|
+
voice: null,
|
|
81
|
+
speed: 1,
|
|
82
|
+
voices: { female: "148da69cf74b4530b1c41d5c1e87c688", male: "c7d997eba056434d8ba3dd1b8dec560e" },
|
|
83
|
+
/* measured: 0.5 / 1200 / 300 took ~0.9 s from the end of speech to the text; this ends turns sooner */
|
|
84
|
+
stt: { smart_turn: 0.3, smart_turn_timeout_ms: 600, endpointing_ms: 200 },
|
|
85
|
+
chunk_pause_ms: 700,
|
|
86
|
+
idle: { nudge_s: 6, close_s: 10 },
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const str = (v: unknown, fallback: string) => (typeof v === "string" && v.trim() ? v.trim() : fallback);
|
|
90
|
+
const num = (v: unknown, fallback: number, min: number, max: number) =>
|
|
91
|
+
typeof v === "number" && Number.isFinite(v) && v >= min && v <= max ? v : fallback;
|
|
92
|
+
|
|
93
|
+
export function cascadeConfig(): CascadeConfig {
|
|
94
|
+
const c = readJson<any>(join(ROOT, "omnivideo.config.json"))?.agent?.cascade ?? {};
|
|
95
|
+
const d = DEFAULTS;
|
|
96
|
+
return {
|
|
97
|
+
llm: str(c.llm, d.llm),
|
|
98
|
+
llm_fallback: str(c.llm_fallback, d.llm_fallback),
|
|
99
|
+
fish_model: str(c.fish_model, d.fish_model),
|
|
100
|
+
fish_fallback: str(c.fish_fallback, d.fish_fallback),
|
|
101
|
+
voice: typeof c.voice === "string" && /^[0-9a-f]{32}$/.test(c.voice) ? c.voice : d.voice,
|
|
102
|
+
speed: num(c.speed, d.speed, 0.5, 2),
|
|
103
|
+
voices: { female: str(c.voices?.female, d.voices.female), male: str(c.voices?.male, d.voices.male) },
|
|
104
|
+
stt: {
|
|
105
|
+
smart_turn: num(c.stt?.smart_turn, d.stt.smart_turn, 0, 1),
|
|
106
|
+
smart_turn_timeout_ms: num(c.stt?.smart_turn_timeout_ms, d.stt.smart_turn_timeout_ms, 1, 5000),
|
|
107
|
+
endpointing_ms: num(c.stt?.endpointing_ms, d.stt.endpointing_ms, 0, 5000),
|
|
108
|
+
},
|
|
109
|
+
chunk_pause_ms: num(c.chunk_pause_ms, d.chunk_pause_ms, 300, 3000),
|
|
110
|
+
idle: {
|
|
111
|
+
nudge_s: num(c.idle?.nudge_s, d.idle.nudge_s, 2, 120),
|
|
112
|
+
close_s: num(c.idle?.close_s, d.idle.close_s, 3, 180),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function key(name: string): string {
|
|
118
|
+
const k = readEnvKeys()[name];
|
|
119
|
+
if (!k) throw new Error(`falta ${name} no .env`);
|
|
120
|
+
return k;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* --- session ------------------------------------------------------------------------------------ */
|
|
124
|
+
/* who Omni is, what she does and doesn't do (agent-persona.ts, shared with Grok), in the grammar of
|
|
125
|
+
the voice the admin picked (agent.gender) */
|
|
126
|
+
|
|
127
|
+
/* how the reply should sound: it becomes voice, so no markdown, lists or emoji */
|
|
128
|
+
const VOICE_RULES = `Sua resposta vira voz na hora:
|
|
129
|
+
- Responda como numa conversa: no máximo duas frases curtas, uns 35 palavras. Se houver mais a dizer, dê o principal e pergunte se a pessoa quer o resto.
|
|
130
|
+
- Nada de markdown, listas, emojis, links ou slugs.
|
|
131
|
+
- Escreva números como se fala ("uns três mil views", "quarenta por cento").
|
|
132
|
+
- Resposta que não precisa de ferramenta: comece com uma frase de duas a quatro palavras, terminada em ponto, que já vira voz enquanto você pensa no resto ("Claro, vamos lá.", "Hum, boa.", "Certo."). Varie, não repita a mesma da resposta anterior.
|
|
133
|
+
- Quando precisar de ferramenta (ver dados, abrir uma tela, preparar ou mudar algo), chame direto, sem escrever nada antes: o estúdio já diz ao usuário, na hora, que você vai checar, abrir ou preparar.
|
|
134
|
+
- Depois do resultado, responda direto o que foi pedido. Nada de "deixa eu ver", "vou checar", "só um minuto": o usuário já ouviu o aviso e você já checou.
|
|
135
|
+
- Mensagens que começam com [Tela] contam o que o usuário fez na tela (Confirmar, Cancelar): reaja em uma frase.
|
|
136
|
+
- [Tela atual: …] diz onde o usuário está: "esse vídeo", "essa cena" e "aqui" são o que está nessa tela.`;
|
|
137
|
+
|
|
138
|
+
type Msg = Record<string, unknown>;
|
|
139
|
+
type CascadeSession = {
|
|
140
|
+
id: string;
|
|
141
|
+
mode: CascadeMode;
|
|
142
|
+
started: number;
|
|
143
|
+
voice: string;
|
|
144
|
+
messages: Msg[];
|
|
145
|
+
/* a turn in flight is aborted when the next one starts (the user spoke over Omni) */
|
|
146
|
+
turn: AbortController | null;
|
|
147
|
+
/* the speculative turn in flight, until the page confirms or drops it */
|
|
148
|
+
spec: Speculation | null;
|
|
149
|
+
/* UI tool calls waiting for the browser's result, by call id */
|
|
150
|
+
ui: Map<string, (output: Record<string, unknown>) => void>;
|
|
151
|
+
/* the screen the last turn said the user was on */
|
|
152
|
+
screen: string | null;
|
|
153
|
+
/* the tool notices played so far: the next one picks another phrase */
|
|
154
|
+
acks: number;
|
|
155
|
+
/* UI tools that timed out in a row, by name: after UI_TIMEOUT_LIMIT the call is refused without waiting */
|
|
156
|
+
uiTimeouts: Map<string, { n: number; at: number }>;
|
|
157
|
+
};
|
|
158
|
+
const sessions = new Map<string, CascadeSession>();
|
|
159
|
+
|
|
160
|
+
export type CascadeStart = {
|
|
161
|
+
id: string;
|
|
162
|
+
mode: CascadeMode;
|
|
163
|
+
config: ReturnType<typeof agentConfig>;
|
|
164
|
+
chunk_pause_ms: number;
|
|
165
|
+
idle: CascadeConfig["idle"];
|
|
166
|
+
/* stream mode: where the browser opens the STT WebSocket, and the token for it */
|
|
167
|
+
stt: { url: string; token: string } | null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/* `history`: an earlier conversation the user continues (the Omni panel's Conversas): its lines go
|
|
171
|
+
back into `messages` as they were said, after the persona and today's context */
|
|
172
|
+
export async function startCascade(
|
|
173
|
+
mode: CascadeMode,
|
|
174
|
+
history: { role: "user" | "assistant"; text: string }[] = [],
|
|
175
|
+
): Promise<CascadeStart> {
|
|
176
|
+
const config = agentConfig();
|
|
177
|
+
if (!config.enabled) throw new Error(OMNI_OFF);
|
|
178
|
+
if (config.engine !== mode) throw new Error(engineMismatch(config.engine));
|
|
179
|
+
const spent = spentToday();
|
|
180
|
+
if (spent >= config.limits.daily_usd)
|
|
181
|
+
throw new Error(
|
|
182
|
+
`limite diário do Omni atingido: US$ ${spent.toFixed(2)} de US$ ${config.limits.daily_usd.toFixed(2)} (agent.limits.daily_usd)`,
|
|
183
|
+
);
|
|
184
|
+
key("OPENROUTER_API_KEY");
|
|
185
|
+
key("FISH_API_KEY");
|
|
186
|
+
const cc = cascadeConfig();
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
for (const [k, s] of sessions) if (now - s.started > 6 * 3600_000) sessions.delete(k);
|
|
189
|
+
const id = `${new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "")}-${crypto.randomUUID().slice(0, 6)}`;
|
|
190
|
+
const voice = cc.voice ?? cc.voices[config.gender];
|
|
191
|
+
const system = `${omniPersona(config.gender)}\n\n${VOICE_RULES}\n\n${await sessionContext(new Date())}`;
|
|
192
|
+
sessions.set(id, {
|
|
193
|
+
id,
|
|
194
|
+
mode,
|
|
195
|
+
started: now,
|
|
196
|
+
voice,
|
|
197
|
+
/* cache_control: the persona and the tools are the same every turn (Anthropic caches them) */
|
|
198
|
+
messages: [
|
|
199
|
+
{ role: "system", content: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }] },
|
|
200
|
+
...(history.length
|
|
201
|
+
? [
|
|
202
|
+
{
|
|
203
|
+
role: "system",
|
|
204
|
+
content:
|
|
205
|
+
"A conversa abaixo é uma conversa anterior que o usuário pediu para continuar. O estado do estúdio pode ter mudado desde então: confira com as ferramentas antes de afirmar algo.",
|
|
206
|
+
},
|
|
207
|
+
...history.map(h => ({ role: h.role, content: h.text })),
|
|
208
|
+
]
|
|
209
|
+
: []),
|
|
210
|
+
],
|
|
211
|
+
turn: null,
|
|
212
|
+
spec: null,
|
|
213
|
+
ui: new Map(),
|
|
214
|
+
uiTimeouts: new Map(),
|
|
215
|
+
screen: null,
|
|
216
|
+
acks: 0,
|
|
217
|
+
});
|
|
218
|
+
registerToolSession(id, { max_actions: config.limits.max_actions, max_tool_calls: config.limits.max_tool_calls });
|
|
219
|
+
logEvent({ session: id, type: "start", mode, voice, llm: cc.llm, tts: cc.fish_model });
|
|
220
|
+
/* the silence and notice clips, made once per voice and speed, one at a time (the free model is
|
|
221
|
+
rate limited); the first request also warms Fish up */
|
|
222
|
+
if (Object.keys(CLIPS).some(k => !existsSync(clipFile(voice, cc, k as ClipName)))) warmFish(voice, cc.fish_model);
|
|
223
|
+
void (async () => {
|
|
224
|
+
for (const k of Object.keys(CLIPS)) await cascadeClip(id, k).catch(() => {});
|
|
225
|
+
})();
|
|
226
|
+
return {
|
|
227
|
+
id,
|
|
228
|
+
mode,
|
|
229
|
+
config,
|
|
230
|
+
chunk_pause_ms: cc.chunk_pause_ms,
|
|
231
|
+
idle: cc.idle,
|
|
232
|
+
stt: mode === "stream" ? await sttSocket(cc) : null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* the first Fish request of a while pays the TLS handshake and a cold start (~3 s measured):
|
|
237
|
+
one tiny free request when the session opens, so the first reply doesn't */
|
|
238
|
+
function warmFish(voice: string, model: string) {
|
|
239
|
+
if (!model.endsWith("-free")) return;
|
|
240
|
+
void fetch(FISH, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: { Authorization: `Bearer ${key("FISH_API_KEY")}`, model, "Content-Type": "application/json" },
|
|
243
|
+
body: JSON.stringify({ text: "Oi.", reference_id: voice, format: "pcm", sample_rate: RATE, latency: "low" }),
|
|
244
|
+
signal: AbortSignal.timeout(15_000),
|
|
245
|
+
})
|
|
246
|
+
.then(r => r.arrayBuffer())
|
|
247
|
+
.catch(() => {});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* what Omni says on its own, in its voice: when nobody speaks (agent.cascade.idle), and the notice
|
|
251
|
+
that plays the moment the model calls a tool in a turn that said nothing yet (ackFor), so the user
|
|
252
|
+
hears "Vou checar." before the wait, not after it. Synthesized once and kept in .agent/samples, as
|
|
253
|
+
raw PCM16 24 kHz */
|
|
254
|
+
const CLIPS = {
|
|
255
|
+
nudge: "Ainda tá aí?",
|
|
256
|
+
bye: "Vou encerrar por aqui. É só me chamar quando quiser.",
|
|
257
|
+
ack_check_1: "Vou checar.",
|
|
258
|
+
ack_check_2: "Deixa eu ver.",
|
|
259
|
+
ack_check_3: "Vou conferir.",
|
|
260
|
+
ack_open_1: "Vou abrir.",
|
|
261
|
+
ack_open_2: "Já abro.",
|
|
262
|
+
ack_prepare_1: "Vou preparar.",
|
|
263
|
+
ack_prepare_2: "Já preparo.",
|
|
264
|
+
ack_confirm: "Confirmando.",
|
|
265
|
+
ack_cancel: "Cancelando.",
|
|
266
|
+
ack_config: "Vou configurar.",
|
|
267
|
+
ack_write: "Vou escrever.",
|
|
268
|
+
ack_send: "Vou enviar.",
|
|
269
|
+
ack_stop: "Vou parar.",
|
|
270
|
+
ack_search: "Vou buscar.",
|
|
271
|
+
/* the user said goodbye (shared/omni-farewell) */
|
|
272
|
+
farewell: FAREWELL_REPLY,
|
|
273
|
+
} as const;
|
|
274
|
+
type ClipName = keyof typeof CLIPS;
|
|
275
|
+
export const isClip = (name: string): name is ClipName => name in CLIPS;
|
|
276
|
+
|
|
277
|
+
const clipFile = (voice: string, cc: CascadeConfig, name: ClipName) =>
|
|
278
|
+
join(AGENT_DIR, "samples", `fish-${voice}-${cc.speed}-${name}.pcm`);
|
|
279
|
+
|
|
280
|
+
/* the notice for a tool, varied between turns; null: the tool needs none (the model speaks for
|
|
281
|
+
end_call and wait_for_work, and playing or pausing the editor is instant) */
|
|
282
|
+
const ACKS = {
|
|
283
|
+
check: ["ack_check_1", "ack_check_2", "ack_check_3"],
|
|
284
|
+
open: ["ack_open_1", "ack_open_2"],
|
|
285
|
+
prepare: ["ack_prepare_1", "ack_prepare_2"],
|
|
286
|
+
confirm: ["ack_confirm"],
|
|
287
|
+
cancel: ["ack_cancel"],
|
|
288
|
+
config: ["ack_config"],
|
|
289
|
+
write: ["ack_write"],
|
|
290
|
+
send: ["ack_send"],
|
|
291
|
+
stop: ["ack_stop"],
|
|
292
|
+
search: ["ack_search"],
|
|
293
|
+
} satisfies Record<string, ClipName[]>;
|
|
294
|
+
function ackGroup(tool: string): keyof typeof ACKS | null {
|
|
295
|
+
if (tool === "end_call" || tool === WAIT_TOOL_NAME || tool === "control_editor") return null;
|
|
296
|
+
if (tool === "confirm_action") return "confirm";
|
|
297
|
+
if (tool === "cancel_action") return "cancel";
|
|
298
|
+
if (tool === "set_omnichat") return "config";
|
|
299
|
+
if (isActionTool(tool)) return "prepare";
|
|
300
|
+
if (tool === "navigate" || tool === "open_claude" || tool === "show_card" || tool === "ask_in_chat") return "open";
|
|
301
|
+
if (tool === "write_to_claude") return "write";
|
|
302
|
+
if (tool === "send_to_claude") return "send";
|
|
303
|
+
if (tool === "stop_claude") return "stop";
|
|
304
|
+
if (tool === "add_epidemic_sound") return "search";
|
|
305
|
+
return "check";
|
|
306
|
+
}
|
|
307
|
+
export function ackFor(tool: string, turn: number): ClipName | null {
|
|
308
|
+
const group = ackGroup(tool);
|
|
309
|
+
if (!group) return null;
|
|
310
|
+
const options: readonly ClipName[] = ACKS[group];
|
|
311
|
+
return options[turn % options.length];
|
|
312
|
+
}
|
|
313
|
+
/* a clip already on disk (startCascade makes them all); null while it is still being made */
|
|
314
|
+
function clipNow(voice: string, cc: CascadeConfig, name: ClipName): Uint8Array | null {
|
|
315
|
+
try {
|
|
316
|
+
return new Uint8Array(readFileSync(clipFile(voice, cc, name)));
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* what a reply that comes after a tool should not open with: the user already heard the notice, and
|
|
323
|
+
the tool already ran. One short sentence, so a real answer ("Vou checar a agenda: tem dois.") stays */
|
|
324
|
+
const FILLER =
|
|
325
|
+
/^(?:(?:ok|certo|beleza|claro|pronto|hum+|então|tá)[,.!]?\s+)?(?:(?:(?:deixa|deixe) eu|deixe-me|vou|já vou|só vou|vamos)\s+(?:dar uma olhada|ver|checar|conferir|olhar|verificar|buscar|consultar|pesquisar)|(?:só )?(?:um|uma) (?:minuto|minutinho|momento|segundo|segundinho|instante)|(?:checando|verificando|conferindo|olhando|buscando))\b[^.!?…:;]{0,40}[.!?…]+(?:\s+|$)/i;
|
|
326
|
+
/* the leading filler sentences of a reply, removed; `done`: the text may still grow (a sentence
|
|
327
|
+
without its end yet is kept until it ends or grows past a short sentence) */
|
|
328
|
+
export function stripFiller(text: string, done: boolean): { text: string; settled: boolean } {
|
|
329
|
+
let t = text.trimStart();
|
|
330
|
+
for (let m = FILLER.exec(t); m; m = FILLER.exec(t)) t = t.slice(m[0].length);
|
|
331
|
+
const settled = done || /[.!?…:;](\s|$)/.test(t) || t.length > 80;
|
|
332
|
+
return { text: t, settled };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const making = new Map<string, Promise<Uint8Array>>();
|
|
336
|
+
|
|
337
|
+
export async function cascadeClip(id: string, name: string): Promise<Uint8Array> {
|
|
338
|
+
const s = sessions.get(id);
|
|
339
|
+
if (!s || !isClip(name)) throw new Error("trecho inválido");
|
|
340
|
+
const cc = cascadeConfig();
|
|
341
|
+
const file = clipFile(s.voice, cc, name);
|
|
342
|
+
if (existsSync(file)) return new Uint8Array(readFileSync(file));
|
|
343
|
+
const pending = making.get(file);
|
|
344
|
+
if (pending) return pending;
|
|
345
|
+
const job = (async () => {
|
|
346
|
+
for (const model of [cc.fish_model, cc.fish_fallback]) {
|
|
347
|
+
const r = await fetch(FISH, {
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: { Authorization: `Bearer ${key("FISH_API_KEY")}`, model, "Content-Type": "application/json" },
|
|
350
|
+
body: JSON.stringify({
|
|
351
|
+
text: CLIPS[name],
|
|
352
|
+
reference_id: s.voice,
|
|
353
|
+
format: "pcm",
|
|
354
|
+
sample_rate: RATE,
|
|
355
|
+
latency: "normal",
|
|
356
|
+
prosody: { speed: cc.speed, volume: 0 },
|
|
357
|
+
}),
|
|
358
|
+
signal: AbortSignal.timeout(20_000),
|
|
359
|
+
}).catch(() => null);
|
|
360
|
+
if (!r?.ok) continue;
|
|
361
|
+
const pcm = new Uint8Array(await r.arrayBuffer());
|
|
362
|
+
mkdirSync(join(AGENT_DIR, "samples"), { recursive: true });
|
|
363
|
+
writeFileSync(file, pcm);
|
|
364
|
+
return pcm;
|
|
365
|
+
}
|
|
366
|
+
throw new Error("o Fish não gerou o trecho");
|
|
367
|
+
})().finally(() => making.delete(file));
|
|
368
|
+
making.set(file, job);
|
|
369
|
+
return job;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* the STT WebSocket for stream mode: the token only opens it (60 s) and the page asks again to reconnect */
|
|
373
|
+
export async function sttSocket(cc = cascadeConfig()): Promise<{ url: string; token: string }> {
|
|
374
|
+
const r = await fetch(`${XAI}/realtime/client_secrets`, {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: { Authorization: `Bearer ${key("XAI_API_KEY")}`, "Content-Type": "application/json" },
|
|
377
|
+
body: JSON.stringify({ expires_after: { seconds: 60 } }),
|
|
378
|
+
signal: AbortSignal.timeout(15_000),
|
|
379
|
+
});
|
|
380
|
+
if (!r.ok) throw new Error(`xAI ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
381
|
+
const secret: any = await r.json();
|
|
382
|
+
const q = new URLSearchParams({
|
|
383
|
+
model: "grok-voice-transcribe-2.0",
|
|
384
|
+
sample_rate: String(RATE),
|
|
385
|
+
encoding: "pcm",
|
|
386
|
+
interim_results: "true",
|
|
387
|
+
language: agentConfig().language.slice(0, 2),
|
|
388
|
+
smart_turn: String(cc.stt.smart_turn),
|
|
389
|
+
smart_turn_timeout: String(cc.stt.smart_turn_timeout_ms),
|
|
390
|
+
endpointing: String(cc.stt.endpointing_ms),
|
|
391
|
+
});
|
|
392
|
+
return { url: `wss://api.x.ai/v1/stt?${q}`, token: secret.value };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export const hasCascade = (id: string) => sessions.has(id);
|
|
396
|
+
|
|
397
|
+
/* streamed STT seconds, reported by the page when the conversation ends: $0.20 per hour */
|
|
398
|
+
export function endCascade(id: string, durationS: number, sttAudioS: number | undefined, reason?: string) {
|
|
399
|
+
const s = sessions.get(id);
|
|
400
|
+
const wall = s ? (Date.now() - s.started) / 1000 : durationS;
|
|
401
|
+
const streamed = s?.mode === "stream" ? Math.min(sttAudioS ?? durationS, wall + 5) : 0;
|
|
402
|
+
logEvent({ session: id, type: "end", duration_s: durationS, reason, mode: s?.mode });
|
|
403
|
+
if (streamed > 0)
|
|
404
|
+
logEvent({
|
|
405
|
+
session: id,
|
|
406
|
+
type: "stt",
|
|
407
|
+
mode: "stream",
|
|
408
|
+
duration_s: Math.round(streamed * 10) / 10,
|
|
409
|
+
cost_usd: (streamed * 0.2) / 3600,
|
|
410
|
+
});
|
|
411
|
+
for (const a of cancelSessionActions(id))
|
|
412
|
+
logEvent({ session: id, type: "action", kind: a.kind, slug: a.slug, canceled: true, reason: "hangup" });
|
|
413
|
+
s?.turn?.abort();
|
|
414
|
+
sessions.delete(id);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* --- a turn ------------------------------------------------------------------------------------- */
|
|
418
|
+
export type TurnEvent =
|
|
419
|
+
| { type: "text"; delta: string }
|
|
420
|
+
| { type: "tool"; id: string; name: string; state: "running" | "ok" | "error"; ms?: number; detail?: string | null }
|
|
421
|
+
| { type: "pending"; action: unknown }
|
|
422
|
+
/* a card decided by voice (confirm_action, cancel_action): the page takes it away and opens its screen */
|
|
423
|
+
| { type: "resolved"; resolved: unknown }
|
|
424
|
+
/* a tool that changed something by itself (set_omnichat): the page opens the screen that shows it */
|
|
425
|
+
| { type: "open"; open: unknown }
|
|
426
|
+
| { type: "audio"; pcm: string }
|
|
427
|
+
| { type: "end_call" }
|
|
428
|
+
| { type: "ui_call"; id: string; name: string; args: unknown }
|
|
429
|
+
| {
|
|
430
|
+
type: "done";
|
|
431
|
+
llm_first_ms: number | null;
|
|
432
|
+
tts_first_ms: number | null;
|
|
433
|
+
ms: number;
|
|
434
|
+
cost_usd: number;
|
|
435
|
+
model: string;
|
|
436
|
+
tts_model: string | null;
|
|
437
|
+
}
|
|
438
|
+
| { type: "error"; message: string };
|
|
439
|
+
|
|
440
|
+
/* the whole registry: the server tools, the UI tools (run in the browser) and OpenRouter's web search
|
|
441
|
+
(run by OpenRouter inside the call: the stream pauses while it searches, ~1.3 s measured) */
|
|
442
|
+
const TOOLS = [
|
|
443
|
+
...[...TOOL_DEFS, ...UI_TOOL_DEFS].map(t => ({
|
|
444
|
+
type: "function",
|
|
445
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
446
|
+
})),
|
|
447
|
+
{
|
|
448
|
+
type: "openrouter:web_search",
|
|
449
|
+
parameters: { engine: "parallel", mode: "turbo", max_results: 5, max_uses: 2, search_context_size: "low" },
|
|
450
|
+
},
|
|
451
|
+
];
|
|
452
|
+
/* how long the browser has to run a UI tool and answer (a Claude tool may navigate, open the panel
|
|
453
|
+
and wait for the reply box to come back after a stop: up to ~6 s in lib/omni-ui) */
|
|
454
|
+
const UI_TIMEOUT_MS = 9000;
|
|
455
|
+
/* a UI tool that timed out this many times in a row is not called again for UI_TIMEOUT_PAUSE_MS: the
|
|
456
|
+
page is not answering, and each retry costs the user UI_TIMEOUT_MS of silence (measured: 7 in a row, 63 s) */
|
|
457
|
+
const UI_TIMEOUT_LIMIT = 2;
|
|
458
|
+
const UI_TIMEOUT_PAUSE_MS = 60_000;
|
|
459
|
+
/* a tool's output as the LLM sees it: bounded (get_video can be long) */
|
|
460
|
+
const MAX_TOOL_CHARS = 16_000;
|
|
461
|
+
const MAX_ROUNDS = 5;
|
|
462
|
+
|
|
463
|
+
type ToolCall = { id: string; name: string; args: string };
|
|
464
|
+
|
|
465
|
+
/* one LLM call, streamed: text deltas go to onText as they arrive; the tool calls come back whole,
|
|
466
|
+
and onCall hears each one's name as soon as it starts (its arguments still streaming) */
|
|
467
|
+
async function complete(
|
|
468
|
+
model: string,
|
|
469
|
+
messages: Msg[],
|
|
470
|
+
signal: AbortSignal,
|
|
471
|
+
onText: (delta: string) => void,
|
|
472
|
+
onCall: (name: string) => void = () => {},
|
|
473
|
+
): Promise<{ text: string; calls: ToolCall[]; cost: number; firstMs: number | null }> {
|
|
474
|
+
const t0 = performance.now();
|
|
475
|
+
const r = await fetch(OPENROUTER, {
|
|
476
|
+
method: "POST",
|
|
477
|
+
headers: { Authorization: `Bearer ${key("OPENROUTER_API_KEY")}`, "Content-Type": "application/json" },
|
|
478
|
+
/* no reasoning: with it Haiku took 5–6 s after a tool to say one sentence (the tools precompute
|
|
479
|
+
what it would reason about, like published_counts) */
|
|
480
|
+
body: JSON.stringify({
|
|
481
|
+
model,
|
|
482
|
+
messages,
|
|
483
|
+
tools: TOOLS,
|
|
484
|
+
stream: true,
|
|
485
|
+
max_tokens: 500,
|
|
486
|
+
reasoning: { effort: "none" },
|
|
487
|
+
usage: { include: true },
|
|
488
|
+
}),
|
|
489
|
+
signal,
|
|
490
|
+
});
|
|
491
|
+
if (!r.ok || !r.body) throw new Error(`${model} ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
492
|
+
const reader = r.body.getReader();
|
|
493
|
+
const dec = new TextDecoder();
|
|
494
|
+
let buf = "";
|
|
495
|
+
let text = "";
|
|
496
|
+
let cost = 0;
|
|
497
|
+
let firstMs: number | null = null;
|
|
498
|
+
const calls = new Map<number, ToolCall>();
|
|
499
|
+
for (;;) {
|
|
500
|
+
const { done, value } = await reader.read();
|
|
501
|
+
if (done) break;
|
|
502
|
+
buf += dec.decode(value, { stream: true });
|
|
503
|
+
let nl = buf.indexOf("\n");
|
|
504
|
+
while (nl >= 0) {
|
|
505
|
+
const line = buf.slice(0, nl).trim();
|
|
506
|
+
buf = buf.slice(nl + 1);
|
|
507
|
+
nl = buf.indexOf("\n");
|
|
508
|
+
if (!line.startsWith("data:") || line === "data: [DONE]") continue;
|
|
509
|
+
let j: any;
|
|
510
|
+
try {
|
|
511
|
+
j = JSON.parse(line.slice(5));
|
|
512
|
+
} catch {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (j.error) throw new Error(`${model}: ${j.error.message ?? "erro"}`);
|
|
516
|
+
const d = j.choices?.[0]?.delta;
|
|
517
|
+
if (d?.content) {
|
|
518
|
+
firstMs ??= Math.round(performance.now() - t0);
|
|
519
|
+
text += d.content;
|
|
520
|
+
onText(d.content);
|
|
521
|
+
}
|
|
522
|
+
for (const tc of d?.tool_calls ?? []) {
|
|
523
|
+
firstMs ??= Math.round(performance.now() - t0);
|
|
524
|
+
const c = calls.get(tc.index ?? 0) ?? { id: "", name: "", args: "" };
|
|
525
|
+
if (tc.id) c.id = tc.id;
|
|
526
|
+
if (tc.function?.name) {
|
|
527
|
+
const first = !c.name;
|
|
528
|
+
c.name += tc.function.name;
|
|
529
|
+
if (first) onCall(c.name);
|
|
530
|
+
}
|
|
531
|
+
if (tc.function?.arguments) c.args += tc.function.arguments;
|
|
532
|
+
calls.set(tc.index ?? 0, c);
|
|
533
|
+
}
|
|
534
|
+
if (j.usage) cost = Number(j.usage.cost) || 0;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
text,
|
|
539
|
+
calls: [...calls.values()].map((c, i) => ({ ...c, id: c.id || `call_${i}` })),
|
|
540
|
+
cost,
|
|
541
|
+
firstMs,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/* the reply's text → sentences → Fish, in order: each sentence's request starts as soon as the
|
|
546
|
+
sentence is complete, and its audio is emitted only after the previous one's */
|
|
547
|
+
class Speaker {
|
|
548
|
+
firstMs: number | null = null;
|
|
549
|
+
model: string | null = null;
|
|
550
|
+
paidBytes = 0;
|
|
551
|
+
private buf = "";
|
|
552
|
+
private spoken = 0;
|
|
553
|
+
private chain: Promise<void> = Promise.resolve();
|
|
554
|
+
|
|
555
|
+
constructor(
|
|
556
|
+
private voice: string,
|
|
557
|
+
private cc: CascadeConfig,
|
|
558
|
+
private t0: number,
|
|
559
|
+
private signal: AbortSignal,
|
|
560
|
+
private emit: (e: TurnEvent) => void,
|
|
561
|
+
) {}
|
|
562
|
+
|
|
563
|
+
push(delta: string) {
|
|
564
|
+
this.buf += delta;
|
|
565
|
+
for (;;) {
|
|
566
|
+
const cut = this.boundary();
|
|
567
|
+
if (cut < 0) break;
|
|
568
|
+
this.say(this.buf.slice(0, cut));
|
|
569
|
+
this.buf = this.buf.slice(cut);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/* the end of the next sentence; the first piece may also break at a comma (or, past 50
|
|
574
|
+
characters, at a space), so the first audio comes sooner */
|
|
575
|
+
private boundary(): number {
|
|
576
|
+
const m = /[.!?…:;](\s|$)|\n/.exec(this.buf);
|
|
577
|
+
if (m && (m.index > 0 || this.spoken > 0)) return m.index + m[0].length;
|
|
578
|
+
if (this.spoken === 0) {
|
|
579
|
+
const comma = this.buf.indexOf(", ", 15);
|
|
580
|
+
if (comma > 0) return comma + 2;
|
|
581
|
+
if (this.buf.length > 50) {
|
|
582
|
+
const space = this.buf.lastIndexOf(" ");
|
|
583
|
+
if (space > 30) return space + 1;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return -1;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
flush() {
|
|
590
|
+
if (this.buf.trim()) this.say(this.buf);
|
|
591
|
+
this.buf = "";
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/* audio that is already made (a clip), in its turn after what was said before it */
|
|
595
|
+
pcm(bytes: Uint8Array) {
|
|
596
|
+
if (this.signal.aborted) return;
|
|
597
|
+
this.spoken++;
|
|
598
|
+
this.chain = this.chain
|
|
599
|
+
.then(() => {
|
|
600
|
+
for (let i = 0; i < bytes.length && !this.signal.aborted; i += 24_000) {
|
|
601
|
+
this.firstMs ??= Math.round(performance.now() - this.t0);
|
|
602
|
+
this.emit({ type: "audio", pcm: Buffer.from(bytes.subarray(i, i + 24_000)).toString("base64") });
|
|
603
|
+
}
|
|
604
|
+
})
|
|
605
|
+
.catch(() => {});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private say(sentence: string) {
|
|
609
|
+
/* markdown the model may still write is not read aloud */
|
|
610
|
+
const text = sentence
|
|
611
|
+
.replace(/[*_#`>]/g, "")
|
|
612
|
+
.replace(/^\s*[-•]\s+/gm, "")
|
|
613
|
+
.trim();
|
|
614
|
+
if (!text || this.signal.aborted) return;
|
|
615
|
+
this.spoken++;
|
|
616
|
+
const audio = this.request(text);
|
|
617
|
+
this.chain = this.chain.then(() => this.drain(audio)).catch(() => {});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private async request(text: string): Promise<Response | null> {
|
|
621
|
+
for (const model of [this.cc.fish_model, this.cc.fish_fallback]) {
|
|
622
|
+
try {
|
|
623
|
+
const r = await fetch(FISH, {
|
|
624
|
+
method: "POST",
|
|
625
|
+
headers: { Authorization: `Bearer ${key("FISH_API_KEY")}`, model, "Content-Type": "application/json" },
|
|
626
|
+
body: JSON.stringify({
|
|
627
|
+
text,
|
|
628
|
+
reference_id: this.voice,
|
|
629
|
+
format: "pcm",
|
|
630
|
+
sample_rate: RATE,
|
|
631
|
+
latency: "low",
|
|
632
|
+
prosody: { speed: this.cc.speed, volume: 0 },
|
|
633
|
+
}),
|
|
634
|
+
signal: this.signal,
|
|
635
|
+
});
|
|
636
|
+
if (r.ok && r.body) {
|
|
637
|
+
this.model = model;
|
|
638
|
+
if (!model.endsWith("-free")) this.paidBytes += new TextEncoder().encode(text).length;
|
|
639
|
+
return r;
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
if (this.signal.aborted) return null;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private async drain(audio: Promise<Response | null>) {
|
|
649
|
+
const r = await audio;
|
|
650
|
+
if (!r?.body || this.signal.aborted) return;
|
|
651
|
+
const reader = r.body.getReader();
|
|
652
|
+
let carry: Uint8Array | null = null;
|
|
653
|
+
for (;;) {
|
|
654
|
+
const { done, value } = await reader.read();
|
|
655
|
+
if (done || this.signal.aborted) break;
|
|
656
|
+
let bytes = value;
|
|
657
|
+
if (carry) {
|
|
658
|
+
bytes = new Uint8Array(carry.length + value.length);
|
|
659
|
+
bytes.set(carry);
|
|
660
|
+
bytes.set(value, carry.length);
|
|
661
|
+
carry = null;
|
|
662
|
+
}
|
|
663
|
+
/* PCM16: whole samples only */
|
|
664
|
+
if (bytes.length % 2) {
|
|
665
|
+
carry = bytes.slice(-1);
|
|
666
|
+
bytes = bytes.slice(0, -1);
|
|
667
|
+
}
|
|
668
|
+
if (!bytes.length) continue;
|
|
669
|
+
this.firstMs ??= Math.round(performance.now() - this.t0);
|
|
670
|
+
this.emit({ type: "audio", pcm: Buffer.from(bytes).toString("base64") });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
done() {
|
|
675
|
+
return this.chain;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/* a speculative turn starts on the STT's locked text, before it says the user finished: the page
|
|
680
|
+
holds its events until it confirms (commitTurn) and drops it if the user goes on talking. Until
|
|
681
|
+
then it runs no tool, and dropped, it leaves nothing in the history. */
|
|
682
|
+
type Speculation = { committed: boolean; wake: ((go: boolean) => void) | null };
|
|
683
|
+
|
|
684
|
+
/* a UI tool runs in the browser: the call goes down the turn's stream and the browser posts the
|
|
685
|
+
result back (resolveUiCall); no answer in time, or the turn cut, and the model hears why */
|
|
686
|
+
async function uiCall(
|
|
687
|
+
s: CascadeSession,
|
|
688
|
+
c: ToolCall,
|
|
689
|
+
emit: (e: TurnEvent) => void,
|
|
690
|
+
signal: AbortSignal,
|
|
691
|
+
): Promise<Record<string, unknown>> {
|
|
692
|
+
const invalid = checkUiCall(c.name, c.args);
|
|
693
|
+
if (invalid) return { error: invalid };
|
|
694
|
+
const failed = s.uiTimeouts.get(c.name);
|
|
695
|
+
if (failed && Date.now() - failed.at > UI_TIMEOUT_PAUSE_MS) s.uiTimeouts.delete(c.name);
|
|
696
|
+
else if (failed && failed.n >= UI_TIMEOUT_LIMIT)
|
|
697
|
+
return {
|
|
698
|
+
error:
|
|
699
|
+
"a tela não está respondendo a essa ferramenta: não tente de novo, diga ao usuário e siga por outro caminho",
|
|
700
|
+
};
|
|
701
|
+
let args: unknown;
|
|
702
|
+
try {
|
|
703
|
+
args = JSON.parse(c.args || "{}");
|
|
704
|
+
} catch {
|
|
705
|
+
return { error: "argumentos não são JSON" };
|
|
706
|
+
}
|
|
707
|
+
return new Promise(resolve => {
|
|
708
|
+
const done = (output: Record<string, unknown>) => {
|
|
709
|
+
clearTimeout(timer);
|
|
710
|
+
s.ui.delete(c.id);
|
|
711
|
+
if (output.error === "a tela não respondeu")
|
|
712
|
+
s.uiTimeouts.set(c.name, { n: (s.uiTimeouts.get(c.name)?.n ?? 0) + 1, at: Date.now() });
|
|
713
|
+
else if (output.error !== "interrompido") s.uiTimeouts.delete(c.name);
|
|
714
|
+
resolve(output);
|
|
715
|
+
};
|
|
716
|
+
const timer = setTimeout(() => done({ error: "a tela não respondeu" }), UI_TIMEOUT_MS);
|
|
717
|
+
signal.addEventListener("abort", () => done({ error: "interrompido" }), { once: true });
|
|
718
|
+
s.ui.set(c.id, done);
|
|
719
|
+
emit({ type: "ui_call", id: c.id, name: c.name, args });
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function resolveUiCall(id: string, callId: string, output: Record<string, unknown>): boolean {
|
|
724
|
+
const done = sessions.get(id)?.ui.get(callId);
|
|
725
|
+
if (!done) return false;
|
|
726
|
+
done(output);
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export function commitTurn(id: string): boolean {
|
|
731
|
+
const spec = sessions.get(id)?.spec;
|
|
732
|
+
if (!spec) return false;
|
|
733
|
+
spec.committed = true;
|
|
734
|
+
spec.wake?.(true);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function confirmed(spec: Speculation, signal: AbortSignal): Promise<boolean> {
|
|
739
|
+
if (spec.committed) return Promise.resolve(true);
|
|
740
|
+
if (signal.aborted) return Promise.resolve(false);
|
|
741
|
+
return new Promise(resolve => {
|
|
742
|
+
spec.wake = resolve;
|
|
743
|
+
signal.addEventListener("abort", () => resolve(false), { once: true });
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/* tool calls the history has no result for (the turn was cut while they ran) get one, or the next
|
|
748
|
+
LLM call would be refused */
|
|
749
|
+
function closeCalls(local: Msg[]) {
|
|
750
|
+
const answered = new Set(local.filter(m => m.role === "tool").map(m => m.tool_call_id));
|
|
751
|
+
for (const m of [...local])
|
|
752
|
+
for (const c of (m.tool_calls as { id: string }[] | undefined) ?? [])
|
|
753
|
+
if (!answered.has(c.id))
|
|
754
|
+
local.push({ role: "tool", tool_call_id: c.id, content: JSON.stringify({ error: "interrompido" }) });
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export async function runTurn(
|
|
758
|
+
id: string,
|
|
759
|
+
text: string,
|
|
760
|
+
signal: AbortSignal,
|
|
761
|
+
emit: (e: TurnEvent) => void,
|
|
762
|
+
speculative = false,
|
|
763
|
+
screen?: string,
|
|
764
|
+
) {
|
|
765
|
+
const s = sessions.get(id);
|
|
766
|
+
if (!s) return emit({ type: "error", message: "sessão desconhecida: encerre e abra a conversa de novo" });
|
|
767
|
+
/* the admin switched Omni off or moved it to another engine: this conversation stops here */
|
|
768
|
+
const now = agentConfig();
|
|
769
|
+
if (!now.enabled || now.engine !== s.mode)
|
|
770
|
+
return emit({ type: "error", message: now.enabled ? engineMismatch(now.engine) : OMNI_OFF });
|
|
771
|
+
s.turn?.abort();
|
|
772
|
+
const ctrl = new AbortController();
|
|
773
|
+
s.turn = ctrl;
|
|
774
|
+
/* the user's own words (a "[Tela] …" note is the page telling what happened): a card staged before
|
|
775
|
+
this turn can now be confirmed by voice */
|
|
776
|
+
if (!text.startsWith("[Tela]")) noteUserTurn(id);
|
|
777
|
+
const spec: Speculation | null = speculative ? { committed: false, wake: null } : null;
|
|
778
|
+
s.spec = spec;
|
|
779
|
+
const abort = () => ctrl.abort();
|
|
780
|
+
signal.addEventListener("abort", abort);
|
|
781
|
+
const cc = cascadeConfig();
|
|
782
|
+
const t0 = performance.now();
|
|
783
|
+
const speaker = new Speaker(s.voice, cc, t0, ctrl.signal, emit);
|
|
784
|
+
/* this turn's part of the history: it joins s.messages when the turn ends (or is cut) */
|
|
785
|
+
/* where the user is, when it changed since the last turn ("esse vídeo" is the one on screen) */
|
|
786
|
+
const where = screen ? describePath(screen) : null;
|
|
787
|
+
const note = where && where !== s.screen ? `[Tela atual: ${where}]\n` : "";
|
|
788
|
+
if (where) s.screen = where;
|
|
789
|
+
const local: Msg[] = [{ role: "user", content: `${note}${text}` }];
|
|
790
|
+
const history = () => [...s.messages, ...local];
|
|
791
|
+
let cost = 0;
|
|
792
|
+
let llmFirst: number | null = null;
|
|
793
|
+
let model = cc.llm;
|
|
794
|
+
/* this round's text so far, as the user heard it (an interrupted round never reaches the history
|
|
795
|
+
otherwise) */
|
|
796
|
+
let partial = "";
|
|
797
|
+
/* whether the user heard anything in this turn yet */
|
|
798
|
+
let voiced = false;
|
|
799
|
+
try {
|
|
800
|
+
/* a goodbye and nothing else: "Até logo!" and the call ends, with no model in between */
|
|
801
|
+
const bye = !text.startsWith("[Tela]") && isFarewell(text);
|
|
802
|
+
if (bye) {
|
|
803
|
+
const bytes = clipNow(s.voice, cc, "farewell");
|
|
804
|
+
emit({ type: "text", delta: FAREWELL_REPLY });
|
|
805
|
+
if (bytes) speaker.pcm(bytes);
|
|
806
|
+
else speaker.push(`${FAREWELL_REPLY} `);
|
|
807
|
+
speaker.flush();
|
|
808
|
+
local.push({ role: "assistant", content: FAREWELL_REPLY });
|
|
809
|
+
}
|
|
810
|
+
for (let round = 0; !bye && round < MAX_ROUNDS; round++) {
|
|
811
|
+
partial = "";
|
|
812
|
+
/* a round after a tool continues the same spoken reply: keep a space between them */
|
|
813
|
+
let gap = local.some(m => m.role === "assistant" && m.content);
|
|
814
|
+
const say = (raw: string) => {
|
|
815
|
+
const d = gap ? ` ${raw.trimStart()}` : raw;
|
|
816
|
+
gap = false;
|
|
817
|
+
partial += d;
|
|
818
|
+
voiced = true;
|
|
819
|
+
emit({ type: "text", delta: d });
|
|
820
|
+
speaker.push(d);
|
|
821
|
+
};
|
|
822
|
+
/* after the notice (or anything said before a tool), the reply goes straight to the answer: a
|
|
823
|
+
"Deixa eu ver." it opens with is held until its sentence ends and dropped */
|
|
824
|
+
let held: string | null = voiced ? "" : null;
|
|
825
|
+
const release = (done: boolean) => {
|
|
826
|
+
if (held === null) return;
|
|
827
|
+
const { text, settled } = stripFiller(held, done);
|
|
828
|
+
if (!settled) return;
|
|
829
|
+
held = null;
|
|
830
|
+
if (text) say(text);
|
|
831
|
+
};
|
|
832
|
+
/* whether the model itself wrote anything this round (the notice doesn't count) */
|
|
833
|
+
let wrote = false;
|
|
834
|
+
const onText = (raw: string) => {
|
|
835
|
+
wrote = true;
|
|
836
|
+
if (held === null) return say(raw);
|
|
837
|
+
held += raw;
|
|
838
|
+
release(false);
|
|
839
|
+
};
|
|
840
|
+
/* a silent turn that calls a tool: the user hears the notice now, while the tool runs */
|
|
841
|
+
const onCall = (name: string) => {
|
|
842
|
+
if (voiced || ctrl.signal.aborted) return;
|
|
843
|
+
const clip = ackFor(name, s.acks);
|
|
844
|
+
if (!clip) return;
|
|
845
|
+
s.acks++;
|
|
846
|
+
const bytes = clipNow(s.voice, cc, clip);
|
|
847
|
+
if (bytes) {
|
|
848
|
+
const d = gap ? ` ${CLIPS[clip]}` : CLIPS[clip];
|
|
849
|
+
gap = false;
|
|
850
|
+
partial += d;
|
|
851
|
+
voiced = true;
|
|
852
|
+
emit({ type: "text", delta: d });
|
|
853
|
+
speaker.pcm(bytes);
|
|
854
|
+
} else say(`${CLIPS[clip]} `);
|
|
855
|
+
held = "";
|
|
856
|
+
};
|
|
857
|
+
let out: Awaited<ReturnType<typeof complete>>;
|
|
858
|
+
try {
|
|
859
|
+
out = await complete(model, history(), ctrl.signal, onText, onCall);
|
|
860
|
+
} catch (e) {
|
|
861
|
+
/* the fallback only when nothing was said yet: a reply doesn't restart halfway */
|
|
862
|
+
if (ctrl.signal.aborted || wrote || model === cc.llm_fallback) throw e;
|
|
863
|
+
model = cc.llm_fallback;
|
|
864
|
+
out = await complete(model, history(), ctrl.signal, onText, onCall);
|
|
865
|
+
}
|
|
866
|
+
cost += out.cost;
|
|
867
|
+
if (round === 0) llmFirst = out.firstMs;
|
|
868
|
+
release(true);
|
|
869
|
+
speaker.flush();
|
|
870
|
+
/* the history keeps what was said (the notice in, the filler out), so the model doesn't repeat it */
|
|
871
|
+
const said = partial.trim();
|
|
872
|
+
partial = "";
|
|
873
|
+
local.push({
|
|
874
|
+
role: "assistant",
|
|
875
|
+
content: said || null,
|
|
876
|
+
...(out.calls.length
|
|
877
|
+
? {
|
|
878
|
+
tool_calls: out.calls.map(c => ({
|
|
879
|
+
id: c.id,
|
|
880
|
+
type: "function",
|
|
881
|
+
function: { name: c.name, arguments: c.args || "{}" },
|
|
882
|
+
})),
|
|
883
|
+
}
|
|
884
|
+
: {}),
|
|
885
|
+
});
|
|
886
|
+
if (!out.calls.length) break;
|
|
887
|
+
/* no tool runs on a guess: a speculative turn waits for the user to have really finished */
|
|
888
|
+
if (spec && !(await confirmed(spec, ctrl.signal))) throw new Error("descartado");
|
|
889
|
+
let ending = false;
|
|
890
|
+
for (const c of out.calls) {
|
|
891
|
+
if (c.name === "end_call") ending = true;
|
|
892
|
+
const tid = `tool-${c.id}`;
|
|
893
|
+
if (c.name !== "end_call") emit({ type: "tool", id: tid, name: c.name, state: "running" });
|
|
894
|
+
const t = performance.now();
|
|
895
|
+
const output = isUiTool(c.name)
|
|
896
|
+
? await uiCall(s, c, emit, ctrl.signal)
|
|
897
|
+
: await runTool(id, c.name, c.args || "{}");
|
|
898
|
+
const ms = Math.round(performance.now() - t);
|
|
899
|
+
const error = typeof output.error === "string" ? output.error : null;
|
|
900
|
+
logEvent({
|
|
901
|
+
session: id,
|
|
902
|
+
type: "tool",
|
|
903
|
+
name: c.name,
|
|
904
|
+
ms,
|
|
905
|
+
error: error ?? undefined,
|
|
906
|
+
cost_usd: typeof output.cost_usd === "number" ? output.cost_usd : undefined,
|
|
907
|
+
model: typeof output.model === "string" ? output.model : undefined,
|
|
908
|
+
args: isLoggedTool(c.name) ? c.args.slice(0, 2000) : undefined,
|
|
909
|
+
});
|
|
910
|
+
logResolved(id, output);
|
|
911
|
+
if (c.name !== "end_call")
|
|
912
|
+
emit({ type: "tool", id: tid, name: c.name, state: error ? "error" : "ok", ms, detail: error });
|
|
913
|
+
if (output.pending_action) emit({ type: "pending", action: output.pending_action });
|
|
914
|
+
if (output.resolved) emit({ type: "resolved", resolved: output.resolved });
|
|
915
|
+
if (output.open) emit({ type: "open", open: output.open });
|
|
916
|
+
local.push({ role: "tool", tool_call_id: c.id, content: JSON.stringify(output).slice(0, MAX_TOOL_CHARS) });
|
|
917
|
+
}
|
|
918
|
+
if (ending) {
|
|
919
|
+
await speaker.done();
|
|
920
|
+
emit({ type: "end_call" });
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
await speaker.done();
|
|
925
|
+
/* a guess that finished before the user did: it only counts once confirmed */
|
|
926
|
+
if (spec && !(await confirmed(spec, ctrl.signal))) throw new Error("descartado");
|
|
927
|
+
s.messages.push(...local);
|
|
928
|
+
if (bye) emit({ type: "end_call" });
|
|
929
|
+
const ttsCost = speaker.paidBytes * FISH_PAID_PER_BYTE;
|
|
930
|
+
logEvent({
|
|
931
|
+
session: id,
|
|
932
|
+
type: "llm",
|
|
933
|
+
model,
|
|
934
|
+
cost_usd: cost,
|
|
935
|
+
first_ms: llmFirst,
|
|
936
|
+
ms: Math.round(performance.now() - t0),
|
|
937
|
+
speculative: speculative || undefined,
|
|
938
|
+
});
|
|
939
|
+
if (speaker.model)
|
|
940
|
+
logEvent({ session: id, type: "tts", model: speaker.model, paid_bytes: speaker.paidBytes, cost_usd: ttsCost });
|
|
941
|
+
emit({
|
|
942
|
+
type: "done",
|
|
943
|
+
llm_first_ms: llmFirst,
|
|
944
|
+
tts_first_ms: speaker.firstMs,
|
|
945
|
+
ms: Math.round(performance.now() - t0),
|
|
946
|
+
cost_usd: cost + ttsCost,
|
|
947
|
+
model,
|
|
948
|
+
tts_model: speaker.model,
|
|
949
|
+
});
|
|
950
|
+
} catch (e) {
|
|
951
|
+
const dropped = !!spec && !spec.committed;
|
|
952
|
+
if (cost)
|
|
953
|
+
logEvent({
|
|
954
|
+
session: id,
|
|
955
|
+
type: "llm",
|
|
956
|
+
model,
|
|
957
|
+
cost_usd: cost,
|
|
958
|
+
aborted: true,
|
|
959
|
+
speculative: speculative || undefined,
|
|
960
|
+
});
|
|
961
|
+
if (dropped) return;
|
|
962
|
+
if (ctrl.signal.aborted) {
|
|
963
|
+
/* interrupted: what was already said stays in the history, so the next turn knows */
|
|
964
|
+
if (partial) local.push({ role: "assistant", content: `${partial} [interrompido pelo usuário]` });
|
|
965
|
+
closeCalls(local);
|
|
966
|
+
s.messages.push(...local);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
emit({ type: "error", message: String((e as Error)?.message ?? e) });
|
|
970
|
+
} finally {
|
|
971
|
+
signal.removeEventListener("abort", abort);
|
|
972
|
+
if (s.turn === ctrl) s.turn = null;
|
|
973
|
+
if (s.spec === spec) s.spec = null;
|
|
974
|
+
}
|
|
975
|
+
}
|