@xopcai/xopc 0.0.389 → 0.0.390
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/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/src/plugin.js +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{activity-store-dvik6t7Z.js → activity-store-DGMAYkw8.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-bWMd5TDC.js → agent-avatar-dicebear-B4AsZzJR.js} +1 -1
- package/dist/gateway/static/root/assets/agent-defaults-settings-panel-9NUY1sO3.js +1 -0
- package/dist/gateway/static/root/assets/agent-plugin-dialog-CNc6ntzP.js +1 -0
- package/dist/gateway/static/root/assets/agents-QNY9Vkrf.js +77 -0
- package/dist/gateway/static/root/assets/{agents-admin-api-BlNGyXpm.js → agents-admin-api-DxdaiFWn.js} +1 -1
- package/dist/gateway/static/root/assets/app-management-settings-panel-DHRfLkYD.js +1 -0
- package/dist/gateway/static/root/assets/{appearance-settings-CUAdx44v.js → appearance-settings-DtKSapgb.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-D5zDB6gp.js → apps-page-B9kuRt_z.js} +1 -1
- package/dist/gateway/static/root/assets/{archive-plugin-Du6dJNcl.js → archive-plugin-AgySTXp_.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-BO8Ig5Z4.js → automations-page-D1o7ZtUW.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-BJenz0P1.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-D7VJjwnr.js → binary-plugins-A1AXOsO6.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-CaJlWTAn.js → block-editor-DUNLkLKF.js} +3 -3
- package/dist/gateway/static/root/assets/{browser-automation-inputs-iyMBJmQX.js → browser-automation-inputs-BvUC6OiW.js} +1 -1
- package/dist/gateway/static/root/assets/browser-automations-page-Dq2LlVMh.js +3 -0
- package/dist/gateway/static/root/assets/browser-settings-page-BOZ-1g51.js +3 -0
- package/dist/gateway/static/root/assets/{capabilities-MCKRMZDK.js → capabilities-BkCutK0V.js} +1 -1
- package/dist/gateway/static/root/assets/{capabilities-CEH1AnXc.js → capabilities-D61uSJPy.js} +1 -1
- package/dist/gateway/static/root/assets/capabilities-page-BDP6gzlv.js +2 -0
- package/dist/gateway/static/root/assets/capabilities-settings-panel-D6PL3Mfl.js +8 -0
- package/dist/gateway/static/root/assets/{capability-header-actions-BIA0Lwt5.js → capability-header-actions-D9uGs4Pw.js} +1 -1
- package/dist/gateway/static/root/assets/channels-xb7n8rwF.js +1 -0
- package/dist/gateway/static/root/assets/chat-terminal-dock-Dgn09YDs.js +36 -0
- package/dist/gateway/static/root/assets/{computer-settings-page-DvY3zgyY.js → computer-settings-page-C66P4ifT.js} +1 -1
- package/dist/gateway/static/root/assets/{connector-service-page-St2-Fj3y.js → connector-service-page-Dp35XdYf.js} +1 -1
- package/dist/gateway/static/root/assets/connectors-page-BHcRxYib.js +1 -0
- package/dist/gateway/static/root/assets/{date-picker-C0-D80ny.js → date-picker-CddHY982.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-CNd7mwMU.js → dependency-picker-Ob6BZTqv.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-C-2wDANn.js → desktop-pet-dmwCV-I4.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-DyhxLrdJ.js → desktop-pet-settings-B-TBkZAZ.js} +1 -1
- package/dist/gateway/static/root/assets/device-pairing-wizard-SWHwRgG3.js +1 -0
- package/dist/gateway/static/root/assets/{directory-picker-path-field-CNivVIPT.js → directory-picker-path-field-6VZa4fT1.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-CGYO3JPw.js → extension-debug-page-C0_njMva.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-Dc51ALAg.js → extension-page-BeBLMFmW.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-provider-BXPILdlw.js → extension-provider-DWI0KAKT.js} +1 -1
- package/dist/gateway/static/root/assets/extension-settings-page-BiLUwoKx.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-config-swr-Bc2MytsF.js → gateway-config-swr-CmYcOqhb.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-realtime-BFK-vlME.js → gateway-realtime-Ci1Jst5g.js} +1 -1
- package/dist/gateway/static/root/assets/gateway-realtime-store-BMUiUQMk.js +1 -0
- package/dist/gateway/static/root/assets/gateway-settings-CzWT_v09.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-startup-retry-BDPMttPx.js → gateway-startup-retry-C-BG5M7H.js} +1 -1
- package/dist/gateway/static/root/assets/{highlighted-body-KQOG7T2V-DAlRGMge.js → highlighted-body-KQOG7T2V-CsA7lCVc.js} +1 -1
- package/dist/gateway/static/root/assets/home-page-BeXJfxsg.js +2 -0
- package/dist/gateway/static/root/assets/image-generation-api-B_DqCwtV.js +1 -0
- package/dist/gateway/static/root/assets/imports-page-oAvAF9QS.js +1 -0
- package/dist/gateway/static/root/assets/index-W9ls-0Yd.css +1 -0
- package/dist/gateway/static/root/assets/index-xTL-vb51.js +260 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BYYC9Qmw.js → keyboard-shortcuts-settings--MjYe-Pb.js} +1 -1
- package/dist/gateway/static/root/assets/{local-app-workbench-page-BKZfn-MS.js → local-app-workbench-page-DL33dtd7.js} +1 -1
- package/dist/gateway/static/root/assets/{local-apps-page-BU8pUsVM.js → local-apps-page-DOa7mxjB.js} +1 -1
- package/dist/gateway/static/root/assets/{local-session-drafts-kFc_MAA9.js → local-session-drafts-DAtLkTPg.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-Dr43FCty.js → locale-store-DMy9W6IG.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-C-GbTYsM.js +2 -0
- package/dist/gateway/static/root/assets/management-settings-C0TzuD3-.js +1 -0
- package/dist/gateway/static/root/assets/{markdown-view-CdKlPjJM.js → markdown-view-wyOHdXEg.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-DE9zt88i.js → media-plugins-Pb2zr_-L.js} +1 -1
- package/dist/gateway/static/root/assets/mermaid-MCJ5UELQ-BxyGGa4D.js +1 -0
- package/dist/gateway/static/root/assets/messages-O0BEUKnx.js +3 -0
- package/dist/gateway/static/root/assets/models-hub-cache-DdH1MODX.js +1 -0
- package/dist/gateway/static/root/assets/note-detail-panel-BFpEqskQ.js +5 -0
- package/dist/gateway/static/root/assets/{notes-api-BHouNL6-.js → notes-api-CYt2pGsm.js} +1 -1
- package/dist/gateway/static/root/assets/notes-page-h-qqFJ6Z.js +8 -0
- package/dist/gateway/static/root/assets/{page-context-capture-button-TQr8NnSc.js → page-context-capture-button-3uj5EcQC.js} +1 -1
- package/dist/gateway/static/root/assets/personal-page-BGyW6cdt.js +1 -0
- package/dist/gateway/static/root/assets/{product-open-page-BEl6syFp.js → product-open-page-hI13tPS1.js} +1 -1
- package/dist/gateway/static/root/assets/{product-preview-content-BxSHnbqz.js → product-preview-content-lT_in0-Q.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-Baj7fQ7N.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-D7DyQFU3.js +1 -0
- package/dist/gateway/static/root/assets/{registry-api-BD5IgDOA.js → registry-api-CgQrCqL6.js} +1 -1
- package/dist/gateway/static/root/assets/remote-access-hub-DU_d93Tm.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-api-CjTRoQWT.js → runtime-tools-api-D12qVKeC.js} +1 -1
- package/dist/gateway/static/root/assets/runtime-tools-settings-panel-DOSPOfCU.js +1 -0
- package/dist/gateway/static/root/assets/scenes-page-swyUIF5A.js +2 -0
- package/dist/gateway/static/root/assets/{schema-form-ByG-R6N2.js → schema-form-CwOFgsyh.js} +1 -1
- package/dist/gateway/static/root/assets/{send-session-input-UAVStisp.js → send-session-input-nH8Cw0US.js} +1 -1
- package/dist/gateway/static/root/assets/{session-api-DvgEiNRS.js → session-api-B9HrwF-c.js} +1 -1
- package/dist/gateway/static/root/assets/{session-manager-BOb1tC_Z.js → session-manager-aXqXuqDX.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-CI_PJmlX.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-DCVaeBLW.js +1 -0
- package/dist/gateway/static/root/assets/{settings-page-C1e9m23j.js → settings-page-BN70wE6R.js} +1 -1
- package/dist/gateway/static/root/assets/setup-status-panel-CHzcVRi-.js +1 -0
- package/dist/gateway/static/root/assets/{share-link-dialog-kE_6v5Dp.js → share-link-dialog-B1TDyhPt.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-BT-_yR1h.js → share-preview-page-Br-eAZaP.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-api-Dg_SqHQE.js → shares-api-Bl1ZBh5S.js} +1 -1
- package/dist/gateway/static/root/assets/shares-settings-jgCC_E4Y.js +1 -0
- package/dist/gateway/static/root/assets/{skill-api-BslUYMnX.js → skill-api-ClUw3SRC.js} +1 -1
- package/dist/gateway/static/root/assets/{skill-reload-api-BQ2rTOP1.js → skill-reload-api--1BfIPzb.js} +1 -1
- package/dist/gateway/static/root/assets/skills-page-C9oRZSuL.js +2 -0
- package/dist/gateway/static/root/assets/{system-settings-panel-GpbPfWwo.js → system-settings-panel-tWrqu7ai.js} +1 -1
- package/dist/gateway/static/root/assets/{task-detail-page-5B5ZVAu0.js → task-detail-page-BYRW7Rrr.js} +2 -2
- package/dist/gateway/static/root/assets/{task-expandable-content-DJrrEBB4.js → task-expandable-content-D2t4ahzR.js} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-BKpYnggg.js → theme-store-BBLjE0fi.js} +1 -1
- package/dist/gateway/static/root/assets/{tracing-settings-page-D71w7dwV.js → tracing-settings-page-5ATxQH5c.js} +1 -1
- package/dist/gateway/static/root/assets/{tunnel-api-BV46CvS8.js → tunnel-api-CF1Qh4wU.js} +1 -1
- package/dist/gateway/static/root/assets/{usage-page-rMuwNpJi.js → usage-page-CuEHzXil.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-CzyqRffr.js → use-autosave-tY3keIbm.js} +1 -1
- package/dist/gateway/static/root/assets/{use-composer-attachments-Dviq-1f6.js → use-composer-attachments-CXsocZjt.js} +1 -1
- package/dist/gateway/static/root/assets/{use-file-preview-expanded-D5Kk7Hon.js → use-file-preview-expanded-CEsxnDnQ.js} +2 -2
- package/dist/gateway/static/root/assets/user-model-page-Dvt5TIZ6.js +2 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-DWtbmacd.js → voice-api-key-field-COTNLUU3.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-config-api-DnIjZm_9.js → voice-config-api-DrxBjSja.js} +1 -1
- package/dist/gateway/static/root/assets/voice-transcribe-api-B47j0_xA.js +1 -0
- package/dist/gateway/static/root/assets/{work-discovery-overlay-ChEG71hz.js → work-discovery-overlay-PByMJrhV.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-pdiRKLxP.js → workflow-run-setup-panel-CSUkMo63.js} +2 -2
- package/dist/gateway/static/root/assets/{workflows-page-D9LMxINt.js → workflows-page-BUDvFo3R.js} +3 -3
- package/dist/gateway/static/root/assets/{workspace-api-CZDvu7uA.js → workspace-api-Bmx1O449.js} +2 -2
- package/dist/gateway/static/root/assets/{xopc-cloud-account-card-CbUgg1SK.js → xopc-cloud-account-card-DVsAbvwP.js} +1 -1
- package/dist/gateway/static/root/index.html +28 -27
- package/dist/package.js +1 -1
- package/dist/packages/gateway-contract/src/discussions.js +3 -2
- package/dist/packages/gateway-contract/src/index.js +4 -0
- package/dist/packages/gateway-contract/src/provider-setup.js +6 -0
- package/dist/packages/gateway-contract/src/recording-outcome.js +6 -0
- package/dist/packages/image-providers/src/catalog.js +10 -0
- package/dist/packages/image-providers/src/execute.js +5 -1
- package/dist/packages/image-providers/src/validation.js +19 -1
- package/dist/src/agent/background-review/run-background-review.d.ts +0 -1
- package/dist/src/agent/background-review/run-background-review.js +10 -2
- package/dist/src/agent/embedded/run-for-session.js +26 -3
- package/dist/src/agent/embedded/run-turn.js +12 -0
- package/dist/src/agent/embedded/xopc-stream-bridge.js +3 -0
- package/dist/src/agent/image/generation/runtime.js +12 -14
- package/dist/src/agent/image/understanding/runtime.js +5 -0
- package/dist/src/agent/inbound/inbound-loop.d.ts +1 -15
- package/dist/src/agent/inbound/inbound-loop.js +5 -0
- package/dist/src/agent/mcp/connection-errors.d.ts +3 -0
- package/dist/src/agent/mcp/connection-errors.js +32 -0
- package/dist/src/agent/mcp/connection-setup.d.ts +2 -0
- package/dist/src/agent/mcp/connection-setup.js +38 -18
- package/dist/src/agent/memory/compaction.js +5 -1
- package/dist/src/agent/memory/idle-compaction-scheduler.js +3 -1
- package/dist/src/agent/models/manager.d.ts +1 -0
- package/dist/src/agent/models/manager.js +4 -2
- package/dist/src/agent/orchestration/agent-orchestrator.d.ts +1 -6
- package/dist/src/agent/orchestration/agent-orchestrator.js +5 -0
- package/dist/src/agent/service/process-direct-one-shot.js +5 -0
- package/dist/src/agent/service/process-direct-streaming.d.ts +1 -0
- package/dist/src/agent/service/process-direct-streaming.js +17 -1
- package/dist/src/agent/tools/image-generate-tool.js +5 -1
- package/dist/src/agent-config/resolver.js +5 -0
- package/dist/src/agent-config/schema.d.ts +26 -6
- package/dist/src/agent-config/schema.js +2 -0
- package/dist/src/ai-assist/text-assist.js +5 -16
- package/dist/src/automations/service/action-executor.js +3 -1
- package/dist/src/channels/index.js +1 -1
- package/dist/src/channels/manager.js +1 -1
- package/dist/src/cli/commands/channels.js +1 -1
- package/dist/src/cli/commands/connectors.js +1 -1
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/cli/commands/extension-marketplace.js +1 -1
- package/dist/src/cli/commands/models.js +1 -1
- package/dist/src/cli/commands/onboard.js +1 -1
- package/dist/src/config/agent-model-intents.js +4 -1
- package/dist/src/config/schema.d.ts +11 -5
- package/dist/src/config/schema.js +11 -3
- package/dist/src/discussions/analyzer.d.ts +2 -0
- package/dist/src/discussions/analyzer.js +38 -9
- package/dist/src/discussions/audio-clip.d.ts +3 -0
- package/dist/src/discussions/audio-clip.js +72 -0
- package/dist/src/discussions/live-worker.js +3 -1
- package/dist/src/discussions/long-review.d.ts +17 -0
- package/dist/src/discussions/long-review.js +278 -0
- package/dist/src/discussions/media-timeline.d.ts +21 -0
- package/dist/src/discussions/media-timeline.js +134 -0
- package/dist/src/discussions/organizer-worker.d.ts +4 -1
- package/dist/src/discussions/organizer-worker.js +17 -12
- package/dist/src/discussions/organizer.js +2 -0
- package/dist/src/discussions/processing.d.ts +43 -0
- package/dist/src/discussions/processing.js +125 -0
- package/dist/src/discussions/recording.js +202 -42
- package/dist/src/discussions/recordingJobs.js +2 -0
- package/dist/src/discussions/repository.js +4 -2
- package/dist/src/discussions/review.d.ts +8 -0
- package/dist/src/discussions/review.js +183 -0
- package/dist/src/discussions/sealer.d.ts +7 -1
- package/dist/src/discussions/sealer.js +163 -87
- package/dist/src/discussions/service.d.ts +7 -1
- package/dist/src/discussions/service.js +51 -15
- package/dist/src/discussions/types.d.ts +2 -1
- package/dist/src/extensions/sdk/index.js +1 -1
- package/dist/src/gateway/agent-builtin-tools.d.ts +1 -1
- package/dist/src/gateway/global-defaults-admin.d.ts +3 -1
- package/dist/src/gateway/global-defaults-admin.js +14 -2
- package/dist/src/gateway/hono/lib/config-payload.d.ts +11 -2
- package/dist/src/gateway/hono/lib/config-payload.js +2 -0
- package/dist/src/gateway/hono/lib/voice-settings-revisions.d.ts +5 -0
- package/dist/src/gateway/hono/lib/voice-settings-revisions.js +23 -0
- package/dist/src/gateway/hono/routes/config-patch/misc.js +8 -1
- package/dist/src/gateway/hono/routes/config.js +12 -0
- package/dist/src/gateway/hono/routes/discussions.js +63 -1
- package/dist/src/gateway/hono/routes/global-defaults.js +8 -1
- package/dist/src/gateway/hono/routes/models.js +12 -4
- package/dist/src/gateway/hono/routes/provider-setup.d.ts +3 -0
- package/dist/src/gateway/hono/routes/provider-setup.js +106 -0
- package/dist/src/gateway/hono/routes/status.js +2 -0
- package/dist/src/gateway/hono/routes/voice.js +23 -10
- package/dist/src/gateway/provider-model-setup.d.ts +4 -0
- package/dist/src/gateway/provider-model-setup.js +56 -0
- package/dist/src/gateway/service/config-coordinator.d.ts +2 -2
- package/dist/src/gateway/service.js +7 -7
- package/dist/src/home-intelligence/host.js +2 -2
- package/dist/src/media-shared/http/provider-http.d.ts +0 -6
- package/dist/src/media-shared/http/provider-http.js +5 -4
- package/dist/src/media-understanding/runner.d.ts +0 -28
- package/dist/src/media-understanding/runner.js +4 -0
- package/dist/src/platform/client.d.ts +1 -1
- package/dist/src/platform/contracts.d.ts +1 -1
- package/dist/src/providers/env-keys.js +3 -0
- package/dist/src/providers/extension-stream-bridge.js +3 -0
- package/dist/src/providers/index.js +15 -0
- package/dist/src/providers/model-call.d.ts +5 -2
- package/dist/src/providers/model-call.js +13 -2
- package/dist/src/providers/model-reference-auditor.js +1 -0
- package/dist/src/providers/model-response.js +1 -1
- package/dist/src/providers/model-usage.d.ts +15 -0
- package/dist/src/providers/model-usage.js +50 -0
- package/dist/src/providers/presentation.js +8 -0
- package/dist/src/providers/setup/engine.d.ts +37 -0
- package/dist/src/providers/setup/engine.js +177 -0
- package/dist/src/providers/setup/profiles.d.ts +4 -0
- package/dist/src/providers/setup/profiles.js +94 -0
- package/dist/src/providers/setup/voice.d.ts +23 -0
- package/dist/src/providers/setup/voice.js +234 -0
- package/dist/src/scenes/agentExecutor.d.ts +1 -0
- package/dist/src/scenes/agentExecutor.js +5 -0
- package/dist/src/storage/sqlite/migrations/253_discussion_processing_recovery.sql +20 -0
- package/dist/src/storage/sqlite/migrations/254_discussion_annotations.sql +17 -0
- package/dist/src/storage/sqlite/migrations/255_discussion_chapter_progress.sql +10 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/usage/recorder.js +2 -1
- package/dist/src/usage/scenario-registry.js +128 -123
- package/dist/src/voice/dashscope/streaming-stt-session.js +3 -0
- package/dist/src/voice/dashscope/streaming-tts-stream.js +3 -0
- package/dist/src/voice/dictation.d.ts +12 -0
- package/dist/src/voice/dictation.js +22 -0
- package/dist/src/voice/metadata/builtin.js +54 -0
- package/dist/src/voice/platform-streams.js +3 -0
- package/dist/src/voice/realtime/omniEngine.js +3 -0
- package/dist/src/voice/realtime/omniRoute.js +3 -2
- package/dist/src/voice/realtime/runtime.js +4 -2
- package/dist/src/voice/stt/factory.js +9 -3
- package/dist/src/voice/stt/providers/bailian-plan-transcription.d.ts +2 -0
- package/dist/src/voice/stt/providers/bailian-plan-transcription.js +32 -0
- package/dist/src/voice/stt/providers/index.d.ts +1 -0
- package/dist/src/voice/stt/providers/index.js +2 -1
- package/dist/src/voice/token-plan.d.ts +8 -0
- package/dist/src/voice/token-plan.js +11 -0
- package/dist/src/voice/tts/factory.js +4 -1
- package/dist/src/voice/tts/providers/bailian-plan-speech.d.ts +2 -0
- package/dist/src/voice/tts/providers/bailian-plan-speech.js +84 -0
- package/dist/src/voice/tts/providers/index.d.ts +1 -0
- package/dist/src/voice/tts/providers/index.js +2 -1
- package/dist/src/voice/tts/speak-core.js +10 -1
- package/package.json +4 -4
- package/dist/gateway/static/root/assets/agent-defaults-settings-panel-Ds0Ue2FB.js +0 -1
- package/dist/gateway/static/root/assets/agent-plugin-dialog-UunIxc1q.js +0 -1
- package/dist/gateway/static/root/assets/agents-BLRM4o-U.js +0 -77
- package/dist/gateway/static/root/assets/app-management-settings-panel-Dmy828Pe.js +0 -1
- package/dist/gateway/static/root/assets/automations-workspace-UcCneEs3.js +0 -6
- package/dist/gateway/static/root/assets/browser-automations-page-BjvQnYfO.js +0 -3
- package/dist/gateway/static/root/assets/browser-settings-page-D5ZghI28.js +0 -3
- package/dist/gateway/static/root/assets/capabilities-page-MC8qIrXo.js +0 -2
- package/dist/gateway/static/root/assets/capabilities-settings-panel-Blz3hEzp.js +0 -8
- package/dist/gateway/static/root/assets/channels-DSg_iCVC.js +0 -1
- package/dist/gateway/static/root/assets/chat-terminal-dock-C97SJEwd.js +0 -36
- package/dist/gateway/static/root/assets/connectors-page-DXk-GAsd.js +0 -1
- package/dist/gateway/static/root/assets/device-pairing-wizard-BeSuq__I.js +0 -1
- package/dist/gateway/static/root/assets/extension-settings-page-jYu6qpY2.js +0 -1
- package/dist/gateway/static/root/assets/fallback-models-CoyzvDo7.js +0 -1
- package/dist/gateway/static/root/assets/gateway-realtime-store-C3vH-Iup.js +0 -1
- package/dist/gateway/static/root/assets/gateway-settings-JlT1dlSj.js +0 -1
- package/dist/gateway/static/root/assets/home-page-DOVl8s-1.js +0 -2
- package/dist/gateway/static/root/assets/image-generation-api-BZBiYDmV.js +0 -1
- package/dist/gateway/static/root/assets/imports-page-DgdBTp_0.js +0 -1
- package/dist/gateway/static/root/assets/index-CmC8JkSM.js +0 -260
- package/dist/gateway/static/root/assets/index-Dgzc0oBu.css +0 -1
- package/dist/gateway/static/root/assets/logs-page-BSepiACn.js +0 -2
- package/dist/gateway/static/root/assets/management-settings-dE_ykPNY.js +0 -1
- package/dist/gateway/static/root/assets/mermaid-MCJ5UELQ-65SE6j-_.js +0 -1
- package/dist/gateway/static/root/assets/messages-DlTNQI92.js +0 -3
- package/dist/gateway/static/root/assets/models-hub-cache-C7FAsEe_.js +0 -1
- package/dist/gateway/static/root/assets/note-detail-panel-D9oqhdyk.js +0 -5
- package/dist/gateway/static/root/assets/notes-page-Bd4_7ph8.js +0 -8
- package/dist/gateway/static/root/assets/personal-page-BvWzq4Wp.js +0 -1
- package/dist/gateway/static/root/assets/project-detail-page-D-qkIXVT.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-BQtw1Dfn.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-CYFNz0Jc.js +0 -1
- package/dist/gateway/static/root/assets/runtime-tools-settings-panel-Qq_NyYx_.js +0 -1
- package/dist/gateway/static/root/assets/scenes-page-hGr-ppYk.js +0 -2
- package/dist/gateway/static/root/assets/sessions-page-CpX1Rluw.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-D2s_MlxR.js +0 -1
- package/dist/gateway/static/root/assets/settings-loading-skeleton-BTqvr_Fw.js +0 -1
- package/dist/gateway/static/root/assets/setup-status-panel-BiHRYDHq.js +0 -1
- package/dist/gateway/static/root/assets/shares-settings-DfcAu08T.js +0 -1
- package/dist/gateway/static/root/assets/skills-page-CvKc5nBN.js +0 -2
- package/dist/gateway/static/root/assets/user-model-page-Dhqw3v_u.js +0 -2
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { getSqliteDatabase, init_transaction, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import { getDiscussionCapture, updateDiscussionCapture } from "./repository.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
//#region src/discussions/processing.ts
|
|
5
|
+
init_transaction();
|
|
6
|
+
const LEASE_MS = 6e4;
|
|
7
|
+
const PROCESSING_MAX_ATTEMPTS = 3;
|
|
8
|
+
function getProcessingRun(id) {
|
|
9
|
+
return getSqliteDatabase().prepare("SELECT * FROM discussion_processing WHERE discussion_id=?").get(id);
|
|
10
|
+
}
|
|
11
|
+
/** A new original-audio generation leaves published transcript/summary revisions untouched. */
|
|
12
|
+
function createProcessingRun(capture, forceAudio = false) {
|
|
13
|
+
return runSqliteWriteTransaction((db) => {
|
|
14
|
+
db.prepare("DELETE FROM discussion_processing WHERE discussion_id=?").run(capture.id);
|
|
15
|
+
db.prepare(`INSERT INTO discussion_processing (discussion_id, run_id, audio_sha256, base_transcript_revision, force_audio)
|
|
16
|
+
VALUES (?, ?, ?, ?, ?)`).run(capture.id, randomUUID(), capture.audioSha256 ?? null, capture.transcriptRevision, forceAudio ? 1 : 0);
|
|
17
|
+
return getProcessingRun(capture.id);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function ensureProcessingRun(capture, alignSource = false) {
|
|
21
|
+
const run = getProcessingRun(capture.id);
|
|
22
|
+
return !run || alignSource && (run.audio_sha256 !== (capture.audioSha256 ?? null) || run.base_transcript_revision !== capture.transcriptRevision) ? createProcessingRun(capture, run?.force_audio === 1) : run;
|
|
23
|
+
}
|
|
24
|
+
function resetProcessingRetry(id) {
|
|
25
|
+
getSqliteDatabase().prepare("UPDATE discussion_processing SET retry_stage=NULL, attempt_count=0, next_attempt_at=NULL WHERE discussion_id=?").run(id);
|
|
26
|
+
}
|
|
27
|
+
function processingProgress(id) {
|
|
28
|
+
const run = getProcessingRun(id);
|
|
29
|
+
if (!run) return void 0;
|
|
30
|
+
const row = getSqliteDatabase().prepare(`SELECT COUNT(*) count, COALESCE(SUM(ended_at_ms-started_at_ms),0) duration
|
|
31
|
+
FROM discussion_transcription_checkpoints WHERE run_id=?`).get(run.run_id);
|
|
32
|
+
return {
|
|
33
|
+
fromAudio: run.force_audio === 1,
|
|
34
|
+
completedChunks: row.count,
|
|
35
|
+
transcribedMs: Math.round(row.duration),
|
|
36
|
+
attemptCount: run.attempt_count,
|
|
37
|
+
maxAttempts: 3,
|
|
38
|
+
...run.next_attempt_at !== null ? { nextAttemptAt: run.next_attempt_at } : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function claimSealingCapture(owner, now) {
|
|
42
|
+
return runSqliteWriteTransaction((db) => {
|
|
43
|
+
const row = db.prepare(`SELECT d.id FROM discussion_captures d LEFT JOIN discussion_processing p ON p.discussion_id=d.id
|
|
44
|
+
WHERE d.status IN ('stopping','sealing') AND (p.next_attempt_at IS NULL OR p.next_attempt_at<=?)
|
|
45
|
+
AND (p.lease_until IS NULL OR p.lease_until<=?) ORDER BY d.updated_at, d.id LIMIT 1`).get(now, Date.now());
|
|
46
|
+
if (!row) return null;
|
|
47
|
+
const capture = getDiscussionCapture(row.id);
|
|
48
|
+
ensureProcessingRun(capture, true);
|
|
49
|
+
db.prepare("UPDATE discussion_processing SET lease_owner=?, lease_until=? WHERE discussion_id=?").run(owner, Date.now() + LEASE_MS, row.id);
|
|
50
|
+
return capture;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function renewSealingLease(id, owner) {
|
|
54
|
+
return getSqliteDatabase().prepare(`UPDATE discussion_processing SET lease_until=? WHERE discussion_id=? AND lease_owner=? AND lease_until>?`).run(Date.now() + LEASE_MS, id, owner, Date.now()).changes === 1;
|
|
55
|
+
}
|
|
56
|
+
function releaseSealingLease(id, owner) {
|
|
57
|
+
getSqliteDatabase().prepare("UPDATE discussion_processing SET lease_owner=NULL,lease_until=NULL WHERE discussion_id=? AND lease_owner=?").run(id, owner);
|
|
58
|
+
}
|
|
59
|
+
function assertProcessingRun(run, owner, signal) {
|
|
60
|
+
signal.throwIfAborted();
|
|
61
|
+
const current = getDiscussionCapture(run.discussion_id);
|
|
62
|
+
if (!current || current.status !== "sealing" || current.audioDeletedAt || current.audioSha256 !== (run.audio_sha256 ?? void 0) || current.transcriptRevision !== run.base_transcript_revision || getProcessingRun(current.id)?.run_id !== run.run_id || !renewSealingLease(current.id, owner)) throw new Error("Transcription processing lease or source revision changed");
|
|
63
|
+
}
|
|
64
|
+
function readCheckpoint(runId, start, end) {
|
|
65
|
+
const row = getSqliteDatabase().prepare("SELECT result_json FROM discussion_transcription_checkpoints WHERE run_id=? AND started_at_ms=? AND ended_at_ms=?").get(runId, start, end);
|
|
66
|
+
return row ? JSON.parse(row.result_json) : void 0;
|
|
67
|
+
}
|
|
68
|
+
function saveCheckpoint(runId, start, end, result) {
|
|
69
|
+
getSqliteDatabase().prepare(`INSERT INTO discussion_transcription_checkpoints (run_id,started_at_ms,ended_at_ms,result_json,created_at) VALUES (?,?,?,?,?)`).run(runId, start, end, JSON.stringify(result), Date.now());
|
|
70
|
+
}
|
|
71
|
+
function isTransientProcessingError(error, depth = 0) {
|
|
72
|
+
if (depth > 3) return false;
|
|
73
|
+
const item = error;
|
|
74
|
+
const status = item?.status ?? item?.statusCode;
|
|
75
|
+
if (status !== void 0) return [
|
|
76
|
+
408,
|
|
77
|
+
425,
|
|
78
|
+
429
|
|
79
|
+
].includes(status) || status >= 500;
|
|
80
|
+
const text = error instanceof Error ? `${error.name} ${error.message}` : String(error);
|
|
81
|
+
if (/not configured|no model configured|no api key|invalid.*key|unauthorized|forbidden|authentication|unsupported|invalid.*(audio|time range)|no text|audio.*missing|checksum mismatch|transcript.*missing|ENOENT|lease|revision changed/i.test(text)) return false;
|
|
82
|
+
if (item?.reasonCode === "not_configured" || item?.reasonCode === "unsupported_format") return false;
|
|
83
|
+
if (item?.reasonCode === "timeout" || item?.reasonCode === "provider_error") return true;
|
|
84
|
+
if (/timeout|timed out|rate.?limit|temporar|unavailable|fetch failed|network|connection error|ECONN|ETIMEDOUT|EAI_AGAIN|socket|overload|internal server|server error|try again|invalid discussion organization|invalid evidence|truncated|\b(408|429|5\d\d)\b|SyntaxError/i.test(text)) return true;
|
|
85
|
+
return item?.cause !== void 0 && item.cause !== error ? isTransientProcessingError(item.cause, depth + 1) : false;
|
|
86
|
+
}
|
|
87
|
+
/** Persist bounded retries instead of sleeping inside a worker and blocking other recordings. */
|
|
88
|
+
function failOrRetryProcessing(capture, stage, error) {
|
|
89
|
+
return runSqliteWriteTransaction((db) => {
|
|
90
|
+
const current = getDiscussionCapture(capture.id);
|
|
91
|
+
if (!current || current.status !== (stage === "organization" ? "organizing" : "sealing")) return null;
|
|
92
|
+
const run = ensureProcessingRun(current);
|
|
93
|
+
const attempts = (run.retry_stage === stage ? run.attempt_count : 0) + 1;
|
|
94
|
+
const retry = attempts < 3 && isTransientProcessingError(error);
|
|
95
|
+
const next = retry ? Date.now() + 5e3 * 4 ** (attempts - 1) : null;
|
|
96
|
+
db.prepare("UPDATE discussion_processing SET retry_stage=?,attempt_count=?,next_attempt_at=? WHERE discussion_id=?").run(stage, attempts, next, capture.id);
|
|
97
|
+
return updateDiscussionCapture(capture.id, {
|
|
98
|
+
status: retry ? current.status : "needs_attention",
|
|
99
|
+
failureStage: stage,
|
|
100
|
+
failureCode: retry ? "processing_retry_scheduled" : `${stage}_failed`,
|
|
101
|
+
failureMessage: (error instanceof Error ? error.message : String(error)).slice(0, 1e3)
|
|
102
|
+
}, [current.status]);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Race the signal as well as passing it downstream, so an uncooperative provider cannot hold the worker. */
|
|
106
|
+
async function withProcessingDeadline(work, parent, timeoutMs) {
|
|
107
|
+
const timeout = new AbortController();
|
|
108
|
+
const timer = setTimeout(() => timeout.abort(/* @__PURE__ */ new Error("Processing request timed out")), timeoutMs);
|
|
109
|
+
timer.unref();
|
|
110
|
+
const signal = AbortSignal.any([parent, timeout.signal]);
|
|
111
|
+
let abort;
|
|
112
|
+
try {
|
|
113
|
+
signal.throwIfAborted();
|
|
114
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
115
|
+
abort = () => reject(signal.reason);
|
|
116
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
117
|
+
});
|
|
118
|
+
return await Promise.race([work(signal), aborted]);
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
if (abort) signal.removeEventListener("abort", abort);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
export { PROCESSING_MAX_ATTEMPTS, assertProcessingRun, claimSealingCapture, createProcessingRun, ensureProcessingRun, failOrRetryProcessing, getProcessingRun, isTransientProcessingError, processingProgress, readCheckpoint, releaseSealingLease, renewSealingLease, resetProcessingRetry, saveCheckpoint, withProcessingDeadline };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { getSqliteDatabase, init_transaction, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
-
import {
|
|
2
|
+
import { DISCUSSION_MAX_SAVED_DURATION_MS } from "../../packages/gateway-contract/src/discussions.js";
|
|
3
3
|
import { init_src } from "../../packages/gateway-contract/src/index.js";
|
|
4
4
|
import { resolveNoteMediaDir } from "../notes/paths.js";
|
|
5
5
|
import { DiscussionServiceError } from "./errors.js";
|
|
6
6
|
import { getDiscussionCapture, updateDiscussionCapture } from "./repository.js";
|
|
7
|
+
import { pcmEpoch, probeMediaEpoch } from "./media-timeline.js";
|
|
7
8
|
import { decodeWavToMonoFloat32 } from "../voice/audio/wav.js";
|
|
8
9
|
import { forEachNormalizedAudioSegment } from "../voice/audio/normalize.js";
|
|
9
10
|
import { join } from "node:path";
|
|
@@ -71,6 +72,20 @@ async function saveRecordingChunk(capture, sequence, sha256, body) {
|
|
|
71
72
|
}
|
|
72
73
|
function validateRecordingManifest(capture, input) {
|
|
73
74
|
if (input.containerMode !== void 0 && (input.containerMode !== "independent_wav" || input.mimeType !== "audio/wav")) throw invalid("Unsupported recording container");
|
|
75
|
+
if (input.events !== void 0 && (!Array.isArray(input.events) || input.events.length > 1e3 || input.events.some((event) => !event || !Number.isSafeInteger(event.recordedAt) || event.recordedAt < 0 || !Number.isSafeInteger(event.savedMs) || event.savedMs < 0 || event.savedMs > 7205e3 || ![
|
|
76
|
+
"start",
|
|
77
|
+
"resume",
|
|
78
|
+
"interrupted",
|
|
79
|
+
"seal"
|
|
80
|
+
].includes(event.kind)))) throw invalid("Invalid recording events");
|
|
81
|
+
if (input.epochs !== void 0) {
|
|
82
|
+
if (input.containerMode || !Array.isArray(input.epochs) || input.epochs.length < 2 || input.epochs.length > 100 || input.epochs.some((epoch, index) => !epoch || !Number.isSafeInteger(epoch.firstChunk) || (index === 0 ? epoch.firstChunk !== 0 : epoch.firstChunk <= input.epochs[index - 1].firstChunk) || epoch.firstChunk >= input.chunkCount || !Number.isSafeInteger(epoch.startedAt) || epoch.startedAt < 0 || epoch.previousEndedAt !== void 0 && (!Number.isSafeInteger(epoch.previousEndedAt) || epoch.previousEndedAt < 0) || typeof epoch.mimeType !== "string" || ![
|
|
83
|
+
"audio/webm",
|
|
84
|
+
"audio/mp4",
|
|
85
|
+
"audio/ogg",
|
|
86
|
+
"audio/wav"
|
|
87
|
+
].includes(epoch.mimeType.split(";")[0]))) throw invalid("Invalid recording epochs");
|
|
88
|
+
}
|
|
74
89
|
const mimeType = input.mimeType.split(";")[0];
|
|
75
90
|
if (![
|
|
76
91
|
"audio/wav",
|
|
@@ -89,48 +104,52 @@ async function completeRecording(capture, input, notes, assertLease, signal) {
|
|
|
89
104
|
const target = join(directory(capture), `assembled-${randomUUID()}`);
|
|
90
105
|
const file = await open(target, "w");
|
|
91
106
|
const hash = createHash("sha256");
|
|
107
|
+
let epochs;
|
|
92
108
|
try {
|
|
93
109
|
try {
|
|
94
|
-
if (input.
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
if (input.epochs) epochs = await assembleEpochs(capture, input, notes, file, signal, assertLease);
|
|
111
|
+
else {
|
|
112
|
+
if (input.containerMode === "independent_wav") {
|
|
113
|
+
const samples = chunks.reduce((total, chunk) => total + (chunk.bytes - 44) / 2, 0);
|
|
114
|
+
if (!Number.isSafeInteger(samples) || samples <= 0 || samples > 16e3 * 7205e3 / 1e3) throw invalid("Invalid PCM recording duration");
|
|
115
|
+
const header = recordingWavHeader(samples);
|
|
116
|
+
hash.update(header);
|
|
117
|
+
await file.writeFile(header);
|
|
118
|
+
}
|
|
119
|
+
for (const chunk of chunks) {
|
|
120
|
+
const chunkHash = createHash("sha256");
|
|
121
|
+
let bytes = 0;
|
|
122
|
+
try {
|
|
123
|
+
if (input.containerMode === "independent_wav") {
|
|
124
|
+
signal.throwIfAborted();
|
|
125
|
+
const buffers = [];
|
|
126
|
+
for await (const buffer of createReadStream(join(directory(capture), String(chunk.sequence)))) {
|
|
127
|
+
signal.throwIfAborted();
|
|
128
|
+
bytes += buffer.length;
|
|
129
|
+
if (bytes > 8388608 || bytes > chunk.bytes) throw invalid("Stored WAV chunk is corrupt");
|
|
130
|
+
buffers.push(buffer);
|
|
131
|
+
}
|
|
132
|
+
const data = Buffer.concat(buffers, bytes);
|
|
133
|
+
if (data.length !== chunk.bytes || data.length <= 44 || data.length > 8388608 || data.length % 2 !== 0 || !data.subarray(0, 44).equals(recordingWavHeader((data.length - 44) / 2)) || createHash("sha256").update(data).digest("hex") !== chunk.sha256) throw invalid("Stored WAV chunk is corrupt");
|
|
134
|
+
hash.update(data.subarray(44));
|
|
135
|
+
await file.writeFile(data.subarray(44));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
108
138
|
for await (const buffer of createReadStream(join(directory(capture), String(chunk.sequence)))) {
|
|
109
139
|
signal.throwIfAborted();
|
|
140
|
+
hash.update(buffer);
|
|
141
|
+
chunkHash.update(buffer);
|
|
110
142
|
bytes += buffer.length;
|
|
111
|
-
|
|
112
|
-
buffers.push(buffer);
|
|
143
|
+
await file.writeFile(buffer);
|
|
113
144
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
signal.throwIfAborted();
|
|
122
|
-
hash.update(buffer);
|
|
123
|
-
chunkHash.update(buffer);
|
|
124
|
-
bytes += buffer.length;
|
|
125
|
-
await file.writeFile(buffer);
|
|
126
|
-
}
|
|
127
|
-
if (bytes !== chunk.bytes || chunkHash.digest("hex") !== chunk.sha256) throw invalid("Stored recording chunk is corrupt");
|
|
128
|
-
} catch (error) {
|
|
129
|
-
if (!signal.aborted && (error instanceof DiscussionServiceError || error.code === "ENOENT")) {
|
|
130
|
-
assertLease();
|
|
131
|
-
getSqliteDatabase().prepare("DELETE FROM discussion_recording_chunks WHERE discussion_id=? AND sequence=?").run(capture.id, chunk.sequence);
|
|
145
|
+
if (bytes !== chunk.bytes || chunkHash.digest("hex") !== chunk.sha256) throw invalid("Stored recording chunk is corrupt");
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (!signal.aborted && (error instanceof DiscussionServiceError || error.code === "ENOENT")) {
|
|
148
|
+
assertLease();
|
|
149
|
+
getSqliteDatabase().prepare("DELETE FROM discussion_recording_chunks WHERE discussion_id=? AND sequence=?").run(capture.id, chunk.sequence);
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
132
152
|
}
|
|
133
|
-
throw error;
|
|
134
153
|
}
|
|
135
154
|
}
|
|
136
155
|
await file.sync();
|
|
@@ -141,7 +160,7 @@ async function completeRecording(capture, input, notes, assertLease, signal) {
|
|
|
141
160
|
await forEachNormalizedAudioSegment({
|
|
142
161
|
filePath: target,
|
|
143
162
|
segmentSeconds: 30,
|
|
144
|
-
maxDurationSeconds:
|
|
163
|
+
maxDurationSeconds: DISCUSSION_MAX_SAVED_DURATION_MS / 1e3,
|
|
145
164
|
signal: AbortSignal.any([signal, AbortSignal.timeout(18e4)])
|
|
146
165
|
}, async (buffer) => {
|
|
147
166
|
durationMs += decodeWavToMonoFloat32(buffer).durationSeconds * 1e3;
|
|
@@ -150,11 +169,37 @@ async function completeRecording(capture, input, notes, assertLease, signal) {
|
|
|
150
169
|
const latest = getDiscussionCapture(capture.id);
|
|
151
170
|
if (!latest || latest.status === "cancelled" || latest.audioDeletedAt) throw new DiscussionServiceError("conflict", "Recording was deleted or cancelled");
|
|
152
171
|
assertLease();
|
|
153
|
-
const audioSha256 = hash.digest("hex");
|
|
172
|
+
const audioSha256 = input.epochs ? await hashFile(target, signal) : hash.digest("hex");
|
|
173
|
+
const outputMime = input.epochs ? "audio/wav" : input.mimeType;
|
|
174
|
+
const timeline = {
|
|
175
|
+
primary: input.epochs || input.containerMode === "independent_wav" || outputMime.includes("wav") ? "pcm" : "encoded",
|
|
176
|
+
epochs: epochs ?? [outputMime.includes("wav") ? pcmEpoch(durationMs) : await probeMediaEpoch(target, durationMs, 0, signal)]
|
|
177
|
+
};
|
|
178
|
+
if (input.events?.length) {
|
|
179
|
+
const events = await notes.addAttachment(capture.noteId, {
|
|
180
|
+
name: `recording-events-${capture.id}.json`,
|
|
181
|
+
buffer: Buffer.from(JSON.stringify({
|
|
182
|
+
version: 1,
|
|
183
|
+
discussionId: capture.id,
|
|
184
|
+
events: input.events
|
|
185
|
+
})),
|
|
186
|
+
mimeType: "application/json",
|
|
187
|
+
retainWithoutReference: true
|
|
188
|
+
}, `meeting-events:${capture.id}`);
|
|
189
|
+
if (!events) throw new Error("Discussion note is missing");
|
|
190
|
+
const saved = await notes.getAttachmentPath(capture.noteId, events.id);
|
|
191
|
+
if (!saved) throw new Error("Saved recording events are missing");
|
|
192
|
+
const durable = await open(saved.filePath, "r");
|
|
193
|
+
try {
|
|
194
|
+
await durable.sync();
|
|
195
|
+
} finally {
|
|
196
|
+
await durable.close();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
154
199
|
const attachment = await notes.addAttachment(capture.noteId, {
|
|
155
|
-
name: input.fileName.trim().slice(0, 200) || "meeting.webm",
|
|
200
|
+
name: input.epochs ? "meeting-resumed.wav" : input.fileName.trim().slice(0, 200) || "meeting.webm",
|
|
156
201
|
buffer: { filePath: target },
|
|
157
|
-
mimeType:
|
|
202
|
+
mimeType: outputMime,
|
|
158
203
|
duration: Math.round(durationMs / 1e3),
|
|
159
204
|
retainWithoutReference: true
|
|
160
205
|
}, `meeting-recording:${capture.id}`);
|
|
@@ -175,13 +220,14 @@ async function completeRecording(capture, input, notes, assertLease, signal) {
|
|
|
175
220
|
}
|
|
176
221
|
const updated = runSqliteWriteTransaction(() => {
|
|
177
222
|
assertLease();
|
|
223
|
+
getSqliteDatabase().prepare("INSERT OR REPLACE INTO discussion_media_timeline (discussion_id,audio_sha256,timeline_json) VALUES (?,?,?)").run(capture.id, audioSha256, JSON.stringify(timeline));
|
|
178
224
|
return updateDiscussionCapture(capture.id, {
|
|
179
225
|
status: "stopping",
|
|
180
226
|
audioAttachmentId: attachment.id,
|
|
181
|
-
mimeType:
|
|
227
|
+
mimeType: outputMime,
|
|
182
228
|
audioSha256,
|
|
183
229
|
durationMs: Math.round(durationMs),
|
|
184
|
-
audioSizeBytes:
|
|
230
|
+
audioSizeBytes: attachment.size
|
|
185
231
|
}, [
|
|
186
232
|
"recording",
|
|
187
233
|
"stopping",
|
|
@@ -195,6 +241,120 @@ async function completeRecording(capture, input, notes, assertLease, signal) {
|
|
|
195
241
|
await rm(target, { force: true });
|
|
196
242
|
}
|
|
197
243
|
}
|
|
244
|
+
async function hashFile(path, signal) {
|
|
245
|
+
const hash = createHash("sha256");
|
|
246
|
+
for await (const buffer of createReadStream(path)) {
|
|
247
|
+
signal.throwIfAborted();
|
|
248
|
+
hash.update(buffer);
|
|
249
|
+
}
|
|
250
|
+
return hash.digest("hex");
|
|
251
|
+
}
|
|
252
|
+
/** Independent recorder containers stay immutable; only decoded PCM is joined for playback/STT. */
|
|
253
|
+
async function assembleEpochs(capture, input, notes, output, signal, assertLease) {
|
|
254
|
+
const epochs = [];
|
|
255
|
+
const chunks = listRecordingChunks(capture.id);
|
|
256
|
+
const sources = [];
|
|
257
|
+
let samples = 0;
|
|
258
|
+
await output.writeFile(recordingWavHeader(0));
|
|
259
|
+
for (const [index, epoch] of input.epochs.entries()) {
|
|
260
|
+
const path = join(directory(capture), `epoch-${randomUUID()}`);
|
|
261
|
+
const source = await open(path, "wx");
|
|
262
|
+
try {
|
|
263
|
+
try {
|
|
264
|
+
const end = input.epochs[index + 1]?.firstChunk ?? chunks.length;
|
|
265
|
+
for (const chunk of chunks.slice(epoch.firstChunk, end)) try {
|
|
266
|
+
let bytes = 0;
|
|
267
|
+
const hash = createHash("sha256");
|
|
268
|
+
for await (const buffer of createReadStream(join(directory(capture), String(chunk.sequence)))) {
|
|
269
|
+
signal.throwIfAborted();
|
|
270
|
+
bytes += buffer.length;
|
|
271
|
+
hash.update(buffer);
|
|
272
|
+
await source.writeFile(buffer);
|
|
273
|
+
}
|
|
274
|
+
if (bytes !== chunk.bytes || hash.digest("hex") !== chunk.sha256) throw invalid("Stored recording chunk is corrupt");
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (!signal.aborted && (error instanceof DiscussionServiceError || error.code === "ENOENT")) {
|
|
277
|
+
assertLease();
|
|
278
|
+
getSqliteDatabase().prepare("DELETE FROM discussion_recording_chunks WHERE discussion_id=? AND sequence=?").run(capture.id, chunk.sequence);
|
|
279
|
+
}
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
await source.sync();
|
|
283
|
+
} finally {
|
|
284
|
+
await source.close();
|
|
285
|
+
}
|
|
286
|
+
let epochSamples = 0;
|
|
287
|
+
await forEachNormalizedAudioSegment({
|
|
288
|
+
filePath: path,
|
|
289
|
+
segmentSeconds: 30,
|
|
290
|
+
maxDurationSeconds: DISCUSSION_MAX_SAVED_DURATION_MS / 1e3,
|
|
291
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(18e4)])
|
|
292
|
+
}, async (buffer) => {
|
|
293
|
+
const audio = decodeWavToMonoFloat32(buffer);
|
|
294
|
+
const pcm = Buffer.alloc(audio.samples.length * 2);
|
|
295
|
+
for (let i = 0; i < audio.samples.length; i++) pcm.writeInt16LE(Math.max(-32768, Math.min(32767, Math.round(audio.samples[i] * 32768))), i * 2);
|
|
296
|
+
samples += audio.samples.length;
|
|
297
|
+
epochSamples += audio.samples.length;
|
|
298
|
+
if (samples > 16e3 * 7205e3 / 1e3) throw invalid("Recording exceeds duration limit");
|
|
299
|
+
await output.writeFile(pcm);
|
|
300
|
+
});
|
|
301
|
+
if (!epochSamples) throw invalid("Recording epoch has no decodable audio");
|
|
302
|
+
const epochDuration = epochSamples / 16;
|
|
303
|
+
epochs.push(epoch.mimeType.includes("wav") ? pcmEpoch(epochDuration, (samples - epochSamples) / 16) : await probeMediaEpoch(path, epochDuration, (samples - epochSamples) / 16, signal));
|
|
304
|
+
assertLease();
|
|
305
|
+
signal.throwIfAborted();
|
|
306
|
+
const sha256 = await hashFile(path, signal);
|
|
307
|
+
const extension = epoch.mimeType.includes("mp4") ? "m4a" : epoch.mimeType.includes("ogg") ? "ogg" : epoch.mimeType.includes("wav") ? "wav" : "webm";
|
|
308
|
+
const attachment = await notes.addAttachment(capture.noteId, {
|
|
309
|
+
name: `recording-part-${capture.id}-${index + 1}.${extension}`,
|
|
310
|
+
buffer: { filePath: path },
|
|
311
|
+
mimeType: epoch.mimeType,
|
|
312
|
+
retainWithoutReference: true
|
|
313
|
+
}, `meeting-source:${capture.id}:${index}:${sha256}`);
|
|
314
|
+
if (!attachment) throw new Error("Discussion note is missing");
|
|
315
|
+
const stored = await notes.getAttachmentPath(capture.noteId, attachment.id);
|
|
316
|
+
if (!stored) throw new Error("Saved recording source is missing");
|
|
317
|
+
const durable = await open(stored.filePath, "r");
|
|
318
|
+
try {
|
|
319
|
+
await durable.sync();
|
|
320
|
+
} finally {
|
|
321
|
+
await durable.close();
|
|
322
|
+
}
|
|
323
|
+
sources.push({
|
|
324
|
+
...epoch,
|
|
325
|
+
attachmentId: attachment.id,
|
|
326
|
+
sha256,
|
|
327
|
+
durationMs: epochSamples / 16
|
|
328
|
+
});
|
|
329
|
+
} finally {
|
|
330
|
+
await rm(path, { force: true });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
await output.write(recordingWavHeader(samples), 0, 44, 0);
|
|
334
|
+
assertLease();
|
|
335
|
+
signal.throwIfAborted();
|
|
336
|
+
const manifestAttachment = await notes.addAttachment(capture.noteId, {
|
|
337
|
+
name: `recording-sources-${capture.id}.json`,
|
|
338
|
+
buffer: Buffer.from(JSON.stringify({
|
|
339
|
+
version: 1,
|
|
340
|
+
discussionId: capture.id,
|
|
341
|
+
noteId: capture.noteId,
|
|
342
|
+
sources
|
|
343
|
+
})),
|
|
344
|
+
mimeType: "application/json",
|
|
345
|
+
retainWithoutReference: true
|
|
346
|
+
}, `meeting-sources:${capture.id}`);
|
|
347
|
+
if (!manifestAttachment) throw new Error("Discussion note is missing");
|
|
348
|
+
const manifestFile = await notes.getAttachmentPath(capture.noteId, manifestAttachment.id);
|
|
349
|
+
if (!manifestFile) throw new Error("Saved recording manifest is missing");
|
|
350
|
+
const durable = await open(manifestFile.filePath, "r");
|
|
351
|
+
try {
|
|
352
|
+
await durable.sync();
|
|
353
|
+
} finally {
|
|
354
|
+
await durable.close();
|
|
355
|
+
}
|
|
356
|
+
return epochs;
|
|
357
|
+
}
|
|
198
358
|
function recordingWavHeader(samples) {
|
|
199
359
|
const header = Buffer.alloc(44);
|
|
200
360
|
header.write("RIFF");
|
|
@@ -27,6 +27,8 @@ function submitRecordingJob(discussionId, input) {
|
|
|
27
27
|
mimeType: input.mimeType,
|
|
28
28
|
fileName: input.fileName,
|
|
29
29
|
lastSequence: input.lastSequence,
|
|
30
|
+
...input.events ? { events: input.events } : {},
|
|
31
|
+
...input.epochs ? { epochs: input.epochs } : {},
|
|
30
32
|
...input.containerMode ? { containerMode: input.containerMode } : {}
|
|
31
33
|
});
|
|
32
34
|
return runSqliteWriteTransaction((db) => {
|
|
@@ -170,7 +170,8 @@ function claimNextDiscussionCapture(owner, now = Date.now(), leaseMs = 5 * 6e4)
|
|
|
170
170
|
return runSqliteWriteTransaction((db) => {
|
|
171
171
|
const row = db.prepare(`SELECT * FROM discussion_captures
|
|
172
172
|
WHERE status='organizing' AND (work_lease_owner IS NULL OR work_lease_expires_at <= ?)
|
|
173
|
-
|
|
173
|
+
AND NOT EXISTS (SELECT 1 FROM discussion_processing p WHERE p.discussion_id=discussion_captures.id AND p.next_attempt_at>?)
|
|
174
|
+
ORDER BY updated_at ASC LIMIT 1`).get(now, now);
|
|
174
175
|
if (!row) return null;
|
|
175
176
|
return db.prepare(`UPDATE discussion_captures SET work_lease_owner=?, work_lease_expires_at=?, updated_at=?
|
|
176
177
|
WHERE id=? AND status='organizing' AND (work_lease_owner IS NULL OR work_lease_expires_at <= ?)`).run(owner, now + leaseMs, now, row.id, now).changes ? getDiscussionCapture(row.id) : null;
|
|
@@ -226,7 +227,8 @@ function completeDiscussionTranscriptSegment(discussionId, sequence, owner, text
|
|
|
226
227
|
raw_text=?, display_text=?, provider=?, language=?, confidence=?, audio_blob=NULL,
|
|
227
228
|
lease_owner=NULL, lease_expires_at=NULL, next_attempt_at=NULL, last_error=NULL,
|
|
228
229
|
revision=revision+1, updated_at=? WHERE discussion_id=? AND sequence=?
|
|
229
|
-
AND status='transcribing' AND lease_owner
|
|
230
|
+
AND status='transcribing' AND lease_owner=?
|
|
231
|
+
AND EXISTS (SELECT 1 FROM discussion_captures d WHERE d.id=discussion_id AND d.status IN ('recording','stopping'))`).run(text, text, provider, language ?? null, confidence ?? null, now, discussionId, sequence, owner).changes;
|
|
230
232
|
if (changed) db.prepare("UPDATE discussion_captures SET transcript_revision=transcript_revision+1, updated_at=? WHERE id=?").run(now, discussionId);
|
|
231
233
|
return changed;
|
|
232
234
|
}) ? getDiscussionTranscriptSegment(discussionId, sequence) : null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DiscussionAnnotation, DiscussionAnnotationInput, DiscussionEvidence, DiscussionResolvedAnnotation } from '@xopcai/gateway-contract';
|
|
2
|
+
import type { NotesService } from '../notes/service.js';
|
|
3
|
+
import { type RecordingTimeline } from './media-timeline.js';
|
|
4
|
+
export declare function listAnnotations(id: string): DiscussionAnnotation[];
|
|
5
|
+
export declare function putAnnotation(id: string, value: DiscussionAnnotationInput): DiscussionAnnotation;
|
|
6
|
+
export declare function recordingTimeline(id: string, notes: NotesService): Promise<RecordingTimeline | undefined>;
|
|
7
|
+
export declare function evidenceFor(id: string, revision: number, ids: number[], notes: NotesService): Promise<DiscussionEvidence>;
|
|
8
|
+
export declare function resolvedAnnotations(id: string, notes: NotesService): Promise<DiscussionResolvedAnnotation[]>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { getSqliteDatabase, init_transaction, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import { DiscussionServiceError } from "./errors.js";
|
|
3
|
+
import { getDiscussionCapture, listDiscussionTranscriptSegments } from "./repository.js";
|
|
4
|
+
import { readTranscriptRevision, saveTranscriptRevision } from "./revisions.js";
|
|
5
|
+
import { pcmEpoch, playbackRanges, pointToPcm, probeMediaEpoch } from "./media-timeline.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
//#region src/discussions/review.ts
|
|
8
|
+
init_transaction();
|
|
9
|
+
const anchorSchema = z.object({
|
|
10
|
+
type: z.enum(["time", "transcript"]),
|
|
11
|
+
capturedAt: z.number().int().nonnegative(),
|
|
12
|
+
epoch: z.number().int().min(0).max(100).optional(),
|
|
13
|
+
coordinate: z.enum(["encoded", "pcm"]).optional(),
|
|
14
|
+
offsetMs: z.number().min(0).max(8e6).optional(),
|
|
15
|
+
positionMs: z.number().min(0).max(8e6).optional(),
|
|
16
|
+
transcriptRevision: z.number().int().nonnegative().optional(),
|
|
17
|
+
segmentIds: z.array(z.number().int().nonnegative()).min(1).max(100).optional()
|
|
18
|
+
}).strict().refine((anchor) => anchor.type === "transcript" ? anchor.transcriptRevision !== void 0 && !!anchor.segmentIds?.length && anchor.offsetMs === void 0 && anchor.positionMs === void 0 : anchor.transcriptRevision === void 0 && anchor.segmentIds === void 0 && !(anchor.offsetMs !== void 0 && anchor.positionMs !== void 0) && (anchor.offsetMs === void 0 || anchor.epoch !== void 0));
|
|
19
|
+
const inputSchema = z.object({
|
|
20
|
+
id: z.string().regex(/^[a-zA-Z0-9_-]{1,100}$/),
|
|
21
|
+
kind: z.enum([
|
|
22
|
+
"highlight",
|
|
23
|
+
"question",
|
|
24
|
+
"note"
|
|
25
|
+
]),
|
|
26
|
+
text: z.string().max(4e3),
|
|
27
|
+
anchor: anchorSchema,
|
|
28
|
+
expectedRevision: z.number().int().nonnegative(),
|
|
29
|
+
deleted: z.boolean().optional()
|
|
30
|
+
});
|
|
31
|
+
const map = (row) => ({
|
|
32
|
+
id: row.id,
|
|
33
|
+
kind: row.kind,
|
|
34
|
+
text: row.text,
|
|
35
|
+
anchor: JSON.parse(row.anchor_json),
|
|
36
|
+
revision: row.revision,
|
|
37
|
+
deleted: !!row.deleted,
|
|
38
|
+
createdAt: row.created_at,
|
|
39
|
+
updatedAt: row.updated_at
|
|
40
|
+
});
|
|
41
|
+
const requireCapture = (id) => {
|
|
42
|
+
const capture = getDiscussionCapture(id);
|
|
43
|
+
if (!capture) throw new DiscussionServiceError("not_found", "Discussion not found");
|
|
44
|
+
return capture;
|
|
45
|
+
};
|
|
46
|
+
function listAnnotations(id) {
|
|
47
|
+
requireCapture(id);
|
|
48
|
+
return getSqliteDatabase().prepare("SELECT * FROM discussion_annotations WHERE discussion_id=? ORDER BY created_at,id").all(id).map(map).sort((a, b) => a.anchor.capturedAt - b.anchor.capturedAt || a.id.localeCompare(b.id));
|
|
49
|
+
}
|
|
50
|
+
function putAnnotation(id, value) {
|
|
51
|
+
const parsed = inputSchema.safeParse(value);
|
|
52
|
+
if (!parsed.success) throw new DiscussionServiceError("invalid_input", "Invalid annotation");
|
|
53
|
+
const input = parsed.data;
|
|
54
|
+
return runSqliteWriteTransaction((db) => {
|
|
55
|
+
const capture = requireCapture(id);
|
|
56
|
+
if (input.anchor.type === "transcript") {
|
|
57
|
+
if (input.anchor.transcriptRevision === capture.transcriptRevision) saveTranscriptRevision(id);
|
|
58
|
+
const segments = readTranscriptRevision(id, input.anchor.transcriptRevision);
|
|
59
|
+
if (!segments || input.anchor.segmentIds.some((sequence) => !segments.some((segment) => segment.sequence === sequence && segment.status === "confirmed"))) throw new DiscussionServiceError("conflict", "Transcript evidence is no longer available");
|
|
60
|
+
}
|
|
61
|
+
const row = db.prepare("SELECT * FROM discussion_annotations WHERE discussion_id=? AND id=?").get(id, input.id);
|
|
62
|
+
const current = row ? map(row) : void 0;
|
|
63
|
+
const anchor = JSON.stringify(input.anchor);
|
|
64
|
+
if (current && current.kind === input.kind && current.text === input.text && current.deleted === !!input.deleted && JSON.stringify(current.anchor) === anchor) return current;
|
|
65
|
+
if ((current?.revision ?? 0) !== input.expectedRevision) throw new DiscussionServiceError("conflict", "Annotation changed; reload before saving");
|
|
66
|
+
if (current && JSON.stringify(current.anchor) !== anchor) {
|
|
67
|
+
const previous = current.anchor;
|
|
68
|
+
if (!(previous.type === "time" && previous.offsetMs === void 0 && previous.positionMs === void 0 && input.anchor.type === "time" && previous.capturedAt === input.anchor.capturedAt && previous.epoch === input.anchor.epoch && previous.coordinate === input.anchor.coordinate)) throw new DiscussionServiceError("conflict", "Original annotation anchor is immutable");
|
|
69
|
+
}
|
|
70
|
+
if (!current && Number(db.prepare("SELECT COUNT(*) n FROM discussion_annotations WHERE discussion_id=?").get(id).n) >= 1e3) throw new DiscussionServiceError("invalid_input", "Annotation limit reached");
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const revision = (current?.revision ?? 0) + 1;
|
|
73
|
+
db.prepare(`INSERT INTO discussion_annotations(discussion_id,id,kind,text,anchor_json,revision,deleted,created_at,updated_at)
|
|
74
|
+
VALUES (?,?,?,?,?,?,?,?,?) ON CONFLICT(discussion_id,id) DO UPDATE SET kind=excluded.kind,text=excluded.text,anchor_json=excluded.anchor_json,revision=excluded.revision,deleted=excluded.deleted,updated_at=excluded.updated_at`).run(id, input.id, input.kind, input.text, anchor, revision, input.deleted ? 1 : 0, current?.createdAt ?? now, now);
|
|
75
|
+
return {
|
|
76
|
+
id: input.id,
|
|
77
|
+
kind: input.kind,
|
|
78
|
+
text: input.text,
|
|
79
|
+
anchor: input.anchor,
|
|
80
|
+
revision,
|
|
81
|
+
deleted: !!input.deleted,
|
|
82
|
+
createdAt: current?.createdAt ?? now,
|
|
83
|
+
updatedAt: now
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const timingAttempts = /* @__PURE__ */ new Map();
|
|
88
|
+
const timingInFlight = /* @__PURE__ */ new Map();
|
|
89
|
+
async function recordingTimeline(id, notes) {
|
|
90
|
+
const capture = requireCapture(id);
|
|
91
|
+
if (!capture.audioAttachmentId || capture.audioDeletedAt || !capture.durationMs) return;
|
|
92
|
+
const key = `${id}:${capture.audioSha256}`;
|
|
93
|
+
if (timingInFlight.has(key)) return timingInFlight.get(key);
|
|
94
|
+
const cached = getSqliteDatabase().prepare("SELECT timeline_json FROM discussion_media_timeline WHERE discussion_id=? AND audio_sha256=?").get(id, capture.audioSha256 ?? "");
|
|
95
|
+
let previous;
|
|
96
|
+
try {
|
|
97
|
+
previous = cached ? JSON.parse(cached.timeline_json) : void 0;
|
|
98
|
+
} catch {}
|
|
99
|
+
if (previous && Array.isArray(previous.epochs) && (previous.primary === "pcm" || previous.epochs.every((epoch) => epoch.verified))) return previous;
|
|
100
|
+
if (capture.mimeType?.includes("wav")) return {
|
|
101
|
+
primary: "pcm",
|
|
102
|
+
epochs: [pcmEpoch(capture.durationMs)]
|
|
103
|
+
};
|
|
104
|
+
if (previous && Date.now() - (timingAttempts.get(key) ?? 0) < 3e4) return previous;
|
|
105
|
+
timingAttempts.set(key, Date.now());
|
|
106
|
+
if (timingAttempts.size > 256) timingAttempts.delete(timingAttempts.keys().next().value);
|
|
107
|
+
const pending = (async () => {
|
|
108
|
+
const attachment = await notes.getAttachmentPath(capture.noteId, capture.audioAttachmentId);
|
|
109
|
+
if (!attachment) return;
|
|
110
|
+
const timeline = {
|
|
111
|
+
primary: "encoded",
|
|
112
|
+
epochs: [await probeMediaEpoch(attachment.filePath, capture.durationMs, 0, AbortSignal.timeout(35e3))]
|
|
113
|
+
};
|
|
114
|
+
if (getDiscussionCapture(id)?.audioSha256 === capture.audioSha256) getSqliteDatabase().prepare("INSERT OR REPLACE INTO discussion_media_timeline(discussion_id,audio_sha256,timeline_json) VALUES(?,?,?)").run(id, capture.audioSha256 ?? "", JSON.stringify(timeline));
|
|
115
|
+
return timeline;
|
|
116
|
+
})().finally(() => timingInFlight.delete(key));
|
|
117
|
+
timingInFlight.set(key, pending);
|
|
118
|
+
return pending;
|
|
119
|
+
}
|
|
120
|
+
async function evidenceFor(id, revision, ids, notes) {
|
|
121
|
+
const capture = requireCapture(id);
|
|
122
|
+
if (!Number.isSafeInteger(revision) || revision < 0 || !ids.length || ids.length > 100 || ids.some((id) => !Number.isSafeInteger(id) || id < 0)) throw new DiscussionServiceError("invalid_input", "Invalid evidence reference");
|
|
123
|
+
if (capture.transcriptRevision === revision) saveTranscriptRevision(id);
|
|
124
|
+
const source = readTranscriptRevision(id, revision);
|
|
125
|
+
if (!source || ids.some((id) => !source.some((segment) => segment.sequence === id && segment.status === "confirmed"))) throw new DiscussionServiceError("not_found", "Referenced transcript revision or segment is unavailable");
|
|
126
|
+
const attachment = capture.audioAttachmentId && !capture.audioDeletedAt ? await notes.getAttachmentPath(capture.noteId, capture.audioAttachmentId) : null;
|
|
127
|
+
const timeline = attachment ? await recordingTimeline(id, notes) : void 0;
|
|
128
|
+
return {
|
|
129
|
+
transcriptRevision: revision,
|
|
130
|
+
stale: revision !== getDiscussionCapture(id)?.transcriptRevision,
|
|
131
|
+
audioAvailable: !!attachment,
|
|
132
|
+
timingVerified: !!timeline && (timeline.primary === "pcm" || timeline.epochs.every((epoch) => epoch.verified)),
|
|
133
|
+
segments: source.filter((segment) => ids.includes(segment.sequence)).map((segment) => ({
|
|
134
|
+
sequence: segment.sequence,
|
|
135
|
+
startedAtMs: segment.startedAtMs,
|
|
136
|
+
endedAtMs: segment.endedAtMs,
|
|
137
|
+
text: segment.displayText ?? segment.rawText ?? "",
|
|
138
|
+
speakerLabel: segment.speakerLabel,
|
|
139
|
+
playback: timeline ? playbackRanges(timeline, segment.startedAtMs, segment.endedAtMs) : []
|
|
140
|
+
}))
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function resolvedAnnotations(id, notes) {
|
|
144
|
+
const capture = requireCapture(id);
|
|
145
|
+
const annotations = listAnnotations(id);
|
|
146
|
+
const timeline = await recordingTimeline(id, notes);
|
|
147
|
+
const current = listDiscussionTranscriptSegments(id).filter((segment) => segment.status === "confirmed");
|
|
148
|
+
return Promise.all(annotations.map(async (annotation) => {
|
|
149
|
+
const anchor = annotation.anchor;
|
|
150
|
+
if (annotation.deleted) return {
|
|
151
|
+
annotation,
|
|
152
|
+
state: "unavailable",
|
|
153
|
+
playback: []
|
|
154
|
+
};
|
|
155
|
+
if (anchor.type === "transcript") {
|
|
156
|
+
const evidence = await evidenceFor(id, anchor.transcriptRevision, anchor.segmentIds, notes).catch(() => void 0);
|
|
157
|
+
return {
|
|
158
|
+
annotation,
|
|
159
|
+
state: evidence ? "located" : "unavailable",
|
|
160
|
+
evidence,
|
|
161
|
+
playback: evidence?.segments.flatMap((segment) => segment.playback) ?? []
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const epoch = timeline?.epochs[anchor.epoch ?? 0];
|
|
165
|
+
const position = anchor.positionMs ?? (epoch && anchor.offsetMs !== void 0 ? anchor.coordinate === "pcm" ? anchor.offsetMs < epoch.durationMs ? epoch.baseMs + anchor.offsetMs : void 0 : pointToPcm(epoch, anchor.offsetMs) : void 0);
|
|
166
|
+
if (!(!capture.audioDeletedAt && !!capture.audioAttachmentId && position !== void 0 && position >= 0 && position < (capture.durationMs ?? 0))) return {
|
|
167
|
+
annotation,
|
|
168
|
+
state: !capture.audioAttachmentId && !capture.audioDeletedAt ? "pending" : "unavailable",
|
|
169
|
+
playback: []
|
|
170
|
+
};
|
|
171
|
+
const refs = current.filter((segment) => segment.startedAtMs <= position && segment.endedAtMs > position).map((segment) => segment.sequence);
|
|
172
|
+
const evidence = refs.length ? await evidenceFor(id, capture.transcriptRevision, refs, notes) : void 0;
|
|
173
|
+
return {
|
|
174
|
+
annotation,
|
|
175
|
+
state: "located",
|
|
176
|
+
positionMs: position,
|
|
177
|
+
playback: timeline ? playbackRanges(timeline, Math.max(0, position - 2e3), Math.min(capture.durationMs, position + 8e3)) : [],
|
|
178
|
+
evidence
|
|
179
|
+
};
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
export { evidenceFor, listAnnotations, putAnnotation, recordingTimeline, resolvedAnnotations };
|