@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,134 @@
|
|
|
1
|
+
/* Approval from the UI: of the motion script scenes and of the content script.
|
|
2
|
+
|
|
3
|
+
The rules live in tools/approval.mjs, which the session also runs: it stays
|
|
4
|
+
in `wait` while the deadline runs (and while the countdown is paused for
|
|
5
|
+
a short while) and moves on by itself when the round ends. If it left earlier (the
|
|
6
|
+
pause went past the deadline, or the video is the chat's), the video session
|
|
7
|
+
resumes from here: when the decision ends on the UI or when the deadline expires. */
|
|
8
|
+
|
|
9
|
+
import { relative } from "node:path";
|
|
10
|
+
import { ROOT } from "./root";
|
|
11
|
+
import { latestSessionFor, replyToSession } from "./sessions";
|
|
12
|
+
import { read, summarize, decide, approveAll, pause, resume, mark, edit } from "../../../tools/approval.mjs";
|
|
13
|
+
|
|
14
|
+
type ApprovalKind = "scenes" | "script";
|
|
15
|
+
|
|
16
|
+
export const scenesApprovalOf = (dir: string) => summarize(read(dir));
|
|
17
|
+
export const scriptApprovalOf = (dir: string) => summarize(read(dir, "script"));
|
|
18
|
+
|
|
19
|
+
export const SCENE_ACTIONS = ["decide", "approve-all", "pause", "resume"] as const;
|
|
20
|
+
type SceneAction = (typeof SCENE_ACTIONS)[number];
|
|
21
|
+
type DecisionBody = { scene?: string; decision?: string; comment?: string };
|
|
22
|
+
|
|
23
|
+
export function decideScenes(dir: string, action: SceneAction, b: DecisionBody) {
|
|
24
|
+
const r =
|
|
25
|
+
action === "decide"
|
|
26
|
+
? decide(dir, String(b.scene ?? ""), String(b.decision ?? ""), String(b.comment ?? ""))
|
|
27
|
+
: action === "approve-all"
|
|
28
|
+
? approveAll(dir)
|
|
29
|
+
: action === "pause"
|
|
30
|
+
? pause(dir)
|
|
31
|
+
: resume(dir);
|
|
32
|
+
if (!r.ok) return r;
|
|
33
|
+
const e = summarize(r.approval);
|
|
34
|
+
const output =
|
|
35
|
+
action === "pause"
|
|
36
|
+
? "contagem pausada enquanto você edita"
|
|
37
|
+
: action === "resume"
|
|
38
|
+
? "a contagem voltou de onde parou"
|
|
39
|
+
: e.status === "pending"
|
|
40
|
+
? `gravado · ${e.pending_count} cena(s) ainda esperando`
|
|
41
|
+
: e.status === "change_requested"
|
|
42
|
+
? `pronto · ${e.change_requests.length} cena(s) pedem alteração`
|
|
43
|
+
: "pronto · todas aprovadas";
|
|
44
|
+
return { ok: true, output };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* the script is a single item: approve, request a change or edit the text (which approves it) */
|
|
48
|
+
export const SCRIPT_ACTIONS = ["decide", "pause", "resume", "edit"] as const;
|
|
49
|
+
type ScriptAction = (typeof SCRIPT_ACTIONS)[number];
|
|
50
|
+
|
|
51
|
+
export function decideScript(dir: string, action: ScriptAction, b: DecisionBody & { text?: string }) {
|
|
52
|
+
const r =
|
|
53
|
+
action === "decide"
|
|
54
|
+
? decide(dir, "script", String(b.decision ?? ""), String(b.comment ?? ""), "script")
|
|
55
|
+
: action === "edit"
|
|
56
|
+
? edit(dir, String(b.text ?? ""))
|
|
57
|
+
: action === "pause"
|
|
58
|
+
? pause(dir, "script")
|
|
59
|
+
: resume(dir, "script");
|
|
60
|
+
if (!r.ok) return r;
|
|
61
|
+
const e = summarize(r.approval);
|
|
62
|
+
const output =
|
|
63
|
+
action === "pause"
|
|
64
|
+
? "contagem pausada enquanto você edita"
|
|
65
|
+
: action === "resume"
|
|
66
|
+
? "a contagem voltou de onde parou"
|
|
67
|
+
: action === "edit"
|
|
68
|
+
? "pronto · sua edição gravada no script.md e aprovada"
|
|
69
|
+
: e.status === "change_requested"
|
|
70
|
+
? "pronto · o roteiro pede alteração"
|
|
71
|
+
: "pronto · roteiro aprovado";
|
|
72
|
+
return { ok: true, output };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* what the resumed session reads, per round */
|
|
76
|
+
const RESUME_PROMPTS: Record<ApprovalKind, { origin: string; text: (rel: string) => string }> = {
|
|
77
|
+
scenes: {
|
|
78
|
+
origin: "scenes-approval",
|
|
79
|
+
text: rel =>
|
|
80
|
+
`A aprovação das cenas do roteiro de motion terminou na tela. Rode \`node tools/approval.mjs wait ${rel}\` para ler o resultado e siga a skill omnivideo (Aprovação das cenas).`,
|
|
81
|
+
},
|
|
82
|
+
script: {
|
|
83
|
+
origin: "script-approval",
|
|
84
|
+
text: rel =>
|
|
85
|
+
`A aprovação do roteiro terminou na tela. Rode \`node tools/approval.mjs wait ${rel} --script\` para ler o resultado e siga a skill omnivideo (Aprovação do roteiro).`,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/* A finished round Claude hasn't read yet: resumes the video's stopped
|
|
90
|
+
session. A running session is in `wait` (which reads by itself) or finishing the
|
|
91
|
+
turn; in that case the end of the turn calls again. Without `kind`, checks both
|
|
92
|
+
rounds. */
|
|
93
|
+
export function resumeAfterApproval(slug: string, dir: string, kind?: ApprovalKind): string {
|
|
94
|
+
if (!kind) return resumeAfterApproval(slug, dir, "script") || resumeAfterApproval(slug, dir, "scenes");
|
|
95
|
+
const a = read(dir, kind);
|
|
96
|
+
if (!a?.completed_at || a.delivered_at || a.resumed_at) return "";
|
|
97
|
+
/* a forgotten round from before (studio off, video driven by the chat) doesn't wake an old session */
|
|
98
|
+
if (Date.now() - Date.parse(a.completed_at) > 3_600_000) return "";
|
|
99
|
+
const s = latestSessionFor("video", slug);
|
|
100
|
+
if (!s || s.state === "running") return "";
|
|
101
|
+
const rel = relative(ROOT, dir).startsWith("..") ? dir : relative(ROOT, dir);
|
|
102
|
+
const r = replyToSession(s.id, RESUME_PROMPTS[kind].text(rel), RESUME_PROMPTS[kind].origin);
|
|
103
|
+
if (r.ok) mark(dir, "resumed_at", kind);
|
|
104
|
+
return r.ok ? " · o Claude retomou a sessão deste vídeo" : ` · o Claude não retomou: ${r.error}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* One alarm per round with the countdown running: at the deadline, reading applies the
|
|
108
|
+
auto approval and a session that isn't in `wait` resumes. Redone on
|
|
109
|
+
every change in the video dir (the file is the source; the timer only wakes up).
|
|
110
|
+
Scenes and script have separate alarms: one doesn't cancel the other. */
|
|
111
|
+
const alarms = new Map<string, ReturnType<typeof setTimeout>>();
|
|
112
|
+
export function watchDeadline(slug: string, dir: string, kind?: ApprovalKind) {
|
|
113
|
+
if (!kind) {
|
|
114
|
+
watchDeadline(slug, dir, "script");
|
|
115
|
+
watchDeadline(slug, dir, "scenes");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const key = `${slug}#${kind}`;
|
|
119
|
+
clearTimeout(alarms.get(key));
|
|
120
|
+
alarms.delete(key);
|
|
121
|
+
const a = read(dir, kind);
|
|
122
|
+
if (!a || a.completed_at || a.mode !== "auto") {
|
|
123
|
+
if (a?.completed_at) resumeAfterApproval(slug, dir, kind);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const left = Math.max(0, Date.parse(a.deadline) - Date.now()) + 500;
|
|
127
|
+
alarms.set(
|
|
128
|
+
key,
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
alarms.delete(key);
|
|
131
|
+
watchDeadline(slug, dir, kind);
|
|
132
|
+
}, left),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Archiving a video takes it off the work list without deleting anything: the dir
|
|
2
|
+
stays as it is, with an archived.json ({ at }) next to scenes.json. Unarchiving
|
|
3
|
+
deletes that file. No pipeline tool reads it; it belongs to the UI only. */
|
|
4
|
+
|
|
5
|
+
import { writeFileSync, renameSync, unlinkSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { readJson } from "./root";
|
|
8
|
+
import { getQueue } from "./queue";
|
|
9
|
+
|
|
10
|
+
type Result = { ok: true } | { ok: false; error: string };
|
|
11
|
+
|
|
12
|
+
export function archiveVideo(dir: string, archived: boolean): Result {
|
|
13
|
+
const file = join(dir, "archived.json");
|
|
14
|
+
if (!archived) {
|
|
15
|
+
try {
|
|
16
|
+
unlinkSync(file);
|
|
17
|
+
} catch {
|
|
18
|
+
/* was not archived */
|
|
19
|
+
}
|
|
20
|
+
return { ok: true };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* the launchd scheduler publishes with the studio open or not: a hidden video must
|
|
24
|
+
not go up unseen. Whoever archives cancels the schedule first. */
|
|
25
|
+
const sch = readJson(join(dir, "publication.json"))?.schedule;
|
|
26
|
+
if (sch && (sch.state === "scheduled" || sch.state === "publishing")) {
|
|
27
|
+
return { ok: false, error: "Este vídeo tem uma publicação agendada: cancele a agenda antes de arquivar." };
|
|
28
|
+
}
|
|
29
|
+
const q = getQueue();
|
|
30
|
+
if (q.running.some(s => s.dir === dir) || q.waiting.some(s => s.dir === dir)) {
|
|
31
|
+
return { ok: false, error: "Este vídeo tem uma tarefa rodando ou na fila: espere terminar para arquivar." };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tmp = `${file}.tmp`;
|
|
35
|
+
writeFileSync(tmp, JSON.stringify({ at: new Date().toISOString() }, null, 2) + "\n");
|
|
36
|
+
renameSync(tmp, file);
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/* Position and text of the burned-in captions, edited from the UI. The rules (safe area,
|
|
2
|
+
anchor, x/y, video → scene → caption) and the HTML live in tools/captions.mjs, the
|
|
3
|
+
same one assemble.mjs uses when rendering: the preview here is the frame that comes out. */
|
|
4
|
+
|
|
5
|
+
import { existsSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { readJson } from "./root";
|
|
8
|
+
import { loadVideo } from "../../../tools/timing.mjs";
|
|
9
|
+
import { keepPrevious } from "../../../tools/history.mjs";
|
|
10
|
+
import {
|
|
11
|
+
ANCHORS,
|
|
12
|
+
STYLE_KEYS,
|
|
13
|
+
STYLE_COLORS,
|
|
14
|
+
STYLES,
|
|
15
|
+
SIDES,
|
|
16
|
+
STYLE_LIMITS,
|
|
17
|
+
captionsHtml,
|
|
18
|
+
captionsOf,
|
|
19
|
+
maxChars,
|
|
20
|
+
styleDefaults,
|
|
21
|
+
positionOf,
|
|
22
|
+
safeAreas,
|
|
23
|
+
replaceSrtText,
|
|
24
|
+
} from "../../../tools/captions.mjs";
|
|
25
|
+
|
|
26
|
+
type Result = { ok: true; output: string } | { ok: false; error: string };
|
|
27
|
+
|
|
28
|
+
export function getCaptionsData(dir: string) {
|
|
29
|
+
const video = loadVideo(dir);
|
|
30
|
+
/* `texts` belongs to the text, not the position: editing a caption doesn't touch the position draft */
|
|
31
|
+
const { texts: _, ...position } = positionOf(video);
|
|
32
|
+
return {
|
|
33
|
+
format: video.format,
|
|
34
|
+
safes: safeAreas(video.format),
|
|
35
|
+
anchors: ANCHORS,
|
|
36
|
+
styles: STYLES,
|
|
37
|
+
defaults: styleDefaults(video.format),
|
|
38
|
+
limits: STYLE_LIMITS,
|
|
39
|
+
max_chars: maxChars(video.format),
|
|
40
|
+
position,
|
|
41
|
+
captions_in_motion: !!video.captions_in_motion,
|
|
42
|
+
scenes: video.scenes.map((c: any) => ({
|
|
43
|
+
id: c.id,
|
|
44
|
+
title: c.title ?? null,
|
|
45
|
+
start: c.start,
|
|
46
|
+
duration: c.duration,
|
|
47
|
+
mp4: !!c.mp4 && existsSync(join(dir, c.mp4)),
|
|
48
|
+
})),
|
|
49
|
+
captions: captionsOf(dir, video),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function captionsPage(dir: string): Response {
|
|
54
|
+
const { html } = captionsHtml(dir, loadVideo(dir));
|
|
55
|
+
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* the motion frame at t (video time), from the scene MP4, to sit behind the preview */
|
|
59
|
+
export async function frameAt(dir: string, t: number): Promise<Response> {
|
|
60
|
+
const video = loadVideo(dir);
|
|
61
|
+
const scene = video.scenes.find((c: any) => t >= c.start && t < c.start + c.duration) ?? video.scenes.at(-1);
|
|
62
|
+
const mp4 = scene?.mp4 ? join(dir, scene.mp4) : null;
|
|
63
|
+
if (!scene || !mp4 || !existsSync(mp4)) return new Response("sem MP4 da cena", { status: 404 });
|
|
64
|
+
const local = Math.min(Math.max(0, t - scene.start), Math.max(0, scene.duration - 1 / video.format.fps));
|
|
65
|
+
const p = Bun.spawn(
|
|
66
|
+
[
|
|
67
|
+
"ffmpeg",
|
|
68
|
+
"-loglevel",
|
|
69
|
+
"error",
|
|
70
|
+
"-ss",
|
|
71
|
+
local.toFixed(3),
|
|
72
|
+
"-i",
|
|
73
|
+
mp4,
|
|
74
|
+
"-frames:v",
|
|
75
|
+
"1",
|
|
76
|
+
"-vf",
|
|
77
|
+
"scale=720:-2",
|
|
78
|
+
"-f",
|
|
79
|
+
"image2pipe",
|
|
80
|
+
"-vcodec",
|
|
81
|
+
"mjpeg",
|
|
82
|
+
"-q:v",
|
|
83
|
+
"4",
|
|
84
|
+
"-",
|
|
85
|
+
],
|
|
86
|
+
{ stdout: "pipe", stderr: "ignore" },
|
|
87
|
+
);
|
|
88
|
+
const img = await new Response(p.stdout).arrayBuffer();
|
|
89
|
+
if ((await p.exited) !== 0 || !img.byteLength) return new Response("ffmpeg não tirou o quadro", { status: 500 });
|
|
90
|
+
return new Response(img, { headers: { "content-type": "image/jpeg", "cache-control": "no-store" } });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* one position level: only known keys, integer x/y inside the frame */
|
|
94
|
+
function sanitizePosition(
|
|
95
|
+
value: any,
|
|
96
|
+
w: number,
|
|
97
|
+
h: number,
|
|
98
|
+
keys: string[],
|
|
99
|
+
safes: Record<string, unknown>,
|
|
100
|
+
): { ok: true; v: Record<string, unknown> } | { ok: false; error: string } {
|
|
101
|
+
if (value == null || typeof value !== "object") return { ok: false, error: "posição inválida" };
|
|
102
|
+
const v: Record<string, unknown> = {};
|
|
103
|
+
for (const k of keys) {
|
|
104
|
+
const x = value[k];
|
|
105
|
+
if (x == null) continue;
|
|
106
|
+
if (k === "anchor" && !ANCHORS.includes(x)) return { ok: false, error: `âncora desconhecida: ${x}` };
|
|
107
|
+
if (k === "style" && !STYLES.includes(x)) return { ok: false, error: `estilo desconhecido: ${x}` };
|
|
108
|
+
if (k === "platform" && !(x in safes)) return { ok: false, error: `sem safe area para ${x} neste formato` };
|
|
109
|
+
if (STYLE_COLORS.includes(k)) {
|
|
110
|
+
if (typeof x !== "string" || !/^#[0-9a-fA-F]{6}$/.test(x))
|
|
111
|
+
return { ok: false, error: `${k}: cor inválida (use #rrggbb): ${x}` };
|
|
112
|
+
v[k] = x.toLowerCase();
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (k in STYLE_LIMITS) {
|
|
116
|
+
const [min, max, digits] = (STYLE_LIMITS as Record<string, number[]>)[k];
|
|
117
|
+
const n = Number(x);
|
|
118
|
+
if (!Number.isFinite(n) || n < min || n > max) return { ok: false, error: `${k}: ${x} fora de ${min} a ${max}` };
|
|
119
|
+
v[k] = +n.toFixed(digits);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (k === "hidden") {
|
|
123
|
+
if (typeof x !== "boolean") return { ok: false, error: "oculta: use true ou false" };
|
|
124
|
+
if (x) v.hidden = true; /* shown is the default: the key only exists when hidden */
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (k === "x" || k === "y") {
|
|
128
|
+
const n = Math.round(Number(x));
|
|
129
|
+
const lim = k === "x" ? w : h;
|
|
130
|
+
if (!Number.isFinite(n) || Math.abs(n) > lim) return { ok: false, error: `${k} fora do quadro: ${x}` };
|
|
131
|
+
v[k] = n;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
v[k] = x;
|
|
135
|
+
}
|
|
136
|
+
return { ok: true, v };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* safe area override: only the sides the video wants to change, as a fraction of the frame */
|
|
140
|
+
function sanitizeSafeArea(value: any): { ok: true; v: Record<string, number> } | { ok: false; error: string } {
|
|
141
|
+
if (value == null) return { ok: true, v: {} };
|
|
142
|
+
if (typeof value !== "object") return { ok: false, error: "safe area inválida" };
|
|
143
|
+
const v: Record<string, number> = {};
|
|
144
|
+
for (const side of SIDES) {
|
|
145
|
+
if (value[side] == null) continue;
|
|
146
|
+
const n = Number(value[side]);
|
|
147
|
+
if (!Number.isFinite(n) || n < 0 || n > 0.8)
|
|
148
|
+
return { ok: false, error: `safe area ${side}: ${value[side]} fora de 0 a 0,8 do quadro` };
|
|
149
|
+
v[side] = Math.round(n * 10000) / 10000;
|
|
150
|
+
}
|
|
151
|
+
return { ok: true, v };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* scope "video" changes the position of all; "scene" and "caption" write (or, with a
|
|
155
|
+
null or empty value, remove) the override for that scene or that caption */
|
|
156
|
+
export function saveCaptionPosition(dir: string, body: any): Result {
|
|
157
|
+
const file = join(dir, "scenes.json");
|
|
158
|
+
const raw = readJson<any>(file);
|
|
159
|
+
if (!raw) return { ok: false, error: "scenes.json não existe" };
|
|
160
|
+
const video = loadVideo(dir);
|
|
161
|
+
const { w, h } = video.format;
|
|
162
|
+
const safes = safeAreas(video.format);
|
|
163
|
+
const cap: Record<string, any> = { ...(raw.captions ?? {}) };
|
|
164
|
+
const scope = String(body?.scope ?? "");
|
|
165
|
+
const target = String(body?.target ?? "");
|
|
166
|
+
let output: string;
|
|
167
|
+
|
|
168
|
+
if (scope === "video") {
|
|
169
|
+
const r = sanitizePosition(body.value, w, h, ["platform", "anchor", "x", "y", "style", ...STYLE_KEYS], safes);
|
|
170
|
+
if (!r.ok) return r;
|
|
171
|
+
Object.assign(cap, r.v);
|
|
172
|
+
/* the video sends the whole state: a style key that didn't come back is back to default */
|
|
173
|
+
for (const k of STYLE_KEYS as string[]) if (!(k in r.v)) delete cap[k];
|
|
174
|
+
if ("safe" in body.value) {
|
|
175
|
+
const sf = sanitizeSafeArea(body.value.safe);
|
|
176
|
+
if (!sf.ok) return sf;
|
|
177
|
+
if (Object.keys(sf.v).length) cap.safe = sf.v;
|
|
178
|
+
else delete cap.safe;
|
|
179
|
+
}
|
|
180
|
+
output = "posição de todas as legendas gravada";
|
|
181
|
+
} else if (scope === "scene" || scope === "caption") {
|
|
182
|
+
const group = scope === "scene" ? "scenes" : "individual";
|
|
183
|
+
const exists =
|
|
184
|
+
scope === "scene"
|
|
185
|
+
? video.scenes.some((c: any) => c.id === target)
|
|
186
|
+
: captionsOf(dir, video).some((l: any) => l.id === target);
|
|
187
|
+
if (!exists) return { ok: false, error: `${scope === "scene" ? "cena" : "legenda"} ${target} não existe` };
|
|
188
|
+
if (scope === "scene" && body.value?.hidden != null)
|
|
189
|
+
return { ok: false, error: "oculta só vale para uma legenda (individuais), não para a cena" };
|
|
190
|
+
const map: Record<string, unknown> = { ...(cap[group] ?? {}) };
|
|
191
|
+
const r =
|
|
192
|
+
body.value == null
|
|
193
|
+
? { ok: true as const, v: {} }
|
|
194
|
+
: sanitizePosition(
|
|
195
|
+
body.value,
|
|
196
|
+
w,
|
|
197
|
+
h,
|
|
198
|
+
["anchor", "x", "y", ...STYLE_KEYS, ...(scope === "caption" ? ["hidden"] : [])],
|
|
199
|
+
safes,
|
|
200
|
+
);
|
|
201
|
+
if (!r.ok) return r;
|
|
202
|
+
if (Object.keys(r.v).length) map[target] = r.v;
|
|
203
|
+
else delete map[target];
|
|
204
|
+
if (Object.keys(map).length) cap[group] = map;
|
|
205
|
+
else delete cap[group];
|
|
206
|
+
output = Object.keys(r.v).length
|
|
207
|
+
? `ajuste de ${target} gravado`
|
|
208
|
+
: `${target} volta a seguir ${scope === "scene" ? "o vídeo" : "a cena"}`;
|
|
209
|
+
} else return { ok: false, error: `escopo desconhecido: ${scope}` };
|
|
210
|
+
|
|
211
|
+
writeScenes(file, raw, cap);
|
|
212
|
+
return { ok: true, output };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* changes a caption's text: goes straight to captions.srt (the previous one stays in
|
|
216
|
+
the stage history) and `captions.texts` keeps the change for the next generation.
|
|
217
|
+
Going back to what the line generated removes the change. */
|
|
218
|
+
export function saveCaptionText(dir: string, body: any): Result {
|
|
219
|
+
const file = join(dir, "scenes.json");
|
|
220
|
+
const raw = readJson<any>(file);
|
|
221
|
+
if (!raw) return { ok: false, error: "scenes.json não existe" };
|
|
222
|
+
const srt = join(dir, "captions.srt");
|
|
223
|
+
if (!existsSync(srt)) return { ok: false, error: "captions.srt não existe: gere a legenda antes" };
|
|
224
|
+
const video = loadVideo(dir);
|
|
225
|
+
const id = String(body?.id ?? "");
|
|
226
|
+
const captions = captionsOf(dir, video);
|
|
227
|
+
const index = captions.findIndex((l: any) => l.id === id);
|
|
228
|
+
if (index < 0) return { ok: false, error: `legenda ${id} não existe` };
|
|
229
|
+
const text = String(body?.text ?? "")
|
|
230
|
+
.replace(/\s+/g, " ")
|
|
231
|
+
.trim();
|
|
232
|
+
if (!text) return { ok: false, error: "texto vazio: para tirar a legenda do vídeo, desligue-a" };
|
|
233
|
+
const current = captions[index];
|
|
234
|
+
if (text === current.text) return { ok: true, output: "já era esse o texto" };
|
|
235
|
+
|
|
236
|
+
const cap: Record<string, any> = { ...(raw.captions ?? {}) };
|
|
237
|
+
const texts: Record<string, { text: string; original: string }> = { ...(cap.texts ?? {}) };
|
|
238
|
+
const original = current.original ?? current.text;
|
|
239
|
+
if (text === original) delete texts[id];
|
|
240
|
+
else texts[id] = { text, original };
|
|
241
|
+
if (Object.keys(texts).length) cap.texts = texts;
|
|
242
|
+
else delete cap.texts;
|
|
243
|
+
|
|
244
|
+
const previous = keepPrevious(dir, "captions", null, ["captions.srt"]);
|
|
245
|
+
replaceSrtText(srt, index, text);
|
|
246
|
+
previous.close();
|
|
247
|
+
writeScenes(file, raw, cap);
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
output: text === original ? `${id} voltou ao texto da fala` : `texto de ${id} gravado: vale na próxima montagem`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* `captions` right after captions_in_motion (or format), like the rest of the contract */
|
|
255
|
+
function writeScenes(file: string, raw: Record<string, unknown>, cap: Record<string, unknown>) {
|
|
256
|
+
const next: Record<string, unknown> = {};
|
|
257
|
+
const after = "captions_in_motion" in raw ? "captions_in_motion" : "format";
|
|
258
|
+
const empty = !Object.keys(cap).length;
|
|
259
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
260
|
+
if (k === "captions") continue;
|
|
261
|
+
next[k] = v;
|
|
262
|
+
if (k === after && !empty) next.captions = cap;
|
|
263
|
+
}
|
|
264
|
+
if (!empty && !("captions" in next)) next.captions = cap;
|
|
265
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
266
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2) + "\n");
|
|
267
|
+
renameSync(tmp, file);
|
|
268
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/* A content's Instagram carousel (videos/<slug>/carousel/): what the UI shows and
|
|
2
|
+
what it requests. State and stages come from tools/carousel.mjs's diagnosis
|
|
3
|
+
(imported, no process spawned); Claude writes and composes it (skill
|
|
4
|
+
av-carousel) through the content's session; publish.mjs carousel publishes it.
|
|
5
|
+
|
|
6
|
+
What a content delivers (video, carousel or both) lives in content.json, written
|
|
7
|
+
by the UI request. Content from before it: video, plus carousel if the dir has one. */
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, statSync, writeFileSync, renameSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { NODE, TOOLS_DIR, run, readJson, readText } from "./root";
|
|
12
|
+
import { openSession, replyToSession, latestSessionFor, type ApiResult } from "./sessions";
|
|
13
|
+
import { diagnosisOf } from "../../../tools/carousel.mjs";
|
|
14
|
+
|
|
15
|
+
export type Deliverable = "video" | "carousel";
|
|
16
|
+
const DELIVERABLES: Deliverable[] = ["video", "carousel"];
|
|
17
|
+
/* content.json written before the rename may still say "carrossel" */
|
|
18
|
+
const LEGACY_DELIVERABLE: Record<string, Deliverable> = { carrossel: "carousel" };
|
|
19
|
+
|
|
20
|
+
export function deliverables(dir: string): Deliverable[] {
|
|
21
|
+
const stored = readJson<{ deliverables?: unknown; entregas?: unknown }>(join(dir, "content.json"));
|
|
22
|
+
const list = stored?.deliverables ?? stored?.entregas;
|
|
23
|
+
if (Array.isArray(list)) {
|
|
24
|
+
const names = list.map(e => LEGACY_DELIVERABLE[String(e)] ?? e);
|
|
25
|
+
const valid = DELIVERABLES.filter(e => names.includes(e));
|
|
26
|
+
if (valid.length) return valid;
|
|
27
|
+
}
|
|
28
|
+
const hasCarousel = existsSync(join(dir, "carousel", "carousel.json"));
|
|
29
|
+
const hasVideo = !hasCarousel || existsSync(join(dir, "scenes.json")) || existsSync(join(dir, "script.md"));
|
|
30
|
+
return DELIVERABLES.filter(e => (e === "video" ? hasVideo : hasCarousel));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeJson(file: string, data: unknown) {
|
|
34
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
35
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
36
|
+
renameSync(tmp, file);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* "Sem narração e voz" (content.json `narration: false`): the video skips the spoken script, the narration, the
|
|
40
|
+
voice and the captions, and goes from the request to the motion script, the composition and the editor.
|
|
41
|
+
Absent means narrated. */
|
|
42
|
+
export const narration = (dir: string) =>
|
|
43
|
+
readJson<{ narration?: unknown }>(join(dir, "content.json"))?.narration !== false;
|
|
44
|
+
|
|
45
|
+
export function writeNarration(dir: string, on: boolean) {
|
|
46
|
+
const file = join(dir, "content.json");
|
|
47
|
+
const { narration: _old, ...rest } = readJson<Record<string, unknown>>(file) ?? {};
|
|
48
|
+
writeJson(file, on ? rest : { ...rest, narration: false });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeDeliverables(dir: string, list: Deliverable[]) {
|
|
52
|
+
const file = join(dir, "content.json");
|
|
53
|
+
const { entregas: _legacy, ...rest } = readJson<Record<string, unknown>>(file) ?? {};
|
|
54
|
+
writeJson(file, { ...rest, deliverables: DELIVERABLES.filter(e => list.includes(e)) });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fileEntry = (dir: string, path: string) => {
|
|
58
|
+
try {
|
|
59
|
+
return { path, size: 0, mtime: Math.round(statSync(join(dir, path)).mtimeMs) };
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/* the list summary: stages and the cover (the first slide's PNG) */
|
|
66
|
+
export function carouselSummary(dir: string) {
|
|
67
|
+
if (!existsSync(join(dir, "carousel", "carousel.json"))) return null;
|
|
68
|
+
let diag: any;
|
|
69
|
+
try {
|
|
70
|
+
diag = diagnosisOf(dir);
|
|
71
|
+
} catch (e: any) {
|
|
72
|
+
return { error: e.message, stages: [], next: null, slides: 0, cover: null, publication: null };
|
|
73
|
+
}
|
|
74
|
+
const c = readJson<any>(join(dir, "carousel", "carousel.json"));
|
|
75
|
+
const first = c?.slides?.[0]?.id;
|
|
76
|
+
return {
|
|
77
|
+
title: diag.title ?? null,
|
|
78
|
+
format: diag.format ?? null,
|
|
79
|
+
slides: diag.slides ?? 0,
|
|
80
|
+
stages: diag.stages as { id: string; name: string; ok: boolean; missing: string | null }[],
|
|
81
|
+
warnings: diag.warnings as string[],
|
|
82
|
+
next: diag.next as string | null,
|
|
83
|
+
cover: first ? fileEntry(dir, `carousel/png/${first}.png`) : null,
|
|
84
|
+
publication: readJson<any>(join(dir, "carousel", "publication.json")),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* the page detail: slides with PNG and warnings, the texts, the export */
|
|
89
|
+
export function carouselDetail(dir: string) {
|
|
90
|
+
const base = carouselSummary(dir);
|
|
91
|
+
if (!base) return null;
|
|
92
|
+
const cdir = join(dir, "carousel");
|
|
93
|
+
const c = readJson<any>(join(cdir, "carousel.json")) ?? {};
|
|
94
|
+
const build = readJson<any>(join(cdir, "build.json"))?.slides ?? {};
|
|
95
|
+
const exp = readJson<any>(join(cdir, "export", "export.json"));
|
|
96
|
+
let jpgs: string[] = [];
|
|
97
|
+
try {
|
|
98
|
+
jpgs = readdirSync(join(cdir, "export"))
|
|
99
|
+
.filter(n => /^\d{2}\.jpg$/.test(n))
|
|
100
|
+
.sort();
|
|
101
|
+
} catch {
|
|
102
|
+
/* no export */
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
...base,
|
|
106
|
+
goal: c.goal ?? null,
|
|
107
|
+
caption: c.caption ?? "",
|
|
108
|
+
origin: c.origin ?? null,
|
|
109
|
+
references: c.references ?? [],
|
|
110
|
+
slide_list: (c.slides ?? []).map((s: any) => ({
|
|
111
|
+
id: s.id,
|
|
112
|
+
role: s.role ?? null,
|
|
113
|
+
title: s.title ?? null,
|
|
114
|
+
text: s.text ?? null,
|
|
115
|
+
hero: s.hero ?? null,
|
|
116
|
+
png: fileEntry(dir, `carousel/png/${s.id}.png`),
|
|
117
|
+
warnings: build[s.id]?.warnings ?? [],
|
|
118
|
+
generated_at: build[s.id]?.generated_at ?? null,
|
|
119
|
+
})),
|
|
120
|
+
sheet: fileEntry(dir, "carousel/_sheet.png"),
|
|
121
|
+
export: exp
|
|
122
|
+
? { at: exp.exported_at, jpgs: jpgs.map(n => fileEntry(dir, `carousel/export/${n}`)).filter(Boolean) }
|
|
123
|
+
: null,
|
|
124
|
+
texts: { script: readText(join(cdir, "script.md")), composition: readText(join(cdir, "composition.md")) },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* --- UI requests -----------------------------------------------------------------------
|
|
129
|
+
Everything goes to the content's session (the same as the video's): resume the latest, or open one. */
|
|
130
|
+
function askAgent(slug: string, dir: string, title: string, message: string, origin: string): ApiResult {
|
|
131
|
+
const latest = latestSessionFor("video", slug);
|
|
132
|
+
if (latest) return replyToSession(latest.id, message, origin);
|
|
133
|
+
return openSession("video", slug, dir, title, `/omnivideo\n${message}`, origin);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* create the carousel: from the dir's video (if any) or from the request */
|
|
137
|
+
export function createCarousel(slug: string, dir: string, title: string, note: string): ApiResult {
|
|
138
|
+
const list = deliverables(dir);
|
|
139
|
+
const hasVideo = existsSync(join(dir, "script.md")) || existsSync(join(dir, "scenes.json"));
|
|
140
|
+
if (!list.includes("carousel")) writeDeliverables(dir, [...list, "carousel"]);
|
|
141
|
+
const extra = note.trim();
|
|
142
|
+
const msg = [
|
|
143
|
+
`Carrossel do Instagram para o conteúdo videos/${slug}, pedido pela tela do studio.`,
|
|
144
|
+
hasVideo
|
|
145
|
+
? `Ele nasce do vídeo da pasta: chame a skill av-carousel com \`videos/${slug} do vídeo\` (init com --from-video). O vídeo não muda.`
|
|
146
|
+
: `Chame a skill av-carousel com \`videos/${slug}\`; o pedido está em videos/${slug}/source/request.md.`,
|
|
147
|
+
extra ? `O que o usuário quer no carrossel: ${extra}` : "",
|
|
148
|
+
"Vá até o export (carousel.mjs export) e pare: publicar é pelo botão da tela.",
|
|
149
|
+
]
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.join("\n");
|
|
152
|
+
return askAgent(slug, dir, title, msg, "carousel");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* change requested on one slide (or on the whole carousel, without slide) */
|
|
156
|
+
export function adjustCarousel(slug: string, dir: string, title: string, body: any): ApiResult {
|
|
157
|
+
if (!existsSync(join(dir, "carousel", "carousel.json")))
|
|
158
|
+
return { ok: false, error: "este conteúdo ainda não tem carrossel" };
|
|
159
|
+
const request = String(body?.request ?? "").trim();
|
|
160
|
+
if (!request) return { ok: false, error: "diga o que mudar" };
|
|
161
|
+
const slide = String(body?.slide ?? "").trim();
|
|
162
|
+
if (slide && !/^s\d{2}$/.test(slide)) return { ok: false, error: "slide inválido" };
|
|
163
|
+
const stage = body?.stage === "script" ? "script" : "composition";
|
|
164
|
+
const target = slide ? `${slide} «${request}»` : `«${request}»`;
|
|
165
|
+
const msg = [
|
|
166
|
+
`Alteração no carrossel de videos/${slug}, pedida pela tela do studio (${stage === "script" ? "etapa Roteiro" : "etapa Composição"}).`,
|
|
167
|
+
`Chame a skill av-carousel com \`videos/${slug} ajustar: ${target}\`: mude só isso, refaça o build do que mudou, leia os PNG e exporte de novo.`,
|
|
168
|
+
].join("\n");
|
|
169
|
+
return askAgent(slug, dir, title, msg, "carousel");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* the post caption, edited in the UI: overrides the one in carousel.json */
|
|
173
|
+
export function saveCaption(dir: string, caption: unknown): ApiResult {
|
|
174
|
+
const cdir = join(dir, "carousel");
|
|
175
|
+
if (!existsSync(join(cdir, "carousel.json"))) return { ok: false, error: "este conteúdo ainda não tem carrossel" };
|
|
176
|
+
const text = String(caption ?? "").trim();
|
|
177
|
+
if (text.length > 2200)
|
|
178
|
+
return { ok: false, error: `a legenda tem ${text.length} caracteres (o Instagram aceita até 2200)` };
|
|
179
|
+
const file = join(cdir, "publication.json");
|
|
180
|
+
writeJson(file, { ...(readJson<object>(file) ?? {}), caption: text });
|
|
181
|
+
return { ok: true, output: "legenda gravada" };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const publishing = new Set<string>();
|
|
185
|
+
export async function publishCarousel(dir: string, body: any): Promise<ApiResult> {
|
|
186
|
+
if (publishing.has(dir)) return { ok: false, error: "este carrossel já está sendo publicado" };
|
|
187
|
+
if (body?.caption != null) {
|
|
188
|
+
const saved = saveCaption(dir, body.caption);
|
|
189
|
+
if (!saved.ok) return saved;
|
|
190
|
+
}
|
|
191
|
+
publishing.add(dir);
|
|
192
|
+
try {
|
|
193
|
+
const r = await run(
|
|
194
|
+
[NODE, join(TOOLS_DIR, "publish.mjs"), "carousel", dir, "--confirm", ...(body?.republish ? ["--again"] : [])],
|
|
195
|
+
{ OMNIVIDEO_ORIGIN: "studio" },
|
|
196
|
+
);
|
|
197
|
+
const output = [r.stdout, r.stderr]
|
|
198
|
+
.map(s => s.trim())
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.join("\n")
|
|
201
|
+
.replace(/^erro: /gm, "");
|
|
202
|
+
return r.code === 0 ? { ok: true, output } : { ok: false, error: output || `publish.mjs saiu com ${r.code}` };
|
|
203
|
+
} finally {
|
|
204
|
+
publishing.delete(dir);
|
|
205
|
+
}
|
|
206
|
+
}
|