@lokutor/sdk 1.2.1 → 1.2.3

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 CHANGED
@@ -827,7 +827,6 @@ declare class ConversationalPanel {
827
827
  private startBtn;
828
828
  private errorEl;
829
829
  private errorText;
830
- private timerEl;
831
830
  private canvas;
832
831
  private muteBtn;
833
832
  private muteSvg;
@@ -863,7 +862,6 @@ declare class ConversationalPanel {
863
862
  showError(text: string): void;
864
863
  /** Destroy the component, removing all DOM and stopping any active session */
865
864
  destroy(): void;
866
- private fmt;
867
865
  private startTimer;
868
866
  private toggleMute;
869
867
  private playConnectingSound;
package/dist/index.d.ts CHANGED
@@ -827,7 +827,6 @@ declare class ConversationalPanel {
827
827
  private startBtn;
828
828
  private errorEl;
829
829
  private errorText;
830
- private timerEl;
831
830
  private canvas;
832
831
  private muteBtn;
833
832
  private muteSvg;
@@ -863,7 +862,6 @@ declare class ConversationalPanel {
863
862
  showError(text: string): void;
864
863
  /** Destroy the component, removing all DOM and stopping any active session */
865
864
  destroy(): void;
866
- private fmt;
867
865
  private startTimer;
868
866
  private toggleMute;
869
867
  private playConnectingSound;
package/dist/index.js CHANGED
@@ -1318,7 +1318,7 @@ var TTSClient = class {
1318
1318
  const req = {
1319
1319
  text: options.text,
1320
1320
  voice: options.voice || "F1" /* F1 */,
1321
- lang: options.language || "en" /* ENGLISH */,
1321
+ language: options.language || "en" /* ENGLISH */,
1322
1322
  speed: options.speed || 1.05,
1323
1323
  steps: options.steps || 24,
1324
1324
  visemes: options.visemes || false
@@ -1407,7 +1407,7 @@ var STTClient = class {
1407
1407
  if (typeof Blob !== "undefined" && options.audio instanceof Blob) {
1408
1408
  const form = new FormData();
1409
1409
  form.append("audio", options.audio, "audio.wav");
1410
- if (options.language) form.append("lang", options.language);
1410
+ if (options.language) form.append("language", options.language);
1411
1411
  if (options.sampleRate) form.append("sample_rate", String(options.sampleRate));
1412
1412
  res = await fetch(url, {
1413
1413
  method: "POST",
@@ -1423,7 +1423,7 @@ var STTClient = class {
1423
1423
  audio: uint8ArrayToBase64(bytes),
1424
1424
  format: options.format || "pcm16",
1425
1425
  sample_rate: options.sampleRate,
1426
- lang: options.language
1426
+ language: options.language
1427
1427
  })
1428
1428
  });
1429
1429
  }
@@ -1495,7 +1495,7 @@ var SpeechToTextClient = class {
1495
1495
  this.ws.onopen = async () => {
1496
1496
  this.isConnected = true;
1497
1497
  this.onStatusChange?.("connected");
1498
- this.ws.send(JSON.stringify({ lang: this.language || "en" /* ENGLISH */, vad: this.vad }));
1498
+ this.ws.send(JSON.stringify({ language: this.language || "en" /* ENGLISH */, vad: this.vad }));
1499
1499
  await this.audioManager.startMicrophone((data) => {
1500
1500
  if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
1501
1501
  this.ws.send(data);
@@ -1765,12 +1765,6 @@ var PANEL_CSS = (
1765
1765
  gap: 1rem;
1766
1766
  letter-spacing: -0.02em;
1767
1767
  }
1768
- .cv-title .cv-timer {
1769
- font-variant-numeric: tabular-nums;
1770
- color: var(--cv-accent);
1771
- font-weight: 400;
1772
- opacity: 0.8;
1773
- }
1774
1768
  .cv-visualizer-wrap {
1775
1769
  position: absolute;
1776
1770
  top: 50%;
@@ -1919,7 +1913,6 @@ var PANEL_CSS = (
1919
1913
  .cv-visualizer-wrap { width: 60px; height: 60px; }
1920
1914
  .cv-header { padding-top: 0.75rem; }
1921
1915
  .cv-title { font-size: 0.7rem; gap: 0.3rem; }
1922
- .cv-title .cv-timer { display: none; }
1923
1916
  .cv-controls { gap: 0.6rem; padding-bottom: 0.6rem; }
1924
1917
  .cv-pill { padding: 0.15rem 0.4rem; gap: 0.3rem; }
1925
1918
  .cv-btn { padding: 0.2rem 0.4rem; font-size: 0.6rem; gap: 0; }
@@ -1937,7 +1930,6 @@ var PANEL_CSS = (
1937
1930
  .cv-visualizer-wrap { width: clamp(80px, 35cqw, 140px); height: clamp(80px, 35cqw, 140px); }
1938
1931
  .cv-header { padding-top: 0.8rem; }
1939
1932
  .cv-title { font-size: clamp(0.7rem, 2.5cqw, 1rem); gap: 0.4rem; }
1940
- .cv-title .cv-timer { font-size: 0.65em; }
1941
1933
  .cv-controls { gap: clamp(0.6rem, 1.5cqw, 1rem); padding-bottom: clamp(0.6rem, 1.5cqw, 1rem); }
1942
1934
  .cv-pill { padding: clamp(0.15rem, 0.5cqw, 0.25rem) clamp(0.35rem, 1cqw, 0.6rem); font-size: 0.65rem; gap: 0.3rem; }
1943
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); }
@@ -1955,7 +1947,6 @@ var PANEL_CSS = (
1955
1947
  .cv-visualizer-wrap { width: clamp(100px, 40cqw, 200px); height: clamp(100px, 40cqw, 200px); }
1956
1948
  .cv-header { padding-top: clamp(1rem, 1.5cqw, 1.5rem); }
1957
1949
  .cv-title { font-size: clamp(0.9rem, 3cqw, 1.3rem); gap: clamp(0.4rem, 1cqw, 0.75rem); }
1958
- .cv-title .cv-timer { font-size: 0.85em; }
1959
1950
  .cv-controls { gap: clamp(0.8rem, 1.5cqw, 1.2rem); padding-bottom: clamp(0.8rem, 1.5cqw, 1.2rem); }
1960
1951
  .cv-pill { padding: clamp(0.2rem, 0.75cqw, 0.3rem) clamp(0.5rem, 1.2cqw, 0.75rem); font-size: 0.7rem; }
1961
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); }
@@ -1971,7 +1962,6 @@ var PANEL_CSS = (
1971
1962
  .cv-visualizer-wrap { width: clamp(140px, 45cqw, 300px); height: clamp(140px, 45cqw, 300px); }
1972
1963
  .cv-header { padding-top: clamp(1.5rem, 2cqw, 2rem); }
1973
1964
  .cv-title { font-size: clamp(1rem, 3cqw, 1.8rem); gap: clamp(0.75rem, 1.5cqw, 1.2rem); }
1974
- .cv-title .cv-timer { font-size: 0.9em; }
1975
1965
  .cv-controls { gap: clamp(1.2rem, 2cqw, 1.8rem); padding-bottom: clamp(1.2rem, 2cqw, 1.8rem); }
1976
1966
  .cv-pill { padding: clamp(0.3rem, 0.8cqw, 0.4rem) clamp(0.75rem, 1.2cqw, 1rem); font-size: 0.85rem; gap: 0.75rem; }
1977
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); }
@@ -2008,7 +1998,6 @@ var ConversationalPanel = class {
2008
1998
  startBtn;
2009
1999
  errorEl;
2010
2000
  errorText;
2011
- timerEl;
2012
2001
  canvas;
2013
2002
  muteBtn;
2014
2003
  muteSvg;
@@ -2060,7 +2049,7 @@ var ConversationalPanel = class {
2060
2049
  </div>
2061
2050
  </div>
2062
2051
  <div class="cv-header">
2063
- <h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")} <span class="cv-timer">00:00</span></h2>
2052
+ <h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")}</h2>
2064
2053
  </div>
2065
2054
  <div class="cv-visualizer-wrap">
2066
2055
  <canvas class="cv-canvas"></canvas>
@@ -2091,7 +2080,6 @@ var ConversationalPanel = class {
2091
2080
  this.startBtn = this.el.querySelector(".cv-curtain-btn");
2092
2081
  this.errorEl = this.el.querySelector(".cv-error");
2093
2082
  this.errorText = this.el.querySelector(".cv-error-text");
2094
- this.timerEl = this.el.querySelector(".cv-timer");
2095
2083
  this.canvas = this.el.querySelector(".cv-canvas");
2096
2084
  this.muteBtn = this.el.querySelector(".cv-btn--mute");
2097
2085
  this.muteSvg = this.muteBtn.querySelector("svg");
@@ -2269,7 +2257,7 @@ var ConversationalPanel = class {
2269
2257
  this.cfg.title = title;
2270
2258
  this.curtainTitle.textContent = title;
2271
2259
  const h2 = this.el.querySelector(".cv-title");
2272
- if (h2) h2.innerHTML = `${this.esc(title)} <span class="cv-timer">${this.timerEl?.textContent || "00:00"}</span>`;
2260
+ if (h2) h2.textContent = title;
2273
2261
  }
2274
2262
  /** Select a voice from the picker */
2275
2263
  /** Update description text */
@@ -2299,21 +2287,13 @@ var ConversationalPanel = class {
2299
2287
  this.el.remove();
2300
2288
  }
2301
2289
  // ─── internal helpers ────────────────────────────────────
2302
- fmt(s) {
2303
- const m = Math.floor(s / 60).toString().padStart(2, "0");
2304
- const sec = (s % 60).toString().padStart(2, "0");
2305
- return `${m}:${sec}`;
2306
- }
2307
2290
  startTimer() {
2308
2291
  let elapsed = 0;
2309
2292
  const maxDur = this.cfg.maxDuration || 300;
2310
2293
  const silentMax = this.cfg.silenceTimeout || 60;
2311
2294
  this._lastSpeechTime = Date.now();
2312
- this.timerEl.textContent = this.fmt(maxDur);
2313
2295
  this.timerTicker = window.setInterval(() => {
2314
2296
  elapsed++;
2315
- const remaining = Math.max(0, maxDur - elapsed);
2316
- this.timerEl.textContent = this.fmt(remaining);
2317
2297
  if (elapsed >= maxDur) {
2318
2298
  this._locked = true;
2319
2299
  this.stop();
package/dist/index.mjs CHANGED
@@ -1267,7 +1267,7 @@ var TTSClient = class {
1267
1267
  const req = {
1268
1268
  text: options.text,
1269
1269
  voice: options.voice || "F1" /* F1 */,
1270
- lang: options.language || "en" /* ENGLISH */,
1270
+ language: options.language || "en" /* ENGLISH */,
1271
1271
  speed: options.speed || 1.05,
1272
1272
  steps: options.steps || 24,
1273
1273
  visemes: options.visemes || false
@@ -1356,7 +1356,7 @@ var STTClient = class {
1356
1356
  if (typeof Blob !== "undefined" && options.audio instanceof Blob) {
1357
1357
  const form = new FormData();
1358
1358
  form.append("audio", options.audio, "audio.wav");
1359
- if (options.language) form.append("lang", options.language);
1359
+ if (options.language) form.append("language", options.language);
1360
1360
  if (options.sampleRate) form.append("sample_rate", String(options.sampleRate));
1361
1361
  res = await fetch(url, {
1362
1362
  method: "POST",
@@ -1372,7 +1372,7 @@ var STTClient = class {
1372
1372
  audio: uint8ArrayToBase64(bytes),
1373
1373
  format: options.format || "pcm16",
1374
1374
  sample_rate: options.sampleRate,
1375
- lang: options.language
1375
+ language: options.language
1376
1376
  })
1377
1377
  });
1378
1378
  }
@@ -1444,7 +1444,7 @@ var SpeechToTextClient = class {
1444
1444
  this.ws.onopen = async () => {
1445
1445
  this.isConnected = true;
1446
1446
  this.onStatusChange?.("connected");
1447
- this.ws.send(JSON.stringify({ lang: this.language || "en" /* ENGLISH */, vad: this.vad }));
1447
+ this.ws.send(JSON.stringify({ language: this.language || "en" /* ENGLISH */, vad: this.vad }));
1448
1448
  await this.audioManager.startMicrophone((data) => {
1449
1449
  if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
1450
1450
  this.ws.send(data);
@@ -1714,12 +1714,6 @@ var PANEL_CSS = (
1714
1714
  gap: 1rem;
1715
1715
  letter-spacing: -0.02em;
1716
1716
  }
1717
- .cv-title .cv-timer {
1718
- font-variant-numeric: tabular-nums;
1719
- color: var(--cv-accent);
1720
- font-weight: 400;
1721
- opacity: 0.8;
1722
- }
1723
1717
  .cv-visualizer-wrap {
1724
1718
  position: absolute;
1725
1719
  top: 50%;
@@ -1868,7 +1862,6 @@ var PANEL_CSS = (
1868
1862
  .cv-visualizer-wrap { width: 60px; height: 60px; }
1869
1863
  .cv-header { padding-top: 0.75rem; }
1870
1864
  .cv-title { font-size: 0.7rem; gap: 0.3rem; }
1871
- .cv-title .cv-timer { display: none; }
1872
1865
  .cv-controls { gap: 0.6rem; padding-bottom: 0.6rem; }
1873
1866
  .cv-pill { padding: 0.15rem 0.4rem; gap: 0.3rem; }
1874
1867
  .cv-btn { padding: 0.2rem 0.4rem; font-size: 0.6rem; gap: 0; }
@@ -1886,7 +1879,6 @@ var PANEL_CSS = (
1886
1879
  .cv-visualizer-wrap { width: clamp(80px, 35cqw, 140px); height: clamp(80px, 35cqw, 140px); }
1887
1880
  .cv-header { padding-top: 0.8rem; }
1888
1881
  .cv-title { font-size: clamp(0.7rem, 2.5cqw, 1rem); gap: 0.4rem; }
1889
- .cv-title .cv-timer { font-size: 0.65em; }
1890
1882
  .cv-controls { gap: clamp(0.6rem, 1.5cqw, 1rem); padding-bottom: clamp(0.6rem, 1.5cqw, 1rem); }
1891
1883
  .cv-pill { padding: clamp(0.15rem, 0.5cqw, 0.25rem) clamp(0.35rem, 1cqw, 0.6rem); font-size: 0.65rem; gap: 0.3rem; }
1892
1884
  .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); }
@@ -1904,7 +1896,6 @@ var PANEL_CSS = (
1904
1896
  .cv-visualizer-wrap { width: clamp(100px, 40cqw, 200px); height: clamp(100px, 40cqw, 200px); }
1905
1897
  .cv-header { padding-top: clamp(1rem, 1.5cqw, 1.5rem); }
1906
1898
  .cv-title { font-size: clamp(0.9rem, 3cqw, 1.3rem); gap: clamp(0.4rem, 1cqw, 0.75rem); }
1907
- .cv-title .cv-timer { font-size: 0.85em; }
1908
1899
  .cv-controls { gap: clamp(0.8rem, 1.5cqw, 1.2rem); padding-bottom: clamp(0.8rem, 1.5cqw, 1.2rem); }
1909
1900
  .cv-pill { padding: clamp(0.2rem, 0.75cqw, 0.3rem) clamp(0.5rem, 1.2cqw, 0.75rem); font-size: 0.7rem; }
1910
1901
  .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); }
@@ -1920,7 +1911,6 @@ var PANEL_CSS = (
1920
1911
  .cv-visualizer-wrap { width: clamp(140px, 45cqw, 300px); height: clamp(140px, 45cqw, 300px); }
1921
1912
  .cv-header { padding-top: clamp(1.5rem, 2cqw, 2rem); }
1922
1913
  .cv-title { font-size: clamp(1rem, 3cqw, 1.8rem); gap: clamp(0.75rem, 1.5cqw, 1.2rem); }
1923
- .cv-title .cv-timer { font-size: 0.9em; }
1924
1914
  .cv-controls { gap: clamp(1.2rem, 2cqw, 1.8rem); padding-bottom: clamp(1.2rem, 2cqw, 1.8rem); }
1925
1915
  .cv-pill { padding: clamp(0.3rem, 0.8cqw, 0.4rem) clamp(0.75rem, 1.2cqw, 1rem); font-size: 0.85rem; gap: 0.75rem; }
1926
1916
  .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); }
@@ -1957,7 +1947,6 @@ var ConversationalPanel = class {
1957
1947
  startBtn;
1958
1948
  errorEl;
1959
1949
  errorText;
1960
- timerEl;
1961
1950
  canvas;
1962
1951
  muteBtn;
1963
1952
  muteSvg;
@@ -2009,7 +1998,7 @@ var ConversationalPanel = class {
2009
1998
  </div>
2010
1999
  </div>
2011
2000
  <div class="cv-header">
2012
- <h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")} <span class="cv-timer">00:00</span></h2>
2001
+ <h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")}</h2>
2013
2002
  </div>
2014
2003
  <div class="cv-visualizer-wrap">
2015
2004
  <canvas class="cv-canvas"></canvas>
@@ -2040,7 +2029,6 @@ var ConversationalPanel = class {
2040
2029
  this.startBtn = this.el.querySelector(".cv-curtain-btn");
2041
2030
  this.errorEl = this.el.querySelector(".cv-error");
2042
2031
  this.errorText = this.el.querySelector(".cv-error-text");
2043
- this.timerEl = this.el.querySelector(".cv-timer");
2044
2032
  this.canvas = this.el.querySelector(".cv-canvas");
2045
2033
  this.muteBtn = this.el.querySelector(".cv-btn--mute");
2046
2034
  this.muteSvg = this.muteBtn.querySelector("svg");
@@ -2218,7 +2206,7 @@ var ConversationalPanel = class {
2218
2206
  this.cfg.title = title;
2219
2207
  this.curtainTitle.textContent = title;
2220
2208
  const h2 = this.el.querySelector(".cv-title");
2221
- if (h2) h2.innerHTML = `${this.esc(title)} <span class="cv-timer">${this.timerEl?.textContent || "00:00"}</span>`;
2209
+ if (h2) h2.textContent = title;
2222
2210
  }
2223
2211
  /** Select a voice from the picker */
2224
2212
  /** Update description text */
@@ -2248,21 +2236,13 @@ var ConversationalPanel = class {
2248
2236
  this.el.remove();
2249
2237
  }
2250
2238
  // ─── internal helpers ────────────────────────────────────
2251
- fmt(s) {
2252
- const m = Math.floor(s / 60).toString().padStart(2, "0");
2253
- const sec = (s % 60).toString().padStart(2, "0");
2254
- return `${m}:${sec}`;
2255
- }
2256
2239
  startTimer() {
2257
2240
  let elapsed = 0;
2258
2241
  const maxDur = this.cfg.maxDuration || 300;
2259
2242
  const silentMax = this.cfg.silenceTimeout || 60;
2260
2243
  this._lastSpeechTime = Date.now();
2261
- this.timerEl.textContent = this.fmt(maxDur);
2262
2244
  this.timerTicker = window.setInterval(() => {
2263
2245
  elapsed++;
2264
- const remaining = Math.max(0, maxDur - elapsed);
2265
- this.timerEl.textContent = this.fmt(remaining);
2266
2246
  if (elapsed >= maxDur) {
2267
2247
  this._locked = true;
2268
2248
  this.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokutor/sdk",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "JavaScript/TypeScript SDK for Lokutor Real-time Voice AI",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/src/client.ts CHANGED
@@ -893,7 +893,7 @@ export class TTSClient {
893
893
  const req = {
894
894
  text: options.text,
895
895
  voice: options.voice || VoiceStyle.F1,
896
- lang: options.language || Language.ENGLISH,
896
+ language: options.language || Language.ENGLISH,
897
897
  speed: options.speed || 1.05,
898
898
  steps: options.steps || 24,
899
899
  visemes: options.visemes || false
@@ -1001,7 +1001,7 @@ export class STTClient {
1001
1001
  if (typeof Blob !== 'undefined' && options.audio instanceof Blob) {
1002
1002
  const form = new FormData();
1003
1003
  form.append('audio', options.audio, 'audio.wav');
1004
- if (options.language) form.append('lang', options.language);
1004
+ if (options.language) form.append('language', options.language);
1005
1005
  if (options.sampleRate) form.append('sample_rate', String(options.sampleRate));
1006
1006
  res = await fetch(url, {
1007
1007
  method: 'POST',
@@ -1019,7 +1019,7 @@ export class STTClient {
1019
1019
  audio: uint8ArrayToBase64(bytes),
1020
1020
  format: options.format || 'pcm16',
1021
1021
  sample_rate: options.sampleRate,
1022
- lang: options.language,
1022
+ language: options.language,
1023
1023
  }),
1024
1024
  });
1025
1025
  }
@@ -1105,7 +1105,7 @@ export class SpeechToTextClient {
1105
1105
  this.ws.onopen = async () => {
1106
1106
  this.isConnected = true;
1107
1107
  this.onStatusChange?.('connected');
1108
- this.ws!.send(JSON.stringify({ lang: this.language || Language.ENGLISH, vad: this.vad }));
1108
+ this.ws!.send(JSON.stringify({ language: this.language || Language.ENGLISH, vad: this.vad }));
1109
1109
 
1110
1110
  await this.audioManager!.startMicrophone((data) => {
1111
1111
  if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
@@ -107,12 +107,6 @@ const PANEL_CSS = /*css*/ `
107
107
  gap: 1rem;
108
108
  letter-spacing: -0.02em;
109
109
  }
110
- .cv-title .cv-timer {
111
- font-variant-numeric: tabular-nums;
112
- color: var(--cv-accent);
113
- font-weight: 400;
114
- opacity: 0.8;
115
- }
116
110
  .cv-visualizer-wrap {
117
111
  position: absolute;
118
112
  top: 50%;
@@ -261,7 +255,6 @@ const PANEL_CSS = /*css*/ `
261
255
  .cv-visualizer-wrap { width: 60px; height: 60px; }
262
256
  .cv-header { padding-top: 0.75rem; }
263
257
  .cv-title { font-size: 0.7rem; gap: 0.3rem; }
264
- .cv-title .cv-timer { display: none; }
265
258
  .cv-controls { gap: 0.6rem; padding-bottom: 0.6rem; }
266
259
  .cv-pill { padding: 0.15rem 0.4rem; gap: 0.3rem; }
267
260
  .cv-btn { padding: 0.2rem 0.4rem; font-size: 0.6rem; gap: 0; }
@@ -279,7 +272,6 @@ const PANEL_CSS = /*css*/ `
279
272
  .cv-visualizer-wrap { width: clamp(80px, 35cqw, 140px); height: clamp(80px, 35cqw, 140px); }
280
273
  .cv-header { padding-top: 0.8rem; }
281
274
  .cv-title { font-size: clamp(0.7rem, 2.5cqw, 1rem); gap: 0.4rem; }
282
- .cv-title .cv-timer { font-size: 0.65em; }
283
275
  .cv-controls { gap: clamp(0.6rem, 1.5cqw, 1rem); padding-bottom: clamp(0.6rem, 1.5cqw, 1rem); }
284
276
  .cv-pill { padding: clamp(0.15rem, 0.5cqw, 0.25rem) clamp(0.35rem, 1cqw, 0.6rem); font-size: 0.65rem; gap: 0.3rem; }
285
277
  .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); }
@@ -297,7 +289,6 @@ const PANEL_CSS = /*css*/ `
297
289
  .cv-visualizer-wrap { width: clamp(100px, 40cqw, 200px); height: clamp(100px, 40cqw, 200px); }
298
290
  .cv-header { padding-top: clamp(1rem, 1.5cqw, 1.5rem); }
299
291
  .cv-title { font-size: clamp(0.9rem, 3cqw, 1.3rem); gap: clamp(0.4rem, 1cqw, 0.75rem); }
300
- .cv-title .cv-timer { font-size: 0.85em; }
301
292
  .cv-controls { gap: clamp(0.8rem, 1.5cqw, 1.2rem); padding-bottom: clamp(0.8rem, 1.5cqw, 1.2rem); }
302
293
  .cv-pill { padding: clamp(0.2rem, 0.75cqw, 0.3rem) clamp(0.5rem, 1.2cqw, 0.75rem); font-size: 0.7rem; }
303
294
  .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); }
@@ -313,7 +304,6 @@ const PANEL_CSS = /*css*/ `
313
304
  .cv-visualizer-wrap { width: clamp(140px, 45cqw, 300px); height: clamp(140px, 45cqw, 300px); }
314
305
  .cv-header { padding-top: clamp(1.5rem, 2cqw, 2rem); }
315
306
  .cv-title { font-size: clamp(1rem, 3cqw, 1.8rem); gap: clamp(0.75rem, 1.5cqw, 1.2rem); }
316
- .cv-title .cv-timer { font-size: 0.9em; }
317
307
  .cv-controls { gap: clamp(1.2rem, 2cqw, 1.8rem); padding-bottom: clamp(1.2rem, 2cqw, 1.8rem); }
318
308
  .cv-pill { padding: clamp(0.3rem, 0.8cqw, 0.4rem) clamp(0.75rem, 1.2cqw, 1rem); font-size: 0.85rem; gap: 0.75rem; }
319
309
  .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); }
@@ -387,7 +377,6 @@ export class ConversationalPanel {
387
377
  private startBtn!: HTMLButtonElement;
388
378
  private errorEl!: HTMLElement;
389
379
  private errorText!: HTMLElement;
390
- private timerEl!: HTMLElement;
391
380
  private canvas!: HTMLCanvasElement;
392
381
  private muteBtn!: HTMLElement;
393
382
  private muteSvg!: SVGElement;
@@ -444,7 +433,7 @@ export class ConversationalPanel {
444
433
  </div>
445
434
  </div>
446
435
  <div class="cv-header">
447
- <h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")} <span class="cv-timer">00:00</span></h2>
436
+ <h2 class="cv-title">${this.esc(this.cfg.title || "Voice Chat")}</h2>
448
437
  </div>
449
438
  <div class="cv-visualizer-wrap">
450
439
  <canvas class="cv-canvas"></canvas>
@@ -478,7 +467,6 @@ export class ConversationalPanel {
478
467
  this.startBtn = this.el.querySelector('.cv-curtain-btn')!;
479
468
  this.errorEl = this.el.querySelector('.cv-error')!;
480
469
  this.errorText = this.el.querySelector('.cv-error-text')!;
481
- this.timerEl = this.el.querySelector('.cv-timer')!;
482
470
  this.canvas = this.el.querySelector('.cv-canvas')!;
483
471
  this.muteBtn = this.el.querySelector('.cv-btn--mute')!;
484
472
  this.muteSvg = this.muteBtn.querySelector('svg')!;
@@ -673,7 +661,7 @@ export class ConversationalPanel {
673
661
  this.cfg.title = title;
674
662
  this.curtainTitle.textContent = title;
675
663
  const h2 = this.el.querySelector('.cv-title');
676
- if (h2) h2.innerHTML = `${this.esc(title)} <span class="cv-timer">${this.timerEl?.textContent || '00:00'}</span>`;
664
+ if (h2) h2.textContent = title;
677
665
  }
678
666
 
679
667
  /** Select a voice from the picker */
@@ -710,22 +698,15 @@ export class ConversationalPanel {
710
698
 
711
699
  // ─── internal helpers ────────────────────────────────────
712
700
 
713
- private fmt(s: number): string {
714
- const m = Math.floor(s / 60).toString().padStart(2, '0');
715
- const sec = (s % 60).toString().padStart(2, '0');
716
- return `${m}:${sec}`;
717
- }
718
-
719
701
  private startTimer() {
702
+ // No visible countdown — this interval only enforces maxDuration and
703
+ // the silence timeout, both real safety/cost-control cutoffs, not UI.
720
704
  let elapsed = 0;
721
705
  const maxDur = this.cfg.maxDuration || 300;
722
706
  const silentMax = this.cfg.silenceTimeout || 60;
723
707
  this._lastSpeechTime = Date.now();
724
- this.timerEl.textContent = this.fmt(maxDur);
725
708
  this.timerTicker = window.setInterval(() => {
726
709
  elapsed++;
727
- const remaining = Math.max(0, maxDur - elapsed);
728
- this.timerEl.textContent = this.fmt(remaining);
729
710
 
730
711
  if (elapsed >= maxDur) {
731
712
  this._locked = true;