beast-agent 2.44.11 → 2.44.13

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.44.11",
4
+ "version": "2.44.13",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -195,41 +195,76 @@ async function install(voiceId) {
195
195
  }
196
196
  }
197
197
 
198
- /* ---------- sentez: metin → { audio: WAV buffer, mime } ---------- */
198
+ /* ---------- sentez ayarları (hız/duraklama/ifade) ----------
199
+ speed: 1.0 normal; >1 hızlı, <1 yavaş → CLI'da --length_scale 1/speed
200
+ (piper'da length_scale büyükse konuşma YAVAŞLAR)
201
+ silence: cümle sonu duraklama sn (--sentence_silence, varsayılan 0.2)
202
+ noiseScale: ifade/doğallık (--noise_scale, varsayılan 0.667; düşük=daha
203
+ stabil/robotik, yüksek=daha canlı ama artefakt riski) */
204
+ function clampNum(n, lo, hi) {
205
+ return Math.max(lo, Math.min(hi, n));
206
+ }
207
+
208
+ function tuningArgs(opts = {}) {
209
+ const args = [];
210
+ const speed = Number(opts.speed);
211
+ if (Number.isFinite(speed) && Math.abs(speed - 1) > 0.001) {
212
+ args.push('--length_scale', String(+(1 / clampNum(speed, 0.5, 2)).toFixed(3)));
213
+ }
214
+ const sil = Number(opts.sentenceSilence);
215
+ if (Number.isFinite(sil)) args.push('--sentence_silence', String(+(clampNum(sil, 0, 2)).toFixed(3)));
216
+ const noise = Number(opts.noiseScale);
217
+ if (Number.isFinite(noise)) args.push('--noise_scale', String(+(clampNum(noise, 0, 1.5)).toFixed(3)));
218
+ return args;
219
+ }
220
+
221
+ /* ---------- sentez: metin → { audio: WAV buffer, mime } ----------
222
+ DİKKAT: piper'ı ASLA stdout'a yazdırmıyoruz (--output_file -). Windows'ta
223
+ binary stdout akışı bozuluyor — çıktı gürültülü/cızırtılı geliyor
224
+ (rhasspy/piper#211: "piping raw output... choppy"). Geçici WAV dosyasına
225
+ yazıp okuyoruz; dosya çıktısı temiz. */
199
226
 
200
227
  function synthesize(text, opts = {}) {
201
228
  const vid = resolveVoice(opts.voice);
202
229
  return (async () => {
203
230
  await ensureRuntime();
204
231
  await ensureVoice(vid);
205
- return new Promise((resolve, reject) => {
206
- const args = [
207
- '--model', voicePath(vid),
208
- '--config', voiceJsonPath(vid),
209
- '--output_file', '-',
210
- '-q',
211
- ];
212
- const p = spawn(exePath(), args, { windowsHide: true, cwd: binDir() });
213
- const chunks = [];
214
- let err = '';
215
- const timer = setTimeout(() => {
216
- try { p.kill(); } catch {}
217
- reject(new Error('piper zaman aşımı'));
218
- }, 120000);
219
- p.stdout.on('data', (d) => chunks.push(d));
220
- p.stderr.on('data', (d) => { err += d.toString(); });
221
- p.on('error', (e) => { clearTimeout(timer); reject(e); });
222
- p.on('close', (code) => {
223
- clearTimeout(timer);
224
- const buf = Buffer.concat(chunks);
225
- if (code !== 0 || buf.length < 44) {
226
- return reject(new Error('piper başarısız (kod ' + code + '): ' + err.slice(0, 140)));
227
- }
228
- resolve({ audio: buf, mime: 'audio/wav' });
232
+ const tmpDir = path.join(rootDir(), 'tmp');
233
+ fs.mkdirSync(tmpDir, { recursive: true });
234
+ const outFile = path.join(tmpDir, 'out-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8) + '.wav');
235
+ try {
236
+ await new Promise((resolve, reject) => {
237
+ const args = [
238
+ '--model', voicePath(vid),
239
+ '--config', voiceJsonPath(vid),
240
+ '--output_file', outFile,
241
+ ...tuningArgs(opts),
242
+ '-q',
243
+ ];
244
+ const p = spawn(exePath(), args, { windowsHide: true, cwd: binDir() });
245
+ let err = '';
246
+ const timer = setTimeout(() => {
247
+ try { p.kill(); } catch {}
248
+ reject(new Error('piper zaman aşımı'));
249
+ }, 120000);
250
+ p.stderr.on('data', (d) => { err += d.toString(); });
251
+ p.on('error', (e) => { clearTimeout(timer); reject(e); });
252
+ p.on('close', (code) => {
253
+ clearTimeout(timer);
254
+ if (code !== 0) return reject(new Error('piper başarısız (kod ' + code + '): ' + err.slice(0, 140)));
255
+ resolve();
256
+ });
257
+ p.stdin.write(String(text).slice(0, 4000));
258
+ p.stdin.end();
229
259
  });
230
- p.stdin.write(String(text).slice(0, 4000));
231
- p.stdin.end();
232
- });
260
+ const buf = fs.readFileSync(outFile);
261
+ if (buf.length < 44 || buf.slice(0, 4).toString('latin1') !== 'RIFF') {
262
+ throw new Error('geçersiz WAV çıktısı');
263
+ }
264
+ return { audio: buf, mime: 'audio/wav' };
265
+ } finally {
266
+ try { fs.unlinkSync(outFile); } catch {}
267
+ }
233
268
  })();
234
269
  }
235
270
 
@@ -239,6 +274,7 @@ module.exports = {
239
274
  synthesize,
240
275
  install,
241
276
  status,
277
+ tuningArgs,
242
278
  ensureRuntime,
243
279
  ensureVoice,
244
280
  runtimeInstalled,
package/src/main.js CHANGED
@@ -703,7 +703,10 @@ function sttBaseUrl() {
703
703
  }
704
704
  function sttProvider() {
705
705
  const explicit = String(settings.sttProvider || '').trim().toLowerCase();
706
- if (explicit === 'groq' || explicit === 'openai' || explicit === 'local') return explicit;
706
+ /* açık seçim: anahtar yoksa bulut seçimi yerel Whisper'a düşer (sessiz hata olmasın) */
707
+ if (explicit === 'local') return 'local';
708
+ if (explicit === 'groq' && sttGroqKey()) return 'groq';
709
+ if (explicit === 'openai' && (sttOpenaiKey() || sttBaseUrl())) return 'openai';
707
710
  if (sttGroqKey()) return 'groq';
708
711
  if (sttOpenaiKey() || sttBaseUrl()) return 'openai';
709
712
  return 'local';
@@ -1057,6 +1060,9 @@ async function synthesizeSpeech(text) {
1057
1060
  if (engine === 'piper') {
1058
1061
  return await piper.synthesize(String(text).slice(0, 4000), {
1059
1062
  voice: cfg.piperVoice || piper.DEFAULT_VOICE,
1063
+ speed: cfg.piperSpeed,
1064
+ sentenceSilence: cfg.piperSilence,
1065
+ noiseScale: cfg.piperNoise,
1060
1066
  });
1061
1067
  }
1062
1068
  if (!cfg.baseUrl || !cfg.key) return null;
@@ -6639,12 +6645,21 @@ ipcMain.handle('stt:transcribe', async (_e, b64, lang, priority) => {
6639
6645
 
6640
6646
  ipcMain.handle('stt:lang:get', () => settings.sttLang || 'tr');
6641
6647
 
6648
+ /* STT MOTORU SEÇİMİ: '' = otomatik (anahtar varsa bulut, yoksa yerel Whisper),
6649
+ 'local' = yerel Whisper, 'groq' | 'openai' = bulut */
6650
+ ipcMain.handle('stt:provider:set', (_e, p) => {
6651
+ const v = String(p || '').trim().toLowerCase();
6652
+ settings.sttProvider = ['local', 'groq', 'openai'].includes(v) ? v : '';
6653
+ saveSettings();
6654
+ return { ok: true, pref: settings.sttProvider, provider: sttProvider(), engineLabel: sttEngineLabel() };
6655
+ });
6656
+
6642
6657
  /* STT DURUMU: ayarlar ekranı için — model indirildi mi/in-progress mi,
6643
6658
  hangi motor, ne kadar disk */
6644
6659
  ipcMain.handle('stt:status', () => {
6645
6660
  const provider = sttProvider();
6646
6661
  const model = sttModelName();
6647
- const out = { provider, engineLabel: sttEngineLabel(), model };
6662
+ const out = { provider, engineLabel: sttEngineLabel(), model, pref: String(settings.sttProvider || '') };
6648
6663
  if (provider !== 'local') { out.state = 'cloud'; return out; }
6649
6664
  const dir = path.join(APP_DIR, 'models', ...model.split('/'));
6650
6665
  let files = [];
@@ -12867,6 +12882,13 @@ ipcMain.handle('wa:queue:get', () => mqueue.stats());
12867
12882
 
12868
12883
  ipcMain.handle('wa:sessions', () => [...waChats.values()]);
12869
12884
 
12885
+ /* TTS sayısal ayar kelepçesi: geçersiz/boş değerde varsayılana döner */
12886
+ function clampTtsNum(v, lo, hi, def) {
12887
+ const n = Number(v);
12888
+ if (!Number.isFinite(n)) return def;
12889
+ return Math.max(lo, Math.min(hi, n));
12890
+ }
12891
+
12870
12892
  ipcMain.handle('wa:tts:get', () => settings.waTts || {});
12871
12893
  ipcMain.handle('wa:tts:set', (_e, cfg) => {
12872
12894
  const eng = String((cfg && cfg.engine) || 'edge');
@@ -12877,6 +12899,10 @@ ipcMain.handle('wa:tts:set', (_e, cfg) => {
12877
12899
  engine: ['edge', 'piper', 'openai'].includes(eng) ? eng : 'edge',
12878
12900
  edgeVoice: String((cfg && cfg.edgeVoice) || 'tr-TR-AhmetNeural').trim(),
12879
12901
  piperVoice: piper.VOICES[pv] ? pv : piper.DEFAULT_VOICE,
12902
+ /* Piper ince ayar: hız (0.6-1.6), cümle sonu duraklama (0-1 sn), ifade (0-1.2) */
12903
+ piperSpeed: clampTtsNum(cfg && cfg.piperSpeed, 0.6, 1.6, 1),
12904
+ piperSilence: clampTtsNum(cfg && cfg.piperSilence, 0, 1, 0.2),
12905
+ piperNoise: clampTtsNum(cfg && cfg.piperNoise, 0, 1.2, 0.667),
12880
12906
  chatAutoSpeak: !!(cfg && cfg.chatAutoSpeak),
12881
12907
  baseUrl: String((cfg && cfg.baseUrl) || '').trim(),
12882
12908
  key: String((cfg && cfg.key) || '').trim(),
package/src/preload.js CHANGED
@@ -67,6 +67,7 @@ contextBridge.exposeInMainWorld('beast', {
67
67
  installStatus: () => ipcRenderer.invoke('install:status'),
68
68
  embedPrefetch: () => ipcRenderer.invoke('embed:prefetch'),
69
69
  sttLangSet: (lang) => ipcRenderer.invoke('stt:lang:set', lang),
70
+ sttProviderSet: (p) => ipcRenderer.invoke('stt:provider:set', p),
70
71
  updateStatus: () => ipcRenderer.invoke('update:status'),
71
72
  updateCheck: () => ipcRenderer.invoke('update:check'),
72
73
  updateInstall: () => ipcRenderer.invoke('update:install'),
@@ -268,11 +268,25 @@
268
268
  tts_save: 'TTS Kaydet',
269
269
  tts_test: 'Test sesi çal',
270
270
  stt_download_now: 'Şimdi indir / hazırla',
271
+ stt_engine: 'STT motoru',
272
+ stt_seg_auto: 'Otomatik',
273
+ stt_seg_whisper: 'Whisper (yerel)',
274
+ tts_stt_title: 'Ses → Yazı',
275
+ tts_stt_sub: 'Mikrofon kaydını yazıya çeviren motor',
276
+ tts_tts_title: 'Yazı → Ses',
277
+ tts_tts_sub: 'Sesli yanıt motoru ve ses seçimi',
278
+ stt_engine_auto: 'Otomatik — anahtar varsa bulut, yoksa yerel Whisper',
279
+ stt_engine_local: 'Whisper — yerel/offline (large-v3-turbo)',
280
+ stt_engine_groq: 'Groq Whisper — bulut (hızlı)',
281
+ stt_engine_openai: 'OpenAI-uyumlu Whisper — bulut',
271
282
  tts_note: 'Edge TTS: ücretsiz bulut motoru — anahtar gerekmez (Ahmet/Emel vb.). Piper: tamamen yerel/offline neural motor (ilk kullanımda ~80MB indirir). OpenAI motoru: uyumlu speech API (tts-1, tts-1-hd; sesler alloy, echo, fable, onyx, nova, shimmer).',
272
283
  tts_engine: 'Motor',
273
284
  tts_edge_voice: 'Edge sesi',
274
285
  tts_piper_voice: 'Piper sesi',
275
286
  tts_piper_download: 'Piper\'ı indir / hazırla',
287
+ tts_piper_speed: 'Konuşma hızı',
288
+ tts_piper_silence: 'Cümle sonu duraklama',
289
+ tts_piper_noise: 'İfade (doğallık)',
276
290
  tts_chat_auto: 'Chat\'te ajanın yazdıklarını otomatik seslendir',
277
291
  tts_on: 'TTS açık — cevaplar sesli de gider',
278
292
  tts_off: 'TTS kapalı',
@@ -994,11 +1008,25 @@
994
1008
  tts_save: 'Save TTS',
995
1009
  tts_test: 'Play test sound',
996
1010
  stt_download_now: 'Download / prepare now',
1011
+ stt_engine: 'STT engine',
1012
+ stt_seg_auto: 'Auto',
1013
+ stt_seg_whisper: 'Whisper (local)',
1014
+ tts_stt_title: 'Speech → Text',
1015
+ tts_stt_sub: 'Engine that transcribes your microphone',
1016
+ tts_tts_title: 'Text → Speech',
1017
+ tts_tts_sub: 'Voice reply engine and voice',
1018
+ stt_engine_auto: 'Auto — cloud if a key exists, otherwise local Whisper',
1019
+ stt_engine_local: 'Whisper — local/offline (large-v3-turbo)',
1020
+ stt_engine_groq: 'Groq Whisper — cloud (fast)',
1021
+ stt_engine_openai: 'OpenAI-compatible Whisper — cloud',
997
1022
  tts_note: 'Edge TTS: free cloud engine — no key needed (Ahmet/Emel etc.). Piper: fully local/offline neural engine (downloads ~80MB on first use). OpenAI engine: compatible speech API (tts-1, tts-1-hd; voices alloy, echo, fable, onyx, nova, shimmer).',
998
1023
  tts_engine: 'Engine',
999
1024
  tts_edge_voice: 'Edge voice',
1000
1025
  tts_piper_voice: 'Piper voice',
1001
1026
  tts_piper_download: 'Download / prepare Piper',
1027
+ tts_piper_speed: 'Speaking speed',
1028
+ tts_piper_silence: 'Sentence pause',
1029
+ tts_piper_noise: 'Expressiveness',
1002
1030
  tts_chat_auto: 'Auto-speak agent replies in chat',
1003
1031
  tts_on: 'TTS on — replies also go as voice',
1004
1032
  tts_off: 'TTS off',
@@ -2554,155 +2554,251 @@ function updateInstallPct(ev) {
2554
2554
  } catch {}
2555
2555
  }
2556
2556
 
2557
- async function renderTtsPane() { const pane = $('#tab-tts');
2557
+ async function renderTtsPane() {
2558
+ const pane = $('#tab-tts');
2558
2559
  if (!pane) return;
2559
2560
  /* STT DURUMU: model indirildi mi / iniyor mu / hangi motor */
2560
2561
  let stt = null;
2561
2562
  try { stt = await beast.sttStatus(); } catch {}
2562
- const stateText = {
2563
- ready: 'HAZIR — model yüklü',
2564
- loading: 'İNİYOR / yükleniyor…',
2565
- downloaded: 'indirildi — ilk kullanımda yüklenir',
2566
- partial: 'kısmen indi — devam edecek',
2567
- missing: 'henüz indirilmedi',
2568
- cloud: 'bulut motoru — indirme gerekmez',
2569
- };
2570
- const sttLine = stt
2571
- ? stt.engineLabel + ' · ' + (stateText[stt.state] || stt.state) + (stt.mb ? ' · ' + stt.mb + ' MB' : '')
2572
- : (window.beast ? 'bilinmiyor' : '');
2573
- const edgeOpts = [
2563
+ const EDGE_VOICES = [
2574
2564
  ['tr-TR-AhmetNeural', 'Ahmet — Türkçe (erkek)'],
2575
2565
  ['tr-TR-EmelNeural', 'Emel — Türkçe (kadın)'],
2576
2566
  ['en-US-GuyNeural', 'Guy — English (male)'],
2577
2567
  ['en-US-AriaNeural', 'Aria — English (female)'],
2578
2568
  ['de-DE-KatjaNeural', 'Katja — Deutsch'],
2579
2569
  ['ar-SA-HamedNeural', 'Hamed — العربية'],
2580
- ]
2581
- .map(([v, n]) => `<option value="${v}">${n}</option>`)
2582
- .join('');
2583
- const piperOpts = [
2570
+ ];
2571
+ const PIPER_VOICES = [
2584
2572
  ['tr_TR-fahrettin-medium', 'Fahrettin — Türkçe (erkek)'],
2585
2573
  ['tr_TR-fettah-medium', 'Fettah — Türkçe (erkek)'],
2586
2574
  ['en_US-lessac-medium', 'Lessac — English (male)'],
2587
2575
  ['en_US-amy-medium', 'Amy — English (female)'],
2588
- ]
2589
- .map(([v, n]) => `<option value="${v}">${n}</option>`)
2590
- .join('');
2576
+ ];
2577
+ const seg = (name, items) => '<div class="voice-seg" id="' + name + '">' +
2578
+ items.map(([v, n]) => `<label><input type="radio" name="${name}" value="${v}"/><span>${n}</span></label>`).join('') +
2579
+ '</div>';
2591
2580
  pane.innerHTML =
2592
2581
  '<h2>' + _t('tts_h2') + '</h2><div class="sub">' + _t('tts_sub') + '</div>' +
2593
- '<div class="sub" style="margin:10px 0 4px;font-weight:700;color:var(--accent)">STT (Ses → Yazı)</div>' +
2594
- `<div class="sub" id="sttStatusTxt" style="margin-bottom:8px">${sttLine}</div>` +
2595
- `<button id="sttDlBtn" class="btn ghost" style="margin-bottom:14px">${_t('stt_download_now')}</button>` +
2596
- '<div class="sub" style="margin:10px 0 4px;font-weight:700;color:var(--accent)">TTS (Yazı → Ses)</div>' +
2597
- `<div class="form-grid" style="grid-template-columns:auto 1fr 1fr;align-items:center;margin-top:10px">
2598
- <label class="lock-row"><input type="checkbox" id="ttsOn" /><span>${_t('tts_active')}</span></label>
2599
- <label style="grid-column:1">${_t('tts_engine')}</label>
2600
- <select id="ttsEngine" class="inp" style="grid-column:2/4">
2601
- <option value="edge">Edge TTS — ücretsiz bulut (Ahmet/Emel…)</option>
2602
- <option value="piper">Piper — yerel & offline (Fahrettin/Fettah…)</option>
2603
- <option value="openai">OpenAI-uyumlu API (tts-1, ses: alloy…)</option>
2604
- </select>
2605
- <label style="grid-column:1">${_t('tts_edge_voice')}</label>
2606
- <select id="ttsEdgeVoice" class="inp" style="grid-column:2/4">${edgeOpts}</select>
2607
- <label style="grid-column:1">${_t('tts_piper_voice')}</label>
2608
- <select id="ttsPiperVoice" class="inp" style="grid-column:2/4">${piperOpts}</select>
2609
- <span id="piperStatusRow" style="grid-column:1/4;display:flex;gap:10px;align-items:center">
2610
- <span class="sub" id="piperStatusTxt" style="margin:0"></span>
2611
- <button id="piperDlBtn" class="btn ghost">${_t('tts_piper_download')}</button>
2612
- </span>
2613
- <label class="lock-row" style="grid-column:1/4"><input type="checkbox" id="ttsChatAuto" /><span>${_t('tts_chat_auto')}</span></label>
2614
- <span id="ttsOpenaiWrap" style="display:contents">
2615
- <input id="ttsUrl" class="inp" placeholder="https://api.openai.com/v1" autocomplete="off" />
2616
- <input id="ttsKey" class="inp" type="password" placeholder="API Key" autocomplete="off" />
2617
- <input id="ttsModel" class="inp" placeholder="tts-1" autocomplete="off" />
2618
- <input id="ttsVoice" class="inp" placeholder="ses: alloy" autocomplete="off" />
2619
- </span>
2620
- <button id="ttsSave" class="btn ghost" style="grid-column:1">${_t('tts_save')}</button>
2621
- <button id="ttsTest" class="btn ghost" style="grid-column:2;justify-self:start">${_t('tts_test')}</button>
2622
- </div>` +
2623
- '<div class="sub" style="margin-top:8px">' + _t('tts_note') + '</div>';
2582
+ `<div class="voice-card" style="margin-top:14px">
2583
+ <div class="voice-head">
2584
+ <div class="voice-ico">🎤</div>
2585
+ <div>
2586
+ <div class="voice-title">${_t('tts_stt_title')}</div>
2587
+ <div class="voice-sub">${_t('tts_stt_sub')}</div>
2588
+ </div>
2589
+ </div>
2590
+ ${seg('sttEngine', [['', _t('stt_seg_auto')], ['local', _t('stt_seg_whisper')], ['groq', 'Groq'], ['openai', 'OpenAI']])}
2591
+ <div class="voice-status">
2592
+ <span id="sttDot" class="voice-dot"></span><span id="sttStatusTxt"></span>
2593
+ <button id="sttDlBtn" class="btn ghost" style="margin-left:auto">${_t('stt_download_now')}</button>
2594
+ </div>
2595
+ </div>
2596
+ <div class="voice-card">
2597
+ <div class="voice-head">
2598
+ <div class="voice-ico">🔊</div>
2599
+ <div>
2600
+ <div class="voice-title">${_t('tts_tts_title')}</div>
2601
+ <div class="voice-sub">${_t('tts_tts_sub')}</div>
2602
+ </div>
2603
+ <label class="lock-row" style="margin-left:auto"><input type="checkbox" id="ttsOn" /><span>${_t('tts_active')}</span></label>
2604
+ </div>
2605
+ ${seg('ttsEngine', [['edge', 'Edge'], ['piper', 'Piper'], ['openai', 'OpenAI']])}
2606
+ <div class="voice-row">
2607
+ <select id="ttsVoiceSel" class="inp"></select>
2608
+ </div>
2609
+ <label class="lock-row" style="margin-top:10px"><input type="checkbox" id="ttsChatAuto" /><span>${_t('tts_chat_auto')}</span></label>
2610
+ <div class="voice-status" id="piperStatusRow">
2611
+ <span id="piperDot" class="voice-dot"></span><span id="piperStatusTxt"></span>
2612
+ <button id="piperDlBtn" class="btn ghost" style="margin-left:auto">${_t('tts_piper_download')}</button>
2613
+ </div>
2614
+ <div class="voice-sliders" id="piperAdvWrap">
2615
+ <div class="voice-slider"><span>${_t('tts_piper_speed')}</span><input id="piperSpeed" type="range" min="0.6" max="1.6" step="0.05" value="1"/><b id="piperSpeedVal">1.00x</b></div>
2616
+ <div class="voice-slider"><span>${_t('tts_piper_silence')}</span><input id="piperSil" type="range" min="0" max="1" step="0.05" value="0.2"/><b id="piperSilVal">0.20s</b></div>
2617
+ <div class="voice-slider"><span>${_t('tts_piper_noise')}</span><input id="piperNoise" type="range" min="0" max="1.2" step="0.05" value="0.667"/><b id="piperNoiseVal">0.67</b></div>
2618
+ </div>
2619
+ <div id="ttsOpenaiWrap" style="display:none">
2620
+ <div class="voice-row" style="margin-top:10px">
2621
+ <input id="ttsUrl" class="inp" placeholder="https://api.openai.com/v1" autocomplete="off" />
2622
+ <input id="ttsKey" class="inp" type="password" placeholder="API Key" autocomplete="off" />
2623
+ </div>
2624
+ <div class="voice-row">
2625
+ <input id="ttsModel" class="inp" placeholder="tts-1" autocomplete="off" />
2626
+ <input id="ttsVoice" class="inp" placeholder="alloy" autocomplete="off" />
2627
+ </div>
2628
+ </div>
2629
+ <div class="voice-actions">
2630
+ <button id="ttsSave" class="btn">${_t('tts_save')}</button>
2631
+ <button id="ttsTest" class="btn ghost">${_t('tts_test')}</button>
2632
+ </div>
2633
+ <div class="voice-note">${_t('tts_note')}</div>
2634
+ </div>`;
2635
+
2636
+ /* ---------- yardımcılar ---------- */
2637
+ const ttsEng = () => (document.querySelector('input[name="ttsEngine"]:checked') || {}).value || 'edge';
2638
+ const setSeg = (name, val) => {
2639
+ document.querySelectorAll('input[name="' + name + '"]').forEach((r) => {
2640
+ r.checked = r.value === String(val);
2641
+ const lab = r.closest('label');
2642
+ if (lab) lab.classList.toggle('on', r.checked);
2643
+ });
2644
+ };
2645
+ const refreshSeg = (name) => {
2646
+ document.querySelectorAll('input[name="' + name + '"]').forEach((r) => {
2647
+ const lab = r.closest('label');
2648
+ if (lab) lab.classList.toggle('on', r.checked);
2649
+ });
2650
+ };
2651
+ const sttName = (st) => (st.provider === 'groq' ? 'Groq Whisper' : st.provider === 'openai' ? 'Whisper (API)' : 'Whisper (yerel)');
2652
+ const sttState = {
2653
+ ready: 'hazır',
2654
+ loading: 'iniyor…',
2655
+ downloaded: 'hazır (ilk kullanımda yüklenir)',
2656
+ partial: 'kısmen indi',
2657
+ missing: 'indirilmedi',
2658
+ cloud: 'bulut',
2659
+ };
2660
+
2661
+ /* ---------- ses listeleri (motor başına) ---------- */
2662
+ let edgeVoice = 'tr-TR-AhmetNeural';
2663
+ let piperVoice = 'tr_TR-fahrettin-medium';
2664
+ const fillVoices = () => {
2665
+ const eng = ttsEng();
2666
+ const sel = $('#ttsVoiceSel');
2667
+ if (!sel) return;
2668
+ const list = eng === 'piper' ? PIPER_VOICES : EDGE_VOICES;
2669
+ sel.innerHTML = list.map(([v, n]) => `<option value="${v}">${n}</option>`).join('');
2670
+ sel.value = eng === 'piper' ? piperVoice : edgeVoice;
2671
+ sel.style.display = eng === 'openai' ? 'none' : '';
2672
+ };
2673
+ const captureVoice = () => {
2674
+ const eng = ttsEng();
2675
+ const sel = $('#ttsVoiceSel');
2676
+ if (!sel) return;
2677
+ if (eng === 'piper') piperVoice = sel.value || piperVoice;
2678
+ else if (eng === 'edge') edgeVoice = sel.value || edgeVoice;
2679
+ };
2624
2680
 
2625
2681
  const syncEngineUi = () => {
2626
- const eng = $('#ttsEngine').value;
2627
- const isEdge = eng === 'edge';
2682
+ const eng = ttsEng();
2628
2683
  const isPiper = eng === 'piper';
2629
- $('#ttsEdgeVoice').disabled = !isEdge;
2630
- $('#ttsPiperVoice').disabled = !isPiper;
2631
- $('#piperDlBtn').disabled = !isPiper;
2632
- $('#piperStatusTxt').style.opacity = isPiper ? '1' : '0.45';
2633
- $('#ttsOpenaiWrap').style.opacity = eng === 'openai' ? '1' : '0.35';
2634
- $('#ttsOpenaiWrap').querySelectorAll('input').forEach((i) => (i.disabled = eng !== 'openai'));
2684
+ $('#piperStatusRow').style.display = isPiper ? '' : 'none';
2685
+ $('#piperAdvWrap').style.display = isPiper ? '' : 'none';
2686
+ $('#ttsOpenaiWrap').style.display = eng === 'openai' ? '' : 'none';
2687
+ };
2688
+
2689
+ /* Piper ince ayar etiketleri (hız/duraklama/ifade) */
2690
+ const syncPiperLabels = () => {
2691
+ const sv = $('#piperSpeedVal'), lv = $('#piperSilVal'), nv = $('#piperNoiseVal');
2692
+ if (sv) sv.textContent = Number($('#piperSpeed').value).toFixed(2) + 'x';
2693
+ if (lv) lv.textContent = Number($('#piperSil').value).toFixed(2) + 's';
2694
+ if (nv) nv.textContent = Number($('#piperNoise').value).toFixed(2);
2695
+ };
2696
+
2697
+ /* ---------- durum satırları ---------- */
2698
+ const refreshPiperStatus = async () => {
2699
+ try {
2700
+ const st = await beast.piperStatus(piperVoice);
2701
+ const txt = $('#piperStatusTxt'), dot = $('#piperDot'), btn = $('#piperDlBtn');
2702
+ if (!st) return null;
2703
+ if (txt) txt.textContent = st.installed ? 'Hazır — yerel/offline' : st.installing ? 'İndiriliyor…' : 'İndirilmedi (~80MB)';
2704
+ if (dot) dot.className = 'voice-dot' + (st.installed ? ' ok' : st.installing ? ' warn' : '');
2705
+ if (btn) btn.disabled = !!st.installed;
2706
+ return st;
2707
+ } catch { return null; }
2708
+ };
2709
+ const refreshSttStatus = async () => {
2710
+ try {
2711
+ stt = await beast.sttStatus();
2712
+ const txt = $('#sttStatusTxt'), dot = $('#sttDot');
2713
+ if (txt && stt) txt.textContent = sttName(stt) + ' · ' + (sttState[stt.state] || stt.state) + (stt.mb ? ' · ' + stt.mb + ' MB' : '');
2714
+ if (dot && stt) {
2715
+ dot.className = 'voice-dot' + (
2716
+ ['ready', 'cloud', 'downloaded'].includes(stt.state) ? ' ok' :
2717
+ ['loading', 'partial'].includes(stt.state) ? ' warn' : ''
2718
+ );
2719
+ }
2720
+ return stt;
2721
+ } catch { return null; }
2635
2722
  };
2636
2723
 
2724
+ /* ---------- ayarları yükle ---------- */
2637
2725
  try {
2638
2726
  const tts = await beast.waGetTts();
2639
2727
  $('#ttsOn').checked = !!tts.enabled;
2640
- $('#ttsEngine').value = ['edge', 'piper', 'openai'].includes(tts.engine) ? tts.engine : 'edge';
2641
- $('#ttsEdgeVoice').value = tts.edgeVoice || 'tr-TR-AhmetNeural';
2642
- $('#ttsPiperVoice').value = tts.piperVoice || 'tr_TR-fahrettin-medium';
2728
+ edgeVoice = tts.edgeVoice || edgeVoice;
2729
+ piperVoice = tts.piperVoice || piperVoice;
2730
+ $('#piperSpeed').value = Number.isFinite(Number(tts.piperSpeed)) ? tts.piperSpeed : 1;
2731
+ $('#piperSil').value = Number.isFinite(Number(tts.piperSilence)) ? tts.piperSilence : 0.2;
2732
+ $('#piperNoise').value = Number.isFinite(Number(tts.piperNoise)) ? tts.piperNoise : 0.667;
2643
2733
  $('#ttsChatAuto').checked = !!tts.chatAutoSpeak;
2644
2734
  $('#ttsUrl').value = tts.baseUrl || '';
2645
2735
  $('#ttsKey').value = tts.key || '';
2646
2736
  $('#ttsModel').value = tts.model || 'tts-1';
2647
2737
  $('#ttsVoice').value = tts.voice || 'alloy';
2738
+ setSeg('ttsEngine', ['edge', 'piper', 'openai'].includes(tts.engine) ? tts.engine : 'edge');
2648
2739
  } catch {}
2649
-
2650
- /* Piper durum satırı: runtime/ses modeli hazır mı (indirilince güncellenir) */
2651
- const refreshPiperStatus = async () => {
2652
- const v = $('#ttsPiperVoice') ? $('#ttsPiperVoice').value : '';
2653
- try {
2654
- const st = await beast.piperStatus(v);
2655
- const el = $('#piperStatusTxt');
2656
- if (el && st) {
2657
- el.textContent = 'Piper: ' + (st.installed
2658
- ? 'hazır — yerel/offline'
2659
- : st.installing
2660
- ? 'indiriliyor…'
2661
- : st.runtime
2662
- ? 'ses modeli indirilecek (ilk kullanımda otomatik)'
2663
- : 'runtime + ses modeli indirilecek (ilk kullanımda otomatik)');
2664
- }
2665
- return st;
2666
- } catch { return null; }
2667
- };
2668
-
2740
+ setSeg('sttEngine', (stt && stt.pref) || '');
2741
+ syncPiperLabels();
2742
+ fillVoices();
2669
2743
  syncEngineUi();
2670
2744
  await refreshPiperStatus();
2671
- $('#ttsEngine').addEventListener('change', () => { syncEngineUi(); refreshPiperStatus(); });
2672
- $('#ttsPiperVoice').addEventListener('change', refreshPiperStatus);
2745
+ await refreshSttStatus();
2746
+
2747
+ /* ---------- olaylar ---------- */
2748
+ ['piperSpeed', 'piperSil', 'piperNoise'].forEach((id) => {
2749
+ const el = $('#' + id);
2750
+ if (el) el.addEventListener('input', syncPiperLabels);
2751
+ });
2752
+ document.querySelectorAll('input[name="ttsEngine"]').forEach((r) => {
2753
+ r.addEventListener('change', () => {
2754
+ captureVoice();
2755
+ refreshSeg('ttsEngine');
2756
+ fillVoices();
2757
+ syncEngineUi();
2758
+ refreshPiperStatus();
2759
+ });
2760
+ });
2761
+ document.querySelectorAll('input[name="sttEngine"]').forEach((r) => {
2762
+ r.addEventListener('change', async () => {
2763
+ refreshSeg('sttEngine');
2764
+ const res = await beast.sttProviderSet(r.value).catch(() => null);
2765
+ if (res && res.ok) {
2766
+ await refreshSttStatus();
2767
+ toast('STT motoru: ' + sttName(res));
2768
+ } else {
2769
+ toast('STT motoru değiştirilemedi');
2770
+ }
2771
+ });
2772
+ });
2673
2773
  $('#piperDlBtn').addEventListener('click', async () => {
2674
- const v = $('#ttsPiperVoice').value;
2774
+ captureVoice();
2675
2775
  toast('Piper indiriliyor… (ilk seferde ~80MB)');
2676
2776
  const poll = setInterval(refreshPiperStatus, 4000);
2677
- const r = await beast.piperInstall(v).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
2777
+ const r = await beast.piperInstall(piperVoice).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
2678
2778
  clearInterval(poll);
2679
2779
  await refreshPiperStatus();
2680
2780
  toast(r && r.ok ? 'Piper hazır ✓ — artık çevrimdışı seslendirme yapılır' : 'Piper indirilemedi: ' + ((r && r.error) || '?'));
2681
2781
  });
2682
-
2683
- /* STT durum satırı: yenile + şimdi indir (yüklenirken 3 sn'de bir güncellenir) */
2684
- const refreshSttStatus = async () => {
2685
- try {
2686
- stt = await beast.sttStatus();
2687
- const el = $('#sttStatusTxt');
2688
- if (el && stt) el.textContent = stt.engineLabel + ' · ' + (stateText[stt.state] || stt.state) + (stt.mb ? ' · ' + stt.mb + ' MB' : '');
2689
- return stt;
2690
- } catch { return null; }
2691
- };
2692
2782
  $('#sttDlBtn').addEventListener('click', async () => {
2783
+ const s0 = await refreshSttStatus();
2784
+ if (s0 && s0.state === 'cloud') { toast('Bulut motoru seçili — indirme gerekmez'); return; }
2693
2785
  await beast.sttPrefetchNow().catch(() => {});
2694
2786
  toast('STT modeli indiriliyor/yükleniyor…');
2695
2787
  const poll = setInterval(async () => {
2696
- const s = await refreshSttStatus();
2697
- if (s && (s.state === 'ready' || s.state === 'cloud')) { clearInterval(poll); toast('STT hazır'); }
2788
+ const s2 = await refreshSttStatus();
2789
+ if (s2 && (s2.state === 'ready' || s2.state === 'cloud')) { clearInterval(poll); toast('STT hazır ✓'); }
2698
2790
  }, 3000);
2699
2791
  });
2700
2792
  $('#ttsSave').addEventListener('click', async () => {
2793
+ captureVoice();
2701
2794
  await beast.waSetTts({
2702
2795
  enabled: $('#ttsOn').checked,
2703
- engine: $('#ttsEngine').value,
2704
- edgeVoice: $('#ttsEdgeVoice').value,
2705
- piperVoice: $('#ttsPiperVoice').value,
2796
+ engine: ttsEng(),
2797
+ edgeVoice,
2798
+ piperVoice,
2799
+ piperSpeed: Number($('#piperSpeed').value),
2800
+ piperSilence: Number($('#piperSil').value),
2801
+ piperNoise: Number($('#piperNoise').value),
2706
2802
  chatAutoSpeak: $('#ttsChatAuto').checked,
2707
2803
  baseUrl: $('#ttsUrl').value.trim(),
2708
2804
  key: $('#ttsKey').value.trim(),
@@ -2717,7 +2813,7 @@ async function renderTtsPane() { const pane = $('#tab-tts');
2717
2813
  });
2718
2814
  $('#ttsTest').addEventListener('click', async () => {
2719
2815
  /* motor + IPC + çalma zincirini uçtan uca dener — sorun neredeyse görünür */
2720
- const eng = $('#ttsEngine').value;
2816
+ const eng = ttsEng();
2721
2817
  const line = eng === 'piper'
2722
2818
  ? 'Merhaba kanka, Piper yerel TTS testi. Ben Beast.'
2723
2819
  : eng === 'openai'
@@ -1741,6 +1741,63 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
1741
1741
  .inp:focus { border-color: var(--accent); }
1742
1742
  .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 10px; }
1743
1743
 
1744
+ /* ---------------- 🎙 Sesli Yanıt (STT / TTS) ---------------- */
1745
+ .voice-card {
1746
+ border: 1px solid var(--border);
1747
+ border-radius: 12px;
1748
+ background: var(--panel);
1749
+ padding: 16px 18px;
1750
+ max-width: 640px;
1751
+ }
1752
+ .voice-card + .voice-card { margin-top: 14px; }
1753
+ .voice-head { display: flex; align-items: center; gap: 11px; }
1754
+ .voice-ico {
1755
+ width: 34px; height: 34px; flex: none;
1756
+ border-radius: 9px;
1757
+ display: flex; align-items: center; justify-content: center;
1758
+ font-size: 17px;
1759
+ background: var(--accent-dim);
1760
+ color: var(--accent);
1761
+ }
1762
+ .voice-title { font-weight: 700; font-size: 13.5px; }
1763
+ .voice-sub { font-size: 12px; color: var(--muted); margin-top: 1px; }
1764
+ .voice-seg { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 13px; }
1765
+ .voice-seg label {
1766
+ position: relative;
1767
+ border: 1px solid var(--border);
1768
+ border-radius: 9px;
1769
+ padding: 7px 13px;
1770
+ font-size: 12.5px;
1771
+ cursor: pointer;
1772
+ color: var(--muted);
1773
+ background: var(--panel2);
1774
+ user-select: none;
1775
+ transition: border-color .12s, color .12s, background .12s;
1776
+ }
1777
+ .voice-seg label:hover { border-color: var(--accent); color: var(--text); }
1778
+ .voice-seg input { position: absolute; opacity: 0; pointer-events: none; }
1779
+ .voice-seg label.on { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); font-weight: 700; }
1780
+ .voice-row { display: flex; align-items: center; gap: 12px; margin-top: 12px; flex-wrap: wrap; }
1781
+ .voice-row .inp { flex: 1; min-width: 220px; margin: 0; }
1782
+ .voice-status { display: flex; align-items: center; gap: 8px; margin-top: 12px; font-size: 12.5px; color: var(--muted); flex-wrap: wrap; }
1783
+ .voice-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
1784
+ .voice-dot.ok { background: var(--ok); }
1785
+ .voice-dot.warn { background: #d9a441; }
1786
+ .voice-sliders { margin-top: 12px; border-top: 1px dashed var(--border); padding-top: 10px; }
1787
+ .voice-slider {
1788
+ display: grid;
1789
+ grid-template-columns: 132px 1fr 48px;
1790
+ align-items: center;
1791
+ gap: 10px;
1792
+ margin-top: 7px;
1793
+ font-size: 12.5px;
1794
+ color: var(--muted);
1795
+ }
1796
+ .voice-slider input[type=range] { width: 100%; accent-color: var(--accent); }
1797
+ .voice-slider b { text-align: right; color: var(--text); font-variant-numeric: tabular-nums; }
1798
+ .voice-actions { display: flex; gap: 8px; margin-top: 15px; }
1799
+ .voice-note { font-size: 12px; color: var(--muted); margin-top: 11px; line-height: 1.5; }
1800
+
1744
1801
  /* ---------------- ☢ Fallout ---------------- */
1745
1802
  .fo-toggles {
1746
1803
  display: flex;