@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,234 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
Fan-out: one fork per scene for a stage (composition, wireframes, narration),
|
|
3
|
+
and the guard that makes the same scene impossible to run twice at once.
|
|
4
|
+
|
|
5
|
+
On 2026-10-17 the composition of 2026-10-16-https-x-ai-news-grok ran twice:
|
|
6
|
+
the orchestrator launched the 8 scene forks and an av-motion fork launched
|
|
7
|
+
another 8, for the same scenes, writing the same files (~US$ 11 lost). Prompt
|
|
8
|
+
rules alone don't stop that; this file does, by construction:
|
|
9
|
+
|
|
10
|
+
- `plan` hands each scene out once. A scene already handed out (planned or
|
|
11
|
+
running, and younger than the TTL) is listed as busy and not handed out
|
|
12
|
+
again: a second `plan`, from whoever, has nothing to launch.
|
|
13
|
+
- a fork starts with `claim`: the scene must be in the fork's run and not
|
|
14
|
+
started yet. A second fork with the same prompt, or one from another run,
|
|
15
|
+
is refused (exit 3) and stops before writing anything.
|
|
16
|
+
- `done` closes the scene; `release` frees a scene whose fork died, so the
|
|
17
|
+
orchestrator can hand it out again.
|
|
18
|
+
|
|
19
|
+
State: videos/<slug>/.fanout/<kind>.json, one item per scene:
|
|
20
|
+
{ "c03": { "run": "r-k2f9", "state": "planned"|"started"|"done", "at": ISO, "started_at"?: ISO } }
|
|
21
|
+
|
|
22
|
+
`withScenesLock` is the lock every parallel writer of scenes.json takes
|
|
23
|
+
(`compose.mjs apply`, `narrate.mjs apply`).
|
|
24
|
+
============================================================================ */
|
|
25
|
+
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, renameSync, statSync } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { randomBytes } from "node:crypto";
|
|
29
|
+
|
|
30
|
+
export const KINDS = ["compose", "wireframe", "narration"];
|
|
31
|
+
export const DEFAULT_TTL_MIN = 45;
|
|
32
|
+
|
|
33
|
+
export class FanoutError extends Error {
|
|
34
|
+
constructor(msg, code = 3) {
|
|
35
|
+
super(msg);
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stateFile = (dir, kind) => join(dir, ".fanout", `${kind}.json`);
|
|
41
|
+
|
|
42
|
+
function checkKind(kind) {
|
|
43
|
+
if (!KINDS.includes(kind)) throw new FanoutError(`tipo desconhecido: ${kind} (${KINDS.join(", ")})`, 2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function read(dir, kind) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(readFileSync(stateFile(dir, kind), "utf8"));
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function write(dir, kind, items) {
|
|
55
|
+
mkdirSync(join(dir, ".fanout"), { recursive: true });
|
|
56
|
+
const file = stateFile(dir, kind);
|
|
57
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
58
|
+
writeFileSync(tmp, JSON.stringify(items, null, 2) + "\n");
|
|
59
|
+
renameSync(tmp, file);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* a lock directory: mkdir is atomic. One left by a call that died is stale after 60 s */
|
|
63
|
+
async function withLock(path, fn, { waitMs = 30_000, staleMs = 60_000 } = {}) {
|
|
64
|
+
const until = Date.now() + waitMs;
|
|
65
|
+
for (;;) {
|
|
66
|
+
try {
|
|
67
|
+
mkdirSync(path);
|
|
68
|
+
break;
|
|
69
|
+
} catch {
|
|
70
|
+
try {
|
|
71
|
+
if (Date.now() - statSync(path).mtimeMs > staleMs) rmSync(path, { recursive: true, force: true });
|
|
72
|
+
} catch {}
|
|
73
|
+
if (Date.now() > until) throw new FanoutError(`lock ocupado há mais de ${waitMs / 1000} s: ${path}`, 1);
|
|
74
|
+
await new Promise(r => setTimeout(r, 100));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return await fn();
|
|
79
|
+
} finally {
|
|
80
|
+
rmSync(path, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const withScenesLock = (dir, fn) => withLock(join(dir, ".scenes.lock"), fn);
|
|
85
|
+
const withStateLock = (dir, fn) => withLock(join(dir, ".fanout.lock"), fn);
|
|
86
|
+
|
|
87
|
+
const ageMin = (iso, now) => (now - Date.parse(iso)) / 60_000;
|
|
88
|
+
|
|
89
|
+
/* planned or started, and younger than the TTL: someone holds the scene */
|
|
90
|
+
function holding(item, now, ttlMin) {
|
|
91
|
+
return item && item.state !== "done" && ageMin(item.started_at ?? item.at, now) < ttlMin;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const newRun = prefix => `${prefix}-${randomBytes(3).toString("hex")}`;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Hands each scene out once. Returns the new run (null when nothing was handed
|
|
98
|
+
* out), the scenes in it and the ones someone else still holds.
|
|
99
|
+
* @param {string} dir video folder
|
|
100
|
+
* @param {string} kind
|
|
101
|
+
* @param {string[]} ids scenes to hand out
|
|
102
|
+
*/
|
|
103
|
+
export async function plan(dir, kind, ids, { ttlMin = DEFAULT_TTL_MIN, now = Date.now() } = {}) {
|
|
104
|
+
checkKind(kind);
|
|
105
|
+
return withStateLock(dir, () => {
|
|
106
|
+
const items = read(dir, kind);
|
|
107
|
+
const run = newRun("r");
|
|
108
|
+
const planned = [],
|
|
109
|
+
busy = [];
|
|
110
|
+
for (const id of ids) {
|
|
111
|
+
const it = items[id];
|
|
112
|
+
if (holding(it, now, ttlMin))
|
|
113
|
+
busy.push({ id, run: it.run, state: it.state, age_min: Math.round(ageMin(it.started_at ?? it.at, now)) });
|
|
114
|
+
else {
|
|
115
|
+
items[id] = { run, state: "planned", at: new Date(now).toISOString() };
|
|
116
|
+
planned.push(id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (planned.length) write(dir, kind, items);
|
|
120
|
+
return { run: planned.length ? run : null, planned, busy };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A fork takes its scene before writing anything. With `run`, the scene must be
|
|
126
|
+
* planned in that run and not started; without it (a scene call made alone), the
|
|
127
|
+
* scene must be free, and a run of its own is created.
|
|
128
|
+
*/
|
|
129
|
+
export async function claim(dir, kind, id, run, { ttlMin = DEFAULT_TTL_MIN, now = Date.now() } = {}) {
|
|
130
|
+
checkKind(kind);
|
|
131
|
+
return withStateLock(dir, () => {
|
|
132
|
+
const items = read(dir, kind);
|
|
133
|
+
const it = items[id];
|
|
134
|
+
const at = new Date(now).toISOString();
|
|
135
|
+
if (!run) {
|
|
136
|
+
if (holding(it, now, ttlMin))
|
|
137
|
+
throw new FanoutError(
|
|
138
|
+
`${id} já está em ${it.state === "started" ? "andamento" : "fila"} na rodada ${it.run} ` +
|
|
139
|
+
`(há ${Math.round(ageMin(it.started_at ?? it.at, now))} min): não rode de novo`,
|
|
140
|
+
);
|
|
141
|
+
const solo = newRun("s");
|
|
142
|
+
items[id] = { run: solo, state: "started", at, started_at: at };
|
|
143
|
+
write(dir, kind, items);
|
|
144
|
+
return solo;
|
|
145
|
+
}
|
|
146
|
+
if (!it || it.run !== run)
|
|
147
|
+
throw new FanoutError(
|
|
148
|
+
it && holding(it, now, ttlMin)
|
|
149
|
+
? `${id} pertence à rodada ${it.run}, não à ${run}: outro fork cuida dela. Pare sem escrever nada`
|
|
150
|
+
: `${id} não foi distribuída na rodada ${run}: pare sem escrever nada`,
|
|
151
|
+
);
|
|
152
|
+
if (it.state === "started")
|
|
153
|
+
throw new FanoutError(`${id} já foi iniciada por outro fork da rodada ${run}: pare sem escrever nada`);
|
|
154
|
+
if (it.state === "done") throw new FanoutError(`${id} já foi concluída na rodada ${run}`);
|
|
155
|
+
if (!holding(it, now, ttlMin)) throw new FanoutError(`a rodada ${run} de ${id} expirou: rode o plan de novo`);
|
|
156
|
+
items[id] = { ...it, state: "started", started_at: at };
|
|
157
|
+
write(dir, kind, items);
|
|
158
|
+
return run;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The fork finished its scene (with or without success: the stage's gate says the rest). */
|
|
163
|
+
export async function done(dir, kind, id, run, { now = Date.now() } = {}) {
|
|
164
|
+
checkKind(kind);
|
|
165
|
+
return withStateLock(dir, () => {
|
|
166
|
+
const items = read(dir, kind);
|
|
167
|
+
const it = items[id];
|
|
168
|
+
if (!it || it.run !== run) throw new FanoutError(`${id} não está na rodada ${run}`, 1);
|
|
169
|
+
items[id] = { ...it, state: "done", done_at: new Date(now).toISOString() };
|
|
170
|
+
write(dir, kind, items);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Frees scenes whose fork died (error, no report), so `plan` hands them out again. */
|
|
175
|
+
export async function release(dir, kind, ids) {
|
|
176
|
+
checkKind(kind);
|
|
177
|
+
return withStateLock(dir, () => {
|
|
178
|
+
const items = read(dir, kind);
|
|
179
|
+
const freed = ids.filter(id => items[id] && items[id].state !== "done");
|
|
180
|
+
for (const id of freed) delete items[id];
|
|
181
|
+
write(dir, kind, items);
|
|
182
|
+
return freed;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Who holds what right now: scenes planned or running, younger than the TTL. */
|
|
187
|
+
export function busy(dir, kind, { ttlMin = DEFAULT_TTL_MIN, now = Date.now() } = {}) {
|
|
188
|
+
checkKind(kind);
|
|
189
|
+
const items = read(dir, kind);
|
|
190
|
+
return Object.entries(items)
|
|
191
|
+
.filter(([, it]) => holding(it, now, ttlMin))
|
|
192
|
+
.map(([id, it]) => ({
|
|
193
|
+
id,
|
|
194
|
+
run: it.run,
|
|
195
|
+
state: it.state,
|
|
196
|
+
age_min: Math.round(ageMin(it.started_at ?? it.at, now)),
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const hasState = (dir, kind) => existsSync(stateFile(dir, kind));
|
|
201
|
+
|
|
202
|
+
/** The Agent tool takes an alias; the config may hold a full id */
|
|
203
|
+
export function agentModel(model) {
|
|
204
|
+
return ["fable", "opus", "sonnet", "haiku"].find(a => String(model).includes(a)) ?? "opus";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Splits ids into waves of at most `parallel`.
|
|
209
|
+
* @param {string[]} ids
|
|
210
|
+
*/
|
|
211
|
+
export function waves(ids, parallel) {
|
|
212
|
+
const out = [];
|
|
213
|
+
for (let i = 0; i < ids.length; i += parallel) out.push(ids.slice(i, i + parallel));
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The lines a `plan` prints for the orchestrator: waves, Agent parameters and each prompt. */
|
|
218
|
+
export function printPlan({ title, run, waves: ws, model, effort, prompt, busy: held, after }) {
|
|
219
|
+
console.log(title);
|
|
220
|
+
if (held.length)
|
|
221
|
+
console.log(
|
|
222
|
+
`em andamento (não lance de novo): ${held.map(b => `${b.id} (rodada ${b.run}, ${b.state === "started" ? "rodando" : "na fila"} há ${b.age_min} min)`).join(", ")}`,
|
|
223
|
+
);
|
|
224
|
+
if (!run) return;
|
|
225
|
+
console.log(`rodada: ${run}`);
|
|
226
|
+
ws.forEach((w, i) => console.log(`onda ${i + 1}: ${w.join(" ")}`));
|
|
227
|
+
console.log(
|
|
228
|
+
`\nCada onda é UMA mensagem com uma chamada Agent por cena (subagent_type general-purpose, ` +
|
|
229
|
+
`model ${model}, effort ${effort}, em primeiro plano); a próxima onda só depois de todos os relatórios. ` +
|
|
230
|
+
`Este plan não se repete: rodar de novo não distribui o que já está nesta rodada. Prompts:`,
|
|
231
|
+
);
|
|
232
|
+
for (const w of ws) for (const id of w) console.log(` ${id}: ${prompt(id)}`);
|
|
233
|
+
if (after) console.log(`\n${after}`);
|
|
234
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
HTTP shared by every platform client: one request with the platforms'
|
|
3
|
+
readable error. Pure: `fetch` is injected (defaults to the global one), so
|
|
4
|
+
the same code runs on the CLI, in a cloud function and in tests.
|
|
5
|
+
============================================================================ */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {typeof globalThis.fetch} Fetch
|
|
9
|
+
* @typedef {Error & { code?: string | null, status?: number }} PlatformError
|
|
10
|
+
* @typedef {{ r: Response, j: any, text: string }} HttpResult
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One HTTP call. On a non-2xx answer throws `label: <status> <message>` with
|
|
15
|
+
* `code` (the platform's error code, when there is one) and `status`.
|
|
16
|
+
* @param {string} url
|
|
17
|
+
* @param {RequestInit} [opts]
|
|
18
|
+
* @param {string} [label]
|
|
19
|
+
* @param {{ fetch?: Fetch }} [deps]
|
|
20
|
+
* @returns {Promise<HttpResult>}
|
|
21
|
+
*/
|
|
22
|
+
export async function request(url, opts, label, { fetch: f = globalThis.fetch } = {}) {
|
|
23
|
+
const r = await f(url, opts);
|
|
24
|
+
const text = await r.text();
|
|
25
|
+
let j = null;
|
|
26
|
+
try {
|
|
27
|
+
j = JSON.parse(text);
|
|
28
|
+
} catch {
|
|
29
|
+
/* not JSON */
|
|
30
|
+
}
|
|
31
|
+
if (!r.ok) {
|
|
32
|
+
const e = j?.error;
|
|
33
|
+
const msg =
|
|
34
|
+
e?.error_user_msg ??
|
|
35
|
+
e?.message ??
|
|
36
|
+
j?.error_description ??
|
|
37
|
+
(typeof e === "string" ? e : null) ??
|
|
38
|
+
text.slice(0, 400);
|
|
39
|
+
/* the code says more than the message (TikTok's is sometimes just "read the guidelines") */
|
|
40
|
+
const code = typeof e === "object" && e?.code != null && e.code !== "ok" ? String(e.code) : null;
|
|
41
|
+
/** @type {PlatformError} */
|
|
42
|
+
const error = new Error(`${label}: ${r.status} ${msg}${code && !String(msg).includes(code) ? ` (${code})` : ""}`);
|
|
43
|
+
error.code = code;
|
|
44
|
+
error.status = r.status;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
return { r, j, text };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {number} ms
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
export const sleep = ms => new Promise(ok => setTimeout(ok, ms));
|
|
55
|
+
|
|
56
|
+
/** One day in ms. */
|
|
57
|
+
export const DAY = 86_400_000;
|
|
58
|
+
/** One hour in ms. */
|
|
59
|
+
export const HOUR = 3_600_000;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Deps every core function accepts. All optional: the defaults are the real ones.
|
|
63
|
+
* @typedef {object} Deps
|
|
64
|
+
* @property {Fetch} [fetch]
|
|
65
|
+
* @property {() => number} [now] epoch ms
|
|
66
|
+
* @property {(ms: number) => Promise<void>} [sleep]
|
|
67
|
+
* @property {(line: string) => void} [log] stdout-like progress
|
|
68
|
+
* @property {(line: string) => void} [warn] stderr-like warnings
|
|
69
|
+
* @property {() => number} [random] in [0, 1)
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fills the missing deps with the real ones.
|
|
74
|
+
* @param {Deps} [deps]
|
|
75
|
+
* @returns {Required<Deps>}
|
|
76
|
+
*/
|
|
77
|
+
export function withDefaults(deps = {}) {
|
|
78
|
+
return {
|
|
79
|
+
fetch: deps.fetch ?? globalThis.fetch,
|
|
80
|
+
now: deps.now ?? Date.now,
|
|
81
|
+
sleep: deps.sleep ?? sleep,
|
|
82
|
+
log: deps.log ?? (() => {}),
|
|
83
|
+
warn: deps.warn ?? (() => {}),
|
|
84
|
+
random: deps.random ?? Math.random,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Token storage: where the platform tokens live (the .env on the CLI, a secret
|
|
90
|
+
* store in the cloud). `get` returns "" when missing.
|
|
91
|
+
* @typedef {object} TokenStore
|
|
92
|
+
* @property {(name: string) => string} get
|
|
93
|
+
* @property {(values: Record<string, string>) => void | Promise<void>} set
|
|
94
|
+
*/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* Entry for `node --test tools/lib`: node resolves the folder to this file, which loads every
|
|
2
|
+
*.test.mjs below it into the same test run. Not a library entry point: import the modules directly. */
|
|
3
|
+
const { readdirSync } = require("node:fs");
|
|
4
|
+
const { join } = require("node:path");
|
|
5
|
+
const { pathToFileURL } = require("node:url");
|
|
6
|
+
|
|
7
|
+
const walk = dir =>
|
|
8
|
+
readdirSync(dir, { withFileTypes: true }).flatMap(e =>
|
|
9
|
+
e.isDirectory() ? walk(join(dir, e.name)) : e.name.endsWith(".test.mjs") ? [join(dir, e.name)] : [],
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
for (const file of walk(__dirname).sort()) await import(pathToFileURL(file).href);
|
|
14
|
+
})();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* A minimal MCP server over stdio (JSON-RPC 2.0, one message per line), enough for the plugin's tools:
|
|
2
|
+
initialize, tools/list, tools/call and ping. The `omni mcp` command serves its tools with it
|
|
3
|
+
(docs/specs/phase-2-plugin-cli.md, step 4); no SDK, so the CLI carries no extra dependency.
|
|
4
|
+
|
|
5
|
+
A tool is { name, description, inputSchema, run(args) → string }; its text goes back as the result, and
|
|
6
|
+
a thrown Error goes back as a tool error the model can read. */
|
|
7
|
+
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
|
|
10
|
+
const PROTOCOL = "2025-06-18";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{ name: string, description: string, inputSchema: object, run: (args: any) => Promise<string> | string }} Tool
|
|
14
|
+
* @param {{ name: string, version: string, tools: Tool[], input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream }} o
|
|
15
|
+
* @returns {Promise<void>} resolves when the input ends
|
|
16
|
+
*/
|
|
17
|
+
export function serveMcp({ name, version, tools, input = process.stdin, output = process.stdout }) {
|
|
18
|
+
const send = (/** @type {unknown} */ msg) => output.write(`${JSON.stringify(msg)}\n`);
|
|
19
|
+
const reply = (/** @type {unknown} */ id, /** @type {unknown} */ result) => send({ jsonrpc: "2.0", id, result });
|
|
20
|
+
const error = (/** @type {unknown} */ id, /** @type {number} */ code, /** @type {string} */ message) =>
|
|
21
|
+
send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
22
|
+
|
|
23
|
+
/** @param {any} msg */
|
|
24
|
+
async function handle(msg) {
|
|
25
|
+
const { id, method, params } = msg ?? {};
|
|
26
|
+
const isRequest = id !== undefined && id !== null;
|
|
27
|
+
if (method === "initialize")
|
|
28
|
+
return reply(id, {
|
|
29
|
+
protocolVersion: params?.protocolVersion ?? PROTOCOL,
|
|
30
|
+
capabilities: { tools: {} },
|
|
31
|
+
serverInfo: { name, version },
|
|
32
|
+
});
|
|
33
|
+
if (method === "ping") return reply(id, {});
|
|
34
|
+
if (method === "tools/list")
|
|
35
|
+
return reply(id, {
|
|
36
|
+
tools: tools.map(({ name: n, description, inputSchema }) => ({ name: n, description, inputSchema })),
|
|
37
|
+
});
|
|
38
|
+
if (method === "tools/call") {
|
|
39
|
+
const tool = tools.find(t => t.name === params?.name);
|
|
40
|
+
if (!tool) return error(id, -32602, `ferramenta desconhecida: ${params?.name}`);
|
|
41
|
+
try {
|
|
42
|
+
const text = await tool.run(params?.arguments ?? {});
|
|
43
|
+
return reply(id, { content: [{ type: "text", text: String(text) }] });
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return reply(id, {
|
|
46
|
+
content: [{ type: "text", text: String(/** @type {Error} */ (e).message) }],
|
|
47
|
+
isError: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/* notifications (initialized, cancelled…) need no answer */
|
|
52
|
+
if (isRequest) error(id, -32601, `método não suportado: ${method}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new Promise(resolve => {
|
|
56
|
+
const rl = createInterface({ input });
|
|
57
|
+
rl.on("line", line => {
|
|
58
|
+
if (!line.trim()) return;
|
|
59
|
+
let msg;
|
|
60
|
+
try {
|
|
61
|
+
msg = JSON.parse(line);
|
|
62
|
+
} catch {
|
|
63
|
+
return error(null, -32700, "JSON inválido");
|
|
64
|
+
}
|
|
65
|
+
void handle(msg);
|
|
66
|
+
});
|
|
67
|
+
rl.on("close", () => resolve());
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
Reel metrics: what is due at a given age (the age milestones and the
|
|
3
|
+
periodic reading), the snapshot shape, and the collection of one video and
|
|
4
|
+
of the account. Pure: the record comes in and goes out; the caller reads
|
|
5
|
+
and writes metrics.json / account.json. Read only on Instagram.
|
|
6
|
+
============================================================================ */
|
|
7
|
+
|
|
8
|
+
import { HOUR, withDefaults } from "../http.mjs";
|
|
9
|
+
|
|
10
|
+
/** @typedef {import("../http.mjs").Deps} Deps */
|
|
11
|
+
/** @typedef {import("../platforms/instagram.mjs").InstagramClient} InstagramClient */
|
|
12
|
+
|
|
13
|
+
/** Every how many hours the collector runs. */
|
|
14
|
+
export const AGENT_STEP_H = 4;
|
|
15
|
+
|
|
16
|
+
/** The age milestones: [name, hours]. */
|
|
17
|
+
export const MILESTONES = /** @type {const} */ ([
|
|
18
|
+
["1h", 1],
|
|
19
|
+
["3h", 3],
|
|
20
|
+
["24h", 24],
|
|
21
|
+
["72h", 72],
|
|
22
|
+
["7d", 168],
|
|
23
|
+
["30d", 720],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How often the Reel is read, by its age (hours): every step in the first 30 days, then daily.
|
|
28
|
+
* @param {number} ageH
|
|
29
|
+
*/
|
|
30
|
+
export const currentInterval = ageH => (ageH < 720 ? AGENT_STEP_H : 24);
|
|
31
|
+
|
|
32
|
+
export const BASE = ["views", "reach", "likes", "comments", "saved", "shares", "total_interactions"];
|
|
33
|
+
export const REEL = ["ig_reels_avg_watch_time", "ig_reels_video_view_total_time", "reels_skip_rate"];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {object} Snapshot
|
|
37
|
+
* @property {string} at
|
|
38
|
+
* @property {number} age_h
|
|
39
|
+
* @property {number | null} views
|
|
40
|
+
* @property {number | null} reach
|
|
41
|
+
* @property {number | null} likes
|
|
42
|
+
* @property {number | null} comments
|
|
43
|
+
* @property {number | null} saves
|
|
44
|
+
* @property {number | null} shares
|
|
45
|
+
* @property {number | null} interactions
|
|
46
|
+
* @property {number | null} avg_watch_s
|
|
47
|
+
* @property {number | null} watched_pct
|
|
48
|
+
* @property {number | null} skip_3s_pct
|
|
49
|
+
* @property {number | null} total_watch_h
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {object} VideoRecord metrics.json `instagram`
|
|
54
|
+
* @property {string} id
|
|
55
|
+
* @property {string} published_at
|
|
56
|
+
* @property {number | null} [duration_s]
|
|
57
|
+
* @property {Array<Snapshot & { milestone: string }>} snapshots
|
|
58
|
+
* @property {Snapshot[]} [hourly]
|
|
59
|
+
* @property {Snapshot} [current]
|
|
60
|
+
* @property {{ at: string, message: string } | null} [error]
|
|
61
|
+
* @property {boolean} [removed]
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One reading from the raw insights.
|
|
66
|
+
* @param {Record<string, any>} m raw metrics by name
|
|
67
|
+
* @param {number | null | undefined} duration seconds of the uploaded MP4
|
|
68
|
+
* @param {number} ageH
|
|
69
|
+
* @param {string} at ISO
|
|
70
|
+
* @returns {Snapshot}
|
|
71
|
+
*/
|
|
72
|
+
export function snapshot(m, duration, ageH, at) {
|
|
73
|
+
const avg = m.ig_reels_avg_watch_time != null ? Math.round(m.ig_reels_avg_watch_time / 100) / 10 : null;
|
|
74
|
+
return {
|
|
75
|
+
at,
|
|
76
|
+
age_h: Math.round(ageH * 10) / 10,
|
|
77
|
+
views: m.views ?? null,
|
|
78
|
+
reach: m.reach ?? null,
|
|
79
|
+
likes: m.likes ?? null,
|
|
80
|
+
comments: m.comments ?? null,
|
|
81
|
+
saves: m.saved ?? null,
|
|
82
|
+
shares: m.shares ?? null,
|
|
83
|
+
interactions: m.total_interactions ?? null,
|
|
84
|
+
avg_watch_s: avg,
|
|
85
|
+
watched_pct: avg != null && duration ? Math.round((avg / duration) * 1000) / 10 : null,
|
|
86
|
+
skip_3s_pct: m.reels_skip_rate ?? null,
|
|
87
|
+
total_watch_h:
|
|
88
|
+
m.ig_reels_video_view_total_time != null ? Math.round((m.ig_reels_video_view_total_time / HOUR) * 10) / 10 : null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What is due now: the milestones in their window and the current reading if due.
|
|
94
|
+
* A milestone is only taken near its age (up to half a milestone later, at least 1 h):
|
|
95
|
+
* a late snapshot would be skewed, so a missed window stays missed.
|
|
96
|
+
* @param {{ snapshots?: Array<{ milestone: string }>, current?: { at: string } }} rec
|
|
97
|
+
* @param {number} ageH
|
|
98
|
+
* @param {boolean} force
|
|
99
|
+
* @param {number} now epoch ms
|
|
100
|
+
* @returns {{ milestones: string[], current: boolean }}
|
|
101
|
+
*/
|
|
102
|
+
export function due(rec, ageH, force, now) {
|
|
103
|
+
const taken = new Set((rec.snapshots ?? []).map(f => f.milestone));
|
|
104
|
+
const milestones = MILESTONES.filter(
|
|
105
|
+
([name, h]) => !taken.has(name) && ageH >= h && ageH <= h + Math.max(1, h / 2),
|
|
106
|
+
).map(([name]) => name);
|
|
107
|
+
const step = currentInterval(ageH);
|
|
108
|
+
const last = rec.current ? Date.parse(rec.current.at) : 0;
|
|
109
|
+
const current = force || now - last >= step * HOUR * 0.95;
|
|
110
|
+
return { milestones, current };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A media's insights; a post that is not a Reel lacks the Reel metrics (the whole call would fail).
|
|
115
|
+
* @param {InstagramClient} client @param {string} id
|
|
116
|
+
*/
|
|
117
|
+
export async function reelInsights(client, id) {
|
|
118
|
+
try {
|
|
119
|
+
return await client.insights(id, [...BASE, ...REEL]);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
const err = /** @type {import("../http.mjs").PlatformError} */ (e);
|
|
122
|
+
if (err.status === 400 && /metric/i.test(err.message)) return client.insights(id, BASE);
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Collects one video if something is due.
|
|
129
|
+
* @param {{ client: InstagramClient, ig: { id: string, published_at: string }, stored?: VideoRecord | null,
|
|
130
|
+
* duration: () => number | null, force: boolean, retryRemoved?: boolean } & Deps} p
|
|
131
|
+
* `stored` is metrics.json's `instagram`; `duration` is only called when the record has none
|
|
132
|
+
* @returns {Promise<null | { rec: VideoRecord, milestones: string[], error: string | null }>}
|
|
133
|
+
* null when nothing was due (nothing to write)
|
|
134
|
+
*/
|
|
135
|
+
export async function collectVideo({ client, ig, stored, duration, force, retryRemoved = force, ...deps }) {
|
|
136
|
+
const d = withDefaults(deps);
|
|
137
|
+
/* Reel republished: another id, another count */
|
|
138
|
+
/** @type {VideoRecord} */
|
|
139
|
+
const rec = stored?.id === ig.id ? stored : { id: ig.id, published_at: ig.published_at, snapshots: [] };
|
|
140
|
+
if (rec.removed && !retryRemoved) return null;
|
|
141
|
+
const ageH = (d.now() - Date.parse(ig.published_at)) / HOUR;
|
|
142
|
+
const { milestones, current } = due(rec, ageH, force, d.now());
|
|
143
|
+
if (!milestones.length && !current) return null;
|
|
144
|
+
rec.duration_s ??= duration();
|
|
145
|
+
try {
|
|
146
|
+
const f = snapshot(await reelInsights(client, ig.id), rec.duration_s, ageH, new Date(d.now()).toISOString());
|
|
147
|
+
for (const milestone of milestones) rec.snapshots.push({ milestone, ...f });
|
|
148
|
+
rec.hourly = [...(rec.hourly ?? []), f];
|
|
149
|
+
rec.current = f;
|
|
150
|
+
rec.error = null;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
const err = /** @type {import("../http.mjs").PlatformError} */ (e);
|
|
153
|
+
rec.error = { at: new Date(d.now()).toISOString(), message: err.message };
|
|
154
|
+
/* the Reel was deleted: stop asking (collect with the video named tries again) */
|
|
155
|
+
if (err.status === 400 && /does not exist|cannot be loaded|nonexisting/i.test(err.message)) rec.removed = true;
|
|
156
|
+
}
|
|
157
|
+
return { rec, milestones, error: rec.error?.message ?? null };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @typedef {object} AccountRecord .metrics/account.json
|
|
162
|
+
* @property {Array<{ day: string, at: string, username: string, followers: number | null, following: number | null,
|
|
163
|
+
* posts: number | null, profile_visits_day: number | null }>} snapshots
|
|
164
|
+
* @property {Array<{ at: string, followers: number | null, profile_visits_day: number | null }>} [hourly]
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The account: one snapshot per day, redone every pass (today's is the newest),
|
|
169
|
+
* and every reading in `hourly`.
|
|
170
|
+
* @param {{ client: InstagramClient, stored?: AccountRecord | null, force: boolean, day?: (now: number) => string } & Deps} p
|
|
171
|
+
* `day` is the calendar day of a time (the runtime's local day by default)
|
|
172
|
+
* @returns {Promise<AccountRecord | null>} null when nothing was due
|
|
173
|
+
*/
|
|
174
|
+
export async function collectAccount({ client, stored, force, day, ...deps }) {
|
|
175
|
+
const d = withDefaults(deps);
|
|
176
|
+
const account = stored ?? { snapshots: [] };
|
|
177
|
+
const today = (day ?? (t => new Date(t).toLocaleDateString("sv-SE")))(d.now());
|
|
178
|
+
const last = account.snapshots.at(-1);
|
|
179
|
+
if (!force && last?.day === today && d.now() - Date.parse(last.at) < HOUR * 0.95) return null;
|
|
180
|
+
const me = await client.account("username,followers_count,follows_count,media_count");
|
|
181
|
+
let visits = null;
|
|
182
|
+
try {
|
|
183
|
+
visits = await client.profileVisits();
|
|
184
|
+
} catch {
|
|
185
|
+
/* small account or no permission: goes without */
|
|
186
|
+
}
|
|
187
|
+
const f = {
|
|
188
|
+
day: today,
|
|
189
|
+
at: new Date(d.now()).toISOString(),
|
|
190
|
+
username: me.username,
|
|
191
|
+
followers: me.followers_count ?? null,
|
|
192
|
+
following: me.follows_count ?? null,
|
|
193
|
+
posts: me.media_count ?? null,
|
|
194
|
+
profile_visits_day: visits,
|
|
195
|
+
};
|
|
196
|
+
if (account.snapshots.at(-1)?.day === today) account.snapshots[account.snapshots.length - 1] = f;
|
|
197
|
+
else account.snapshots.push(f);
|
|
198
|
+
/* every reading also goes into hourly: the followers curve comes from it */
|
|
199
|
+
account.hourly = [
|
|
200
|
+
...(account.hourly ?? []),
|
|
201
|
+
{ at: f.at, followers: f.followers, profile_visits_day: f.profile_visits_day },
|
|
202
|
+
];
|
|
203
|
+
return account;
|
|
204
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* "Sem narração e voz": content.json `narration: false`. The video has no spoken script, narration, voice or
|
|
2
|
+
captions; scenes.json holds beats with `t` and a `min_duration` per scene, and the assembly is picture,
|
|
3
|
+
effects and the bed. Absent means narrated. `omnivideo.py narrated()` reads the same flag. */
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
/** @param {string} videoDir */
|
|
9
|
+
export function isNarrated(videoDir) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(join(videoDir, "content.json"), "utf8")).narration !== false;
|
|
12
|
+
} catch {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|