@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,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* ============================================================================
|
|
3
|
+
Voice envelope for the VoiceAvatar component (templates/components/voiceavatar).
|
|
4
|
+
|
|
5
|
+
node tools/voice-envelope.mjs voice.wav [more.wav …] --out env.js
|
|
6
|
+
node tools/voice-envelope.mjs --scene videos/<slug> <sceneId> [--out env.js]
|
|
7
|
+
|
|
8
|
+
Writes { fps: 60, duration, low, mid, high, all }: four per-frame arrays, 0..1.
|
|
9
|
+
It emulates what the live orb reads from a browser AnalyserNode
|
|
10
|
+
(sampleRate 48 kHz, fftSize 256, Blackman window, smoothingTimeConstant 0.8,
|
|
11
|
+
-100…-30 dB mapped to a byte) and averages the same bands: low 0–200 Hz,
|
|
12
|
+
mid 200–2000 Hz, high 2–20 kHz, all = whole spectrum. Doing it here, once,
|
|
13
|
+
makes the render deterministic: the scene never decodes audio.
|
|
14
|
+
|
|
15
|
+
Several WAVs are laid end to end; in --scene mode each line of the scene
|
|
16
|
+
goes at its `start` (scenes.json), so the envelope is on the scene's clock.
|
|
17
|
+
|
|
18
|
+
--out file.json writes JSON; --out file.js writes `window.VOICE_ENVELOPE = …`
|
|
19
|
+
(what a scene loads with <script src>); no --out prints JSON to stdout.
|
|
20
|
+
Requires ffmpeg.
|
|
21
|
+
============================================================================ */
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
25
|
+
import { dirname, join, resolve } from "node:path";
|
|
26
|
+
|
|
27
|
+
const SR = 48000;
|
|
28
|
+
const FPS = 60;
|
|
29
|
+
const FFT = 256;
|
|
30
|
+
const BINS = FFT / 2;
|
|
31
|
+
const SMOOTH = 0.8;
|
|
32
|
+
const MIN_DB = -100;
|
|
33
|
+
const MAX_DB = -30;
|
|
34
|
+
const STEP = SR / FPS; /* 800 samples per frame */
|
|
35
|
+
|
|
36
|
+
/* --- arguments ------------------------------------------------------------- */
|
|
37
|
+
const argv = process.argv.slice(2);
|
|
38
|
+
const flag = n => {
|
|
39
|
+
const i = argv.indexOf(`--${n}`);
|
|
40
|
+
return i < 0 ? null : argv[i + 1];
|
|
41
|
+
};
|
|
42
|
+
const sceneDir = flag("scene");
|
|
43
|
+
const out = flag("out");
|
|
44
|
+
const positional = [];
|
|
45
|
+
for (let i = 0; i < argv.length; i++) {
|
|
46
|
+
if (argv[i].startsWith("--")) {
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
positional.push(argv[i]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fail = msg => {
|
|
54
|
+
console.error(`erro: ${msg}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/* --- decode: any audio → mono float32 at 48 kHz ------------------------------ */
|
|
59
|
+
function decode(file) {
|
|
60
|
+
const r = spawnSync("ffmpeg", ["-v", "error", "-i", file, "-f", "f32le", "-ac", "1", "-ar", String(SR), "-"], {
|
|
61
|
+
maxBuffer: 1 << 30,
|
|
62
|
+
});
|
|
63
|
+
if (r.status !== 0) fail(`ffmpeg não leu ${file}: ${r.stderr}`);
|
|
64
|
+
const b = r.stdout;
|
|
65
|
+
return new Float32Array(b.buffer, b.byteOffset, Math.floor(b.byteLength / 4));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* --- the signal: the files placed on one timeline --------------------------- */
|
|
69
|
+
const parts = []; /* { start (s), samples } */
|
|
70
|
+
if (sceneDir) {
|
|
71
|
+
const dir = resolve(sceneDir);
|
|
72
|
+
const video = JSON.parse(readFileSync(join(dir, "scenes.json"), "utf8"));
|
|
73
|
+
const scene = video.scenes.find(s => s.id === positional[0]);
|
|
74
|
+
if (!scene) fail(`cena "${positional[0]}" não existe em ${dir}/scenes.json`);
|
|
75
|
+
for (const l of scene.lines ?? []) {
|
|
76
|
+
if (!l.wav || l.start == null) continue;
|
|
77
|
+
parts.push({ start: l.start, samples: decode(join(dir, l.wav)) });
|
|
78
|
+
}
|
|
79
|
+
if (!parts.length) fail(`a cena ${scene.id} não tem fala com áudio e posição (rode a etapa de voz)`);
|
|
80
|
+
} else {
|
|
81
|
+
if (!positional.length) {
|
|
82
|
+
console.error("uso: node voice-envelope.mjs <audio…> [--out env.js]");
|
|
83
|
+
console.error(" node voice-envelope.mjs --scene videos/<slug> <sceneId> [--out env.js]");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
let at = 0;
|
|
87
|
+
for (const f of positional) {
|
|
88
|
+
const samples = decode(f);
|
|
89
|
+
parts.push({ start: at, samples });
|
|
90
|
+
at += samples.length / SR;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const total = Math.max(...parts.map(p => Math.round(p.start * SR) + p.samples.length));
|
|
94
|
+
const signal = new Float32Array(total);
|
|
95
|
+
for (const p of parts) {
|
|
96
|
+
const o = Math.round(p.start * SR);
|
|
97
|
+
for (let i = 0; i < p.samples.length; i++) signal[o + i] += p.samples[i];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* --- analyser emulation ------------------------------------------------------ */
|
|
101
|
+
const window_ = Float32Array.from({ length: FFT }, (_, n) => {
|
|
102
|
+
const a = 0.16;
|
|
103
|
+
return (1 - a) / 2 - 0.5 * Math.cos((2 * Math.PI * n) / FFT) + (a / 2) * Math.cos((4 * Math.PI * n) / FFT);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/* iterative radix-2 FFT, in place */
|
|
107
|
+
function fft(re, im) {
|
|
108
|
+
const n = re.length;
|
|
109
|
+
for (let i = 1, j = 0; i < n; i++) {
|
|
110
|
+
let bit = n >> 1;
|
|
111
|
+
for (; j & bit; bit >>= 1) j ^= bit;
|
|
112
|
+
j ^= bit;
|
|
113
|
+
if (i < j) {
|
|
114
|
+
[re[i], re[j]] = [re[j], re[i]];
|
|
115
|
+
[im[i], im[j]] = [im[j], im[i]];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (let len = 2; len <= n; len <<= 1) {
|
|
119
|
+
const ang = (-2 * Math.PI) / len;
|
|
120
|
+
const wr = Math.cos(ang);
|
|
121
|
+
const wi = Math.sin(ang);
|
|
122
|
+
for (let i = 0; i < n; i += len) {
|
|
123
|
+
let cr = 1;
|
|
124
|
+
let ci = 0;
|
|
125
|
+
for (let k = 0; k < len / 2; k++) {
|
|
126
|
+
const a = i + k;
|
|
127
|
+
const b = a + len / 2;
|
|
128
|
+
const tr = re[b] * cr - im[b] * ci;
|
|
129
|
+
const ti = re[b] * ci + im[b] * cr;
|
|
130
|
+
re[b] = re[a] - tr;
|
|
131
|
+
im[b] = im[a] - ti;
|
|
132
|
+
re[a] += tr;
|
|
133
|
+
im[a] += ti;
|
|
134
|
+
const nr = cr * wr - ci * wi;
|
|
135
|
+
ci = cr * wi + ci * wr;
|
|
136
|
+
cr = nr;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* the same index math as the orb: [round(f / (sr/2) * bins)] inclusive */
|
|
143
|
+
const range = (lo, hi) => [Math.round((lo / (SR / 2)) * BINS), Math.round((hi / (SR / 2)) * BINS)];
|
|
144
|
+
const BANDS = { low: range(0, 200), mid: range(200, 2000), high: range(2000, 20000), all: [0, BINS - 1] };
|
|
145
|
+
|
|
146
|
+
const frames = Math.ceil(total / STEP);
|
|
147
|
+
const result = { fps: FPS, duration: +(total / SR).toFixed(3), low: [], mid: [], high: [], all: [] };
|
|
148
|
+
const smoothed = new Float32Array(BINS);
|
|
149
|
+
const re = new Float32Array(FFT);
|
|
150
|
+
const im = new Float32Array(FFT);
|
|
151
|
+
const bytes = new Float32Array(BINS);
|
|
152
|
+
|
|
153
|
+
for (let f = 0; f < frames; f++) {
|
|
154
|
+
const end = Math.min((f + 1) * STEP, total);
|
|
155
|
+
for (let i = 0; i < FFT; i++) {
|
|
156
|
+
const s = end - FFT + i;
|
|
157
|
+
re[i] = (s >= 0 ? signal[s] : 0) * window_[i];
|
|
158
|
+
im[i] = 0;
|
|
159
|
+
}
|
|
160
|
+
fft(re, im);
|
|
161
|
+
for (let k = 0; k < BINS; k++) {
|
|
162
|
+
const mag = Math.hypot(re[k], im[k]) / FFT;
|
|
163
|
+
smoothed[k] = SMOOTH * smoothed[k] + (1 - SMOOTH) * mag;
|
|
164
|
+
const db = 20 * Math.log10(Math.max(smoothed[k], 1e-12));
|
|
165
|
+
bytes[k] = Math.min(255, Math.max(0, Math.floor((255 / (MAX_DB - MIN_DB)) * (db - MIN_DB))));
|
|
166
|
+
}
|
|
167
|
+
for (const [name, [lo, hi]] of Object.entries(BANDS)) {
|
|
168
|
+
let sum = 0;
|
|
169
|
+
for (let k = lo; k <= hi; k++) sum += bytes[k];
|
|
170
|
+
result[name].push(+(sum / (hi - lo + 1) / 255).toFixed(3));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* --- output ------------------------------------------------------------------ */
|
|
175
|
+
const json = JSON.stringify(result);
|
|
176
|
+
if (!out) {
|
|
177
|
+
process.stdout.write(`${json}\n`);
|
|
178
|
+
} else {
|
|
179
|
+
mkdirSync(dirname(resolve(out)), { recursive: true });
|
|
180
|
+
writeFileSync(out, out.endsWith(".js") ? `window.VOICE_ENVELOPE = ${json};\n` : `${json}\n`);
|
|
181
|
+
const peak = k => Math.max(...result[k]).toFixed(2);
|
|
182
|
+
console.error(
|
|
183
|
+
`envelope: ${frames} frames (${result.duration}s) → ${out} pico low ${peak("low")} mid ${peak("mid")} high ${peak("high")} all ${peak("all")}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
package/tools/voice.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""omnivideo voice providers: Fish Audio, Cartesia, VoiceStudio and ElevenLabs.
|
|
2
|
+
|
|
3
|
+
The order comes from `voice.order` in the config: the first is the default, the
|
|
4
|
+
others are fallbacks, in that order. A video picks its own in `voice.provider`
|
|
5
|
+
of scenes.json (the studio screen writes there); the rest of the order stays
|
|
6
|
+
as fallback.
|
|
7
|
+
|
|
8
|
+
Voice profile: `voice.profiles` in the config keeps, by name, the Fish voice_id,
|
|
9
|
+
the Cartesia one and the ElevenLabs one, the VoiceStudio profile name and the
|
|
10
|
+
`order` to try them in (the first is preferred, the others are fallbacks). The
|
|
11
|
+
video picks its own in `voice.voice_profile` of scenes.json; without a choice,
|
|
12
|
+
`voice.default_profile` applies; with no profile at all, each provider's
|
|
13
|
+
`voice_id` and the global `voice.order`.
|
|
14
|
+
|
|
15
|
+
Each provider can tell whether it is ready (key, voice, service up), generate a
|
|
16
|
+
line as WAV and compute the line hash with its settings. The hash is what tells
|
|
17
|
+
whether the line must be generated again.
|
|
18
|
+
|
|
19
|
+
Keys: environment variable or `.env` at the omnivideo root (outside git), named
|
|
20
|
+
in `voice.<provider>.key_env`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import io
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import threading
|
|
29
|
+
import wave
|
|
30
|
+
|
|
31
|
+
from omni_paths import DATA_ROOT as ROOT
|
|
32
|
+
|
|
33
|
+
CFG = json.loads((ROOT / "omnivideo.config.json").read_text())
|
|
34
|
+
NAMES = {"fish": "Fish Audio", "cartesia": "Cartesia", "voicestudio": "VoiceStudio", "elevenlabs": "ElevenLabs"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Failed(Exception):
|
|
38
|
+
"""The provider did not generate this line. `disable` = no point trying the
|
|
39
|
+
next lines with it in this run (no key, no credit, service down)."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, msg, disable=True, rate_limited=False):
|
|
42
|
+
super().__init__(msg)
|
|
43
|
+
self.disable = disable
|
|
44
|
+
# 429: the provider is up, just too many lines at once (the voice runs in
|
|
45
|
+
# parallel): wait and try the same provider again, don't drop it
|
|
46
|
+
self.rate_limited = rate_limited
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _env():
|
|
50
|
+
vals = {}
|
|
51
|
+
path = ROOT / ".env"
|
|
52
|
+
if path.exists():
|
|
53
|
+
for row in path.read_text().splitlines():
|
|
54
|
+
row = row.strip()
|
|
55
|
+
if row and not row.startswith("#") and "=" in row:
|
|
56
|
+
k, v = row.split("=", 1)
|
|
57
|
+
vals[k.strip()] = v.strip().strip('"').strip("'")
|
|
58
|
+
vals.update({k: v for k, v in os.environ.items() if v})
|
|
59
|
+
return vals
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def spoken_text(line):
|
|
63
|
+
"""The text sent to the voice provider. `text` is also the caption and does not
|
|
64
|
+
change; here the pronunciation map of `voice.pronunciation` in the config
|
|
65
|
+
applies (term → how it is said), for every provider and every video. A term
|
|
66
|
+
that does not occur leaves the text as is: old lines keep the same hash."""
|
|
67
|
+
text = line["text"]
|
|
68
|
+
for term, said_as in (CFG["voice"].get("pronunciation") or {}).items():
|
|
69
|
+
if term and not term.startswith("_"):
|
|
70
|
+
text = re.sub(rf"(?<![\w.]){re.escape(term)}(?!\w|\.\d)", said_as, text, flags=re.IGNORECASE)
|
|
71
|
+
return text
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _digest(line, extra):
|
|
75
|
+
key = json.dumps([spoken_text(line), line.get("instruct", ""), extra], ensure_ascii=False, sort_keys=True)
|
|
76
|
+
return hashlib.sha1(key.encode()).hexdigest()[:12]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _speed(data, cfg):
|
|
80
|
+
return float({**cfg, **(data.get("voice") or {})}.get("speed", 1.0))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def voice_profile(data):
|
|
84
|
+
"""The video's voice profile (`voice.voice_profile` in scenes.json, the id of a
|
|
85
|
+
`voice.profiles` item) or, without a choice, the config's `voice.default_profile`.
|
|
86
|
+
Returns `(profile, reason)`: with no profile registered, each provider's
|
|
87
|
+
`voice_id` applies (`(None, None)`); a chosen profile that no longer exists
|
|
88
|
+
is a reason, not a silent voice swap."""
|
|
89
|
+
cfg = CFG["voice"]
|
|
90
|
+
profiles = cfg.get("profiles") or []
|
|
91
|
+
requested = (data.get("voice") or {}).get("voice_profile")
|
|
92
|
+
if requested:
|
|
93
|
+
found = next((p for p in profiles if p.get("id") == requested), None)
|
|
94
|
+
if found:
|
|
95
|
+
return found, None
|
|
96
|
+
return None, f"o perfil de voz “{requested}” não existe mais (voice.profiles na config)"
|
|
97
|
+
default = cfg.get("default_profile")
|
|
98
|
+
found = next((p for p in profiles if p.get("id") == default), None) if default else None
|
|
99
|
+
return found, None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _voice_id(cfg, data, provider):
|
|
103
|
+
"""The provider's voice_id for this video: the profile's, or the config's without
|
|
104
|
+
a profile. A profile without this provider's id does not fall back to the
|
|
105
|
+
config's (it would be another voice): the provider is left out and the line
|
|
106
|
+
moves on to the next."""
|
|
107
|
+
profile, reason = voice_profile(data)
|
|
108
|
+
if reason:
|
|
109
|
+
return None, reason
|
|
110
|
+
if profile is None:
|
|
111
|
+
return cfg.get("voice_id"), None
|
|
112
|
+
voice = profile.get(provider)
|
|
113
|
+
if not voice:
|
|
114
|
+
return None, f"o perfil “{profile.get('name', profile.get('id'))}” não tem voz na {NAMES[provider]}"
|
|
115
|
+
return voice, None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _http_error(name, resp):
|
|
119
|
+
body = resp.text[:300].replace("\n", " ")
|
|
120
|
+
if resp.status_code == 429:
|
|
121
|
+
return Failed(f"{name} respondeu 429 (muitas falas ao mesmo tempo): {body}", disable=False, rate_limited=True)
|
|
122
|
+
# 401/402/403: key or credit; 5xx: service. Either way, the rest of the
|
|
123
|
+
# lines in this run go to the next provider: consistent voice
|
|
124
|
+
return Failed(f"{name} respondeu {resp.status_code}: {body}", disable=True)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_free(model):
|
|
128
|
+
"""`s2.1-pro-free` is the same model as `s2.1-pro`, without charging the API balance."""
|
|
129
|
+
return str(model or "").endswith("-free")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Fish:
|
|
133
|
+
"""`voice.fish.model` is tried first (the free one, by default) and
|
|
134
|
+
`voice.fish.fallback_model` next (the paid one, charged to the API balance).
|
|
135
|
+
A model that fails on key, quota or service is dropped for the rest of the run."""
|
|
136
|
+
|
|
137
|
+
id = "fish"
|
|
138
|
+
|
|
139
|
+
def __init__(self, requests, data):
|
|
140
|
+
self.r = requests
|
|
141
|
+
self.cfg = CFG["voice"]["fish"]
|
|
142
|
+
self.speed = _speed(data, self.cfg)
|
|
143
|
+
self.key = _env().get(self.cfg["key_env"])
|
|
144
|
+
self.voice_id, self._no_voice = _voice_id(self.cfg, data, "fish")
|
|
145
|
+
# the model that generated the line, per thread (the lines run in parallel):
|
|
146
|
+
# the usage log only charges the paid one
|
|
147
|
+
self._local = threading.local()
|
|
148
|
+
self.disabled = set()
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def model(self):
|
|
152
|
+
return getattr(self._local, "model", None)
|
|
153
|
+
|
|
154
|
+
def ready(self):
|
|
155
|
+
if not self.key:
|
|
156
|
+
return f"sem chave ({self.cfg['key_env']} no ambiente ou no .env)"
|
|
157
|
+
if self._no_voice:
|
|
158
|
+
return self._no_voice
|
|
159
|
+
if not self.voice_id:
|
|
160
|
+
return "sem voice_id (o reference_id do modelo de voz clonado, em voice.fish.voice_id)"
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def config_hash(self):
|
|
164
|
+
# free and paid give the same voice: swapping one for the other does not redo the lines
|
|
165
|
+
model = self.cfg["model"].removesuffix("-free")
|
|
166
|
+
# hash-compat: do not translate
|
|
167
|
+
return {"provider": "fish", "voz": self.voice_id, "modelo": model, "speed": self.speed}
|
|
168
|
+
|
|
169
|
+
def generate(self, line):
|
|
170
|
+
models = [
|
|
171
|
+
m
|
|
172
|
+
for m in dict.fromkeys((self.cfg["model"], self.cfg.get("fallback_model")))
|
|
173
|
+
if m and m not in self.disabled
|
|
174
|
+
]
|
|
175
|
+
if not models:
|
|
176
|
+
raise Failed("Fish Audio: nenhum modelo disponível nesta execução")
|
|
177
|
+
for i, model in enumerate(models):
|
|
178
|
+
try:
|
|
179
|
+
audio = self._tts(line, model)
|
|
180
|
+
self._local.model = model
|
|
181
|
+
return audio
|
|
182
|
+
except Failed as exc:
|
|
183
|
+
# too many at once is not a reason to go to the paid model: the caller waits and retries
|
|
184
|
+
if exc.rate_limited:
|
|
185
|
+
raise
|
|
186
|
+
if exc.disable:
|
|
187
|
+
self.disabled.add(model)
|
|
188
|
+
if i == len(models) - 1:
|
|
189
|
+
raise
|
|
190
|
+
print(f"⚠ {line['id']}: {exc} → tentando o {models[i + 1]}")
|
|
191
|
+
|
|
192
|
+
def _tts(self, line, model):
|
|
193
|
+
body = {
|
|
194
|
+
"text": spoken_text(line),
|
|
195
|
+
"reference_id": self.voice_id,
|
|
196
|
+
"format": "wav",
|
|
197
|
+
"sample_rate": 44100,
|
|
198
|
+
"latency": "normal",
|
|
199
|
+
"prosody": {"speed": self.speed, "volume": 0},
|
|
200
|
+
}
|
|
201
|
+
try:
|
|
202
|
+
resp = self.r.post(
|
|
203
|
+
"https://api.fish.audio/v1/tts",
|
|
204
|
+
json=body,
|
|
205
|
+
timeout=self.cfg["timeout_s"],
|
|
206
|
+
headers={"Authorization": f"Bearer {self.key}", "model": model},
|
|
207
|
+
)
|
|
208
|
+
except self.r.RequestException as exc:
|
|
209
|
+
raise Failed(f"Fish Audio ({model}): {exc}")
|
|
210
|
+
if resp.status_code != 200:
|
|
211
|
+
raise _http_error(f"Fish Audio ({model})", resp)
|
|
212
|
+
return resp.content
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Cartesia:
|
|
216
|
+
id = "cartesia"
|
|
217
|
+
|
|
218
|
+
def __init__(self, requests, data):
|
|
219
|
+
self.r = requests
|
|
220
|
+
self.cfg = CFG["voice"]["cartesia"]
|
|
221
|
+
# Cartesia accepts 0.6 to 1.5
|
|
222
|
+
self.speed = min(1.5, max(0.6, _speed(data, self.cfg)))
|
|
223
|
+
self.key = _env().get(self.cfg["key_env"])
|
|
224
|
+
self.voice_id, self._no_voice = _voice_id(self.cfg, data, "cartesia")
|
|
225
|
+
|
|
226
|
+
def ready(self):
|
|
227
|
+
if not self.key:
|
|
228
|
+
return f"sem chave ({self.cfg['key_env']} no ambiente ou no .env)"
|
|
229
|
+
if self._no_voice:
|
|
230
|
+
return self._no_voice
|
|
231
|
+
if not self.voice_id:
|
|
232
|
+
return "sem voice_id (o id da voz clonada, em voice.cartesia.voice_id)"
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
def config_hash(self):
|
|
236
|
+
# hash-compat: do not translate
|
|
237
|
+
return {
|
|
238
|
+
"provider": "cartesia",
|
|
239
|
+
"voz": self.voice_id,
|
|
240
|
+
"modelo": self.cfg["model"],
|
|
241
|
+
"idioma": self.cfg["language"],
|
|
242
|
+
"speed": self.speed,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
def generate(self, line):
|
|
246
|
+
body = {
|
|
247
|
+
"model_id": self.cfg["model"],
|
|
248
|
+
"transcript": spoken_text(line),
|
|
249
|
+
"voice": {"mode": "id", "id": self.voice_id},
|
|
250
|
+
"language": self.cfg["language"],
|
|
251
|
+
"generation_config": {"speed": self.speed},
|
|
252
|
+
"output_format": {"container": "wav", "encoding": "pcm_s16le", "sample_rate": 44100},
|
|
253
|
+
}
|
|
254
|
+
try:
|
|
255
|
+
resp = self.r.post(
|
|
256
|
+
"https://api.cartesia.ai/tts/bytes",
|
|
257
|
+
json=body,
|
|
258
|
+
timeout=self.cfg["timeout_s"],
|
|
259
|
+
headers={"Authorization": f"Bearer {self.key}", "Cartesia-Version": self.cfg["api_version"]},
|
|
260
|
+
)
|
|
261
|
+
except self.r.RequestException as exc:
|
|
262
|
+
raise Failed(f"Cartesia: {exc}")
|
|
263
|
+
if resp.status_code != 200:
|
|
264
|
+
raise _http_error("Cartesia", resp)
|
|
265
|
+
return resp.content
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class ElevenLabs:
|
|
269
|
+
"""ElevenLabs TTS. The response is 16-bit PCM, mono, 44.1 kHz (no header): it
|
|
270
|
+
becomes WAV here, the same format as the other providers. No per-provider
|
|
271
|
+
line in `usage.py`: consumption stays in the ElevenLabs account."""
|
|
272
|
+
|
|
273
|
+
id = "elevenlabs"
|
|
274
|
+
|
|
275
|
+
def __init__(self, requests, data):
|
|
276
|
+
self.r = requests
|
|
277
|
+
self.cfg = CFG["voice"].get("elevenlabs") or {}
|
|
278
|
+
# ElevenLabs accepts 0.7 to 1.2
|
|
279
|
+
self.speed = min(1.2, max(0.7, _speed(data, self.cfg)))
|
|
280
|
+
self.key = _env().get(self.cfg.get("key_env", "ELEVENLABS_API_KEY"))
|
|
281
|
+
self.voice_id, self._no_voice = _voice_id(self.cfg, data, "elevenlabs")
|
|
282
|
+
|
|
283
|
+
def ready(self):
|
|
284
|
+
if not self.key:
|
|
285
|
+
return f"sem chave ({self.cfg.get('key_env', 'ELEVENLABS_API_KEY')} no ambiente ou no .env)"
|
|
286
|
+
if self._no_voice:
|
|
287
|
+
return self._no_voice
|
|
288
|
+
if not self.voice_id:
|
|
289
|
+
return "sem voice_id (o id da voz na ElevenLabs, em voice.elevenlabs.voice_id)"
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
def config_hash(self):
|
|
293
|
+
# hash-compat: do not translate
|
|
294
|
+
return {
|
|
295
|
+
"provider": "elevenlabs",
|
|
296
|
+
"voz": self.voice_id,
|
|
297
|
+
"modelo": self.cfg.get("model"),
|
|
298
|
+
"idioma": self.cfg.get("language"),
|
|
299
|
+
"speed": self.speed,
|
|
300
|
+
"estabilidade": self.cfg.get("stability"),
|
|
301
|
+
"similaridade": self.cfg.get("similarity"),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
def generate(self, line):
|
|
305
|
+
body = {
|
|
306
|
+
"text": spoken_text(line),
|
|
307
|
+
"model_id": self.cfg.get("model", "eleven_multilingual_v2"),
|
|
308
|
+
"voice_settings": {
|
|
309
|
+
"speed": self.speed,
|
|
310
|
+
"stability": self.cfg.get("stability", 0.5),
|
|
311
|
+
"similarity_boost": self.cfg.get("similarity", 0.75),
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
if self.cfg.get("language"):
|
|
315
|
+
body["language_code"] = self.cfg["language"]
|
|
316
|
+
try:
|
|
317
|
+
resp = self.r.post(
|
|
318
|
+
f"https://api.elevenlabs.io/v1/text-to-speech/{self.voice_id}",
|
|
319
|
+
params={"output_format": "pcm_44100"},
|
|
320
|
+
json=body,
|
|
321
|
+
timeout=self.cfg.get("timeout_s", 120),
|
|
322
|
+
headers={"xi-api-key": self.key},
|
|
323
|
+
)
|
|
324
|
+
except self.r.RequestException as exc:
|
|
325
|
+
raise Failed(f"ElevenLabs: {exc}")
|
|
326
|
+
if resp.status_code != 200:
|
|
327
|
+
raise _http_error("ElevenLabs", resp)
|
|
328
|
+
buf = io.BytesIO()
|
|
329
|
+
with wave.open(buf, "wb") as w:
|
|
330
|
+
w.setnchannels(1)
|
|
331
|
+
w.setsampwidth(2)
|
|
332
|
+
w.setframerate(44100)
|
|
333
|
+
w.writeframes(resp.content)
|
|
334
|
+
return buf.getvalue()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class VoiceStudio:
|
|
338
|
+
"""Local, profile picked by name: the user redoes the clone and the id changes.
|
|
339
|
+
|
|
340
|
+
A nonexistent profile_id is not an error in VoiceStudio: it generates with a
|
|
341
|
+
generic voice, without the clone. That is why the id never goes fixed in the config.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
id = "voicestudio"
|
|
345
|
+
|
|
346
|
+
def __init__(self, requests, data):
|
|
347
|
+
self.r = requests
|
|
348
|
+
self.cfg = CFG["voicestudio"]
|
|
349
|
+
self.vs = {**self.cfg, **(data.get("voice") or {})}
|
|
350
|
+
# the video's voice profile may point to another VoiceStudio profile
|
|
351
|
+
profile, _ = voice_profile(data)
|
|
352
|
+
if profile and profile.get("voicestudio") and not (data.get("voice") or {}).get("profile"):
|
|
353
|
+
self.vs["profile"] = profile["voicestudio"]
|
|
354
|
+
self.url = self.cfg["url"]
|
|
355
|
+
self._settings = None
|
|
356
|
+
self._reason = None
|
|
357
|
+
|
|
358
|
+
def _resolve(self):
|
|
359
|
+
if self._settings or self._reason:
|
|
360
|
+
return
|
|
361
|
+
try:
|
|
362
|
+
self.r.get(f"{self.url}/health", timeout=5).raise_for_status()
|
|
363
|
+
profiles = self.r.get(f"{self.url}/profiles", timeout=10).json()
|
|
364
|
+
except self.r.RequestException:
|
|
365
|
+
self._reason = f"não responde em {self.url} (abra o app ou `bun run dev:api` no repo)"
|
|
366
|
+
return
|
|
367
|
+
name = self.vs["profile"]
|
|
368
|
+
hits = [p for p in profiles if p.get("name") == name]
|
|
369
|
+
if len(hits) != 1:
|
|
370
|
+
found = ", ".join(f"“{p.get('name')}”" for p in profiles) or "nenhum"
|
|
371
|
+
self._reason = f"preciso de exatamente um perfil chamado “{name}”; achei {len(hits)}. Perfis: {found}"
|
|
372
|
+
return
|
|
373
|
+
self._settings = {k: self.vs[k] for k in ("language", "num_step", "speed")}
|
|
374
|
+
self._settings["profile_id"] = hits[0]["id"]
|
|
375
|
+
|
|
376
|
+
def ready(self):
|
|
377
|
+
self._resolve()
|
|
378
|
+
return self._reason
|
|
379
|
+
|
|
380
|
+
def config_hash(self):
|
|
381
|
+
# same format as before the providers existed: lines already generated stay valid
|
|
382
|
+
self._resolve()
|
|
383
|
+
return self._settings
|
|
384
|
+
|
|
385
|
+
def generate(self, line):
|
|
386
|
+
if self.ready():
|
|
387
|
+
raise Failed(f"VoiceStudio {self._reason}")
|
|
388
|
+
form = {"text": spoken_text(line), **{k: str(v) for k, v in self._settings.items()}}
|
|
389
|
+
if line.get("instruct"):
|
|
390
|
+
form["instruct"] = line["instruct"]
|
|
391
|
+
try:
|
|
392
|
+
resp = self.r.post(f"{self.url}/generate", data=form, timeout=self.cfg["timeout_s"])
|
|
393
|
+
except self.r.RequestException as exc:
|
|
394
|
+
# a timeout does not take VoiceStudio down for the next lines
|
|
395
|
+
raise Failed(f"VoiceStudio: {exc}", disable=False)
|
|
396
|
+
if resp.status_code != 200 or not resp.headers.get("content-type", "").startswith("audio/"):
|
|
397
|
+
raise Failed(f"VoiceStudio respondeu {resp.status_code}: {resp.text[:300]}", disable=False)
|
|
398
|
+
return resp.content
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
CLASSES = {"fish": Fish, "cartesia": Cartesia, "voicestudio": VoiceStudio, "elevenlabs": ElevenLabs}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def video_order(data):
|
|
405
|
+
"""This video's try order. With a voice profile, it is the profile's `order`
|
|
406
|
+
(only the providers it configured); without one, the config's `voice.order`.
|
|
407
|
+
`voice.provider` in scenes.json moves one provider to the front."""
|
|
408
|
+
profile, _ = voice_profile(data)
|
|
409
|
+
base = []
|
|
410
|
+
if profile:
|
|
411
|
+
base = [p for p in (profile.get("order") or []) if p in CLASSES and profile.get(p)]
|
|
412
|
+
# profile without `order` (or with an old one): the configured ones, in config order
|
|
413
|
+
base += [p for p in CFG["voice"]["order"] if p in CLASSES and profile.get(p) and p not in base]
|
|
414
|
+
if not base:
|
|
415
|
+
base = [p for p in CFG["voice"]["order"] if p in CLASSES]
|
|
416
|
+
first = (data.get("voice") or {}).get("provider")
|
|
417
|
+
if first in CLASSES:
|
|
418
|
+
return [first] + [p for p in base if p != first]
|
|
419
|
+
return base
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def preferred(data):
|
|
423
|
+
return video_order(data)[0]
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def chain(requests, data):
|
|
427
|
+
"""The video's provider and then the fallbacks, in profile (or config) order."""
|
|
428
|
+
return [CLASSES[p](requests, data) for p in video_order(data)]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def digest(provider, line):
|
|
432
|
+
extra = provider.config_hash()
|
|
433
|
+
return _digest(line, extra) if extra is not None else None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def status(requests=None):
|
|
437
|
+
"""For the studio and for checking: each provider ready, or why not."""
|
|
438
|
+
if requests is None:
|
|
439
|
+
import requests
|
|
440
|
+
return [
|
|
441
|
+
{"id": c.id, "name": NAMES[c.id], "reason": c(requests, {}).ready()}
|
|
442
|
+
for c in (CLASSES[p] for p in CFG["voice"]["order"])
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
if __name__ == "__main__":
|
|
447
|
+
for p in status():
|
|
448
|
+
print(f"{'✓' if not p['reason'] else '·'} {p['name']}{'' if not p['reason'] else ': ' + p['reason']}")
|