@lokutor/sdk 1.1.43 → 1.2.1
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 -5
- package/dist/index.d.ts +150 -5
- package/dist/index.js +339 -15
- package/dist/index.mjs +334 -14
- package/package.json +1 -1
- package/src/browser-audio.ts +32 -5
- package/src/client.ts +219 -0
- package/src/conversational-panel.ts +56 -12
- 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;
|
|
@@ -1592,12 +1873,47 @@ var PANEL_CSS = (
|
|
|
1592
1873
|
}
|
|
1593
1874
|
.cv-error.is-visible { display: flex; }
|
|
1594
1875
|
.cv-error-icon { color: var(--cv-accent); flex-shrink: 0; }
|
|
1876
|
+
.cv-voice-picker {
|
|
1877
|
+
display: flex;
|
|
1878
|
+
flex-wrap: wrap;
|
|
1879
|
+
justify-content: center;
|
|
1880
|
+
gap: 0.35rem;
|
|
1881
|
+
z-index: 2;
|
|
1882
|
+
}
|
|
1883
|
+
.cv-voice-btn {
|
|
1884
|
+
padding: 0.25rem 0.5rem;
|
|
1885
|
+
border-radius: 100px;
|
|
1886
|
+
border: 1px solid rgba(255,255,255,0.15);
|
|
1887
|
+
background: rgba(255,255,255,0.05);
|
|
1888
|
+
color: rgba(255,255,255,0.6);
|
|
1889
|
+
font-size: 0.65rem;
|
|
1890
|
+
font-weight: 600;
|
|
1891
|
+
cursor: pointer;
|
|
1892
|
+
transition: all 0.2s ease;
|
|
1893
|
+
}
|
|
1894
|
+
.cv-voice-btn:hover {
|
|
1895
|
+
background: rgba(255,255,255,0.12);
|
|
1896
|
+
color: #fff;
|
|
1897
|
+
}
|
|
1898
|
+
.cv-voice-btn.is-selected {
|
|
1899
|
+
background: #fff;
|
|
1900
|
+
color: #000;
|
|
1901
|
+
border-color: #fff;
|
|
1902
|
+
}
|
|
1903
|
+
.cv-voice-label {
|
|
1904
|
+
font-size: 0.55rem;
|
|
1905
|
+
text-transform: uppercase;
|
|
1906
|
+
letter-spacing: 0.08em;
|
|
1907
|
+
color: rgba(255,255,255,0.35);
|
|
1908
|
+
z-index: 2;
|
|
1909
|
+
}
|
|
1595
1910
|
|
|
1596
1911
|
/* === COMPACT MODE: < 300px width === */
|
|
1597
1912
|
@container (max-width: 299px) {
|
|
1598
1913
|
.cv-curtain-content { padding: 0.5rem; gap: 0.3rem; }
|
|
1599
1914
|
.cv-curtain-title { font-size: 0.8rem; }
|
|
1600
1915
|
.cv-curtain-desc { display: none; }
|
|
1916
|
+
.cv-voice-picker { display: none; }
|
|
1601
1917
|
.cv-curtain-btn { padding: 0.3rem 0.6rem; font-size: 0.6rem; gap: 0.3rem; }
|
|
1602
1918
|
.cv-curtain-btn svg { display: none; }
|
|
1603
1919
|
.cv-visualizer-wrap { width: 60px; height: 60px; }
|
|
@@ -1682,11 +1998,13 @@ var ConversationalPanel = class {
|
|
|
1682
1998
|
isRunning = false;
|
|
1683
1999
|
_locked = false;
|
|
1684
2000
|
_lastSpeechTime = 0;
|
|
1685
|
-
|
|
2001
|
+
selectedVoice = "M1";
|
|
1686
2002
|
el;
|
|
1687
2003
|
curtain;
|
|
1688
2004
|
curtainTitle;
|
|
1689
2005
|
curtainDesc;
|
|
2006
|
+
curtainBg;
|
|
2007
|
+
curtainOverlay;
|
|
1690
2008
|
startBtn;
|
|
1691
2009
|
errorEl;
|
|
1692
2010
|
errorText;
|
|
@@ -1706,6 +2024,7 @@ var ConversationalPanel = class {
|
|
|
1706
2024
|
constructor(cfg) {
|
|
1707
2025
|
this.cfg = cfg;
|
|
1708
2026
|
this.container = cfg.container;
|
|
2027
|
+
this.selectedVoice = cfg.voice || "M1";
|
|
1709
2028
|
injectStyles();
|
|
1710
2029
|
this.buildDOM();
|
|
1711
2030
|
}
|
|
@@ -1721,9 +2040,9 @@ var ConversationalPanel = class {
|
|
|
1721
2040
|
<div class="cv-curtain-bg"></div>
|
|
1722
2041
|
<div class="cv-curtain-overlay"></div>
|
|
1723
2042
|
<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
|
-
|
|
2043
|
+
<h3 class="cv-curtain-title">${this.esc(this.cfg.title || "Voice Chat")}</h3>
|
|
2044
|
+
<p class="cv-curtain-desc">${this.esc(this.cfg.description || "")}</p>
|
|
2045
|
+
<button class="cv-curtain-btn">
|
|
1727
2046
|
<span>Start Conversation</span>
|
|
1728
2047
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="20" height="20">
|
|
1729
2048
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
|
@@ -1741,7 +2060,7 @@ var ConversationalPanel = class {
|
|
|
1741
2060
|
</div>
|
|
1742
2061
|
</div>
|
|
1743
2062
|
<div class="cv-header">
|
|
1744
|
-
<h2 class="cv-title">${this.esc(this.cfg.title)} <span class="cv-timer">00:00</span></h2>
|
|
2063
|
+
<h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")} <span class="cv-timer">00:00</span></h2>
|
|
1745
2064
|
</div>
|
|
1746
2065
|
<div class="cv-visualizer-wrap">
|
|
1747
2066
|
<canvas class="cv-canvas"></canvas>
|
|
@@ -1819,7 +2138,7 @@ var ConversationalPanel = class {
|
|
|
1819
2138
|
apiKey: this.cfg.apiKey,
|
|
1820
2139
|
tools: this.cfg.tools,
|
|
1821
2140
|
prompt: this.cfg.prompt,
|
|
1822
|
-
voice: this.
|
|
2141
|
+
voice: this.selectedVoice,
|
|
1823
2142
|
language: this.cfg.language || "en",
|
|
1824
2143
|
onStatusChange: (status) => {
|
|
1825
2144
|
this.el.classList.remove("cv-is-speaking", "cv-is-thinking");
|
|
@@ -1952,6 +2271,7 @@ var ConversationalPanel = class {
|
|
|
1952
2271
|
const h2 = this.el.querySelector(".cv-title");
|
|
1953
2272
|
if (h2) h2.innerHTML = `${this.esc(title)} <span class="cv-timer">${this.timerEl?.textContent || "00:00"}</span>`;
|
|
1954
2273
|
}
|
|
2274
|
+
/** Select a voice from the picker */
|
|
1955
2275
|
/** Update description text */
|
|
1956
2276
|
setDescription(desc) {
|
|
1957
2277
|
this.cfg.description = desc;
|
|
@@ -2082,6 +2402,9 @@ var ConversationalPanel = class {
|
|
|
2082
2402
|
DEFAULT_URLS,
|
|
2083
2403
|
Language,
|
|
2084
2404
|
LokutorError,
|
|
2405
|
+
NodeAudioManager,
|
|
2406
|
+
STTClient,
|
|
2407
|
+
SpeechToTextClient,
|
|
2085
2408
|
StreamResampler,
|
|
2086
2409
|
TTSClient,
|
|
2087
2410
|
VoiceAgentClient,
|
|
@@ -2097,5 +2420,6 @@ var ConversationalPanel = class {
|
|
|
2097
2420
|
resample,
|
|
2098
2421
|
resampleWithAntiAliasing,
|
|
2099
2422
|
simpleConversation,
|
|
2100
|
-
simpleTTS
|
|
2423
|
+
simpleTTS,
|
|
2424
|
+
simpleTranscribe
|
|
2101
2425
|
});
|