@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,421 @@
|
|
|
1
|
+
/* What the UI starts: a new video, a new reference, and chatting with Claude
|
|
2
|
+
about a video or a reference that already exists. Each one becomes a session
|
|
3
|
+
(api/services/sessions.ts) with the same request one would make in the chat. */
|
|
4
|
+
|
|
5
|
+
import { copyFileSync, mkdirSync, writeFileSync, renameSync, existsSync, statSync, rmSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join, resolve, basename, extname, sep } from "node:path";
|
|
8
|
+
import { ROOT, PYTHON, readJson } from "./root";
|
|
9
|
+
import { videoDirs, refreshStatus } from "./videos";
|
|
10
|
+
import { LIBRARY_DIR, REFERENCE_NAME_RE, referenceDir } from "./references";
|
|
11
|
+
import { openSession, replyToSession, latestSessionFor, type ApiResult, type SessionType } from "./sessions";
|
|
12
|
+
import { pauseAfterVoice, pauseNotice } from "./voice-pause";
|
|
13
|
+
import { writeDeliverables, writeNarration, type Deliverable } from "./carousel";
|
|
14
|
+
|
|
15
|
+
const VIDEOS_DIR = join(ROOT, "videos");
|
|
16
|
+
|
|
17
|
+
function today() {
|
|
18
|
+
const d = new Date();
|
|
19
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function slugify(text: string) {
|
|
23
|
+
return text
|
|
24
|
+
.normalize("NFD")
|
|
25
|
+
.replace(/[̀-ͯ]/g, "")
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
28
|
+
.trim()
|
|
29
|
+
.split(" ")
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.slice(0, 5)
|
|
32
|
+
.join("-")
|
|
33
|
+
.slice(0, 48)
|
|
34
|
+
.replace(/-+$/, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type VideoRequest = {
|
|
38
|
+
request: string;
|
|
39
|
+
subject?: string;
|
|
40
|
+
format?: string;
|
|
41
|
+
duration?: string;
|
|
42
|
+
reference?: string;
|
|
43
|
+
voice?: string;
|
|
44
|
+
voice_profile?: string;
|
|
45
|
+
/* false: "Sem narração e voz" */
|
|
46
|
+
narration?: boolean;
|
|
47
|
+
deliverables?: unknown;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const PROVIDERS = ["fish", "cartesia", "voicestudio"];
|
|
51
|
+
|
|
52
|
+
/* Voice profile (`voice.profiles` in the config): the chosen id, or "" if the id doesn't exist. */
|
|
53
|
+
function profileIdIfExists(id: unknown): string {
|
|
54
|
+
const target = String(id ?? "");
|
|
55
|
+
const profiles: { id: string }[] = readJson<any>(join(ROOT, "omnivideo.config.json"))?.voice?.profiles ?? [];
|
|
56
|
+
return profiles.some(p => p.id === target) ? target : "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* A video's voice: `voice.provider` and `voice.voice_profile` in scenes.json. The other
|
|
60
|
+
providers in the config order remain as fallback. Changing regenerates the lines
|
|
61
|
+
on the next `voice` (the requested provider and the profile's voice_id go into the hash). */
|
|
62
|
+
export function chooseVoice(dir: string, choice: { provider?: unknown; voice_profile?: unknown }): ApiResult {
|
|
63
|
+
const provider = choice.provider == null || choice.provider === "" ? "" : String(choice.provider);
|
|
64
|
+
const profile =
|
|
65
|
+
choice.voice_profile == null || choice.voice_profile === "" ? "" : profileIdIfExists(choice.voice_profile);
|
|
66
|
+
if (provider && !PROVIDERS.includes(provider)) return { ok: false, error: `provider desconhecido: ${provider}` };
|
|
67
|
+
if (choice.voice_profile && !profile)
|
|
68
|
+
return { ok: false, error: `o perfil de voz “${choice.voice_profile}” não existe` };
|
|
69
|
+
if (!provider && !profile) return { ok: false, error: "escolha o provider ou o perfil de voz" };
|
|
70
|
+
const file = join(dir, "scenes.json");
|
|
71
|
+
const scenes = readJson<any>(file);
|
|
72
|
+
if (!scenes)
|
|
73
|
+
return { ok: false, error: "o vídeo ainda não tem scenes.json: a escolha do pedido vale até ele existir" };
|
|
74
|
+
const voice = { ...(scenes.voice ?? {}) };
|
|
75
|
+
if ((!provider || voice.provider === provider) && (!profile || voice.voice_profile === profile))
|
|
76
|
+
return { ok: true, output: "já era este" };
|
|
77
|
+
if (provider) voice.provider = provider;
|
|
78
|
+
if (profile) voice.voice_profile = profile;
|
|
79
|
+
scenes.voice = voice;
|
|
80
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
81
|
+
writeFileSync(tmp, JSON.stringify(scenes, null, 2) + "\n");
|
|
82
|
+
renameSync(tmp, file);
|
|
83
|
+
const hasVoice = (scenes.scenes ?? []).some((c: any) => (c.lines ?? []).some((l: any) => l.wav));
|
|
84
|
+
if (!hasVoice) return { ok: true, output: "gravado" };
|
|
85
|
+
/* the voice model changed: what comes after the voice is no longer useful, and waits for the new lines */
|
|
86
|
+
const pause = pauseAfterVoice(dir, ["video-voice"]);
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
output: `gravado: as falas que já têm voz ficam como estão até você gerar de novo (todas pelo botão, ou uma a uma em cada fala); ${pauseNotice(pause)}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function createVideo(p: VideoRequest): Promise<ApiResult & { slug?: string }> {
|
|
94
|
+
const request = String(p.request ?? "").trim();
|
|
95
|
+
if (!request) return { ok: false, error: "escreva o pedido: roteiro, tema, material ou link" };
|
|
96
|
+
/* what the content delivers; with no choice, video (the request from before the carousel) */
|
|
97
|
+
const deliverables: Deliverable[] = (["video", "carousel"] as const).filter(e =>
|
|
98
|
+
Array.isArray(p.deliverables) ? p.deliverables.includes(e) : e === "video",
|
|
99
|
+
);
|
|
100
|
+
if (!deliverables.length) return { ok: false, error: "escolha vídeo, carrossel ou os dois" };
|
|
101
|
+
const hasVideo = deliverables.includes("video");
|
|
102
|
+
const format = p.format === "16:9" ? "16:9" : p.format === "9:16" ? "9:16" : "";
|
|
103
|
+
const reference = String(p.reference ?? "").trim();
|
|
104
|
+
if (reference && !referenceDir(reference)) return { ok: false, error: `o estilo visual “${reference}” não existe` };
|
|
105
|
+
/* "Sem narração e voz": no voice is chosen, and the spoken stages do not run */
|
|
106
|
+
const silent = p.narration === false;
|
|
107
|
+
const voice = !silent && PROVIDERS.includes(String(p.voice)) ? String(p.voice) : "";
|
|
108
|
+
const voiceProfile = silent ? "" : profileIdIfExists(p.voice_profile);
|
|
109
|
+
const profileName = voiceProfile
|
|
110
|
+
? String(
|
|
111
|
+
(readJson<any>(join(ROOT, "omnivideo.config.json"))?.voice?.profiles ?? []).find(
|
|
112
|
+
(x: any) => x.id === voiceProfile,
|
|
113
|
+
)?.name ?? voiceProfile,
|
|
114
|
+
)
|
|
115
|
+
: "";
|
|
116
|
+
|
|
117
|
+
const base = `${today()}-${slugify(p.subject?.trim() || request) || (hasVideo ? "video" : "carousel")}`;
|
|
118
|
+
let slug = base;
|
|
119
|
+
for (let n = 2; existsSync(join(VIDEOS_DIR, slug)) || videoDirs().has(slug); n++) slug = `${base}-${n}`;
|
|
120
|
+
const dir = join(VIDEOS_DIR, slug);
|
|
121
|
+
mkdirSync(join(dir, "source"), { recursive: true });
|
|
122
|
+
writeDeliverables(dir, deliverables);
|
|
123
|
+
if (hasVideo && silent) writeNarration(dir, false);
|
|
124
|
+
|
|
125
|
+
const title = p.subject?.trim() || request.split("\n")[0].slice(0, 80);
|
|
126
|
+
const choices = [
|
|
127
|
+
`| entregas | ${deliverables.map(e => (e === "video" ? "vídeo" : "carrossel do Instagram")).join(" e ")} |`,
|
|
128
|
+
hasVideo && format && `| formato | ${format} |`,
|
|
129
|
+
p.duration?.trim() && `| duração alvo | ${p.duration.trim()} |`,
|
|
130
|
+
reference && `| estilo visual | ${reference} |`,
|
|
131
|
+
hasVideo &&
|
|
132
|
+
silent &&
|
|
133
|
+
"| narração e voz | sem narração e voz: pula o roteiro falado, a narração, a voz e a legenda |",
|
|
134
|
+
hasVideo && voice && `| voz | ${voice} (os outros providers ficam de fallback) |`,
|
|
135
|
+
hasVideo && voiceProfile && `| perfil de voz | ${profileName} (\`${voiceProfile}\`) |`,
|
|
136
|
+
].filter(Boolean);
|
|
137
|
+
writeFileSync(
|
|
138
|
+
join(dir, "source", "request.md"),
|
|
139
|
+
`# ${title}\n\nPedido feito pela tela do studio em ${new Date().toLocaleString("pt-BR")}, palavra por palavra:\n\n` +
|
|
140
|
+
`${request
|
|
141
|
+
.split("\n")
|
|
142
|
+
.map(l => `> ${l}`)
|
|
143
|
+
.join("\n")}\n` +
|
|
144
|
+
(choices.length ? `\n## Escolhido na tela\n\n| | |\n|---|---|\n${choices.join("\n")}\n` : ""),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
/* carousel only: no voice or motion; the av-carousel skill goes up to the export */
|
|
148
|
+
if (!hasVideo) {
|
|
149
|
+
const prompt = [
|
|
150
|
+
"/omnivideo",
|
|
151
|
+
`Conteúdo novo pedido pela tela do studio, só carrossel do Instagram (sem vídeo). A pasta já existe: videos/${slug} (use este slug, não crie outro).`,
|
|
152
|
+
`O pedido, palavra por palavra, e o que foi escolhido na tela estão em videos/${slug}/source/request.md: leia antes de tudo e use como o pedido literal do brief (entregas: carrossel).`,
|
|
153
|
+
reference ? `Use o estilo visual ${reference} no estilo do carrossel.` : "",
|
|
154
|
+
`Chame a skill av-carousel com \`videos/${slug}\` e vá até o export (carousel.mjs export). Nenhuma etapa do vídeo roda. Pare no export: publicar é pelo botão da tela.`,
|
|
155
|
+
]
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join("\n");
|
|
158
|
+
await refreshStatus(slug);
|
|
159
|
+
const r = openSession("video", slug, dir, title, prompt);
|
|
160
|
+
return { ...r, slug };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const prompt = [
|
|
164
|
+
"/omnivideo",
|
|
165
|
+
`Vídeo novo pedido pela tela do studio. A pasta já existe: videos/${slug} (use este slug, não crie outro).`,
|
|
166
|
+
`O pedido, palavra por palavra, e o que foi escolhido na tela estão em videos/${slug}/source/request.md: leia antes de tudo e use como o pedido literal do brief.`,
|
|
167
|
+
reference ? `Use o estilo visual ${reference} no roteiro de motion.` : "",
|
|
168
|
+
!silent && (voice || voiceProfile)
|
|
169
|
+
? `No scenes.json, grave "voice": { ${[voice && `"provider": "${voice}"`, voiceProfile && `"voice_profile": "${voiceProfile}"`].filter(Boolean).join(", ")} }.`
|
|
170
|
+
: "",
|
|
171
|
+
silent
|
|
172
|
+
? `Este vídeo é SEM NARRAÇÃO E VOZ (videos/${slug}/content.json, narration: false). Pule o roteiro falado (etapa 1: não escreva script.md nem abra rodada de aprovação do roteiro), a narração (4), a voz (5) e a legenda (7). Parta do pedido (source/request.md) direto para o roteiro de motion (etapa 2), depois composição (3), render (6) e montagem (8); a duração de cada cena vem da composição (min_duration e os beats em segundos), sem voz. Siga a skill omnivideo, seção "Sem narração e voz".`
|
|
173
|
+
: "Depois do roteiro, espere a aprovação dele (approval.mjs wait --script, em primeiro plano); saindo com 3, chame a av-script com `ajustar roteiro: «…»` e espere a nova rodada.",
|
|
174
|
+
"Depois do roteiro de motion, espere a aprovação das cenas (approval.mjs wait, em primeiro plano); a contagem pausa enquanto o usuário edita e volta sozinha; só se ele sair com 4, pare ali: a tela retoma a sessão quando a rodada terminar. O mesmo vale para a saída 4 na aprovação do roteiro.",
|
|
175
|
+
"Com as cenas aprovadas, siga direto para a composição (etapa 3) e vá até a entrega (etapa 8), sem parar para pedir aprovação.",
|
|
176
|
+
deliverables.includes("carousel")
|
|
177
|
+
? `Este conteúdo também entrega um carrossel do Instagram (content.json): depois do vídeo entregue (etapa 8), chame a av-carousel com \`videos/${slug} do vídeo\` e vá até o export do carrossel.`
|
|
178
|
+
: "",
|
|
179
|
+
]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join("\n");
|
|
182
|
+
await refreshStatus(slug);
|
|
183
|
+
const r = openSession("video", slug, dir, title, prompt);
|
|
184
|
+
return { ...r, slug };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* a new visual style: a link, photos sent through the upload (their paths in .uploads/), or both. The name is
|
|
188
|
+
optional: the form does not ask for one, so it comes from the link or the photos (the Omni agent still names it) */
|
|
189
|
+
export type ReferenceRequest = { name?: string; source?: string; images?: string[]; note?: string };
|
|
190
|
+
|
|
191
|
+
const VIDEO_FILE = /\.(mp4|mov|webm|m4v|mkv|avi|mpe?g)$/i;
|
|
192
|
+
const IMAGE_FILE = /\.(png|jpe?g|webp|heic|avif)$/i;
|
|
193
|
+
const MAX_STYLE_PHOTOS = 12;
|
|
194
|
+
const UPLOADS_DIR = join(ROOT, ".uploads");
|
|
195
|
+
|
|
196
|
+
/* What the person pastes in the Source field: a link, or a path as Finder and the
|
|
197
|
+
terminal hand it over (quoted, file://, escaped space, ~/, relative to the
|
|
198
|
+
project dir, which is where `reference.mjs` resolves from). */
|
|
199
|
+
export function normalizeSource(raw: string): string {
|
|
200
|
+
let f = raw.trim();
|
|
201
|
+
const quoted = /^(["'])([\s\S]*)\1$/.exec(f);
|
|
202
|
+
if (quoted) f = quoted[2].trim();
|
|
203
|
+
if (/^https?:\/\//i.test(f)) return f;
|
|
204
|
+
if (/^file:\/\//i.test(f)) {
|
|
205
|
+
try {
|
|
206
|
+
return decodeURIComponent(new URL(f).pathname);
|
|
207
|
+
} catch {
|
|
208
|
+
return f;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
f = f.replace(/\\(.)/g, "$1");
|
|
212
|
+
if (f === "~" || f.startsWith("~/")) return join(homedir(), f.slice(1));
|
|
213
|
+
return resolve(ROOT, f);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Video or photo sent from the UI (upload) to serve as a visual style's source.
|
|
217
|
+
It stays in .uploads/ at the root; `measure` (video) or createReference (photos) copies it to the style's dir. */
|
|
218
|
+
export async function storeUpload(
|
|
219
|
+
rawName: string,
|
|
220
|
+
body: ReadableStream | null,
|
|
221
|
+
): Promise<ApiResult & { path?: string }> {
|
|
222
|
+
let original = "";
|
|
223
|
+
try {
|
|
224
|
+
original = decodeURIComponent(rawName);
|
|
225
|
+
} catch {
|
|
226
|
+
original = rawName;
|
|
227
|
+
}
|
|
228
|
+
const name = basename(original)
|
|
229
|
+
.normalize("NFD")
|
|
230
|
+
.replace(/[̀-ͯ]/g, "")
|
|
231
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
232
|
+
.replace(/^[.-]+/, "");
|
|
233
|
+
if (!VIDEO_FILE.test(name) && !IMAGE_FILE.test(name))
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
error: "o arquivo precisa ser uma foto (png, jpg, webp) ou um vídeo (mp4, mov, webm, m4v, mkv, avi)",
|
|
237
|
+
};
|
|
238
|
+
if (!body) return { ok: false, error: "arquivo vazio" };
|
|
239
|
+
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
240
|
+
const dest = join(UPLOADS_DIR, `${Date.now()}-${name}`);
|
|
241
|
+
try {
|
|
242
|
+
const bytes = await Bun.write(dest, new Response(body));
|
|
243
|
+
if (!bytes) throw new Error("arquivo vazio");
|
|
244
|
+
} catch (e: any) {
|
|
245
|
+
rmSync(dest, { force: true });
|
|
246
|
+
return { ok: false, error: `não deu para guardar o upload: ${e?.message ?? e}` };
|
|
247
|
+
}
|
|
248
|
+
return { ok: true, path: dest };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* the name the form does not ask for: from the link (site and id) or the file, else the date; never one that
|
|
252
|
+
is taken */
|
|
253
|
+
function autoStyleName(source: string): string {
|
|
254
|
+
let base = "";
|
|
255
|
+
if (/^https?:\/\//.test(source)) {
|
|
256
|
+
try {
|
|
257
|
+
const u = new URL(source);
|
|
258
|
+
const site = u.hostname.replace(/^www\./, "").split(".")[0];
|
|
259
|
+
base = `${site}-${u.searchParams.get("v") ?? u.pathname.split("/").filter(Boolean).pop() ?? ""}`;
|
|
260
|
+
} catch {
|
|
261
|
+
/* not a URL after all: the date below */
|
|
262
|
+
}
|
|
263
|
+
} else if (source) base = basename(source, extname(source));
|
|
264
|
+
let slug = base
|
|
265
|
+
.normalize("NFD")
|
|
266
|
+
.replace(/[̀-ͯ]/g, "")
|
|
267
|
+
.toLowerCase()
|
|
268
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
269
|
+
.replace(/^-+|-+$/g, "")
|
|
270
|
+
.slice(0, 40)
|
|
271
|
+
.replace(/-+$/, "");
|
|
272
|
+
if (!slug) {
|
|
273
|
+
const d = new Date().toISOString();
|
|
274
|
+
slug = `estilo-${d.slice(2, 10).replace(/-/g, "")}-${d.slice(11, 16).replace(":", "")}`;
|
|
275
|
+
}
|
|
276
|
+
let name = slug;
|
|
277
|
+
for (let n = 2; referenceDir(name); n++) name = `${slug}-${n}`;
|
|
278
|
+
return name;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function createReference(p: ReferenceRequest): ApiResult & { name?: string } {
|
|
282
|
+
const rawSource = String(p.source ?? "").trim();
|
|
283
|
+
const source = rawSource ? normalizeSource(rawSource) : "";
|
|
284
|
+
const images = (p.images ?? []).filter(Boolean);
|
|
285
|
+
const asked = String(p.name ?? "").trim();
|
|
286
|
+
if (!source && !images.length)
|
|
287
|
+
return { ok: false, error: "mande um link (Instagram, YouTube…) ou ao menos uma foto do estilo visual" };
|
|
288
|
+
if (images.length > MAX_STYLE_PHOTOS)
|
|
289
|
+
return { ok: false, error: `no máximo ${MAX_STYLE_PHOTOS} fotos por estilo visual` };
|
|
290
|
+
if (asked && !REFERENCE_NAME_RE.test(asked))
|
|
291
|
+
return { ok: false, error: "nome: minúsculas, números e hífens (ex.: apura-paraguai)" };
|
|
292
|
+
if (asked && referenceDir(asked))
|
|
293
|
+
return { ok: false, error: `já existe o estilo visual “${asked}”: abra ele e peça o ajuste por lá` };
|
|
294
|
+
if (source && !/^https?:\/\//.test(source)) {
|
|
295
|
+
let st: ReturnType<typeof statSync>;
|
|
296
|
+
try {
|
|
297
|
+
st = statSync(source);
|
|
298
|
+
} catch {
|
|
299
|
+
return { ok: false, error: `não achei o arquivo ${source}` };
|
|
300
|
+
}
|
|
301
|
+
if (!st.isFile()) return { ok: false, error: `${source} não é um arquivo (aponte para o vídeo, não para a pasta)` };
|
|
302
|
+
if (!VIDEO_FILE.test(source))
|
|
303
|
+
return { ok: false, error: `${basename(source)} não parece um vídeo (mp4, mov, webm, m4v, mkv, avi)` };
|
|
304
|
+
}
|
|
305
|
+
/* photos only come from the upload: nothing else on the disk is copied into the library */
|
|
306
|
+
for (const img of images) {
|
|
307
|
+
const abs = resolve(img);
|
|
308
|
+
if (!abs.startsWith(UPLOADS_DIR + sep) || !IMAGE_FILE.test(abs) || !existsSync(abs))
|
|
309
|
+
return { ok: false, error: `a foto ${basename(img)} não foi enviada por aqui: envie de novo` };
|
|
310
|
+
}
|
|
311
|
+
const name = asked || autoStyleName(source || (images[0] ? basename(images[0]).replace(/^\d+-/, "") : ""));
|
|
312
|
+
const dir = join(LIBRARY_DIR, name);
|
|
313
|
+
const photos: string[] = [];
|
|
314
|
+
if (images.length) {
|
|
315
|
+
mkdirSync(join(dir, "images"), { recursive: true });
|
|
316
|
+
images.forEach((img, i) => {
|
|
317
|
+
const dest = join(dir, "images", `${String(i + 1).padStart(2, "0")}${extname(img).toLowerCase()}`);
|
|
318
|
+
copyFileSync(resolve(img), dest);
|
|
319
|
+
photos.push(dest);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/* the step by step (measure, Type, curves, guide, proof in code) is the av-reference skill */
|
|
323
|
+
const prompt = [
|
|
324
|
+
"/av-reference",
|
|
325
|
+
`Estilo visual novo para a biblioteca de estilos visuais (${LIBRARY_DIR}), pedido pela tela do studio.`,
|
|
326
|
+
`nome: ${name}`,
|
|
327
|
+
source ? `link: ${source}` : "",
|
|
328
|
+
photos.length ? `fotos (${photos.length}, já na pasta do estilo): ${photos.join(", ")}` : "",
|
|
329
|
+
source
|
|
330
|
+
? `Comece por: node tools/reference.mjs measure ${JSON.stringify(source)} ${name}`
|
|
331
|
+
: "Não há vídeo: não rode `measure`. Parta das fotos (seção “From photos” da skill): leia cada uma com Read, crie sheets/ e o GUIDE.md a partir de templates/reference.md, e diga no Tipo que ritmo, cortes e curvas não foram medidos.",
|
|
332
|
+
source && photos.length
|
|
333
|
+
? "As fotos entram junto: leia cada uma com Read e use no guia o que elas mostram do estilo (cor, tipografia, composição)."
|
|
334
|
+
: "",
|
|
335
|
+
p.note?.trim() ? `O que o usuário quer aprender com ele: ${p.note.trim()}` : "",
|
|
336
|
+
]
|
|
337
|
+
.filter(s => s !== "")
|
|
338
|
+
.join("\n");
|
|
339
|
+
const r = openSession("reference", name, dir, name, prompt);
|
|
340
|
+
/* nothing started: the photos copied for it do not stay as a style without a guide */
|
|
341
|
+
if (!r.ok && photos.length) rmSync(dir, { recursive: true, force: true });
|
|
342
|
+
return { ...r, name };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* A message from the UI for a video or a reference that already exists: resumes the
|
|
346
|
+
studio's last session, or opens a new one from the files. */
|
|
347
|
+
export function chatWith(type: SessionType, target: string, message: string): ApiResult {
|
|
348
|
+
const text = String(message ?? "").trim();
|
|
349
|
+
const last = latestSessionFor(type, target);
|
|
350
|
+
if (last) return replyToSession(last.id, text || "Continue de onde parou.", text ? "user" : "resume");
|
|
351
|
+
/* the editor's edit chat: only the editor and render of this video (skill av-editor) */
|
|
352
|
+
if (type === "edit") {
|
|
353
|
+
const dir = videoDirs().get(target);
|
|
354
|
+
if (!dir) return { ok: false, error: "vídeo não encontrado" };
|
|
355
|
+
if (!text) return { ok: false, error: "diga o que editar" };
|
|
356
|
+
const prompt = [
|
|
357
|
+
"/av-editor",
|
|
358
|
+
`Vídeo videos/${target}, aberto no editor do studio. Comece por \`node tools/editor.mjs show videos/${target}\`.`,
|
|
359
|
+
`Pedido feito pelo chat do editor: ${text}`,
|
|
360
|
+
].join("\n");
|
|
361
|
+
return openSession("edit", target, dir, `${target} · editor`, prompt, "user");
|
|
362
|
+
}
|
|
363
|
+
if (type === "video") {
|
|
364
|
+
const dir = videoDirs().get(target);
|
|
365
|
+
if (!dir) return { ok: false, error: "vídeo não encontrado" };
|
|
366
|
+
const prompt = [
|
|
367
|
+
"/omnivideo",
|
|
368
|
+
`Retome o vídeo videos/${target}: \`${PYTHON} tools/omnivideo.py status videos/${target}\` diz a próxima etapa. Se o roteiro espera aprovação, rode \`node tools/approval.mjs wait videos/${target} --script\`; se o roteiro de motion espera aprovação das cenas, rode \`node tools/approval.mjs wait videos/${target}\`.`,
|
|
369
|
+
text ? `Pedido feito pela tela: ${text}` : "Continue de onde ele parou.",
|
|
370
|
+
].join("\n");
|
|
371
|
+
return openSession("video", target, dir, target, prompt, "resume");
|
|
372
|
+
}
|
|
373
|
+
const dir = referenceDir(target);
|
|
374
|
+
if (!dir) return { ok: false, error: "estilo visual não encontrado" };
|
|
375
|
+
if (!text) return { ok: false, error: "diga o que fazer no estilo visual" };
|
|
376
|
+
const prompt = `/av-reference\nEstilo visual ${target}, em ${dir} (guia, measurements.json, sheets, proof; tools/reference.mjs mede, extrai quadros e compara a prova).\nPedido feito pela tela: ${text}`;
|
|
377
|
+
return openSession("reference", target, dir, target, prompt, "user");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Swapping the visual style of a video already started. Claude redoes it
|
|
381
|
+
(skill omnivideo, Retomar ou refazer, "estilo visual"): motion.md
|
|
382
|
+
and the scenes.json `references` belong to stage 2. The UI only sends the
|
|
383
|
+
request to the video session, which resumes (or opens). Empty: Claude picks
|
|
384
|
+
another one from the library. */
|
|
385
|
+
export async function swapReference(slug: string, reference: string, note: string): Promise<ApiResult> {
|
|
386
|
+
const dir = videoDirs().get(slug);
|
|
387
|
+
if (!dir) return { ok: false, error: "vídeo não encontrado" };
|
|
388
|
+
const scenes = readJson<any>(join(dir, "scenes.json"));
|
|
389
|
+
if (!scenes || !existsSync(join(dir, "motion.md"))) {
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
error: "o roteiro de motion ainda não foi escrito: o estilo visual pode ir no pedido, e a etapa 2 usa ele",
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const next = reference.trim();
|
|
396
|
+
if (next && !referenceDir(next)) return { ok: false, error: `o estilo visual “${next}” não existe` };
|
|
397
|
+
const current: string[] = scenes.references ?? [];
|
|
398
|
+
const obs = note.trim();
|
|
399
|
+
if (next && current.length === 1 && current[0] === next && !obs)
|
|
400
|
+
return { ok: false, error: "este já é o estilo visual do vídeo: diga no comentário o que mudar nele" };
|
|
401
|
+
const request = [
|
|
402
|
+
next
|
|
403
|
+
? `Troque o estilo visual deste vídeo para «${next}»${current.length ? `, no lugar de ${current.map(r => `«${r}»`).join(" e ")}` : ""}.`
|
|
404
|
+
: `Troque o estilo visual deste vídeo: escolha outro dos estilos visuais${current.length ? ` no lugar de ${current.map(r => `«${r}»`).join(" e ")}` : ""}, pela essência de cada uma.`,
|
|
405
|
+
obs ? `O que o usuário quer no estilo: ${obs}` : "",
|
|
406
|
+
`Siga a skill omnivideo, Retomar ou refazer, linha "estilo visual": refaça só o que depende do estilo visual, na ordem dela.`,
|
|
407
|
+
]
|
|
408
|
+
.filter(Boolean)
|
|
409
|
+
.join("\n");
|
|
410
|
+
const r = chatWith("video", slug, request);
|
|
411
|
+
if (!r.ok) return r;
|
|
412
|
+
/* the swap is recorded right away, without waiting for Claude to get there, so the UI can
|
|
413
|
+
show what is being redone */
|
|
414
|
+
const swap = { from: current, to: next || null, note: obs || null, requested_at: new Date().toISOString() };
|
|
415
|
+
mkdirSync(join(dir, "motion"), { recursive: true });
|
|
416
|
+
const file = join(dir, "motion", "reference-swap.json");
|
|
417
|
+
writeFileSync(`${file}.${process.pid}.tmp`, JSON.stringify(swap, null, 2) + "\n");
|
|
418
|
+
renameSync(`${file}.${process.pid}.tmp`, file);
|
|
419
|
+
await refreshStatus(slug);
|
|
420
|
+
return { ...r, output: `${r.output} · o motion volta a ser feito com o estilo visual novo` };
|
|
421
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/* Live updates. Watches the video dirs and .queue/ and pushes over SSE only
|
|
2
|
+
the notice of what changed; the UI rereads what it needs. Queue process
|
|
3
|
+
liveness and services are checked from time to time, only with someone on the UI. */
|
|
4
|
+
|
|
5
|
+
import { watch, existsSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { Server } from "bun";
|
|
8
|
+
import { QUEUE_DIR, ROOT } from "./root";
|
|
9
|
+
import { videoRoots, refreshStatus, videoDirs } from "./videos";
|
|
10
|
+
import { watchDeadline } from "./approval";
|
|
11
|
+
import { getQueue } from "./queue";
|
|
12
|
+
import { getServices } from "./services";
|
|
13
|
+
import { SESSIONS_DIR, listSessions, onSessionsChange } from "./sessions";
|
|
14
|
+
import { LIBRARY_DIR } from "./references";
|
|
15
|
+
|
|
16
|
+
type Client = (event: string, data: unknown) => void;
|
|
17
|
+
const clients = new Set<Client>();
|
|
18
|
+
|
|
19
|
+
function emit(event: string, data: unknown) {
|
|
20
|
+
for (const c of clients) c(event, data);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Omni's settings changed in the admin panel: every open page rereads them (and follows them) */
|
|
24
|
+
export function agentChanged() {
|
|
25
|
+
emit("agent", {});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* --- videos --------------------------------------------------------------------
|
|
29
|
+
A burst of changes (a Premiere export writes the file for minutes) becomes
|
|
30
|
+
one read every 3 s at most, not one per event. */
|
|
31
|
+
const pending = new Map<string, { timer: ReturnType<typeof setTimeout>; since: number }>();
|
|
32
|
+
function videoChanged(slug: string) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const p = pending.get(slug);
|
|
35
|
+
if (p) clearTimeout(p.timer);
|
|
36
|
+
const since = p?.since ?? now;
|
|
37
|
+
const timer = setTimeout(
|
|
38
|
+
async () => {
|
|
39
|
+
pending.delete(slug);
|
|
40
|
+
await refreshStatus(slug);
|
|
41
|
+
emit("video", { slug });
|
|
42
|
+
/* the script or scenes countdown restarted, paused or ended: redo the alarms */
|
|
43
|
+
const dir = videoDirs().get(slug);
|
|
44
|
+
if (dir) watchDeadline(slug, dir);
|
|
45
|
+
},
|
|
46
|
+
now - since > 3000 ? 0 : 400,
|
|
47
|
+
);
|
|
48
|
+
pending.set(slug, { timer, since });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* --- queue ------------------------------------------------------------------------ */
|
|
52
|
+
let lastQueue = "";
|
|
53
|
+
function checkQueue() {
|
|
54
|
+
const q = getQueue();
|
|
55
|
+
const text = JSON.stringify(q);
|
|
56
|
+
if (text !== lastQueue) {
|
|
57
|
+
lastQueue = text;
|
|
58
|
+
emit("queue", q);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let queueScheduled: ReturnType<typeof setTimeout> | null = null;
|
|
62
|
+
function scheduleQueueCheck() {
|
|
63
|
+
if (queueScheduled) return;
|
|
64
|
+
queueScheduled = setTimeout(() => {
|
|
65
|
+
queueScheduled = null;
|
|
66
|
+
checkQueue();
|
|
67
|
+
}, 120);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* --- Claude sessions and references ---------------------------------------------
|
|
71
|
+
A turn's jsonl grows with every message: the list goes out at most every
|
|
72
|
+
600 ms. Process liveness is checked from time to time, like the queue's. */
|
|
73
|
+
let lastSessions = "";
|
|
74
|
+
function checkSessions() {
|
|
75
|
+
const s = listSessions();
|
|
76
|
+
const text = JSON.stringify(s);
|
|
77
|
+
if (text !== lastSessions) {
|
|
78
|
+
lastSessions = text;
|
|
79
|
+
emit("sessions", s);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let sessionsScheduled: ReturnType<typeof setTimeout> | null = null;
|
|
83
|
+
function scheduleSessionsCheck() {
|
|
84
|
+
if (sessionsScheduled) return;
|
|
85
|
+
sessionsScheduled = setTimeout(() => {
|
|
86
|
+
sessionsScheduled = null;
|
|
87
|
+
checkSessions();
|
|
88
|
+
}, 600);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const changedRefs = new Set<string>();
|
|
92
|
+
let refsScheduled: ReturnType<typeof setTimeout> | null = null;
|
|
93
|
+
function referenceChanged(name: string) {
|
|
94
|
+
changedRefs.add(name);
|
|
95
|
+
if (refsScheduled) clearTimeout(refsScheduled);
|
|
96
|
+
refsScheduled = setTimeout(() => {
|
|
97
|
+
refsScheduled = null;
|
|
98
|
+
for (const n of changedRefs) emit("reference", { name: n });
|
|
99
|
+
changedRefs.clear();
|
|
100
|
+
}, 500);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* --- omnichat -----------------------------------------------------------------
|
|
104
|
+
The rule and record of a video (videos/<slug>/), of its carousel (videos/<slug>/carousel/) or of an outside post
|
|
105
|
+
(.omnichat/posts/<code>/), publication.json (the Reel went up) and the account pause:
|
|
106
|
+
the omnichat page rereads. The agent's lock and log, which change every minute, don't. */
|
|
107
|
+
const OMNICHAT_FILES = /^(omnichat|omnichat-record|publication)\.json$/;
|
|
108
|
+
const autoPending = new Map<string, ReturnType<typeof setTimeout>>();
|
|
109
|
+
function omnichatChanged(data: { slug?: string; code?: string }) {
|
|
110
|
+
const key = data.slug ? `v:${data.slug}` : data.code ? `p:${data.code}` : "account";
|
|
111
|
+
clearTimeout(autoPending.get(key));
|
|
112
|
+
autoPending.set(
|
|
113
|
+
key,
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
autoPending.delete(key);
|
|
116
|
+
emit("omnichat", data);
|
|
117
|
+
}, 300),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* --- analytics -------------------------------------------------------------------
|
|
122
|
+
A Reel's metrics (videos/<slug>/metrics.json, which the agent rewrites on every
|
|
123
|
+
read), its analysis, the account snapshot (.metrics/account.json) and channel/
|
|
124
|
+
(playbook and analyses): the Analytics page rereads. The agent's lock and log don't. */
|
|
125
|
+
const ANALYTICS_FILES = /^(metrics\.json|analysis\.md|publication\.json)$/;
|
|
126
|
+
let analyticsScheduled: ReturnType<typeof setTimeout> | null = null;
|
|
127
|
+
function analyticsChanged() {
|
|
128
|
+
if (analyticsScheduled) clearTimeout(analyticsScheduled);
|
|
129
|
+
analyticsScheduled = setTimeout(() => {
|
|
130
|
+
analyticsScheduled = null;
|
|
131
|
+
emit("analytics", {});
|
|
132
|
+
}, 500);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function startWatching() {
|
|
136
|
+
for (const root of videoRoots) {
|
|
137
|
+
if (!existsSync(root)) continue;
|
|
138
|
+
watch(root, { recursive: true }, (_, name) => {
|
|
139
|
+
const path = String(name ?? "");
|
|
140
|
+
const slug = path.split("/")[0];
|
|
141
|
+
if (!slug || slug.startsWith(".") || path.endsWith(".DS_Store")) return;
|
|
142
|
+
videoChanged(slug);
|
|
143
|
+
const rest = path.slice(slug.length + 1);
|
|
144
|
+
/* the content's carousel has its own rule, in <slug>/carousel/ */
|
|
145
|
+
if (OMNICHAT_FILES.test(rest) || OMNICHAT_FILES.test(rest.replace(/^carousel\//, ""))) omnichatChanged({ slug });
|
|
146
|
+
if (ANALYTICS_FILES.test(path.slice(slug.length + 1))) analyticsChanged();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const OMNICHAT_DIR = join(ROOT, ".omnichat");
|
|
150
|
+
mkdirSync(OMNICHAT_DIR, { recursive: true });
|
|
151
|
+
watch(OMNICHAT_DIR, { recursive: true }, (_, name) => {
|
|
152
|
+
const path = String(name ?? "");
|
|
153
|
+
const [dir, code, file] = path.split("/");
|
|
154
|
+
if (dir === "posts" && code && (!file || OMNICHAT_FILES.test(file))) omnichatChanged({ code });
|
|
155
|
+
else if (path === "pause.json") omnichatChanged({});
|
|
156
|
+
});
|
|
157
|
+
for (const dir of [join(ROOT, ".metrics"), join(ROOT, "channel")]) {
|
|
158
|
+
mkdirSync(dir, { recursive: true });
|
|
159
|
+
watch(dir, { recursive: true }, (_, name) => {
|
|
160
|
+
const path = String(name ?? "");
|
|
161
|
+
if (path.endsWith(".md") || path === "account.json") analyticsChanged();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
mkdirSync(QUEUE_DIR, { recursive: true });
|
|
165
|
+
watch(QUEUE_DIR, scheduleQueueCheck);
|
|
166
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
167
|
+
watch(SESSIONS_DIR, { recursive: true }, scheduleSessionsCheck);
|
|
168
|
+
onSessionsChange(scheduleSessionsCheck);
|
|
169
|
+
if (existsSync(LIBRARY_DIR)) {
|
|
170
|
+
watch(LIBRARY_DIR, { recursive: true }, (_, name) => {
|
|
171
|
+
const ref = String(name ?? "").split("/")[0];
|
|
172
|
+
if (ref && !ref.startsWith(".") && !String(name).endsWith(".DS_Store")) referenceChanged(ref);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
setInterval(() => {
|
|
176
|
+
if (clients.size) checkQueue();
|
|
177
|
+
}, 2000);
|
|
178
|
+
setInterval(() => {
|
|
179
|
+
if (clients.size) checkSessions();
|
|
180
|
+
}, 3000);
|
|
181
|
+
setInterval(async () => {
|
|
182
|
+
if (clients.size) emit("services", await getServices());
|
|
183
|
+
}, 4000);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function eventsStream(req: Request, server: Server<unknown>): Response {
|
|
187
|
+
/* the SSE stays open; Bun's default drops an idle connection after 10 s */
|
|
188
|
+
server.timeout(req, 0);
|
|
189
|
+
const encoder = new TextEncoder();
|
|
190
|
+
let client: Client = () => {};
|
|
191
|
+
let ping: ReturnType<typeof setInterval>;
|
|
192
|
+
const leave = () => {
|
|
193
|
+
clients.delete(client);
|
|
194
|
+
clearInterval(ping);
|
|
195
|
+
};
|
|
196
|
+
const body = new ReadableStream({
|
|
197
|
+
start(ctrl) {
|
|
198
|
+
client = (event, data) => {
|
|
199
|
+
try {
|
|
200
|
+
ctrl.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
201
|
+
} catch {
|
|
202
|
+
leave();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
clients.add(client);
|
|
206
|
+
client("queue", getQueue());
|
|
207
|
+
client("sessions", listSessions());
|
|
208
|
+
getServices().then(s => client("services", s));
|
|
209
|
+
ping = setInterval(() => client("ping", Date.now()), 15_000);
|
|
210
|
+
req.signal.addEventListener("abort", leave);
|
|
211
|
+
},
|
|
212
|
+
cancel: leave,
|
|
213
|
+
});
|
|
214
|
+
return new Response(body, { headers: { "content-type": "text/event-stream", "cache-control": "no-cache" } });
|
|
215
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Which file a media URL points to: a video's file (/media), a reference's (/ref), a stored voice
|
|
2
|
+
sample or original recording. Null when the path is not allowed or climbs out of its dir. */
|
|
3
|
+
|
|
4
|
+
import { resolve, sep } from "node:path";
|
|
5
|
+
import { videoDirs } from "./videos";
|
|
6
|
+
import { referenceFile } from "./references";
|
|
7
|
+
import { CLONES_DIR } from "./clone";
|
|
8
|
+
import { SAMPLES_DIR } from "./settings";
|
|
9
|
+
|
|
10
|
+
/* what a video's or reference's dir may serve */
|
|
11
|
+
const ALLOWED_MEDIA = /\.(png|jpe?g|webp|gif|mp4|mov|webm|wav|mp3|m4a|srt|json|md|txt)$/i;
|
|
12
|
+
|
|
13
|
+
/* /media/<slug>/<path> */
|
|
14
|
+
export function videoMediaFile(slug: string, rel: string): string | null {
|
|
15
|
+
const dir = videoDirs().get(slug);
|
|
16
|
+
if (!dir || !ALLOWED_MEDIA.test(rel)) return null;
|
|
17
|
+
const abs = resolve(dir, rel);
|
|
18
|
+
return abs.startsWith(dir + sep) ? abs : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* /ref/<name>/<path>: media plus the reference's CSS */
|
|
22
|
+
export function referenceMediaFile(name: string, rel: string): string | null {
|
|
23
|
+
if (!ALLOWED_MEDIA.test(rel) && !/\.css$/i.test(rel)) return null;
|
|
24
|
+
return referenceFile(name, rel);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* /api/voice-original/<clone>: the recording a clone was made from */
|
|
28
|
+
export const originalWavFile = (clone: string) =>
|
|
29
|
+
/^[a-z0-9-]+$/.test(clone) ? resolve(CLONES_DIR, clone, "original.wav") : null;
|
|
30
|
+
|
|
31
|
+
/* /api/voice-sample/<file>: a profile's generated sample */
|
|
32
|
+
export const sampleWavFile = (file: string) => (/^[a-z0-9-]+\.wav$/.test(file) ? resolve(SAMPLES_DIR, file) : null);
|