@vellumai/assistant 0.3.5 → 0.3.7
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/README.md +51 -0
- package/eslint.config.mjs +31 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
- package/scripts/ipc/generate-swift.ts +18 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +338 -1
- package/src/__tests__/approval-conversation-turn.test.ts +214 -0
- package/src/__tests__/browser-manager.test.ts +1 -0
- package/src/__tests__/call-conversation-messages.test.ts +130 -0
- package/src/__tests__/call-orchestrator.test.ts +752 -271
- package/src/__tests__/call-pointer-messages.test.ts +148 -0
- package/src/__tests__/call-recovery.test.ts +3 -0
- package/src/__tests__/call-routes-http.test.ts +5 -0
- package/src/__tests__/call-store.test.ts +3 -0
- package/src/__tests__/channel-approval-routes.test.ts +1260 -85
- package/src/__tests__/channel-approval.test.ts +37 -0
- package/src/__tests__/channel-approvals.test.ts +4 -65
- package/src/__tests__/channel-guardian.test.ts +556 -0
- package/src/__tests__/channel-readiness-service.test.ts +74 -7
- package/src/__tests__/checker.test.ts +14 -7
- package/src/__tests__/clarification-resolver.test.ts +44 -24
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
- package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
- package/src/__tests__/config-schema.test.ts +12 -7
- package/src/__tests__/context-window-manager.test.ts +30 -2
- package/src/__tests__/contradiction-checker.test.ts +20 -5
- package/src/__tests__/credential-security-invariants.test.ts +6 -2
- package/src/__tests__/db-migration-rollback.test.ts +752 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
- package/src/__tests__/fuzzy-match-property.test.ts +5 -5
- package/src/__tests__/guardian-action-store.test.ts +123 -0
- package/src/__tests__/guardian-action-sweep.test.ts +277 -0
- package/src/__tests__/guardian-dispatch.test.ts +389 -0
- package/src/__tests__/guardian-question-copy.test.ts +47 -0
- package/src/__tests__/handlers-telegram-config.test.ts +4 -2
- package/src/__tests__/handlers-twilio-config.test.ts +126 -0
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +228 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
- package/src/__tests__/model-intents.test.ts +96 -0
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
- package/src/__tests__/provider-error-scenarios.test.ts +621 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
- package/src/__tests__/qdrant-manager.test.ts +27 -20
- package/src/__tests__/relay-server.test.ts +779 -40
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +2 -0
- package/src/__tests__/run-orchestrator.test.ts +20 -4
- package/src/__tests__/runtime-runs-http.test.ts +17 -1
- package/src/__tests__/runtime-runs.test.ts +16 -0
- package/src/__tests__/schedule-store.test.ts +18 -4
- package/src/__tests__/scheduler-recurrence.test.ts +13 -4
- package/src/__tests__/session-abort-tool-results.test.ts +6 -0
- package/src/__tests__/session-agent-loop.test.ts +857 -0
- package/src/__tests__/session-conflict-gate.test.ts +6 -0
- package/src/__tests__/session-pre-run-repair.test.ts +6 -0
- package/src/__tests__/session-profile-injection.test.ts +6 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/session-queue.test.ts +6 -0
- package/src/__tests__/session-runtime-assembly.test.ts +237 -13
- package/src/__tests__/session-slash-known.test.ts +6 -0
- package/src/__tests__/session-slash-queue.test.ts +6 -0
- package/src/__tests__/session-slash-unknown.test.ts +6 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/session-workspace-injection.test.ts +6 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/skills.test.ts +2 -0
- package/src/__tests__/sms-messaging-provider.test.ts +2 -1
- package/src/__tests__/starter-task-flow.test.ts +2 -0
- package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +2 -0
- package/src/__tests__/task-management-tools.test.ts +2 -2
- package/src/__tests__/task-runner.test.ts +14 -4
- package/src/__tests__/terminal-tools.test.ts +25 -19
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
- package/src/__tests__/tool-executor.test.ts +23 -24
- package/src/__tests__/trust-store.test.ts +3 -3
- package/src/__tests__/twilio-rest.test.ts +29 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
- package/src/__tests__/twilio-routes.test.ts +141 -21
- package/src/__tests__/user-reference.test.ts +2 -0
- package/src/__tests__/voice-quality.test.ts +222 -0
- package/src/__tests__/web-search.test.ts +45 -29
- package/src/agent/loop.ts +1 -1
- package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
- package/src/amazon/client.ts +1418 -0
- package/src/amazon/request-extractor.ts +135 -0
- package/src/amazon/session.ts +109 -0
- package/src/autonomy/autonomy-store.ts +5 -5
- package/src/browser-extension-relay/client.ts +124 -0
- package/src/browser-extension-relay/protocol.ts +63 -0
- package/src/browser-extension-relay/server.ts +177 -0
- package/src/bundler/app-bundler.ts +3 -3
- package/src/bundler/bundle-signer.ts +1 -1
- package/src/bundler/signature-verifier.ts +1 -1
- package/src/calls/call-conversation-messages.ts +33 -0
- package/src/calls/call-domain.ts +106 -5
- package/src/calls/call-orchestrator.ts +252 -54
- package/src/calls/call-pointer-messages.ts +53 -0
- package/src/calls/call-recovery.ts +3 -8
- package/src/calls/call-store.ts +69 -87
- package/src/calls/elevenlabs-config.ts +3 -2
- package/src/calls/guardian-action-sweep.ts +105 -0
- package/src/calls/guardian-dispatch.ts +203 -0
- package/src/calls/guardian-question-copy.ts +133 -0
- package/src/calls/relay-server.ts +466 -8
- package/src/calls/speaker-identification.ts +1 -1
- package/src/calls/twilio-config.ts +7 -5
- package/src/calls/twilio-provider.ts +6 -4
- package/src/calls/twilio-rest.ts +40 -15
- package/src/calls/twilio-routes.ts +60 -45
- package/src/calls/types.ts +3 -1
- package/src/channels/types.ts +25 -0
- package/src/cli/amazon.ts +815 -0
- package/src/cli/config-commands.ts +2 -2
- package/src/cli/core-commands.ts +4 -3
- package/src/cli/influencer.ts +244 -0
- package/src/cli/map.ts +89 -6
- package/src/cli.ts +1 -1
- package/src/config/agent-schema.ts +171 -0
- package/src/config/bundled-skills/amazon/SKILL.md +127 -0
- package/src/config/bundled-skills/amazon/icon.svg +13 -0
- package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
- package/src/config/bundled-skills/browser/SKILL.md +1 -0
- package/src/config/bundled-skills/browser/TOOLS.json +17 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
- package/src/config/bundled-skills/doordash/SKILL.md +51 -51
- package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
- package/src/config/bundled-skills/influencer/SKILL.md +144 -0
- package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +72 -95
- package/src/config/bundled-skills/media-processing/TOOLS.json +57 -147
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +7 -9
- package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +88 -253
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +22 -153
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +28 -51
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +35 -270
- package/src/config/bundled-skills/messaging/SKILL.md +12 -2
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +86 -21
- package/src/config/bundled-skills/twitter/icon.svg +14 -0
- package/src/config/bundled-tool-registry.ts +310 -0
- package/src/config/calls-schema.ts +181 -0
- package/src/config/core-schema.ts +309 -0
- package/src/config/defaults.ts +27 -3
- package/src/config/env-registry.ts +169 -0
- package/src/config/env.ts +175 -0
- package/src/config/loader.ts +6 -6
- package/src/config/memory-schema.ts +528 -0
- package/src/config/sandbox-schema.ts +55 -0
- package/src/config/schema.ts +157 -1138
- package/src/config/skill-state.ts +1 -1
- package/src/config/skills-schema.ts +32 -0
- package/src/config/skills.ts +35 -24
- package/src/config/system-prompt.ts +107 -56
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/user-reference.ts +4 -9
- package/src/config/vellum-skills/catalog.json +0 -7
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +112 -14
- package/src/context/window-manager.ts +27 -7
- package/src/daemon/approval-generators.ts +186 -0
- package/src/daemon/approved-devices-store.ts +140 -0
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/classifier.ts +35 -32
- package/src/daemon/config-watcher.ts +1 -1
- package/src/daemon/daemon-control.ts +254 -0
- package/src/daemon/handlers/apps.ts +2 -3
- package/src/daemon/handlers/config-channels.ts +158 -0
- package/src/daemon/handlers/config-inbox.ts +540 -0
- package/src/daemon/handlers/config-ingress.ts +231 -0
- package/src/daemon/handlers/config-integrations.ts +258 -0
- package/src/daemon/handlers/config-model.ts +143 -0
- package/src/daemon/handlers/config-parental.ts +163 -0
- package/src/daemon/handlers/config-scheduling.ts +172 -0
- package/src/daemon/handlers/config-slack.ts +92 -0
- package/src/daemon/handlers/config-telegram.ts +301 -0
- package/src/daemon/handlers/config-tools.ts +177 -0
- package/src/daemon/handlers/config-trust.ts +104 -0
- package/src/daemon/handlers/config-twilio.ts +1080 -0
- package/src/daemon/handlers/config.ts +53 -2463
- package/src/daemon/handlers/diagnostics.ts +1 -1
- package/src/daemon/handlers/dictation.ts +4 -6
- package/src/daemon/handlers/documents.ts +18 -32
- package/src/daemon/handlers/index.ts +9 -0
- package/src/daemon/handlers/misc.ts +3 -5
- package/src/daemon/handlers/pairing.ts +98 -0
- package/src/daemon/handlers/sessions.ts +74 -5
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/handlers/twitter-auth.ts +2 -0
- package/src/daemon/handlers/work-items.ts +2 -2
- package/src/daemon/handlers/workspace-files.ts +4 -3
- package/src/daemon/install-cli-launchers.ts +113 -0
- package/src/daemon/ipc-contract/apps.ts +356 -0
- package/src/daemon/ipc-contract/browser.ts +74 -0
- package/src/daemon/ipc-contract/computer-use.ts +151 -0
- package/src/daemon/ipc-contract/diagnostics.ts +56 -0
- package/src/daemon/ipc-contract/documents.ts +74 -0
- package/src/daemon/ipc-contract/inbox.ts +209 -0
- package/src/daemon/ipc-contract/integrations.ts +284 -0
- package/src/daemon/ipc-contract/memory.ts +48 -0
- package/src/daemon/ipc-contract/messages.ts +211 -0
- package/src/daemon/ipc-contract/pairing.ts +45 -0
- package/src/daemon/ipc-contract/parental-control.ts +95 -0
- package/src/daemon/ipc-contract/schedules.ts +97 -0
- package/src/daemon/ipc-contract/sessions.ts +321 -0
- package/src/daemon/ipc-contract/shared.ts +42 -0
- package/src/daemon/ipc-contract/skills.ts +120 -0
- package/src/daemon/ipc-contract/subagents.ts +58 -0
- package/src/daemon/ipc-contract/surfaces.ts +250 -0
- package/src/daemon/ipc-contract/trust.ts +60 -0
- package/src/daemon/ipc-contract/work-items.ts +225 -0
- package/src/daemon/ipc-contract/workspace.ts +113 -0
- package/src/daemon/ipc-contract-inventory.json +62 -0
- package/src/daemon/ipc-contract-inventory.ts +55 -29
- package/src/daemon/ipc-contract.ts +227 -2527
- package/src/daemon/ipc-protocol.ts +1 -1
- package/src/daemon/ipc-validate.ts +7 -0
- package/src/daemon/lifecycle.ts +97 -379
- package/src/daemon/pairing-store.ts +177 -0
- package/src/daemon/providers-setup.ts +43 -0
- package/src/daemon/ride-shotgun-handler.ts +67 -2
- package/src/daemon/server.ts +60 -44
- package/src/daemon/session-agent-loop-handlers.ts +421 -0
- package/src/daemon/session-agent-loop.ts +113 -275
- package/src/daemon/session-dynamic-profile.ts +1 -1
- package/src/daemon/session-history.ts +1 -1
- package/src/daemon/session-media-retry.ts +1 -1
- package/src/daemon/session-messaging.ts +37 -2
- package/src/daemon/session-notifiers.ts +5 -25
- package/src/daemon/session-process.ts +99 -59
- package/src/daemon/session-queue-manager.ts +98 -4
- package/src/daemon/session-runtime-assembly.ts +149 -15
- package/src/daemon/session-surfaces.ts +26 -4
- package/src/daemon/session-tool-setup.ts +28 -30
- package/src/daemon/session-workspace.ts +1 -1
- package/src/daemon/session.ts +24 -1
- package/src/daemon/shutdown-handlers.ts +122 -0
- package/src/daemon/trace-emitter.ts +1 -1
- package/src/daemon/watch-handler.ts +36 -33
- package/src/doordash/cart-queries.ts +787 -0
- package/src/doordash/client.ts +144 -127
- package/src/doordash/order-queries.ts +85 -0
- package/src/doordash/queries.ts +10 -1308
- package/src/doordash/search-queries.ts +203 -0
- package/src/doordash/session.ts +3 -2
- package/src/doordash/store-queries.ts +246 -0
- package/src/doordash/types.ts +367 -0
- package/src/email/providers/agentmail.ts +2 -1
- package/src/email/providers/index.ts +3 -2
- package/src/email/service.ts +3 -2
- package/src/errors.ts +43 -0
- package/src/home-base/prebuilt/seed.ts +1 -1
- package/src/hooks/cli.ts +6 -5
- package/src/hooks/config.ts +6 -8
- package/src/hooks/discovery.ts +6 -5
- package/src/hooks/manager.ts +4 -3
- package/src/hooks/runner.ts +2 -2
- package/src/hooks/templates.ts +5 -5
- package/src/inbound/public-ingress-urls.ts +3 -1
- package/src/index.ts +4 -2
- package/src/influencer/client.ts +1104 -0
- package/src/instrument.ts +4 -3
- package/src/logfire.ts +4 -3
- package/src/memory/admin.ts +25 -35
- package/src/memory/attachments-store.ts +4 -7
- package/src/memory/channel-delivery-store.ts +30 -1
- package/src/memory/channel-guardian-store.ts +200 -1
- package/src/memory/clarification-resolver.ts +37 -33
- package/src/memory/conflict-store.ts +67 -61
- package/src/memory/contradiction-checker.ts +141 -117
- package/src/memory/conversation-store.ts +335 -51
- package/src/memory/db-connection.ts +27 -4
- package/src/memory/db-init.ts +121 -4
- package/src/memory/db.ts +14 -1
- package/src/memory/embedding-backend.ts +27 -5
- package/src/memory/embedding-ollama.ts +2 -1
- package/src/memory/entity-extractor.ts +38 -35
- package/src/memory/guardian-action-store.ts +430 -0
- package/src/memory/inbox-escalation-projection.ts +59 -0
- package/src/memory/inbox-thread-store.ts +218 -0
- package/src/memory/ingress-invite-store.ts +338 -0
- package/src/memory/ingress-member-store.ts +350 -0
- package/src/memory/items-extractor.ts +91 -97
- package/src/memory/job-handlers/index-maintenance.ts +3 -3
- package/src/memory/job-handlers/media-processing.ts +11 -42
- package/src/memory/job-handlers/summarization.ts +32 -26
- package/src/memory/job-utils.ts +3 -10
- package/src/memory/jobs-store.ts +6 -9
- package/src/memory/jobs-worker.ts +51 -36
- package/src/memory/migrations/001-job-deferrals.ts +45 -0
- package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
- package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
- package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
- package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
- package/src/memory/migrations/017-memory-items-indexes.ts +12 -0
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
- package/src/memory/migrations/index.ts +24 -0
- package/src/memory/migrations/registry.ts +79 -0
- package/src/memory/migrations/validate-migration-state.ts +69 -0
- package/src/memory/qdrant-manager.ts +49 -8
- package/src/memory/query-builder.ts +1 -1
- package/src/memory/raw-query.ts +119 -0
- package/src/memory/recall-cache.ts +4 -1
- package/src/memory/retriever.ts +163 -47
- package/src/memory/schema-migration.ts +25 -984
- package/src/memory/schema.ts +130 -7
- package/src/memory/search/entity.ts +10 -19
- package/src/memory/search/lexical.ts +81 -52
- package/src/memory/search/ranking.ts +21 -22
- package/src/memory/search/semantic.ts +157 -19
- package/src/memory/shared-app-links-store.ts +4 -5
- package/src/memory/validation.ts +19 -0
- package/src/messaging/draft-store.ts +5 -6
- package/src/messaging/providers/sms/adapter.ts +3 -6
- package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
- package/src/messaging/providers/whatsapp/adapter.ts +136 -0
- package/src/messaging/providers/whatsapp/client.ts +67 -0
- package/src/messaging/style-analyzer.ts +5 -4
- package/src/messaging/thread-summarizer.ts +61 -69
- package/src/messaging/triage-engine.ts +62 -71
- package/src/migrations/config-merge.ts +53 -0
- package/src/migrations/data-layout.ts +68 -0
- package/src/migrations/data-merge.ts +33 -0
- package/src/migrations/hooks-merge.ts +90 -0
- package/src/migrations/index.ts +6 -0
- package/src/migrations/log.ts +23 -0
- package/src/migrations/skills-merge.ts +33 -0
- package/src/migrations/workspace-layout.ts +79 -0
- package/src/permissions/checker.ts +126 -11
- package/src/permissions/prompter.ts +14 -0
- package/src/permissions/shell-identity.ts +31 -1
- package/src/permissions/trust-store.ts +21 -1
- package/src/providers/anthropic/client.ts +4 -4
- package/src/providers/failover.ts +2 -2
- package/src/providers/model-intents.ts +70 -0
- package/src/providers/ollama/client.ts +2 -1
- package/src/providers/provider-send-message.ts +176 -0
- package/src/providers/registry.ts +71 -30
- package/src/providers/retry.ts +35 -1
- package/src/providers/types.ts +12 -1
- package/src/runtime/approval-conversation-turn.ts +97 -0
- package/src/runtime/approval-message-composer.ts +115 -5
- package/src/runtime/assistant-event-hub.ts +3 -1
- package/src/runtime/channel-approval-parser.ts +36 -2
- package/src/runtime/channel-approvals.ts +0 -21
- package/src/runtime/channel-guardian-service.ts +48 -7
- package/src/runtime/channel-readiness-service.ts +160 -34
- package/src/runtime/channel-readiness-types.ts +10 -4
- package/src/runtime/channel-retry-sweep.ts +184 -0
- package/src/runtime/guardian-context-resolver.ts +108 -0
- package/src/runtime/http-server.ts +289 -745
- package/src/runtime/http-types.ts +56 -3
- package/src/runtime/middleware/auth.ts +116 -0
- package/src/runtime/middleware/error-handler.ts +33 -0
- package/src/runtime/middleware/twilio-validation.ts +127 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +49 -6
- package/src/runtime/routes/channel-delivery-routes.ts +170 -0
- package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
- package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
- package/src/runtime/routes/channel-route-shared.ts +144 -0
- package/src/runtime/routes/channel-routes.ts +32 -1634
- package/src/runtime/routes/conversation-routes.ts +50 -7
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/identity-routes.ts +126 -0
- package/src/runtime/routes/pairing-routes.ts +144 -0
- package/src/runtime/routes/run-routes.ts +15 -1
- package/src/runtime/run-orchestrator.ts +52 -34
- package/src/schedule/schedule-store.ts +36 -32
- package/src/schedule/scheduler.ts +3 -3
- package/src/security/encrypted-store.ts +5 -7
- package/src/security/oauth2.ts +45 -15
- package/src/security/parental-control-store.ts +183 -0
- package/src/security/secret-allowlist.ts +4 -3
- package/src/security/secret-scanner.ts +5 -5
- package/src/security/secure-keys.ts +1 -1
- package/src/security/token-manager.ts +3 -2
- package/src/services/vercel-deploy.ts +6 -2
- package/src/skills/tool-manifest.ts +3 -3
- package/src/skills/vellum-catalog-remote.ts +75 -16
- package/src/slack/slack-webhook.ts +2 -1
- package/src/swarm/orchestrator.ts +92 -1
- package/src/swarm/router-planner.ts +6 -9
- package/src/swarm/worker-prompts.ts +9 -12
- package/src/tasks/task-compiler.ts +19 -28
- package/src/tasks/task-runner.ts +1 -1
- package/src/tools/assets/search.ts +15 -14
- package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
- package/src/tools/browser/auto-navigate.ts +1 -0
- package/src/tools/browser/browser-execution.ts +13 -1
- package/src/tools/browser/browser-manager.ts +119 -4
- package/src/tools/browser/network-recorder.ts +5 -0
- package/src/tools/credentials/broker.ts +11 -2
- package/src/tools/credentials/metadata-store.ts +18 -14
- package/src/tools/credentials/post-connect-hooks.ts +61 -0
- package/src/tools/credentials/vault.ts +49 -23
- package/src/tools/executor.ts +80 -18
- package/src/tools/host-terminal/cli-discover.ts +1 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
- package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
- package/src/tools/network/script-proxy/server.ts +1 -1
- package/src/tools/network/script-proxy/session-manager.ts +6 -5
- package/src/tools/network/web-fetch.ts +18 -2
- package/src/tools/network/web-search.ts +7 -3
- package/src/tools/reminder/reminder-store.ts +14 -15
- package/src/tools/schedule/create.ts +1 -0
- package/src/tools/schedule/list.ts +2 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
- package/src/tools/skills/skill-script-runner.ts +24 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/tasks/work-item-enqueue.ts +2 -2
- package/src/tools/terminal/evaluate-typescript.ts +21 -12
- package/src/tools/terminal/parser.ts +50 -0
- package/src/tools/watcher/delete.ts +6 -0
- package/src/tools/weather/service.ts +1 -1
- package/src/twitter/client.ts +190 -24
- package/src/twitter/session.ts +4 -3
- package/src/util/clipboard.ts +1 -1
- package/src/util/errors.ts +65 -8
- package/src/util/fs.ts +40 -0
- package/src/util/json.ts +10 -0
- package/src/util/log-redact.ts +189 -0
- package/src/util/logger.ts +25 -18
- package/src/util/object.ts +3 -0
- package/src/util/platform.ts +72 -365
- package/src/util/pricing.ts +1 -1
- package/src/util/promise-guard.ts +1 -1
- package/src/util/retry.ts +19 -0
- package/src/util/row-mapper.ts +79 -0
- package/src/util/silently.ts +21 -0
- package/src/watcher/engine.ts +5 -1
- package/src/watcher/provider-types.ts +20 -0
- package/src/watcher/providers/github.ts +156 -0
- package/src/watcher/providers/gmail.ts +1 -0
- package/src/watcher/providers/google-calendar.ts +1 -0
- package/src/watcher/providers/linear.ts +460 -0
- package/src/watcher/providers/slack.ts +1 -0
- package/src/work-items/work-item-runner.ts +1 -1
- package/src/workspace/git-service.ts +1 -1
- package/src/workspace/provider-commit-message-generator.ts +51 -22
- package/src/__tests__/call-bridge.test.ts +0 -517
- package/src/__tests__/session-process-bridge.test.ts +0 -244
- package/src/calls/call-bridge.ts +0 -168
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +0 -137
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +0 -280
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +0 -144
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +0 -136
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +0 -95
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +0 -267
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +0 -110
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +0 -235
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +0 -142
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +0 -150
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { join, dirname } from 'node:path';
|
|
2
|
+
import { mkdir, readdir, rename, rm, writeFile, readFile } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
getMediaAssetById,
|
|
6
|
+
insertKeyframesBatch,
|
|
7
|
+
deleteKeyframesForAsset,
|
|
8
|
+
createProcessingStage,
|
|
9
|
+
updateProcessingStage,
|
|
10
|
+
getProcessingStagesForAsset,
|
|
11
|
+
type ProcessingStage,
|
|
12
|
+
} from '../../../../memory/media-store.js';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Constants
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const FFMPEG_TIMEOUT_MS = 600_000;
|
|
19
|
+
const MIN_FRAMES_PER_SEGMENT = 4;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface PreprocessConfig {
|
|
26
|
+
intervalSeconds: number;
|
|
27
|
+
shortEdge: number;
|
|
28
|
+
deadTimeThreshold: number;
|
|
29
|
+
segmentDuration: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TimeRange {
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Segment {
|
|
38
|
+
id: string;
|
|
39
|
+
startSeconds: number;
|
|
40
|
+
endSeconds: number;
|
|
41
|
+
framePaths: string[];
|
|
42
|
+
frameTimestamps: number[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SubjectGroup {
|
|
46
|
+
label: string;
|
|
47
|
+
dominantColor: string;
|
|
48
|
+
identifiers: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SubjectRegistry {
|
|
52
|
+
groups: SubjectGroup[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SectionBoundary {
|
|
56
|
+
label: string;
|
|
57
|
+
startSeconds: number;
|
|
58
|
+
endSeconds: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PreprocessManifest {
|
|
62
|
+
assetId: string;
|
|
63
|
+
videoPath: string;
|
|
64
|
+
durationSeconds: number;
|
|
65
|
+
segments: Segment[];
|
|
66
|
+
deadTimeRanges: TimeRange[];
|
|
67
|
+
subjectRegistry: SubjectRegistry;
|
|
68
|
+
sectionBoundaries: SectionBoundary[];
|
|
69
|
+
config: PreprocessConfig;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PreprocessOptions {
|
|
73
|
+
intervalSeconds?: number;
|
|
74
|
+
segmentDuration?: number;
|
|
75
|
+
deadTimeThreshold?: number;
|
|
76
|
+
sectionConfigPath?: string;
|
|
77
|
+
skipDeadTime?: boolean;
|
|
78
|
+
shortEdge?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Spawn helper (same pattern as the existing extract-keyframes)
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function spawnWithTimeout(
|
|
86
|
+
cmd: string[],
|
|
87
|
+
timeoutMs: number,
|
|
88
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe' });
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
proc.kill();
|
|
93
|
+
reject(new Error(`Process timed out after ${timeoutMs}ms: ${cmd[0]}`));
|
|
94
|
+
}, timeoutMs);
|
|
95
|
+
proc.exited.then(async (exitCode) => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
const stdout = await new Response(proc.stdout).text();
|
|
98
|
+
const stderr = await new Response(proc.stderr).text();
|
|
99
|
+
resolve({ exitCode, stdout, stderr });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Pure functions (exported for testing)
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse mpdecimate filter stderr output to extract dropped frame timestamps.
|
|
110
|
+
* mpdecimate logs lines like:
|
|
111
|
+
* pts_time:12.34 ... drop
|
|
112
|
+
* We collect the pts_time values of dropped frames.
|
|
113
|
+
*/
|
|
114
|
+
export function parseDroppedFrameTimestamps(stderr: string): number[] {
|
|
115
|
+
const timestamps: number[] = [];
|
|
116
|
+
const lines = stderr.split('\n');
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
// mpdecimate marks dropped frames with "drop" at the end of the line
|
|
119
|
+
if (!line.includes('drop')) continue;
|
|
120
|
+
const match = line.match(/pts_time:([\d.]+)/);
|
|
121
|
+
if (match) {
|
|
122
|
+
timestamps.push(parseFloat(match[1]));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return timestamps.sort((a, b) => a - b);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build dead-time ranges from dropped frame timestamps.
|
|
130
|
+
* Groups consecutive dropped frames and merges ranges where the gap
|
|
131
|
+
* between dropped frames is small (within gapThresholdSeconds).
|
|
132
|
+
* Only ranges longer than minDurationSeconds are kept.
|
|
133
|
+
*/
|
|
134
|
+
export function buildDeadTimeRanges(
|
|
135
|
+
droppedTimestamps: number[],
|
|
136
|
+
gapThresholdSeconds: number = 1.0,
|
|
137
|
+
minDurationSeconds: number = 5.0,
|
|
138
|
+
): TimeRange[] {
|
|
139
|
+
if (droppedTimestamps.length === 0) return [];
|
|
140
|
+
|
|
141
|
+
const ranges: TimeRange[] = [];
|
|
142
|
+
let rangeStart = droppedTimestamps[0];
|
|
143
|
+
let rangeEnd = droppedTimestamps[0];
|
|
144
|
+
|
|
145
|
+
for (let i = 1; i < droppedTimestamps.length; i++) {
|
|
146
|
+
const ts = droppedTimestamps[i];
|
|
147
|
+
if (ts - rangeEnd <= gapThresholdSeconds) {
|
|
148
|
+
// Extend current range
|
|
149
|
+
rangeEnd = ts;
|
|
150
|
+
} else {
|
|
151
|
+
// Close current range if long enough
|
|
152
|
+
if (rangeEnd - rangeStart >= minDurationSeconds) {
|
|
153
|
+
ranges.push({ start: rangeStart, end: rangeEnd });
|
|
154
|
+
}
|
|
155
|
+
rangeStart = ts;
|
|
156
|
+
rangeEnd = ts;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Close final range
|
|
161
|
+
if (rangeEnd - rangeStart >= minDurationSeconds) {
|
|
162
|
+
ranges.push({ start: rangeStart, end: rangeEnd });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return ranges;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Compute live time ranges by subtracting dead-time ranges from the total duration.
|
|
170
|
+
*/
|
|
171
|
+
export function computeLiveRanges(
|
|
172
|
+
durationSeconds: number,
|
|
173
|
+
deadTimeRanges: TimeRange[],
|
|
174
|
+
): TimeRange[] {
|
|
175
|
+
if (deadTimeRanges.length === 0) {
|
|
176
|
+
return [{ start: 0, end: durationSeconds }];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sorted = [...deadTimeRanges].sort((a, b) => a.start - b.start);
|
|
180
|
+
const live: TimeRange[] = [];
|
|
181
|
+
let cursor = 0;
|
|
182
|
+
|
|
183
|
+
for (const dead of sorted) {
|
|
184
|
+
if (dead.start > cursor) {
|
|
185
|
+
live.push({ start: cursor, end: dead.start });
|
|
186
|
+
}
|
|
187
|
+
cursor = Math.max(cursor, dead.end);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (cursor < durationSeconds) {
|
|
191
|
+
live.push({ start: cursor, end: durationSeconds });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return live;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create non-overlapping segments of a given duration within live time ranges.
|
|
199
|
+
*/
|
|
200
|
+
export function createSegments(
|
|
201
|
+
liveRanges: TimeRange[],
|
|
202
|
+
segmentDuration: number,
|
|
203
|
+
): Array<{ id: string; startSeconds: number; endSeconds: number }> {
|
|
204
|
+
if (segmentDuration <= 0) {
|
|
205
|
+
throw new Error(`segmentDuration must be positive, got ${segmentDuration}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const segments: Array<{ id: string; startSeconds: number; endSeconds: number }> = [];
|
|
209
|
+
let segIndex = 0;
|
|
210
|
+
|
|
211
|
+
for (const range of liveRanges) {
|
|
212
|
+
let cursor = range.start;
|
|
213
|
+
while (cursor < range.end) {
|
|
214
|
+
const end = Math.min(cursor + segmentDuration, range.end);
|
|
215
|
+
segIndex++;
|
|
216
|
+
const id = `seg-${String(segIndex).padStart(3, '0')}`;
|
|
217
|
+
segments.push({ id, startSeconds: cursor, endSeconds: end });
|
|
218
|
+
cursor = end;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return segments;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Compute the effective frame extraction interval for a segment,
|
|
227
|
+
* ensuring at least MIN_FRAMES_PER_SEGMENT frames are produced.
|
|
228
|
+
*/
|
|
229
|
+
export function computeEffectiveInterval(
|
|
230
|
+
segmentDurationSeconds: number,
|
|
231
|
+
requestedInterval: number,
|
|
232
|
+
): number {
|
|
233
|
+
const framesAtRequestedInterval = Math.floor(segmentDurationSeconds / requestedInterval);
|
|
234
|
+
if (framesAtRequestedInterval >= MIN_FRAMES_PER_SEGMENT) {
|
|
235
|
+
return requestedInterval;
|
|
236
|
+
}
|
|
237
|
+
// Reduce interval to guarantee minimum frame count
|
|
238
|
+
return segmentDurationSeconds / MIN_FRAMES_PER_SEGMENT;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate frame timestamps for a segment based on effective interval.
|
|
243
|
+
*/
|
|
244
|
+
export function generateFrameTimestamps(
|
|
245
|
+
segStart: number,
|
|
246
|
+
segEnd: number,
|
|
247
|
+
interval: number,
|
|
248
|
+
): number[] {
|
|
249
|
+
const effectiveInterval = computeEffectiveInterval(segEnd - segStart, interval);
|
|
250
|
+
const timestamps: number[] = [];
|
|
251
|
+
let t = segStart;
|
|
252
|
+
while (t < segEnd) {
|
|
253
|
+
timestamps.push(parseFloat(t.toFixed(3)));
|
|
254
|
+
t += effectiveInterval;
|
|
255
|
+
}
|
|
256
|
+
return timestamps;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create default section boundaries by splitting the duration into equal halves.
|
|
261
|
+
*/
|
|
262
|
+
export function createDefaultSections(durationSeconds: number): SectionBoundary[] {
|
|
263
|
+
const mid = durationSeconds / 2;
|
|
264
|
+
return [
|
|
265
|
+
{ label: 'section_1', startSeconds: 0, endSeconds: mid },
|
|
266
|
+
{ label: 'section_2', startSeconds: mid, endSeconds: durationSeconds },
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Subject registry (color sampling)
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Sample ~10 frames evenly spread across the video for subject registry analysis.
|
|
276
|
+
* Returns indices into the total frame set.
|
|
277
|
+
*/
|
|
278
|
+
export function sampleFrameIndices(totalFrames: number, sampleCount: number = 10): number[] {
|
|
279
|
+
if (totalFrames <= sampleCount) {
|
|
280
|
+
return Array.from({ length: totalFrames }, (_, i) => i);
|
|
281
|
+
}
|
|
282
|
+
const step = totalFrames / sampleCount;
|
|
283
|
+
const indices: number[] = [];
|
|
284
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
285
|
+
indices.push(Math.min(Math.floor(i * step), totalFrames - 1));
|
|
286
|
+
}
|
|
287
|
+
return indices;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Extract dominant colors from a frame image using ffmpeg's thumbnail and showinfo filters.
|
|
292
|
+
* Returns hex color strings.
|
|
293
|
+
*/
|
|
294
|
+
async function extractDominantColors(framePath: string): Promise<string[]> {
|
|
295
|
+
// Use ffmpeg to extract a palette from the frame
|
|
296
|
+
const result = await spawnWithTimeout([
|
|
297
|
+
'ffmpeg', '-i', framePath,
|
|
298
|
+
'-vf', 'palettegen=max_colors=4:stats_mode=diff',
|
|
299
|
+
'-f', 'null', '-',
|
|
300
|
+
], 30_000);
|
|
301
|
+
|
|
302
|
+
// Fallback: return empty if analysis fails
|
|
303
|
+
if (result.exitCode !== 0) return [];
|
|
304
|
+
|
|
305
|
+
// Parse palette info from stderr — look for color hex values
|
|
306
|
+
const colors: string[] = [];
|
|
307
|
+
const colorMatches = result.stderr.matchAll(/0x([0-9a-fA-F]{6})/g);
|
|
308
|
+
for (const m of colorMatches) {
|
|
309
|
+
colors.push(`#${m[1].toLowerCase()}`);
|
|
310
|
+
}
|
|
311
|
+
return colors.length > 0 ? colors.slice(0, 4) : [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Build a subject registry by sampling frames and detecting dominant non-background colors.
|
|
316
|
+
*/
|
|
317
|
+
async function buildSubjectRegistry(framePaths: string[]): Promise<SubjectRegistry> {
|
|
318
|
+
if (framePaths.length === 0) {
|
|
319
|
+
return { groups: [] };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const indices = sampleFrameIndices(framePaths.length);
|
|
323
|
+
const sampledPaths = indices.map((i) => framePaths[i]).filter(Boolean);
|
|
324
|
+
|
|
325
|
+
// Collect all dominant colors across sampled frames
|
|
326
|
+
const colorCounts = new Map<string, number>();
|
|
327
|
+
for (const path of sampledPaths) {
|
|
328
|
+
const colors = await extractDominantColors(path);
|
|
329
|
+
for (const c of colors) {
|
|
330
|
+
colorCounts.set(c, (colorCounts.get(c) || 0) + 1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Sort by frequency and pick top groups (skip very common colors likely to be court/background)
|
|
335
|
+
const sorted = [...colorCounts.entries()]
|
|
336
|
+
.sort((a, b) => b[1] - a[1])
|
|
337
|
+
.slice(0, 4);
|
|
338
|
+
|
|
339
|
+
const groups: SubjectGroup[] = sorted.map((entry, i) => ({
|
|
340
|
+
label: `group_${String.fromCharCode(97 + i)}`,
|
|
341
|
+
dominantColor: entry[0],
|
|
342
|
+
identifiers: [],
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
return { groups };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Main preprocess function
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
export async function preprocessForAsset(
|
|
353
|
+
assetId: string,
|
|
354
|
+
options: PreprocessOptions = {},
|
|
355
|
+
onProgress?: (msg: string) => void,
|
|
356
|
+
): Promise<PreprocessManifest> {
|
|
357
|
+
const config: PreprocessConfig = {
|
|
358
|
+
intervalSeconds: options.intervalSeconds ?? 3,
|
|
359
|
+
segmentDuration: options.segmentDuration ?? 20,
|
|
360
|
+
deadTimeThreshold: options.deadTimeThreshold ?? 0.02,
|
|
361
|
+
shortEdge: options.shortEdge ?? 480,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const skipDeadTime = options.skipDeadTime ?? true;
|
|
365
|
+
|
|
366
|
+
const asset = getMediaAssetById(assetId);
|
|
367
|
+
if (!asset) {
|
|
368
|
+
throw new Error(`Media asset not found: ${assetId}`);
|
|
369
|
+
}
|
|
370
|
+
if (asset.mediaType !== 'video') {
|
|
371
|
+
throw new Error(`Preprocess requires a video asset. Got: ${asset.mediaType}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const durationSeconds = asset.durationSeconds ?? 0;
|
|
375
|
+
if (durationSeconds <= 0) {
|
|
376
|
+
throw new Error('Video asset has no duration information.');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Find or create processing stage
|
|
380
|
+
let stage: ProcessingStage | undefined;
|
|
381
|
+
const existingStages = getProcessingStagesForAsset(assetId);
|
|
382
|
+
stage = existingStages.find((s) => s.stage === 'preprocess');
|
|
383
|
+
if (!stage) {
|
|
384
|
+
stage = createProcessingStage({ assetId, stage: 'preprocess' });
|
|
385
|
+
}
|
|
386
|
+
updateProcessingStage(stage.id, { status: 'running', startedAt: Date.now() });
|
|
387
|
+
|
|
388
|
+
const pipelineDir = join(dirname(asset.filePath), 'pipeline', assetId);
|
|
389
|
+
const framesDir = join(pipelineDir, 'frames');
|
|
390
|
+
const tempDir = framesDir + '-tmp-' + randomUUID();
|
|
391
|
+
await mkdir(tempDir, { recursive: true });
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
// Step 1: Dead-time detection
|
|
395
|
+
onProgress?.('Detecting dead time with mpdecimate filter...\n');
|
|
396
|
+
let deadTimeRanges: TimeRange[] = [];
|
|
397
|
+
|
|
398
|
+
if (skipDeadTime) {
|
|
399
|
+
const mpdecimateResult = await spawnWithTimeout([
|
|
400
|
+
'ffmpeg', '-i', asset.filePath,
|
|
401
|
+
'-vf', `mpdecimate=hi=64*${config.deadTimeThreshold}:lo=64*${config.deadTimeThreshold}:frac=1`,
|
|
402
|
+
'-loglevel', 'debug',
|
|
403
|
+
'-f', 'null', '-',
|
|
404
|
+
], FFMPEG_TIMEOUT_MS);
|
|
405
|
+
|
|
406
|
+
const droppedTimestamps = parseDroppedFrameTimestamps(mpdecimateResult.stderr);
|
|
407
|
+
deadTimeRanges = buildDeadTimeRanges(droppedTimestamps);
|
|
408
|
+
onProgress?.(`Found ${deadTimeRanges.length} dead-time range(s).\n`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Step 2: Segmentation
|
|
412
|
+
onProgress?.('Creating segments...\n');
|
|
413
|
+
const liveRanges = computeLiveRanges(durationSeconds, deadTimeRanges);
|
|
414
|
+
const rawSegments = createSegments(liveRanges, config.segmentDuration);
|
|
415
|
+
onProgress?.(`Created ${rawSegments.length} segment(s) from ${liveRanges.length} live range(s).\n`);
|
|
416
|
+
|
|
417
|
+
// Step 3: Frame extraction per segment
|
|
418
|
+
onProgress?.('Extracting frames per segment...\n');
|
|
419
|
+
const segments: Segment[] = [];
|
|
420
|
+
const allFramePaths: string[] = [];
|
|
421
|
+
|
|
422
|
+
const scaleFilter = `scale='if(gt(iw,ih),-1,${config.shortEdge})':'if(gt(iw,ih),${config.shortEdge},-1)'`;
|
|
423
|
+
|
|
424
|
+
for (const seg of rawSegments) {
|
|
425
|
+
const segDuration = seg.endSeconds - seg.startSeconds;
|
|
426
|
+
const effectiveInterval = computeEffectiveInterval(segDuration, config.intervalSeconds);
|
|
427
|
+
const _frameTimestamps = generateFrameTimestamps(seg.startSeconds, seg.endSeconds, config.intervalSeconds);
|
|
428
|
+
|
|
429
|
+
const segTempDir = join(tempDir, seg.id);
|
|
430
|
+
await mkdir(segTempDir, { recursive: true });
|
|
431
|
+
|
|
432
|
+
const vfFilter = `fps=1/${effectiveInterval},${scaleFilter}`;
|
|
433
|
+
|
|
434
|
+
const result = await spawnWithTimeout([
|
|
435
|
+
'ffmpeg', '-y',
|
|
436
|
+
'-ss', String(seg.startSeconds),
|
|
437
|
+
'-t', String(segDuration),
|
|
438
|
+
'-i', asset.filePath,
|
|
439
|
+
'-vf', vfFilter,
|
|
440
|
+
'-q:v', '2',
|
|
441
|
+
join(segTempDir, 'frame-%06d.jpg'),
|
|
442
|
+
], FFMPEG_TIMEOUT_MS);
|
|
443
|
+
|
|
444
|
+
if (result.exitCode !== 0) {
|
|
445
|
+
onProgress?.(`Warning: frame extraction failed for ${seg.id}: ${result.stderr.slice(0, 200)}\n`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const files = await readdir(segTempDir);
|
|
450
|
+
const frameFiles = files
|
|
451
|
+
.filter((f) => f.startsWith('frame-') && f.endsWith('.jpg'))
|
|
452
|
+
.sort();
|
|
453
|
+
|
|
454
|
+
const framePaths = frameFiles.map((f) => join(framesDir, seg.id, f));
|
|
455
|
+
allFramePaths.push(...framePaths.map((_, i) => join(segTempDir, frameFiles[i])));
|
|
456
|
+
|
|
457
|
+
// Use actual extracted frame count to recalculate timestamps
|
|
458
|
+
const actualTimestamps = frameFiles.map((_, i) =>
|
|
459
|
+
parseFloat((seg.startSeconds + i * effectiveInterval).toFixed(3))
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
segments.push({
|
|
463
|
+
id: seg.id,
|
|
464
|
+
startSeconds: seg.startSeconds,
|
|
465
|
+
endSeconds: seg.endSeconds,
|
|
466
|
+
framePaths,
|
|
467
|
+
frameTimestamps: actualTimestamps,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const totalFrames = segments.reduce((sum, s) => sum + s.framePaths.length, 0);
|
|
472
|
+
if (rawSegments.length > 0 && totalFrames === 0) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
`All ${rawSegments.length} segment(s) failed frame extraction — zero usable frames produced.`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
onProgress?.(`Extracted ${totalFrames} total frames across ${segments.length} segments.\n`);
|
|
478
|
+
|
|
479
|
+
// Atomically swap temp dir to durable path
|
|
480
|
+
await rm(framesDir, { recursive: true, force: true });
|
|
481
|
+
await mkdir(dirname(framesDir), { recursive: true });
|
|
482
|
+
await rename(tempDir, framesDir);
|
|
483
|
+
|
|
484
|
+
// Step 4: Subject registry
|
|
485
|
+
onProgress?.('Building subject registry...\n');
|
|
486
|
+
const allExtractedPaths = segments.flatMap((s) => s.framePaths);
|
|
487
|
+
const subjectRegistry = await buildSubjectRegistry(allExtractedPaths);
|
|
488
|
+
onProgress?.(`Identified ${subjectRegistry.groups.length} subject group(s).\n`);
|
|
489
|
+
|
|
490
|
+
// Step 5: Section boundaries
|
|
491
|
+
let sectionBoundaries: SectionBoundary[];
|
|
492
|
+
if (options.sectionConfigPath) {
|
|
493
|
+
const raw = await readFile(options.sectionConfigPath, 'utf-8');
|
|
494
|
+
sectionBoundaries = JSON.parse(raw) as SectionBoundary[];
|
|
495
|
+
} else {
|
|
496
|
+
sectionBoundaries = createDefaultSections(durationSeconds);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Step 6: Register keyframes in DB
|
|
500
|
+
onProgress?.('Registering keyframes in database...\n');
|
|
501
|
+
deleteKeyframesForAsset(assetId);
|
|
502
|
+
|
|
503
|
+
const keyframeRows = segments.flatMap((seg) =>
|
|
504
|
+
seg.framePaths.map((fp, i) => ({
|
|
505
|
+
assetId,
|
|
506
|
+
timestamp: seg.frameTimestamps[i] ?? seg.startSeconds + i * config.intervalSeconds,
|
|
507
|
+
filePath: fp,
|
|
508
|
+
metadata: { segmentId: seg.id, frameIndex: i, intervalSeconds: config.intervalSeconds },
|
|
509
|
+
}))
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
if (keyframeRows.length > 0) {
|
|
513
|
+
insertKeyframesBatch(keyframeRows);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Step 7: Write manifest
|
|
517
|
+
const manifest: PreprocessManifest = {
|
|
518
|
+
assetId,
|
|
519
|
+
videoPath: asset.filePath,
|
|
520
|
+
durationSeconds,
|
|
521
|
+
segments,
|
|
522
|
+
deadTimeRanges,
|
|
523
|
+
subjectRegistry,
|
|
524
|
+
sectionBoundaries,
|
|
525
|
+
config,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const manifestPath = join(pipelineDir, 'manifest.json');
|
|
529
|
+
await mkdir(pipelineDir, { recursive: true });
|
|
530
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
531
|
+
|
|
532
|
+
// Update stage
|
|
533
|
+
updateProcessingStage(stage.id, {
|
|
534
|
+
status: 'completed',
|
|
535
|
+
progress: 100,
|
|
536
|
+
completedAt: Date.now(),
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
onProgress?.(`Preprocess complete. Manifest written to ${manifestPath}\n`);
|
|
540
|
+
|
|
541
|
+
return manifest;
|
|
542
|
+
} catch (err) {
|
|
543
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
544
|
+
const msg = (err as Error).message;
|
|
545
|
+
updateProcessingStage(stage.id, {
|
|
546
|
+
status: 'failed',
|
|
547
|
+
lastError: msg.slice(0, 500),
|
|
548
|
+
});
|
|
549
|
+
throw err;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Processing pipeline service.
|
|
3
3
|
*
|
|
4
4
|
* Orchestrates the full media processing pipeline with reliability features:
|
|
5
|
-
* - Sequential stage execution:
|
|
5
|
+
* - Sequential stage execution: preprocess -> map -> reduce
|
|
6
6
|
* - Stage-level retries with exponential backoff
|
|
7
7
|
* - Resumability: checks processing_stages to find last completed stage
|
|
8
8
|
* - Cancellation support: cooperative cancellation via asset status = 'cancelled'
|
|
@@ -27,10 +27,9 @@ import { computeRetryDelay, sleep } from '../../../../util/retry.js';
|
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
29
|
export type PipelineStageName =
|
|
30
|
-
| '
|
|
31
|
-
| '
|
|
32
|
-
| '
|
|
33
|
-
| 'event_detection';
|
|
30
|
+
| 'preprocess'
|
|
31
|
+
| 'map'
|
|
32
|
+
| 'reduce';
|
|
34
33
|
|
|
35
34
|
export interface StageHandler {
|
|
36
35
|
/** Execute the stage. Throw on failure. */
|
|
@@ -60,10 +59,9 @@ export interface PipelineResult {
|
|
|
60
59
|
// ---------------------------------------------------------------------------
|
|
61
60
|
|
|
62
61
|
const STAGE_ORDER: PipelineStageName[] = [
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'event_detection',
|
|
62
|
+
'preprocess',
|
|
63
|
+
'map',
|
|
64
|
+
'reduce',
|
|
67
65
|
];
|
|
68
66
|
|
|
69
67
|
// ---------------------------------------------------------------------------
|