reflet-sdk 0.7.10 → 0.7.12
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/CHANGELOG.md +13 -0
- package/README.md +26 -12
- package/dist/desktop.js +1 -1
- package/dist/{devtools-layer-COilp7Ht.js → devtools-layer-CSEp-eZw.js} +3 -3
- package/dist/{devtools-layer-COilp7Ht.js.map → devtools-layer-CSEp-eZw.js.map} +1 -1
- package/dist/devtools-next.js +1 -1
- package/dist/devtools-vite.js +3 -3
- package/dist/feedback.js +4 -4
- package/dist/{handler-D8c3woEz.js → handler-BX8ITlGZ.js} +3 -3
- package/dist/{handler-D8c3woEz.js.map → handler-BX8ITlGZ.js.map} +1 -1
- package/dist/{protocol-ClxYWmhu.js → protocol-P5T66Bs3.js} +4 -1
- package/dist/protocol-P5T66Bs3.js.map +1 -0
- package/dist/{reflet-proxy-Dg6mea-4.js → reflet-proxy-CiMPGYzD.js} +2 -2
- package/dist/{reflet-proxy-Dg6mea-4.js.map → reflet-proxy-CiMPGYzD.js.map} +1 -1
- package/dist/{submit-D7XKlgee.js → submit-CqeMuBPt.js} +2 -2
- package/dist/{submit-D7XKlgee.js.map → submit-CqeMuBPt.js.map} +1 -1
- package/dist/{team-layer-By_81DBU.js → team-layer-BdytvHiG.js} +3 -3
- package/dist/{team-layer-By_81DBU.js.map → team-layer-BdytvHiG.js.map} +1 -1
- package/dist/{use-sheet-tab-QLQ1oeso.js → use-sheet-tab-DTGgQ_1c.js} +786 -57
- package/dist/use-sheet-tab-DTGgQ_1c.js.map +1 -0
- package/package.json +1 -1
- package/dist/protocol-ClxYWmhu.js.map +0 -1
- package/dist/use-sheet-tab-QLQ1oeso.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as EDITORS, n as DEVTOOLS_REQUEST_HEADER, r as DEVTOOLS_ROUTE_BASE, t as DEVTOOLS_ENDPOINTS } from "./protocol-
|
|
2
|
-
import { $ as inspectReactElement, A as PencilIcon, B as DEFAULT_WIDGET_LABELS, C as CheckIcon, D as GripIcon, E as CodeIcon, G as buildElementSelection, H as clip, I as TrashIcon, J as getElementRect, K as buildSelector, N as Svg, P as TargetIcon, Q as getFiberFromNode, T as CloseIcon, U as collectPageContext, V as SDK_VERSION, Y as REDACT_ATTRIBUTE, Z as redactUrl, _ as isSubmitEnter, a as uploadBlob, at as capturedViewportSize, c as useFloatingPosition, d as SelectionOutline, et as resolveComponentStack, h as startsInWidget, it as capturedFromCanvas, k as MoreIcon, l as onViewportChange, m as listenToKeydown, o as ElementPicker, ot as releaseCapture, p as listenOnPageAndWidget, q as describeElement, r as deriveTitle, rt as captureViewport, s as isWidgetOwned, tt as shortenSourcePath, u as visibleViewport, v as matchesHotkey } from "./submit-
|
|
1
|
+
import { a as EDITORS, n as DEVTOOLS_REQUEST_HEADER, r as DEVTOOLS_ROUTE_BASE, t as DEVTOOLS_ENDPOINTS } from "./protocol-P5T66Bs3.js";
|
|
2
|
+
import { $ as inspectReactElement, A as PencilIcon, B as DEFAULT_WIDGET_LABELS, C as CheckIcon, D as GripIcon, E as CodeIcon, G as buildElementSelection, H as clip, I as TrashIcon, J as getElementRect, K as buildSelector, N as Svg, P as TargetIcon, Q as getFiberFromNode, T as CloseIcon, U as collectPageContext, V as SDK_VERSION, Y as REDACT_ATTRIBUTE, Z as redactUrl, _ as isSubmitEnter, a as uploadBlob, at as capturedViewportSize, c as useFloatingPosition, d as SelectionOutline, et as resolveComponentStack, h as startsInWidget, it as capturedFromCanvas, k as MoreIcon, l as onViewportChange, m as listenToKeydown, o as ElementPicker, ot as releaseCapture, p as listenOnPageAndWidget, q as describeElement, r as deriveTitle, rt as captureViewport, s as isWidgetOwned, tt as shortenSourcePath, u as visibleViewport, v as matchesHotkey } from "./submit-CqeMuBPt.js";
|
|
3
3
|
import { t as REFLET_Z_INDEX } from "./z-index-CwokqVq_.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import * as G$1 from "react";
|
|
@@ -271,6 +271,7 @@ const sessionIdentitySchema = z.object({
|
|
|
271
271
|
limits: sessionLimitsSchema,
|
|
272
272
|
organizationId: z.string(),
|
|
273
273
|
organizationName: z.string(),
|
|
274
|
+
speakersConfigured: z.boolean().default(false),
|
|
274
275
|
transcriptionConfigured: z.boolean(),
|
|
275
276
|
user: z.object({
|
|
276
277
|
email: z.string().nullable(),
|
|
@@ -448,6 +449,20 @@ const contextOptionSchema = z.object({
|
|
|
448
449
|
item: segmentContextSchema,
|
|
449
450
|
selected: z.boolean()
|
|
450
451
|
});
|
|
452
|
+
const spokenRunSchema = z.object({
|
|
453
|
+
speaker: z.string().nullable(),
|
|
454
|
+
text: z.string()
|
|
455
|
+
});
|
|
456
|
+
z.object({
|
|
457
|
+
endMs: z.number(),
|
|
458
|
+
speaker: z.string(),
|
|
459
|
+
startMs: z.number()
|
|
460
|
+
});
|
|
461
|
+
const speakerTokenSchema = z.object({
|
|
462
|
+
expiresAt: z.number(),
|
|
463
|
+
token: z.string(),
|
|
464
|
+
url: z.string()
|
|
465
|
+
});
|
|
451
466
|
const sessionSegmentSchema = z.object({
|
|
452
467
|
audioExpiresAt: z.number().nullable(),
|
|
453
468
|
context: z.array(segmentContextSchema),
|
|
@@ -464,6 +479,7 @@ const sessionSegmentSchema = z.object({
|
|
|
464
479
|
language: z.string().nullable(),
|
|
465
480
|
revision: z.number(),
|
|
466
481
|
source: z.enum(["voice", "typed"]),
|
|
482
|
+
speech: z.array(spokenRunSchema).nullable().default(null),
|
|
467
483
|
startMs: z.number(),
|
|
468
484
|
text: z.string(),
|
|
469
485
|
textOrigin: z.enum(["transcription", "human"]),
|
|
@@ -662,6 +678,10 @@ const segmentResponse = z.object({ segment: sessionSegmentSchema });
|
|
|
662
678
|
function createRecordingEndpoints({ post }) {
|
|
663
679
|
return {
|
|
664
680
|
addEvidence: async (body) => (await post("/api/v1/sessions/evidence", body, z.object({ evidence: sessionEvidenceSchema }))).evidence,
|
|
681
|
+
addSpeakerTurns: (body) => post("/api/v1/sessions/speakers", body, z.object({
|
|
682
|
+
accepted: z.number(),
|
|
683
|
+
dropped: z.number()
|
|
684
|
+
})),
|
|
665
685
|
addVoiceSegment: async (body) => (await post("/api/v1/sessions/segments", body, segmentResponse)).segment,
|
|
666
686
|
collection: async (body) => (await post("/api/v1/sessions/collection", body, summaryResponse)).session,
|
|
667
687
|
events: (body) => post("/api/v1/sessions/events", body, z.object({
|
|
@@ -669,6 +689,7 @@ function createRecordingEndpoints({ post }) {
|
|
|
669
689
|
dropped: z.number(),
|
|
670
690
|
limitReached: z.boolean()
|
|
671
691
|
})),
|
|
692
|
+
speakerToken: () => post("/api/v1/sessions/speakers/token", {}, speakerTokenSchema),
|
|
672
693
|
start: async (body) => (await post("/api/v1/sessions/start", body, summaryResponse)).session,
|
|
673
694
|
uploadUrl: async (body) => (await post("/api/v1/sessions/upload-url", body, z.object({ uploadUrl: z.string() }))).uploadUrl
|
|
674
695
|
};
|
|
@@ -1125,6 +1146,7 @@ function operationBytes(payload) {
|
|
|
1125
1146
|
case "voice": return payload.audio?.size ?? SMALL_OPERATION_BYTES;
|
|
1126
1147
|
case "evidence": return (payload.image?.blob?.size ?? 0) + JSON.stringify(payload.evidence).length + SMALL_OPERATION_BYTES;
|
|
1127
1148
|
case "events": return JSON.stringify(payload.events).length;
|
|
1149
|
+
case "speakers": return JSON.stringify(payload.turns).length;
|
|
1128
1150
|
default: return SMALL_OPERATION_BYTES;
|
|
1129
1151
|
}
|
|
1130
1152
|
}
|
|
@@ -1228,6 +1250,13 @@ async function sendSessionOperation(context, payload) {
|
|
|
1228
1250
|
kind: "limit",
|
|
1229
1251
|
limit: "events"
|
|
1230
1252
|
} : { kind: "done" };
|
|
1253
|
+
case "speakers":
|
|
1254
|
+
await api.addSpeakerTurns({
|
|
1255
|
+
clientKey: payload.clientKey,
|
|
1256
|
+
sessionId,
|
|
1257
|
+
turns: payload.turns
|
|
1258
|
+
});
|
|
1259
|
+
return { kind: "done" };
|
|
1231
1260
|
case "voice":
|
|
1232
1261
|
await sendVoice(context, payload);
|
|
1233
1262
|
return { kind: "done" };
|
|
@@ -1328,7 +1357,7 @@ var SessionOutbox = class {
|
|
|
1328
1357
|
}
|
|
1329
1358
|
async enqueue(sessionKey, payload) {
|
|
1330
1359
|
const bytes = operationBytes(payload);
|
|
1331
|
-
if (this.snapshot.pendingBytes + bytes > this.options.maxLocalBufferBytes) this.failStorage(BUFFER_FULL);
|
|
1360
|
+
if (this.snapshot.pendingBytes + bytes > this.options.limits.maxLocalBufferBytes) this.failStorage(BUFFER_FULL);
|
|
1332
1361
|
try {
|
|
1333
1362
|
await this.options.store.addOperation({
|
|
1334
1363
|
accountKey: this.options.accountKey,
|
|
@@ -1401,6 +1430,7 @@ var SessionOutbox = class {
|
|
|
1401
1430
|
pendingOperations: remaining.length
|
|
1402
1431
|
});
|
|
1403
1432
|
await this.forgetFinishedDrafts(remaining);
|
|
1433
|
+
await this.forgetOldVoices();
|
|
1404
1434
|
this.afterPass(pass);
|
|
1405
1435
|
}
|
|
1406
1436
|
async dropAbandonedUploads(drafts) {
|
|
@@ -1486,6 +1516,12 @@ var SessionOutbox = class {
|
|
|
1486
1516
|
await Promise.all(finished.map((draft) => store.deleteDraft(draft.sessionKey)));
|
|
1487
1517
|
if (finished.length > 0) this.options.onDraftsChanged();
|
|
1488
1518
|
}
|
|
1519
|
+
async forgetOldVoices() {
|
|
1520
|
+
const { accountKey, limits, store } = this.options;
|
|
1521
|
+
const keptSince = Date.now() - limits.audioRetentionMs;
|
|
1522
|
+
const expired = (await store.readDrafts(accountKey)).filter((draft) => draft.lastActiveAt < keptSince && Object.keys(draft.speakerReferences ?? {}).length > 0);
|
|
1523
|
+
await Promise.all(expired.map((draft) => store.patchDraft(draft.sessionKey, { speakerReferences: {} })));
|
|
1524
|
+
}
|
|
1489
1525
|
};
|
|
1490
1526
|
function createOutbox(options) {
|
|
1491
1527
|
return new SessionOutbox(options);
|
|
@@ -1502,6 +1538,10 @@ const IDLE_SYNC = {
|
|
|
1502
1538
|
pendingBytes: 0,
|
|
1503
1539
|
pendingOperations: 0
|
|
1504
1540
|
};
|
|
1541
|
+
const UNBOUNDED_LIMITS = {
|
|
1542
|
+
audioRetentionMs: Number.POSITIVE_INFINITY,
|
|
1543
|
+
maxLocalBufferBytes: Number.POSITIVE_INFINITY
|
|
1544
|
+
};
|
|
1505
1545
|
function visibleDraft(stored, isLive) {
|
|
1506
1546
|
if (stored.state === "cancelled") return [];
|
|
1507
1547
|
const leftOpen = stored.state === "recording" || stored.state === "paused";
|
|
@@ -1553,7 +1593,7 @@ var LocalSessionsRegistry = class {
|
|
|
1553
1593
|
if (!stored || this.live.has(sessionKey) || isClosed) return;
|
|
1554
1594
|
const { outbox } = this.ensureOutbox({
|
|
1555
1595
|
...options,
|
|
1556
|
-
|
|
1596
|
+
limits: null
|
|
1557
1597
|
});
|
|
1558
1598
|
const atMs = Math.max(0, stored.lastActiveAt - stored.startedAt);
|
|
1559
1599
|
await outbox.enqueue(sessionKey, {
|
|
@@ -1615,11 +1655,11 @@ var LocalSessionsRegistry = class {
|
|
|
1615
1655
|
const existing = this.accounts.get(accountKey);
|
|
1616
1656
|
if (existing && !existing.stopped && existing.api === api && existing.outbox.getSnapshot().blockedBy !== "auth") return existing;
|
|
1617
1657
|
existing?.outbox.stop();
|
|
1618
|
-
const
|
|
1658
|
+
const limits = options.limits ?? existing?.limits ?? UNBOUNDED_LIMITS;
|
|
1619
1659
|
const outbox = createOutbox({
|
|
1620
1660
|
accountKey,
|
|
1621
1661
|
api,
|
|
1622
|
-
|
|
1662
|
+
limits,
|
|
1623
1663
|
onDraftsChanged: () => this.draftsChanged(accountKey),
|
|
1624
1664
|
store: this.store
|
|
1625
1665
|
});
|
|
@@ -1629,7 +1669,7 @@ var LocalSessionsRegistry = class {
|
|
|
1629
1669
|
});
|
|
1630
1670
|
const connected = {
|
|
1631
1671
|
api,
|
|
1632
|
-
|
|
1672
|
+
limits,
|
|
1633
1673
|
outbox,
|
|
1634
1674
|
stopped: false
|
|
1635
1675
|
};
|
|
@@ -1826,7 +1866,7 @@ async function openMicrophone(options) {
|
|
|
1826
1866
|
//#endregion
|
|
1827
1867
|
//#region src/sessions/capture/audio/speech-chunker.ts
|
|
1828
1868
|
const SPEECH_SAMPLE_RATE = 16e3;
|
|
1829
|
-
const SAMPLES_PER_MS = SPEECH_SAMPLE_RATE / 1e3;
|
|
1869
|
+
const SAMPLES_PER_MS$3 = SPEECH_SAMPLE_RATE / 1e3;
|
|
1830
1870
|
const WINDOW_MS = 30;
|
|
1831
1871
|
const WINDOW_SAMPLES = 480;
|
|
1832
1872
|
const POST_ROLL_WINDOWS = 300 / WINDOW_MS;
|
|
@@ -1897,9 +1937,9 @@ var UtteranceChunker = class {
|
|
|
1897
1937
|
this.preRoll = [];
|
|
1898
1938
|
if (open.speechWindows * WINDOW_MS < MIN_SPEECH_MS) return;
|
|
1899
1939
|
const samples = concatenate(open.windows.slice(0, keptWindows));
|
|
1900
|
-
const startMs = this.options.startMs + Math.round(open.startSample / SAMPLES_PER_MS);
|
|
1940
|
+
const startMs = this.options.startMs + Math.round(open.startSample / SAMPLES_PER_MS$3);
|
|
1901
1941
|
this.options.onChunk({
|
|
1902
|
-
endMs: startMs + Math.round(samples.length / SAMPLES_PER_MS),
|
|
1942
|
+
endMs: startMs + Math.round(samples.length / SAMPLES_PER_MS$3),
|
|
1903
1943
|
samples,
|
|
1904
1944
|
startMs
|
|
1905
1945
|
});
|
|
@@ -1947,6 +1987,18 @@ const MAX_NEGATIVE_SAMPLE = 32768;
|
|
|
1947
1987
|
function writeText(view, offset, text) {
|
|
1948
1988
|
for (let index = 0; index < text.length; index += 1) view.setUint8(offset + index, text.charCodeAt(index));
|
|
1949
1989
|
}
|
|
1990
|
+
function writePcm16(view, offset, samples) {
|
|
1991
|
+
samples.forEach((sample, index) => {
|
|
1992
|
+
const clamped = Math.max(-1, Math.min(1, sample));
|
|
1993
|
+
const scale = clamped < 0 ? MAX_NEGATIVE_SAMPLE : MAX_POSITIVE_SAMPLE;
|
|
1994
|
+
view.setInt16(offset + index * BYTES_PER_SAMPLE, Math.round(clamped * scale), true);
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
function encodePcm16(samples) {
|
|
1998
|
+
const buffer = /* @__PURE__ */ new ArrayBuffer(samples.length * BYTES_PER_SAMPLE);
|
|
1999
|
+
writePcm16(new DataView(buffer), 0, samples);
|
|
2000
|
+
return buffer;
|
|
2001
|
+
}
|
|
1950
2002
|
/** 16-bit mono PCM WAV, the format the transcription endpoint expects. */
|
|
1951
2003
|
function encodeWav(samples, sampleRate) {
|
|
1952
2004
|
const dataBytes = samples.length * BYTES_PER_SAMPLE;
|
|
@@ -1964,11 +2016,7 @@ function encodeWav(samples, sampleRate) {
|
|
|
1964
2016
|
view.setUint16(34, 16, true);
|
|
1965
2017
|
writeText(view, 36, "data");
|
|
1966
2018
|
view.setUint32(40, dataBytes, true);
|
|
1967
|
-
|
|
1968
|
-
const clamped = Math.max(-1, Math.min(1, sample));
|
|
1969
|
-
const scale = clamped < 0 ? MAX_NEGATIVE_SAMPLE : MAX_POSITIVE_SAMPLE;
|
|
1970
|
-
view.setInt16(WAV_HEADER_BYTES + index * BYTES_PER_SAMPLE, Math.round(clamped * scale), true);
|
|
1971
|
-
});
|
|
2019
|
+
writePcm16(view, WAV_HEADER_BYTES, samples);
|
|
1972
2020
|
return buffer;
|
|
1973
2021
|
}
|
|
1974
2022
|
function maxWavDurationMs(maxBytes) {
|
|
@@ -2276,6 +2324,7 @@ async function openVoiceCapture() {
|
|
|
2276
2324
|
const samples = downsample(frame);
|
|
2277
2325
|
options.onLevel(meterLevel(samples));
|
|
2278
2326
|
speech.push(samples);
|
|
2327
|
+
options.onSamples?.(samples);
|
|
2279
2328
|
};
|
|
2280
2329
|
},
|
|
2281
2330
|
stop: () => {
|
|
@@ -2309,7 +2358,8 @@ function startCollectionRun$1(options) {
|
|
|
2309
2358
|
maxSegmentMs: options.maxSegmentMs,
|
|
2310
2359
|
onChunk: options.onChunk,
|
|
2311
2360
|
onLevel: options.onLevel,
|
|
2312
|
-
|
|
2361
|
+
onSamples: options.onSamples,
|
|
2362
|
+
startMs: options.startMs
|
|
2313
2363
|
});
|
|
2314
2364
|
let interactions = options.collectEvents ? startInteractionRecorder(options) : null;
|
|
2315
2365
|
const navigation = startNavigationWatch(options);
|
|
@@ -2506,6 +2556,542 @@ function createEvidenceScheduler(options) {
|
|
|
2506
2556
|
};
|
|
2507
2557
|
}
|
|
2508
2558
|
//#endregion
|
|
2559
|
+
//#region src/sessions/capture/speakers/stream/base64.ts
|
|
2560
|
+
const BYTES_PER_CONVERSION = 32768;
|
|
2561
|
+
function bytesToBase64(buffer) {
|
|
2562
|
+
const bytes = new Uint8Array(buffer);
|
|
2563
|
+
let binary = "";
|
|
2564
|
+
for (let offset = 0; offset < bytes.length; offset += BYTES_PER_CONVERSION) binary += String.fromCharCode(...bytes.subarray(offset, offset + BYTES_PER_CONVERSION));
|
|
2565
|
+
return btoa(binary);
|
|
2566
|
+
}
|
|
2567
|
+
//#endregion
|
|
2568
|
+
//#region src/sessions/capture/speakers/reference-clips.ts
|
|
2569
|
+
const SAMPLES_PER_MS$2 = SPEECH_SAMPLE_RATE / 1e3;
|
|
2570
|
+
const MIN_PIECE_MS = 1e3;
|
|
2571
|
+
const MIN_REFERENCE_SAMPLES = 3e3 * SAMPLES_PER_MS$2;
|
|
2572
|
+
const MAX_REFERENCE_SAMPLES = 6e3 * SAMPLES_PER_MS$2;
|
|
2573
|
+
const PIECE_END_MARGIN_MS = 250;
|
|
2574
|
+
const MAX_REFERENCES = 8;
|
|
2575
|
+
function referencePiece(chunk, turn) {
|
|
2576
|
+
const fromMs = Math.max(turn.startMs, chunk.startMs);
|
|
2577
|
+
const toMs = Math.min(turn.endMs - PIECE_END_MARGIN_MS, chunk.endMs);
|
|
2578
|
+
if (toMs - fromMs < MIN_PIECE_MS) return null;
|
|
2579
|
+
return chunk.samples.slice(Math.round((fromMs - chunk.startMs) * SAMPLES_PER_MS$2), Math.round((toMs - chunk.startMs) * SAMPLES_PER_MS$2));
|
|
2580
|
+
}
|
|
2581
|
+
function joined(voice) {
|
|
2582
|
+
const samples = new Float32Array(voice.samples);
|
|
2583
|
+
let offset = 0;
|
|
2584
|
+
for (const piece of voice.pieces) {
|
|
2585
|
+
samples.set(piece, offset);
|
|
2586
|
+
offset += piece.length;
|
|
2587
|
+
}
|
|
2588
|
+
return samples;
|
|
2589
|
+
}
|
|
2590
|
+
function becameUsable(before, after) {
|
|
2591
|
+
return before < MIN_REFERENCE_SAMPLES && after >= MIN_REFERENCE_SAMPLES || before < MAX_REFERENCE_SAMPLES && after >= MAX_REFERENCE_SAMPLES;
|
|
2592
|
+
}
|
|
2593
|
+
var ReferenceClips = class {
|
|
2594
|
+
saved;
|
|
2595
|
+
voices = /* @__PURE__ */ new Map();
|
|
2596
|
+
constructor(saved) {
|
|
2597
|
+
this.saved = saved;
|
|
2598
|
+
}
|
|
2599
|
+
keepFrom(chunk, turns) {
|
|
2600
|
+
let changed = false;
|
|
2601
|
+
for (const turn of turns) {
|
|
2602
|
+
const voice = this.voices.get(turn.speaker) ?? {
|
|
2603
|
+
pieces: [],
|
|
2604
|
+
samples: 0
|
|
2605
|
+
};
|
|
2606
|
+
const room = MAX_REFERENCE_SAMPLES - voice.samples;
|
|
2607
|
+
const isSaved = turn.speaker in this.saved;
|
|
2608
|
+
const piece = room > 0 && !isSaved ? referencePiece(chunk, turn) : null;
|
|
2609
|
+
if (piece) {
|
|
2610
|
+
const kept = piece.subarray(0, room);
|
|
2611
|
+
const before = voice.samples;
|
|
2612
|
+
voice.pieces.push(kept);
|
|
2613
|
+
voice.samples += kept.length;
|
|
2614
|
+
this.voices.set(turn.speaker, voice);
|
|
2615
|
+
changed ||= becameUsable(before, voice.samples);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
return changed;
|
|
2619
|
+
}
|
|
2620
|
+
references() {
|
|
2621
|
+
const references = { ...this.saved };
|
|
2622
|
+
for (const [speaker, voice] of this.voices) if (voice.samples >= MIN_REFERENCE_SAMPLES) references[speaker] = bytesToBase64(encodeWav(joined(voice), SPEECH_SAMPLE_RATE));
|
|
2623
|
+
return Object.fromEntries(Object.entries(references).slice(0, MAX_REFERENCES));
|
|
2624
|
+
}
|
|
2625
|
+
};
|
|
2626
|
+
var SpeakerLabels = class {
|
|
2627
|
+
handedOut;
|
|
2628
|
+
onHandOut;
|
|
2629
|
+
constructor(options) {
|
|
2630
|
+
this.handedOut = options.handedOut;
|
|
2631
|
+
this.onHandOut = options.onHandOut;
|
|
2632
|
+
}
|
|
2633
|
+
handOut() {
|
|
2634
|
+
if (this.handedOut >= 10) return null;
|
|
2635
|
+
this.handedOut += 1;
|
|
2636
|
+
this.onHandOut(this.handedOut);
|
|
2637
|
+
return `S${this.handedOut}`;
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
function createSpeakerIdMap(labels, referenceNames) {
|
|
2641
|
+
const known = new Map(referenceNames.map((name) => [name, name]));
|
|
2642
|
+
return (speakerId) => {
|
|
2643
|
+
const label = known.get(speakerId);
|
|
2644
|
+
if (label !== void 0) return label;
|
|
2645
|
+
const handedOut = labels.handOut();
|
|
2646
|
+
known.set(speakerId, handedOut);
|
|
2647
|
+
return handedOut;
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
const EVENT_ARRIVAL_LAG_MS = 250;
|
|
2651
|
+
const SAMPLES_PER_MS$1 = SPEECH_SAMPLE_RATE / 1e3;
|
|
2652
|
+
function speakerEventMs(stream) {
|
|
2653
|
+
const sentMs = stream.samplesSent / SAMPLES_PER_MS$1;
|
|
2654
|
+
const streamStartMs = stream.streamStartSample / SAMPLES_PER_MS$1;
|
|
2655
|
+
const spokenMs = sentMs - 480 - EVENT_ARRIVAL_LAG_MS;
|
|
2656
|
+
return Math.round(stream.runStartMs + Math.max(streamStartMs, spokenMs));
|
|
2657
|
+
}
|
|
2658
|
+
var SpeakerTurns = class {
|
|
2659
|
+
closed = [];
|
|
2660
|
+
open = null;
|
|
2661
|
+
undrained = [];
|
|
2662
|
+
add(speaker, atMs) {
|
|
2663
|
+
const { open } = this;
|
|
2664
|
+
if (open !== null && open.speaker === speaker) {
|
|
2665
|
+
open.undrainedFromMs ??= open.endMs;
|
|
2666
|
+
open.endMs = atMs;
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
this.closeOpenTurn();
|
|
2670
|
+
this.open = speaker === null ? null : {
|
|
2671
|
+
endMs: atMs,
|
|
2672
|
+
speaker,
|
|
2673
|
+
startMs: atMs,
|
|
2674
|
+
undrainedFromMs: atMs
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
drain() {
|
|
2678
|
+
const drained = this.undrained;
|
|
2679
|
+
this.undrained = [];
|
|
2680
|
+
const { open } = this;
|
|
2681
|
+
if (open && open.undrainedFromMs !== null) {
|
|
2682
|
+
drained.push(undrainedPart(open, open.undrainedFromMs));
|
|
2683
|
+
open.undrainedFromMs = null;
|
|
2684
|
+
}
|
|
2685
|
+
return drained;
|
|
2686
|
+
}
|
|
2687
|
+
takeTurnsOverlapping(range) {
|
|
2688
|
+
const { open } = this;
|
|
2689
|
+
const candidates = open ? [...this.closed, wholeTurn(open)] : this.closed;
|
|
2690
|
+
this.closed = this.closed.filter((turn) => turn.endMs > range.endMs);
|
|
2691
|
+
return candidates.filter((turn) => turn.startMs < range.endMs && turn.endMs > range.startMs);
|
|
2692
|
+
}
|
|
2693
|
+
closeOpenTurn() {
|
|
2694
|
+
const { open } = this;
|
|
2695
|
+
if (open === null) return;
|
|
2696
|
+
if (open.undrainedFromMs !== null) this.undrained.push(undrainedPart(open, open.undrainedFromMs));
|
|
2697
|
+
this.closed.push(wholeTurn(open));
|
|
2698
|
+
this.open = null;
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
2701
|
+
function undrainedPart(turn, fromMs) {
|
|
2702
|
+
return {
|
|
2703
|
+
endMs: turn.endMs,
|
|
2704
|
+
speaker: turn.speaker,
|
|
2705
|
+
startMs: fromMs
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
function wholeTurn(turn) {
|
|
2709
|
+
return {
|
|
2710
|
+
endMs: turn.endMs,
|
|
2711
|
+
speaker: turn.speaker,
|
|
2712
|
+
startMs: turn.startMs
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
//#endregion
|
|
2716
|
+
//#region src/sessions/capture/speakers/stream/realtime-connection.ts
|
|
2717
|
+
const serverMessageSchema = z.discriminatedUnion("type", [
|
|
2718
|
+
z.object({ type: z.literal("session.created") }),
|
|
2719
|
+
z.object({ type: z.literal("session.updated") }),
|
|
2720
|
+
z.object({
|
|
2721
|
+
speaker_id: z.string().nullish(),
|
|
2722
|
+
type: z.literal("transcription.segment")
|
|
2723
|
+
}),
|
|
2724
|
+
z.object({ type: z.literal("transcription.done") }),
|
|
2725
|
+
z.object({
|
|
2726
|
+
error: z.object({ message: z.unknown() }),
|
|
2727
|
+
type: z.literal("error")
|
|
2728
|
+
})
|
|
2729
|
+
]);
|
|
2730
|
+
const HANDLED_MESSAGE_TYPES = new Set(serverMessageSchema.options.map((option) => option.shape.type.value));
|
|
2731
|
+
const envelopeSchema = z.object({ type: z.string() });
|
|
2732
|
+
function readServerMessage(data) {
|
|
2733
|
+
if (typeof data !== "string") throw new Error("Mistral sent a binary message.");
|
|
2734
|
+
const message = JSON.parse(data);
|
|
2735
|
+
const { type } = envelopeSchema.parse(message);
|
|
2736
|
+
return HANDLED_MESSAGE_TYPES.has(type) ? serverMessageSchema.parse(message) : null;
|
|
2737
|
+
}
|
|
2738
|
+
function sessionUpdate(references) {
|
|
2739
|
+
const hasReferences = Object.keys(references).length > 0;
|
|
2740
|
+
return {
|
|
2741
|
+
session: {
|
|
2742
|
+
audio_format: {
|
|
2743
|
+
encoding: "pcm_s16le",
|
|
2744
|
+
sample_rate: SPEECH_SAMPLE_RATE
|
|
2745
|
+
},
|
|
2746
|
+
diarize: true,
|
|
2747
|
+
...hasReferences ? { speaker_references: references } : {},
|
|
2748
|
+
target_streaming_delay_ms: 480
|
|
2749
|
+
},
|
|
2750
|
+
type: "session.update"
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
function openRealtimeConnection(options) {
|
|
2754
|
+
const socket = new WebSocket(options.token.url, ["realtime", options.token.token]);
|
|
2755
|
+
let phase = "connecting";
|
|
2756
|
+
let markClosed = () => void 0;
|
|
2757
|
+
const closed = new Promise((resolve) => {
|
|
2758
|
+
markClosed = resolve;
|
|
2759
|
+
});
|
|
2760
|
+
const shutDown = () => {
|
|
2761
|
+
phase = "closed";
|
|
2762
|
+
socket.close();
|
|
2763
|
+
markClosed();
|
|
2764
|
+
};
|
|
2765
|
+
const fail = (failure) => {
|
|
2766
|
+
if (phase !== "closed") {
|
|
2767
|
+
shutDown();
|
|
2768
|
+
options.onFailure(failure);
|
|
2769
|
+
}
|
|
2770
|
+
};
|
|
2771
|
+
const send = (message) => socket.send(JSON.stringify(message));
|
|
2772
|
+
const handle = (message) => {
|
|
2773
|
+
switch (message.type) {
|
|
2774
|
+
case "session.created":
|
|
2775
|
+
phase = "configuring";
|
|
2776
|
+
send(sessionUpdate(options.references));
|
|
2777
|
+
return;
|
|
2778
|
+
case "session.updated":
|
|
2779
|
+
phase = "streaming";
|
|
2780
|
+
options.onReady();
|
|
2781
|
+
return;
|
|
2782
|
+
case "transcription.segment":
|
|
2783
|
+
if (message.speaker_id) options.onSpeaker(message.speaker_id);
|
|
2784
|
+
return;
|
|
2785
|
+
case "transcription.done":
|
|
2786
|
+
if (phase === "ending") {
|
|
2787
|
+
shutDown();
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
fail({
|
|
2791
|
+
kind: "closed",
|
|
2792
|
+
message: "Mistral ended the speaker stream."
|
|
2793
|
+
});
|
|
2794
|
+
return;
|
|
2795
|
+
default: {
|
|
2796
|
+
const reason = message.error.message;
|
|
2797
|
+
fail({
|
|
2798
|
+
kind: "error",
|
|
2799
|
+
message: typeof reason === "string" ? reason : JSON.stringify(reason)
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
socket.addEventListener("message", (event) => {
|
|
2805
|
+
let message;
|
|
2806
|
+
try {
|
|
2807
|
+
message = readServerMessage(event.data);
|
|
2808
|
+
} catch (error) {
|
|
2809
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2810
|
+
fail({
|
|
2811
|
+
kind: "error",
|
|
2812
|
+
message: `Unreadable speaker event: ${reason}`
|
|
2813
|
+
});
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
if (message && phase !== "closed") handle(message);
|
|
2817
|
+
});
|
|
2818
|
+
const lose = (message) => {
|
|
2819
|
+
if (phase === "ending") {
|
|
2820
|
+
shutDown();
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
fail({
|
|
2824
|
+
kind: "closed",
|
|
2825
|
+
message
|
|
2826
|
+
});
|
|
2827
|
+
};
|
|
2828
|
+
socket.addEventListener("error", () => lose("The speaker stream failed, or the page's security policy blocked it."));
|
|
2829
|
+
socket.addEventListener("close", (event) => lose(`The speaker stream closed (${event.code}).`));
|
|
2830
|
+
return {
|
|
2831
|
+
close: shutDown,
|
|
2832
|
+
end: () => {
|
|
2833
|
+
if (phase === "streaming") {
|
|
2834
|
+
phase = "ending";
|
|
2835
|
+
send({ type: "input_audio.end" });
|
|
2836
|
+
} else if (phase !== "ending") shutDown();
|
|
2837
|
+
return closed;
|
|
2838
|
+
},
|
|
2839
|
+
sendAudio: (samples) => {
|
|
2840
|
+
if (phase !== "streaming") throw new Error("The speaker stream is not ready for audio.");
|
|
2841
|
+
send({
|
|
2842
|
+
audio: bytesToBase64(encodePcm16(samples)),
|
|
2843
|
+
type: "input_audio.append"
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
//#endregion
|
|
2849
|
+
//#region src/sessions/capture/speakers/stream/sample-queue.ts
|
|
2850
|
+
var SampleQueue = class {
|
|
2851
|
+
maxSamples;
|
|
2852
|
+
parts = [];
|
|
2853
|
+
queuedSamples = 0;
|
|
2854
|
+
constructor(maxSamples) {
|
|
2855
|
+
this.maxSamples = maxSamples;
|
|
2856
|
+
}
|
|
2857
|
+
get size() {
|
|
2858
|
+
return this.queuedSamples;
|
|
2859
|
+
}
|
|
2860
|
+
clear() {
|
|
2861
|
+
this.parts = [];
|
|
2862
|
+
this.queuedSamples = 0;
|
|
2863
|
+
}
|
|
2864
|
+
push(samples) {
|
|
2865
|
+
if (this.queuedSamples + samples.length > this.maxSamples) return false;
|
|
2866
|
+
this.parts.push(samples);
|
|
2867
|
+
this.queuedSamples += samples.length;
|
|
2868
|
+
return true;
|
|
2869
|
+
}
|
|
2870
|
+
take(maxSamples) {
|
|
2871
|
+
const taken = new Float32Array(Math.min(maxSamples, this.queuedSamples));
|
|
2872
|
+
let filled = 0;
|
|
2873
|
+
while (filled < taken.length) {
|
|
2874
|
+
const part = this.parts[0];
|
|
2875
|
+
if (!part) break;
|
|
2876
|
+
const used = Math.min(part.length, taken.length - filled);
|
|
2877
|
+
taken.set(part.subarray(0, used), filled);
|
|
2878
|
+
filled += used;
|
|
2879
|
+
if (used === part.length) this.parts.shift();
|
|
2880
|
+
else this.parts[0] = part.subarray(used);
|
|
2881
|
+
}
|
|
2882
|
+
this.queuedSamples -= taken.length;
|
|
2883
|
+
return taken;
|
|
2884
|
+
}
|
|
2885
|
+
};
|
|
2886
|
+
//#endregion
|
|
2887
|
+
//#region src/sessions/capture/speakers/speaker-tracker.ts
|
|
2888
|
+
const SAMPLES_PER_MS = SPEECH_SAMPLE_RATE / 1e3;
|
|
2889
|
+
const PACE_INTERVAL_MS = 100;
|
|
2890
|
+
const MAX_SAMPLES_PER_PACE = 2e3;
|
|
2891
|
+
const MAX_QUEUED_SAMPLES = 3e4 * SAMPLES_PER_MS;
|
|
2892
|
+
const FINISH_TIMEOUT_MS = 3e3;
|
|
2893
|
+
const RECONNECT_DELAYS_MS = [
|
|
2894
|
+
1e3,
|
|
2895
|
+
2e3,
|
|
2896
|
+
4e3
|
|
2897
|
+
];
|
|
2898
|
+
var SpeakerTracker = class {
|
|
2899
|
+
connection = null;
|
|
2900
|
+
failedAttempts = 0;
|
|
2901
|
+
finished;
|
|
2902
|
+
finishTimer;
|
|
2903
|
+
isStreaming = false;
|
|
2904
|
+
markFinished = () => void 0;
|
|
2905
|
+
options;
|
|
2906
|
+
pacer;
|
|
2907
|
+
queue = new SampleQueue(MAX_QUEUED_SAMPLES);
|
|
2908
|
+
reconnectTimer;
|
|
2909
|
+
samplesSent = 0;
|
|
2910
|
+
state = "running";
|
|
2911
|
+
streamStartSample = 0;
|
|
2912
|
+
turns = new SpeakerTurns();
|
|
2913
|
+
constructor(options) {
|
|
2914
|
+
this.options = options;
|
|
2915
|
+
this.finished = new Promise((resolve) => {
|
|
2916
|
+
this.markFinished = resolve;
|
|
2917
|
+
});
|
|
2918
|
+
this.pacer = window.setInterval(this.pace, PACE_INTERVAL_MS);
|
|
2919
|
+
this.connect();
|
|
2920
|
+
}
|
|
2921
|
+
close() {
|
|
2922
|
+
this.stop();
|
|
2923
|
+
}
|
|
2924
|
+
drain() {
|
|
2925
|
+
return this.turns.drain();
|
|
2926
|
+
}
|
|
2927
|
+
finish() {
|
|
2928
|
+
if (this.state === "running") {
|
|
2929
|
+
this.state = "finishing";
|
|
2930
|
+
this.finishTimer = window.setTimeout(this.stop, FINISH_TIMEOUT_MS);
|
|
2931
|
+
if (!this.isStreaming) this.stop();
|
|
2932
|
+
}
|
|
2933
|
+
return this.finished;
|
|
2934
|
+
}
|
|
2935
|
+
heardUntilMs() {
|
|
2936
|
+
return speakerEventMs({
|
|
2937
|
+
runStartMs: this.options.startMs,
|
|
2938
|
+
samplesSent: this.samplesSent,
|
|
2939
|
+
streamStartSample: this.streamStartSample
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
push(samples) {
|
|
2943
|
+
if (this.state === "running" && !this.queue.push(samples)) this.giveUp();
|
|
2944
|
+
}
|
|
2945
|
+
takeTurnsOverlapping(range) {
|
|
2946
|
+
return this.turns.takeTurnsOverlapping(range);
|
|
2947
|
+
}
|
|
2948
|
+
connect = () => {
|
|
2949
|
+
this.options.requestToken().then((token) => token ? this.open(token) : this.giveUp()).catch(() => this.retry());
|
|
2950
|
+
};
|
|
2951
|
+
giveUp() {
|
|
2952
|
+
if (this.state !== "stopped") {
|
|
2953
|
+
this.stop();
|
|
2954
|
+
this.options.onUnavailable();
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
lose(failure) {
|
|
2958
|
+
this.connection = null;
|
|
2959
|
+
this.isStreaming = false;
|
|
2960
|
+
if (this.state !== "running") this.stop();
|
|
2961
|
+
else if (failure.kind === "error") this.giveUp();
|
|
2962
|
+
else this.retry();
|
|
2963
|
+
}
|
|
2964
|
+
open = (token) => {
|
|
2965
|
+
if (this.state !== "running") return;
|
|
2966
|
+
const references = this.options.references();
|
|
2967
|
+
const labelOf = createSpeakerIdMap(this.options.labels, Object.keys(references));
|
|
2968
|
+
this.connection = openRealtimeConnection({
|
|
2969
|
+
onFailure: (failure) => this.lose(failure),
|
|
2970
|
+
onReady: () => {
|
|
2971
|
+
this.failedAttempts = 0;
|
|
2972
|
+
this.streamStartSample = this.samplesSent;
|
|
2973
|
+
this.isStreaming = true;
|
|
2974
|
+
},
|
|
2975
|
+
onSpeaker: (speakerId) => this.turns.add(labelOf(speakerId), this.heardUntilMs()),
|
|
2976
|
+
references,
|
|
2977
|
+
token
|
|
2978
|
+
});
|
|
2979
|
+
};
|
|
2980
|
+
pace = () => {
|
|
2981
|
+
const { connection } = this;
|
|
2982
|
+
if (!(connection && this.isStreaming)) return;
|
|
2983
|
+
const samples = this.queue.take(MAX_SAMPLES_PER_PACE);
|
|
2984
|
+
if (samples.length > 0) {
|
|
2985
|
+
connection.sendAudio(samples);
|
|
2986
|
+
this.samplesSent += samples.length;
|
|
2987
|
+
}
|
|
2988
|
+
if (this.state === "finishing" && this.queue.size === 0) {
|
|
2989
|
+
this.isStreaming = false;
|
|
2990
|
+
connection.end().then(this.stop, this.stop);
|
|
2991
|
+
}
|
|
2992
|
+
};
|
|
2993
|
+
retry() {
|
|
2994
|
+
if (this.state !== "running") return;
|
|
2995
|
+
const delayMs = RECONNECT_DELAYS_MS[this.failedAttempts];
|
|
2996
|
+
if (delayMs === void 0) {
|
|
2997
|
+
this.giveUp();
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
this.failedAttempts += 1;
|
|
3001
|
+
this.reconnectTimer = window.setTimeout(this.connect, delayMs);
|
|
3002
|
+
}
|
|
3003
|
+
stop = () => {
|
|
3004
|
+
if (this.state === "stopped") return;
|
|
3005
|
+
this.state = "stopped";
|
|
3006
|
+
window.clearInterval(this.pacer);
|
|
3007
|
+
window.clearTimeout(this.reconnectTimer);
|
|
3008
|
+
window.clearTimeout(this.finishTimer);
|
|
3009
|
+
this.isStreaming = false;
|
|
3010
|
+
this.connection?.close();
|
|
3011
|
+
this.connection = null;
|
|
3012
|
+
this.queue.clear();
|
|
3013
|
+
this.markFinished();
|
|
3014
|
+
};
|
|
3015
|
+
};
|
|
3016
|
+
//#endregion
|
|
3017
|
+
//#region src/sessions/capture/speakers/speaker-detection.ts
|
|
3018
|
+
const REFERENCE_SETTLE_MS = 1e3;
|
|
3019
|
+
const MAX_UNSETTLED_CHUNKS = 3;
|
|
3020
|
+
var SpeakerDetection = class {
|
|
3021
|
+
clips;
|
|
3022
|
+
labels;
|
|
3023
|
+
options;
|
|
3024
|
+
run = null;
|
|
3025
|
+
unsettled = [];
|
|
3026
|
+
previousRunSettled = Promise.resolve();
|
|
3027
|
+
constructor(options) {
|
|
3028
|
+
this.options = options;
|
|
3029
|
+
this.clips = new ReferenceClips(options.references);
|
|
3030
|
+
this.labels = new SpeakerLabels({
|
|
3031
|
+
handedOut: options.labelCount,
|
|
3032
|
+
onHandOut: options.onLabelCount
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
closeRun() {
|
|
3036
|
+
const { run } = this;
|
|
3037
|
+
this.run = null;
|
|
3038
|
+
if (!run) return [];
|
|
3039
|
+
run.close();
|
|
3040
|
+
this.keepVoices(run, this.unsettled.splice(0));
|
|
3041
|
+
return run.drain();
|
|
3042
|
+
}
|
|
3043
|
+
drainTurns() {
|
|
3044
|
+
return this.run?.drain() ?? [];
|
|
3045
|
+
}
|
|
3046
|
+
async finishRun() {
|
|
3047
|
+
const { run } = this;
|
|
3048
|
+
this.run = null;
|
|
3049
|
+
if (!run) return [];
|
|
3050
|
+
const unsettled = this.unsettled.splice(0);
|
|
3051
|
+
const settled = run.finish().then(() => this.keepVoices(run, unsettled));
|
|
3052
|
+
this.previousRunSettled = settled;
|
|
3053
|
+
await settled;
|
|
3054
|
+
return run.drain();
|
|
3055
|
+
}
|
|
3056
|
+
keepReferences(chunk) {
|
|
3057
|
+
if (this.run) {
|
|
3058
|
+
this.unsettled.push(chunk);
|
|
3059
|
+
this.settleReferences();
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
settleReferences() {
|
|
3063
|
+
const { run } = this;
|
|
3064
|
+
if (!run) return;
|
|
3065
|
+
const heardMs = run.heardUntilMs();
|
|
3066
|
+
const overflow = this.unsettled.length - MAX_UNSETTLED_CHUNKS;
|
|
3067
|
+
const firstUnsettled = this.unsettled.findIndex((chunk, index) => index >= overflow && chunk.endMs + REFERENCE_SETTLE_MS > heardMs);
|
|
3068
|
+
const settledCount = firstUnsettled === -1 ? this.unsettled.length : firstUnsettled;
|
|
3069
|
+
this.keepVoices(run, this.unsettled.splice(0, settledCount));
|
|
3070
|
+
}
|
|
3071
|
+
pushSamples = (samples) => {
|
|
3072
|
+
this.run?.push(samples);
|
|
3073
|
+
};
|
|
3074
|
+
startRun(startMs) {
|
|
3075
|
+
const { previousRunSettled } = this;
|
|
3076
|
+
this.run = new SpeakerTracker({
|
|
3077
|
+
labels: this.labels,
|
|
3078
|
+
onUnavailable: () => this.options.onState("unavailable"),
|
|
3079
|
+
references: () => this.clips.references(),
|
|
3080
|
+
requestToken: async () => {
|
|
3081
|
+
await previousRunSettled;
|
|
3082
|
+
return await this.options.requestToken();
|
|
3083
|
+
},
|
|
3084
|
+
startMs
|
|
3085
|
+
});
|
|
3086
|
+
this.options.onState("detecting");
|
|
3087
|
+
}
|
|
3088
|
+
keepVoices(run, chunks) {
|
|
3089
|
+
let changed = false;
|
|
3090
|
+
for (const chunk of chunks) changed = this.clips.keepFrom(chunk, run.takeTurnsOverlapping(chunk)) || changed;
|
|
3091
|
+
if (changed) this.options.onReferences(this.clips.references());
|
|
3092
|
+
}
|
|
3093
|
+
};
|
|
3094
|
+
//#endregion
|
|
2509
3095
|
//#region src/sessions/recorder/live/event-batcher.ts
|
|
2510
3096
|
const FLUSH_INTERVAL_MS = 5e3;
|
|
2511
3097
|
const NOTE_WAIT_MS = 6e4;
|
|
@@ -2565,6 +3151,7 @@ function createEventBatcher(options) {
|
|
|
2565
3151
|
}
|
|
2566
3152
|
//#endregion
|
|
2567
3153
|
//#region src/sessions/recorder/live/session-payloads.ts
|
|
3154
|
+
const MAX_TURNS_PER_UPLOAD = 200;
|
|
2568
3155
|
function newSessionDraft(options) {
|
|
2569
3156
|
const startedAt = Date.now();
|
|
2570
3157
|
return {
|
|
@@ -2598,7 +3185,8 @@ function startPayload(draft) {
|
|
|
2598
3185
|
startedAt: draft.startedAt,
|
|
2599
3186
|
surface: draft.surface,
|
|
2600
3187
|
...draft.title ? { title: draft.title } : {},
|
|
2601
|
-
...draft.continues ? { continues: draft.continues } : {}
|
|
3188
|
+
...draft.continues ? { continues: draft.continues } : {},
|
|
3189
|
+
...draft.detectSpeakers ? { detectSpeakers: true } : {}
|
|
2602
3190
|
},
|
|
2603
3191
|
kind: "start"
|
|
2604
3192
|
};
|
|
@@ -2634,6 +3222,15 @@ function voicePayload(chunk) {
|
|
|
2634
3222
|
storageId: null
|
|
2635
3223
|
};
|
|
2636
3224
|
}
|
|
3225
|
+
function speakerPayloads(turns) {
|
|
3226
|
+
const payloads = [];
|
|
3227
|
+
for (let from = 0; from < turns.length; from += MAX_TURNS_PER_UPLOAD) payloads.push({
|
|
3228
|
+
clientKey: crypto.randomUUID(),
|
|
3229
|
+
kind: "speakers",
|
|
3230
|
+
turns: turns.slice(from, from + MAX_TURNS_PER_UPLOAD)
|
|
3231
|
+
});
|
|
3232
|
+
return payloads;
|
|
3233
|
+
}
|
|
2637
3234
|
//#endregion
|
|
2638
3235
|
//#region src/sessions/recorder/live/session-quota.ts
|
|
2639
3236
|
var SessionQuota = class {
|
|
@@ -2667,6 +3264,7 @@ var SessionQuota = class {
|
|
|
2667
3264
|
const LARGEST_EVENT_BATCH = 100;
|
|
2668
3265
|
const TICK_MS = 1e3;
|
|
2669
3266
|
const DRAFT_SAVE_INTERVAL_MS = 5e3;
|
|
3267
|
+
const SPEAKER_SETTLE_MS = 2500;
|
|
2670
3268
|
var LiveSession = class {
|
|
2671
3269
|
clock;
|
|
2672
3270
|
sessionKey;
|
|
@@ -2677,6 +3275,8 @@ var LiveSession = class {
|
|
|
2677
3275
|
options;
|
|
2678
3276
|
quota;
|
|
2679
3277
|
run = null;
|
|
3278
|
+
speakers;
|
|
3279
|
+
speakersSettling;
|
|
2680
3280
|
stopListening = null;
|
|
2681
3281
|
tick;
|
|
2682
3282
|
constructor(options) {
|
|
@@ -2695,6 +3295,7 @@ var LiveSession = class {
|
|
|
2695
3295
|
limits: options.limits,
|
|
2696
3296
|
onEvidence: (captured) => this.persist(evidencePayload(captured))
|
|
2697
3297
|
});
|
|
3298
|
+
this.speakers = options.speakers && this.detectSpeakers(options.speakers);
|
|
2698
3299
|
}
|
|
2699
3300
|
annotate(key, note) {
|
|
2700
3301
|
this.events.annotate(key, note);
|
|
@@ -2708,9 +3309,11 @@ var LiveSession = class {
|
|
|
2708
3309
|
if (Date.now() - this.lastSavedAt >= DRAFT_SAVE_INTERVAL_MS) this.saveDraft();
|
|
2709
3310
|
}
|
|
2710
3311
|
async end() {
|
|
3312
|
+
this.stopRun();
|
|
3313
|
+
const atMs = this.clock.atMs();
|
|
3314
|
+
await this.finishSpeakers();
|
|
2711
3315
|
this.stop();
|
|
2712
3316
|
const { outbox, store } = this.options.account;
|
|
2713
|
-
const atMs = this.clock.atMs();
|
|
2714
3317
|
await outbox.enqueue(this.sessionKey, {
|
|
2715
3318
|
action: "end",
|
|
2716
3319
|
atMs,
|
|
@@ -2739,7 +3342,9 @@ var LiveSession = class {
|
|
|
2739
3342
|
};
|
|
2740
3343
|
}
|
|
2741
3344
|
pause(reason) {
|
|
2742
|
-
this.
|
|
3345
|
+
this.stopRun();
|
|
3346
|
+
if (reason === "interrupted") this.persistTurns(this.speakers?.closeRun() ?? []);
|
|
3347
|
+
else this.finishSpeakers().catch(this.hooks.onStorageFailure);
|
|
2743
3348
|
if (!this.clock.isPaused()) {
|
|
2744
3349
|
const atMs = this.clock.pause(reason);
|
|
2745
3350
|
this.persist({
|
|
@@ -2782,6 +3387,9 @@ var LiveSession = class {
|
|
|
2782
3387
|
const { limits } = this.options;
|
|
2783
3388
|
const hasVoiceRoom = this.hasVoiceRoom();
|
|
2784
3389
|
if (!hasVoiceRoom) voice.stop();
|
|
3390
|
+
const startMs = this.clock.atMs();
|
|
3391
|
+
const speakers = hasVoiceRoom ? this.speakers : null;
|
|
3392
|
+
speakers?.startRun(startMs);
|
|
2785
3393
|
this.run = startCollectionRun$1({
|
|
2786
3394
|
atMs: this.clock.atMs,
|
|
2787
3395
|
collectEvents: !this.quota.stopped.has("events"),
|
|
@@ -2802,6 +3410,8 @@ var LiveSession = class {
|
|
|
2802
3410
|
});
|
|
2803
3411
|
this.evidence.navigated();
|
|
2804
3412
|
},
|
|
3413
|
+
onSamples: speakers?.pushSamples,
|
|
3414
|
+
startMs,
|
|
2805
3415
|
voice: hasVoiceRoom ? voice : null
|
|
2806
3416
|
});
|
|
2807
3417
|
this.listen();
|
|
@@ -2812,17 +3422,15 @@ var LiveSession = class {
|
|
|
2812
3422
|
this.events.stop();
|
|
2813
3423
|
}
|
|
2814
3424
|
stopCollecting() {
|
|
2815
|
-
this.
|
|
2816
|
-
this.
|
|
2817
|
-
this.stopListening?.abort();
|
|
2818
|
-
this.stopListening = null;
|
|
2819
|
-
this.evidence.cancelPending();
|
|
2820
|
-
window.clearInterval(this.tick);
|
|
2821
|
-
this.events.flushAll();
|
|
3425
|
+
this.stopRun();
|
|
3426
|
+
this.speakers?.closeRun();
|
|
2822
3427
|
}
|
|
2823
3428
|
stopKind(limit) {
|
|
2824
3429
|
this.quota.stopped.add(limit);
|
|
2825
|
-
if (limit === "segments")
|
|
3430
|
+
if (limit === "segments") {
|
|
3431
|
+
this.run?.stopVoice();
|
|
3432
|
+
this.finishSpeakers().catch(this.hooks.onStorageFailure);
|
|
3433
|
+
}
|
|
2826
3434
|
if (limit === "events") this.run?.stopEvents();
|
|
2827
3435
|
}
|
|
2828
3436
|
draftPatch(state) {
|
|
@@ -2833,6 +3441,21 @@ var LiveSession = class {
|
|
|
2833
3441
|
lastActiveAt: this.lastSavedAt
|
|
2834
3442
|
};
|
|
2835
3443
|
}
|
|
3444
|
+
detectSpeakers(setup) {
|
|
3445
|
+
const { store } = this.options.account;
|
|
3446
|
+
return new SpeakerDetection({
|
|
3447
|
+
labelCount: setup.labelCount,
|
|
3448
|
+
onLabelCount: (speakerLabelCount) => store.patchDraft(this.sessionKey, { speakerLabelCount }).catch(this.hooks.onStorageFailure),
|
|
3449
|
+
onReferences: (speakerReferences) => store.patchDraft(this.sessionKey, { speakerReferences }).catch(this.hooks.onStorageFailure),
|
|
3450
|
+
onState: this.hooks.onSpeakers,
|
|
3451
|
+
references: setup.references,
|
|
3452
|
+
requestToken: setup.requestToken
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
async finishSpeakers() {
|
|
3456
|
+
const turns = await this.speakers?.finishRun() ?? [];
|
|
3457
|
+
for (const payload of speakerPayloads(turns)) await this.options.account.outbox.enqueue(this.sessionKey, payload);
|
|
3458
|
+
}
|
|
2836
3459
|
listen() {
|
|
2837
3460
|
this.stopListening = new AbortController();
|
|
2838
3461
|
const listen = { signal: this.stopListening.signal };
|
|
@@ -2844,9 +3467,23 @@ var LiveSession = class {
|
|
|
2844
3467
|
}
|
|
2845
3468
|
recordChunk = (chunk) => {
|
|
2846
3469
|
if (!this.quota.acceptSegment()) return;
|
|
3470
|
+
this.persistTurns(this.speakers?.drainTurns() ?? []);
|
|
2847
3471
|
this.persist(voicePayload(chunk));
|
|
3472
|
+
this.speakers?.keepReferences(chunk);
|
|
3473
|
+
this.settleSpeakers();
|
|
2848
3474
|
if (this.quota.segmentsFull()) this.hooks.reachLimit("segments");
|
|
2849
3475
|
};
|
|
3476
|
+
persistTurns(turns) {
|
|
3477
|
+
for (const payload of speakerPayloads(turns)) this.persist(payload);
|
|
3478
|
+
}
|
|
3479
|
+
settleSpeakers() {
|
|
3480
|
+
if (!this.speakers) return;
|
|
3481
|
+
window.clearTimeout(this.speakersSettling);
|
|
3482
|
+
this.speakersSettling = window.setTimeout(() => {
|
|
3483
|
+
this.persistTurns(this.speakers?.drainTurns() ?? []);
|
|
3484
|
+
this.speakers?.settleReferences();
|
|
3485
|
+
}, SPEAKER_SETTLE_MS);
|
|
3486
|
+
}
|
|
2850
3487
|
saveDraft(state) {
|
|
2851
3488
|
this.options.account.store.patchDraft(this.sessionKey, this.draftPatch(state)).catch(this.hooks.onStorageFailure);
|
|
2852
3489
|
}
|
|
@@ -2862,8 +3499,32 @@ var LiveSession = class {
|
|
|
2862
3499
|
}
|
|
2863
3500
|
if (this.quota.eventsFull()) this.hooks.reachLimit("events");
|
|
2864
3501
|
};
|
|
3502
|
+
stopRun() {
|
|
3503
|
+
this.run?.stop();
|
|
3504
|
+
this.run = null;
|
|
3505
|
+
this.stopListening?.abort();
|
|
3506
|
+
this.stopListening = null;
|
|
3507
|
+
this.evidence.cancelPending();
|
|
3508
|
+
window.clearInterval(this.tick);
|
|
3509
|
+
window.clearTimeout(this.speakersSettling);
|
|
3510
|
+
this.events.flushAll();
|
|
3511
|
+
}
|
|
2865
3512
|
};
|
|
2866
3513
|
//#endregion
|
|
3514
|
+
//#region src/sessions/recorder/live/speaker-token.ts
|
|
3515
|
+
const RETRYABLE_TOKEN_STATUSES = /* @__PURE__ */ new Set([
|
|
3516
|
+
408,
|
|
3517
|
+
500,
|
|
3518
|
+
502,
|
|
3519
|
+
504
|
|
3520
|
+
]);
|
|
3521
|
+
function requestSpeakerToken(api) {
|
|
3522
|
+
return () => api.speakerToken().catch((error) => {
|
|
3523
|
+
if (error instanceof SessionApiError && !RETRYABLE_TOKEN_STATUSES.has(error.status)) return null;
|
|
3524
|
+
throw error;
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
3527
|
+
//#endregion
|
|
2867
3528
|
//#region src/sessions/recorder/recording-controller.ts
|
|
2868
3529
|
const MAX_NOTE_CHARS = 1e3;
|
|
2869
3530
|
const MAX_TITLE_CHARS = 120;
|
|
@@ -2879,6 +3540,7 @@ const IDLE = {
|
|
|
2879
3540
|
phase: "idle",
|
|
2880
3541
|
sessionId: null,
|
|
2881
3542
|
sessionKey: null,
|
|
3543
|
+
speakers: "off",
|
|
2882
3544
|
startedAt: null
|
|
2883
3545
|
};
|
|
2884
3546
|
function cleanText(text, maxChars) {
|
|
@@ -2900,7 +3562,7 @@ var RecordingController = class {
|
|
|
2900
3562
|
this.account = localSessions.connect({
|
|
2901
3563
|
accountKey: options.account.key,
|
|
2902
3564
|
api: options.api,
|
|
2903
|
-
|
|
3565
|
+
limits: options.limits
|
|
2904
3566
|
});
|
|
2905
3567
|
this.stopIssues = this.account.outbox.subscribeIssues(this.handleIssue);
|
|
2906
3568
|
this.stopSessionIds = localSessions.subscribe(this.syncSessionId);
|
|
@@ -2998,6 +3660,7 @@ var RecordingController = class {
|
|
|
2998
3660
|
const draft = newSessionDraft({
|
|
2999
3661
|
accountKey: this.options.account.key,
|
|
3000
3662
|
continues: options.continues,
|
|
3663
|
+
detectSpeakers: options.detectSpeakers,
|
|
3001
3664
|
surface: this.options.surface,
|
|
3002
3665
|
title: cleanText(options.title, MAX_TITLE_CHARS) ?? ""
|
|
3003
3666
|
});
|
|
@@ -3044,13 +3707,21 @@ var RecordingController = class {
|
|
|
3044
3707
|
onHidden: () => this.pauseFor("hidden"),
|
|
3045
3708
|
onLevel: (level) => this.emitLevel(level),
|
|
3046
3709
|
onPageHide: () => this.interrupt(),
|
|
3710
|
+
onSpeakers: (speakers) => {
|
|
3711
|
+
if (this.live === live) this.publish({ speakers });
|
|
3712
|
+
},
|
|
3047
3713
|
onStorageFailure: (error) => this.failStorage(live, error),
|
|
3048
3714
|
onTick: () => this.onTick(live),
|
|
3049
3715
|
reachLimit: (limit) => this.reachLimit(live, limit)
|
|
3050
3716
|
},
|
|
3051
3717
|
includeSource: this.options.surface === "devtools",
|
|
3052
3718
|
limits: this.options.limits,
|
|
3053
|
-
sessionKey
|
|
3719
|
+
sessionKey,
|
|
3720
|
+
speakers: draft.detectSpeakers ? {
|
|
3721
|
+
labelCount: draft.speakerLabelCount ?? 0,
|
|
3722
|
+
references: draft.speakerReferences ?? {},
|
|
3723
|
+
requestToken: requestSpeakerToken(this.options.api)
|
|
3724
|
+
} : null
|
|
3054
3725
|
});
|
|
3055
3726
|
this.live = live;
|
|
3056
3727
|
this.releaseClaim = this.account.claim({
|
|
@@ -3336,6 +4007,14 @@ function formatClock(ms) {
|
|
|
3336
4007
|
function countLabel(count, noun) {
|
|
3337
4008
|
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
3338
4009
|
}
|
|
4010
|
+
const SPEAKER_LABEL_PREFIX = /^S/;
|
|
4011
|
+
function speakerLabel(speaker) {
|
|
4012
|
+
return speaker.replace(SPEAKER_LABEL_PREFIX, "Speaker ");
|
|
4013
|
+
}
|
|
4014
|
+
function attributedText(said) {
|
|
4015
|
+
if (!said.speech) return said.text;
|
|
4016
|
+
return said.speech.map((run) => run.speaker ? `${speakerLabel(run.speaker)}: ${run.text}` : run.text).join(" ");
|
|
4017
|
+
}
|
|
3339
4018
|
function spokenDuration(ms) {
|
|
3340
4019
|
const totalSeconds = Math.max(0, Math.floor(ms / SECOND_MS));
|
|
3341
4020
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -3768,7 +4447,8 @@ function statusText(snapshot, sync, maxDurationMs) {
|
|
|
3768
4447
|
if (snapshot.limitsReached.length > 0) return `Limit reached for ${snapshot.limitsReached.join(", ")}`;
|
|
3769
4448
|
const problem = syncProblem(sync);
|
|
3770
4449
|
if (problem) return problem;
|
|
3771
|
-
|
|
4450
|
+
if (snapshot.nearLimit) return `${minutesLeft(maxDurationMs - snapshot.activeMs)} min left`;
|
|
4451
|
+
return snapshot.speakers === "unavailable" ? "Speakers aren't being detected" : "";
|
|
3772
4452
|
}
|
|
3773
4453
|
function markMoment(recorder, focus, setOpenMark) {
|
|
3774
4454
|
const key = recorder.mark();
|
|
@@ -4889,7 +5569,7 @@ function ExtractList({ detail, extracts }) {
|
|
|
4889
5569
|
children: quotes.map((segment) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
|
|
4890
5570
|
className: "dt-ses-time",
|
|
4891
5571
|
children: formatClock(segment.startMs)
|
|
4892
|
-
}), ` “${segment
|
|
5572
|
+
}), ` “${attributedText(segment)}”`] }, segment.id))
|
|
4893
5573
|
}),
|
|
4894
5574
|
/* @__PURE__ */ jsx("div", {
|
|
4895
5575
|
className: "dt-ses-thumbs",
|
|
@@ -4967,7 +5647,7 @@ function ChangeSources({ change, context }) {
|
|
|
4967
5647
|
children: [/* @__PURE__ */ jsx("span", {
|
|
4968
5648
|
className: "dt-ses-time",
|
|
4969
5649
|
children: formatClock(segment.startMs)
|
|
4970
|
-
}), ` “${segment
|
|
5650
|
+
}), ` “${attributedText(segment)}”`]
|
|
4971
5651
|
}, segment.id)), evidence.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
4972
5652
|
className: "dt-ses-thumbs",
|
|
4973
5653
|
children: evidence.map((item) => /* @__PURE__ */ jsx(EvidenceThumb, { evidence: item }, item.id))
|
|
@@ -5294,7 +5974,7 @@ function VerdictQuotes({ context, segmentIds }) {
|
|
|
5294
5974
|
children: [/* @__PURE__ */ jsx("span", {
|
|
5295
5975
|
className: "dt-ses-time",
|
|
5296
5976
|
children: formatClock(segment.startMs)
|
|
5297
|
-
}), ` “${segment
|
|
5977
|
+
}), ` “${attributedText(segment)}”`]
|
|
5298
5978
|
}, segment.id));
|
|
5299
5979
|
}
|
|
5300
5980
|
const CHECK_LABELS = {
|
|
@@ -5984,7 +6664,7 @@ function MomentView({ handlers, segmentId, session }) {
|
|
|
5984
6664
|
segment ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5985
6665
|
/* @__PURE__ */ jsx("blockquote", {
|
|
5986
6666
|
className: "dt-ses-moment-quote",
|
|
5987
|
-
children: segment
|
|
6667
|
+
children: attributedText(segment) || "No speech was heard here."
|
|
5988
6668
|
}),
|
|
5989
6669
|
/* @__PURE__ */ jsx(MomentContext, {
|
|
5990
6670
|
detail,
|
|
@@ -6807,7 +7487,8 @@ function DisclosureList() {
|
|
|
6807
7487
|
}), /* @__PURE__ */ jsxs("ul", {
|
|
6808
7488
|
className: "dt-ses-list",
|
|
6809
7489
|
children: [
|
|
6810
|
-
/* @__PURE__ */ jsx("li", { children: "Your voice while recording. It is sent to Reflet and transcribed by
|
|
7490
|
+
/* @__PURE__ */ jsx("li", { children: "Your voice while recording. It is sent to Reflet and transcribed by ElevenLabs Scribe through OpenRouter with zero data retention. The audio is deleted as soon as its transcript is saved, within 24 hours at most." }),
|
|
7491
|
+
/* @__PURE__ */ jsx("li", { children: "If you tick “Several people are talking”, your voice is also streamed live to Mistral (Voxtral) to tell the voices apart. Mistral keeps it up to 30 days and does not train on it." }),
|
|
6811
7492
|
/* @__PURE__ */ jsx("li", { children: "The page: its address without sensitive parameters, its title, the scroll position and the window size." }),
|
|
6812
7493
|
/* @__PURE__ */ jsx("li", { children: "What you click, point at and mark: element names and positions, never your keystrokes." }),
|
|
6813
7494
|
/* @__PURE__ */ jsxs("li", { children: [
|
|
@@ -6869,23 +7550,44 @@ function StartProblem({ failure, onRetry, onWriteNote }) {
|
|
|
6869
7550
|
})]
|
|
6870
7551
|
});
|
|
6871
7552
|
}
|
|
6872
|
-
function
|
|
7553
|
+
function SpeakersChoice({ detectSpeakers, isBusy, onChange }) {
|
|
7554
|
+
const hintId = useId();
|
|
7555
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("label", {
|
|
7556
|
+
className: "dt-ses-check",
|
|
7557
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
7558
|
+
"aria-describedby": hintId,
|
|
7559
|
+
checked: detectSpeakers,
|
|
7560
|
+
disabled: isBusy,
|
|
7561
|
+
onChange: (event) => onChange(event.target.checked),
|
|
7562
|
+
type: "checkbox"
|
|
7563
|
+
}), /* @__PURE__ */ jsx("span", { children: "Several people are talking" })]
|
|
7564
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
7565
|
+
className: "dt-meta",
|
|
7566
|
+
id: hintId,
|
|
7567
|
+
children: "Mistral tells the voices apart so each passage shows who spoke."
|
|
7568
|
+
})] });
|
|
7569
|
+
}
|
|
7570
|
+
function StartActions({ children, isBusy, onStart }) {
|
|
6873
7571
|
return /* @__PURE__ */ jsxs("div", {
|
|
6874
7572
|
className: "dt-ses-start",
|
|
6875
|
-
children: [
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
children
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
7573
|
+
children: [
|
|
7574
|
+
/* @__PURE__ */ jsx("p", {
|
|
7575
|
+
className: "dt-meta",
|
|
7576
|
+
children: "Talk while you use the app. Reflet turns what you say and show into intentions you can brief an agent with."
|
|
7577
|
+
}),
|
|
7578
|
+
children,
|
|
7579
|
+
/* @__PURE__ */ jsx("div", {
|
|
7580
|
+
className: "dt-actions",
|
|
7581
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
7582
|
+
className: "dt-btn",
|
|
7583
|
+
"data-variant": "primary",
|
|
7584
|
+
disabled: isBusy,
|
|
7585
|
+
onClick: onStart,
|
|
7586
|
+
type: "button",
|
|
7587
|
+
children: [/* @__PURE__ */ jsx(MicIcon, {}), "Start session"]
|
|
7588
|
+
})
|
|
6887
7589
|
})
|
|
6888
|
-
|
|
7590
|
+
]
|
|
6889
7591
|
});
|
|
6890
7592
|
}
|
|
6891
7593
|
function useRecorderStart(onStarted) {
|
|
@@ -6914,20 +7616,26 @@ function MicrophonePrompt({ isRequesting }) {
|
|
|
6914
7616
|
});
|
|
6915
7617
|
}
|
|
6916
7618
|
function StartSession({ onStarted, onWriteNote }) {
|
|
6917
|
-
const { account } = useSessionWorkspace();
|
|
7619
|
+
const { account, identity } = useSessionWorkspace();
|
|
6918
7620
|
const { failure, isBusy, isRequesting, start } = useRecorderStart(onStarted);
|
|
6919
7621
|
const [isFirstVisit] = useState(() => !hasSeenDisclosure(account.key));
|
|
6920
|
-
const
|
|
7622
|
+
const [detectSpeakers, setDetectSpeakers] = useState(false);
|
|
7623
|
+
const startSession = () => start({ detectSpeakers });
|
|
6921
7624
|
const disclosure = /* @__PURE__ */ jsx(SessionDisclosure, { accountKey: account.key });
|
|
6922
7625
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6923
7626
|
isFirstVisit && disclosure,
|
|
6924
7627
|
failure ? /* @__PURE__ */ jsx(StartProblem, {
|
|
6925
7628
|
failure,
|
|
6926
|
-
onRetry:
|
|
7629
|
+
onRetry: startSession,
|
|
6927
7630
|
onWriteNote
|
|
6928
7631
|
}) : /* @__PURE__ */ jsx(StartActions, {
|
|
6929
7632
|
isBusy,
|
|
6930
|
-
onStart:
|
|
7633
|
+
onStart: startSession,
|
|
7634
|
+
children: identity.speakersConfigured && /* @__PURE__ */ jsx(SpeakersChoice, {
|
|
7635
|
+
detectSpeakers,
|
|
7636
|
+
isBusy,
|
|
7637
|
+
onChange: setDetectSpeakers
|
|
7638
|
+
})
|
|
6931
7639
|
}),
|
|
6932
7640
|
/* @__PURE__ */ jsx(MicrophonePrompt, { isRequesting }),
|
|
6933
7641
|
!isFirstVisit && disclosure
|
|
@@ -7089,7 +7797,7 @@ function Sources({ context, proposal }) {
|
|
|
7089
7797
|
children: [/* @__PURE__ */ jsx("span", {
|
|
7090
7798
|
className: "dt-ses-time",
|
|
7091
7799
|
children: formatClock(segment.startMs)
|
|
7092
|
-
}), ` “${segment
|
|
7800
|
+
}), ` “${attributedText(segment)}”`]
|
|
7093
7801
|
}, segment.id)),
|
|
7094
7802
|
/* @__PURE__ */ jsx("div", {
|
|
7095
7803
|
className: "dt-ses-thumbs",
|
|
@@ -7257,7 +7965,7 @@ function IntentionSource({ context, proposal }) {
|
|
|
7257
7965
|
children: range
|
|
7258
7966
|
}),
|
|
7259
7967
|
" “",
|
|
7260
|
-
segment
|
|
7968
|
+
attributedText(segment),
|
|
7261
7969
|
"”"
|
|
7262
7970
|
]
|
|
7263
7971
|
}), firstContext && /* @__PURE__ */ jsx("span", {
|
|
@@ -8525,11 +9233,30 @@ function SegmentBody({ onType, review, segment }) {
|
|
|
8525
9233
|
className: "dt-meta",
|
|
8526
9234
|
children: "No speech was heard here."
|
|
8527
9235
|
});
|
|
9236
|
+
if (segment.speech) return /* @__PURE__ */ jsx(SpokenRuns, { speech: segment.speech });
|
|
8528
9237
|
return /* @__PURE__ */ jsx("p", {
|
|
8529
9238
|
className: "dt-note-text",
|
|
8530
9239
|
children: segment.text
|
|
8531
9240
|
});
|
|
8532
9241
|
}
|
|
9242
|
+
function SpokenRuns({ speech }) {
|
|
9243
|
+
let startsAt = 0;
|
|
9244
|
+
const lines = speech.map((run) => {
|
|
9245
|
+
const line = {
|
|
9246
|
+
...run,
|
|
9247
|
+
startsAt
|
|
9248
|
+
};
|
|
9249
|
+
startsAt += run.text.length;
|
|
9250
|
+
return line;
|
|
9251
|
+
});
|
|
9252
|
+
return /* @__PURE__ */ jsx(Fragment, { children: lines.map((line) => /* @__PURE__ */ jsxs("p", {
|
|
9253
|
+
className: "dt-note-text",
|
|
9254
|
+
children: [line.speaker && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
9255
|
+
className: "dt-ses-speaker",
|
|
9256
|
+
children: speakerLabel(line.speaker)
|
|
9257
|
+
}), " "] }), line.text]
|
|
9258
|
+
}, line.startsAt)) });
|
|
9259
|
+
}
|
|
8533
9260
|
function ContextChips({ segment }) {
|
|
8534
9261
|
if (segment.context.length === 0) return null;
|
|
8535
9262
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -8649,7 +9376,8 @@ function contextLine(item, detail) {
|
|
|
8649
9376
|
].filter((part) => part !== null).join(" — ");
|
|
8650
9377
|
}
|
|
8651
9378
|
function spokenText(segment) {
|
|
8652
|
-
|
|
9379
|
+
const text = attributedText(segment);
|
|
9380
|
+
if (text) return text.replace(WHITESPACE_RUN, " ");
|
|
8653
9381
|
return segment.transcription.state === "empty" ? "(no speech heard)" : "(not transcribed)";
|
|
8654
9382
|
}
|
|
8655
9383
|
function passageLines(segment, detail) {
|
|
@@ -9425,6 +10153,7 @@ ${`
|
|
|
9425
10153
|
.dt-ses-seg:focus { outline: 2px solid var(--rf-accent); outline-offset: -2px; }
|
|
9426
10154
|
.dt-ses-seg[data-kind="note"] { padding-block: 2px; }
|
|
9427
10155
|
.dt-ses-seg-body { display: flex; flex: 1; flex-direction: column; gap: 4px; min-width: 0; }
|
|
10156
|
+
.dt-ses-speaker { color: var(--rf-text-muted); font-size: 11.5px; font-weight: 600; }
|
|
9428
10157
|
`}${`
|
|
9429
10158
|
.dt-ses-thumbs { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
9430
10159
|
.dt-ses-shot { position: relative; display: inline-block; max-width: 100%; line-height: 0; }
|
|
@@ -13595,4 +14324,4 @@ function useSheetTab() {
|
|
|
13595
14324
|
//#endregion
|
|
13596
14325
|
export { sessionSheetPlace as A, teamTransport as B, AccountFooter as C, MissingRouteHint as D, MISSING_SECRET_KEY_HINT as E, useWorkspaceLoader as F, SessionApiError as G, disconnectBoard as H, purgeAccount as I, reportFailure as J, sessionApiErrorSchema as K, bridgeBoardCall as L, RecordingLayer as M, SessionWorkspaceProvider as N, SESSIONS_STYLES as O, failureMessage as P, bridgeTransport as R, InboxPanel as S, CONNECT_REQUIREMENT as T, probeDevRoute as U, callReflet as V, startBoardConnect as W, DevtoolsBarButtons as _, NotePicker as a, CodeTab as b, isActiveNote as c, useNotes as d, findOnPage as f, DevtoolsBar as g, DevtoolsSheet as h, NotesPanel as i, useSessionNavigation as j, SessionsPanel as k, isNoteOnPage as l, DEVTOOLS_STYLES as m, useSheetTab as n, enrichNote as o, usePageHighlight as p, Notices as q, NotePins as r, hasTriedToLocate as s, FIRST_SHEET_TAB as t, unsentNoteCount as u, SHEET_TABS as v, AccountNote as w, useCodeLookup as x, SessionsBarButton as y, devtoolsTransport as z };
|
|
13597
14326
|
|
|
13598
|
-
//# sourceMappingURL=use-sheet-tab-
|
|
14327
|
+
//# sourceMappingURL=use-sheet-tab-DTGgQ_1c.js.map
|