@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,217 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
Clips: where each piece of animation (a scene's MP4) and of voice (a line's
|
|
3
|
+
WAV) shows up in the video.
|
|
4
|
+
|
|
5
|
+
The voice owns time: the scene starts at `scene.start` and lasts
|
|
6
|
+
`scene.duration`; the line sits at `scene.start + line.start`. The studio
|
|
7
|
+
editor lets you change that without redoing anything, through clips:
|
|
8
|
+
|
|
9
|
+
scene.clips [{ from, to, offset, speed? }] pieces of the scene MP4
|
|
10
|
+
line.clips [{ from, to, offset, speed? }] pieces of the line WAV
|
|
11
|
+
|
|
12
|
+
`from`/`to` are seconds inside the file; `offset` is how far the piece moves
|
|
13
|
+
from its own place (negative = earlier). `speed` (0.5 to 2, default 1) plays
|
|
14
|
+
the piece faster: it lasts (to − from) / speed in the video, starting at the
|
|
15
|
+
same place, and what came after stays put (the gap holds the last frame, or
|
|
16
|
+
is silent). The voice keeps its pitch (atempo). The razor splits a clip in two;
|
|
17
|
+
deleting a clip removes the piece; dragging changes `offset`. Without
|
|
18
|
+
`clips`, the whole file is one clip (for a scene, with the old
|
|
19
|
+
`video_offset`, if any).
|
|
20
|
+
|
|
21
|
+
Beat and transition effects move with the animation piece they fall in;
|
|
22
|
+
captions and ducking move with the voice piece.
|
|
23
|
+
|
|
24
|
+
Image on a single track, like an editor: where two pieces overlap, the one
|
|
25
|
+
that started last shows; in a gap, the last frame of the piece that ended
|
|
26
|
+
before holds (or the first frame of the first one, before everything).
|
|
27
|
+
|
|
28
|
+
assemble.mjs and sound.mjs use this; so does the studio screen. Times in
|
|
29
|
+
scenes.json seconds (without the range cuts).
|
|
30
|
+
============================================================================ */
|
|
31
|
+
|
|
32
|
+
const EPS = 1e-6;
|
|
33
|
+
/* `video_em` = legacy name of `video_offset` */
|
|
34
|
+
export const videoOffset = scene => Number(scene?.video_offset ?? scene?.video_em) || 0;
|
|
35
|
+
|
|
36
|
+
export const CLIP_SPEED = [0.5, 2];
|
|
37
|
+
/* a clip's speed, inside the range (1 when absent) */
|
|
38
|
+
export const clipSpeed = c => {
|
|
39
|
+
const v = Number(c?.speed);
|
|
40
|
+
return Number.isFinite(v) && v > 0 ? Math.min(CLIP_SPEED[1], Math.max(CLIP_SPEED[0], v)) : 1;
|
|
41
|
+
};
|
|
42
|
+
/* how long the piece lasts in the video */
|
|
43
|
+
export const clipLength = c => (c.to - c.from) / clipSpeed(c);
|
|
44
|
+
/* `speed` only when it isn't 1, so an untouched clip stays { from, to, offset } */
|
|
45
|
+
const normal = c => {
|
|
46
|
+
const v = clipSpeed(c);
|
|
47
|
+
return { from: +c.from, to: +c.to, offset: Number(c.offset) || 0, ...(v !== 1 ? { speed: v } : {}) };
|
|
48
|
+
};
|
|
49
|
+
export const lineDuration = l => l.dur ?? l.end - l.start;
|
|
50
|
+
|
|
51
|
+
export function videoClips(scene) {
|
|
52
|
+
return Array.isArray(scene.clips)
|
|
53
|
+
? scene.clips.map(normal)
|
|
54
|
+
: [{ from: 0, to: scene.duration, offset: videoOffset(scene) }];
|
|
55
|
+
}
|
|
56
|
+
export function lineClips(line) {
|
|
57
|
+
return Array.isArray(line.clips) ? line.clips.map(normal) : [{ from: 0, to: lineDuration(line), offset: 0 }];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* the arrangement was edited: some clip moved, cut or deleted */
|
|
61
|
+
const isDefault = (clips, dur) =>
|
|
62
|
+
clips.length === 1 &&
|
|
63
|
+
Math.abs(clips[0].from) < EPS &&
|
|
64
|
+
Math.abs(clips[0].to - dur) < 1e-3 &&
|
|
65
|
+
!clips[0].offset &&
|
|
66
|
+
clipSpeed(clips[0]) === 1;
|
|
67
|
+
export const videoEdited = scenes => scenes.some(c => !isDefault(videoClips(c), c.duration));
|
|
68
|
+
export const voiceEdited = scenes =>
|
|
69
|
+
scenes.some(c => (c.lines ?? []).some(l => !isDefault(lineClips(l), lineDuration(l))));
|
|
70
|
+
|
|
71
|
+
/* every animation piece: { i (scene), k (clip), from, to, offset, speed, start and end (in the video) } */
|
|
72
|
+
export function videoPieces(scenes) {
|
|
73
|
+
return scenes.flatMap((c, i) =>
|
|
74
|
+
videoClips(c).map((cl, k) => ({
|
|
75
|
+
i,
|
|
76
|
+
k,
|
|
77
|
+
from: cl.from,
|
|
78
|
+
to: cl.to,
|
|
79
|
+
offset: cl.offset,
|
|
80
|
+
speed: clipSpeed(cl),
|
|
81
|
+
start: c.start + cl.from + cl.offset,
|
|
82
|
+
end: c.start + cl.from + cl.offset + clipLength(cl),
|
|
83
|
+
})),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
/* every voice piece: { scene, line, k, from, to, offset, speed, start } */
|
|
87
|
+
export function voicePieces(scenes) {
|
|
88
|
+
return scenes.flatMap(c =>
|
|
89
|
+
(c.lines ?? []).flatMap(l =>
|
|
90
|
+
lineClips(l).map((cl, k) => ({
|
|
91
|
+
scene: c,
|
|
92
|
+
line: l,
|
|
93
|
+
k,
|
|
94
|
+
from: cl.from,
|
|
95
|
+
to: cl.to,
|
|
96
|
+
offset: cl.offset,
|
|
97
|
+
speed: clipSpeed(cl),
|
|
98
|
+
start: c.start + l.start + cl.from + cl.offset,
|
|
99
|
+
})),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* the instant, in the video, of a `local` point of the scene's animation (null: fell in a deleted piece) */
|
|
105
|
+
export function animationInstant(scene, local) {
|
|
106
|
+
const cl = videoClips(scene).find(x => local >= x.from - EPS && local < x.to + EPS);
|
|
107
|
+
return cl ? scene.start + cl.from + cl.offset + (local - cl.from) / clipSpeed(cl) : null;
|
|
108
|
+
}
|
|
109
|
+
/* the start of the scene's animation in the video (the scene change): the first piece's */
|
|
110
|
+
export function animationStart(scene) {
|
|
111
|
+
const cl = videoClips(scene).sort((a, b) => a.from - b.from)[0];
|
|
112
|
+
return cl ? scene.start + cl.from + cl.offset : null;
|
|
113
|
+
}
|
|
114
|
+
/* a span [a, b] inside the line, in the video: one piece per clip that holds it (the rest was deleted) */
|
|
115
|
+
export function voiceSpans(scene, line, a, b) {
|
|
116
|
+
const base = scene.start + line.start;
|
|
117
|
+
return lineClips(line).flatMap(cl => {
|
|
118
|
+
const from = Math.max(a, cl.from),
|
|
119
|
+
to = Math.min(b, cl.to);
|
|
120
|
+
const at = x => base + cl.from + cl.offset + (x - cl.from) / clipSpeed(cl);
|
|
121
|
+
return to > from + EPS ? [[at(from), at(to)]] : [];
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* what shows at instant t: { i (scene), local (second inside the MP4), frozen, speed (how fast the
|
|
126
|
+
MP4 runs there) } */
|
|
127
|
+
export function frameAt(scenes, t) {
|
|
128
|
+
const pieces = videoPieces(scenes);
|
|
129
|
+
if (!pieces.length) return { i: 0, local: 0, frozen: true, speed: 1 };
|
|
130
|
+
let covering = null;
|
|
131
|
+
for (const p of pieces) if (p.start <= t && t < p.end && (!covering || p.start >= covering.start)) covering = p;
|
|
132
|
+
if (covering)
|
|
133
|
+
return {
|
|
134
|
+
i: covering.i,
|
|
135
|
+
local: covering.from + (t - covering.start) * covering.speed,
|
|
136
|
+
frozen: false,
|
|
137
|
+
speed: covering.speed,
|
|
138
|
+
};
|
|
139
|
+
/* gap: the last frame of the piece that ended last before t */
|
|
140
|
+
let ended = null;
|
|
141
|
+
for (const p of pieces) if (p.end <= t && (!ended || p.end >= ended.end)) ended = p;
|
|
142
|
+
if (ended) return { i: ended.i, local: ended.to - 1e-3, frozen: true, speed: 1 };
|
|
143
|
+
/* before all of them: the first frame of the one that starts first */
|
|
144
|
+
const first = pieces.reduce((m, p) => (p.start < m.start ? p : m));
|
|
145
|
+
return { i: first.i, local: first.from, frozen: true, speed: 1 };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* the whole track in frames: [{ i, from, frames, since, frozen, speed }], `from` and `since`
|
|
149
|
+
in frames (in the video and inside the scene MP4); frozen repeats frame `since`; a running
|
|
150
|
+
segment takes frames × speed frames of the MP4 */
|
|
151
|
+
export function videoTrack(scenes, total, fps) {
|
|
152
|
+
const q = s => Math.round(s * fps);
|
|
153
|
+
const end = q(total);
|
|
154
|
+
const marks = new Set([0, end]);
|
|
155
|
+
for (const p of videoPieces(scenes)) {
|
|
156
|
+
for (const m of [q(p.start), q(p.end)]) if (m > 0 && m < end) marks.add(m);
|
|
157
|
+
}
|
|
158
|
+
const points = [...marks].sort((a, b) => a - b);
|
|
159
|
+
const segs = [];
|
|
160
|
+
for (let k = 0; k < points.length - 1; k++) {
|
|
161
|
+
const a = points[k],
|
|
162
|
+
b = points[k + 1];
|
|
163
|
+
const v = frameAt(scenes, (a + 0.5) / fps);
|
|
164
|
+
const nq = q(scenes[v.i].duration);
|
|
165
|
+
/* the MP4 frame at the start of the segment */
|
|
166
|
+
const since = Math.max(
|
|
167
|
+
0,
|
|
168
|
+
Math.min(
|
|
169
|
+
nq - 1,
|
|
170
|
+
v.frozen ? Math.floor(v.local * fps + EPS) : Math.round((v.local - (0.5 * v.speed) / fps) * fps),
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
const last = segs.at(-1);
|
|
174
|
+
/* join with the previous one when it is the same scene running at the same speed, or the same frozen frame */
|
|
175
|
+
if (
|
|
176
|
+
last &&
|
|
177
|
+
last.i === v.i &&
|
|
178
|
+
last.frozen === v.frozen &&
|
|
179
|
+
last.speed === v.speed &&
|
|
180
|
+
(v.frozen ? last.since === since : Math.abs(last.since + last.frames * last.speed - since) < 1 + EPS)
|
|
181
|
+
)
|
|
182
|
+
last.frames += b - a;
|
|
183
|
+
else segs.push({ i: v.i, from: a, frames: b - a, since, frozen: v.frozen, speed: v.speed });
|
|
184
|
+
}
|
|
185
|
+
return segs;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* --- validate what the screen sends --------------------------------------------
|
|
189
|
+
clips sorted, inside the file, not overlapping in the file; `step` snaps
|
|
190
|
+
(1/fps for animation, 1 ms for voice); `offset` from -30 to 30 s */
|
|
191
|
+
export function sanitizeClips(clips, dur, step) {
|
|
192
|
+
if (!Array.isArray(clips)) throw new Error("clips: tem de ser uma lista");
|
|
193
|
+
const a = s => Math.round(s / step) * step;
|
|
194
|
+
const list = clips
|
|
195
|
+
.map(c => {
|
|
196
|
+
const from = a(Math.max(0, Math.min(dur, Number(c?.from)))),
|
|
197
|
+
to = a(Math.max(0, Math.min(dur, Number(c?.to))));
|
|
198
|
+
const offset = Number(c?.offset ?? 0);
|
|
199
|
+
if (![from, to, offset].every(Number.isFinite)) throw new Error(`clip inválido: ${JSON.stringify(c)}`);
|
|
200
|
+
if (Math.abs(offset) > 30) throw new Error(`clip: offset de -30 a 30 segundos; veio ${offset}`);
|
|
201
|
+
const speed = c?.speed == null ? 1 : Number(c.speed);
|
|
202
|
+
if (!(speed >= CLIP_SPEED[0] && speed <= CLIP_SPEED[1]))
|
|
203
|
+
throw new Error(`clip: velocidade de ${CLIP_SPEED[0] * 100}% a ${CLIP_SPEED[1] * 100}%; veio ${c?.speed}`);
|
|
204
|
+
return { from, to, offset: a(offset), speed: Math.round(speed * 100) / 100 };
|
|
205
|
+
})
|
|
206
|
+
.filter(c => c.to - c.from >= step - EPS)
|
|
207
|
+
.sort((x, y) => x.from - y.from);
|
|
208
|
+
for (let k = 1; k < list.length; k++) if (list[k].from < list[k - 1].to - EPS) list[k].from = list[k - 1].to;
|
|
209
|
+
return list
|
|
210
|
+
.filter(c => c.to - c.from >= step - EPS)
|
|
211
|
+
.map(c => ({
|
|
212
|
+
from: +c.from.toFixed(4),
|
|
213
|
+
to: +c.to.toFixed(4),
|
|
214
|
+
offset: +c.offset.toFixed(4),
|
|
215
|
+
...(c.speed !== 1 ? { speed: c.speed } : {}),
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* ============================================================================
|
|
3
|
+
Thumbnail of a video in HTML + SVG + CSS, on request. Four slots:
|
|
4
|
+
v1, v2 and v3 are three drawn ideas; v4 follows the user's instruction.
|
|
5
|
+
|
|
6
|
+
node tools/thumbnail.mjs videos/<slug> size aspect and pixels of this video's thumbnail
|
|
7
|
+
node tools/thumbnail.mjs videos/<slug> draw three different ideas for v1, v2 and v3
|
|
8
|
+
node tools/thumbnail.mjs videos/<slug> render [vN,…] thumbnail/vN.html → vN.png (default: the ones that exist)
|
|
9
|
+
node tools/thumbnail.mjs videos/<slug> snapshot v2 copies v2.html and v2.png to history/ before changing it
|
|
10
|
+
node tools/thumbnail.mjs videos/<slug> choose v2 the selected one becomes thumbnail/thumbnail.png, the cover
|
|
11
|
+
that publish.mjs sends (choose none removes it)
|
|
12
|
+
node tools/thumbnail.mjs videos/<slug> status thumbnail/thumbnail.json
|
|
13
|
+
|
|
14
|
+
Claude creates the versions (skill av-thumbnail), in the video's style;
|
|
15
|
+
this tool is the mechanical part: the right size for the video's aspect,
|
|
16
|
+
the idea draw, the PNG of each HTML and the record in
|
|
17
|
+
thumbnail/thumbnail.json. Rendering some doesn't delete the others: the
|
|
18
|
+
random ones and the manual one are requested and redone separately.
|
|
19
|
+
|
|
20
|
+
Size from the aspect in scenes.json:
|
|
21
|
+
16:9 → 1280×720 (what YouTube asks for)
|
|
22
|
+
9:16 → 1080×1920 (Shorts, Reels, TikTok)
|
|
23
|
+
other → the video's own format
|
|
24
|
+
|
|
25
|
+
Each HTML is a still, self-contained image (no JS needed, no external
|
|
26
|
+
files): the tool adds the ready signal that render.mjs waits for, so the
|
|
27
|
+
source only has to load. `<title>` becomes the version name and
|
|
28
|
+
`<meta name="description">` its idea, in thumbnail.json and in the UI.
|
|
29
|
+
|
|
30
|
+
thumbnail/request.json belongs to the UI (studio/api/services/thumbnail.ts):
|
|
31
|
+
{ "random": { "at": "…" } | null,
|
|
32
|
+
"manual": { "at": "…", "instruction": "…" } | null,
|
|
33
|
+
"changes": { "v2": { "at": "…", "instruction": "…" } } }
|
|
34
|
+
The v4 instruction and the change requested for each version go from it into thumbnail.json.
|
|
35
|
+
|
|
36
|
+
Nothing here is sent to the platforms: publish.mjs does not upload the cover.
|
|
37
|
+
============================================================================ */
|
|
38
|
+
|
|
39
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, renameSync, rmSync } from "node:fs";
|
|
40
|
+
import { spawnSync } from "node:child_process";
|
|
41
|
+
import { join, resolve } from "node:path";
|
|
42
|
+
import { keepPrevious } from "./history.mjs";
|
|
43
|
+
import { CODE_ROOT, DATA_ROOT as ROOT } from "./lib/paths.mjs";
|
|
44
|
+
|
|
45
|
+
const RENDER = join(CODE_ROOT, "tools", "render.mjs");
|
|
46
|
+
const RANDOM = ["v1", "v2", "v3"];
|
|
47
|
+
const MANUAL = "v4";
|
|
48
|
+
const IDS = [...RANDOM, MANUAL];
|
|
49
|
+
|
|
50
|
+
/* where the three random ones start from; the draw changes on every request (PT: shown to Claude) */
|
|
51
|
+
const IDEAS = [
|
|
52
|
+
["tipografia", "uma palavra ou número enorme, o resto some"],
|
|
53
|
+
["ilustração", "um objeto ou diagrama em SVG como herói, texto mínimo"],
|
|
54
|
+
["contraste", "antes/depois, certo/errado, duas metades"],
|
|
55
|
+
["pergunta", "o gancho do roteiro como pergunta curta, com um símbolo"],
|
|
56
|
+
["dado", "o número do vídeo como painel: placar, medidor, contador"],
|
|
57
|
+
["cena", 'o herói da cena "wow" redesenhado grande, recortado de perto'],
|
|
58
|
+
["metáfora", "um objeto do dia a dia que explica a tese sem palavras"],
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function fail(msg) {
|
|
62
|
+
console.error(`erro: ${msg}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const readJson = file => {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
function writeJson(file, data) {
|
|
73
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
74
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
75
|
+
renameSync(tmp, file);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const [target, cmd = "status", ...rest] = process.argv.slice(2);
|
|
79
|
+
if (!target) fail("uso: thumbnail.mjs videos/<slug> size|draw|render [vN,…]|snapshot <vN>|choose <vN>|status");
|
|
80
|
+
const VIDEO_DIR = resolve(target);
|
|
81
|
+
const scenes = readJson(join(VIDEO_DIR, "scenes.json"));
|
|
82
|
+
if (!scenes) fail(`${target} não tem scenes.json`);
|
|
83
|
+
const DIR = join(VIDEO_DIR, "thumbnail");
|
|
84
|
+
const JSON_FILE = join(DIR, "thumbnail.json");
|
|
85
|
+
|
|
86
|
+
function size() {
|
|
87
|
+
const f = scenes.format;
|
|
88
|
+
if (!f) fail("o scenes.json não tem format");
|
|
89
|
+
const aspect = f.aspect ?? `${f.w}:${f.h}`;
|
|
90
|
+
if (aspect === "16:9") return { aspect, w: 1280, h: 720 };
|
|
91
|
+
if (aspect === "9:16") return { aspect, w: 1080, h: 1920 };
|
|
92
|
+
return { aspect, w: f.w, h: f.h };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function draw() {
|
|
96
|
+
const bag = [...IDEAS];
|
|
97
|
+
for (let i = bag.length - 1; i > 0; i--) {
|
|
98
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
99
|
+
[bag[i], bag[j]] = [bag[j], bag[i]];
|
|
100
|
+
}
|
|
101
|
+
bag.slice(0, 3).forEach(([name, how], i) => console.log(`${RANDOM[i]}: ${name} (${how})`));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* request.json from before this split ({ at, request }, older { em, pedido }) was the three's */
|
|
105
|
+
function requests() {
|
|
106
|
+
const p = readJson(join(DIR, "request.json"));
|
|
107
|
+
if (p && ("at" in p || "em" in p)) return { random: { at: p.at ?? p.em }, manual: null, changes: {} };
|
|
108
|
+
return { random: p?.random ?? null, manual: p?.manual ?? null, changes: p?.changes ?? {} };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const attribute = (html, re) => {
|
|
112
|
+
const m = re.exec(html);
|
|
113
|
+
return m ? m[1].replace(/\s+/g, " ").trim() : null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/* The ready signal render waits for, after measuring the content box:
|
|
117
|
+
the union of the visible leaves (an <svg> counts whole), without what covers
|
|
118
|
+
the whole frame (background, texture). It becomes a [data-continues] that
|
|
119
|
+
render.mjs --measure returns. */
|
|
120
|
+
const READY = `<script>document.fonts.ready.then(()=>{
|
|
121
|
+
const W=innerWidth,H=innerHeight;let x0=W,y0=H,x1=0,y1=0;
|
|
122
|
+
for(const e of document.body.querySelectorAll("*")){
|
|
123
|
+
const tag=e.tagName.toLowerCase();
|
|
124
|
+
if(["script","style","link","meta"].includes(tag))continue;
|
|
125
|
+
if(tag!=="svg"&&e.closest("svg"))continue;
|
|
126
|
+
if(tag!=="svg"&&e.children.length)continue;
|
|
127
|
+
const cs=getComputedStyle(e);if(cs.visibility==="hidden"||+cs.opacity===0)continue;
|
|
128
|
+
const r=e.getBoundingClientRect();if(r.width<1||r.height<1)continue;
|
|
129
|
+
if(r.width>=.9*W&&r.height>=.9*H)continue;
|
|
130
|
+
x0=Math.min(x0,Math.max(0,r.left));y0=Math.min(y0,Math.max(0,r.top));
|
|
131
|
+
x1=Math.max(x1,Math.min(W,r.right));y1=Math.max(y1,Math.min(H,r.bottom));
|
|
132
|
+
}
|
|
133
|
+
if(x1>x0&&y1>y0){const m=document.createElement("div");m.dataset.continues="content";
|
|
134
|
+
Object.assign(m.style,{position:"fixed",left:x0+"px",top:y0+"px",width:(x1-x0)+"px",height:(y1-y0)+"px",pointerEvents:"none"});
|
|
135
|
+
document.body.appendChild(m);}
|
|
136
|
+
window.__ready=true;
|
|
137
|
+
})</script>`;
|
|
138
|
+
|
|
139
|
+
/* 9:16 has the content centred in x and y (the profile grid crops through the middle) */
|
|
140
|
+
const MEASUREMENTS = "MEASUREMENTS ";
|
|
141
|
+
function offCenter(stdout, t) {
|
|
142
|
+
if (t.h <= t.w) return null;
|
|
143
|
+
const line = (stdout ?? "").split("\n").find(l => l.startsWith(MEASUREMENTS));
|
|
144
|
+
const box = line ? JSON.parse(line.slice(MEASUREMENTS.length)).find(m => m.name === "content") : null;
|
|
145
|
+
if (!box) return null;
|
|
146
|
+
const dx = (box.x + box.w / 2) / t.w - 0.5,
|
|
147
|
+
dy = (box.y + box.h / 2) / t.h - 0.5;
|
|
148
|
+
const pct = n => `${Math.round(Math.abs(n) * 100)}%`;
|
|
149
|
+
const errors = [
|
|
150
|
+
Math.abs(dx) > 0.05 ? `${pct(dx)} para a ${dx < 0 ? "esquerda" : "direita"}` : null,
|
|
151
|
+
Math.abs(dy) > 0.06 ? `${pct(dy)} para ${dy < 0 ? "cima" : "baixo"}` : null,
|
|
152
|
+
].filter(Boolean);
|
|
153
|
+
return errors.length ? `conteúdo fora do centro em 9:16 (${errors.join(", ")}): centre em x e y` : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function render(list) {
|
|
157
|
+
const t = size();
|
|
158
|
+
const requested = list ? list.split(",").filter(Boolean) : IDS;
|
|
159
|
+
const invalid = requested.filter(id => !IDS.includes(id));
|
|
160
|
+
if (invalid.length) fail(`versão inválida: ${invalid.join(", ")} (use ${IDS.join(", ")})`);
|
|
161
|
+
const present = requested.filter(id => existsSync(join(DIR, `${id}.html`)));
|
|
162
|
+
if (!present.length) fail(`escreva thumbnail/${requested.join(".html, ")}.html antes (${t.w}×${t.h}, ${t.aspect})`);
|
|
163
|
+
const missing = requested.filter(id => list && !present.includes(id));
|
|
164
|
+
if (missing.length) console.warn(`aviso: sem HTML para ${missing.join(", ")}`);
|
|
165
|
+
|
|
166
|
+
const previous = readJson(JSON_FILE);
|
|
167
|
+
/* the format changed (video redone in another aspect): the old versions are no use */
|
|
168
|
+
const same = previous?.format?.w === t.w && previous?.format?.h === t.h;
|
|
169
|
+
const versions = new Map((same ? (previous.versions ?? []) : []).map(v => [v.id, v]));
|
|
170
|
+
const { manual, changes } = requests();
|
|
171
|
+
for (const id of present) {
|
|
172
|
+
const html = readFileSync(join(DIR, `${id}.html`), "utf8");
|
|
173
|
+
const png = join(DIR, `${id}.png`);
|
|
174
|
+
/* the previous image goes to history/thumbnail/<vN>/ (the HTML was already rewritten) */
|
|
175
|
+
const kept = keepPrevious(VIDEO_DIR, "thumbnail", id, [`thumbnail/${id}.png`]);
|
|
176
|
+
|
|
177
|
+
/* side copy, with the ready signal render waits for, so the font
|
|
178
|
+
loads without the scene having to remember it */
|
|
179
|
+
const temp = join(DIR, `.render-${id}.html`);
|
|
180
|
+
writeFileSync(temp, /<\/body>/i.test(html) ? html.replace(/<\/body>/i, `${READY}</body>`) : html + READY);
|
|
181
|
+
const r = spawnSync(
|
|
182
|
+
"node",
|
|
183
|
+
[RENDER, temp, "--still", png, "--at", "0", "--w", String(t.w), "--h", String(t.h), "--measure"],
|
|
184
|
+
{ encoding: "utf8" },
|
|
185
|
+
);
|
|
186
|
+
rmSync(temp, { force: true });
|
|
187
|
+
if (r.status !== 0)
|
|
188
|
+
fail(`${id}: o render saiu com ${r.status}\n${(r.stderr || r.stdout || "").trim().slice(-600)}`);
|
|
189
|
+
kept.close();
|
|
190
|
+
const off = offCenter(r.stdout, t);
|
|
191
|
+
if (off) console.warn(`aviso: ${id}: ${off}`);
|
|
192
|
+
|
|
193
|
+
versions.set(id, {
|
|
194
|
+
id,
|
|
195
|
+
name: attribute(html, /<title[^>]*>([^<]*)<\/title>/i) ?? id,
|
|
196
|
+
/* `descricao` is the legacy meta name */
|
|
197
|
+
description: attribute(html, /<meta\s+name=["'](?:description|descricao)["']\s+content=["']([^"']*)["']/i),
|
|
198
|
+
html: `thumbnail/${id}.html`,
|
|
199
|
+
png: `thumbnail/${id}.png`,
|
|
200
|
+
generated_at: new Date().toISOString(),
|
|
201
|
+
...(id === MANUAL ? { instruction: manual?.instruction ?? null } : {}),
|
|
202
|
+
/* what was asked to change in this version (the UI shows it); gone when it is redone from scratch */
|
|
203
|
+
...(changes[id] ? { change: changes[id] } : {}),
|
|
204
|
+
});
|
|
205
|
+
console.log(`${id}: thumbnail/${id}.png (${t.w}×${t.h})`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* the selected one only drops if its image changed: it goes to history */
|
|
209
|
+
let selected = same ? (previous?.selected ?? null) : null;
|
|
210
|
+
const final = join(DIR, "thumbnail.png");
|
|
211
|
+
if (!selected || present.includes(selected)) {
|
|
212
|
+
if (existsSync(final)) {
|
|
213
|
+
keepPrevious(VIDEO_DIR, "thumbnail", "final", ["thumbnail/thumbnail.png"]).close();
|
|
214
|
+
rmSync(final, { force: true });
|
|
215
|
+
}
|
|
216
|
+
selected = null;
|
|
217
|
+
}
|
|
218
|
+
writeJson(JSON_FILE, {
|
|
219
|
+
format: t,
|
|
220
|
+
generated_at: new Date().toISOString(),
|
|
221
|
+
versions: IDS.map(id => versions.get(id)).filter(Boolean),
|
|
222
|
+
selected,
|
|
223
|
+
...(selected && previous?.selected_at ? { selected_at: previous.selected_at } : {}),
|
|
224
|
+
});
|
|
225
|
+
console.log(
|
|
226
|
+
`${present.length} versão(ões) renderizada(s) · escolha com: node tools/thumbnail.mjs ${target} choose <${IDS.join("|")}>`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* before changing a version: the current HTML and PNG go to history/thumbnail/<vN>/ */
|
|
231
|
+
function snapshot(id) {
|
|
232
|
+
if (!IDS.includes(id)) fail(`versão inválida: ${id || "(vazia)"} (use ${IDS.join(", ")})`);
|
|
233
|
+
if (!existsSync(join(DIR, `${id}.html`))) fail(`thumbnail/${id}.html não existe`);
|
|
234
|
+
keepPrevious(VIDEO_DIR, "thumbnail", id, [`thumbnail/${id}.html`, `thumbnail/${id}.png`]).close({ keep: true });
|
|
235
|
+
console.log(`${id}: versão de agora guardada em history/thumbnail/${id}/`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function choose(id) {
|
|
239
|
+
const data = readJson(JSON_FILE);
|
|
240
|
+
if (id === "none") {
|
|
241
|
+
const final = join(DIR, "thumbnail.png");
|
|
242
|
+
if (existsSync(final)) {
|
|
243
|
+
keepPrevious(VIDEO_DIR, "thumbnail", "final", ["thumbnail/thumbnail.png"]).close();
|
|
244
|
+
rmSync(final, { force: true });
|
|
245
|
+
}
|
|
246
|
+
if (data) {
|
|
247
|
+
delete data.selected_at;
|
|
248
|
+
writeJson(JSON_FILE, { ...data, selected: null });
|
|
249
|
+
}
|
|
250
|
+
console.log("nenhuma thumbnail selecionada: a publicação usa um quadro do vídeo");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (!IDS.includes(id)) fail(`versão inválida: ${id || "(vazia)"} (use ${IDS.join(", ")} ou none)`);
|
|
254
|
+
if (!data?.versions?.some(v => v.id === id)) fail(`a versão ${id} ainda não foi renderizada`);
|
|
255
|
+
const source = join(DIR, `${id}.png`);
|
|
256
|
+
if (!existsSync(source)) fail(`thumbnail/${id}.png não existe: renderize de novo`);
|
|
257
|
+
const final = join(DIR, "thumbnail.png");
|
|
258
|
+
keepPrevious(VIDEO_DIR, "thumbnail", "final", ["thumbnail/thumbnail.png"]).close();
|
|
259
|
+
copyFileSync(source, final);
|
|
260
|
+
writeJson(JSON_FILE, { ...data, selected: id, selected_at: new Date().toISOString() });
|
|
261
|
+
console.log(`${id} é a thumbnail do vídeo: thumbnail/thumbnail.png`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
mkdirSync(DIR, { recursive: true });
|
|
265
|
+
if (cmd === "size") console.log(JSON.stringify(size()));
|
|
266
|
+
else if (cmd === "draw") draw();
|
|
267
|
+
else if (cmd === "render") render(rest[0]);
|
|
268
|
+
else if (cmd === "snapshot") snapshot(rest[0]);
|
|
269
|
+
else if (cmd === "choose") choose(rest[0]);
|
|
270
|
+
else if (cmd === "status") console.log(JSON.stringify(readJson(JSON_FILE) ?? { versions: [], size: size() }, null, 2));
|
|
271
|
+
else fail(`comando desconhecido: ${cmd}`);
|
package/tools/timing.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
Time references in scenes.json → seconds inside the scene.
|
|
3
|
+
|
|
4
|
+
Each video's build.mjs uses this to place every motion beat exactly where
|
|
5
|
+
the voice says the word:
|
|
6
|
+
|
|
7
|
+
import { loadVideo, beatTime } from "../../../tools/timing.mjs";
|
|
8
|
+
const video = loadVideo(new URL("..", import.meta.url).pathname);
|
|
9
|
+
for (const scene of video.scenes) {
|
|
10
|
+
for (const beat of scene.beats) {
|
|
11
|
+
const t = beatTime(scene, beat); // seconds in the scene
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Accepted forms:
|
|
16
|
+
2.4 number, passed through
|
|
17
|
+
"c01f2" line start
|
|
18
|
+
"c01f2:end" line end (legacy ":fim" also accepted)
|
|
19
|
+
"c01f2:milissegundos" start of the word's first occurrence
|
|
20
|
+
"c01f2:token#2" second occurrence
|
|
21
|
+
"c01f2:end-0.3" any form above with an offset (+/-)
|
|
22
|
+
|
|
23
|
+
Before `omnivideo.py timing` runs, the line has no audio yet: its time is
|
|
24
|
+
estimated from the config's average pace and `video.provisional` is true.
|
|
25
|
+
Good for stills and contact sheets; the final render requires real timing.
|
|
26
|
+
============================================================================ */
|
|
27
|
+
|
|
28
|
+
import { readFileSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { DATA_ROOT as ROOT } from "./lib/paths.mjs";
|
|
31
|
+
|
|
32
|
+
const CFG = JSON.parse(readFileSync(join(ROOT, "omnivideo.config.json"), "utf8"));
|
|
33
|
+
|
|
34
|
+
const norm = w =>
|
|
35
|
+
w
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.normalize("NFC")
|
|
38
|
+
.replace(/[^\p{L}\p{N}]/gu, "");
|
|
39
|
+
|
|
40
|
+
export function loadVideo(videoDir) {
|
|
41
|
+
const video = JSON.parse(readFileSync(join(videoDir, "scenes.json"), "utf8"));
|
|
42
|
+
const { lead_in, pause_between_lines, final_hold, words_per_second } = CFG.timing;
|
|
43
|
+
video.provisional = false;
|
|
44
|
+
let clock = 0;
|
|
45
|
+
for (const scene of video.scenes) {
|
|
46
|
+
let cursor = scene.lead_in ?? lead_in;
|
|
47
|
+
for (const line of scene.lines ?? []) {
|
|
48
|
+
if (line.start == null) {
|
|
49
|
+
/* with audio (the voice generated before `timing`), the duration is real */
|
|
50
|
+
video.provisional = true;
|
|
51
|
+
const n = line.text.split(/\s+/).length;
|
|
52
|
+
line.start = cursor;
|
|
53
|
+
line.end = cursor + (line.dur ?? n / words_per_second);
|
|
54
|
+
}
|
|
55
|
+
cursor = line.end + pause_between_lines;
|
|
56
|
+
}
|
|
57
|
+
/* without `timing` run (including no lines yet) duration and start are
|
|
58
|
+
estimated too */
|
|
59
|
+
if (scene.duration == null) {
|
|
60
|
+
video.provisional = true;
|
|
61
|
+
const end = scene.lines?.length ? scene.lines.at(-1).end : 0;
|
|
62
|
+
scene.duration = Math.max(end + (scene.hold ?? final_hold), scene.min_duration ?? 0);
|
|
63
|
+
}
|
|
64
|
+
if (scene.start == null) {
|
|
65
|
+
video.provisional = true;
|
|
66
|
+
scene.start = clock;
|
|
67
|
+
}
|
|
68
|
+
clock = scene.start + scene.duration;
|
|
69
|
+
}
|
|
70
|
+
video.total_duration ??= clock;
|
|
71
|
+
return video;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* A beat is born in composition with only `t` (provisional
|
|
75
|
+
seconds, for the still and the contact sheet). Narration ties each beat to a
|
|
76
|
+
line or word in `enters_with`, and from then on that rules. */
|
|
77
|
+
export function beatTime(scene, beat) {
|
|
78
|
+
if (beat.enters_with != null) return resolveTime(scene, beat.enters_with);
|
|
79
|
+
if (beat.t != null) return beat.t;
|
|
80
|
+
throw new Error(`${scene.id}: beat ${beat.element} sem enters_with nem t`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveTime(scene, ref) {
|
|
84
|
+
if (typeof ref === "number") return ref;
|
|
85
|
+
const m = /^([^:+-]+)(?::([^+\-#]+)(?:#(\d+))?)?([+-]\d+(?:\.\d+)?)?$/.exec(String(ref).trim());
|
|
86
|
+
if (!m) throw new Error(`referência de tempo inválida: ${ref}`);
|
|
87
|
+
const [, lineId, target, occurrence, offset] = m;
|
|
88
|
+
const line = (scene.lines ?? []).find(l => l.id === lineId);
|
|
89
|
+
if (!line) throw new Error(`${scene.id}: fala ${lineId} não existe`);
|
|
90
|
+
|
|
91
|
+
let t;
|
|
92
|
+
if (!target) t = line.start;
|
|
93
|
+
else if (target === "end" || target === "fim")
|
|
94
|
+
t = line.end; // ":fim" = legacy alias
|
|
95
|
+
else if (line.words) {
|
|
96
|
+
const hits = line.words.filter(p => norm(p.w) === norm(target));
|
|
97
|
+
const hit = hits[(+occurrence || 1) - 1];
|
|
98
|
+
if (!hit) throw new Error(`${scene.id}: “${target}” não aparece na fala ${lineId}`);
|
|
99
|
+
t = line.start + hit.s;
|
|
100
|
+
} else {
|
|
101
|
+
/* no word timing yet: proportional to the position in the text */
|
|
102
|
+
const words = line.text.split(/\s+/);
|
|
103
|
+
const idx = words.findIndex(p => norm(p) === norm(target));
|
|
104
|
+
if (idx < 0) throw new Error(`${scene.id}: “${target}” não aparece na fala ${lineId}`);
|
|
105
|
+
t = line.start + ((line.end - line.start) * idx) / words.length;
|
|
106
|
+
}
|
|
107
|
+
return Math.round((t + (offset ? +offset : 0)) * 1000) / 1000;
|
|
108
|
+
}
|