@crievo/omni 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +41 -0
- package/README.md +2 -2
- package/bin/omni.mjs +14 -0
- package/docs/onboarding/clone-voice.png +0 -0
- package/docs/onboarding/getting-started.png +0 -0
- package/docs/onboarding/new-video.png +0 -0
- package/docs/onboarding/providers.png +0 -0
- package/docs/onboarding/publish.png +0 -0
- package/docs/onboarding/references.png +0 -0
- package/docs/onboarding.md +212 -0
- package/omnivideo.config.example.json +437 -0
- package/package.json +40 -4
- package/reference/captions.md +156 -0
- package/reference/carousel-contract.md +91 -0
- package/reference/cloud.md +96 -0
- package/reference/learnings.md +792 -0
- package/reference/omnichat.md +305 -0
- package/reference/premiere-mcp.md +91 -0
- package/reference/publish.md +305 -0
- package/reference/scenes-contract.md +109 -0
- package/reference/studio.md +666 -0
- package/reference/voicestudio.md +81 -0
- package/reference/writing-for-voice.md +67 -0
- package/requirements.txt +3 -0
- package/studio/api/app.ts +87 -0
- package/studio/api/lib/errors.ts +33 -0
- package/studio/api/lib/files.ts +41 -0
- package/studio/api/lib/hono.ts +9 -0
- package/studio/api/lib/result.ts +14 -0
- package/studio/api/lib/validate.ts +31 -0
- package/studio/api/lib/video.ts +10 -0
- package/studio/api/middleware/json-charset.ts +11 -0
- package/studio/api/middleware/local-only.ts +32 -0
- package/studio/api/middleware/no-timeout.ts +10 -0
- package/studio/api/routes/admin.ts +18 -0
- package/studio/api/routes/agent-cascade.ts +100 -0
- package/studio/api/routes/agent-history.ts +23 -0
- package/studio/api/routes/agent.ts +162 -0
- package/studio/api/routes/analytics.ts +31 -0
- package/studio/api/routes/approval.ts +60 -0
- package/studio/api/routes/archive.ts +12 -0
- package/studio/api/routes/captions.ts +28 -0
- package/studio/api/routes/carousel.ts +38 -0
- package/studio/api/routes/clone.ts +27 -0
- package/studio/api/routes/create.ts +19 -0
- package/studio/api/routes/events.ts +11 -0
- package/studio/api/routes/export.ts +14 -0
- package/studio/api/routes/media.ts +30 -0
- package/studio/api/routes/omnichat.ts +113 -0
- package/studio/api/routes/onboarding.ts +14 -0
- package/studio/api/routes/publish.ts +40 -0
- package/studio/api/routes/queue.ts +14 -0
- package/studio/api/routes/references.ts +40 -0
- package/studio/api/routes/runner.ts +17 -0
- package/studio/api/routes/schedule.ts +47 -0
- package/studio/api/routes/services.ts +11 -0
- package/studio/api/routes/sessions.ts +27 -0
- package/studio/api/routes/settings.ts +102 -0
- package/studio/api/routes/sound.ts +45 -0
- package/studio/api/routes/thumbnail.ts +18 -0
- package/studio/api/routes/usage.ts +13 -0
- package/studio/api/routes/videos.ts +41 -0
- package/studio/api/routes/voices.ts +71 -0
- package/studio/api/services/admin.ts +113 -0
- package/studio/api/services/agent-cascade.ts +975 -0
- package/studio/api/services/agent-claude.ts +47 -0
- package/studio/api/services/agent-history.ts +103 -0
- package/studio/api/services/agent-persona.ts +77 -0
- package/studio/api/services/agent-tools.ts +2237 -0
- package/studio/api/services/agent-ui-tools.ts +259 -0
- package/studio/api/services/agent-wait.ts +32 -0
- package/studio/api/services/agent.ts +553 -0
- package/studio/api/services/analytics.ts +92 -0
- package/studio/api/services/approval.ts +134 -0
- package/studio/api/services/archive.ts +38 -0
- package/studio/api/services/captions.ts +268 -0
- package/studio/api/services/carousel.ts +206 -0
- package/studio/api/services/classify.ts +196 -0
- package/studio/api/services/clone-phrases.ts +75 -0
- package/studio/api/services/clone.ts +722 -0
- package/studio/api/services/create.ts +421 -0
- package/studio/api/services/events.ts +215 -0
- package/studio/api/services/media.ts +32 -0
- package/studio/api/services/omnichat.ts +624 -0
- package/studio/api/services/onboarding.ts +77 -0
- package/studio/api/services/publish.ts +287 -0
- package/studio/api/services/queue.ts +150 -0
- package/studio/api/services/references.ts +269 -0
- package/studio/api/services/root.ts +60 -0
- package/studio/api/services/services.ts +114 -0
- package/studio/api/services/sessions.ts +1007 -0
- package/studio/api/services/settings.ts +983 -0
- package/studio/api/services/sound.ts +311 -0
- package/studio/api/services/thumbnail.ts +133 -0
- package/studio/api/services/usage.ts +27 -0
- package/studio/api/services/videos.ts +418 -0
- package/studio/api/services/voice-pause.ts +98 -0
- package/studio/api/services/voices.ts +324 -0
- package/studio/runner/runner.ts +244 -0
- package/studio/shared/omni-actions.ts +68 -0
- package/studio/shared/omni-admin.ts +52 -0
- package/studio/shared/omni-farewell.ts +48 -0
- package/studio/shared/omni-hotword.ts +20 -0
- package/studio/shared/omni-screens.ts +228 -0
- package/studio/shared/schemas/admin.ts +15 -0
- package/studio/shared/schemas/agent-cascade.ts +58 -0
- package/studio/shared/schemas/agent-history.ts +73 -0
- package/studio/shared/schemas/agent.ts +50 -0
- package/studio/shared/schemas/analytics.ts +6 -0
- package/studio/shared/schemas/approval.ts +22 -0
- package/studio/shared/schemas/captions.ts +19 -0
- package/studio/shared/schemas/carousel.ts +26 -0
- package/studio/shared/schemas/clone.ts +40 -0
- package/studio/shared/schemas/common.ts +16 -0
- package/studio/shared/schemas/omnichat.ts +54 -0
- package/studio/shared/schemas/onboarding.ts +5 -0
- package/studio/shared/schemas/publish.ts +45 -0
- package/studio/shared/schemas/references.ts +20 -0
- package/studio/shared/schemas/settings.ts +82 -0
- package/studio/shared/schemas/sound.ts +22 -0
- package/studio/shared/schemas/videos.ts +38 -0
- package/studio/shared/schemas/voices.ts +23 -0
- package/studio/shared/types/api.ts +1290 -0
- package/studio/studio.ts +148 -0
- package/studio/tsconfig.json +15 -0
- package/studio/web/assets/logos/cartesia.svg +4 -0
- package/studio/web/assets/logos/elevenlabs.svg +12 -0
- package/studio/web/assets/logos/fish.svg +24 -0
- package/studio/web/assets/logos/omni-black.png +0 -0
- package/studio/web/assets/logos/omni-white.png +0 -0
- package/studio/web/assets/logos/voicestudio.svg +21 -0
- package/studio/web/components/AgentMark.tsx +7 -0
- package/studio/web/components/AgentWalking.tsx +11 -0
- package/studio/web/components/ApiKeyField.tsx +104 -0
- package/studio/web/components/AppShell.tsx +64 -0
- package/studio/web/components/ApprovalNotice.tsx +65 -0
- package/studio/web/components/AvatarGroupIcon.tsx +22 -0
- package/studio/web/components/AvatarNameField.tsx +86 -0
- package/studio/web/components/AvatarPicker.tsx +84 -0
- package/studio/web/components/BrandLogo.tsx +16 -0
- package/studio/web/components/CartesiaPlanSetting.tsx +90 -0
- package/studio/web/components/ClaudeWalking.tsx +32 -0
- package/studio/web/components/CloneVoiceDialog/GuidedScript.tsx +39 -0
- package/studio/web/components/CloneVoiceDialog/LanguageBar.tsx +37 -0
- package/studio/web/components/CloneVoiceDialog/LevelMeter.tsx +31 -0
- package/studio/web/components/CloneVoiceDialog/ProviderChoice.tsx +81 -0
- package/studio/web/components/CloneVoiceDialog/QualityPanel.tsx +62 -0
- package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +50 -0
- package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +79 -0
- package/studio/web/components/CloneVoiceDialog/RedoSummary.tsx +54 -0
- package/studio/web/components/CloneVoiceDialog/SaveStep.tsx +91 -0
- package/studio/web/components/CloneVoiceDialog/SourceBar.tsx +30 -0
- package/studio/web/components/CloneVoiceDialog/index.tsx +141 -0
- package/studio/web/components/CloneVoiceDialog/types.ts +28 -0
- package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +528 -0
- package/studio/web/components/ConnectionSignals.tsx +51 -0
- package/studio/web/components/EditVoiceDialog/EditVoiceFooter.tsx +72 -0
- package/studio/web/components/EditVoiceDialog/ProviderCards.tsx +81 -0
- package/studio/web/components/EditVoiceDialog/index.tsx +256 -0
- package/studio/web/components/ErrorBanner.tsx +115 -0
- package/studio/web/components/FinishedTask.tsx +41 -0
- package/studio/web/components/FishModelSetting.tsx +69 -0
- package/studio/web/components/FishPriceSetting.tsx +44 -0
- package/studio/web/components/HumanBanner.tsx +64 -0
- package/studio/web/components/Lightbox.tsx +41 -0
- package/studio/web/components/LogStream.tsx +42 -0
- package/studio/web/components/Markdown.tsx +21 -0
- package/studio/web/components/MissingDmsPanel/BatchSend.tsx +137 -0
- package/studio/web/components/MissingDmsPanel/CommentCard.tsx +168 -0
- package/studio/web/components/MissingDmsPanel/CommentFeed.tsx +45 -0
- package/studio/web/components/MissingDmsPanel/DmBubble.tsx +38 -0
- package/studio/web/components/MissingDmsPanel/ReplyComposer.tsx +63 -0
- package/studio/web/components/MissingDmsPanel/index.tsx +98 -0
- package/studio/web/components/NotFound.tsx +48 -0
- package/studio/web/components/Omni/OmniAvatarPicker.tsx +92 -0
- package/studio/web/components/Omni/OmniCards.tsx +202 -0
- package/studio/web/components/Omni/OmniGlow.tsx +183 -0
- package/studio/web/components/Omni/OmniHistory.tsx +119 -0
- package/studio/web/components/Omni/OmniHotword.tsx +40 -0
- package/studio/web/components/Omni/OmniMicSelect.tsx +92 -0
- package/studio/web/components/Omni/OmniVoiceSample.tsx +81 -0
- package/studio/web/components/Omni/OmniWidget.tsx +743 -0
- package/studio/web/components/Omni/PendingActions.tsx +200 -0
- package/studio/web/components/Omni/Transcript.tsx +127 -0
- package/studio/web/components/OmnichatAccountStatus.tsx +87 -0
- package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +150 -0
- package/studio/web/components/OmnichatRulePanel/RuleDryRun.tsx +15 -0
- package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +42 -0
- package/studio/web/components/OmnichatRulePanel/RuleSkeleton.tsx +18 -0
- package/studio/web/components/OmnichatRulePanel/RuleStatus.tsx +55 -0
- package/studio/web/components/OmnichatRulePanel/index.tsx +115 -0
- package/studio/web/components/OmnichatRulePanel/rule-steps.ts +67 -0
- package/studio/web/components/OmnichatRulePanel/steps/MessageStep.tsx +91 -0
- package/studio/web/components/OmnichatRulePanel/steps/RepliesStep.tsx +201 -0
- package/studio/web/components/OmnichatRulePanel/steps/TriggerStep.tsx +76 -0
- package/studio/web/components/OmnichatRulePanel/steps/types.ts +11 -0
- package/studio/web/components/Palette.tsx +17 -0
- package/studio/web/components/PlatformCard.tsx +109 -0
- package/studio/web/components/PlatformIcon.tsx +19 -0
- package/studio/web/components/ProviderCard.tsx +80 -0
- package/studio/web/components/ProviderDetail.tsx +367 -0
- package/studio/web/components/ProviderLogo.tsx +20 -0
- package/studio/web/components/QueuePanel.tsx +77 -0
- package/studio/web/components/ReferenceActions.tsx +89 -0
- package/studio/web/components/ReferencePicker.tsx +136 -0
- package/studio/web/components/ResultMessage.tsx +7 -0
- package/studio/web/components/RuleForm.tsx +138 -0
- package/studio/web/components/RuleLog.tsx +151 -0
- package/studio/web/components/RunningTask.tsx +23 -0
- package/studio/web/components/SampleList.tsx +55 -0
- package/studio/web/components/SessionPanel.tsx +352 -0
- package/studio/web/components/SettingRow.tsx +14 -0
- package/studio/web/components/SettingsCategorySkeleton.tsx +25 -0
- package/studio/web/components/SidePanel.tsx +77 -0
- package/studio/web/components/Sidebar.tsx +289 -0
- package/studio/web/components/StageStepper.tsx +87 -0
- package/studio/web/components/StudioOffline.tsx +80 -0
- package/studio/web/components/TaskName.tsx +17 -0
- package/studio/web/components/TopBanners.tsx +30 -0
- package/studio/web/components/VideoOmnichatPanel.tsx +287 -0
- package/studio/web/components/VoiceAvatar.tsx +33 -0
- package/studio/web/components/VoiceCarousel.tsx +196 -0
- package/studio/web/components/VoiceCategory.tsx +9 -0
- package/studio/web/components/VoiceModal.tsx +39 -0
- package/studio/web/components/VoiceProfileCard.tsx +75 -0
- package/studio/web/components/VoiceProfilePicker.tsx +107 -0
- package/studio/web/components/VoiceProfilesSettings.tsx +150 -0
- package/studio/web/components/VoiceProviderCard.tsx +118 -0
- package/studio/web/components/VoiceProvidersCard.tsx +26 -0
- package/studio/web/components/WaitList.tsx +21 -0
- package/studio/web/components/icons.tsx +247 -0
- package/studio/web/components/ui/accordion.tsx +47 -0
- package/studio/web/components/ui/badge.tsx +18 -0
- package/studio/web/components/ui/calendar.tsx +43 -0
- package/studio/web/components/ui/chart.tsx +229 -0
- package/studio/web/components/ui/checkbox.tsx +26 -0
- package/studio/web/components/ui/cx.ts +2 -0
- package/studio/web/components/ui/date-time-picker.tsx +84 -0
- package/studio/web/components/ui/panel.tsx +23 -0
- package/studio/web/components/ui/picker.tsx +58 -0
- package/studio/web/components/ui/popover.tsx +24 -0
- package/studio/web/components/ui/select.tsx +89 -0
- package/studio/web/components/ui/skeleton.tsx +10 -0
- package/studio/web/components/ui/slider.tsx +68 -0
- package/studio/web/components/ui/switch.tsx +13 -0
- package/studio/web/components/ui/tooltip.test.tsx +30 -0
- package/studio/web/components/ui/tooltip.tsx +59 -0
- package/studio/web/hooks/useAccounts.ts +27 -0
- package/studio/web/hooks/useAdmin.ts +12 -0
- package/studio/web/hooks/useAnalytics.ts +21 -0
- package/studio/web/hooks/useAvatarAudioReactive.ts +87 -0
- package/studio/web/hooks/useBrowserConnect.ts +48 -0
- package/studio/web/hooks/useClonePlan.ts +14 -0
- package/studio/web/hooks/useConnected.ts +7 -0
- package/studio/web/hooks/useLiveQuery.ts +29 -0
- package/studio/web/hooks/useMenuCollapsed.ts +32 -0
- package/studio/web/hooks/useMicDevices.ts +53 -0
- package/studio/web/hooks/useMissingDms.ts +211 -0
- package/studio/web/hooks/useMissingSetupSteps.ts +26 -0
- package/studio/web/hooks/useNow.ts +11 -0
- package/studio/web/hooks/useOmniAgent.ts +278 -0
- package/studio/web/hooks/useOmniClaude.ts +124 -0
- package/studio/web/hooks/useOmniDock.ts +142 -0
- package/studio/web/hooks/useOmniGreeting.ts +60 -0
- package/studio/web/hooks/useOmniHotword.ts +128 -0
- package/studio/web/hooks/useOmnichat.ts +26 -0
- package/studio/web/hooks/useOnboarding.ts +27 -0
- package/studio/web/hooks/usePost.ts +23 -0
- package/studio/web/hooks/useQueue.ts +10 -0
- package/studio/web/hooks/useReferences.ts +20 -0
- package/studio/web/hooks/useRuleEditor.ts +101 -0
- package/studio/web/hooks/useRunner.ts +24 -0
- package/studio/web/hooks/useSaveAction.ts +23 -0
- package/studio/web/hooks/useSchedule.ts +11 -0
- package/studio/web/hooks/useServices.ts +10 -0
- package/studio/web/hooks/useSessions.ts +15 -0
- package/studio/web/hooks/useSettings.ts +11 -0
- package/studio/web/hooks/useSettingsSection.ts +30 -0
- package/studio/web/hooks/useSidePanel.ts +23 -0
- package/studio/web/hooks/useSound.ts +30 -0
- package/studio/web/hooks/useUsage.ts +11 -0
- package/studio/web/hooks/useVideoVoice.ts +36 -0
- package/studio/web/hooks/useVideos.ts +21 -0
- package/studio/web/hooks/useVoiceProfilePreview.ts +52 -0
- package/studio/web/hooks/useVoiceProfiles.ts +7 -0
- package/studio/web/hooks/useVoiceRunning.ts +10 -0
- package/studio/web/hooks/useVoiceStudioClosed.ts +8 -0
- package/studio/web/index.html +19 -0
- package/studio/web/lib/agent.ts +7 -0
- package/studio/web/lib/alerts.ts +210 -0
- package/studio/web/lib/analytics-metrics.ts +351 -0
- package/studio/web/lib/api.ts +70 -0
- package/studio/web/lib/audio.ts +39 -0
- package/studio/web/lib/avatars.ts +41 -0
- package/studio/web/lib/calendar.ts +218 -0
- package/studio/web/lib/caption-position.ts +249 -0
- package/studio/web/lib/carousel.ts +24 -0
- package/studio/web/lib/chime.ts +42 -0
- package/studio/web/lib/clone-voice.ts +55 -0
- package/studio/web/lib/export.ts +11 -0
- package/studio/web/lib/files.ts +8 -0
- package/studio/web/lib/format.ts +93 -0
- package/studio/web/lib/media.ts +29 -0
- package/studio/web/lib/mic-device.ts +72 -0
- package/studio/web/lib/missing-dms.ts +70 -0
- package/studio/web/lib/omni-cards.ts +42 -0
- package/studio/web/lib/omni-cascade.ts +1067 -0
- package/studio/web/lib/omni-greeting-audio.ts +24 -0
- package/studio/web/lib/omni-hotword.ts +231 -0
- package/studio/web/lib/omni-panel.ts +67 -0
- package/studio/web/lib/omni-realtime.ts +811 -0
- package/studio/web/lib/omni-ui.ts +402 -0
- package/studio/web/lib/omni-wait.ts +174 -0
- package/studio/web/lib/omnichat-api.ts +44 -0
- package/studio/web/lib/orb.ts +270 -0
- package/studio/web/lib/pcm-recorder.ts +115 -0
- package/studio/web/lib/platforms.ts +17 -0
- package/studio/web/lib/providers.ts +27 -0
- package/studio/web/lib/publish-draft.ts +52 -0
- package/studio/web/lib/publish-settings.ts +81 -0
- package/studio/web/lib/query-client.ts +122 -0
- package/studio/web/lib/references.ts +13 -0
- package/studio/web/lib/services.ts +40 -0
- package/studio/web/lib/sessions.ts +103 -0
- package/studio/web/lib/settings-categories.ts +58 -0
- package/studio/web/lib/sound-audio.ts +33 -0
- package/studio/web/lib/sound-timeline.ts +291 -0
- package/studio/web/lib/sse.ts +86 -0
- package/studio/web/lib/stage-history.ts +141 -0
- package/studio/web/lib/stages.ts +344 -0
- package/studio/web/lib/studio-offline.ts +60 -0
- package/studio/web/lib/upload.ts +24 -0
- package/studio/web/lib/video-tabs.ts +118 -0
- package/studio/web/lib/voice-profile-draft.ts +34 -0
- package/studio/web/lib/voice-profiles.ts +3 -0
- package/studio/web/main.tsx +14 -0
- package/studio/web/pages/AdminPage/index.tsx +123 -0
- package/studio/web/pages/AgentPage/components/Measurements.tsx +102 -0
- package/studio/web/pages/AgentPage/index.tsx +28 -0
- package/studio/web/pages/AnalyticsPage/components/About.tsx +52 -0
- package/studio/web/pages/AnalyticsPage/components/ChannelCards.tsx +106 -0
- package/studio/web/pages/AnalyticsPage/components/ChannelView.tsx +137 -0
- package/studio/web/pages/AnalyticsPage/components/CollectionStatus.tsx +46 -0
- package/studio/web/pages/AnalyticsPage/components/Comparison.tsx +68 -0
- package/studio/web/pages/AnalyticsPage/components/Diagnosis.tsx +50 -0
- package/studio/web/pages/AnalyticsPage/components/Highlights.tsx +49 -0
- package/studio/web/pages/AnalyticsPage/components/Hint.tsx +11 -0
- package/studio/web/pages/AnalyticsPage/components/HookVsBody.tsx +100 -0
- package/studio/web/pages/AnalyticsPage/components/HourlyWave.tsx +133 -0
- package/studio/web/pages/AnalyticsPage/components/Insights.tsx +70 -0
- package/studio/web/pages/AnalyticsPage/components/MetricCard.tsx +46 -0
- package/studio/web/pages/AnalyticsPage/components/PlanSection.tsx +40 -0
- package/studio/web/pages/AnalyticsPage/components/ProgressionChart.tsx +155 -0
- package/studio/web/pages/AnalyticsPage/components/ReelBarsChart.tsx +154 -0
- package/studio/web/pages/AnalyticsPage/components/ReelThumb.tsx +19 -0
- package/studio/web/pages/AnalyticsPage/components/ReelView.tsx +290 -0
- package/studio/web/pages/AnalyticsPage/components/ReelsTable.tsx +269 -0
- package/studio/web/pages/AnalyticsPage/components/RequestWithFocus.tsx +38 -0
- package/studio/web/pages/AnalyticsPage/components/SegmentedControl.tsx +35 -0
- package/studio/web/pages/AnalyticsPage/components/VsMedian.tsx +31 -0
- package/studio/web/pages/AnalyticsPage/index.tsx +17 -0
- package/studio/web/pages/GettingStartedPage/index.tsx +174 -0
- package/studio/web/pages/NewVideoPage/components/DeliverablesPicker.tsx +62 -0
- package/studio/web/pages/NewVideoPage/components/FormatPicker.tsx +33 -0
- package/studio/web/pages/NewVideoPage/index.tsx +133 -0
- package/studio/web/pages/NotFoundPage/index.tsx +9 -0
- package/studio/web/pages/OmnichatPage/components/NewFromLink.tsx +59 -0
- package/studio/web/pages/OmnichatPage/components/RuleListItem.tsx +64 -0
- package/studio/web/pages/OmnichatPage/components/RulePreview.tsx +14 -0
- package/studio/web/pages/OmnichatPage/components/RuleView.tsx +88 -0
- package/studio/web/pages/OmnichatPage/index.tsx +153 -0
- package/studio/web/pages/QueuePage/index.tsx +22 -0
- package/studio/web/pages/ReferencePage/components/ReferenceTexts.tsx +131 -0
- package/studio/web/pages/ReferencePage/index.tsx +216 -0
- package/studio/web/pages/ReferencesPage/components/NewReferenceForm.tsx +150 -0
- package/studio/web/pages/ReferencesPage/components/ReferenceCard.tsx +42 -0
- package/studio/web/pages/ReferencesPage/components/ReferenceGridSkeleton.tsx +31 -0
- package/studio/web/pages/ReferencesPage/index.tsx +67 -0
- package/studio/web/pages/SchedulePage/components/CalendarSkeleton.tsx +18 -0
- package/studio/web/pages/SchedulePage/components/DayView.tsx +177 -0
- package/studio/web/pages/SchedulePage/components/HourPicker.tsx +103 -0
- package/studio/web/pages/SchedulePage/components/MonthDayCell.tsx +111 -0
- package/studio/web/pages/SchedulePage/components/MonthView.tsx +47 -0
- package/studio/web/pages/SchedulePage/components/PlatformIcons.tsx +12 -0
- package/studio/web/pages/SchedulePage/components/PostThumb.tsx +15 -0
- package/studio/web/pages/SchedulePage/components/WeekColumn.tsx +75 -0
- package/studio/web/pages/SchedulePage/components/WeekView.tsx +35 -0
- package/studio/web/pages/SchedulePage/index.tsx +285 -0
- package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +263 -0
- package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +20 -0
- package/studio/web/pages/SettingsPage/components/ModelPicker.tsx +49 -0
- package/studio/web/pages/SettingsPage/components/ModelsSettings.tsx +138 -0
- package/studio/web/pages/SettingsPage/components/PlatformsCategory.tsx +23 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/AccountStatus.tsx +29 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/AutoPublishPanel.tsx +90 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/BrowserConnect.tsx +62 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/CheckField.tsx +19 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/InstagramPanel.tsx +113 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +146 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SaveBar.tsx +33 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +132 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCard.tsx +47 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCardGroup.tsx +19 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/TextPanel.tsx +55 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/TiktokPanel.tsx +123 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/YoutubePanel.tsx +100 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/badges.tsx +15 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/index.tsx +40 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/types.ts +10 -0
- package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +389 -0
- package/studio/web/pages/SettingsPage/components/VoiceProvidersCategory.tsx +97 -0
- package/studio/web/pages/SettingsPage/index.tsx +85 -0
- package/studio/web/pages/VideoPage/components/AudioPlayer.tsx +113 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/AxisField.tsx +50 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionList.tsx +56 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionListItem.tsx +92 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionMargins.tsx +57 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionStyleFields.tsx +70 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionTextForm.tsx +87 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/ColorField.tsx +37 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/MarginField.tsx +59 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/NumberField.tsx +57 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/PositionControls.tsx +140 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/captionEditor.ts +215 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/useCaptionPosition.ts +154 -0
- package/studio/web/pages/VideoPage/components/CarouselChangeRequest.tsx +72 -0
- package/studio/web/pages/VideoPage/components/CarouselCompositionStage.tsx +80 -0
- package/studio/web/pages/VideoPage/components/CarouselCreate.tsx +63 -0
- package/studio/web/pages/VideoPage/components/CarouselMissing.tsx +19 -0
- package/studio/web/pages/VideoPage/components/CarouselPublishStage.tsx +173 -0
- package/studio/web/pages/VideoPage/components/CarouselScriptStage.tsx +82 -0
- package/studio/web/pages/VideoPage/components/CarouselSlideCard.tsx +49 -0
- package/studio/web/pages/VideoPage/components/CarouselViewer.tsx +55 -0
- package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +53 -0
- package/studio/web/pages/VideoPage/components/CompositionStage.tsx +78 -0
- package/studio/web/pages/VideoPage/components/CoverPicker.tsx +32 -0
- package/studio/web/pages/VideoPage/components/EditorStage.tsx +53 -0
- package/studio/web/pages/VideoPage/components/LineTakes.tsx +160 -0
- package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +124 -0
- package/studio/web/pages/VideoPage/components/LineVoicePicker.tsx +37 -0
- package/studio/web/pages/VideoPage/components/MediaImage.tsx +25 -0
- package/studio/web/pages/VideoPage/components/MotionScriptStage.tsx +39 -0
- package/studio/web/pages/VideoPage/components/NarrationStage.tsx +70 -0
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +387 -0
- package/studio/web/pages/VideoPage/components/PublishStage.tsx +76 -0
- package/studio/web/pages/VideoPage/components/PublishText.tsx +149 -0
- package/studio/web/pages/VideoPage/components/ScenesApprovalPanel.tsx +278 -0
- package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +72 -0
- package/studio/web/pages/VideoPage/components/ScriptApprovalPanel.tsx +189 -0
- package/studio/web/pages/VideoPage/components/ScriptStage.tsx +17 -0
- package/studio/web/pages/VideoPage/components/SequencePlayer.tsx +146 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +117 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/AreaSelection.tsx +74 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedPanel.tsx +94 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +45 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionOverlay.tsx +44 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +32 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionsPanel.tsx +22 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/ClipBlock.tsx +111 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/ClipPanel.tsx +76 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CutsPanel.tsx +44 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EditorTabBar.tsx +73 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +46 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectItem.tsx +89 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectPanel.tsx +232 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectsRow.tsx +21 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/Lane.tsx +39 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/MarkerRow.tsx +40 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/MuteButton.tsx +36 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/NumberInput.tsx +37 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/PlanWarnings.tsx +19 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/PlayerResizeHandle.tsx +50 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/PreviewControls.tsx +51 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SceneRow.tsx +54 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SettingsPanel.tsx +65 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +99 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SoundPreview.tsx +82 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SpeedPanel.tsx +128 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineDock.tsx +56 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +26 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineSkeleton.tsx +21 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineToolbar.tsx +160 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineTracks.tsx +121 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TrackSidebar.tsx +30 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/VoiceRow.tsx +53 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/Waveform.tsx +97 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/clipActions.ts +50 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/effectActions.ts +161 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/index.tsx +161 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/timelineGeometry.ts +45 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/types.ts +43 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useAutosave.ts +107 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useEditorShortcuts.ts +168 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useFitHeight.ts +40 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useFollowPlayhead.ts +21 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useListenMix.ts +43 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayback.ts +231 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayerWidth.ts +27 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundData.ts +124 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +511 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/CompareVersions.tsx +111 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/VersionCard.tsx +81 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/VersionMedia.tsx +24 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/VersionText.tsx +15 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/index.tsx +109 -0
- package/studio/web/pages/VideoPage/components/StageProcess.tsx +126 -0
- package/studio/web/pages/VideoPage/components/StageResult.tsx +69 -0
- package/studio/web/pages/VideoPage/components/SwapReference.tsx +92 -0
- package/studio/web/pages/VideoPage/components/ThumbnailPanel.tsx +252 -0
- package/studio/web/pages/VideoPage/components/TimingPanel.tsx +89 -0
- package/studio/web/pages/VideoPage/components/VideoPlayer.tsx +18 -0
- package/studio/web/pages/VideoPage/components/VideoSkeleton.tsx +50 -0
- package/studio/web/pages/VideoPage/components/VideoVoiceInfo.tsx +33 -0
- package/studio/web/pages/VideoPage/components/VideoVoicePicker.tsx +165 -0
- package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +56 -0
- package/studio/web/pages/VideoPage/components/WithLogo.tsx +10 -0
- package/studio/web/pages/VideoPage/index.tsx +295 -0
- package/studio/web/pages/VideosPage/components/PublishedOn.tsx +85 -0
- package/studio/web/pages/VideosPage/components/ServicesPanel.tsx +81 -0
- package/studio/web/pages/VideosPage/components/SessionsPanel.tsx +62 -0
- package/studio/web/pages/VideosPage/components/VideoListSkeleton.tsx +25 -0
- package/studio/web/pages/VideosPage/components/VideoRow.tsx +122 -0
- package/studio/web/pages/VideosPage/index.tsx +134 -0
- package/studio/web/pages/VoicePage/index.tsx +28 -0
- package/studio/web/router.tsx +201 -0
- package/studio/web/styles/base/elements.css +55 -0
- package/studio/web/styles/base/utilities.css +25 -0
- package/studio/web/styles/components/badge.css +33 -0
- package/studio/web/styles/components/claude-running.css +27 -0
- package/studio/web/styles/components/claude-session.css +212 -0
- package/studio/web/styles/components/claude-walking.css +92 -0
- package/studio/web/styles/components/clone-voice-save.css +136 -0
- package/studio/web/styles/components/clone-voice.css +379 -0
- package/studio/web/styles/components/controls.css +70 -0
- package/studio/web/styles/components/data-and-image.css +54 -0
- package/studio/web/styles/components/forms.css +132 -0
- package/studio/web/styles/components/human-banner.css +99 -0
- package/studio/web/styles/components/lightbox.css +50 -0
- package/studio/web/styles/components/markdown.css +68 -0
- package/studio/web/styles/components/modal.css +70 -0
- package/studio/web/styles/components/omni-cards.css +105 -0
- package/studio/web/styles/components/omni.css +859 -0
- package/studio/web/styles/components/omnichat.css +538 -0
- package/studio/web/styles/components/panel.css +115 -0
- package/studio/web/styles/components/platform-card.css +140 -0
- package/studio/web/styles/components/provider-card.css +132 -0
- package/studio/web/styles/components/queue.css +185 -0
- package/studio/web/styles/components/reference-actions.css +133 -0
- package/studio/web/styles/components/reference-picker.css +146 -0
- package/studio/web/styles/components/results-drawer.css +224 -0
- package/studio/web/styles/components/segmented.css +115 -0
- package/studio/web/styles/components/side-panel-tab.css +23 -0
- package/studio/web/styles/components/status-signals.css +66 -0
- package/studio/web/styles/components/stepper.css +208 -0
- package/studio/web/styles/components/table.css +19 -0
- package/studio/web/styles/components/tabs.css +24 -0
- package/studio/web/styles/components/ui/accordion.css +71 -0
- package/studio/web/styles/components/ui/chart.css +162 -0
- package/studio/web/styles/components/ui/checkbox.css +37 -0
- package/studio/web/styles/components/ui/date-picker.css +153 -0
- package/studio/web/styles/components/ui/select.css +110 -0
- package/studio/web/styles/components/ui/skeleton.css +25 -0
- package/studio/web/styles/components/ui/slider.css +81 -0
- package/studio/web/styles/components/ui/switch.css +41 -0
- package/studio/web/styles/components/ui/tooltip.css +24 -0
- package/studio/web/styles/components/voice-carousel.css +255 -0
- package/studio/web/styles/components/voice-editor.css +179 -0
- package/studio/web/styles/components/voice-grid.css +56 -0
- package/studio/web/styles/components/voice-profiles.css +192 -0
- package/studio/web/styles/design-system.md +136 -0
- package/studio/web/styles/index.css +116 -0
- package/studio/web/styles/layout/page.css +41 -0
- package/studio/web/styles/layout/sidebar-groups.css +39 -0
- package/studio/web/styles/layout/sidebar.css +111 -0
- package/studio/web/styles/pages/agent.css +26 -0
- package/studio/web/styles/pages/analytics/dashboard.css +614 -0
- package/studio/web/styles/pages/analytics/insights.css +48 -0
- package/studio/web/styles/pages/getting-started.css +166 -0
- package/studio/web/styles/pages/new-video/form.css +32 -0
- package/studio/web/styles/pages/not-found.css +59 -0
- package/studio/web/styles/pages/references.css +204 -0
- package/studio/web/styles/pages/schedule/calendar.css +541 -0
- package/studio/web/styles/pages/schedule/chip-states.css +60 -0
- package/studio/web/styles/pages/settings/models.css +72 -0
- package/studio/web/styles/pages/settings/shell.css +361 -0
- package/studio/web/styles/pages/settings/usage.css +63 -0
- package/studio/web/styles/pages/studio-offline.css +105 -0
- package/studio/web/styles/pages/video/caption-controls.css +194 -0
- package/studio/web/styles/pages/video/captions-grid.css +70 -0
- package/studio/web/styles/pages/video/captions.css +79 -0
- package/studio/web/styles/pages/video/editor-chat.css +103 -0
- package/studio/web/styles/pages/video/export.css +50 -0
- package/studio/web/styles/pages/video/frames.css +29 -0
- package/studio/web/styles/pages/video/fullscreen-stage.css +400 -0
- package/studio/web/styles/pages/video/instagram-carousel.css +309 -0
- package/studio/web/styles/pages/video/layout.css +285 -0
- package/studio/web/styles/pages/video/narration.css +138 -0
- package/studio/web/styles/pages/video/preview-overview.css +11 -0
- package/studio/web/styles/pages/video/processing-notice.css +34 -0
- package/studio/web/styles/pages/video/publish-when.css +76 -0
- package/studio/web/styles/pages/video/publish.css +97 -0
- package/studio/web/styles/pages/video/result.css +96 -0
- package/studio/web/styles/pages/video/scenes.css +167 -0
- package/studio/web/styles/pages/video/sound-timeline.css +672 -0
- package/studio/web/styles/pages/video/sound-toolbar.css +149 -0
- package/studio/web/styles/pages/video/take-player.css +100 -0
- package/studio/web/styles/pages/video/thumbnail.css +157 -0
- package/studio/web/styles/pages/video/timeline.css +117 -0
- package/studio/web/styles/pages/video/voice-picker.css +14 -0
- package/studio/web/styles/pages/video-list/active-row.css +27 -0
- package/studio/web/styles/pages/video-list/card-link.css +49 -0
- package/studio/web/styles/pages/video-list/list.css +86 -0
- package/studio/web/styles/theme/identity/details.css +204 -0
- package/studio/web/styles/theme/identity/shell.css +249 -0
- package/studio/web/styles/theme/identity/surfaces.css +183 -0
- package/studio/web/styles/theme/identity/titles-and-buttons.css +120 -0
- package/studio/web/styles/theme/no-borders/blocks.css +208 -0
- package/studio/web/styles/theme/no-borders/controls.css +256 -0
- package/studio/web/styles/tokens.css +245 -0
- package/studio/web/types/api.ts +2 -0
- package/studio/web/types/assets.d.ts +9 -0
- package/templates/brief.md +27 -0
- package/templates/captions.html +204 -0
- package/templates/carousel-composition.md +47 -0
- package/templates/carousel-script.md +60 -0
- package/templates/components/voiceavatar/README.md +87 -0
- package/templates/components/voiceavatar/demo.html +50 -0
- package/templates/components/voiceavatar/voiceavatar.js +415 -0
- package/templates/motion.md +80 -0
- package/templates/playbook.md +43 -0
- package/templates/reference.md +123 -0
- package/templates/scene.html +112 -0
- package/templates/scenes.example.json +96 -0
- package/templates/script.md +63 -0
- package/templates/slide.html +71 -0
- package/templates/wireframe.html +91 -0
- package/tools/analysis.mjs +300 -0
- package/tools/approval.mjs +442 -0
- package/tools/assemble.mjs +942 -0
- package/tools/build-motion.mjs +437 -0
- package/tools/captions.mjs +246 -0
- package/tools/carousel.mjs +558 -0
- package/tools/compose.mjs +307 -0
- package/tools/cuts.mjs +74 -0
- package/tools/editor.mjs +419 -0
- package/tools/epidemic-capture.js +54 -0
- package/tools/fanout.mjs +69 -0
- package/tools/history.mjs +70 -0
- package/tools/history.py +61 -0
- package/tools/job-queue.mjs +180 -0
- package/tools/job_queue.py +430 -0
- package/tools/lib/agents.mjs +412 -0
- package/tools/lib/channel.mjs +399 -0
- package/tools/lib/claude-settings.mjs +67 -0
- package/tools/lib/cli.mjs +9 -0
- package/tools/lib/fanout.mjs +234 -0
- package/tools/lib/http.mjs +94 -0
- package/tools/lib/index.js +14 -0
- package/tools/lib/mcp-server.mjs +69 -0
- package/tools/lib/metrics/metrics.mjs +204 -0
- package/tools/lib/narration.mjs +15 -0
- package/tools/lib/oauth-client.mjs +295 -0
- package/tools/lib/omnichat/config.mjs +139 -0
- package/tools/lib/omnichat/errors.mjs +42 -0
- package/tools/lib/omnichat/extract.mjs +82 -0
- package/tools/lib/omnichat/gate.mjs +151 -0
- package/tools/lib/omnichat/manual.mjs +166 -0
- package/tools/lib/omnichat/match.mjs +78 -0
- package/tools/lib/omnichat/pass.mjs +393 -0
- package/tools/lib/omnichat/record.mjs +208 -0
- package/tools/lib/omnichat/text.mjs +315 -0
- package/tools/lib/omnichat/webhook.mjs +166 -0
- package/tools/lib/pairing.mjs +99 -0
- package/tools/lib/paths.mjs +41 -0
- package/tools/lib/platforms/instagram.mjs +469 -0
- package/tools/lib/platforms/tiktok.mjs +343 -0
- package/tools/lib/platforms/youtube.mjs +228 -0
- package/tools/lib/publish/publish.mjs +244 -0
- package/tools/lib/schedule/schedule.mjs +191 -0
- package/tools/lib/service.mjs +134 -0
- package/tools/lib/skill-client.mjs +126 -0
- package/tools/lib/skills-release.mjs +196 -0
- package/tools/lib/testing.mjs +52 -0
- package/tools/lib/up.mjs +187 -0
- package/tools/metrics.mjs +415 -0
- package/tools/models.mjs +134 -0
- package/tools/narrate.mjs +257 -0
- package/tools/omni.mjs +298 -0
- package/tools/omni_paths.py +12 -0
- package/tools/omnichat.mjs +1577 -0
- package/tools/omnivideo.py +1122 -0
- package/tools/package.mjs +130 -0
- package/tools/platforms.mjs +236 -0
- package/tools/premiere.mjs +533 -0
- package/tools/publish.mjs +1496 -0
- package/tools/reference.mjs +1408 -0
- package/tools/render.mjs +426 -0
- package/tools/runner.mjs +156 -0
- package/tools/setup.mjs +466 -0
- package/tools/skills-release.mjs +93 -0
- package/tools/sound.mjs +1323 -0
- package/tools/soundtrack.mjs +217 -0
- package/tools/thumbnail.mjs +271 -0
- package/tools/timing.mjs +108 -0
- package/tools/usage.py +628 -0
- package/tools/voice-envelope.mjs +185 -0
- package/tools/voice.py +448 -0
- package/tools/voice_sample.py +75 -0
- package/tools/wireframe.mjs +230 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* ============================================================================
|
|
3
|
+
Builds each scene's final HTML from motion/src/<id>.html, with the beat
|
|
4
|
+
times already computed, and optionally renders.
|
|
5
|
+
|
|
6
|
+
node tools/build-motion.mjs videos/<slug> HTML only
|
|
7
|
+
node tools/build-motion.mjs videos/<slug> --stills + a still per scene
|
|
8
|
+
node tools/build-motion.mjs videos/<slug> --sheets + contact sheet at the beats
|
|
9
|
+
node tools/build-motion.mjs videos/<slug> --transitions + a sheet for each cut between scenes
|
|
10
|
+
node tools/build-motion.mjs videos/<slug> --render + final MP4 (needs real timing;
|
|
11
|
+
waits for the machine queue turn)
|
|
12
|
+
node tools/build-motion.mjs videos/<slug> --scene c02 only that scene (and its two cuts); the
|
|
13
|
+
neighbours are built into temporary files, so
|
|
14
|
+
scenes composed in parallel never overwrite
|
|
15
|
+
each other's HTML
|
|
16
|
+
|
|
17
|
+
The source scene never writes seconds for a beat. It uses the beat variable:
|
|
18
|
+
|
|
19
|
+
<div data-anim style="animation-name:rise; --start:var(--b-barra-jev);">
|
|
20
|
+
<div data-anim style="animation-name:rise; --start:calc(var(--b-titulo) + 120ms);">
|
|
21
|
+
|
|
22
|
+
(based on templates/scene.html) and this script injects before </head>:
|
|
23
|
+
|
|
24
|
+
<style id="omnivideo-beats">:root{ --b-titulo:0.4s; --b-barra-jev:5.767s; --scene-dur:7.6s;
|
|
25
|
+
--scene-start:12.4s; --video-dur:41.2s; }</style>
|
|
26
|
+
<script id="omnivideo-data">window.OMNIVIDEO = { scene, start, duration, total_duration, provisional, beats, lines, video }</script>
|
|
27
|
+
|
|
28
|
+
GSAP and Motion read window.OMNIVIDEO.beats to position the paused timeline.
|
|
29
|
+
`video` is the overview of every scene in video seconds (beats, lines and
|
|
30
|
+
words): what a continuous composition uses, drawn once in _common.html and
|
|
31
|
+
positioned at OMNIVIDEO.start + t.
|
|
32
|
+
|
|
33
|
+
Legacy names (scene sources written before the English rename) keep
|
|
34
|
+
working: the PT keys of OMNIVIDEO (cena, inicio, duracao, duracao_total,
|
|
35
|
+
provisorio, falas[{texto, inicio, fim, palavras}], video{duracao_total,
|
|
36
|
+
cenas}), --cena-dur / --cena-inicio, <!-- omnivideo:comum -->,
|
|
37
|
+
motion/src/_comum.html and [data-continua].
|
|
38
|
+
|
|
39
|
+
Continuity between scenes:
|
|
40
|
+
- `--scene-start` is the video second where the scene starts. Background and
|
|
41
|
+
camera that cross the cut move in video time
|
|
42
|
+
(`--start:calc(0s - var(--scene-start))`), not scene time; otherwise they
|
|
43
|
+
restart at every cut.
|
|
44
|
+
- motion/src/_common.html, if present, replaces <!-- omnivideo:common -->
|
|
45
|
+
in every scene: the same background, same code, across the whole video.
|
|
46
|
+
motion/src/_common.<id>.html (a scene's own part, written by that scene
|
|
47
|
+
when the composition runs in parallel) is pasted right after it, in scene
|
|
48
|
+
order, so no two scenes edit the same file.
|
|
49
|
+
- --transitions draws each cut (the outgoing scene at the cut instant | the
|
|
50
|
+
incoming one at its first frame | both overlaid) and, if the scene
|
|
51
|
+
declares `transition: { continues: [...] }`, measures the
|
|
52
|
+
[data-continues] on both sides.
|
|
53
|
+
============================================================================ */
|
|
54
|
+
|
|
55
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, renameSync } from "node:fs";
|
|
56
|
+
import { spawnSync } from "node:child_process";
|
|
57
|
+
import { join, resolve, dirname } from "node:path";
|
|
58
|
+
import { tmpdir } from "node:os";
|
|
59
|
+
import { loadVideo, beatTime } from "./timing.mjs";
|
|
60
|
+
import { turn } from "./job-queue.mjs";
|
|
61
|
+
import { keepPrevious } from "./history.mjs";
|
|
62
|
+
import { CODE_ROOT, DATA_ROOT as ROOT } from "./lib/paths.mjs";
|
|
63
|
+
|
|
64
|
+
const CFG = JSON.parse(readFileSync(join(ROOT, "omnivideo.config.json"), "utf8"));
|
|
65
|
+
|
|
66
|
+
const args = process.argv.slice(2);
|
|
67
|
+
const dir = resolve(args[0] ?? "");
|
|
68
|
+
const flag = n => args.includes(`--${n}`);
|
|
69
|
+
const onlyScene = args.includes("--scene") ? args[args.indexOf("--scene") + 1] : null;
|
|
70
|
+
if (!args[0] || !existsSync(join(dir, "scenes.json"))) {
|
|
71
|
+
console.error(
|
|
72
|
+
"uso: node tools/build-motion.mjs videos/<slug> [--stills] [--sheets] [--transitions] [--render] [--scene cNN]",
|
|
73
|
+
);
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let video = loadVideo(dir);
|
|
78
|
+
function requireRealTiming() {
|
|
79
|
+
if (flag("render") && video.provisional) {
|
|
80
|
+
console.error(
|
|
81
|
+
"erro: o render final precisa do tempo real da voz. Rode antes:\n" +
|
|
82
|
+
" omnivideo.py voice → omnivideo.py timing (e omnivideo.py captions, para beats em palavras)",
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
requireRealTiming();
|
|
88
|
+
/* the final render takes the machine: wait for the queue turn (tools/job-queue.mjs)
|
|
89
|
+
and reload scenes.json, which may have changed while waiting */
|
|
90
|
+
if (flag("render")) {
|
|
91
|
+
await turn("render", dir);
|
|
92
|
+
video = loadVideo(dir);
|
|
93
|
+
requireRealTiming();
|
|
94
|
+
}
|
|
95
|
+
const { w, h, fps } = video.format;
|
|
96
|
+
const HOLD = 0.6;
|
|
97
|
+
const TOL_PX = 2; /* a continuing element may not jump more than this at the cut */
|
|
98
|
+
const MARKER = "<!-- omnivideo:common -->";
|
|
99
|
+
const selected = id => !onlyScene || onlyScene === id;
|
|
100
|
+
|
|
101
|
+
function render(...a) {
|
|
102
|
+
const r = spawnSync(
|
|
103
|
+
"node",
|
|
104
|
+
[
|
|
105
|
+
resolve(CODE_ROOT, CFG.motion.render ?? "tools/render.mjs"),
|
|
106
|
+
...a,
|
|
107
|
+
"--w",
|
|
108
|
+
String(w),
|
|
109
|
+
"--h",
|
|
110
|
+
String(h),
|
|
111
|
+
"--jobs",
|
|
112
|
+
String(CFG.motion.jobs ?? 3),
|
|
113
|
+
],
|
|
114
|
+
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
115
|
+
);
|
|
116
|
+
if (r.status !== 0) throw new Error(r.stderr.toString() || `render.mjs saiu com ${r.status}`);
|
|
117
|
+
return r.stdout.toString();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* _comum.html = legacy name of _common.html. The scenes' own parts
|
|
121
|
+
(_common.<id>.html, parallel composition) follow it in scene order. */
|
|
122
|
+
const commonFile = ["_common.html", "_comum.html"].map(f => join(dir, "motion", "src", f)).find(existsSync) ?? null;
|
|
123
|
+
const commonParts = [
|
|
124
|
+
...(commonFile ? [commonFile] : []),
|
|
125
|
+
...video.scenes.map(s => join(dir, "motion", "src", `_common.${s.id}.html`)).filter(existsSync),
|
|
126
|
+
];
|
|
127
|
+
const common = commonParts.length
|
|
128
|
+
? commonParts
|
|
129
|
+
.map(
|
|
130
|
+
(f, i) =>
|
|
131
|
+
(i ? `<!-- omnivideo:common · motion/src/${f.split("/").at(-1)} -->\n` : "") + readFileSync(f, "utf8"),
|
|
132
|
+
)
|
|
133
|
+
.join("\n")
|
|
134
|
+
: null;
|
|
135
|
+
const commonName = commonParts.length ? `motion/src/${commonParts[0].split("/").at(-1)}` : null;
|
|
136
|
+
|
|
137
|
+
/* Overview of the whole video in VIDEO seconds (beats, lines and words), for
|
|
138
|
+
the continuous composition: a system that carries from scene to scene is
|
|
139
|
+
drawn once, in video time, and each scene renders its own window. */
|
|
140
|
+
const abs = (c, s) => +(c.start + s).toFixed(4);
|
|
141
|
+
const overview = {
|
|
142
|
+
total_duration: video.total_duration,
|
|
143
|
+
scenes: video.scenes.map(c => ({
|
|
144
|
+
id: c.id,
|
|
145
|
+
start: c.start,
|
|
146
|
+
duration: c.duration,
|
|
147
|
+
beats: Object.fromEntries((c.beats ?? []).map(b => [b.element, abs(c, beatTime(c, b))])),
|
|
148
|
+
lines: (c.lines ?? []).map(l => ({
|
|
149
|
+
id: l.id,
|
|
150
|
+
text: l.text,
|
|
151
|
+
start: abs(c, l.start),
|
|
152
|
+
end: abs(c, l.end),
|
|
153
|
+
words: l.words?.map(p => ({ w: p.w, s: abs(c, l.start + p.s), e: abs(c, l.start + p.e) })) ?? null,
|
|
154
|
+
})),
|
|
155
|
+
})),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/* PT aliases on the same objects, so scene code written before the rename keeps
|
|
159
|
+
working (same values; `beats` is the same in both) */
|
|
160
|
+
const LEGACY_ALIASES =
|
|
161
|
+
"(o=>{const l=x=>Object.assign(x,{texto:x.text,inicio:x.start,fim:x.end,palavras:x.words});" +
|
|
162
|
+
"o.lines.forEach(l);o.video.scenes.forEach(s=>{s.lines.forEach(l);Object.assign(s,{inicio:s.start,duracao:s.duration,falas:s.lines})});" +
|
|
163
|
+
"Object.assign(o.video,{duracao_total:o.video.total_duration,cenas:o.video.scenes});" +
|
|
164
|
+
"Object.assign(o,{cena:o.scene,inicio:o.start,duracao:o.duration,duracao_total:o.total_duration,provisorio:o.provisional,falas:o.lines})})(window.OMNIVIDEO);";
|
|
165
|
+
|
|
166
|
+
/* --- 1. HTML of every scene -------------------------------------------------
|
|
167
|
+
It is just text, and keeps the injected times consistent between neighbours:
|
|
168
|
+
the transitions compare the two. Stills, sheets and render are for the
|
|
169
|
+
selected one. With --scene, only the selected scene's HTML is written in
|
|
170
|
+
place; its neighbours (for the cuts) go to temporary files next to it, which
|
|
171
|
+
keeps relative asset paths and never touches a scene another call may be
|
|
172
|
+
composing or rendering at the same moment. */
|
|
173
|
+
const built = {};
|
|
174
|
+
const scratch = [];
|
|
175
|
+
const sceneIndex = Object.fromEntries(video.scenes.map((s, i) => [s.id, i]));
|
|
176
|
+
const neighbour = id =>
|
|
177
|
+
onlyScene != null && id !== onlyScene && Math.abs(sceneIndex[id] - (sceneIndex[onlyScene] ?? -9)) === 1;
|
|
178
|
+
for (const scene of video.scenes) {
|
|
179
|
+
if (onlyScene && !selected(scene.id) && !neighbour(scene.id)) continue;
|
|
180
|
+
const src = join(dir, "motion", "src", `${scene.id}.html`);
|
|
181
|
+
if (!existsSync(src)) {
|
|
182
|
+
if (selected(scene.id)) {
|
|
183
|
+
console.error(`${scene.id}: falta ${src}`);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const beats = {};
|
|
190
|
+
const report = [];
|
|
191
|
+
for (const beat of scene.beats ?? []) {
|
|
192
|
+
const t = beatTime(scene, beat);
|
|
193
|
+
beats[beat.element] = t;
|
|
194
|
+
const source =
|
|
195
|
+
beat.enters_with && !video.provisional
|
|
196
|
+
? beat.enters_with
|
|
197
|
+
: beat.enters_with
|
|
198
|
+
? `${beat.enters_with} (estimado)`
|
|
199
|
+
: "provisório";
|
|
200
|
+
const slack = scene.duration - t;
|
|
201
|
+
report.push(
|
|
202
|
+
` ${beat.element.padEnd(18)} ${t.toFixed(2).padStart(6)}s ${source}${slack < HOLD + 0.6 ? " ⚠ pouco tempo até o fim da cena" : ""}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const vars = Object.entries(beats)
|
|
207
|
+
.map(([k, t]) => `--b-${k}:${t}s;`)
|
|
208
|
+
.join(" ");
|
|
209
|
+
const data = {
|
|
210
|
+
scene: scene.id,
|
|
211
|
+
start: scene.start,
|
|
212
|
+
duration: scene.duration,
|
|
213
|
+
total_duration: video.total_duration,
|
|
214
|
+
provisional: video.provisional,
|
|
215
|
+
beats,
|
|
216
|
+
lines: (scene.lines ?? []).map(l => ({
|
|
217
|
+
id: l.id,
|
|
218
|
+
text: l.text,
|
|
219
|
+
start: l.start,
|
|
220
|
+
end: l.end,
|
|
221
|
+
words: l.words ?? null,
|
|
222
|
+
})),
|
|
223
|
+
video: overview,
|
|
224
|
+
};
|
|
225
|
+
const injection =
|
|
226
|
+
`<style id="omnivideo-beats">:root{ ${vars} --scene-dur:${scene.duration}s; --scene-start:${scene.start}s; --video-dur:${video.total_duration}s;` +
|
|
227
|
+
` --cena-dur:${scene.duration}s; --cena-inicio:${scene.start}s; }</style>\n` +
|
|
228
|
+
`<script id="omnivideo-data">window.OMNIVIDEO = ${JSON.stringify(data)};\n${LEGACY_ALIASES}</script>\n`;
|
|
229
|
+
|
|
230
|
+
let html = readFileSync(src, "utf8");
|
|
231
|
+
if (!html.includes("</head>")) throw new Error(`${src}: sem </head>`);
|
|
232
|
+
html = html.replace(
|
|
233
|
+
/<style id="omnivideo-beats">[\s\S]*?<\/style>\n?|<script id="omnivideo-(?:data|dados)">[\s\S]*?<\/script>\n?/g,
|
|
234
|
+
"",
|
|
235
|
+
);
|
|
236
|
+
/* replace with a function: line text or code containing `$&` doesn't become a pattern */
|
|
237
|
+
html = html.replace("</head>", () => `${injection}</head>`);
|
|
238
|
+
if (common) {
|
|
239
|
+
/* only the marker alone on its line: quoted in a comment, it doesn't count.
|
|
240
|
+
omnivideo:comum = legacy marker */
|
|
241
|
+
const markerLine = /^[ \t]*<!-- omnivideo:(?:common|comum) -->[ \t]*$/m;
|
|
242
|
+
if (markerLine.test(html))
|
|
243
|
+
html = html.replace(markerLine, () => `<!-- omnivideo:common · ${commonName} -->\n${common}`);
|
|
244
|
+
else if (selected(scene.id))
|
|
245
|
+
console.warn(`${scene.id}: ⚠ existe ${commonName}, mas a cena não tem ${MARKER} sozinho numa linha`);
|
|
246
|
+
}
|
|
247
|
+
/* Components (templates/components/<name>/<name>.js) are pasted in, so the built
|
|
248
|
+
scene is one self-contained file. Marker alone on its line:
|
|
249
|
+
<!-- omnivideo:component voiceavatar --> the component's code
|
|
250
|
+
<!-- omnivideo:envelope --> window.VOICE_ENVELOPE of THIS scene (voice-envelope.mjs),
|
|
251
|
+
null while the scene has no voice yet: the orb idles */
|
|
252
|
+
html = html.replace(/^[ \t]*<!-- omnivideo:component ([\w-]+) -->[ \t]*$/gm, (_, name) => {
|
|
253
|
+
const file = join(CODE_ROOT, "templates", "components", name, `${name}.js`);
|
|
254
|
+
if (!existsSync(file)) {
|
|
255
|
+
console.warn(`${scene.id}: ⚠ componente "${name}" não existe em ${file}`);
|
|
256
|
+
return `<!-- omnivideo:component ${name} · não encontrado -->`;
|
|
257
|
+
}
|
|
258
|
+
const code = readFileSync(file, "utf8");
|
|
259
|
+
/* a literal closing tag anywhere in the code, comments included, ends the inline script */
|
|
260
|
+
if (/<\/script/i.test(code)) throw new Error(`${file}: contém "</script" e quebraria o <script> inline`);
|
|
261
|
+
return `<!-- omnivideo:component ${name} -->\n<script>\n${code}</script>`;
|
|
262
|
+
});
|
|
263
|
+
html = html.replace(/^[ \t]*<!-- omnivideo:envelope -->[ \t]*$/m, () => {
|
|
264
|
+
const r = spawnSync("node", [join(CODE_ROOT, "tools", "voice-envelope.mjs"), "--scene", dir, scene.id], {
|
|
265
|
+
maxBuffer: 1 << 28,
|
|
266
|
+
});
|
|
267
|
+
const env = r.status === 0 ? r.stdout.toString().trim() : "null";
|
|
268
|
+
if (r.status !== 0 && selected(scene.id) && !video.provisional)
|
|
269
|
+
console.warn(
|
|
270
|
+
`${scene.id}: ⚠ sem envelope de voz (${r.stderr.toString().trim()}); o orbe fica só em movimento ocioso`,
|
|
271
|
+
);
|
|
272
|
+
return `<!-- omnivideo:envelope -->\n<script>window.VOICE_ENVELOPE = ${env};</script>`;
|
|
273
|
+
});
|
|
274
|
+
const finalOut = join(dir, scene.html ?? `motion/${scene.id}.html`);
|
|
275
|
+
const out = neighbour(scene.id) ? join(dirname(finalOut), `.${scene.id}.neighbour-${process.pid}.html`) : finalOut;
|
|
276
|
+
if (out !== finalOut) scratch.push(out);
|
|
277
|
+
writeFileSync(out, html);
|
|
278
|
+
built[scene.id] = { out, beats };
|
|
279
|
+
|
|
280
|
+
if (selected(scene.id)) {
|
|
281
|
+
console.log(
|
|
282
|
+
`${scene.id} · ${scene.duration.toFixed(2)}s a partir de ${scene.start.toFixed(2)}s${video.provisional ? " (provisório)" : ""} → ${out}`,
|
|
283
|
+
);
|
|
284
|
+
report.forEach(l => console.log(l));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* --- 2. stills, sheets and render of the selected scene --------------------- */
|
|
289
|
+
mkdirSync(join(dir, "motion", "stills"), { recursive: true });
|
|
290
|
+
const ok = [];
|
|
291
|
+
for (const scene of video.scenes) {
|
|
292
|
+
if (!selected(scene.id) || !built[scene.id]) continue;
|
|
293
|
+
const { out, beats } = built[scene.id];
|
|
294
|
+
const times = Object.values(beats).sort((a, b) => a - b);
|
|
295
|
+
const composition =
|
|
296
|
+
flag("stills") || flag("sheets")
|
|
297
|
+
? keepPrevious(dir, "composition", scene.id, [
|
|
298
|
+
`motion/stills/${scene.id}.png`,
|
|
299
|
+
`motion/stills/${scene.id}-sheet.png`,
|
|
300
|
+
])
|
|
301
|
+
: null;
|
|
302
|
+
if (flag("stills")) {
|
|
303
|
+
const t = Math.min((times.at(-1) ?? 0) + HOLD + 0.6, scene.duration - 1 / fps);
|
|
304
|
+
const png = join(dir, "motion", "stills", `${scene.id}.png`);
|
|
305
|
+
render(out, "--still", png, "--at", t.toFixed(3));
|
|
306
|
+
console.log(`${scene.id} · still em ${t.toFixed(2)}s → ${png}`);
|
|
307
|
+
}
|
|
308
|
+
if (flag("sheets")) {
|
|
309
|
+
const marks = [0, ...times.map(t => t + 0.3), scene.duration - 1 / fps]
|
|
310
|
+
.map(t => Math.min(Math.max(t, 0), scene.duration - 1 / fps))
|
|
311
|
+
.filter((t, i, a) => a.indexOf(t) === i)
|
|
312
|
+
.sort((a, b) => a - b);
|
|
313
|
+
const png = join(dir, "motion", "stills", `${scene.id}-sheet.png`);
|
|
314
|
+
render(out, "--sheet", png, "--at", marks.map(t => t.toFixed(2)).join(","));
|
|
315
|
+
console.log(`${scene.id} · folha em ${marks.map(t => t.toFixed(1)).join(", ")}s → ${png}`);
|
|
316
|
+
}
|
|
317
|
+
composition?.close();
|
|
318
|
+
if (flag("render")) {
|
|
319
|
+
const mp4 = join(dir, scene.mp4 ?? `motion/${scene.id}.mp4`);
|
|
320
|
+
const previousRender = keepPrevious(dir, "render", scene.id, [
|
|
321
|
+
scene.mp4 ?? `motion/${scene.id}.mp4`,
|
|
322
|
+
`motion/${scene.id}.render.json`,
|
|
323
|
+
]);
|
|
324
|
+
render(out, mp4, "--dur", String(scene.duration), "--fps", String(fps));
|
|
325
|
+
/* a scene with its background in video time depends on its own start: if an
|
|
326
|
+
earlier scene changes duration, this one must render again. status and
|
|
327
|
+
assemble compare this record with scenes.json. */
|
|
328
|
+
const videoTime = /var\(--(?:scene-start|cena-inicio)|OMNIVIDEO\??\.(?:start|inicio)/.test(
|
|
329
|
+
readFileSync(out, "utf8"),
|
|
330
|
+
);
|
|
331
|
+
writeFileSync(
|
|
332
|
+
join(dir, "motion", `${scene.id}.render.json`),
|
|
333
|
+
JSON.stringify({ start: scene.start, duration: scene.duration, video_time: videoTime }) + "\n",
|
|
334
|
+
);
|
|
335
|
+
previousRender.close();
|
|
336
|
+
console.log(`${scene.id} · render → ${mp4}${videoTime ? " (fundo no tempo do vídeo)" : ""}`);
|
|
337
|
+
}
|
|
338
|
+
ok.push(scene.id);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* --- 3. transitions ----------------------------------------------------------
|
|
342
|
+
The outgoing scene is captured at t = duration, the cut instant: the frame
|
|
343
|
+
that would come after its last one. If nothing jumps, the incoming one at
|
|
344
|
+
t = 0 matches it in everything that continues, and the overlay comes out sharp. */
|
|
345
|
+
const MEASUREMENTS = "MEASUREMENTS ";
|
|
346
|
+
function readMeasurements(renderOut) {
|
|
347
|
+
const line = renderOut.split("\n").find(l => l.startsWith(MEASUREMENTS));
|
|
348
|
+
return line ? JSON.parse(line.slice(MEASUREMENTS.length)) : [];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* Three panels, always in this order: outgoing | incoming | both at 50 %.
|
|
352
|
+
No labels: this machine's ffmpeg has no drawtext. */
|
|
353
|
+
function transitionSheet(pngA, pngB, out) {
|
|
354
|
+
const width = w >= h ? 640 : 360;
|
|
355
|
+
const graph =
|
|
356
|
+
`[0]split[a0][a1];[1]split[b0][b1];[a1][b1]blend=all_mode=average[m];` +
|
|
357
|
+
`[a0]scale=${width}:-2,pad=iw+8:ih:0:0:white[A];[b0]scale=${width}:-2,pad=iw+8:ih:0:0:white[B];[m]scale=${width}:-2[M];` +
|
|
358
|
+
`[A][B][M]hstack=inputs=3,pad=iw+16:ih+16:8:8:white`;
|
|
359
|
+
const r = spawnSync(
|
|
360
|
+
"ffmpeg",
|
|
361
|
+
["-y", "-loglevel", "error", "-i", pngA, "-i", pngB, "-filter_complex", graph, "-frames:v", "1", out],
|
|
362
|
+
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
363
|
+
);
|
|
364
|
+
if (r.status !== 0) throw new Error(`ffmpeg (folha de passagem): ${r.stderr}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function transitionKind(p) {
|
|
368
|
+
if (p == null || p === "cut") return { kind: "corte seco", continues: [] };
|
|
369
|
+
if (Array.isArray(p?.continues) && p.continues.length)
|
|
370
|
+
return { kind: `continua: ${p.continues.join(", ")}`, continues: p.continues };
|
|
371
|
+
/* { sound } without continues: hard cut with an effect at the cut (sound.mjs) */
|
|
372
|
+
if (p && typeof p === "object" && p.sound && p.continues == null) return { kind: "corte seco", continues: [] };
|
|
373
|
+
return {
|
|
374
|
+
kind: `⚠ passagem inválida (${JSON.stringify(p)}); use "cut", { "continues": [...] } ou { "sound": ... }`,
|
|
375
|
+
continues: [],
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (flag("transitions")) {
|
|
380
|
+
const tmp = join(tmpdir(), `transitions-${process.pid}`);
|
|
381
|
+
mkdirSync(tmp, { recursive: true });
|
|
382
|
+
try {
|
|
383
|
+
for (let i = 1; i < video.scenes.length; i++) {
|
|
384
|
+
const a = video.scenes[i - 1],
|
|
385
|
+
b = video.scenes[i];
|
|
386
|
+
if (onlyScene && onlyScene !== a.id && onlyScene !== b.id) continue;
|
|
387
|
+
if (!built[a.id] || !built[b.id]) {
|
|
388
|
+
console.warn(`${a.id} → ${b.id}: falta o HTML de uma das duas`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const { kind, continues } = transitionKind(b.transition);
|
|
392
|
+
/* on a hard cut what matters is the outgoing scene's real last frame; when
|
|
393
|
+
it continues, the cut instant (in a continuous composition it is
|
|
394
|
+
already the next one's first frame) */
|
|
395
|
+
const tA = continues.length ? a.duration : Math.max(0, a.duration - 1 / fps);
|
|
396
|
+
const pngA = join(tmp, `${a.id}.png`),
|
|
397
|
+
pngB = join(tmp, `${b.id}.png`);
|
|
398
|
+
const ma = readMeasurements(render(built[a.id].out, "--still", pngA, "--at", tA.toFixed(4), "--measure"));
|
|
399
|
+
const mb = readMeasurements(render(built[b.id].out, "--still", pngB, "--at", "0", "--measure"));
|
|
400
|
+
const png = join(dir, "motion", "stills", `transition-${a.id}-${b.id}.png`);
|
|
401
|
+
const previousCut = keepPrevious(dir, "composition", `transition-${a.id}-${b.id}`, [
|
|
402
|
+
`motion/stills/transition-${a.id}-${b.id}.png`,
|
|
403
|
+
]);
|
|
404
|
+
/* rendered aside and renamed: two scene calls running in parallel share this cut */
|
|
405
|
+
const pngTmp = join(dirname(png), `.transition-${a.id}-${b.id}-${process.pid}.png`);
|
|
406
|
+
transitionSheet(pngA, pngB, pngTmp);
|
|
407
|
+
renameSync(pngTmp, png);
|
|
408
|
+
previousCut.close();
|
|
409
|
+
|
|
410
|
+
console.log(`${a.id} → ${b.id} · ${kind} → ${png}`);
|
|
411
|
+
for (const name of continues) {
|
|
412
|
+
const x = ma.find(m => m.name === name),
|
|
413
|
+
y = mb.find(m => m.name === name);
|
|
414
|
+
if (!x || !y) {
|
|
415
|
+
console.log(
|
|
416
|
+
` ${name.padEnd(18)} ⚠ falta [data-continues="${name}"] em ${[!x && a.id, !y && b.id].filter(Boolean).join(" e ")}`,
|
|
417
|
+
);
|
|
418
|
+
process.exitCode = 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const d = Math.max(Math.abs(x.x - y.x), Math.abs(x.y - y.y), Math.abs(x.w - y.w), Math.abs(x.h - y.h));
|
|
422
|
+
const dOp = Math.abs(x.opacity - y.opacity);
|
|
423
|
+
const jumps = d > TOL_PX || dOp > 0.05;
|
|
424
|
+
console.log(
|
|
425
|
+
` ${name.padEnd(18)} desvio ${d.toFixed(1)} px · opacidade ${x.opacity.toFixed(2)} → ${y.opacity.toFixed(2)}` +
|
|
426
|
+
(jumps ? " ⚠ pula no corte" : " ok"),
|
|
427
|
+
);
|
|
428
|
+
if (jumps) process.exitCode = 1;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} finally {
|
|
432
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for (const f of scratch) rmSync(f, { force: true });
|
|
437
|
+
if (!ok.length) process.exitCode = 1;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
Burned-in captions: where they come from and where they sit. Used by
|
|
3
|
+
assemble.mjs (render) and by the studio (preview), so both see the same HTML.
|
|
4
|
+
|
|
5
|
+
Each caption in captions.srt gets an id `<line>.<n>` (the n-th caption of
|
|
6
|
+
that line; a caption never spans two lines) and its scene.
|
|
7
|
+
|
|
8
|
+
The position lives in `captions` in scenes.json, on three levels, each one
|
|
9
|
+
over the previous:
|
|
10
|
+
|
|
11
|
+
"captions": {
|
|
12
|
+
"platform": "all", safe area: shorts, tiktok, reels, all (9:16) · youtube (16:9)
|
|
13
|
+
"anchor": "bottom-center", 3×3 grid: top|middle|bottom - left|center|right
|
|
14
|
+
"x": 0, "y": 0, offset in frame px (+x right, +y down)
|
|
15
|
+
"style": "shadow", shadow | pill (default in the config's captions.style)
|
|
16
|
+
"safe": { "bottom": 0.12 }, this video's safe area tweak, per side (fraction of the frame)
|
|
17
|
+
"color": "#ffd400", text color
|
|
18
|
+
"background": "#000000", "background_opacity": 0.7, pill: background color and opacity
|
|
19
|
+
"outline": 4, "outline_color": "#000000", text outline: width (frame px) and color
|
|
20
|
+
"shadow_color": "#000000", "shadow_opacity": 0.5, shadow: color, opacity,
|
|
21
|
+
"shadow_blur": 12, "shadow_x": 0, "shadow_y": 6 blur and offset (frame px)
|
|
22
|
+
"scenes": { "c04": { "anchor": "top-center", "y": 40 } },
|
|
23
|
+
"individual": { "f03.2": { "x": -30 }, "f04.1": { "hidden": true } }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
The anchor pins the caption box to the edge (or center) of the safe area;
|
|
27
|
+
the offset moves from there. A caption may go outside the safe area: only
|
|
28
|
+
the frame limits it, and what goes past is flagged (`outside`, with side and
|
|
29
|
+
px). Each platform's safe area is in the config's `captions.safe_area`, as a
|
|
30
|
+
fraction of the frame; `all` is the most restrictive on each side. `safe` in
|
|
31
|
+
scenes.json overrides only the sides it lists, for that video.
|
|
32
|
+
|
|
33
|
+
`hidden: true` on an individual caption turns it off: it stays in
|
|
34
|
+
captions.srt, but assemble doesn't burn it (captionsOf flags `hidden`, and
|
|
35
|
+
assemble drops it). Without the key, the caption shows.
|
|
36
|
+
|
|
37
|
+
A caption's text can be edited in the UI: the new one goes straight to
|
|
38
|
+
captions.srt (that's what assemble and Premiere read), and `captions.texts`
|
|
39
|
+
keeps the swap so the next generation doesn't lose it:
|
|
40
|
+
|
|
41
|
+
"texts": { "f03.2": { "text": "de 70 a 500 ms", "original": "de 70 a 500 milissegundos" } }
|
|
42
|
+
|
|
43
|
+
`original` is what the line generated. `omnivideo.py captions` puts the
|
|
44
|
+
edited text back on the caption of the same line that still generates the
|
|
45
|
+
same `original`; if the line changed and no longer does, the edit is
|
|
46
|
+
dropped, with a warning.
|
|
47
|
+
|
|
48
|
+
Color, outline and shadow follow the same three levels as the position
|
|
49
|
+
(video → scene → caption): a key absent from every level gets the style's
|
|
50
|
+
default (`styleDefaults`). Touching any shadow key replaces the style's
|
|
51
|
+
shadow with a single layer, with the five values.
|
|
52
|
+
============================================================================ */
|
|
53
|
+
|
|
54
|
+
import { readFileSync, existsSync, writeFileSync, renameSync } from "node:fs";
|
|
55
|
+
import { join } from "node:path";
|
|
56
|
+
import { CODE_ROOT, DATA_ROOT as ROOT } from "./lib/paths.mjs";
|
|
57
|
+
|
|
58
|
+
const CFG = JSON.parse(readFileSync(join(ROOT, "omnivideo.config.json"), "utf8"));
|
|
59
|
+
|
|
60
|
+
export const ROWS = ["top", "middle", "bottom"];
|
|
61
|
+
export const COLUMNS = ["left", "center", "right"];
|
|
62
|
+
export const ANCHORS = ROWS.flatMap(r => COLUMNS.map(c => `${r}-${c}`));
|
|
63
|
+
export const STYLES = ["pill", "shadow"];
|
|
64
|
+
export const SIDES = ["top", "bottom", "left", "right"];
|
|
65
|
+
|
|
66
|
+
/* color, outline and shadow: key → { type, range } (color as #rrggbb; the rest, a number) */
|
|
67
|
+
export const STYLE_KEYS = [
|
|
68
|
+
"color",
|
|
69
|
+
"background",
|
|
70
|
+
"background_opacity",
|
|
71
|
+
"outline",
|
|
72
|
+
"outline_color",
|
|
73
|
+
"shadow_color",
|
|
74
|
+
"shadow_opacity",
|
|
75
|
+
"shadow_blur",
|
|
76
|
+
"shadow_x",
|
|
77
|
+
"shadow_y",
|
|
78
|
+
];
|
|
79
|
+
export const STYLE_LIMITS = {
|
|
80
|
+
background_opacity: [0, 1, 2],
|
|
81
|
+
outline: [0, 40, 0],
|
|
82
|
+
shadow_opacity: [0, 1, 2],
|
|
83
|
+
shadow_blur: [0, 100, 0],
|
|
84
|
+
shadow_x: [-100, 100, 0],
|
|
85
|
+
shadow_y: [-100, 100, 0] /* [min, max, decimals] */,
|
|
86
|
+
};
|
|
87
|
+
export const STYLE_COLORS = ["color", "background", "outline_color", "shadow_color"];
|
|
88
|
+
|
|
89
|
+
/* what each style draws untouched, in frame px (same as the template's CSS) */
|
|
90
|
+
export function styleDefaults(format) {
|
|
91
|
+
const body = ((aspectOf(format) === "16:9" ? 2.4 : 4.4) / 100) * format.w;
|
|
92
|
+
const vw = n => Math.round((n / 100) * format.w);
|
|
93
|
+
const common = {
|
|
94
|
+
background: "#ffffff",
|
|
95
|
+
background_opacity: 0.9,
|
|
96
|
+
outline: 0,
|
|
97
|
+
outline_color: "#000000",
|
|
98
|
+
shadow_color: "#000000",
|
|
99
|
+
shadow_x: 0,
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
pill: { ...common, color: "#111111", shadow_opacity: 0.18, shadow_blur: vw(2.4), shadow_y: vw(0.4) },
|
|
103
|
+
shadow: {
|
|
104
|
+
...common,
|
|
105
|
+
color: "#ffffff",
|
|
106
|
+
shadow_opacity: 0.55,
|
|
107
|
+
shadow_blur: Math.round(0.12 * body),
|
|
108
|
+
shadow_y: Math.round(0.04 * body),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const aspectOf = format => format.aspect ?? (format.w < format.h ? "9:16" : "16:9");
|
|
114
|
+
|
|
115
|
+
/* a caption's character limit (past it, the template breaks into two lines) */
|
|
116
|
+
export const maxChars = format => CFG.captions?.max_chars?.[aspectOf(format)] ?? null;
|
|
117
|
+
|
|
118
|
+
/* the format's safe areas, plus `all` (the largest inset on each side) */
|
|
119
|
+
export function safeAreas(format) {
|
|
120
|
+
const table = { ...(CFG.captions?.safe_area?.[aspectOf(format)] ?? {}) };
|
|
121
|
+
const vals = Object.values(table);
|
|
122
|
+
if (vals.length > 1) table.all = Object.fromEntries(SIDES.map(s => [s, Math.max(...vals.map(v => v[s] ?? 0))]));
|
|
123
|
+
return table;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* the video's position with the config defaults underneath */
|
|
127
|
+
export function positionOf(video) {
|
|
128
|
+
const defaults = {
|
|
129
|
+
platform: "all",
|
|
130
|
+
anchor: "bottom-center",
|
|
131
|
+
x: 0,
|
|
132
|
+
y: 0,
|
|
133
|
+
style: CFG.captions?.style ?? "shadow",
|
|
134
|
+
...(CFG.captions?.default_position ?? {}),
|
|
135
|
+
};
|
|
136
|
+
const p = { ...defaults, ...(video.captions ?? {}) };
|
|
137
|
+
const safes = safeAreas(video.format);
|
|
138
|
+
if (!safes[p.platform]) p.platform = Object.keys(safes)[0] ?? "all";
|
|
139
|
+
return p;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const srtTime = s => {
|
|
143
|
+
const [h, m, r] = s.trim().replace(",", ".").split(":");
|
|
144
|
+
return +h * 3600 + +m * 60 + +r;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export function readSrt(file) {
|
|
148
|
+
return readFileSync(file, "utf8")
|
|
149
|
+
.replace(/\r/g, "")
|
|
150
|
+
.split(/\n\s*\n/)
|
|
151
|
+
.flatMap(block => {
|
|
152
|
+
const rows = block.trim().split("\n");
|
|
153
|
+
const i = rows.findIndex(l => l.includes("-->"));
|
|
154
|
+
if (i < 0) return [];
|
|
155
|
+
const [a, b] = rows[i].split("-->");
|
|
156
|
+
const text = rows
|
|
157
|
+
.slice(i + 1)
|
|
158
|
+
.join(" ")
|
|
159
|
+
.trim();
|
|
160
|
+
return text ? [{ s: srtTime(a), e: srtTime(b), text }] : [];
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* replaces the text of the `index`-th caption (readSrt order), leaving the rest of the file untouched */
|
|
165
|
+
export function replaceSrtText(file, index, text) {
|
|
166
|
+
let k = -1;
|
|
167
|
+
const blocks = readFileSync(file, "utf8")
|
|
168
|
+
.replace(/\r/g, "")
|
|
169
|
+
.split(/\n\s*\n/)
|
|
170
|
+
.map(block => {
|
|
171
|
+
const rows = block.trim().split("\n");
|
|
172
|
+
const i = rows.findIndex(l => l.includes("-->"));
|
|
173
|
+
if (
|
|
174
|
+
i < 0 ||
|
|
175
|
+
!rows
|
|
176
|
+
.slice(i + 1)
|
|
177
|
+
.join(" ")
|
|
178
|
+
.trim()
|
|
179
|
+
)
|
|
180
|
+
return block.trim();
|
|
181
|
+
k += 1;
|
|
182
|
+
return k === index ? [...rows.slice(0, i + 1), text].join("\n") : block.trim();
|
|
183
|
+
});
|
|
184
|
+
if (index > k) throw new Error(`o captions.srt não tem a legenda ${index + 1}`);
|
|
185
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
186
|
+
writeFileSync(tmp, blocks.filter(Boolean).join("\n\n") + "\n");
|
|
187
|
+
renameSync(tmp, file);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* [{ id, scene, line, s, e, text }] in video time; `video` comes from loadVideo() */
|
|
191
|
+
export function captionsOf(dir, video) {
|
|
192
|
+
const file = join(dir, "captions.srt");
|
|
193
|
+
if (!existsSync(file)) return [];
|
|
194
|
+
const lines = video.scenes.flatMap(sc =>
|
|
195
|
+
(sc.lines ?? []).map(l => ({ id: l.id, scene: sc.id, a: sc.start + l.start, b: sc.start + l.end })),
|
|
196
|
+
);
|
|
197
|
+
const count = {};
|
|
198
|
+
return readSrt(file).map((cue, k) => {
|
|
199
|
+
const mid = (cue.s + cue.e) / 2;
|
|
200
|
+
const l =
|
|
201
|
+
lines.find(x => mid >= x.a - 0.05 && mid <= x.b + 0.05) ??
|
|
202
|
+
lines.reduce((m, x) => (Math.abs((x.a + x.b) / 2 - mid) < Math.abs((m.a + m.b) / 2 - mid) ? x : m), lines[0]);
|
|
203
|
+
const scene = l?.scene ?? video.scenes.find(sc => mid >= sc.start && mid < sc.start + sc.duration)?.id ?? null;
|
|
204
|
+
const base = l?.id ?? `l${k + 1}`;
|
|
205
|
+
count[base] = (count[base] ?? 0) + 1;
|
|
206
|
+
const id = l ? `${base}.${count[base]}` : base;
|
|
207
|
+
/* edited in the UI: `original` is what the line generated */
|
|
208
|
+
const ed = video.captions?.texts?.[id];
|
|
209
|
+
return {
|
|
210
|
+
id,
|
|
211
|
+
scene,
|
|
212
|
+
line: l?.id ?? null,
|
|
213
|
+
s: +cue.s.toFixed(3),
|
|
214
|
+
e: +cue.e.toFixed(3),
|
|
215
|
+
text: cue.text,
|
|
216
|
+
hidden: video.captions?.individual?.[id]?.hidden === true,
|
|
217
|
+
...(ed?.text === cue.text && ed.original ? { original: ed.original } : {}),
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* templates/captions.html with the data, the position and the video's _captions.css */
|
|
223
|
+
export function captionsHtml(dir, video, captions = captionsOf(dir, video)) {
|
|
224
|
+
const pos = positionOf(video);
|
|
225
|
+
const { w, h } = video.format;
|
|
226
|
+
const cssFile = join(dir, "motion", "src", "_captions.css");
|
|
227
|
+
const css = existsSync(cssFile) ? readFileSync(cssFile, "utf8") : "";
|
|
228
|
+
const data = {
|
|
229
|
+
w,
|
|
230
|
+
h,
|
|
231
|
+
safes: safeAreas(video.format),
|
|
232
|
+
position: pos,
|
|
233
|
+
keys: ["anchor", "x", "y", ...STYLE_KEYS],
|
|
234
|
+
defaults: styleDefaults(video.format),
|
|
235
|
+
captions,
|
|
236
|
+
};
|
|
237
|
+
const json = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
238
|
+
const html = readFileSync(join(CODE_ROOT, "templates", "captions.html"), "utf8")
|
|
239
|
+
.replace(
|
|
240
|
+
'<html lang="pt-BR">',
|
|
241
|
+
`<html lang="pt-BR" data-format="${aspectOf(video.format)}" data-style="${pos.style}">`,
|
|
242
|
+
)
|
|
243
|
+
.replace(/\/\*omnivideo:data\*\/\{[\s\S]*?\};/, () => `${json};`)
|
|
244
|
+
.replace("/*omnivideo:video-style*/", () => css);
|
|
245
|
+
return { html, pos, css: !!css };
|
|
246
|
+
}
|