@lokutor/sdk 1.1.43 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +150 -7
- package/dist/index.d.ts +150 -7
- package/dist/index.js +340 -36
- package/dist/index.mjs +335 -35
- package/package.json +1 -1
- package/src/browser-audio.ts +32 -5
- package/src/client.ts +219 -0
- package/src/conversational-panel.ts +59 -34
- package/src/index.ts +15 -1
- package/src/node-audio.ts +15 -3
- package/src/types.ts +59 -0
package/dist/index.js
CHANGED
|
@@ -26,6 +26,9 @@ __export(index_exports, {
|
|
|
26
26
|
DEFAULT_URLS: () => DEFAULT_URLS,
|
|
27
27
|
Language: () => Language,
|
|
28
28
|
LokutorError: () => LokutorError,
|
|
29
|
+
NodeAudioManager: () => NodeAudioManager,
|
|
30
|
+
STTClient: () => STTClient,
|
|
31
|
+
SpeechToTextClient: () => SpeechToTextClient,
|
|
29
32
|
StreamResampler: () => StreamResampler,
|
|
30
33
|
TTSClient: () => TTSClient,
|
|
31
34
|
VoiceAgentClient: () => VoiceAgentClient,
|
|
@@ -41,7 +44,8 @@ __export(index_exports, {
|
|
|
41
44
|
resample: () => resample,
|
|
42
45
|
resampleWithAntiAliasing: () => resampleWithAntiAliasing,
|
|
43
46
|
simpleConversation: () => simpleConversation,
|
|
44
|
-
simpleTTS: () => simpleTTS
|
|
47
|
+
simpleTTS: () => simpleTTS,
|
|
48
|
+
simpleTranscribe: () => simpleTranscribe
|
|
45
49
|
});
|
|
46
50
|
module.exports = __toCommonJS(index_exports);
|
|
47
51
|
|
|
@@ -107,7 +111,8 @@ var AUDIO_CONFIG = {
|
|
|
107
111
|
};
|
|
108
112
|
var DEFAULT_URLS = {
|
|
109
113
|
VOICE_AGENT: "wss://api.lokutor.com/ws/agent",
|
|
110
|
-
TTS: "wss://api.lokutor.com/ws/tts"
|
|
114
|
+
TTS: "wss://api.lokutor.com/ws/tts",
|
|
115
|
+
STT: "wss://api.lokutor.com/ws/stt"
|
|
111
116
|
};
|
|
112
117
|
var LokutorError = class extends Error {
|
|
113
118
|
code;
|
|
@@ -287,6 +292,10 @@ var BrowserAudioManager = class {
|
|
|
287
292
|
mediaStreamAudioSourceNode = null;
|
|
288
293
|
scriptProcessor = null;
|
|
289
294
|
analyserNode = null;
|
|
295
|
+
// Reused across getAmplitude() calls instead of allocating a new
|
|
296
|
+
// Uint8Array on every call — this runs once per animation frame (~60/sec)
|
|
297
|
+
// from the visualizer, on the same main thread as audio capture.
|
|
298
|
+
amplitudeBuffer = null;
|
|
290
299
|
mediaStream = null;
|
|
291
300
|
resampler = null;
|
|
292
301
|
// Playback scheduling
|
|
@@ -353,7 +362,7 @@ var BrowserAudioManager = class {
|
|
|
353
362
|
}
|
|
354
363
|
});
|
|
355
364
|
this.mediaStreamAudioSourceNode = this.audioContext.createMediaStreamSource(this.mediaStream);
|
|
356
|
-
const bufferSize =
|
|
365
|
+
const bufferSize = 1024;
|
|
357
366
|
this.scriptProcessor = this.audioContext.createScriptProcessor(
|
|
358
367
|
bufferSize,
|
|
359
368
|
1,
|
|
@@ -387,7 +396,6 @@ var BrowserAudioManager = class {
|
|
|
387
396
|
*/
|
|
388
397
|
_processAudioInput(event) {
|
|
389
398
|
if (!this.onAudioInput || !this.audioContext || !this.isListening) return;
|
|
390
|
-
if (this.isMuted) return;
|
|
391
399
|
const inputBuffer = event.inputBuffer;
|
|
392
400
|
const inputData = inputBuffer.getChannelData(0);
|
|
393
401
|
const outputBuffer = event.outputBuffer;
|
|
@@ -399,6 +407,9 @@ var BrowserAudioManager = class {
|
|
|
399
407
|
processedData = this.resampler.process(processedData);
|
|
400
408
|
}
|
|
401
409
|
if (processedData.length === 0) return;
|
|
410
|
+
if (this.isMuted) {
|
|
411
|
+
processedData = new Float32Array(processedData.length);
|
|
412
|
+
}
|
|
402
413
|
const int16Data = float32ToPcm16(processedData);
|
|
403
414
|
const uint8Data = new Uint8Array(
|
|
404
415
|
int16Data.buffer,
|
|
@@ -522,9 +533,11 @@ var BrowserAudioManager = class {
|
|
|
522
533
|
*/
|
|
523
534
|
getAmplitude() {
|
|
524
535
|
if (!this.analyserNode) return 0;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
536
|
+
if (!this.amplitudeBuffer || this.amplitudeBuffer.length !== this.analyserNode.frequencyBinCount) {
|
|
537
|
+
this.amplitudeBuffer = new Uint8Array(this.analyserNode.frequencyBinCount);
|
|
538
|
+
}
|
|
539
|
+
this.analyserNode.getByteTimeDomainData(this.amplitudeBuffer);
|
|
540
|
+
const rms = calculateRMS(this.amplitudeBuffer);
|
|
528
541
|
return Math.min(rms * 10, 1);
|
|
529
542
|
}
|
|
530
543
|
/**
|
|
@@ -631,6 +644,14 @@ function base64ToUint8Array(base64) {
|
|
|
631
644
|
}
|
|
632
645
|
return bytes;
|
|
633
646
|
}
|
|
647
|
+
function uint8ArrayToBase64(bytes) {
|
|
648
|
+
let binaryString = "";
|
|
649
|
+
const chunkSize = 32768;
|
|
650
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
651
|
+
binaryString += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
|
652
|
+
}
|
|
653
|
+
return btoa(binaryString);
|
|
654
|
+
}
|
|
634
655
|
function normalizeVisemes(payload) {
|
|
635
656
|
if (!Array.isArray(payload)) return [];
|
|
636
657
|
const normalized = [];
|
|
@@ -667,6 +688,7 @@ function extractVisemePayload(msg) {
|
|
|
667
688
|
var VoiceAgentClient = class {
|
|
668
689
|
ws = null;
|
|
669
690
|
apiKey;
|
|
691
|
+
agentId = "";
|
|
670
692
|
prompt;
|
|
671
693
|
voice;
|
|
672
694
|
language;
|
|
@@ -699,6 +721,7 @@ var VoiceAgentClient = class {
|
|
|
699
721
|
serverUrl;
|
|
700
722
|
constructor(config) {
|
|
701
723
|
this.apiKey = config.apiKey;
|
|
724
|
+
this.agentId = config.agentId || "";
|
|
702
725
|
this.prompt = config.prompt;
|
|
703
726
|
this.voice = config.voice || "F1" /* F1 */;
|
|
704
727
|
this.language = config.language || "en" /* ENGLISH */;
|
|
@@ -743,6 +766,10 @@ var VoiceAgentClient = class {
|
|
|
743
766
|
const separator = url.includes("?") ? "&" : "?";
|
|
744
767
|
url += `${separator}api_key=${this.apiKey}`;
|
|
745
768
|
}
|
|
769
|
+
if (this.agentId) {
|
|
770
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
771
|
+
url += `${separator}agent_id=${this.agentId}`;
|
|
772
|
+
}
|
|
746
773
|
const redactedUrl = url.replace(/api_key=[^&]+/, "api_key=***");
|
|
747
774
|
sdkTrace("ws.connect", {
|
|
748
775
|
endpoint: this.serverUrl,
|
|
@@ -1362,6 +1389,164 @@ var TTSClient = class {
|
|
|
1362
1389
|
});
|
|
1363
1390
|
}
|
|
1364
1391
|
};
|
|
1392
|
+
var STTClient = class {
|
|
1393
|
+
apiKey;
|
|
1394
|
+
baseUrl;
|
|
1395
|
+
constructor(config) {
|
|
1396
|
+
this.apiKey = config.apiKey;
|
|
1397
|
+
this.baseUrl = wsToHttp(config.serverUrl || DEFAULT_URLS.STT).replace(/\/ws\/stt\/?$/, "");
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Transcribe a complete audio clip. Pass a Blob/File (e.g. from a file
|
|
1401
|
+
* input or MediaRecorder) for WAV/compressed audio, or a raw PCM16
|
|
1402
|
+
* buffer with `format: "pcm16"` and `sampleRate` set.
|
|
1403
|
+
*/
|
|
1404
|
+
async transcribe(options) {
|
|
1405
|
+
const url = `${this.baseUrl}/stt/transcribe`;
|
|
1406
|
+
let res;
|
|
1407
|
+
if (typeof Blob !== "undefined" && options.audio instanceof Blob) {
|
|
1408
|
+
const form = new FormData();
|
|
1409
|
+
form.append("audio", options.audio, "audio.wav");
|
|
1410
|
+
if (options.language) form.append("lang", options.language);
|
|
1411
|
+
if (options.sampleRate) form.append("sample_rate", String(options.sampleRate));
|
|
1412
|
+
res = await fetch(url, {
|
|
1413
|
+
method: "POST",
|
|
1414
|
+
headers: { "X-API-Key": this.apiKey },
|
|
1415
|
+
body: form
|
|
1416
|
+
});
|
|
1417
|
+
} else {
|
|
1418
|
+
const bytes = options.audio instanceof Uint8Array ? options.audio : new Uint8Array(options.audio);
|
|
1419
|
+
res = await fetch(url, {
|
|
1420
|
+
method: "POST",
|
|
1421
|
+
headers: { "X-API-Key": this.apiKey, "Content-Type": "application/json" },
|
|
1422
|
+
body: JSON.stringify({
|
|
1423
|
+
audio: uint8ArrayToBase64(bytes),
|
|
1424
|
+
format: options.format || "pcm16",
|
|
1425
|
+
sample_rate: options.sampleRate,
|
|
1426
|
+
lang: options.language
|
|
1427
|
+
})
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
if (!res.ok) {
|
|
1431
|
+
const detail = await res.text().catch(() => "");
|
|
1432
|
+
throw new LokutorError("internal.error", `HTTP ${res.status} from ${url}`, {
|
|
1433
|
+
detail,
|
|
1434
|
+
retryable: res.status >= 500
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
const data = await res.json();
|
|
1438
|
+
return {
|
|
1439
|
+
text: data.text ?? "",
|
|
1440
|
+
latencyMs: data.latency_ms ?? 0,
|
|
1441
|
+
engine: data.engine ?? "",
|
|
1442
|
+
sampleRate: data.sample_rate ?? 0,
|
|
1443
|
+
language: data.language ?? "",
|
|
1444
|
+
durationSeconds: data.duration_seconds ?? 0,
|
|
1445
|
+
segments: data.segments
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
var SpeechToTextClient = class {
|
|
1450
|
+
apiKey;
|
|
1451
|
+
serverUrl;
|
|
1452
|
+
language;
|
|
1453
|
+
vad;
|
|
1454
|
+
onPartialTranscript;
|
|
1455
|
+
onFinalTranscript;
|
|
1456
|
+
onError;
|
|
1457
|
+
onStatusChange;
|
|
1458
|
+
ws = null;
|
|
1459
|
+
audioManager = null;
|
|
1460
|
+
isConnected = false;
|
|
1461
|
+
constructor(config) {
|
|
1462
|
+
this.apiKey = config.apiKey;
|
|
1463
|
+
this.serverUrl = config.serverUrl || DEFAULT_URLS.STT;
|
|
1464
|
+
this.language = config.language;
|
|
1465
|
+
this.vad = config.vad || "silero";
|
|
1466
|
+
this.onPartialTranscript = config.onPartialTranscript;
|
|
1467
|
+
this.onFinalTranscript = config.onFinalTranscript;
|
|
1468
|
+
this.onError = config.onError;
|
|
1469
|
+
this.onStatusChange = config.onStatusChange;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Connect and start streaming microphone audio.
|
|
1473
|
+
* @param customAudioManager Optional replacement for the default audio hardware handler (e.g. NodeAudioManager for CLI use)
|
|
1474
|
+
*/
|
|
1475
|
+
async connect(customAudioManager) {
|
|
1476
|
+
this.audioManager = customAudioManager || (typeof window !== "undefined" ? new BrowserAudioManager() : null);
|
|
1477
|
+
if (!this.audioManager) {
|
|
1478
|
+
throw new LokutorError("internal.error", "No audio manager available \u2014 pass one explicitly outside the browser (e.g. NodeAudioManager).");
|
|
1479
|
+
}
|
|
1480
|
+
await this.audioManager.init();
|
|
1481
|
+
this.onStatusChange?.("connecting");
|
|
1482
|
+
return new Promise((resolve, reject) => {
|
|
1483
|
+
let settled = false;
|
|
1484
|
+
const settle = (fn) => {
|
|
1485
|
+
if (!settled) {
|
|
1486
|
+
settled = true;
|
|
1487
|
+
fn();
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
try {
|
|
1491
|
+
let url = this.serverUrl;
|
|
1492
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
1493
|
+
url += `${separator}api_key=${this.apiKey}`;
|
|
1494
|
+
this.ws = new WebSocket(url);
|
|
1495
|
+
this.ws.onopen = async () => {
|
|
1496
|
+
this.isConnected = true;
|
|
1497
|
+
this.onStatusChange?.("connected");
|
|
1498
|
+
this.ws.send(JSON.stringify({ lang: this.language || "en" /* ENGLISH */, vad: this.vad }));
|
|
1499
|
+
await this.audioManager.startMicrophone((data) => {
|
|
1500
|
+
if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
|
|
1501
|
+
this.ws.send(data);
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
settle(() => resolve(true));
|
|
1505
|
+
};
|
|
1506
|
+
this.ws.onmessage = (event) => {
|
|
1507
|
+
if (typeof event.data !== "string") return;
|
|
1508
|
+
try {
|
|
1509
|
+
const msg = JSON.parse(event.data);
|
|
1510
|
+
if (msg.type === "transcript") {
|
|
1511
|
+
if (msg.isFinal) {
|
|
1512
|
+
this.onFinalTranscript?.(msg.data ?? "");
|
|
1513
|
+
} else {
|
|
1514
|
+
this.onPartialTranscript?.(msg.data ?? "");
|
|
1515
|
+
}
|
|
1516
|
+
} else if (msg.type === "error") {
|
|
1517
|
+
this.onError?.(new LokutorError("internal.error", msg.data ?? "STT error"));
|
|
1518
|
+
}
|
|
1519
|
+
} catch {
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
this.ws.onerror = (err) => {
|
|
1523
|
+
const lokutorErr = new LokutorError("internal.error", "WebSocket error", { original: err });
|
|
1524
|
+
this.onError?.(lokutorErr);
|
|
1525
|
+
settle(() => reject(lokutorErr));
|
|
1526
|
+
};
|
|
1527
|
+
this.ws.onclose = () => {
|
|
1528
|
+
this.isConnected = false;
|
|
1529
|
+
this.onStatusChange?.("disconnected");
|
|
1530
|
+
};
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
settle(() => reject(err));
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
/** Force-finalize whatever utterance is currently in progress. */
|
|
1537
|
+
endUtterance() {
|
|
1538
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1539
|
+
this.ws.send(JSON.stringify({ type: "end" }));
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
disconnect() {
|
|
1543
|
+
this.isConnected = false;
|
|
1544
|
+
this.audioManager?.stopMicrophone();
|
|
1545
|
+
this.audioManager?.cleanup();
|
|
1546
|
+
this.ws?.close();
|
|
1547
|
+
this.ws = null;
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1365
1550
|
async function simpleConversation(config) {
|
|
1366
1551
|
const client = new VoiceAgentClient(config);
|
|
1367
1552
|
await client.connect();
|
|
@@ -1371,6 +1556,102 @@ async function simpleTTS(options) {
|
|
|
1371
1556
|
const client = new TTSClient({ apiKey: options.apiKey });
|
|
1372
1557
|
return client.synthesize(options);
|
|
1373
1558
|
}
|
|
1559
|
+
async function simpleTranscribe(options) {
|
|
1560
|
+
const client = new STTClient({ apiKey: options.apiKey, serverUrl: options.serverUrl });
|
|
1561
|
+
return client.transcribe(options);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// src/node-audio.ts
|
|
1565
|
+
function optionalImport(moduleName) {
|
|
1566
|
+
return import(moduleName);
|
|
1567
|
+
}
|
|
1568
|
+
var NodeAudioManager = class {
|
|
1569
|
+
speaker = null;
|
|
1570
|
+
recorder = null;
|
|
1571
|
+
recordingStream = null;
|
|
1572
|
+
isMuted = false;
|
|
1573
|
+
isListening = false;
|
|
1574
|
+
constructor() {
|
|
1575
|
+
}
|
|
1576
|
+
async init() {
|
|
1577
|
+
try {
|
|
1578
|
+
const Speaker = await optionalImport("speaker").catch(() => null);
|
|
1579
|
+
if (!Speaker) {
|
|
1580
|
+
console.warn('\u26A0\uFE0F Package "speaker" is missing. Hardware output will be disabled.');
|
|
1581
|
+
console.warn("\u{1F449} Run: npm install speaker");
|
|
1582
|
+
}
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
console.error("Error initializing Node audio:", e);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
async startMicrophone(onAudioInput) {
|
|
1588
|
+
if (this.isListening) return;
|
|
1589
|
+
try {
|
|
1590
|
+
const recorder = await optionalImport("node-record-lpcm16").catch(() => null);
|
|
1591
|
+
if (!recorder) {
|
|
1592
|
+
throw new Error('Package "node-record-lpcm16" is missing. Microphone input failed.\n\u{1F449} Run: npm install node-record-lpcm16');
|
|
1593
|
+
}
|
|
1594
|
+
console.log("\u{1F3A4} Starting microphone (Node.js)...");
|
|
1595
|
+
this.recordingStream = recorder.record({
|
|
1596
|
+
sampleRate: AUDIO_CONFIG.SAMPLE_RATE,
|
|
1597
|
+
threshold: 0,
|
|
1598
|
+
verbose: false,
|
|
1599
|
+
recordProgram: "sox"
|
|
1600
|
+
// default
|
|
1601
|
+
});
|
|
1602
|
+
this.recordingStream.stream().on("data", (chunk) => {
|
|
1603
|
+
if (!this.isMuted && onAudioInput) {
|
|
1604
|
+
onAudioInput(new Uint8Array(chunk));
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
this.isListening = true;
|
|
1608
|
+
} catch (e) {
|
|
1609
|
+
console.error("Failed to start microphone:", e.message);
|
|
1610
|
+
throw e;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
stopMicrophone() {
|
|
1614
|
+
if (this.recordingStream) {
|
|
1615
|
+
this.recordingStream.stop();
|
|
1616
|
+
this.recordingStream = null;
|
|
1617
|
+
}
|
|
1618
|
+
this.isListening = false;
|
|
1619
|
+
}
|
|
1620
|
+
async playAudio(pcm16Data) {
|
|
1621
|
+
try {
|
|
1622
|
+
if (!this.speaker) {
|
|
1623
|
+
const Speaker = (await optionalImport("speaker")).default;
|
|
1624
|
+
this.speaker = new Speaker({
|
|
1625
|
+
channels: AUDIO_CONFIG.CHANNELS,
|
|
1626
|
+
bitDepth: 16,
|
|
1627
|
+
sampleRate: AUDIO_CONFIG.SPEAKER_SAMPLE_RATE
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
this.speaker.write(Buffer.from(pcm16Data));
|
|
1631
|
+
} catch (e) {
|
|
1632
|
+
console.error("NodeAudioManager: speaker playback failed:", e);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
stopPlayback() {
|
|
1636
|
+
if (this.speaker) {
|
|
1637
|
+
this.speaker.end();
|
|
1638
|
+
this.speaker = null;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
cleanup() {
|
|
1642
|
+
this.stopMicrophone();
|
|
1643
|
+
this.stopPlayback();
|
|
1644
|
+
}
|
|
1645
|
+
isMicMuted() {
|
|
1646
|
+
return this.isMuted;
|
|
1647
|
+
}
|
|
1648
|
+
setMuted(muted) {
|
|
1649
|
+
this.isMuted = muted;
|
|
1650
|
+
}
|
|
1651
|
+
getAmplitude() {
|
|
1652
|
+
return 0;
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1374
1655
|
|
|
1375
1656
|
// src/conversational-panel.ts
|
|
1376
1657
|
var PANEL_CSS = (
|
|
@@ -1466,7 +1747,7 @@ var PANEL_CSS = (
|
|
|
1466
1747
|
}
|
|
1467
1748
|
.cv-curtain-btn svg { transition: transform 0.3s ease; }
|
|
1468
1749
|
.cv-curtain-btn:hover svg { transform: translateX(4px); }
|
|
1469
|
-
.cv-
|
|
1750
|
+
.cv-error {
|
|
1470
1751
|
display: flex;
|
|
1471
1752
|
flex-direction: column;
|
|
1472
1753
|
align-items: center;
|
|
@@ -1484,12 +1765,6 @@ var PANEL_CSS = (
|
|
|
1484
1765
|
gap: 1rem;
|
|
1485
1766
|
letter-spacing: -0.02em;
|
|
1486
1767
|
}
|
|
1487
|
-
.cv-title .cv-timer {
|
|
1488
|
-
font-variant-numeric: tabular-nums;
|
|
1489
|
-
color: var(--cv-accent);
|
|
1490
|
-
font-weight: 400;
|
|
1491
|
-
opacity: 0.8;
|
|
1492
|
-
}
|
|
1493
1768
|
.cv-visualizer-wrap {
|
|
1494
1769
|
position: absolute;
|
|
1495
1770
|
top: 50%;
|
|
@@ -1592,18 +1867,52 @@ var PANEL_CSS = (
|
|
|
1592
1867
|
}
|
|
1593
1868
|
.cv-error.is-visible { display: flex; }
|
|
1594
1869
|
.cv-error-icon { color: var(--cv-accent); flex-shrink: 0; }
|
|
1870
|
+
.cv-voice-picker {
|
|
1871
|
+
display: flex;
|
|
1872
|
+
flex-wrap: wrap;
|
|
1873
|
+
justify-content: center;
|
|
1874
|
+
gap: 0.35rem;
|
|
1875
|
+
z-index: 2;
|
|
1876
|
+
}
|
|
1877
|
+
.cv-voice-btn {
|
|
1878
|
+
padding: 0.25rem 0.5rem;
|
|
1879
|
+
border-radius: 100px;
|
|
1880
|
+
border: 1px solid rgba(255,255,255,0.15);
|
|
1881
|
+
background: rgba(255,255,255,0.05);
|
|
1882
|
+
color: rgba(255,255,255,0.6);
|
|
1883
|
+
font-size: 0.65rem;
|
|
1884
|
+
font-weight: 600;
|
|
1885
|
+
cursor: pointer;
|
|
1886
|
+
transition: all 0.2s ease;
|
|
1887
|
+
}
|
|
1888
|
+
.cv-voice-btn:hover {
|
|
1889
|
+
background: rgba(255,255,255,0.12);
|
|
1890
|
+
color: #fff;
|
|
1891
|
+
}
|
|
1892
|
+
.cv-voice-btn.is-selected {
|
|
1893
|
+
background: #fff;
|
|
1894
|
+
color: #000;
|
|
1895
|
+
border-color: #fff;
|
|
1896
|
+
}
|
|
1897
|
+
.cv-voice-label {
|
|
1898
|
+
font-size: 0.55rem;
|
|
1899
|
+
text-transform: uppercase;
|
|
1900
|
+
letter-spacing: 0.08em;
|
|
1901
|
+
color: rgba(255,255,255,0.35);
|
|
1902
|
+
z-index: 2;
|
|
1903
|
+
}
|
|
1595
1904
|
|
|
1596
1905
|
/* === COMPACT MODE: < 300px width === */
|
|
1597
1906
|
@container (max-width: 299px) {
|
|
1598
1907
|
.cv-curtain-content { padding: 0.5rem; gap: 0.3rem; }
|
|
1599
1908
|
.cv-curtain-title { font-size: 0.8rem; }
|
|
1600
1909
|
.cv-curtain-desc { display: none; }
|
|
1910
|
+
.cv-voice-picker { display: none; }
|
|
1601
1911
|
.cv-curtain-btn { padding: 0.3rem 0.6rem; font-size: 0.6rem; gap: 0.3rem; }
|
|
1602
1912
|
.cv-curtain-btn svg { display: none; }
|
|
1603
1913
|
.cv-visualizer-wrap { width: 60px; height: 60px; }
|
|
1604
1914
|
.cv-header { padding-top: 0.75rem; }
|
|
1605
1915
|
.cv-title { font-size: 0.7rem; gap: 0.3rem; }
|
|
1606
|
-
.cv-title .cv-timer { display: none; }
|
|
1607
1916
|
.cv-controls { gap: 0.6rem; padding-bottom: 0.6rem; }
|
|
1608
1917
|
.cv-pill { padding: 0.15rem 0.4rem; gap: 0.3rem; }
|
|
1609
1918
|
.cv-btn { padding: 0.2rem 0.4rem; font-size: 0.6rem; gap: 0; }
|
|
@@ -1621,7 +1930,6 @@ var PANEL_CSS = (
|
|
|
1621
1930
|
.cv-visualizer-wrap { width: clamp(80px, 35cqw, 140px); height: clamp(80px, 35cqw, 140px); }
|
|
1622
1931
|
.cv-header { padding-top: 0.8rem; }
|
|
1623
1932
|
.cv-title { font-size: clamp(0.7rem, 2.5cqw, 1rem); gap: 0.4rem; }
|
|
1624
|
-
.cv-title .cv-timer { font-size: 0.65em; }
|
|
1625
1933
|
.cv-controls { gap: clamp(0.6rem, 1.5cqw, 1rem); padding-bottom: clamp(0.6rem, 1.5cqw, 1rem); }
|
|
1626
1934
|
.cv-pill { padding: clamp(0.15rem, 0.5cqw, 0.25rem) clamp(0.35rem, 1cqw, 0.6rem); font-size: 0.65rem; gap: 0.3rem; }
|
|
1627
1935
|
.cv-btn { padding: clamp(0.2rem, 0.5cqw, 0.3rem) clamp(0.35rem, 1cqw, 0.6rem); font-size: clamp(0.6rem, 1.2cqw, 0.7rem); }
|
|
@@ -1639,7 +1947,6 @@ var PANEL_CSS = (
|
|
|
1639
1947
|
.cv-visualizer-wrap { width: clamp(100px, 40cqw, 200px); height: clamp(100px, 40cqw, 200px); }
|
|
1640
1948
|
.cv-header { padding-top: clamp(1rem, 1.5cqw, 1.5rem); }
|
|
1641
1949
|
.cv-title { font-size: clamp(0.9rem, 3cqw, 1.3rem); gap: clamp(0.4rem, 1cqw, 0.75rem); }
|
|
1642
|
-
.cv-title .cv-timer { font-size: 0.85em; }
|
|
1643
1950
|
.cv-controls { gap: clamp(0.8rem, 1.5cqw, 1.2rem); padding-bottom: clamp(0.8rem, 1.5cqw, 1.2rem); }
|
|
1644
1951
|
.cv-pill { padding: clamp(0.2rem, 0.75cqw, 0.3rem) clamp(0.5rem, 1.2cqw, 0.75rem); font-size: 0.7rem; }
|
|
1645
1952
|
.cv-btn { padding: clamp(0.25rem, 0.75cqw, 0.35rem) clamp(0.5rem, 1.2cqw, 0.75rem); font-size: clamp(0.65rem, 1.2cqw, 0.8rem); }
|
|
@@ -1655,7 +1962,6 @@ var PANEL_CSS = (
|
|
|
1655
1962
|
.cv-visualizer-wrap { width: clamp(140px, 45cqw, 300px); height: clamp(140px, 45cqw, 300px); }
|
|
1656
1963
|
.cv-header { padding-top: clamp(1.5rem, 2cqw, 2rem); }
|
|
1657
1964
|
.cv-title { font-size: clamp(1rem, 3cqw, 1.8rem); gap: clamp(0.75rem, 1.5cqw, 1.2rem); }
|
|
1658
|
-
.cv-title .cv-timer { font-size: 0.9em; }
|
|
1659
1965
|
.cv-controls { gap: clamp(1.2rem, 2cqw, 1.8rem); padding-bottom: clamp(1.2rem, 2cqw, 1.8rem); }
|
|
1660
1966
|
.cv-pill { padding: clamp(0.3rem, 0.8cqw, 0.4rem) clamp(0.75rem, 1.2cqw, 1rem); font-size: 0.85rem; gap: 0.75rem; }
|
|
1661
1967
|
.cv-btn { padding: clamp(0.35rem, 0.8cqw, 0.4rem) clamp(0.75rem, 1.2cqw, 1rem); font-size: clamp(0.75rem, 1.2cqw, 0.9rem); }
|
|
@@ -1682,15 +1988,16 @@ var ConversationalPanel = class {
|
|
|
1682
1988
|
isRunning = false;
|
|
1683
1989
|
_locked = false;
|
|
1684
1990
|
_lastSpeechTime = 0;
|
|
1685
|
-
|
|
1991
|
+
selectedVoice = "M1";
|
|
1686
1992
|
el;
|
|
1687
1993
|
curtain;
|
|
1688
1994
|
curtainTitle;
|
|
1689
1995
|
curtainDesc;
|
|
1996
|
+
curtainBg;
|
|
1997
|
+
curtainOverlay;
|
|
1690
1998
|
startBtn;
|
|
1691
1999
|
errorEl;
|
|
1692
2000
|
errorText;
|
|
1693
|
-
timerEl;
|
|
1694
2001
|
canvas;
|
|
1695
2002
|
muteBtn;
|
|
1696
2003
|
muteSvg;
|
|
@@ -1706,6 +2013,7 @@ var ConversationalPanel = class {
|
|
|
1706
2013
|
constructor(cfg) {
|
|
1707
2014
|
this.cfg = cfg;
|
|
1708
2015
|
this.container = cfg.container;
|
|
2016
|
+
this.selectedVoice = cfg.voice || "M1";
|
|
1709
2017
|
injectStyles();
|
|
1710
2018
|
this.buildDOM();
|
|
1711
2019
|
}
|
|
@@ -1721,9 +2029,9 @@ var ConversationalPanel = class {
|
|
|
1721
2029
|
<div class="cv-curtain-bg"></div>
|
|
1722
2030
|
<div class="cv-curtain-overlay"></div>
|
|
1723
2031
|
<div class="cv-curtain-content">
|
|
1724
|
-
<h3 class="cv-curtain-title">${this.esc(this.cfg.title)}</h3>
|
|
1725
|
-
<p class="cv-curtain-desc">${this.esc(this.cfg.description)}</p>
|
|
1726
|
-
|
|
2032
|
+
<h3 class="cv-curtain-title">${this.esc(this.cfg.title || "Voice Chat")}</h3>
|
|
2033
|
+
<p class="cv-curtain-desc">${this.esc(this.cfg.description || "")}</p>
|
|
2034
|
+
<button class="cv-curtain-btn">
|
|
1727
2035
|
<span>Start Conversation</span>
|
|
1728
2036
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20">
|
|
1729
2037
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
|
@@ -1741,7 +2049,7 @@ var ConversationalPanel = class {
|
|
|
1741
2049
|
</div>
|
|
1742
2050
|
</div>
|
|
1743
2051
|
<div class="cv-header">
|
|
1744
|
-
<h2 class="cv-title">${this.esc(this.cfg.title
|
|
2052
|
+
<h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")}</h2>
|
|
1745
2053
|
</div>
|
|
1746
2054
|
<div class="cv-visualizer-wrap">
|
|
1747
2055
|
<canvas class="cv-canvas"></canvas>
|
|
@@ -1772,7 +2080,6 @@ var ConversationalPanel = class {
|
|
|
1772
2080
|
this.startBtn = this.el.querySelector(".cv-curtain-btn");
|
|
1773
2081
|
this.errorEl = this.el.querySelector(".cv-error");
|
|
1774
2082
|
this.errorText = this.el.querySelector(".cv-error-text");
|
|
1775
|
-
this.timerEl = this.el.querySelector(".cv-timer");
|
|
1776
2083
|
this.canvas = this.el.querySelector(".cv-canvas");
|
|
1777
2084
|
this.muteBtn = this.el.querySelector(".cv-btn--mute");
|
|
1778
2085
|
this.muteSvg = this.muteBtn.querySelector("svg");
|
|
@@ -1819,7 +2126,7 @@ var ConversationalPanel = class {
|
|
|
1819
2126
|
apiKey: this.cfg.apiKey,
|
|
1820
2127
|
tools: this.cfg.tools,
|
|
1821
2128
|
prompt: this.cfg.prompt,
|
|
1822
|
-
voice: this.
|
|
2129
|
+
voice: this.selectedVoice,
|
|
1823
2130
|
language: this.cfg.language || "en",
|
|
1824
2131
|
onStatusChange: (status) => {
|
|
1825
2132
|
this.el.classList.remove("cv-is-speaking", "cv-is-thinking");
|
|
@@ -1950,8 +2257,9 @@ var ConversationalPanel = class {
|
|
|
1950
2257
|
this.cfg.title = title;
|
|
1951
2258
|
this.curtainTitle.textContent = title;
|
|
1952
2259
|
const h2 = this.el.querySelector(".cv-title");
|
|
1953
|
-
if (h2) h2.
|
|
2260
|
+
if (h2) h2.textContent = title;
|
|
1954
2261
|
}
|
|
2262
|
+
/** Select a voice from the picker */
|
|
1955
2263
|
/** Update description text */
|
|
1956
2264
|
setDescription(desc) {
|
|
1957
2265
|
this.cfg.description = desc;
|
|
@@ -1979,21 +2287,13 @@ var ConversationalPanel = class {
|
|
|
1979
2287
|
this.el.remove();
|
|
1980
2288
|
}
|
|
1981
2289
|
// ─── internal helpers ────────────────────────────────────
|
|
1982
|
-
fmt(s) {
|
|
1983
|
-
const m = Math.floor(s / 60).toString().padStart(2, "0");
|
|
1984
|
-
const sec = (s % 60).toString().padStart(2, "0");
|
|
1985
|
-
return `${m}:${sec}`;
|
|
1986
|
-
}
|
|
1987
2290
|
startTimer() {
|
|
1988
2291
|
let elapsed = 0;
|
|
1989
2292
|
const maxDur = this.cfg.maxDuration || 300;
|
|
1990
2293
|
const silentMax = this.cfg.silenceTimeout || 60;
|
|
1991
2294
|
this._lastSpeechTime = Date.now();
|
|
1992
|
-
this.timerEl.textContent = this.fmt(maxDur);
|
|
1993
2295
|
this.timerTicker = window.setInterval(() => {
|
|
1994
2296
|
elapsed++;
|
|
1995
|
-
const remaining = Math.max(0, maxDur - elapsed);
|
|
1996
|
-
this.timerEl.textContent = this.fmt(remaining);
|
|
1997
2297
|
if (elapsed >= maxDur) {
|
|
1998
2298
|
this._locked = true;
|
|
1999
2299
|
this.stop();
|
|
@@ -2082,6 +2382,9 @@ var ConversationalPanel = class {
|
|
|
2082
2382
|
DEFAULT_URLS,
|
|
2083
2383
|
Language,
|
|
2084
2384
|
LokutorError,
|
|
2385
|
+
NodeAudioManager,
|
|
2386
|
+
STTClient,
|
|
2387
|
+
SpeechToTextClient,
|
|
2085
2388
|
StreamResampler,
|
|
2086
2389
|
TTSClient,
|
|
2087
2390
|
VoiceAgentClient,
|
|
@@ -2097,5 +2400,6 @@ var ConversationalPanel = class {
|
|
|
2097
2400
|
resample,
|
|
2098
2401
|
resampleWithAntiAliasing,
|
|
2099
2402
|
simpleConversation,
|
|
2100
|
-
simpleTTS
|
|
2403
|
+
simpleTTS,
|
|
2404
|
+
simpleTranscribe
|
|
2101
2405
|
});
|