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.
@@ -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-ClxYWmhu.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-D7XKlgee.js";
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
- maxLocalBufferBytes: null
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 maxLocalBufferBytes = options.maxLocalBufferBytes ?? existing?.maxLocalBufferBytes ?? Number.POSITIVE_INFINITY;
1658
+ const limits = options.limits ?? existing?.limits ?? UNBOUNDED_LIMITS;
1619
1659
  const outbox = createOutbox({
1620
1660
  accountKey,
1621
1661
  api,
1622
- maxLocalBufferBytes,
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
- maxLocalBufferBytes,
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
- samples.forEach((sample, index) => {
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
- startMs: options.atMs()
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.stopCollecting();
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.run?.stop();
2816
- this.run = null;
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") this.run?.stopVoice();
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
- maxLocalBufferBytes: options.limits.maxLocalBufferBytes
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
- return snapshot.nearLimit ? `${minutesLeft(maxDurationMs - snapshot.activeMs)} min left` : "";
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.text}”`] }, segment.id))
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.text}”`]
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.text}”`]
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.text || "No speech was heard here."
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 Whisper through OpenRouter with zero data retention. The audio is deleted as soon as its transcript is saved, within 24 hours at most." }),
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 StartActions({ isBusy, onStart }) {
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: [/* @__PURE__ */ jsx("p", {
6876
- className: "dt-meta",
6877
- children: "Talk while you use the app. Reflet turns what you say and show into intentions you can brief an agent with."
6878
- }), /* @__PURE__ */ jsx("div", {
6879
- className: "dt-actions",
6880
- children: /* @__PURE__ */ jsxs("button", {
6881
- className: "dt-btn",
6882
- "data-variant": "primary",
6883
- disabled: isBusy,
6884
- onClick: onStart,
6885
- type: "button",
6886
- children: [/* @__PURE__ */ jsx(MicIcon, {}), "Start session"]
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 startBlank = () => start({});
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: startBlank,
7629
+ onRetry: startSession,
6927
7630
  onWriteNote
6928
7631
  }) : /* @__PURE__ */ jsx(StartActions, {
6929
7632
  isBusy,
6930
- onStart: startBlank
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.text}”`]
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.text,
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
- if (segment.text) return segment.text.replace(WHITESPACE_RUN, " ");
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-QLQ1oeso.js.map
14327
+ //# sourceMappingURL=use-sheet-tab-DTGgQ_1c.js.map