@remnic/core 9.4.2 → 9.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +6 -6
- package/dist/access-admin-ops-surface.js +28 -28
- package/dist/access-boundary.d.ts +27 -8
- package/dist/access-boundary.js +33 -29
- package/dist/access-cli.js +83 -81
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +52 -6
- package/dist/access-http.js +40 -38
- package/dist/access-identity-continuity-surface.d.ts +5 -5
- package/dist/access-identity-continuity-surface.js +28 -28
- package/dist/access-lcm-surface.d.ts +6 -6
- package/dist/access-lcm-surface.js +28 -28
- package/dist/access-mcp.d.ts +16 -6
- package/dist/access-mcp.js +37 -35
- package/dist/access-observe-write-surface.d.ts +6 -6
- package/dist/access-observe-write-surface.js +28 -28
- package/dist/access-operations-batch.js +33 -32
- package/dist/access-operations.d.ts +9 -9
- package/dist/access-operations.js +35 -34
- package/dist/access-recall-surface.d.ts +6 -6
- package/dist/access-recall-surface.js +28 -28
- package/dist/access-schema.d.ts +4 -4
- package/dist/access-schema.js +4 -4
- package/dist/{access-service-m-2VO_Ig.d.ts → access-service-CXJaql3A.d.ts} +4 -4
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +28 -28
- package/dist/access-surface-catalog.d.ts +6 -6
- package/dist/access-surface-catalog.js +1 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/access-token-capabilities.d.ts +174 -0
- package/dist/access-token-capabilities.js +33 -0
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +2 -2
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-HMHQHUAG.js → auto-sync-4A5YBWYO.js} +4 -4
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +5 -5
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +9 -9
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-RAARDY3M.js} +2 -2
- package/dist/capsule-crypto-RAARDY3M.js.map +1 -0
- package/dist/{catalog-BxWokdC2.d.ts → catalog-C9cu67Ig.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +13 -13
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-6ZIETXZ2.js → chunk-23JFRB73.js} +2 -2
- package/dist/{chunk-IC6QX5TC.js → chunk-3XIY7MDQ.js} +6 -6
- package/dist/{chunk-FZGGZAAU.js → chunk-4GNEDXDI.js} +146 -2
- package/dist/chunk-4GNEDXDI.js.map +1 -0
- package/dist/{chunk-LOR6ZM5R.js → chunk-4LV23CHK.js} +2 -2
- package/dist/{chunk-E2SPGGUI.js → chunk-4OKLXWDZ.js} +13 -6
- package/dist/chunk-4OKLXWDZ.js.map +1 -0
- package/dist/{chunk-AER6MT24.js → chunk-6SXVCD7W.js} +2 -2
- package/dist/{chunk-OHDJ34CC.js → chunk-72WG5QKN.js} +2 -2
- package/dist/{chunk-CZAZX7JP.js → chunk-7MW3CVLD.js} +4 -4
- package/dist/{chunk-CXLNZXG5.js → chunk-7XGSIUP5.js} +1 -1
- package/dist/{chunk-CZAA5N5S.js → chunk-AYZJID4S.js} +2 -2
- package/dist/{chunk-M7XQSUBB.js → chunk-B3ABVQ36.js} +116 -9
- package/dist/chunk-B3ABVQ36.js.map +1 -0
- package/dist/{chunk-6BISDEN6.js → chunk-BEM4D2QG.js} +14 -14
- package/dist/{chunk-ZPOROUTX.js → chunk-CYWA2CXR.js} +5 -5
- package/dist/{chunk-M5QKGHCR.js → chunk-CYWCKFCL.js} +5 -5
- package/dist/chunk-D7FOKIY5.js +173 -0
- package/dist/chunk-D7FOKIY5.js.map +1 -0
- package/dist/{chunk-4BFRBP45.js → chunk-DEG5ULFJ.js} +2 -2
- package/dist/{chunk-JLR3JSTL.js → chunk-DS7MM2ES.js} +3 -3
- package/dist/{chunk-2BFLDH5F.js → chunk-EEIROWEG.js} +2 -2
- package/dist/{chunk-5QZDMSSN.js → chunk-FKCRGLRD.js} +237 -82
- package/dist/{chunk-5QZDMSSN.js.map → chunk-FKCRGLRD.js.map} +1 -1
- package/dist/{chunk-X4A62ET7.js → chunk-H2DXDQCQ.js} +17 -17
- package/dist/{chunk-27FLVMDC.js → chunk-HLN7RROI.js} +7 -7
- package/dist/chunk-IC2TCZJ2.js +54 -0
- package/dist/chunk-IC2TCZJ2.js.map +1 -0
- package/dist/{chunk-2U2A5HF4.js → chunk-J3XHPFEU.js} +6 -6
- package/dist/{chunk-AKUCU7CI.js → chunk-JI5AVXYO.js} +5 -5
- package/dist/{chunk-KCJ5H6XQ.js → chunk-JSNVWNGX.js} +2 -2
- package/dist/{chunk-7Z2DZNWN.js → chunk-KL4UAXUY.js} +43 -2
- package/dist/{chunk-7Z2DZNWN.js.map → chunk-KL4UAXUY.js.map} +1 -1
- package/dist/{chunk-FLVL4L3L.js → chunk-KTULX4YO.js} +4 -4
- package/dist/{chunk-ILFJ7AVS.js → chunk-LADFWYHY.js} +35 -21
- package/dist/chunk-LADFWYHY.js.map +1 -0
- package/dist/{chunk-437S3G37.js → chunk-LNQUAJNC.js} +1 -1
- package/dist/{chunk-EPGWWCZF.js → chunk-LR5DZ56F.js} +6 -6
- package/dist/{chunk-YDSWRD33.js → chunk-N3JDIRBW.js} +4 -4
- package/dist/{chunk-OUMTBFSQ.js → chunk-NKCPUHCI.js} +5 -5
- package/dist/{chunk-7QWMTOIF.js → chunk-P454KXD6.js} +112 -34
- package/dist/chunk-P454KXD6.js.map +1 -0
- package/dist/{chunk-DR6PEJU3.js → chunk-PCLIWBGD.js} +44 -59
- package/dist/chunk-PCLIWBGD.js.map +1 -0
- package/dist/{chunk-BGTAL46Y.js → chunk-PNLFN2PZ.js} +2 -2
- package/dist/{chunk-P6SYWE4G.js → chunk-PVUJ6WVV.js} +10 -10
- package/dist/{chunk-YPEMNNNY.js → chunk-RGK27ELN.js} +6 -6
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-66FLU5BT.js → chunk-SIRPLF54.js} +6 -6
- package/dist/{chunk-M7GGMGMZ.js → chunk-SVOHGUMI.js} +1202 -126
- package/dist/chunk-SVOHGUMI.js.map +1 -0
- package/dist/{chunk-HRCACVHL.js → chunk-TLUDFPZL.js} +2 -2
- package/dist/{chunk-2FOD2YY3.js → chunk-UFUKHGZD.js} +236 -103
- package/dist/chunk-UFUKHGZD.js.map +1 -0
- package/dist/{chunk-2MR3MFQB.js → chunk-UPSAT4PY.js} +2 -2
- package/dist/{chunk-4P4626RY.js → chunk-UTRJFDP2.js} +4 -4
- package/dist/{chunk-CGS7UKWM.js → chunk-VCW74PNH.js} +2 -2
- package/dist/{chunk-5GPPACXK.js → chunk-VSMRDBFT.js} +2 -1
- package/dist/{chunk-NG7YBCJO.js → chunk-XCL57M6Q.js} +4 -4
- package/dist/{chunk-CLX5VDNY.js → chunk-Y4RKAQZ7.js} +4 -4
- package/dist/{chunk-DGYCWCQ7.js → chunk-Y7UNNAZD.js} +2 -2
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-XN3TUUX2.js → chunk-YQB32INI.js} +2 -2
- package/dist/{cli-EiBBOt3r.d.ts → cli-CqdPA6z9.d.ts} +3 -3
- package/dist/cli.d.ts +7 -7
- package/dist/cli.js +61 -59
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/engine.js +9 -9
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +9 -9
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +13 -11
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/contradiction/index.js +4 -4
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/dashboard-runtime.js +2 -2
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-retrieval.js +9 -9
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +5 -5
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{first-start-migration-DY6YCRC2.js → first-start-migration-VBJQL6OI.js} +5 -5
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +271 -17
- package/dist/index.js +163 -99
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +2 -2
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -9
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/mcp-memory-inspector-app.d.ts +6 -6
- package/dist/mcp-read-only-tools.d.ts +16 -0
- package/dist/mcp-read-only-tools.js +8 -0
- package/dist/mcp-read-only-tools.js.map +1 -0
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- package/dist/namespaces/migrate.js +19 -19
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +10 -10
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/namespaces/storage.js +9 -9
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +25 -25
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +11 -11
- package/dist/{orchestrator-B2Y28Z9t.d.ts → orchestrator-CWFrT8AF.d.ts} +91 -60
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +85 -83
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-Db9mOkzB.d.ts → qmd--fnPMK60.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +4 -4
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/document-scanner.js +2 -2
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +9 -9
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +13 -13
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +3 -3
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +3 -3
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +3 -3
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-BDVIbLRw.d.ts → semantic-consolidation-O5lMZ4LG.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +9 -9
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage-C1maCNbc.d.ts +1591 -0
- package/dist/storage.d.ts +5 -1239
- package/dist/storage.js +8 -8
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/{tier-stats-TG6GUZ6N.js → tier-stats-5XF4UBRQ.js} +4 -4
- package/dist/tokens.d.ts +17 -2
- package/dist/tokens.js +3 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-ljkd82rn.d.ts → types-CGgcNWCG.d.ts} +31 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +10 -10
- package/package.json +2 -2
- package/src/access-boundary.test.ts +62 -0
- package/src/access-boundary.ts +165 -126
- package/src/access-http.test.ts +781 -0
- package/src/access-http.ts +334 -108
- package/src/access-mcp.test.ts +264 -0
- package/src/access-mcp.ts +45 -70
- package/src/access-operations-batch.ts +35 -12
- package/src/access-surface-catalog.test.ts +22 -14
- package/src/access-surface-catalog.ts +6 -3
- package/src/access-token-capabilities.test.ts +844 -0
- package/src/access-token-capabilities.ts +391 -0
- package/src/chat/chat-mcp.test.ts +232 -0
- package/src/chat/chat-session.ts +34 -0
- package/src/cli-wearables-fuse.test.ts +197 -0
- package/src/cli.ts +25 -0
- package/src/index.ts +14 -0
- package/src/mcp-read-only-tools.ts +61 -0
- package/src/storage.ts +16 -0
- package/src/tokens.ts +32 -3
- package/src/wearables/cli.test.ts +102 -0
- package/src/wearables/cli.ts +65 -0
- package/src/wearables/config.test.ts +32 -0
- package/src/wearables/config.ts +52 -0
- package/src/wearables/day-store.test.ts +246 -0
- package/src/wearables/day-store.ts +272 -3
- package/src/wearables/fusion/cluster.ts +230 -0
- package/src/wearables/fusion/fuse.ts +218 -0
- package/src/wearables/fusion/fusion.test.ts +2575 -0
- package/src/wearables/fusion/index.ts +50 -0
- package/src/wearables/fusion/reconcile.ts +831 -0
- package/src/wearables/fusion/reconstruct.ts +251 -0
- package/src/wearables/fusion/store.ts +480 -0
- package/src/wearables/fusion/types.ts +228 -0
- package/src/wearables/index.ts +41 -0
- package/src/wearables/service.test.ts +1030 -2
- package/src/wearables/service.ts +258 -16
- package/src/wearables/speakers.test.ts +128 -0
- package/src/wearables/speakers.ts +58 -4
- package/src/wearables/storage-io.test.ts +146 -0
- package/src/wearables/types.ts +31 -0
- package/dist/chunk-2FOD2YY3.js.map +0 -1
- package/dist/chunk-7QWMTOIF.js.map +0 -1
- package/dist/chunk-DR6PEJU3.js.map +0 -1
- package/dist/chunk-E2SPGGUI.js.map +0 -1
- package/dist/chunk-FZGGZAAU.js.map +0 -1
- package/dist/chunk-ILFJ7AVS.js.map +0 -1
- package/dist/chunk-M7GGMGMZ.js.map +0 -1
- package/dist/chunk-M7XQSUBB.js.map +0 -1
- /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-token-capabilities.js.map} +0 -0
- /package/dist/{auto-sync-HMHQHUAG.js.map → auto-sync-4A5YBWYO.js.map} +0 -0
- /package/dist/{chunk-6ZIETXZ2.js.map → chunk-23JFRB73.js.map} +0 -0
- /package/dist/{chunk-IC6QX5TC.js.map → chunk-3XIY7MDQ.js.map} +0 -0
- /package/dist/{chunk-LOR6ZM5R.js.map → chunk-4LV23CHK.js.map} +0 -0
- /package/dist/{chunk-AER6MT24.js.map → chunk-6SXVCD7W.js.map} +0 -0
- /package/dist/{chunk-OHDJ34CC.js.map → chunk-72WG5QKN.js.map} +0 -0
- /package/dist/{chunk-CZAZX7JP.js.map → chunk-7MW3CVLD.js.map} +0 -0
- /package/dist/{chunk-CXLNZXG5.js.map → chunk-7XGSIUP5.js.map} +0 -0
- /package/dist/{chunk-CZAA5N5S.js.map → chunk-AYZJID4S.js.map} +0 -0
- /package/dist/{chunk-6BISDEN6.js.map → chunk-BEM4D2QG.js.map} +0 -0
- /package/dist/{chunk-ZPOROUTX.js.map → chunk-CYWA2CXR.js.map} +0 -0
- /package/dist/{chunk-M5QKGHCR.js.map → chunk-CYWCKFCL.js.map} +0 -0
- /package/dist/{chunk-4BFRBP45.js.map → chunk-DEG5ULFJ.js.map} +0 -0
- /package/dist/{chunk-JLR3JSTL.js.map → chunk-DS7MM2ES.js.map} +0 -0
- /package/dist/{chunk-2BFLDH5F.js.map → chunk-EEIROWEG.js.map} +0 -0
- /package/dist/{chunk-X4A62ET7.js.map → chunk-H2DXDQCQ.js.map} +0 -0
- /package/dist/{chunk-27FLVMDC.js.map → chunk-HLN7RROI.js.map} +0 -0
- /package/dist/{chunk-2U2A5HF4.js.map → chunk-J3XHPFEU.js.map} +0 -0
- /package/dist/{chunk-AKUCU7CI.js.map → chunk-JI5AVXYO.js.map} +0 -0
- /package/dist/{chunk-KCJ5H6XQ.js.map → chunk-JSNVWNGX.js.map} +0 -0
- /package/dist/{chunk-FLVL4L3L.js.map → chunk-KTULX4YO.js.map} +0 -0
- /package/dist/{chunk-437S3G37.js.map → chunk-LNQUAJNC.js.map} +0 -0
- /package/dist/{chunk-EPGWWCZF.js.map → chunk-LR5DZ56F.js.map} +0 -0
- /package/dist/{chunk-YDSWRD33.js.map → chunk-N3JDIRBW.js.map} +0 -0
- /package/dist/{chunk-OUMTBFSQ.js.map → chunk-NKCPUHCI.js.map} +0 -0
- /package/dist/{chunk-BGTAL46Y.js.map → chunk-PNLFN2PZ.js.map} +0 -0
- /package/dist/{chunk-P6SYWE4G.js.map → chunk-PVUJ6WVV.js.map} +0 -0
- /package/dist/{chunk-YPEMNNNY.js.map → chunk-RGK27ELN.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-66FLU5BT.js.map → chunk-SIRPLF54.js.map} +0 -0
- /package/dist/{chunk-HRCACVHL.js.map → chunk-TLUDFPZL.js.map} +0 -0
- /package/dist/{chunk-2MR3MFQB.js.map → chunk-UPSAT4PY.js.map} +0 -0
- /package/dist/{chunk-4P4626RY.js.map → chunk-UTRJFDP2.js.map} +0 -0
- /package/dist/{chunk-CGS7UKWM.js.map → chunk-VCW74PNH.js.map} +0 -0
- /package/dist/{chunk-5GPPACXK.js.map → chunk-VSMRDBFT.js.map} +0 -0
- /package/dist/{chunk-NG7YBCJO.js.map → chunk-XCL57M6Q.js.map} +0 -0
- /package/dist/{chunk-CLX5VDNY.js.map → chunk-Y4RKAQZ7.js.map} +0 -0
- /package/dist/{chunk-DGYCWCQ7.js.map → chunk-Y7UNNAZD.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-XN3TUUX2.js.map → chunk-YQB32INI.js.map} +0 -0
- /package/dist/{first-start-migration-DY6YCRC2.js.map → first-start-migration-VBJQL6OI.js.map} +0 -0
- /package/dist/{tier-stats-TG6GUZ6N.js.map → tier-stats-5XF4UBRQ.js.map} +0 -0
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wearable cross-source fusion — deterministic reconciliation.
|
|
3
|
+
*
|
|
4
|
+
* Given one cluster (conversations from N sources that overlapped in
|
|
5
|
+
* time), produce a single reconciled timeline where each utterance
|
|
6
|
+
* carries the best source's text plus per-segment provenance, and
|
|
7
|
+
* conflicts that could not be settled deterministically are recorded as
|
|
8
|
+
* disagreements.
|
|
9
|
+
*
|
|
10
|
+
* Reconciliation rules (all deterministic, no LLM):
|
|
11
|
+
* - text: prefer higher source trust, then the more-complete (longer)
|
|
12
|
+
* transcript over a truncated one; otherwise a stable tie-break.
|
|
13
|
+
* - disagreement: when two sources offer genuinely different text for
|
|
14
|
+
* the same window/speaker (neither contains the other), record every
|
|
15
|
+
* candidate and keep a provisional winner marked uncertain — we never
|
|
16
|
+
* silently pick.
|
|
17
|
+
* - speaker: resolved labels unify the wearer across sources; generic
|
|
18
|
+
* labels that cannot be cross-matched keep a lowered confidence.
|
|
19
|
+
*
|
|
20
|
+
* Full segment-level alignment beyond time windows is a deferred
|
|
21
|
+
* follow-up (see PR body). This pass aligns by (speaker, time window).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { resolveSpeaker, type SpeakerRegistry } from "../speakers.js";
|
|
25
|
+
import { maxSegmentExtent } from "./cluster.js";
|
|
26
|
+
import type { WearableConversation } from "../types.js";
|
|
27
|
+
import type {
|
|
28
|
+
FusionConversationInput,
|
|
29
|
+
FusionOptions,
|
|
30
|
+
FusionSegmentInput,
|
|
31
|
+
FusedDisagreement,
|
|
32
|
+
FusedSegment,
|
|
33
|
+
FusedSpeaker,
|
|
34
|
+
SegmentPickReason,
|
|
35
|
+
} from "./types.js";
|
|
36
|
+
|
|
37
|
+
/** Default max drift (ms) for two segments to align across sources. */
|
|
38
|
+
export const DEFAULT_WINDOW_TOLERANCE_MS = 30_000;
|
|
39
|
+
/** Default trust weight for a source with no configured prior. */
|
|
40
|
+
export const DEFAULT_SOURCE_TRUST = 0.8;
|
|
41
|
+
/** Trust prior at which a source is considered "more trusted" than another. */
|
|
42
|
+
const TRUST_DECISION_EPSILON = 1e-9;
|
|
43
|
+
/** Labels that cannot be confidently cross-matched between sources.
|
|
44
|
+
* Raw provider diarization keys (e.g. Omi `SPEAKER_00`) are generic too:
|
|
45
|
+
* they carry no cross-source identity, so they get the same lowered
|
|
46
|
+
* confidence as bare "Speaker N". */
|
|
47
|
+
const GENERIC_LABEL_PATTERN = /^(Speaker \d+|Unknown speaker|SPEAKER_\d+)$/i;
|
|
48
|
+
|
|
49
|
+
export interface FuseClusterResult {
|
|
50
|
+
sources: string[];
|
|
51
|
+
speakers: FusedSpeaker[];
|
|
52
|
+
title?: string;
|
|
53
|
+
summary?: string;
|
|
54
|
+
startIso: string;
|
|
55
|
+
endIso?: string;
|
|
56
|
+
segments: FusedSegment[];
|
|
57
|
+
disagreements: FusedDisagreement[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface TaggedSegment {
|
|
61
|
+
source: string;
|
|
62
|
+
conversationId: string;
|
|
63
|
+
speakerLabel: string;
|
|
64
|
+
isSelf: boolean;
|
|
65
|
+
text: string;
|
|
66
|
+
startMs: number;
|
|
67
|
+
startIso?: string;
|
|
68
|
+
endIso?: string;
|
|
69
|
+
sourceTrust: number;
|
|
70
|
+
/** Original position in the flattened input (sources in first-appearance
|
|
71
|
+
* order, then segment order within each source). Carried through
|
|
72
|
+
* alignment so equal-time segments keep their transcript sequence. */
|
|
73
|
+
originalIndex: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface AlignGroup {
|
|
77
|
+
/** Match key: "self" for the wearer, else the resolved label. */
|
|
78
|
+
key: string;
|
|
79
|
+
speakerLabel: string;
|
|
80
|
+
isSelf: boolean;
|
|
81
|
+
members: TaggedSegment[];
|
|
82
|
+
anchorMs: number;
|
|
83
|
+
/** The anchor member's ORIGINAL start ISO — the canonical utterance
|
|
84
|
+
* time the fused segment is emitted at (groups are sorted by anchorMs,
|
|
85
|
+
* so the emitted startIso must come from here, not from the chosen
|
|
86
|
+
* higher-trust source's later clock, or pre-sorted chronology breaks).
|
|
87
|
+
* Undefined for untimestamped (missing-start) groups. */
|
|
88
|
+
anchorStartIso?: string;
|
|
89
|
+
/** The anchor member's ORIGINAL end ISO, when known. */
|
|
90
|
+
anchorEndIso?: string;
|
|
91
|
+
/** Earliest original sequence index among members — the secondary sort
|
|
92
|
+
* key so equal-anchorMs groups keep transcript/source order. */
|
|
93
|
+
originalIndex: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Match key that unifies the wearer across sources. */
|
|
97
|
+
function speakerKey(label: string, isSelf: boolean): string {
|
|
98
|
+
return isSelf ? "self" : label;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveTrust(
|
|
102
|
+
source: string,
|
|
103
|
+
trustMap: Record<string, number> | undefined,
|
|
104
|
+
): number {
|
|
105
|
+
const value = trustMap?.[source];
|
|
106
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
107
|
+
return Math.min(1, Math.max(0, value));
|
|
108
|
+
}
|
|
109
|
+
return DEFAULT_SOURCE_TRUST;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Lowercased, punctuation-stripped word sequence — order-sensitive. */
|
|
113
|
+
function normalizeWords(text: string): string[] {
|
|
114
|
+
return text
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
117
|
+
.split(/\s+/)
|
|
118
|
+
.filter((word) => word.length > 0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function wordsEqual(a: string[], b: string[]): boolean {
|
|
122
|
+
if (a.length !== b.length) return false;
|
|
123
|
+
for (let i = 0; i < a.length; i++) {
|
|
124
|
+
if (a[i] !== b[i]) return false;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** True when `short` is an exact leading prefix of `long` (truncation). */
|
|
130
|
+
function isWordPrefix(short: string[], long: string[]): boolean {
|
|
131
|
+
if (short.length === 0 || short.length > long.length) return false;
|
|
132
|
+
for (let i = 0; i < short.length; i++) {
|
|
133
|
+
if (short[i] !== long[i]) return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Whether two normalized word sequences corroborate the SAME utterance.
|
|
139
|
+
* Exact match always corroborates; a leading-word-prefix (truncation)
|
|
140
|
+
* corroborates only when `allowPrefix` is set (timestamped alignment),
|
|
141
|
+
* so the more-complete-wins override can reunite a clipped transcript
|
|
142
|
+
* with its full wording. Untimed alignment passes `allowPrefix = false`
|
|
143
|
+
* and demands an exact word match. */
|
|
144
|
+
function wordsCorroborate(
|
|
145
|
+
a: string[],
|
|
146
|
+
b: string[],
|
|
147
|
+
allowPrefix: boolean,
|
|
148
|
+
): boolean {
|
|
149
|
+
if (wordsEqual(a, b)) return true;
|
|
150
|
+
if (!allowPrefix) return false;
|
|
151
|
+
return isWordPrefix(a, b) || isWordPrefix(b, a);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function clamp01(value: number): number {
|
|
155
|
+
if (!Number.isFinite(value)) return 0;
|
|
156
|
+
return Math.min(1, Math.max(0, value));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build fusion inputs from raw provider conversations + the shared
|
|
161
|
+
* speaker registry. This is the precise connector path: full ISO
|
|
162
|
+
* timestamps and resolved speaker labels. (The on-demand service path
|
|
163
|
+
* instead reconstructs inputs from stored transcripts — see
|
|
164
|
+
* `reconstruct.ts`.)
|
|
165
|
+
*/
|
|
166
|
+
export function fusionInputsFromConversations(
|
|
167
|
+
sourceId: string,
|
|
168
|
+
conversations: readonly WearableConversation[],
|
|
169
|
+
registry: SpeakerRegistry,
|
|
170
|
+
): FusionConversationInput[] {
|
|
171
|
+
return conversations.map((conversation) => {
|
|
172
|
+
const segments: FusionSegmentInput[] = conversation.segments.map((raw) => {
|
|
173
|
+
const resolved = resolveSpeaker(sourceId, raw, registry);
|
|
174
|
+
return {
|
|
175
|
+
speaker: resolved.label,
|
|
176
|
+
isSelf: resolved.isSelf,
|
|
177
|
+
text: raw.text,
|
|
178
|
+
...(raw.startIso !== undefined ? { startIso: raw.startIso } : {}),
|
|
179
|
+
...(raw.endIso !== undefined ? { endIso: raw.endIso } : {}),
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
source: sourceId,
|
|
184
|
+
conversationId: conversation.id,
|
|
185
|
+
startIso: conversation.startIso,
|
|
186
|
+
...(conversation.endIso !== undefined ? { endIso: conversation.endIso } : {}),
|
|
187
|
+
...(conversation.title !== undefined ? { title: conversation.title } : {}),
|
|
188
|
+
...(conversation.summary !== undefined ? { summary: conversation.summary } : {}),
|
|
189
|
+
segments,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Fuse one cluster into a reconciled conversation (without id/date).
|
|
196
|
+
*/
|
|
197
|
+
export function fuseCluster(
|
|
198
|
+
cluster: readonly FusionConversationInput[],
|
|
199
|
+
options: FusionOptions = {},
|
|
200
|
+
): FuseClusterResult {
|
|
201
|
+
const trustMap = options.sourceTrust;
|
|
202
|
+
const toleranceMs = options.windowToleranceMs ?? DEFAULT_WINDOW_TOLERANCE_MS;
|
|
203
|
+
|
|
204
|
+
// Sources in first-appearance order.
|
|
205
|
+
const sources: string[] = [];
|
|
206
|
+
const sourcesSeen = new Set<string>();
|
|
207
|
+
for (const conv of cluster) {
|
|
208
|
+
if (!sourcesSeen.has(conv.source)) {
|
|
209
|
+
sourcesSeen.add(conv.source);
|
|
210
|
+
sources.push(conv.source);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Earliest start / latest end across the cluster. The fused END considers
|
|
215
|
+
// BOTH every conversation's explicit endIso AND every segment's own extent
|
|
216
|
+
// (endIso ?? startIso) across ALL members: a stored transcript whose
|
|
217
|
+
// heading end renders as "--:--" rebuilds segments with only a start, so
|
|
218
|
+
// its later segment starts must still extend the fused window — otherwise
|
|
219
|
+
// a source with an explicit SHORT end would clip the fused conversation
|
|
220
|
+
// before the missing-end source's last utterance (issue #1849). Segment
|
|
221
|
+
// extents use the SAME `maxSegmentExtent` primitive as the cluster
|
|
222
|
+
// interval, so there is one coherent end derivation in the pipeline.
|
|
223
|
+
let startMs = Number.POSITIVE_INFINITY;
|
|
224
|
+
let endMs = Number.NEGATIVE_INFINITY;
|
|
225
|
+
let startIso: string | undefined;
|
|
226
|
+
let endIso: string | undefined;
|
|
227
|
+
for (const conv of cluster) {
|
|
228
|
+
const ms = Date.parse(conv.startIso);
|
|
229
|
+
if (Number.isFinite(ms) && ms < startMs) {
|
|
230
|
+
startMs = ms;
|
|
231
|
+
startIso = conv.startIso;
|
|
232
|
+
}
|
|
233
|
+
if (conv.endIso !== undefined) {
|
|
234
|
+
const endM = Date.parse(conv.endIso);
|
|
235
|
+
if (Number.isFinite(endM) && endM > endMs) {
|
|
236
|
+
endMs = endM;
|
|
237
|
+
endIso = conv.endIso;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Fall back to / extend with the latest segment extent across ALL members
|
|
242
|
+
// so a missing-end ("--:--") source's later segment starts are not lost.
|
|
243
|
+
const segmentExtent = maxSegmentExtent(
|
|
244
|
+
cluster.flatMap((conv) => conv.segments),
|
|
245
|
+
);
|
|
246
|
+
if (segmentExtent !== undefined && segmentExtent.ms > endMs) {
|
|
247
|
+
endMs = segmentExtent.ms;
|
|
248
|
+
endIso = segmentExtent.iso;
|
|
249
|
+
}
|
|
250
|
+
// Clamp the fused window end to >= start so it is always valid, mirroring
|
|
251
|
+
// cluster.ts. A malformed explicit/derived end preceding the cluster start
|
|
252
|
+
// would otherwise yield a negative-length window.
|
|
253
|
+
if (endIso !== undefined && Number.isFinite(startMs) && endMs < startMs) {
|
|
254
|
+
endIso = startIso;
|
|
255
|
+
endMs = startMs;
|
|
256
|
+
}
|
|
257
|
+
if (startIso === undefined) {
|
|
258
|
+
// No parseable start in the cluster — fall back to the first conv's
|
|
259
|
+
// start string so the required field stays present downstream.
|
|
260
|
+
startIso = cluster[0]?.startIso ?? new Date(0).toISOString();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Title / summary: first non-empty in cluster order.
|
|
264
|
+
let title: string | undefined;
|
|
265
|
+
let summary: string | undefined;
|
|
266
|
+
for (const conv of cluster) {
|
|
267
|
+
if (title === undefined && conv.title !== undefined && conv.title.trim().length > 0) {
|
|
268
|
+
title = conv.title.trim();
|
|
269
|
+
}
|
|
270
|
+
if (
|
|
271
|
+
summary === undefined &&
|
|
272
|
+
conv.summary !== undefined &&
|
|
273
|
+
conv.summary.trim().length > 0
|
|
274
|
+
) {
|
|
275
|
+
summary = conv.summary.trim();
|
|
276
|
+
}
|
|
277
|
+
if (title !== undefined && summary !== undefined) break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Flatten + tag every segment with its source provenance and trust.
|
|
281
|
+
const tagged: TaggedSegment[] = [];
|
|
282
|
+
for (const conv of cluster) {
|
|
283
|
+
const sourceTrust = resolveTrust(conv.source, trustMap);
|
|
284
|
+
for (const segment of conv.segments) {
|
|
285
|
+
const startMsValue =
|
|
286
|
+
segment.startIso !== undefined ? Date.parse(segment.startIso) : NaN;
|
|
287
|
+
tagged.push({
|
|
288
|
+
source: conv.source,
|
|
289
|
+
conversationId: conv.conversationId,
|
|
290
|
+
speakerLabel: segment.speaker,
|
|
291
|
+
isSelf: segment.isSelf,
|
|
292
|
+
text: segment.text,
|
|
293
|
+
startMs: Number.isFinite(startMsValue)
|
|
294
|
+
? (startMsValue as number)
|
|
295
|
+
: Number.POSITIVE_INFINITY,
|
|
296
|
+
...(segment.startIso !== undefined ? { startIso: segment.startIso } : {}),
|
|
297
|
+
...(segment.endIso !== undefined ? { endIso: segment.endIso } : {}),
|
|
298
|
+
sourceTrust,
|
|
299
|
+
originalIndex: tagged.length,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Deterministic order: time first. Equal-time (and both-missing)
|
|
305
|
+
// segments keep their ORIGINAL transcript/source sequence instead of
|
|
306
|
+
// being scrambled by source or speaker label, so minute-precision and
|
|
307
|
+
// untimestamped utterances are not reordered relative to their input.
|
|
308
|
+
tagged.sort((a, b) => {
|
|
309
|
+
const aFinite = Number.isFinite(a.startMs);
|
|
310
|
+
const bFinite = Number.isFinite(b.startMs);
|
|
311
|
+
if (aFinite && bFinite && a.startMs !== b.startMs) return a.startMs - b.startMs;
|
|
312
|
+
if (aFinite !== bFinite) return aFinite ? -1 : 1;
|
|
313
|
+
return a.originalIndex - b.originalIndex;
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Greedy cross-source alignment: a group holds at most one segment per
|
|
317
|
+
// source (so two sequential same-source utterances never collapse). A
|
|
318
|
+
// segment joins an existing group when their time windows match (or both
|
|
319
|
+
// are untimestamped) AND the text corroborates the SAME utterance.
|
|
320
|
+
// Speaker is matched in two tiers: first an exact same-speaker group;
|
|
321
|
+
// only when none exists does a segment join a group whose utterance
|
|
322
|
+
// corroborates but whose speaker label DISAGREES. That is a speaker
|
|
323
|
+
// conflict on the same utterance — fused into one segment and recorded
|
|
324
|
+
// as a disagreement (see reconcileGroup) rather than emitted as two
|
|
325
|
+
// separate segments. This does NOT collapse distinct utterances: the
|
|
326
|
+
// text must corroborate, so different-worded same-window segments
|
|
327
|
+
// (the r2 separation) still stay apart.
|
|
328
|
+
const groups: AlignGroup[] = [];
|
|
329
|
+
for (const seg of tagged) {
|
|
330
|
+
const key = speakerKey(seg.speakerLabel, seg.isSelf);
|
|
331
|
+
const segMissing = !Number.isFinite(seg.startMs);
|
|
332
|
+
const segWords = normalizeWords(seg.text);
|
|
333
|
+
|
|
334
|
+
// True when `group` has room for `seg`'s source and corroborates the
|
|
335
|
+
// same utterance (matching time window + corroborating text). Speaker
|
|
336
|
+
// is intentionally NOT tested here — the caller decides whether a
|
|
337
|
+
// same-speaker match or a speaker-conflict match is acceptable.
|
|
338
|
+
const corroborates = (group: AlignGroup): boolean => {
|
|
339
|
+
if (group.members.some((member) => member.source === seg.source)) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
// Missing-start segments only align with other missing-start groups.
|
|
343
|
+
const groupMissing = !Number.isFinite(group.anchorMs);
|
|
344
|
+
if (groupMissing !== segMissing) return false;
|
|
345
|
+
if (!groupMissing && Math.abs(seg.startMs - group.anchorMs) > toleranceMs) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
// Cross-source corroboration gate: distinct utterances that merely
|
|
349
|
+
// share a speaker and a time window (timestamped) — or a speaker key
|
|
350
|
+
// alone (untimed) — must NOT collapse. Timestamped alignment accepts
|
|
351
|
+
// an exact word match OR a leading-word prefix (truncation) so the
|
|
352
|
+
// more-complete-wins override can reunite a clipped transcript with
|
|
353
|
+
// its full wording; untimed alignment has no anchor, so exact only.
|
|
354
|
+
return group.members.some((member) =>
|
|
355
|
+
wordsCorroborate(normalizeWords(member.text), segWords, !groupMissing),
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Choose the CLOSEST corroborating group instead of the first match
|
|
360
|
+
// (issue #1849). Within windowToleranceMs several groups can corroborate
|
|
361
|
+
// the same short utterance — e.g. a speaker repeating "yes" at 10:00 and
|
|
362
|
+
// 10:20 with a 30s window — so a later source segment at 10:21
|
|
363
|
+
// corroborates BOTH. First-match would attach it to the earliest (10:00)
|
|
364
|
+
// group even though 10:20 is nearer, misattributing provenance and
|
|
365
|
+
// potentially reordering the fused timeline. Scan every candidate and
|
|
366
|
+
// keep the one whose anchor is nearest to this segment by
|
|
367
|
+
// |anchorMs - startMs|; ties break on the smaller anchor, then on the
|
|
368
|
+
// order the group was created (stable, never dependent on key order).
|
|
369
|
+
// Single-occurrence behavior is unchanged: the lone corroborating group
|
|
370
|
+
// is trivially the closest.
|
|
371
|
+
const closestCorroborating = (
|
|
372
|
+
wantSameSpeaker: boolean,
|
|
373
|
+
): AlignGroup | undefined => {
|
|
374
|
+
let best: AlignGroup | undefined;
|
|
375
|
+
let bestDelta = Infinity;
|
|
376
|
+
for (const group of groups) {
|
|
377
|
+
if (
|
|
378
|
+
(group.key === key) !== wantSameSpeaker ||
|
|
379
|
+
!corroborates(group)
|
|
380
|
+
) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const delta =
|
|
384
|
+
!segMissing && Number.isFinite(group.anchorMs)
|
|
385
|
+
? Math.abs(seg.startMs - group.anchorMs)
|
|
386
|
+
: 0;
|
|
387
|
+
if (
|
|
388
|
+
best === undefined ||
|
|
389
|
+
delta < bestDelta ||
|
|
390
|
+
(delta === bestDelta && group.anchorMs < best.anchorMs)
|
|
391
|
+
) {
|
|
392
|
+
best = group;
|
|
393
|
+
bestDelta = delta;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return best;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Prefer an exact same-speaker corroborating group.
|
|
400
|
+
let chosen = closestCorroborating(true);
|
|
401
|
+
if (chosen === undefined) {
|
|
402
|
+
// Fall back: same utterance (time+text corroborate) but a different
|
|
403
|
+
// speaker label -> speaker conflict, aligned into one segment.
|
|
404
|
+
chosen = closestCorroborating(false);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (chosen !== undefined) {
|
|
408
|
+
chosen.members.push(seg);
|
|
409
|
+
if (seg.originalIndex < chosen.originalIndex) {
|
|
410
|
+
chosen.originalIndex = seg.originalIndex;
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
groups.push({
|
|
414
|
+
key,
|
|
415
|
+
speakerLabel: seg.speakerLabel,
|
|
416
|
+
isSelf: seg.isSelf,
|
|
417
|
+
members: [seg],
|
|
418
|
+
anchorMs: seg.startMs,
|
|
419
|
+
// The creating segment is the EARLIEST member (tagged is
|
|
420
|
+
// time-sorted), so its clock IS the group anchor that groups are
|
|
421
|
+
// sorted by and that the fused segment must be emitted at.
|
|
422
|
+
anchorStartIso: seg.startIso,
|
|
423
|
+
anchorEndIso: seg.endIso,
|
|
424
|
+
originalIndex: seg.originalIndex,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Output groups in time order; missing-start groups last. Equal-anchorMs
|
|
430
|
+
// (and all-missing) groups tie-break on original transcript/source
|
|
431
|
+
// sequence — not source/speaker label — so equal-time utterances keep
|
|
432
|
+
// the order they appeared in their input transcripts.
|
|
433
|
+
groups.sort((a, b) => {
|
|
434
|
+
const aMissing = !Number.isFinite(a.anchorMs);
|
|
435
|
+
const bMissing = !Number.isFinite(b.anchorMs);
|
|
436
|
+
if (aMissing !== bMissing) return aMissing ? 1 : -1;
|
|
437
|
+
if (!aMissing && a.anchorMs !== b.anchorMs) return a.anchorMs - b.anchorMs;
|
|
438
|
+
return a.originalIndex - b.originalIndex;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const segments: FusedSegment[] = [];
|
|
442
|
+
const disagreements: FusedDisagreement[] = [];
|
|
443
|
+
for (const group of groups) {
|
|
444
|
+
const reconciled = reconcileGroup(group);
|
|
445
|
+
segments.push(reconciled.segment);
|
|
446
|
+
disagreements.push(...reconciled.disagreements);
|
|
447
|
+
}
|
|
448
|
+
// Belt-and-suspenders: segments are built from anchor-sorted groups with
|
|
449
|
+
// each segment's startIso pinned to its group anchor, so the array is
|
|
450
|
+
// already chronological. This explicit, STABLE re-sort guarantees that
|
|
451
|
+
// invariant end to end — no trust-based text/content swap can ever
|
|
452
|
+
// reorder the timeline, even if a future change alters how a fused
|
|
453
|
+
// segment's timestamp is derived. Stability contract:
|
|
454
|
+
// - timestamped segments sort by startMs ascending;
|
|
455
|
+
// - missing/invalid timestamps keep their position (timestamped before
|
|
456
|
+
// missing, preserving original relative order among missing ones);
|
|
457
|
+
// - equal timestamps preserve prior order via a deterministic secondary
|
|
458
|
+
// key (the pre-sort index), never reordering by source/speaker — so
|
|
459
|
+
// cross-source clock skew does not scramble same-anchor utterances.
|
|
460
|
+
const preSortOrder = new Map(segments.map((seg, i) => [seg, i]));
|
|
461
|
+
segments.sort((a, b) => {
|
|
462
|
+
const aMs = a.startIso !== undefined ? Date.parse(a.startIso) : Number.NaN;
|
|
463
|
+
const bMs = b.startIso !== undefined ? Date.parse(b.startIso) : Number.NaN;
|
|
464
|
+
const aFinite = Number.isFinite(aMs);
|
|
465
|
+
const bFinite = Number.isFinite(bMs);
|
|
466
|
+
if (aFinite && bFinite) {
|
|
467
|
+
if (aMs !== bMs) return aMs - bMs;
|
|
468
|
+
} else if (aFinite !== bFinite) {
|
|
469
|
+
return aFinite ? -1 : 1;
|
|
470
|
+
}
|
|
471
|
+
// Equal timestamp (or both missing): preserve prior order — never let
|
|
472
|
+
// a naive ts-only comparator scramble equal/missing-time entries.
|
|
473
|
+
return (preSortOrder.get(a) ?? 0) - (preSortOrder.get(b) ?? 0);
|
|
474
|
+
});
|
|
475
|
+
// Cross-segment ASR disagreements: distinct utterances kept as separate
|
|
476
|
+
// segments (never collapsed) yet sharing a speaker + time window and
|
|
477
|
+
// disagreeing on wording. Neither side's content is lost — both remain
|
|
478
|
+
// visible segments — but the unresolved conflict is still surfaced for
|
|
479
|
+
// review and each involved segment's confidence is lowered, matching
|
|
480
|
+
// how an in-group conflict is flagged.
|
|
481
|
+
detectCrossSegmentDisagreements(segments, toleranceMs, disagreements);
|
|
482
|
+
|
|
483
|
+
const result: FuseClusterResult = {
|
|
484
|
+
sources,
|
|
485
|
+
speakers: collectSpeakers(tagged),
|
|
486
|
+
startIso,
|
|
487
|
+
segments,
|
|
488
|
+
disagreements,
|
|
489
|
+
};
|
|
490
|
+
if (endIso !== undefined) result.endIso = endIso;
|
|
491
|
+
if (title !== undefined) result.title = title;
|
|
492
|
+
if (summary !== undefined) result.summary = summary;
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
interface ReconciledGroup {
|
|
497
|
+
segment: FusedSegment;
|
|
498
|
+
disagreements: FusedDisagreement[];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function reconcileGroup(group: AlignGroup): ReconciledGroup {
|
|
502
|
+
const members = group.members;
|
|
503
|
+
// Rank: higher trust, then more-complete (longer), then stable source
|
|
504
|
+
// order, finally the original transcript index — a total order so the
|
|
505
|
+
// comparator returns 0 only for an identical item (never nonzero on a
|
|
506
|
+
// tie), keeping the sort stable and contract-compliant.
|
|
507
|
+
const ranked = [...members].sort((a, b) => {
|
|
508
|
+
if (Math.abs(b.sourceTrust - a.sourceTrust) > TRUST_DECISION_EPSILON) {
|
|
509
|
+
return b.sourceTrust - a.sourceTrust;
|
|
510
|
+
}
|
|
511
|
+
if (b.text.length !== a.text.length) return b.text.length - a.text.length;
|
|
512
|
+
if (a.source !== b.source) return a.source < b.source ? -1 : 1;
|
|
513
|
+
if (a.conversationId !== b.conversationId) {
|
|
514
|
+
return a.conversationId < b.conversationId ? -1 : 1;
|
|
515
|
+
}
|
|
516
|
+
return a.originalIndex - b.originalIndex;
|
|
517
|
+
});
|
|
518
|
+
const top = ranked[0];
|
|
519
|
+
const topWords = normalizeWords(top.text);
|
|
520
|
+
|
|
521
|
+
// Truncation override: the top-ranked (highest-trust) member may be a
|
|
522
|
+
// CLIPPED version of a corroborating candidate — e.g. a summary source
|
|
523
|
+
// clipped at "The deploy window opens at" while a verbatim source has
|
|
524
|
+
// the full sentence. Trust dominates the rank, so without this the
|
|
525
|
+
// truncated high-trust text would win despite the documented
|
|
526
|
+
// "more-complete wins" rule. When the top's words are a strict prefix of
|
|
527
|
+
// another member's words, adopt the LONGEST such more-complete member's
|
|
528
|
+
// text + provenance.
|
|
529
|
+
let chosen = top;
|
|
530
|
+
if (topWords.length > 0) {
|
|
531
|
+
let bestLen = -1;
|
|
532
|
+
for (const candidate of ranked) {
|
|
533
|
+
if (candidate === top) continue;
|
|
534
|
+
const candidateWords = normalizeWords(candidate.text);
|
|
535
|
+
if (
|
|
536
|
+
isWordPrefix(topWords, candidateWords) &&
|
|
537
|
+
candidateWords.length > topWords.length &&
|
|
538
|
+
candidateWords.length > bestLen
|
|
539
|
+
) {
|
|
540
|
+
chosen = candidate;
|
|
541
|
+
bestLen = candidateWords.length;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const truncatedOverride = chosen !== top;
|
|
546
|
+
const others = ranked.filter((member) => member !== chosen);
|
|
547
|
+
|
|
548
|
+
const chosenWords = normalizeWords(chosen.text);
|
|
549
|
+
|
|
550
|
+
const reason: SegmentPickReason =
|
|
551
|
+
members.length === 1
|
|
552
|
+
? "only-source"
|
|
553
|
+
: truncatedOverride
|
|
554
|
+
? "more-complete"
|
|
555
|
+
: others.some(
|
|
556
|
+
(r) => chosen.sourceTrust - r.sourceTrust > TRUST_DECISION_EPSILON,
|
|
557
|
+
)
|
|
558
|
+
? "higher-trust"
|
|
559
|
+
: others.some((r) => chosen.text.length - r.text.length > 0)
|
|
560
|
+
? "more-complete"
|
|
561
|
+
: "tie-break";
|
|
562
|
+
|
|
563
|
+
// Conflict detection: a candidate whose word sequence differs from the
|
|
564
|
+
// chosen text AND is not a containment (one a prefix/truncation of the
|
|
565
|
+
// other). Containment is "more-complete wins" with no disagreement.
|
|
566
|
+
const disagree: Array<{ source: string; value: string }> = [];
|
|
567
|
+
let conflict = false;
|
|
568
|
+
for (const candidate of others) {
|
|
569
|
+
const candidateWords = normalizeWords(candidate.text);
|
|
570
|
+
if (chosenWords.length === 0 && candidateWords.length === 0) continue;
|
|
571
|
+
if (wordsEqual(chosenWords, candidateWords)) continue;
|
|
572
|
+
if (
|
|
573
|
+
isWordPrefix(candidateWords, chosenWords) ||
|
|
574
|
+
isWordPrefix(chosenWords, candidateWords)
|
|
575
|
+
) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
conflict = true;
|
|
579
|
+
disagree.push({ source: candidate.source, value: candidate.text });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Speaker-attribution conflict: members of the same utterance disagree
|
|
583
|
+
// on who spoke (same time window + corroborating text, different label).
|
|
584
|
+
// Detected from member labels — not the group key — so a group that
|
|
585
|
+
// absorbed a conflicting speaker is handled identically. The chosen
|
|
586
|
+
// (top-ranked) member's attribution wins the segment provisionally;
|
|
587
|
+
// every source's label is surfaced as a disagreement with full
|
|
588
|
+
// provenance so no attribution is silently lost.
|
|
589
|
+
const distinctSpeakerKeys = new Set(
|
|
590
|
+
members.map((m) => speakerKey(m.speakerLabel, m.isSelf)),
|
|
591
|
+
);
|
|
592
|
+
const speakerConflict = distinctSpeakerKeys.size > 1;
|
|
593
|
+
|
|
594
|
+
const alternatives = others.map((candidate) => ({
|
|
595
|
+
source: candidate.source,
|
|
596
|
+
text: candidate.text,
|
|
597
|
+
}));
|
|
598
|
+
|
|
599
|
+
// Confidence: single source -> its trust; corroboration boosts; a
|
|
600
|
+
// recorded text OR speaker conflict lowers it.
|
|
601
|
+
const confidence =
|
|
602
|
+
conflict || speakerConflict
|
|
603
|
+
? clamp01(chosen.sourceTrust * 0.7)
|
|
604
|
+
: members.length > 1
|
|
605
|
+
? clamp01(chosen.sourceTrust + 0.1 * (members.length - 1))
|
|
606
|
+
: clamp01(chosen.sourceTrust);
|
|
607
|
+
|
|
608
|
+
// TIMELINE POSITION (startIso/endIso) comes from the group/window ANCHOR
|
|
609
|
+
// — the canonical utterance time the groups are sorted by — NOT from the
|
|
610
|
+
// chosen (higher-trust) source's clock. The chosen source may be LATER
|
|
611
|
+
// than the anchor (a low-trust utterance at 10:00 corroborated by a
|
|
612
|
+
// high-trust source at 10:25 within tolerance); emitting chosen.startIso
|
|
613
|
+
// would print the fused segment at 10:25 even though the group sits at
|
|
614
|
+
// the 10:00 anchor, jumping it past an intervening 10:10 utterance and
|
|
615
|
+
// corrupting the chronology. The chosen source still provides the TEXT;
|
|
616
|
+
// only the timeline position is anchored. Its original clock is kept in
|
|
617
|
+
// provenance so the source's recording time stays traceable.
|
|
618
|
+
const segment: FusedSegment = {
|
|
619
|
+
speaker: chosen.speakerLabel,
|
|
620
|
+
isSelf: chosen.isSelf,
|
|
621
|
+
text: chosen.text,
|
|
622
|
+
confidence,
|
|
623
|
+
provenance: {
|
|
624
|
+
source: chosen.source,
|
|
625
|
+
conversationId: chosen.conversationId,
|
|
626
|
+
sourceTrust: chosen.sourceTrust,
|
|
627
|
+
reason,
|
|
628
|
+
alternatives,
|
|
629
|
+
...(chosen.startIso !== undefined
|
|
630
|
+
? { sourceStartIso: chosen.startIso }
|
|
631
|
+
: {}),
|
|
632
|
+
...(chosen.endIso !== undefined ? { sourceEndIso: chosen.endIso } : {}),
|
|
633
|
+
},
|
|
634
|
+
...(group.anchorStartIso !== undefined
|
|
635
|
+
? { startIso: group.anchorStartIso }
|
|
636
|
+
: {}),
|
|
637
|
+
...(group.anchorEndIso !== undefined ? { endIso: group.anchorEndIso } : {}),
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const disagreements: FusedDisagreement[] = [];
|
|
641
|
+
// The disagreement subject is the EMITTED timeline position (the group
|
|
642
|
+
// anchor), coherent with segment.startIso — not the chosen source's
|
|
643
|
+
// possibly-later clock.
|
|
644
|
+
const anchorIso =
|
|
645
|
+
group.anchorStartIso ??
|
|
646
|
+
(Number.isFinite(group.anchorMs)
|
|
647
|
+
? new Date(group.anchorMs).toISOString()
|
|
648
|
+
: undefined);
|
|
649
|
+
if (conflict) {
|
|
650
|
+
disagreements.push({
|
|
651
|
+
kind: "asr-text",
|
|
652
|
+
subject: anchorIso ?? "(no timestamp)",
|
|
653
|
+
candidates: [{ source: chosen.source, value: chosen.text }, ...disagree],
|
|
654
|
+
provisional: { source: chosen.source, value: chosen.text },
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
if (speakerConflict) {
|
|
658
|
+
// One candidate per source (a group holds one member per source),
|
|
659
|
+
// sorted by source for determinism; the chosen source is provisional.
|
|
660
|
+
const speakerCandidates = members
|
|
661
|
+
.map((m) => ({ source: m.source, value: m.speakerLabel }))
|
|
662
|
+
.sort((a, b) =>
|
|
663
|
+
a.source < b.source ? -1 : a.source > b.source ? 1 : 0,
|
|
664
|
+
);
|
|
665
|
+
disagreements.push({
|
|
666
|
+
kind: "speaker",
|
|
667
|
+
subject: anchorIso ?? "(no timestamp)",
|
|
668
|
+
candidates: speakerCandidates,
|
|
669
|
+
provisional: { source: chosen.source, value: chosen.speakerLabel },
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return { segment, disagreements };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Detect ASR-text disagreements BETWEEN segments that were intentionally
|
|
678
|
+
* kept separate (distinct utterances) yet share a speaker and a time
|
|
679
|
+
* window and disagree on wording. Unlike an in-group conflict (same
|
|
680
|
+
* utterance bridged by a truncation), these never merged — so neither
|
|
681
|
+
* side's content is lost — but the unresolved disagreement is still
|
|
682
|
+
* surfaced for review and each involved segment's confidence is lowered.
|
|
683
|
+
*
|
|
684
|
+
* Timestamped only: untimestamped segments have no window anchor to compare
|
|
685
|
+
* against. Mutates `confidence` on involved segments and appends to `out`.
|
|
686
|
+
*/
|
|
687
|
+
function detectCrossSegmentDisagreements(
|
|
688
|
+
segments: FusedSegment[],
|
|
689
|
+
toleranceMs: number,
|
|
690
|
+
out: FusedDisagreement[],
|
|
691
|
+
): void {
|
|
692
|
+
const n = segments.length;
|
|
693
|
+
if (n < 2) return;
|
|
694
|
+
const claimed = new Array<boolean>(n).fill(false);
|
|
695
|
+
for (let i = 0; i < n; i++) {
|
|
696
|
+
if (claimed[i]) continue;
|
|
697
|
+
const seed = segments[i]!;
|
|
698
|
+
const seedMs =
|
|
699
|
+
seed.startIso !== undefined ? Date.parse(seed.startIso) : NaN;
|
|
700
|
+
if (!Number.isFinite(seedMs)) continue;
|
|
701
|
+
const seedWords = normalizeWords(seed.text);
|
|
702
|
+
const clusterIdx: number[] = [];
|
|
703
|
+
for (let j = i + 1; j < n; j++) {
|
|
704
|
+
if (claimed[j]) continue;
|
|
705
|
+
const cand = segments[j]!;
|
|
706
|
+
// Speaker identity is matched via speakerKey() — the SAME
|
|
707
|
+
// normalization grouping uses (line 305) — not the raw display
|
|
708
|
+
// label. The wearer is often labeled differently per source (e.g.
|
|
709
|
+
// "Me (you)" vs an override "Alex (you)"); both normalize to
|
|
710
|
+
// "self". Matching on the raw label would skip them, leaving a real
|
|
711
|
+
// same-window ASR-text disagreement undetected and both confidences
|
|
712
|
+
// wrongly high. (Issue #1849.)
|
|
713
|
+
if (
|
|
714
|
+
speakerKey(cand.speaker, cand.isSelf) !==
|
|
715
|
+
speakerKey(seed.speaker, seed.isSelf)
|
|
716
|
+
) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (cand.provenance.source === seed.provenance.source) continue;
|
|
720
|
+
const candMs =
|
|
721
|
+
cand.startIso !== undefined ? Date.parse(cand.startIso) : NaN;
|
|
722
|
+
if (!Number.isFinite(candMs)) continue;
|
|
723
|
+
if (Math.abs(candMs - seedMs) > toleranceMs) continue;
|
|
724
|
+
if (wordsCorroborate(seedWords, normalizeWords(cand.text), true)) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
clusterIdx.push(j);
|
|
728
|
+
}
|
|
729
|
+
if (clusterIdx.length === 0) continue;
|
|
730
|
+
clusterIdx.unshift(i);
|
|
731
|
+
for (const idx of clusterIdx) claimed[idx] = true;
|
|
732
|
+
|
|
733
|
+
const involved = clusterIdx.map((idx) => segments[idx]!);
|
|
734
|
+
// Stable secondary key: each involved segment's position in the
|
|
735
|
+
// reconciled segment array (clusterIdx is ascending). Two cands can
|
|
736
|
+
// share a source, so source alone is NOT a total order here; the
|
|
737
|
+
// position tie-break makes the comparator return 0 only for an
|
|
738
|
+
// identical item and keeps the ranking deterministic — mirroring
|
|
739
|
+
// reconcileGroup's originalIndex tie-break.
|
|
740
|
+
const involvedOrder = new Map(involved.map((seg, i) => [seg, i]));
|
|
741
|
+
involved.sort((a, b) => {
|
|
742
|
+
if (
|
|
743
|
+
Math.abs(b.provenance.sourceTrust - a.provenance.sourceTrust) >
|
|
744
|
+
TRUST_DECISION_EPSILON
|
|
745
|
+
) {
|
|
746
|
+
return b.provenance.sourceTrust - a.provenance.sourceTrust;
|
|
747
|
+
}
|
|
748
|
+
if (b.text.length !== a.text.length) return b.text.length - a.text.length;
|
|
749
|
+
if (a.provenance.source !== b.provenance.source) {
|
|
750
|
+
return a.provenance.source < b.provenance.source ? -1 : 1;
|
|
751
|
+
}
|
|
752
|
+
return involvedOrder.get(a)! - involvedOrder.get(b)!;
|
|
753
|
+
});
|
|
754
|
+
const provisional = involved[0]!;
|
|
755
|
+
const anchorIso = involved
|
|
756
|
+
.map((s) => s.startIso)
|
|
757
|
+
.filter((v): v is string => v !== undefined)
|
|
758
|
+
.sort()[0];
|
|
759
|
+
|
|
760
|
+
for (const seg of involved) {
|
|
761
|
+
seg.confidence = clamp01(seg.provenance.sourceTrust * 0.7);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
out.push({
|
|
765
|
+
kind: "asr-text",
|
|
766
|
+
subject: anchorIso ?? "(no timestamp)",
|
|
767
|
+
candidates: involved.map((s) => ({
|
|
768
|
+
source: s.provenance.source,
|
|
769
|
+
value: s.text,
|
|
770
|
+
})),
|
|
771
|
+
provisional: {
|
|
772
|
+
source: provisional.provenance.source,
|
|
773
|
+
value: provisional.text,
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Build the reconciled speaker list. The wearer unifies across all
|
|
781
|
+
* sources; generic labels (bare "Speaker N" or a raw key) seen in only
|
|
782
|
+
* one source get a lowered confidence since they could not be
|
|
783
|
+
* confidently matched across sources.
|
|
784
|
+
*/
|
|
785
|
+
function collectSpeakers(tagged: readonly TaggedSegment[]): FusedSpeaker[] {
|
|
786
|
+
interface Acc {
|
|
787
|
+
label: string;
|
|
788
|
+
isSelf: boolean;
|
|
789
|
+
sources: Set<string>;
|
|
790
|
+
generic: boolean;
|
|
791
|
+
}
|
|
792
|
+
const order: string[] = [];
|
|
793
|
+
const acc = new Map<string, Acc>();
|
|
794
|
+
for (const seg of tagged) {
|
|
795
|
+
const key = speakerKey(seg.speakerLabel, seg.isSelf);
|
|
796
|
+
let entry = acc.get(key);
|
|
797
|
+
if (entry === undefined) {
|
|
798
|
+
entry = {
|
|
799
|
+
label: seg.speakerLabel,
|
|
800
|
+
isSelf: seg.isSelf,
|
|
801
|
+
sources: new Set<string>(),
|
|
802
|
+
generic: GENERIC_LABEL_PATTERN.test(seg.speakerLabel),
|
|
803
|
+
};
|
|
804
|
+
acc.set(key, entry);
|
|
805
|
+
order.push(key);
|
|
806
|
+
}
|
|
807
|
+
entry.sources.add(seg.source);
|
|
808
|
+
}
|
|
809
|
+
return order.map((key) => {
|
|
810
|
+
const entry = acc.get(key);
|
|
811
|
+
if (entry === undefined) throw new Error("fusion speaker accumulator miss");
|
|
812
|
+
const sourceCount = entry.sources.size;
|
|
813
|
+
const confidence = entry.isSelf
|
|
814
|
+
? sourceCount > 1
|
|
815
|
+
? 0.95
|
|
816
|
+
: 0.85
|
|
817
|
+
: entry.generic
|
|
818
|
+
? sourceCount > 1
|
|
819
|
+
? 0.7
|
|
820
|
+
: 0.5
|
|
821
|
+
: sourceCount > 1
|
|
822
|
+
? 0.9
|
|
823
|
+
: 0.7;
|
|
824
|
+
return {
|
|
825
|
+
label: entry.label,
|
|
826
|
+
isSelf: entry.isSelf,
|
|
827
|
+
confidence,
|
|
828
|
+
sources: [...entry.sources].sort(),
|
|
829
|
+
};
|
|
830
|
+
});
|
|
831
|
+
}
|