@crievo/omni 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +41 -0
- package/README.md +2 -2
- package/bin/omni.mjs +18 -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.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,1067 @@
|
|
|
1
|
+
/* Omni in cascade mode in the browser (api/services/agent-cascade.ts has the why): the microphone
|
|
2
|
+
is heard here, and each turn's text, tools and audio stream back from /api/agent/cascade/:id/turn.
|
|
3
|
+
- stream: the microphone goes to xAI's STT WebSocket the whole time; its speech_final (Smart Turn)
|
|
4
|
+
ends the user's turn.
|
|
5
|
+
- chunks: this page finds each utterance (energy, then a pause) and posts it whole to
|
|
6
|
+
/api/agent/stt; nothing is sent while nobody speaks.
|
|
7
|
+
Same surface as OmniConversation (lib/omni-realtime), so useOmniConversation drives either one.
|
|
8
|
+
The reply plays through an <audio> element fed by a MediaStream, like Grok's, so the browser's echo
|
|
9
|
+
cancellation subtracts it from the microphone.
|
|
10
|
+
Waiting (wait_for_work, lib/omni-wait.ts): the STT socket closes and the silence timers stop; the
|
|
11
|
+
server session (its `messages`) stays. Speaking, typing or the awaited Claude stopping wakes it;
|
|
12
|
+
the first words heard while the STT reconnects are replayed into it. */
|
|
13
|
+
|
|
14
|
+
import { api, enc, send } from "./api";
|
|
15
|
+
import { openMic } from "./mic-device";
|
|
16
|
+
import { greetingSamples } from "./omni-greeting-audio";
|
|
17
|
+
import { currentPath, openScreen, runUiTool } from "./omni-ui";
|
|
18
|
+
import type { NavigateArgs } from "../../shared/omni-screens";
|
|
19
|
+
import {
|
|
20
|
+
historyMessages,
|
|
21
|
+
WAIT_OUTPUT,
|
|
22
|
+
WAIT_TOOL,
|
|
23
|
+
type WaitTarget,
|
|
24
|
+
waitHint,
|
|
25
|
+
waitTargetOf,
|
|
26
|
+
WakeDetector,
|
|
27
|
+
WorkWatch,
|
|
28
|
+
workDoneText,
|
|
29
|
+
} from "./omni-wait";
|
|
30
|
+
import type { AgentPendingAction, AgentResolvedAction } from "../types/api";
|
|
31
|
+
import type { ConfirmResult, OmniCallbacks, OmniEntry, OmniStatus, OmniWait } from "./omni-realtime";
|
|
32
|
+
|
|
33
|
+
export type CascadeMode = "stream" | "chunks";
|
|
34
|
+
|
|
35
|
+
const RATE = 24000;
|
|
36
|
+
const CHUNK_MS = 100;
|
|
37
|
+
const WORKLET = `
|
|
38
|
+
class Capture extends AudioWorkletProcessor {
|
|
39
|
+
constructor() { super(); this.buf = new Int16Array(2400); this.n = 0; }
|
|
40
|
+
process(inputs) {
|
|
41
|
+
const ch = inputs[0] && inputs[0][0];
|
|
42
|
+
if (ch) for (let i = 0; i < ch.length; i++) {
|
|
43
|
+
const s = Math.max(-1, Math.min(1, ch[i]));
|
|
44
|
+
this.buf[this.n++] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
45
|
+
if (this.n === this.buf.length) { this.port.postMessage(this.buf.buffer, [this.buf.buffer]); this.buf = new Int16Array(2400); this.n = 0; }
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
registerProcessor("omni-cascade-capture", Capture);
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
/* speech: RMS above SPEAK for 2 chunks (200 ms). While Omni talks, the microphone also hears what
|
|
54
|
+
the echo canceller leaves of its voice: `echo` follows that level, and speaking over Omni takes
|
|
55
|
+
ECHO_RATIO times it (a fixed 0.07 missed a real voice over Omni, 2026-10-17). */
|
|
56
|
+
const SPEAK = 0.03;
|
|
57
|
+
const ECHO_START = 0.02;
|
|
58
|
+
const ECHO_RATIO = 2.5;
|
|
59
|
+
const PREROLL = 3;
|
|
60
|
+
/* chunks mode: an utterance waits this long after its text before it counts as the whole turn;
|
|
61
|
+
speaking again before that joins the two (a pause mid-sentence isn't the end of the turn).
|
|
62
|
+
The reply is already being prepared meanwhile, so this rarely delays the first audio. */
|
|
63
|
+
const JOIN_MS = 900;
|
|
64
|
+
|
|
65
|
+
const sec = (ms: number | null | undefined) => (ms == null ? "–" : `${(ms / 1000).toFixed(1).replace(".", ",")} s`);
|
|
66
|
+
|
|
67
|
+
function toBase64(bytes: Uint8Array): string {
|
|
68
|
+
let s = "";
|
|
69
|
+
for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
70
|
+
return btoa(s);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fromBase64Pcm(b64: string): Float32Array<ArrayBuffer> {
|
|
74
|
+
const bin = atob(b64);
|
|
75
|
+
const n = bin.length >> 1;
|
|
76
|
+
const out = new Float32Array(n);
|
|
77
|
+
for (let i = 0; i < n; i++) {
|
|
78
|
+
const v = bin.charCodeAt(2 * i) | (bin.charCodeAt(2 * i + 1) << 8);
|
|
79
|
+
out[i] = (v >= 0x8000 ? v - 0x10000 : v) / 0x8000;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type TurnEvent =
|
|
85
|
+
| { type: "text"; delta: string }
|
|
86
|
+
| { type: "tool"; id: string; name: string; state: "running" | "ok" | "error"; ms?: number; detail?: string | null }
|
|
87
|
+
| { type: "pending"; action: AgentPendingAction }
|
|
88
|
+
| { type: "resolved"; resolved: AgentResolvedAction }
|
|
89
|
+
| { type: "open"; open: NavigateArgs }
|
|
90
|
+
| { type: "audio"; pcm: string }
|
|
91
|
+
| { type: "end_call" }
|
|
92
|
+
| { type: "ui_call"; id: string; name: string; args: unknown }
|
|
93
|
+
| {
|
|
94
|
+
type: "done";
|
|
95
|
+
llm_first_ms: number | null;
|
|
96
|
+
tts_first_ms: number | null;
|
|
97
|
+
ms: number;
|
|
98
|
+
cost_usd: number;
|
|
99
|
+
model: string;
|
|
100
|
+
tts_model: string | null;
|
|
101
|
+
}
|
|
102
|
+
| { type: "error"; message: string };
|
|
103
|
+
|
|
104
|
+
type Speculation = {
|
|
105
|
+
ctrl: AbortController;
|
|
106
|
+
text: string;
|
|
107
|
+
committed: boolean;
|
|
108
|
+
queue: TurnEvent[];
|
|
109
|
+
handle: ((e: TurnEvent) => void) | null;
|
|
110
|
+
decide: ((go: boolean) => void) | null;
|
|
111
|
+
/* end of speech → the confirmed text, set on commit */
|
|
112
|
+
sttMs: number | null;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/* what the silence clips say (api/services/agent-cascade.ts CLIPS), for the echo check */
|
|
116
|
+
const CLIP_TEXT = { nudge: "Ainda tá aí?", bye: "Vou encerrar por aqui. É só me chamar quando quiser." };
|
|
117
|
+
|
|
118
|
+
const words = (t: string) =>
|
|
119
|
+
t
|
|
120
|
+
.toLowerCase()
|
|
121
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
122
|
+
.split(/\s+/)
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
|
|
125
|
+
/* a transcript that is Omni's own words read back by the microphone: most of its words are in what
|
|
126
|
+
Omni just said. One or two words ("sim", "não", "pode") are never taken for an echo */
|
|
127
|
+
function echoOf(text: string, said: string): boolean {
|
|
128
|
+
const heard = words(text);
|
|
129
|
+
if (heard.length < 3 || !said) return false;
|
|
130
|
+
const spoken = new Set(words(said));
|
|
131
|
+
return heard.filter(w => spoken.has(w)).length / heard.length >= 0.7;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* the guess and the final text say the same thing (punctuation and case aside) */
|
|
135
|
+
const same = (a: string, b: string) => {
|
|
136
|
+
const norm = (t: string) =>
|
|
137
|
+
t
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.replace(/[^\p{L}\p{N}\s]/gu, "")
|
|
140
|
+
.replace(/\s+/g, " ")
|
|
141
|
+
.trim();
|
|
142
|
+
return norm(a) === norm(b);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export class OmniCascade {
|
|
146
|
+
readonly analyser: AnalyserNode;
|
|
147
|
+
readonly micAnalyser: AnalyserNode;
|
|
148
|
+
sessionId: string | null = null;
|
|
149
|
+
private ctx: AudioContext;
|
|
150
|
+
private out: MediaStreamAudioDestinationNode;
|
|
151
|
+
private player: HTMLAudioElement;
|
|
152
|
+
private mic: MediaStream | null = null;
|
|
153
|
+
private worklet: AudioWorkletNode | null = null;
|
|
154
|
+
private stt: WebSocket | null = null;
|
|
155
|
+
private sttReady = false;
|
|
156
|
+
private sttRetries = 0;
|
|
157
|
+
private sttAudioS = 0;
|
|
158
|
+
private playing = new Set<AudioBufferSourceNode>();
|
|
159
|
+
private playhead = 0;
|
|
160
|
+
private entries: OmniEntry[] = [];
|
|
161
|
+
private status: OmniStatus = "idle";
|
|
162
|
+
private muted = false;
|
|
163
|
+
private closed = false;
|
|
164
|
+
private startedAt = 0;
|
|
165
|
+
private n = 0;
|
|
166
|
+
private turn: AbortController | null = null;
|
|
167
|
+
private ending = false;
|
|
168
|
+
private idleTimer: ReturnType<typeof setInterval> | null = null;
|
|
169
|
+
/* silence: since when nobody has said anything (Omni's own "Ainda tá aí?" doesn't restart it);
|
|
170
|
+
at nudgeMs Omni asks, at closeMs it says goodbye and hangs up (agent.cascade.idle) */
|
|
171
|
+
private silenceAt: number | null = null;
|
|
172
|
+
private nudgeMs = 6000;
|
|
173
|
+
private closeMs = 10_000;
|
|
174
|
+
private nudged = false;
|
|
175
|
+
private closing = false;
|
|
176
|
+
/* a silence clip is playing: the status it causes is not activity */
|
|
177
|
+
private clipActive = false;
|
|
178
|
+
private clips: Partial<Record<"nudge" | "bye", Float32Array<ArrayBuffer>>> = {};
|
|
179
|
+
/* right after Omni's voice stops, a final transcript is its echo, not the user */
|
|
180
|
+
private echoUntil = 0;
|
|
181
|
+
/* what Omni said last (its reply or a silence clip): an echo of it reads back as these words */
|
|
182
|
+
private said = "";
|
|
183
|
+
/* chunks mode: when the current utterance began */
|
|
184
|
+
private speechStartAt = 0;
|
|
185
|
+
private maxMs = 10 * 60_000;
|
|
186
|
+
private pauseMs = 700;
|
|
187
|
+
/* the local voice detector */
|
|
188
|
+
private loud = 0;
|
|
189
|
+
private inSpeech = false;
|
|
190
|
+
private quietMs = 0;
|
|
191
|
+
private lastVoiceAt = 0;
|
|
192
|
+
/* the user spoke over Omni (or while it thought): an STT final then is theirs, not an echo */
|
|
193
|
+
private heardUser = false;
|
|
194
|
+
private preroll: Int16Array[] = [];
|
|
195
|
+
private utterance: Int16Array[] = [];
|
|
196
|
+
/* the user bubble being filled (stream mode's interim text) */
|
|
197
|
+
private userEntry: string | null = null;
|
|
198
|
+
private finalWait: ReturnType<typeof setTimeout> | null = null;
|
|
199
|
+
/* stream mode: the text the STT already locked in this utterance (is_final chunks) */
|
|
200
|
+
private locked: string[] = [];
|
|
201
|
+
/* a reply started on that locked text before the STT's speech_final: its events wait here until
|
|
202
|
+
speech_final confirms the same text (commitSpec), or it is dropped (dropSpec) */
|
|
203
|
+
private spec: Speculation | null = null;
|
|
204
|
+
/* chunks mode: the residual echo heard while Omni talks */
|
|
205
|
+
private echo = ECHO_START;
|
|
206
|
+
/* chunks mode: the last utterance sent, and what is carried into the next one when the user resumed */
|
|
207
|
+
private lastUser: { entry: string; text: string } | null = null;
|
|
208
|
+
private carry: { entry: string; text: string } | null = null;
|
|
209
|
+
private joinTimer: ReturnType<typeof setTimeout> | null = null;
|
|
210
|
+
/* wait_for_work: the call sleeps once Omni finished saying so; asleep, `wait`; the time spent
|
|
211
|
+
asleep doesn't count toward max_session_min */
|
|
212
|
+
private waitFor: WaitTarget | null = null;
|
|
213
|
+
private wait: OmniWait | null = null;
|
|
214
|
+
private waitMaxMs = 20 * 60_000;
|
|
215
|
+
private waitedMs = 0;
|
|
216
|
+
private sleepTimer: ReturnType<typeof setInterval> | null = null;
|
|
217
|
+
|
|
218
|
+
/* `history`: an earlier conversation the user continues; it shows above and goes back into the
|
|
219
|
+
server session's messages */
|
|
220
|
+
constructor(
|
|
221
|
+
private cb: OmniCallbacks,
|
|
222
|
+
private mode: CascadeMode,
|
|
223
|
+
private history: OmniEntry[] = [],
|
|
224
|
+
) {
|
|
225
|
+
this.entries = [...history];
|
|
226
|
+
this.ctx = new AudioContext({ sampleRate: RATE });
|
|
227
|
+
this.analyser = this.ctx.createAnalyser();
|
|
228
|
+
this.analyser.fftSize = 1024;
|
|
229
|
+
this.micAnalyser = this.ctx.createAnalyser();
|
|
230
|
+
this.micAnalyser.fftSize = 1024;
|
|
231
|
+
this.out = this.ctx.createMediaStreamDestination();
|
|
232
|
+
this.analyser.connect(this.out);
|
|
233
|
+
this.player = new Audio();
|
|
234
|
+
this.player.srcObject = this.out.stream;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async start() {
|
|
238
|
+
this.setStatus("connecting");
|
|
239
|
+
try {
|
|
240
|
+
await this.ctx.resume();
|
|
241
|
+
void this.player.play();
|
|
242
|
+
const mic = openMic();
|
|
243
|
+
const history = this.history.length ? historyMessages(this.history) : undefined;
|
|
244
|
+
const s = await send(api.agent.cascade.session.$post({ json: { mode: this.mode, history } }));
|
|
245
|
+
if (!s.ok || !("id" in s) || !s.id) {
|
|
246
|
+
void mic.then(m => m.getTracks().forEach(t => t.stop())).catch(() => {});
|
|
247
|
+
throw new Error(s.error ?? "não deu para abrir a sessão");
|
|
248
|
+
}
|
|
249
|
+
this.sessionId = s.id;
|
|
250
|
+
this.nudgeMs = (s.idle?.nudge_s ?? 6) * 1000;
|
|
251
|
+
this.closeMs = (s.idle?.close_s ?? 10) * 1000;
|
|
252
|
+
void this.loadClips();
|
|
253
|
+
this.maxMs = (s.config?.limits?.max_session_min ?? 10) * 60_000;
|
|
254
|
+
this.waitMaxMs = (s.config?.wait_max_min ?? 20) * 60_000;
|
|
255
|
+
this.pauseMs = s.chunk_pause_ms ?? 700;
|
|
256
|
+
this.mic = await mic;
|
|
257
|
+
if (this.mode === "stream" && s.stt) await this.openStt(s.stt.url, s.stt.token);
|
|
258
|
+
await this.startMic();
|
|
259
|
+
this.startedAt = performance.now();
|
|
260
|
+
this.push({
|
|
261
|
+
kind: "note",
|
|
262
|
+
id: "mode",
|
|
263
|
+
text:
|
|
264
|
+
this.mode === "stream"
|
|
265
|
+
? "Cascata · STT em streaming → Haiku 5.5 → Fish"
|
|
266
|
+
: "Cascata · STT por trechos → Haiku 5.5 → Fish",
|
|
267
|
+
});
|
|
268
|
+
/* the greeting may still be playing */
|
|
269
|
+
this.setStatus(this.playing.size ? "speaking" : "listening");
|
|
270
|
+
this.idleTimer = setInterval(() => this.checkIdle(), 500);
|
|
271
|
+
} catch (e) {
|
|
272
|
+
this.fail(String((e as Error)?.message ?? e));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* --- hearing ---------------------------------------------------------------------------------- */
|
|
277
|
+
private openStt(url: string, token: string) {
|
|
278
|
+
return new Promise<void>((resolve, reject) => {
|
|
279
|
+
this.sttReady = false;
|
|
280
|
+
const ws = new WebSocket(url, [`xai-client-secret.${token}`]);
|
|
281
|
+
this.stt = ws;
|
|
282
|
+
let opened = false;
|
|
283
|
+
ws.onerror = () => {
|
|
284
|
+
if (!opened) reject(new Error("a conexão com o STT da xAI falhou"));
|
|
285
|
+
};
|
|
286
|
+
ws.onclose = () => {
|
|
287
|
+
if (this.stt !== ws) return;
|
|
288
|
+
this.stt = null;
|
|
289
|
+
this.sttReady = false;
|
|
290
|
+
if (opened && !this.closed) void this.reconnectStt();
|
|
291
|
+
};
|
|
292
|
+
ws.onmessage = ev => {
|
|
293
|
+
let m: any;
|
|
294
|
+
try {
|
|
295
|
+
m = JSON.parse(String(ev.data));
|
|
296
|
+
} catch {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (m.type === "transcript.created") {
|
|
300
|
+
opened = true;
|
|
301
|
+
this.sttReady = true;
|
|
302
|
+
this.sttRetries = 0;
|
|
303
|
+
resolve();
|
|
304
|
+
} else if (m.type === "transcript.partial") this.onTranscript(m);
|
|
305
|
+
else if (m.type === "error") this.cb.onError(`STT: ${m.message ?? "erro"}`);
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private async reconnectStt() {
|
|
311
|
+
if (++this.sttRetries > 5 || !this.sessionId) return this.fail("o STT da xAI caiu e não voltou");
|
|
312
|
+
const r = await send(api.agent.cascade[":id"].stt.$post({ param: { id: enc(this.sessionId) }, json: {} }));
|
|
313
|
+
if (this.closed) return;
|
|
314
|
+
if (!r.ok || !("url" in r)) return this.fail(r.error ?? "o STT da xAI caiu");
|
|
315
|
+
await this.openStt(r.url, r.token).catch(() => this.reconnectStt());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private onTranscript(m: { text?: string; is_final?: boolean; speech_final?: boolean }) {
|
|
319
|
+
const text = (m.text ?? "").trim();
|
|
320
|
+
if (!text) return;
|
|
321
|
+
/* while Omni speaks or a reply is on its way, only what the local detector heard as the user
|
|
322
|
+
counts (the rest is echo) */
|
|
323
|
+
const busy = this.playing.size > 0 || (this.turn != null && !this.guessing());
|
|
324
|
+
if (busy && !this.heardUser) return;
|
|
325
|
+
/* right after Omni stops, only its own words coming back are echo: a quick "sim" is the user */
|
|
326
|
+
if (!this.heardUser && performance.now() < this.echoUntil && echoOf(text, this.said)) return;
|
|
327
|
+
/* the bubble shows what is locked plus what is still being heard */
|
|
328
|
+
const shown = m.speech_final ? text : [...this.locked, text].join(" ");
|
|
329
|
+
if (!this.userEntry) {
|
|
330
|
+
this.userEntry = `u-${++this.n}`;
|
|
331
|
+
this.push({ kind: "user", id: this.userEntry, text: shown, pending: true });
|
|
332
|
+
} else this.setUser(this.userEntry, shown, !m.speech_final);
|
|
333
|
+
/* speech too quiet for the local detector: its last loud chunk is from before, so the turn starts now */
|
|
334
|
+
const now = performance.now();
|
|
335
|
+
const endAt = this.lastVoiceAt && now - this.lastVoiceAt < 4000 ? this.lastVoiceAt : now;
|
|
336
|
+
if (!m.speech_final) {
|
|
337
|
+
if (!m.is_final) return;
|
|
338
|
+
this.locked.push(text);
|
|
339
|
+
/* text locked and the user quiet: the reply starts on a guess (~0.2 s before speech_final) */
|
|
340
|
+
if (!this.inSpeech && !this.turn && !this.playing.size)
|
|
341
|
+
void this.runTurn(this.locked.join(" "), endAt, Math.round(now - endAt), false, true);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const id = this.userEntry;
|
|
345
|
+
this.userEntry = null;
|
|
346
|
+
this.locked = [];
|
|
347
|
+
this.setUser(id, text, false);
|
|
348
|
+
if (this.finalWait) clearTimeout(this.finalWait);
|
|
349
|
+
if (this.guessing()) {
|
|
350
|
+
if (same(this.spec!.text, text)) return this.commitSpec(Math.round(now - endAt));
|
|
351
|
+
this.dropSpec();
|
|
352
|
+
}
|
|
353
|
+
void this.runTurn(text, endAt, Math.round(now - endAt));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* a speculative reply is in flight and not confirmed yet */
|
|
357
|
+
private guessing() {
|
|
358
|
+
return !!this.spec && !this.spec.committed;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private commitSpec(sttMs: number) {
|
|
362
|
+
const spec = this.spec;
|
|
363
|
+
if (!spec || spec.committed || !this.sessionId) return;
|
|
364
|
+
spec.committed = true;
|
|
365
|
+
spec.sttMs = sttMs;
|
|
366
|
+
this.setStatus("thinking");
|
|
367
|
+
void send(api.agent.cascade[":id"].commit.$post({ param: { id: enc(this.sessionId) }, json: {} }));
|
|
368
|
+
const queued = spec.queue;
|
|
369
|
+
spec.queue = [];
|
|
370
|
+
for (const e of queued) spec.handle?.(e);
|
|
371
|
+
spec.decide?.(true);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* the user went on talking (or said something else): the guess goes away unheard */
|
|
375
|
+
private dropSpec() {
|
|
376
|
+
const spec = this.spec;
|
|
377
|
+
if (!spec) return;
|
|
378
|
+
this.spec = null;
|
|
379
|
+
spec.ctrl.abort();
|
|
380
|
+
spec.decide?.(false);
|
|
381
|
+
if (this.turn === spec.ctrl) this.turn = null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private async startMic() {
|
|
385
|
+
if (!this.mic) return;
|
|
386
|
+
const url = URL.createObjectURL(new Blob([WORKLET], { type: "application/javascript" }));
|
|
387
|
+
await this.ctx.audioWorklet.addModule(url);
|
|
388
|
+
URL.revokeObjectURL(url);
|
|
389
|
+
const source = this.ctx.createMediaStreamSource(this.mic);
|
|
390
|
+
this.worklet = new AudioWorkletNode(this.ctx, "omni-cascade-capture");
|
|
391
|
+
this.worklet.port.onmessage = (e: MessageEvent<ArrayBuffer>) => this.onChunk(new Int16Array(e.data));
|
|
392
|
+
source.connect(this.worklet);
|
|
393
|
+
source.connect(this.micAnalyser);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private onChunk(pcm: Int16Array<ArrayBuffer>) {
|
|
397
|
+
if (this.closed) return;
|
|
398
|
+
if (this.wait) return this.onWaitChunk(pcm);
|
|
399
|
+
if (this.mode === "stream" && this.stt && this.sttReady) {
|
|
400
|
+
/* muted: silence keeps the STT session alive without hearing anything */
|
|
401
|
+
this.stt.send(this.muted ? new Int16Array(pcm.length) : pcm);
|
|
402
|
+
this.sttAudioS += CHUNK_MS / 1000;
|
|
403
|
+
}
|
|
404
|
+
if (this.muted) return;
|
|
405
|
+
let sum = 0;
|
|
406
|
+
for (let i = 0; i < pcm.length; i++) {
|
|
407
|
+
const v = pcm[i] / 0x8000;
|
|
408
|
+
sum += v * v;
|
|
409
|
+
}
|
|
410
|
+
const rms = Math.sqrt(sum / pcm.length);
|
|
411
|
+
const omniTalking = this.playing.size > 0;
|
|
412
|
+
if (!omniTalking) this.echo = ECHO_START;
|
|
413
|
+
const threshold = omniTalking ? Math.max(SPEAK, this.echo * ECHO_RATIO) : SPEAK;
|
|
414
|
+
/* what the microphone hears of Omni (anything under the bar), slowly averaged */
|
|
415
|
+
if (omniTalking && !this.inSpeech && rms <= threshold) this.echo = this.echo * 0.85 + rms * 0.15;
|
|
416
|
+
const need = 2;
|
|
417
|
+
if (rms > threshold) {
|
|
418
|
+
this.loud++;
|
|
419
|
+
this.quietMs = 0;
|
|
420
|
+
this.lastVoiceAt = performance.now();
|
|
421
|
+
if (!this.inSpeech && this.loud >= need) this.onSpeechStart();
|
|
422
|
+
} else {
|
|
423
|
+
this.loud = 0;
|
|
424
|
+
if (this.inSpeech && rms < SPEAK * 0.7) this.quietMs += CHUNK_MS;
|
|
425
|
+
}
|
|
426
|
+
if (this.inSpeech) {
|
|
427
|
+
if (this.mode === "chunks") this.utterance.push(pcm);
|
|
428
|
+
if (this.quietMs >= (this.mode === "chunks" ? this.pauseMs : 500)) this.onSpeechEnd();
|
|
429
|
+
} else if (this.mode === "chunks") {
|
|
430
|
+
this.preroll.push(pcm);
|
|
431
|
+
if (this.preroll.length > PREROLL) this.preroll.shift();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private onSpeechStart() {
|
|
436
|
+
this.inSpeech = true;
|
|
437
|
+
/* the user spoke before the call slept: it stays awake */
|
|
438
|
+
this.cancelSleep();
|
|
439
|
+
this.speechStartAt = performance.now() - CHUNK_MS * 2;
|
|
440
|
+
/* chunks: the user resumed before the last utterance counted as a whole turn: they go together */
|
|
441
|
+
if (this.mode === "chunks" && this.guessing() && this.lastUser) {
|
|
442
|
+
this.carry = this.lastUser;
|
|
443
|
+
this.lastUser = null;
|
|
444
|
+
if (this.joinTimer) clearTimeout(this.joinTimer);
|
|
445
|
+
this.dropSpec();
|
|
446
|
+
}
|
|
447
|
+
/* barge-in: Omni stops talking and the turn in flight is dropped */
|
|
448
|
+
if (this.playing.size || this.turn) {
|
|
449
|
+
this.heardUser = true;
|
|
450
|
+
this.interrupt();
|
|
451
|
+
}
|
|
452
|
+
if (this.mode === "chunks") {
|
|
453
|
+
this.utterance = [...this.preroll];
|
|
454
|
+
this.preroll = [];
|
|
455
|
+
this.userEntry = `u-${++this.n}`;
|
|
456
|
+
this.push({ kind: "user", id: this.userEntry, text: "", pending: true });
|
|
457
|
+
}
|
|
458
|
+
this.setStatus("user");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private onSpeechEnd() {
|
|
462
|
+
this.inSpeech = false;
|
|
463
|
+
this.quietMs = 0;
|
|
464
|
+
if (this.mode === "stream") {
|
|
465
|
+
/* the STT's speech_final ends the turn; until it comes, Omni waits (a noise never finalizes) */
|
|
466
|
+
if (this.status === "user") this.setStatus("thinking");
|
|
467
|
+
if (this.finalWait) clearTimeout(this.finalWait);
|
|
468
|
+
this.finalWait = setTimeout(() => {
|
|
469
|
+
if (this.guessing()) this.dropSpec();
|
|
470
|
+
if (!this.turn && !this.playing.size && this.status === "thinking") this.setStatus("listening");
|
|
471
|
+
}, 2500);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const chunks = this.utterance;
|
|
475
|
+
this.utterance = [];
|
|
476
|
+
const id = this.userEntry;
|
|
477
|
+
this.userEntry = null;
|
|
478
|
+
/* no minimum length: speech already took 200 ms over the bar to start, and "sim" is short */
|
|
479
|
+
if (!id) {
|
|
480
|
+
this.dropUser(id);
|
|
481
|
+
if (this.carry) return this.sendCarry();
|
|
482
|
+
return this.setStatus("listening");
|
|
483
|
+
}
|
|
484
|
+
void this.transcribe(id, chunks, this.lastVoiceAt, this.speechStartAt);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private async transcribe(id: string, chunks: Int16Array[], endAt: number, startedAt = endAt) {
|
|
488
|
+
this.setStatus("thinking");
|
|
489
|
+
const length = chunks.reduce((n, c) => n + c.length, 0);
|
|
490
|
+
const all = new Int16Array(length);
|
|
491
|
+
let at = 0;
|
|
492
|
+
for (const c of chunks) {
|
|
493
|
+
all.set(c, at);
|
|
494
|
+
at += c.length;
|
|
495
|
+
}
|
|
496
|
+
const r = await send(
|
|
497
|
+
api.agent.stt.$post({
|
|
498
|
+
json: { session: this.sessionId ?? "-", audio: toBase64(new Uint8Array(all.buffer)) },
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
if (this.closed) return;
|
|
502
|
+
const heard = r.ok && "text" in r ? String(r.text ?? "").trim() : "";
|
|
503
|
+
/* an utterance that began as Omni's voice stopped and reads as its own words: the echo, not the user */
|
|
504
|
+
const text = startedAt < this.echoUntil && echoOf(heard, this.said) ? "" : heard;
|
|
505
|
+
if (!text) {
|
|
506
|
+
this.dropUser(id);
|
|
507
|
+
if (this.carry && !this.inSpeech) return this.sendCarry();
|
|
508
|
+
if (!r.ok && r.error !== "áudio curto demais") this.cb.onError(r.error ?? "não entendi o áudio");
|
|
509
|
+
if (!this.turn && !this.playing.size) this.setStatus("listening");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
/* the user started talking again while this was transcribed: it goes with what comes next */
|
|
513
|
+
if (this.inSpeech) {
|
|
514
|
+
this.carry = this.joined(id, text);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const turn = this.joined(id, text);
|
|
518
|
+
this.setUser(turn.entry, turn.text, false);
|
|
519
|
+
this.lastUser = turn;
|
|
520
|
+
const sttMs = Math.round(performance.now() - endAt);
|
|
521
|
+
const reply = this.runTurn(turn.text, endAt, sttMs, false, true);
|
|
522
|
+
if (this.joinTimer) clearTimeout(this.joinTimer);
|
|
523
|
+
this.joinTimer = setTimeout(() => {
|
|
524
|
+
this.lastUser = null;
|
|
525
|
+
this.commitSpec(sttMs);
|
|
526
|
+
}, JOIN_MS);
|
|
527
|
+
await reply;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* what was carried had nothing after it (a noise): it is the turn on its own */
|
|
531
|
+
private sendCarry() {
|
|
532
|
+
const carry = this.carry;
|
|
533
|
+
this.carry = null;
|
|
534
|
+
if (!carry) return;
|
|
535
|
+
this.lastUser = null;
|
|
536
|
+
void this.runTurn(carry.text, this.lastVoiceAt || performance.now(), null);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* this utterance, after what was carried from before: one bubble, one text */
|
|
540
|
+
private joined(id: string, text: string): { entry: string; text: string } {
|
|
541
|
+
const carry = this.carry;
|
|
542
|
+
this.carry = null;
|
|
543
|
+
if (!carry) return { entry: id, text };
|
|
544
|
+
this.dropUser(id);
|
|
545
|
+
const all = `${carry.text} ${text}`;
|
|
546
|
+
this.setUser(carry.entry, all, false);
|
|
547
|
+
return { entry: carry.entry, text: all };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/* --- a turn ------------------------------------------------------------------------------------ */
|
|
551
|
+
private async runTurn(text: string, endAt: number, sttMs: number | null, silent = false, speculative = false) {
|
|
552
|
+
if (!this.sessionId || this.closed) return;
|
|
553
|
+
this.interrupt();
|
|
554
|
+
this.heardUser = false;
|
|
555
|
+
const ctrl = new AbortController();
|
|
556
|
+
this.turn = ctrl;
|
|
557
|
+
const spec: Speculation | null = speculative
|
|
558
|
+
? { ctrl, text, committed: false, queue: [], handle: null, decide: null, sttMs }
|
|
559
|
+
: null;
|
|
560
|
+
this.spec = spec;
|
|
561
|
+
if (!silent && !spec) this.setStatus("thinking");
|
|
562
|
+
const item = `a-${++this.n}`;
|
|
563
|
+
let latency: number | null = null;
|
|
564
|
+
const handle = (e: TurnEvent) => {
|
|
565
|
+
switch (e.type) {
|
|
566
|
+
case "text":
|
|
567
|
+
this.appendAssistant(item, e.delta);
|
|
568
|
+
break;
|
|
569
|
+
case "audio":
|
|
570
|
+
if (latency == null) {
|
|
571
|
+
latency = Math.round(performance.now() - endAt);
|
|
572
|
+
this.patchAssistant(item, { latency_ms: latency });
|
|
573
|
+
}
|
|
574
|
+
this.play(fromBase64Pcm(e.pcm));
|
|
575
|
+
this.setStatus("speaking");
|
|
576
|
+
break;
|
|
577
|
+
case "tool":
|
|
578
|
+
this.upsertTool(e);
|
|
579
|
+
if (!this.playing.size) this.setStatus("thinking");
|
|
580
|
+
break;
|
|
581
|
+
case "pending":
|
|
582
|
+
this.cb.onPending(e.action);
|
|
583
|
+
break;
|
|
584
|
+
case "resolved":
|
|
585
|
+
this.cb.onResolved?.(e.resolved);
|
|
586
|
+
break;
|
|
587
|
+
case "open":
|
|
588
|
+
void openScreen(e.open);
|
|
589
|
+
break;
|
|
590
|
+
case "end_call":
|
|
591
|
+
this.ending = true;
|
|
592
|
+
break;
|
|
593
|
+
case "ui_call":
|
|
594
|
+
void this.uiCall(e);
|
|
595
|
+
break;
|
|
596
|
+
case "done": {
|
|
597
|
+
const stt = spec ? spec.sttMs : sttMs;
|
|
598
|
+
const voice = e.tts_first_ms != null && e.llm_first_ms != null ? e.tts_first_ms - e.llm_first_ms : null;
|
|
599
|
+
const parts = [
|
|
600
|
+
stt != null ? `texto ${sec(stt)}` : null,
|
|
601
|
+
`LLM ${sec(e.llm_first_ms)}`,
|
|
602
|
+
`voz +${sec(voice)}`,
|
|
603
|
+
spec && this.mode === "stream" ? "antecipado" : null,
|
|
604
|
+
`US$ ${e.cost_usd.toFixed(4).replace(".", ",")}`,
|
|
605
|
+
].filter(Boolean);
|
|
606
|
+
this.patchAssistant(item, { breakdown: parts.join(" · ") });
|
|
607
|
+
if (latency != null)
|
|
608
|
+
this.log({
|
|
609
|
+
type: "turn",
|
|
610
|
+
latency_ms: latency,
|
|
611
|
+
stt_ms: stt,
|
|
612
|
+
llm_first_ms: e.llm_first_ms,
|
|
613
|
+
tts_first_ms: e.tts_first_ms,
|
|
614
|
+
item,
|
|
615
|
+
});
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case "error":
|
|
619
|
+
this.cb.onError(e.message);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
if (spec) spec.handle = handle;
|
|
624
|
+
try {
|
|
625
|
+
const r = await fetch(`/api/agent/cascade/${enc(this.sessionId)}/turn`, {
|
|
626
|
+
method: "POST",
|
|
627
|
+
headers: { "Content-Type": "application/json" },
|
|
628
|
+
body: JSON.stringify({ text, speculative: speculative || undefined, screen: currentPath() }),
|
|
629
|
+
signal: ctrl.signal,
|
|
630
|
+
});
|
|
631
|
+
if (!r.ok || !r.body) throw new Error(`o turno falhou (${r.status})`);
|
|
632
|
+
const reader = r.body.getReader();
|
|
633
|
+
const dec = new TextDecoder();
|
|
634
|
+
let buf = "";
|
|
635
|
+
for (;;) {
|
|
636
|
+
const { done, value } = await reader.read();
|
|
637
|
+
if (done) break;
|
|
638
|
+
buf += dec.decode(value, { stream: true });
|
|
639
|
+
let nl = buf.indexOf("\n");
|
|
640
|
+
while (nl >= 0) {
|
|
641
|
+
const line = buf.slice(0, nl);
|
|
642
|
+
buf = buf.slice(nl + 1);
|
|
643
|
+
nl = buf.indexOf("\n");
|
|
644
|
+
if (!line.trim()) continue;
|
|
645
|
+
const e = JSON.parse(line) as TurnEvent;
|
|
646
|
+
if (ctrl.signal.aborted) return;
|
|
647
|
+
if (spec && !spec.committed) spec.queue.push(e);
|
|
648
|
+
else handle(e);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/* a guess whose reply finished before the STT confirmed it: wait for the verdict */
|
|
652
|
+
if (spec && !spec.committed && !(await new Promise<boolean>(r => (spec.decide = r)))) return;
|
|
653
|
+
} catch (e) {
|
|
654
|
+
if (ctrl.signal.aborted || this.closed) return;
|
|
655
|
+
this.cb.onError(String((e as Error)?.message ?? e));
|
|
656
|
+
} finally {
|
|
657
|
+
if (this.turn === ctrl) this.turn = null;
|
|
658
|
+
if (this.spec === spec) this.spec = null;
|
|
659
|
+
}
|
|
660
|
+
if (ctrl.signal.aborted || this.closed) return;
|
|
661
|
+
if (this.ending) return this.hangUpAfterSpeech();
|
|
662
|
+
if (this.waitFor) return this.sleepAfterSpeech();
|
|
663
|
+
if (!this.playing.size && this.status !== "user") this.setStatus("listening");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* a UI tool the reply asked for (navigate): it runs here and the result goes back to the turn */
|
|
667
|
+
private async uiCall(e: Extract<TurnEvent, { type: "ui_call" }>) {
|
|
668
|
+
const id = `tool-${e.id}`;
|
|
669
|
+
this.upsertTool({ type: "tool", id, name: e.name, state: "running" });
|
|
670
|
+
const t0 = performance.now();
|
|
671
|
+
/* wait_for_work is this page's own: the call sleeps after the turn (runTurn) */
|
|
672
|
+
if (e.name === WAIT_TOOL) this.waitFor = waitTargetOf(e.args);
|
|
673
|
+
const output: Record<string, unknown> = e.name === WAIT_TOOL ? WAIT_OUTPUT : await runUiTool(e.name, e.args);
|
|
674
|
+
const error = typeof output.error === "string" ? output.error : null;
|
|
675
|
+
this.upsertTool({
|
|
676
|
+
type: "tool",
|
|
677
|
+
id,
|
|
678
|
+
name: e.name,
|
|
679
|
+
state: error ? "error" : "ok",
|
|
680
|
+
ms: Math.round(performance.now() - t0),
|
|
681
|
+
detail: error,
|
|
682
|
+
});
|
|
683
|
+
if (!this.sessionId) return;
|
|
684
|
+
void send(
|
|
685
|
+
api.agent.cascade[":id"]["ui-result"].$post({
|
|
686
|
+
param: { id: enc(this.sessionId) },
|
|
687
|
+
json: { call_id: e.id, output },
|
|
688
|
+
}),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/* the user spoke over Omni: the reply in flight stops where it was (a guess goes away unheard) */
|
|
693
|
+
private interrupt() {
|
|
694
|
+
if (this.guessing()) this.dropSpec();
|
|
695
|
+
this.turn?.abort();
|
|
696
|
+
this.turn = null;
|
|
697
|
+
this.spec = null;
|
|
698
|
+
this.stopPlayback();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
setMuted(muted: boolean) {
|
|
702
|
+
this.muted = muted;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/* until the user first speaks in a call the panel opened by itself, its silence is the panel's to
|
|
706
|
+
handle (OmniWidget closes it quietly): no "Ainda tá aí?" and no idle hang-up here */
|
|
707
|
+
private idleHeld = false;
|
|
708
|
+
holdIdle(hold: boolean) {
|
|
709
|
+
this.idleHeld = hold;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/* the recorded greeting, as Omni's first words in a call the panel opened by itself: through the
|
|
713
|
+
call's output, so the user can talk over it like over any reply */
|
|
714
|
+
async greet(gender: string) {
|
|
715
|
+
const samples = await greetingSamples(this.ctx, gender);
|
|
716
|
+
if (!samples || this.closed || this.status === "user" || this.status === "error") return;
|
|
717
|
+
this.clipActive = true;
|
|
718
|
+
this.play(samples);
|
|
719
|
+
this.setStatus("speaking");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
sendText(text: string) {
|
|
723
|
+
const t = text.trim();
|
|
724
|
+
if (!t) return;
|
|
725
|
+
this.push({ kind: "user", id: `u-${++this.n}`, text: t, pending: false });
|
|
726
|
+
if (this.wait) return void this.wake(t);
|
|
727
|
+
void this.runTurn(t, performance.now(), null);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async confirm(action: AgentPendingAction): Promise<ConfirmResult> {
|
|
731
|
+
const r = await send(api.agent.actions[":id"].confirm.$post({ param: { id: enc(action.id) }, json: {} }));
|
|
732
|
+
void this.runTurn(
|
|
733
|
+
r.ok
|
|
734
|
+
? `[Tela] O usuário apertou Confirmar e deu certo: ${action.summary}. Resultado: ${(r.output ?? "").slice(-300)}.${waitHint(action.kind)}`
|
|
735
|
+
: `[Tela] O usuário apertou Confirmar, mas falhou: ${action.summary}. Erro: ${r.error}`,
|
|
736
|
+
performance.now(),
|
|
737
|
+
null,
|
|
738
|
+
true,
|
|
739
|
+
);
|
|
740
|
+
return r;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async cancel(action: AgentPendingAction) {
|
|
744
|
+
await send(api.agent.actions[":id"].cancel.$post({ param: { id: enc(action.id) }, json: {} }));
|
|
745
|
+
void this.runTurn(`[Tela] O usuário cancelou: ${action.summary}.`, performance.now(), null, true);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
feedback(item: string, value: "up" | "down" | null) {
|
|
749
|
+
this.entries = this.entries.map(e => (e.kind === "assistant" && e.id === item ? { ...e, feedback: value } : e));
|
|
750
|
+
this.cb.onEntries(this.entries);
|
|
751
|
+
this.log({ type: "feedback", item, value: value ?? "none" });
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private hangUpAfterSpeech() {
|
|
755
|
+
const wait = setInterval(() => {
|
|
756
|
+
if (this.playing.size) return;
|
|
757
|
+
clearInterval(wait);
|
|
758
|
+
this.push({ kind: "note", id: `end-${Date.now()}`, text: "O Omni encerrou a conversa." });
|
|
759
|
+
this.stop("agent");
|
|
760
|
+
}, 200);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private checkIdle() {
|
|
764
|
+
if (this.wait) {
|
|
765
|
+
if (!this.wait.waking && performance.now() - this.wait.since >= this.waitMaxMs) {
|
|
766
|
+
this.push({
|
|
767
|
+
kind: "note",
|
|
768
|
+
id: `wait-max-${Date.now()}`,
|
|
769
|
+
text: `Encerrei a conversa: ${Math.round(this.waitMaxMs / 60_000)} min esperando sem ninguém chamar.`,
|
|
770
|
+
});
|
|
771
|
+
this.stop("wait_max");
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (this.startedAt && performance.now() - this.startedAt - this.waitedMs >= this.maxMs) {
|
|
776
|
+
this.push({
|
|
777
|
+
kind: "note",
|
|
778
|
+
id: `max-${Date.now()}`,
|
|
779
|
+
text: `Encerrei a conversa: chegou ao limite de ${Math.round(this.maxMs / 60_000)} min.`,
|
|
780
|
+
});
|
|
781
|
+
return this.stop("max");
|
|
782
|
+
}
|
|
783
|
+
if (this.idleHeld || this.status !== "listening" || this.silenceAt == null || this.closing) return;
|
|
784
|
+
const quiet = performance.now() - this.silenceAt;
|
|
785
|
+
if (quiet >= this.closeMs) {
|
|
786
|
+
this.closing = true;
|
|
787
|
+
this.playClip("bye");
|
|
788
|
+
const wait = setInterval(() => {
|
|
789
|
+
if (this.playing.size) return;
|
|
790
|
+
clearInterval(wait);
|
|
791
|
+
this.push({
|
|
792
|
+
kind: "note",
|
|
793
|
+
id: `idle-${Date.now()}`,
|
|
794
|
+
text: `Encerrei a conversa: ${Math.round(this.closeMs / 1000)} s sem ninguém falar.`,
|
|
795
|
+
});
|
|
796
|
+
this.stop("idle");
|
|
797
|
+
}, 200);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (!this.nudged && quiet >= this.nudgeMs) {
|
|
801
|
+
this.nudged = true;
|
|
802
|
+
this.playClip("nudge");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/* "Ainda tá aí?" and the goodbye, made once on the server in Omni's voice */
|
|
807
|
+
private async loadClips() {
|
|
808
|
+
for (const name of ["nudge", "bye"] as const) {
|
|
809
|
+
if (!this.sessionId) return;
|
|
810
|
+
try {
|
|
811
|
+
const r = await fetch(`/api/agent/cascade/${enc(this.sessionId)}/clip/${name}`);
|
|
812
|
+
if (!r.ok) continue;
|
|
813
|
+
const pcm = new Int16Array(await r.arrayBuffer());
|
|
814
|
+
const out = new Float32Array(pcm.length);
|
|
815
|
+
for (let i = 0; i < pcm.length; i++) out[i] = pcm[i] / 0x8000;
|
|
816
|
+
this.clips[name] = out;
|
|
817
|
+
} catch {
|
|
818
|
+
/* without the clip Omni just stays quiet (and still hangs up on time) */
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private playClip(name: "nudge" | "bye") {
|
|
824
|
+
const clip = this.clips[name];
|
|
825
|
+
if (!clip) return;
|
|
826
|
+
this.said = CLIP_TEXT[name];
|
|
827
|
+
this.clipActive = true;
|
|
828
|
+
this.play(clip);
|
|
829
|
+
this.setStatus("speaking");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
stop(reason = "user") {
|
|
833
|
+
if (this.closed) return;
|
|
834
|
+
this.closed = true;
|
|
835
|
+
if (this.idleTimer) clearInterval(this.idleTimer);
|
|
836
|
+
if (this.finalWait) clearTimeout(this.finalWait);
|
|
837
|
+
if (this.joinTimer) clearTimeout(this.joinTimer);
|
|
838
|
+
this.cancelSleep();
|
|
839
|
+
if (this.wait) {
|
|
840
|
+
this.waitedMs += performance.now() - this.wait.since;
|
|
841
|
+
this.wait.watch.stop();
|
|
842
|
+
this.wait = null;
|
|
843
|
+
}
|
|
844
|
+
if (this.startedAt)
|
|
845
|
+
this.log({
|
|
846
|
+
type: "end",
|
|
847
|
+
duration_s: Math.round((performance.now() - this.startedAt - this.waitedMs) / 100) / 10,
|
|
848
|
+
stt_audio_s: Math.round(this.sttAudioS * 10) / 10,
|
|
849
|
+
reason,
|
|
850
|
+
});
|
|
851
|
+
this.interrupt();
|
|
852
|
+
this.worklet?.disconnect();
|
|
853
|
+
this.mic?.getTracks().forEach(t => t.stop());
|
|
854
|
+
const stt = this.stt;
|
|
855
|
+
this.stt = null;
|
|
856
|
+
stt?.close(1000);
|
|
857
|
+
this.player.pause();
|
|
858
|
+
void this.ctx.close();
|
|
859
|
+
this.setStatus("idle");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/* --- waiting ------------------------------------------------------------------------------------ */
|
|
863
|
+
/* Omni said it will wait: the call sleeps once that sentence finished playing */
|
|
864
|
+
private sleepAfterSpeech() {
|
|
865
|
+
if (this.sleepTimer) return;
|
|
866
|
+
this.sleepTimer = setInterval(() => {
|
|
867
|
+
if (this.playing.size) return;
|
|
868
|
+
this.cancelSleep(false);
|
|
869
|
+
this.sleep();
|
|
870
|
+
}, 200);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/* `drop`: the user spoke first, so the wait is off */
|
|
874
|
+
private cancelSleep(drop = true) {
|
|
875
|
+
if (this.sleepTimer) clearInterval(this.sleepTimer);
|
|
876
|
+
this.sleepTimer = null;
|
|
877
|
+
if (drop) this.waitFor = null;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
private sleep() {
|
|
881
|
+
const target = this.waitFor ?? { type: null, target: null };
|
|
882
|
+
this.waitFor = null;
|
|
883
|
+
if (this.closed || this.wait) return;
|
|
884
|
+
/* the STT stops hearing (stream mode bills every streamed second); the server session stays */
|
|
885
|
+
const stt = this.stt;
|
|
886
|
+
this.stt = null;
|
|
887
|
+
this.sttReady = false;
|
|
888
|
+
stt?.close(1000);
|
|
889
|
+
if (this.finalWait) clearTimeout(this.finalWait);
|
|
890
|
+
if (this.joinTimer) clearTimeout(this.joinTimer);
|
|
891
|
+
this.dropUser(this.userEntry);
|
|
892
|
+
this.userEntry = null;
|
|
893
|
+
this.inSpeech = false;
|
|
894
|
+
this.loud = 0;
|
|
895
|
+
this.quietMs = 0;
|
|
896
|
+
this.utterance = [];
|
|
897
|
+
this.preroll = [];
|
|
898
|
+
this.locked = [];
|
|
899
|
+
this.lastUser = null;
|
|
900
|
+
this.carry = null;
|
|
901
|
+
this.nudged = false;
|
|
902
|
+
const watch = new WorkWatch(target, s => void this.wake(workDoneText(s)));
|
|
903
|
+
this.wait = { since: performance.now(), detector: new WakeDetector(), watch, waking: false, held: [] };
|
|
904
|
+
this.push({ kind: "note", id: `wait-${Date.now()}`, text: "Esperando o estúdio. É só falar para me chamar." });
|
|
905
|
+
this.setStatus("waiting");
|
|
906
|
+
void watch.start();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private onWaitChunk(pcm: Int16Array<ArrayBuffer>) {
|
|
910
|
+
const w = this.wait;
|
|
911
|
+
if (!w) return;
|
|
912
|
+
if (w.waking) {
|
|
913
|
+
w.held.push(pcm);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (this.muted || !w.detector.feed(pcm)) return;
|
|
917
|
+
w.held = w.detector.take();
|
|
918
|
+
void this.wake(null);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/* `text`: what Omni hears first (the work's end, or a typed message); null: the user is speaking,
|
|
922
|
+
and what was heard while the STT came back is replayed into the detector and the STT */
|
|
923
|
+
private async wake(text: string | null) {
|
|
924
|
+
const w = this.wait;
|
|
925
|
+
if (!w || w.waking || this.closed) return;
|
|
926
|
+
w.waking = true;
|
|
927
|
+
w.watch.stop();
|
|
928
|
+
if (this.mode === "stream" && this.sessionId) {
|
|
929
|
+
this.setStatus("connecting");
|
|
930
|
+
const r = await send(api.agent.cascade[":id"].stt.$post({ param: { id: enc(this.sessionId) }, json: {} }));
|
|
931
|
+
if (this.closed) return;
|
|
932
|
+
if (!r.ok || !("url" in r)) return this.fail(r.error ?? "não deu para voltar a ouvir");
|
|
933
|
+
try {
|
|
934
|
+
await this.openStt(r.url, r.token);
|
|
935
|
+
} catch (e) {
|
|
936
|
+
return this.fail(String((e as Error)?.message ?? e));
|
|
937
|
+
}
|
|
938
|
+
if (this.closed) return;
|
|
939
|
+
}
|
|
940
|
+
this.waitedMs += performance.now() - w.since;
|
|
941
|
+
this.wait = null;
|
|
942
|
+
this.setStatus("listening");
|
|
943
|
+
if (text) return void this.runTurn(text, performance.now(), null, true);
|
|
944
|
+
for (const c of w.held) this.onChunk(c as Int16Array<ArrayBuffer>);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* --- audio out ---------------------------------------------------------------------------------- */
|
|
948
|
+
private play(samples: Float32Array<ArrayBuffer>) {
|
|
949
|
+
if (!samples.length) return;
|
|
950
|
+
const buf = this.ctx.createBuffer(1, samples.length, RATE);
|
|
951
|
+
buf.copyToChannel(samples, 0);
|
|
952
|
+
const src = this.ctx.createBufferSource();
|
|
953
|
+
src.buffer = buf;
|
|
954
|
+
src.connect(this.analyser);
|
|
955
|
+
const at = Math.max(this.ctx.currentTime + 0.02, this.playhead);
|
|
956
|
+
src.start(at);
|
|
957
|
+
this.playhead = at + buf.duration;
|
|
958
|
+
this.playing.add(src);
|
|
959
|
+
src.onended = () => {
|
|
960
|
+
this.playing.delete(src);
|
|
961
|
+
if (this.playing.size) return;
|
|
962
|
+
this.echoUntil = performance.now() + 1200;
|
|
963
|
+
/* silent but still working (a search, a tool, the rest of the reply): "thinking" shimmers the avatar */
|
|
964
|
+
if (this.status === "speaking") this.setStatus(this.turn ? "thinking" : "listening");
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private stopPlayback() {
|
|
969
|
+
for (const s of this.playing) {
|
|
970
|
+
s.onended = null;
|
|
971
|
+
try {
|
|
972
|
+
s.stop();
|
|
973
|
+
} catch {
|
|
974
|
+
/* already stopped */
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
this.playing.clear();
|
|
978
|
+
this.playhead = 0;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/* --- transcript ---------------------------------------------------------------------------------- */
|
|
982
|
+
private push(e: OmniEntry) {
|
|
983
|
+
if (this.entries.some(x => x.id === e.id && x.kind === e.kind)) return;
|
|
984
|
+
this.entries = [...this.entries, e];
|
|
985
|
+
this.cb.onEntries(this.entries);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
private setUser(id: string, text: string, pending: boolean) {
|
|
989
|
+
this.entries = this.entries.map(x => (x.kind === "user" && x.id === id ? { ...x, text, pending } : x));
|
|
990
|
+
this.cb.onEntries(this.entries);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
private dropUser(id: string | null) {
|
|
994
|
+
if (!id) return;
|
|
995
|
+
this.entries = this.entries.filter(x => !(x.kind === "user" && x.id === id));
|
|
996
|
+
this.cb.onEntries(this.entries);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private appendAssistant(id: string, delta: string) {
|
|
1000
|
+
if (!this.entries.some(x => x.kind === "assistant" && x.id === id)) {
|
|
1001
|
+
this.push({ kind: "assistant", id, text: "", latency_ms: null, feedback: null });
|
|
1002
|
+
this.said = "";
|
|
1003
|
+
}
|
|
1004
|
+
this.said += delta;
|
|
1005
|
+
this.entries = this.entries.map(x => (x.kind === "assistant" && x.id === id ? { ...x, text: x.text + delta } : x));
|
|
1006
|
+
this.cb.onEntries(this.entries);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private patchAssistant(id: string, patch: { latency_ms?: number; breakdown?: string }) {
|
|
1010
|
+
if (!this.entries.some(x => x.kind === "assistant" && x.id === id))
|
|
1011
|
+
this.push({ kind: "assistant", id, text: "", latency_ms: null, feedback: null });
|
|
1012
|
+
this.entries = this.entries.map(x => (x.kind === "assistant" && x.id === id ? { ...x, ...patch } : x));
|
|
1013
|
+
this.cb.onEntries(this.entries);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private upsertTool(e: Extract<TurnEvent, { type: "tool" }>) {
|
|
1017
|
+
const entry: OmniEntry = {
|
|
1018
|
+
kind: "tool",
|
|
1019
|
+
id: e.id,
|
|
1020
|
+
name: e.name,
|
|
1021
|
+
state: e.state,
|
|
1022
|
+
detail: e.detail ?? null,
|
|
1023
|
+
ms: e.ms ?? null,
|
|
1024
|
+
};
|
|
1025
|
+
if (this.entries.some(x => x.kind === "tool" && x.id === e.id))
|
|
1026
|
+
this.entries = this.entries.map(x => (x.kind === "tool" && x.id === e.id ? entry : x));
|
|
1027
|
+
else this.entries = [...this.entries, entry];
|
|
1028
|
+
this.cb.onEntries(this.entries);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/* --- plumbing ------------------------------------------------------------------------------------ */
|
|
1032
|
+
private setStatus(s: OmniStatus) {
|
|
1033
|
+
if (this.status === s) return;
|
|
1034
|
+
this.status = s;
|
|
1035
|
+
if (s === "listening") {
|
|
1036
|
+
if (!this.clipActive) this.silenceAt = performance.now();
|
|
1037
|
+
this.clipActive = false;
|
|
1038
|
+
} else if (!(s === "speaking" && this.clipActive)) this.silenceAt = null;
|
|
1039
|
+
if (s === "user" || s === "thinking") this.nudged = false;
|
|
1040
|
+
this.cb.onStatus(s);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private fail(message: string) {
|
|
1044
|
+
this.cb.onError(message);
|
|
1045
|
+
this.log({ type: "error", message });
|
|
1046
|
+
this.stop("error");
|
|
1047
|
+
this.setStatus("error");
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private log(
|
|
1051
|
+
body:
|
|
1052
|
+
| {
|
|
1053
|
+
type: "turn";
|
|
1054
|
+
latency_ms: number;
|
|
1055
|
+
stt_ms: number | null;
|
|
1056
|
+
llm_first_ms: number | null;
|
|
1057
|
+
tts_first_ms: number | null;
|
|
1058
|
+
item?: string;
|
|
1059
|
+
}
|
|
1060
|
+
| { type: "feedback"; item: string; value: "up" | "down" | "none" }
|
|
1061
|
+
| { type: "end"; duration_s: number; stt_audio_s?: number; reason?: string }
|
|
1062
|
+
| { type: "error"; message: string },
|
|
1063
|
+
) {
|
|
1064
|
+
if (!this.sessionId) return;
|
|
1065
|
+
void send(api.agent.cascade[":id"].log.$post({ param: { id: enc(this.sessionId) }, json: body }));
|
|
1066
|
+
}
|
|
1067
|
+
}
|