@absolutejs/voice 0.0.22-beta.576 → 0.0.22-beta.578

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.
@@ -1533,12 +1533,165 @@ var createVoiceController = (path, options = {}) => {
1533
1533
  };
1534
1534
  };
1535
1535
 
1536
+ // src/client/timeStretch.ts
1537
+ var HOP_MS = 10;
1538
+ var SEEK_MS = 5;
1539
+ var ENERGY_EPSILON = 0.000001;
1540
+ var HALF = 0.5;
1541
+ var MS_PER_SECOND = 1000;
1542
+ var makeHann = (length) => {
1543
+ const weights = new Float32Array(length);
1544
+ for (let index = 0;index < length; index += 1) {
1545
+ weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
1546
+ }
1547
+ return weights;
1548
+ };
1549
+ var correlationScore = (base, start, ref, length) => {
1550
+ let dot = 0;
1551
+ let energy = 0;
1552
+ for (let index = 0;index < length; index += 1) {
1553
+ const sample = base[start + index] ?? 0;
1554
+ dot += sample * (ref[index] ?? 0);
1555
+ energy += sample * sample;
1556
+ }
1557
+ return dot / Math.sqrt(energy + ENERGY_EPSILON);
1558
+ };
1559
+ var overlapAddGrain = (src, off, tail, weights, hop) => {
1560
+ const out = new Float32Array(hop);
1561
+ const nextTail = new Float32Array(hop);
1562
+ for (let index = 0;index < hop; index += 1) {
1563
+ out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
1564
+ nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
1565
+ }
1566
+ return { nextTail, out };
1567
+ };
1568
+ var createTimeStretcher = () => {
1569
+ let sampleRate = 0;
1570
+ let channelCount = 0;
1571
+ let hop = 0;
1572
+ let frameLen = 0;
1573
+ let seek = 0;
1574
+ let weights = new Float32Array(0);
1575
+ let buffers = [];
1576
+ let inputStart = 0;
1577
+ let analysisPos = 0;
1578
+ let olaTail = [];
1579
+ let naturalRef = null;
1580
+ const init = (rate, channels) => {
1581
+ sampleRate = rate;
1582
+ channelCount = channels;
1583
+ hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
1584
+ frameLen = hop * 2;
1585
+ seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
1586
+ weights = makeHann(frameLen);
1587
+ buffers = Array.from({ length: channels }, () => new Float32Array(0));
1588
+ olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
1589
+ inputStart = 0;
1590
+ analysisPos = seek;
1591
+ naturalRef = null;
1592
+ };
1593
+ const reset = () => {
1594
+ buffers = buffers.map(() => new Float32Array(0));
1595
+ olaTail = olaTail.map(() => new Float32Array(hop));
1596
+ inputStart = 0;
1597
+ analysisPos = seek;
1598
+ naturalRef = null;
1599
+ };
1600
+ const append = (input) => {
1601
+ for (let channel = 0;channel < channelCount; channel += 1) {
1602
+ const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
1603
+ const existing = buffers[channel] ?? new Float32Array(0);
1604
+ const merged = new Float32Array(existing.length + incoming.length);
1605
+ merged.set(existing, 0);
1606
+ merged.set(incoming, existing.length);
1607
+ buffers[channel] = merged;
1608
+ }
1609
+ };
1610
+ const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
1611
+ const compact = () => {
1612
+ const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
1613
+ if (keepFrom <= inputStart)
1614
+ return;
1615
+ const drop = keepFrom - inputStart;
1616
+ for (let channel = 0;channel < channelCount; channel += 1) {
1617
+ buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
1618
+ }
1619
+ inputStart = keepFrom;
1620
+ };
1621
+ const bestOffset = (center) => {
1622
+ if (!naturalRef)
1623
+ return 0;
1624
+ const [base] = buffers;
1625
+ if (!base)
1626
+ return 0;
1627
+ let bestDelta = 0;
1628
+ let bestScore = -Infinity;
1629
+ for (let delta = -seek;delta <= seek; delta += 1) {
1630
+ const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
1631
+ if (score <= bestScore)
1632
+ continue;
1633
+ bestScore = score;
1634
+ bestDelta = delta;
1635
+ }
1636
+ return bestDelta;
1637
+ };
1638
+ const process = (input, speed, rate) => {
1639
+ const channels = Math.max(1, input.length);
1640
+ if (sampleRate !== rate || channelCount !== channels)
1641
+ init(rate, channels);
1642
+ append(input);
1643
+ const analysisHop = hop * speed;
1644
+ const segments = Array.from({ length: channelCount }, () => []);
1645
+ const emitGrain = (pos) => {
1646
+ const off = pos - inputStart;
1647
+ for (let channel = 0;channel < channelCount; channel += 1) {
1648
+ const src = buffers[channel];
1649
+ const tail = olaTail[channel];
1650
+ if (!src || !tail)
1651
+ continue;
1652
+ const grain = overlapAddGrain(src, off, tail, weights, hop);
1653
+ olaTail[channel] = grain.nextTail;
1654
+ segments[channel]?.push(grain.out);
1655
+ }
1656
+ };
1657
+ const captureRef = (pos) => {
1658
+ const ref = new Float32Array(frameLen);
1659
+ const refOff = pos + hop - inputStart;
1660
+ const [base] = buffers;
1661
+ if (base)
1662
+ ref.set(base.subarray(refOff, refOff + frameLen));
1663
+ naturalRef = ref;
1664
+ };
1665
+ const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
1666
+ while (canEmit()) {
1667
+ const center = Math.round(analysisPos);
1668
+ const pos = center + bestOffset(center);
1669
+ emitGrain(pos);
1670
+ captureRef(pos);
1671
+ analysisPos += analysisHop;
1672
+ }
1673
+ compact();
1674
+ return segments.map((channelSegments) => {
1675
+ const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
1676
+ const merged = new Float32Array(total);
1677
+ let offset = 0;
1678
+ for (const seg of channelSegments) {
1679
+ merged.set(seg, offset);
1680
+ offset += seg.length;
1681
+ }
1682
+ return merged;
1683
+ });
1684
+ };
1685
+ return { process, reset };
1686
+ };
1687
+
1536
1688
  // src/client/audioPlayer.ts
1537
1689
  var DEFAULT_LOOKAHEAD_MS = 15;
1538
1690
  var DEFAULT_VOLUME = 1;
1539
1691
  var DEFAULT_PLAYBACK_RATE = 1;
1540
1692
  var MIN_PLAYBACK_RATE = 0.5;
1541
1693
  var MAX_PLAYBACK_RATE = 2;
1694
+ var STRETCH_BYPASS_EPSILON = 0.01;
1542
1695
  var createInitialState3 = () => ({
1543
1696
  activeSourceCount: 0,
1544
1697
  error: null,
@@ -1601,6 +1754,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1601
1754
  let outputNode = null;
1602
1755
  let volume = clampVolume(options.volume);
1603
1756
  let playbackRate = clampPlaybackRate(options.playbackRate);
1757
+ let stretcher = null;
1604
1758
  let queueEndTime = 0;
1605
1759
  let syncPromise = Promise.resolve();
1606
1760
  let interruptStartedAt = null;
@@ -1633,6 +1787,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1633
1787
  const resolveInterrupt = (latencyMs) => {
1634
1788
  clearInterruptTimer();
1635
1789
  interruptStartedAt = null;
1790
+ stretcher?.reset();
1636
1791
  setState({
1637
1792
  activeSourceCount: sourceNodes.size,
1638
1793
  isPlaying: false,
@@ -1697,13 +1852,11 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1697
1852
  queueEndTime = audioContext.currentTime;
1698
1853
  return audioContext;
1699
1854
  };
1700
- const scheduleChunk = async (chunk) => {
1701
- const context = await ensureAudioContext();
1702
- const buffer = decodePCM16LEChunk(context, chunk);
1855
+ const scheduleBuffer = (context, buffer, rate) => {
1703
1856
  const node = context.createBufferSource();
1704
1857
  node.buffer = buffer;
1705
1858
  if (node.playbackRate) {
1706
- node.playbackRate.value = playbackRate;
1859
+ node.playbackRate.value = rate;
1707
1860
  }
1708
1861
  node.connect(outputNode ?? context.destination);
1709
1862
  node.onended = () => {
@@ -1716,7 +1869,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1716
1869
  maybeResolveInterrupt();
1717
1870
  };
1718
1871
  const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1719
- queueEndTime = startAt + buffer.duration / playbackRate;
1872
+ queueEndTime = startAt + buffer.duration / rate;
1720
1873
  sourceNodes.add(node);
1721
1874
  setState({
1722
1875
  activeSourceCount: sourceNodes.size,
@@ -1724,6 +1877,34 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1724
1877
  });
1725
1878
  node.start(startAt);
1726
1879
  };
1880
+ const scheduleChunk = async (chunk) => {
1881
+ const context = await ensureAudioContext();
1882
+ const buffer = decodePCM16LEChunk(context, chunk);
1883
+ if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
1884
+ stretcher?.reset();
1885
+ scheduleBuffer(context, buffer, playbackRate);
1886
+ return;
1887
+ }
1888
+ const channels = Math.max(1, chunk.format.channels);
1889
+ const input = [];
1890
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1891
+ input.push(buffer.getChannelData(channelIndex));
1892
+ }
1893
+ stretcher ??= createTimeStretcher();
1894
+ const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
1895
+ const outLength = stretched[0]?.length ?? 0;
1896
+ if (outLength === 0) {
1897
+ return;
1898
+ }
1899
+ const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
1900
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1901
+ const channelOut = stretched[channelIndex];
1902
+ if (!channelOut)
1903
+ continue;
1904
+ outBuffer.getChannelData(channelIndex).set(channelOut);
1905
+ }
1906
+ scheduleBuffer(context, outBuffer, 1);
1907
+ };
1727
1908
  const stopQueuedPlayback = (options2) => {
1728
1909
  for (const node of [...sourceNodes]) {
1729
1910
  node.stop?.();
@@ -2,6 +2,7 @@ export { bindVoiceReactiveSource, voiceSseReactiveSource, } from "./reactiveSour
2
2
  export type { VoiceReactiveSource, VoiceSseReactiveSourceOptions, } from "./reactiveSource";
3
3
  export { createVoiceConnection } from "./connection";
4
4
  export { createVoiceAudioPlayer, decodeVoiceAudioChunk } from "./audioPlayer";
5
+ export { createTimeStretcher, type TimeStretcher } from "./timeStretch";
5
6
  export { createVoiceStream } from "./createVoiceStream";
6
7
  export { createVoiceBrowserMediaReporter } from "./browserMedia";
7
8
  export type { VoiceBrowserMediaReporter } from "./browserMedia";
@@ -370,12 +370,165 @@ var createVoiceConnection = (path, options = {}) => {
370
370
  getSessionId: () => state.sessionId
371
371
  };
372
372
  };
373
+ // src/client/timeStretch.ts
374
+ var HOP_MS = 10;
375
+ var SEEK_MS = 5;
376
+ var ENERGY_EPSILON = 0.000001;
377
+ var HALF = 0.5;
378
+ var MS_PER_SECOND = 1000;
379
+ var makeHann = (length) => {
380
+ const weights = new Float32Array(length);
381
+ for (let index = 0;index < length; index += 1) {
382
+ weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
383
+ }
384
+ return weights;
385
+ };
386
+ var correlationScore = (base, start, ref, length) => {
387
+ let dot = 0;
388
+ let energy = 0;
389
+ for (let index = 0;index < length; index += 1) {
390
+ const sample = base[start + index] ?? 0;
391
+ dot += sample * (ref[index] ?? 0);
392
+ energy += sample * sample;
393
+ }
394
+ return dot / Math.sqrt(energy + ENERGY_EPSILON);
395
+ };
396
+ var overlapAddGrain = (src, off, tail, weights, hop) => {
397
+ const out = new Float32Array(hop);
398
+ const nextTail = new Float32Array(hop);
399
+ for (let index = 0;index < hop; index += 1) {
400
+ out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
401
+ nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
402
+ }
403
+ return { nextTail, out };
404
+ };
405
+ var createTimeStretcher = () => {
406
+ let sampleRate = 0;
407
+ let channelCount = 0;
408
+ let hop = 0;
409
+ let frameLen = 0;
410
+ let seek = 0;
411
+ let weights = new Float32Array(0);
412
+ let buffers = [];
413
+ let inputStart = 0;
414
+ let analysisPos = 0;
415
+ let olaTail = [];
416
+ let naturalRef = null;
417
+ const init = (rate, channels) => {
418
+ sampleRate = rate;
419
+ channelCount = channels;
420
+ hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
421
+ frameLen = hop * 2;
422
+ seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
423
+ weights = makeHann(frameLen);
424
+ buffers = Array.from({ length: channels }, () => new Float32Array(0));
425
+ olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
426
+ inputStart = 0;
427
+ analysisPos = seek;
428
+ naturalRef = null;
429
+ };
430
+ const reset = () => {
431
+ buffers = buffers.map(() => new Float32Array(0));
432
+ olaTail = olaTail.map(() => new Float32Array(hop));
433
+ inputStart = 0;
434
+ analysisPos = seek;
435
+ naturalRef = null;
436
+ };
437
+ const append = (input) => {
438
+ for (let channel = 0;channel < channelCount; channel += 1) {
439
+ const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
440
+ const existing = buffers[channel] ?? new Float32Array(0);
441
+ const merged = new Float32Array(existing.length + incoming.length);
442
+ merged.set(existing, 0);
443
+ merged.set(incoming, existing.length);
444
+ buffers[channel] = merged;
445
+ }
446
+ };
447
+ const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
448
+ const compact = () => {
449
+ const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
450
+ if (keepFrom <= inputStart)
451
+ return;
452
+ const drop = keepFrom - inputStart;
453
+ for (let channel = 0;channel < channelCount; channel += 1) {
454
+ buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
455
+ }
456
+ inputStart = keepFrom;
457
+ };
458
+ const bestOffset = (center) => {
459
+ if (!naturalRef)
460
+ return 0;
461
+ const [base] = buffers;
462
+ if (!base)
463
+ return 0;
464
+ let bestDelta = 0;
465
+ let bestScore = -Infinity;
466
+ for (let delta = -seek;delta <= seek; delta += 1) {
467
+ const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
468
+ if (score <= bestScore)
469
+ continue;
470
+ bestScore = score;
471
+ bestDelta = delta;
472
+ }
473
+ return bestDelta;
474
+ };
475
+ const process = (input, speed, rate) => {
476
+ const channels = Math.max(1, input.length);
477
+ if (sampleRate !== rate || channelCount !== channels)
478
+ init(rate, channels);
479
+ append(input);
480
+ const analysisHop = hop * speed;
481
+ const segments = Array.from({ length: channelCount }, () => []);
482
+ const emitGrain = (pos) => {
483
+ const off = pos - inputStart;
484
+ for (let channel = 0;channel < channelCount; channel += 1) {
485
+ const src = buffers[channel];
486
+ const tail = olaTail[channel];
487
+ if (!src || !tail)
488
+ continue;
489
+ const grain = overlapAddGrain(src, off, tail, weights, hop);
490
+ olaTail[channel] = grain.nextTail;
491
+ segments[channel]?.push(grain.out);
492
+ }
493
+ };
494
+ const captureRef = (pos) => {
495
+ const ref = new Float32Array(frameLen);
496
+ const refOff = pos + hop - inputStart;
497
+ const [base] = buffers;
498
+ if (base)
499
+ ref.set(base.subarray(refOff, refOff + frameLen));
500
+ naturalRef = ref;
501
+ };
502
+ const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
503
+ while (canEmit()) {
504
+ const center = Math.round(analysisPos);
505
+ const pos = center + bestOffset(center);
506
+ emitGrain(pos);
507
+ captureRef(pos);
508
+ analysisPos += analysisHop;
509
+ }
510
+ compact();
511
+ return segments.map((channelSegments) => {
512
+ const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
513
+ const merged = new Float32Array(total);
514
+ let offset = 0;
515
+ for (const seg of channelSegments) {
516
+ merged.set(seg, offset);
517
+ offset += seg.length;
518
+ }
519
+ return merged;
520
+ });
521
+ };
522
+ return { process, reset };
523
+ };
524
+
373
525
  // src/client/audioPlayer.ts
374
526
  var DEFAULT_LOOKAHEAD_MS = 15;
375
527
  var DEFAULT_VOLUME = 1;
376
528
  var DEFAULT_PLAYBACK_RATE = 1;
377
529
  var MIN_PLAYBACK_RATE = 0.5;
378
530
  var MAX_PLAYBACK_RATE = 2;
531
+ var STRETCH_BYPASS_EPSILON = 0.01;
379
532
  var createInitialState = () => ({
380
533
  activeSourceCount: 0,
381
534
  error: null,
@@ -438,6 +591,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
438
591
  let outputNode = null;
439
592
  let volume = clampVolume(options.volume);
440
593
  let playbackRate = clampPlaybackRate(options.playbackRate);
594
+ let stretcher = null;
441
595
  let queueEndTime = 0;
442
596
  let syncPromise = Promise.resolve();
443
597
  let interruptStartedAt = null;
@@ -470,6 +624,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
470
624
  const resolveInterrupt = (latencyMs) => {
471
625
  clearInterruptTimer();
472
626
  interruptStartedAt = null;
627
+ stretcher?.reset();
473
628
  setState({
474
629
  activeSourceCount: sourceNodes.size,
475
630
  isPlaying: false,
@@ -534,13 +689,11 @@ var createVoiceAudioPlayer = (source, options = {}) => {
534
689
  queueEndTime = audioContext.currentTime;
535
690
  return audioContext;
536
691
  };
537
- const scheduleChunk = async (chunk) => {
538
- const context = await ensureAudioContext();
539
- const buffer = decodePCM16LEChunk(context, chunk);
692
+ const scheduleBuffer = (context, buffer, rate) => {
540
693
  const node = context.createBufferSource();
541
694
  node.buffer = buffer;
542
695
  if (node.playbackRate) {
543
- node.playbackRate.value = playbackRate;
696
+ node.playbackRate.value = rate;
544
697
  }
545
698
  node.connect(outputNode ?? context.destination);
546
699
  node.onended = () => {
@@ -553,7 +706,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
553
706
  maybeResolveInterrupt();
554
707
  };
555
708
  const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
556
- queueEndTime = startAt + buffer.duration / playbackRate;
709
+ queueEndTime = startAt + buffer.duration / rate;
557
710
  sourceNodes.add(node);
558
711
  setState({
559
712
  activeSourceCount: sourceNodes.size,
@@ -561,6 +714,34 @@ var createVoiceAudioPlayer = (source, options = {}) => {
561
714
  });
562
715
  node.start(startAt);
563
716
  };
717
+ const scheduleChunk = async (chunk) => {
718
+ const context = await ensureAudioContext();
719
+ const buffer = decodePCM16LEChunk(context, chunk);
720
+ if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
721
+ stretcher?.reset();
722
+ scheduleBuffer(context, buffer, playbackRate);
723
+ return;
724
+ }
725
+ const channels = Math.max(1, chunk.format.channels);
726
+ const input = [];
727
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
728
+ input.push(buffer.getChannelData(channelIndex));
729
+ }
730
+ stretcher ??= createTimeStretcher();
731
+ const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
732
+ const outLength = stretched[0]?.length ?? 0;
733
+ if (outLength === 0) {
734
+ return;
735
+ }
736
+ const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
737
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
738
+ const channelOut = stretched[channelIndex];
739
+ if (!channelOut)
740
+ continue;
741
+ outBuffer.getChannelData(channelIndex).set(channelOut);
742
+ }
743
+ scheduleBuffer(context, outBuffer, 1);
744
+ };
564
745
  const stopQueuedPlayback = (options2) => {
565
746
  for (const node of [...sourceNodes]) {
566
747
  node.stop?.();
@@ -12303,6 +12484,7 @@ export {
12303
12484
  createVoiceAudioPlayer,
12304
12485
  createVoiceAgentSquadStatusViewModel,
12305
12486
  createVoiceAgentSquadStatusStore,
12487
+ createTimeStretcher,
12306
12488
  createMicrophoneCapture,
12307
12489
  buildVoiceAgentSquadStatusReport,
12308
12490
  bindVoiceReactiveSource,
@@ -0,0 +1,5 @@
1
+ export type TimeStretcher = {
2
+ process: (input: Float32Array[], speed: number, sampleRate: number) => Float32Array[];
3
+ reset: () => void;
4
+ };
5
+ export declare const createTimeStretcher: () => TimeStretcher;
package/dist/index.js CHANGED
@@ -45633,15 +45633,18 @@ var createCachedTTS = (inner, options) => {
45633
45633
  }
45634
45634
  };
45635
45635
  const cached = cache.get(key) ?? await loadFromStore(key);
45636
- if (cached) {
45636
+ if (cached && cached.length > 0) {
45637
45637
  await replayEvents(cached);
45638
45638
  return;
45639
45639
  }
45640
45640
  capture = [];
45641
45641
  await session.send(text);
45642
45642
  const rendered = capture;
45643
- remember(key, rendered);
45644
45643
  capture = null;
45644
+ if (rendered.length === 0) {
45645
+ return;
45646
+ }
45647
+ remember(key, rendered);
45645
45648
  if (store) {
45646
45649
  try {
45647
45650
  await store.set(key, rendered);
@@ -1577,12 +1577,165 @@ var buildSessionCorrectionAudit = (raw, generic, experimental, benchmarkSeeded,
1577
1577
  }
1578
1578
  };
1579
1579
  };
1580
+ // src/client/timeStretch.ts
1581
+ var HOP_MS = 10;
1582
+ var SEEK_MS = 5;
1583
+ var ENERGY_EPSILON = 0.000001;
1584
+ var HALF = 0.5;
1585
+ var MS_PER_SECOND = 1000;
1586
+ var makeHann = (length) => {
1587
+ const weights = new Float32Array(length);
1588
+ for (let index = 0;index < length; index += 1) {
1589
+ weights[index] = HALF - HALF * Math.cos(2 * Math.PI * index / length);
1590
+ }
1591
+ return weights;
1592
+ };
1593
+ var correlationScore = (base, start, ref, length) => {
1594
+ let dot = 0;
1595
+ let energy = 0;
1596
+ for (let index = 0;index < length; index += 1) {
1597
+ const sample = base[start + index] ?? 0;
1598
+ dot += sample * (ref[index] ?? 0);
1599
+ energy += sample * sample;
1600
+ }
1601
+ return dot / Math.sqrt(energy + ENERGY_EPSILON);
1602
+ };
1603
+ var overlapAddGrain = (src, off, tail, weights, hop) => {
1604
+ const out = new Float32Array(hop);
1605
+ const nextTail = new Float32Array(hop);
1606
+ for (let index = 0;index < hop; index += 1) {
1607
+ out[index] = (tail[index] ?? 0) + (src[off + index] ?? 0) * (weights[index] ?? 0);
1608
+ nextTail[index] = (src[off + hop + index] ?? 0) * (weights[hop + index] ?? 0);
1609
+ }
1610
+ return { nextTail, out };
1611
+ };
1612
+ var createTimeStretcher = () => {
1613
+ let sampleRate = 0;
1614
+ let channelCount = 0;
1615
+ let hop = 0;
1616
+ let frameLen = 0;
1617
+ let seek = 0;
1618
+ let weights = new Float32Array(0);
1619
+ let buffers = [];
1620
+ let inputStart = 0;
1621
+ let analysisPos = 0;
1622
+ let olaTail = [];
1623
+ let naturalRef = null;
1624
+ const init = (rate, channels) => {
1625
+ sampleRate = rate;
1626
+ channelCount = channels;
1627
+ hop = Math.max(1, Math.round(sampleRate * HOP_MS / MS_PER_SECOND));
1628
+ frameLen = hop * 2;
1629
+ seek = Math.max(1, Math.round(sampleRate * SEEK_MS / MS_PER_SECOND));
1630
+ weights = makeHann(frameLen);
1631
+ buffers = Array.from({ length: channels }, () => new Float32Array(0));
1632
+ olaTail = Array.from({ length: channels }, () => new Float32Array(hop));
1633
+ inputStart = 0;
1634
+ analysisPos = seek;
1635
+ naturalRef = null;
1636
+ };
1637
+ const reset = () => {
1638
+ buffers = buffers.map(() => new Float32Array(0));
1639
+ olaTail = olaTail.map(() => new Float32Array(hop));
1640
+ inputStart = 0;
1641
+ analysisPos = seek;
1642
+ naturalRef = null;
1643
+ };
1644
+ const append = (input) => {
1645
+ for (let channel = 0;channel < channelCount; channel += 1) {
1646
+ const incoming = input[channel] ?? input[0] ?? new Float32Array(0);
1647
+ const existing = buffers[channel] ?? new Float32Array(0);
1648
+ const merged = new Float32Array(existing.length + incoming.length);
1649
+ merged.set(existing, 0);
1650
+ merged.set(incoming, existing.length);
1651
+ buffers[channel] = merged;
1652
+ }
1653
+ };
1654
+ const inputEnd = () => inputStart + (buffers[0]?.length ?? 0);
1655
+ const compact = () => {
1656
+ const keepFrom = Math.max(inputStart, Math.floor(analysisPos) - seek - 1);
1657
+ if (keepFrom <= inputStart)
1658
+ return;
1659
+ const drop = keepFrom - inputStart;
1660
+ for (let channel = 0;channel < channelCount; channel += 1) {
1661
+ buffers[channel] = (buffers[channel] ?? new Float32Array(0)).slice(drop);
1662
+ }
1663
+ inputStart = keepFrom;
1664
+ };
1665
+ const bestOffset = (center) => {
1666
+ if (!naturalRef)
1667
+ return 0;
1668
+ const [base] = buffers;
1669
+ if (!base)
1670
+ return 0;
1671
+ let bestDelta = 0;
1672
+ let bestScore = -Infinity;
1673
+ for (let delta = -seek;delta <= seek; delta += 1) {
1674
+ const score = correlationScore(base, center + delta - inputStart, naturalRef, frameLen);
1675
+ if (score <= bestScore)
1676
+ continue;
1677
+ bestScore = score;
1678
+ bestDelta = delta;
1679
+ }
1680
+ return bestDelta;
1681
+ };
1682
+ const process2 = (input, speed, rate) => {
1683
+ const channels = Math.max(1, input.length);
1684
+ if (sampleRate !== rate || channelCount !== channels)
1685
+ init(rate, channels);
1686
+ append(input);
1687
+ const analysisHop = hop * speed;
1688
+ const segments = Array.from({ length: channelCount }, () => []);
1689
+ const emitGrain = (pos) => {
1690
+ const off = pos - inputStart;
1691
+ for (let channel = 0;channel < channelCount; channel += 1) {
1692
+ const src = buffers[channel];
1693
+ const tail = olaTail[channel];
1694
+ if (!src || !tail)
1695
+ continue;
1696
+ const grain = overlapAddGrain(src, off, tail, weights, hop);
1697
+ olaTail[channel] = grain.nextTail;
1698
+ segments[channel]?.push(grain.out);
1699
+ }
1700
+ };
1701
+ const captureRef = (pos) => {
1702
+ const ref = new Float32Array(frameLen);
1703
+ const refOff = pos + hop - inputStart;
1704
+ const [base] = buffers;
1705
+ if (base)
1706
+ ref.set(base.subarray(refOff, refOff + frameLen));
1707
+ naturalRef = ref;
1708
+ };
1709
+ const canEmit = () => Math.floor(analysisPos) - seek >= inputStart && Math.floor(analysisPos) + seek + frameLen + hop <= inputEnd();
1710
+ while (canEmit()) {
1711
+ const center = Math.round(analysisPos);
1712
+ const pos = center + bestOffset(center);
1713
+ emitGrain(pos);
1714
+ captureRef(pos);
1715
+ analysisPos += analysisHop;
1716
+ }
1717
+ compact();
1718
+ return segments.map((channelSegments) => {
1719
+ const total = channelSegments.reduce((sum, seg) => sum + seg.length, 0);
1720
+ const merged = new Float32Array(total);
1721
+ let offset = 0;
1722
+ for (const seg of channelSegments) {
1723
+ merged.set(seg, offset);
1724
+ offset += seg.length;
1725
+ }
1726
+ return merged;
1727
+ });
1728
+ };
1729
+ return { process: process2, reset };
1730
+ };
1731
+
1580
1732
  // src/client/audioPlayer.ts
1581
1733
  var DEFAULT_LOOKAHEAD_MS = 15;
1582
1734
  var DEFAULT_VOLUME = 1;
1583
1735
  var DEFAULT_PLAYBACK_RATE = 1;
1584
1736
  var MIN_PLAYBACK_RATE = 0.5;
1585
1737
  var MAX_PLAYBACK_RATE = 2;
1738
+ var STRETCH_BYPASS_EPSILON = 0.01;
1586
1739
  var createInitialState = () => ({
1587
1740
  activeSourceCount: 0,
1588
1741
  error: null,
@@ -1645,6 +1798,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1645
1798
  let outputNode = null;
1646
1799
  let volume = clampVolume(options.volume);
1647
1800
  let playbackRate = clampPlaybackRate(options.playbackRate);
1801
+ let stretcher = null;
1648
1802
  let queueEndTime = 0;
1649
1803
  let syncPromise = Promise.resolve();
1650
1804
  let interruptStartedAt = null;
@@ -1677,6 +1831,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1677
1831
  const resolveInterrupt = (latencyMs) => {
1678
1832
  clearInterruptTimer();
1679
1833
  interruptStartedAt = null;
1834
+ stretcher?.reset();
1680
1835
  setState({
1681
1836
  activeSourceCount: sourceNodes.size,
1682
1837
  isPlaying: false,
@@ -1741,13 +1896,11 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1741
1896
  queueEndTime = audioContext.currentTime;
1742
1897
  return audioContext;
1743
1898
  };
1744
- const scheduleChunk = async (chunk) => {
1745
- const context = await ensureAudioContext();
1746
- const buffer = decodePCM16LEChunk(context, chunk);
1899
+ const scheduleBuffer = (context, buffer, rate) => {
1747
1900
  const node = context.createBufferSource();
1748
1901
  node.buffer = buffer;
1749
1902
  if (node.playbackRate) {
1750
- node.playbackRate.value = playbackRate;
1903
+ node.playbackRate.value = rate;
1751
1904
  }
1752
1905
  node.connect(outputNode ?? context.destination);
1753
1906
  node.onended = () => {
@@ -1760,7 +1913,7 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1760
1913
  maybeResolveInterrupt();
1761
1914
  };
1762
1915
  const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1763
- queueEndTime = startAt + buffer.duration / playbackRate;
1916
+ queueEndTime = startAt + buffer.duration / rate;
1764
1917
  sourceNodes.add(node);
1765
1918
  setState({
1766
1919
  activeSourceCount: sourceNodes.size,
@@ -1768,6 +1921,34 @@ var createVoiceAudioPlayer = (source, options = {}) => {
1768
1921
  });
1769
1922
  node.start(startAt);
1770
1923
  };
1924
+ const scheduleChunk = async (chunk) => {
1925
+ const context = await ensureAudioContext();
1926
+ const buffer = decodePCM16LEChunk(context, chunk);
1927
+ if (Math.abs(playbackRate - 1) <= STRETCH_BYPASS_EPSILON) {
1928
+ stretcher?.reset();
1929
+ scheduleBuffer(context, buffer, playbackRate);
1930
+ return;
1931
+ }
1932
+ const channels = Math.max(1, chunk.format.channels);
1933
+ const input = [];
1934
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1935
+ input.push(buffer.getChannelData(channelIndex));
1936
+ }
1937
+ stretcher ??= createTimeStretcher();
1938
+ const stretched = stretcher.process(input, playbackRate, chunk.format.sampleRateHz);
1939
+ const outLength = stretched[0]?.length ?? 0;
1940
+ if (outLength === 0) {
1941
+ return;
1942
+ }
1943
+ const outBuffer = context.createBuffer(channels, outLength, chunk.format.sampleRateHz);
1944
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1945
+ const channelOut = stretched[channelIndex];
1946
+ if (!channelOut)
1947
+ continue;
1948
+ outBuffer.getChannelData(channelIndex).set(channelOut);
1949
+ }
1950
+ scheduleBuffer(context, outBuffer, 1);
1951
+ };
1771
1952
  const stopQueuedPlayback = (options2) => {
1772
1953
  for (const node of [...sourceNodes]) {
1773
1954
  node.stop?.();
package/package.json CHANGED
@@ -1,157 +1,157 @@
1
1
  {
2
- "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.576",
4
- "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/absolutejs/voice.git"
8
- },
9
- "files": [
10
- "dist",
11
- "README.md"
12
- ],
13
- "main": "./dist/index.js",
14
- "types": "./dist/index.d.ts",
15
- "license": "BSL-1.1",
16
- "author": "Alex Kahn",
17
- "scripts": {
18
- "build": "rm -rf dist && bun build ./src/index.ts ./src/client/index.ts ./src/react/index.ts ./src/vue/index.ts ./src/svelte/index.ts ./src/angular/index.ts ./src/testing/index.ts ./src/drizzle/index.ts --outdir dist --target bun --external elysia --external react --external vue --external @angular/core --external @absolutejs/absolute --external @absolutejs/ai --external @absolutejs/media --external drizzle-orm && bun build ./src/client/htmxBootstrap.ts --outdir dist/client --target browser --format esm && bun build ./src/embed/index.ts --outfile dist/embed/voice-widget.js --target browser --format iife --minify && bun build ./src/embed/index.ts --outdir dist/embed --target browser --format esm && tsc --emitDeclarationOnly --project tsconfig.json",
19
- "config": "absolute config",
20
- "format": "absolute prettier --write",
21
- "knip": "knip",
22
- "lint": "absolute eslint",
23
- "release": "bun run format && bun run build && bun publish",
24
- "test": "bun test ./test/*.test.ts",
25
- "test:adapters": "bun test ./test/live/*.test.ts",
26
- "test:assemblyai": "bun test ./test/live/assemblyai.live.test.ts",
27
- "test:deepgram": "bun test ./test/live/deepgram.live.test.ts",
28
- "test:elevenlabs": "bun test ./test/live/elevenlabs.live.test.ts",
29
- "test:openai": "bun test ./test/live/openai.live.test.ts",
30
- "typecheck": "absolute typecheck"
31
- },
32
- "exports": {
33
- ".": {
34
- "import": "./dist/index.js",
35
- "types": "./dist/index.d.ts"
36
- },
37
- "./client": {
38
- "browser": "./dist/client/index.js",
39
- "import": "./dist/client/index.js",
40
- "types": "./dist/client/index.d.ts"
41
- },
42
- "./react": {
43
- "browser": "./dist/react/index.js",
44
- "import": "./dist/react/index.js",
45
- "types": "./dist/react/index.d.ts"
46
- },
47
- "./vue": {
48
- "browser": "./dist/vue/index.js",
49
- "import": "./dist/vue/index.js",
50
- "types": "./dist/vue/index.d.ts"
51
- },
52
- "./svelte": {
53
- "import": "./dist/svelte/index.js",
54
- "types": "./dist/svelte/index.d.ts"
55
- },
56
- "./angular": {
57
- "import": "./dist/angular/index.js",
58
- "types": "./dist/angular/index.d.ts"
59
- },
60
- "./drizzle": {
61
- "import": "./dist/drizzle/index.js",
62
- "types": "./dist/drizzle/index.d.ts",
63
- "default": "./dist/drizzle/index.js"
64
- },
65
- "./testing": {
66
- "import": "./dist/testing/index.js",
67
- "types": "./dist/testing/index.d.ts"
68
- },
69
- "./embed": {
70
- "browser": "./dist/embed/index.js",
71
- "import": "./dist/embed/index.js",
72
- "types": "./dist/embed/index.d.ts"
73
- },
74
- "./embed/voice-widget.js": "./dist/embed/voice-widget.js"
75
- },
76
- "typesVersions": {
77
- "*": {
78
- "testing": [
79
- "dist/testing/index.d.ts"
80
- ],
81
- "client": [
82
- "dist/client/index.d.ts"
83
- ],
84
- "react": [
85
- "dist/react/index.d.ts"
86
- ],
87
- "svelte": [
88
- "dist/svelte/index.d.ts"
89
- ],
90
- "vue": [
91
- "dist/vue/index.d.ts"
92
- ],
93
- "angular": [
94
- "dist/angular/index.d.ts"
95
- ],
96
- "drizzle": [
97
- "dist/drizzle/index.d.ts"
98
- ],
99
- "embed": [
100
- "dist/embed/index.d.ts"
101
- ]
102
- }
103
- },
104
- "peerDependencies": {
105
- "@absolutejs/absolute": ">=0.19.0-beta.646",
106
- "@absolutejs/ai": ">=0.0.5",
107
- "@angular/core": ">=21.0.0",
108
- "drizzle-orm": ">=1.0.0-rc.1",
109
- "elysia": ">=1.4.18",
110
- "react": ">=19.0.0",
111
- "vue": ">=3.5.0"
112
- },
113
- "peerDependenciesMeta": {
114
- "@absolutejs/ai": {
115
- "optional": true
116
- },
117
- "drizzle-orm": {
118
- "optional": true
119
- },
120
- "@angular/core": {
121
- "optional": true
122
- },
123
- "react": {
124
- "optional": true
125
- },
126
- "vue": {
127
- "optional": true
128
- }
129
- },
130
- "dependencies": {
131
- "@absolutejs/media": "0.0.1-beta.19"
132
- },
133
- "devDependencies": {
134
- "@absolutejs/absolute": "0.19.0-beta.1009",
135
- "@absolutejs/ai": "0.0.5",
136
- "@angular/core": "^21.0.0",
137
- "@electric-sql/pglite": "^0.4.5",
138
- "@eslint/js": "^10.0.1",
139
- "@stylistic/eslint-plugin": "^5.10.0",
140
- "@types/bun": "1.3.9",
141
- "@types/react": "19.2.0",
142
- "@typescript-eslint/parser": "^8.57.2",
143
- "drizzle-orm": "1.0.0-rc.3",
144
- "elysia": "1.4.18",
145
- "eslint": "^10.1.0",
146
- "eslint-plugin-absolute": "0.11.0-beta.3",
147
- "eslint-plugin-promise": "^7.2.1",
148
- "eslint-plugin-security": "^4.0.0",
149
- "globals": "^17.4.0",
150
- "knip": "^6.14.1",
151
- "prettier": "^3.4.0",
152
- "react": "19.2.1",
153
- "typescript": "^5.9.3",
154
- "typescript-eslint": "^8.57.2",
155
- "vue": "3.5.27"
156
- }
2
+ "name": "@absolutejs/voice",
3
+ "version": "0.0.22-beta.578",
4
+ "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/absolutejs/voice.git"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "license": "BSL-1.1",
16
+ "author": "Alex Kahn",
17
+ "scripts": {
18
+ "build": "rm -rf dist && bun build ./src/index.ts ./src/client/index.ts ./src/react/index.ts ./src/vue/index.ts ./src/svelte/index.ts ./src/angular/index.ts ./src/testing/index.ts ./src/drizzle/index.ts --outdir dist --target bun --external elysia --external react --external vue --external @angular/core --external @absolutejs/absolute --external @absolutejs/ai --external @absolutejs/media --external drizzle-orm && bun build ./src/client/htmxBootstrap.ts --outdir dist/client --target browser --format esm && bun build ./src/embed/index.ts --outfile dist/embed/voice-widget.js --target browser --format iife --minify && bun build ./src/embed/index.ts --outdir dist/embed --target browser --format esm && tsc --emitDeclarationOnly --project tsconfig.json",
19
+ "config": "absolute config",
20
+ "format": "absolute prettier --write",
21
+ "knip": "knip",
22
+ "lint": "absolute eslint",
23
+ "release": "bun run format && bun run build && bun publish",
24
+ "test": "bun test ./test/*.test.ts",
25
+ "test:adapters": "bun test ./test/live/*.test.ts",
26
+ "test:assemblyai": "bun test ./test/live/assemblyai.live.test.ts",
27
+ "test:deepgram": "bun test ./test/live/deepgram.live.test.ts",
28
+ "test:elevenlabs": "bun test ./test/live/elevenlabs.live.test.ts",
29
+ "test:openai": "bun test ./test/live/openai.live.test.ts",
30
+ "typecheck": "absolute typecheck"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "import": "./dist/index.js",
35
+ "types": "./dist/index.d.ts"
36
+ },
37
+ "./client": {
38
+ "browser": "./dist/client/index.js",
39
+ "import": "./dist/client/index.js",
40
+ "types": "./dist/client/index.d.ts"
41
+ },
42
+ "./react": {
43
+ "browser": "./dist/react/index.js",
44
+ "import": "./dist/react/index.js",
45
+ "types": "./dist/react/index.d.ts"
46
+ },
47
+ "./vue": {
48
+ "browser": "./dist/vue/index.js",
49
+ "import": "./dist/vue/index.js",
50
+ "types": "./dist/vue/index.d.ts"
51
+ },
52
+ "./svelte": {
53
+ "import": "./dist/svelte/index.js",
54
+ "types": "./dist/svelte/index.d.ts"
55
+ },
56
+ "./angular": {
57
+ "import": "./dist/angular/index.js",
58
+ "types": "./dist/angular/index.d.ts"
59
+ },
60
+ "./drizzle": {
61
+ "import": "./dist/drizzle/index.js",
62
+ "types": "./dist/drizzle/index.d.ts",
63
+ "default": "./dist/drizzle/index.js"
64
+ },
65
+ "./testing": {
66
+ "import": "./dist/testing/index.js",
67
+ "types": "./dist/testing/index.d.ts"
68
+ },
69
+ "./embed": {
70
+ "browser": "./dist/embed/index.js",
71
+ "import": "./dist/embed/index.js",
72
+ "types": "./dist/embed/index.d.ts"
73
+ },
74
+ "./embed/voice-widget.js": "./dist/embed/voice-widget.js"
75
+ },
76
+ "typesVersions": {
77
+ "*": {
78
+ "testing": [
79
+ "dist/testing/index.d.ts"
80
+ ],
81
+ "client": [
82
+ "dist/client/index.d.ts"
83
+ ],
84
+ "react": [
85
+ "dist/react/index.d.ts"
86
+ ],
87
+ "svelte": [
88
+ "dist/svelte/index.d.ts"
89
+ ],
90
+ "vue": [
91
+ "dist/vue/index.d.ts"
92
+ ],
93
+ "angular": [
94
+ "dist/angular/index.d.ts"
95
+ ],
96
+ "drizzle": [
97
+ "dist/drizzle/index.d.ts"
98
+ ],
99
+ "embed": [
100
+ "dist/embed/index.d.ts"
101
+ ]
102
+ }
103
+ },
104
+ "peerDependencies": {
105
+ "@absolutejs/absolute": ">=0.19.0-beta.646",
106
+ "@absolutejs/ai": ">=0.0.5",
107
+ "@angular/core": ">=21.0.0",
108
+ "drizzle-orm": ">=1.0.0-rc.1",
109
+ "elysia": ">=1.4.18",
110
+ "react": ">=19.0.0",
111
+ "vue": ">=3.5.0"
112
+ },
113
+ "peerDependenciesMeta": {
114
+ "@absolutejs/ai": {
115
+ "optional": true
116
+ },
117
+ "drizzle-orm": {
118
+ "optional": true
119
+ },
120
+ "@angular/core": {
121
+ "optional": true
122
+ },
123
+ "react": {
124
+ "optional": true
125
+ },
126
+ "vue": {
127
+ "optional": true
128
+ }
129
+ },
130
+ "dependencies": {
131
+ "@absolutejs/media": "0.0.1-beta.19"
132
+ },
133
+ "devDependencies": {
134
+ "@absolutejs/absolute": "0.19.0-beta.1009",
135
+ "@absolutejs/ai": "0.0.5",
136
+ "@angular/core": "^21.0.0",
137
+ "@electric-sql/pglite": "^0.4.5",
138
+ "@eslint/js": "^10.0.1",
139
+ "@stylistic/eslint-plugin": "^5.10.0",
140
+ "@types/bun": "1.3.9",
141
+ "@types/react": "19.2.0",
142
+ "@typescript-eslint/parser": "^8.57.2",
143
+ "drizzle-orm": "1.0.0-rc.3",
144
+ "elysia": "1.4.18",
145
+ "eslint": "^10.1.0",
146
+ "eslint-plugin-absolute": "0.11.0-beta.3",
147
+ "eslint-plugin-promise": "^7.2.1",
148
+ "eslint-plugin-security": "^4.0.0",
149
+ "globals": "^17.4.0",
150
+ "knip": "^6.14.1",
151
+ "prettier": "^3.4.0",
152
+ "react": "19.2.1",
153
+ "typescript": "^5.9.3",
154
+ "typescript-eslint": "^8.57.2",
155
+ "vue": "3.5.27"
156
+ }
157
157
  }