beast-agent 2.44.12 → 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.12",
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,6 +195,29 @@ async function install(voiceId) {
195
195
  }
196
196
  }
197
197
 
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
+
198
221
  /* ---------- sentez: metin → { audio: WAV buffer, mime } ----------
199
222
  DİKKAT: piper'ı ASLA stdout'a yazdırmıyoruz (--output_file -). Windows'ta
200
223
  binary stdout akışı bozuluyor — çıktı gürültülü/cızırtılı geliyor
@@ -215,6 +238,7 @@ function synthesize(text, opts = {}) {
215
238
  '--model', voicePath(vid),
216
239
  '--config', voiceJsonPath(vid),
217
240
  '--output_file', outFile,
241
+ ...tuningArgs(opts),
218
242
  '-q',
219
243
  ];
220
244
  const p = spawn(exePath(), args, { windowsHide: true, cwd: binDir() });
@@ -250,6 +274,7 @@ module.exports = {
250
274
  synthesize,
251
275
  install,
252
276
  status,
277
+ tuningArgs,
253
278
  ensureRuntime,
254
279
  ensureVoice,
255
280
  runtimeInstalled,
package/src/main.js CHANGED
@@ -1060,6 +1060,9 @@ async function synthesizeSpeech(text) {
1060
1060
  if (engine === 'piper') {
1061
1061
  return await piper.synthesize(String(text).slice(0, 4000), {
1062
1062
  voice: cfg.piperVoice || piper.DEFAULT_VOICE,
1063
+ speed: cfg.piperSpeed,
1064
+ sentenceSilence: cfg.piperSilence,
1065
+ noiseScale: cfg.piperNoise,
1063
1066
  });
1064
1067
  }
1065
1068
  if (!cfg.baseUrl || !cfg.key) return null;
@@ -12879,6 +12882,13 @@ ipcMain.handle('wa:queue:get', () => mqueue.stats());
12879
12882
 
12880
12883
  ipcMain.handle('wa:sessions', () => [...waChats.values()]);
12881
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
+
12882
12892
  ipcMain.handle('wa:tts:get', () => settings.waTts || {});
12883
12893
  ipcMain.handle('wa:tts:set', (_e, cfg) => {
12884
12894
  const eng = String((cfg && cfg.engine) || 'edge');
@@ -12889,6 +12899,10 @@ ipcMain.handle('wa:tts:set', (_e, cfg) => {
12889
12899
  engine: ['edge', 'piper', 'openai'].includes(eng) ? eng : 'edge',
12890
12900
  edgeVoice: String((cfg && cfg.edgeVoice) || 'tr-TR-AhmetNeural').trim(),
12891
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),
12892
12906
  chatAutoSpeak: !!(cfg && cfg.chatAutoSpeak),
12893
12907
  baseUrl: String((cfg && cfg.baseUrl) || '').trim(),
12894
12908
  key: String((cfg && cfg.key) || '').trim(),
@@ -269,6 +269,12 @@
269
269
  tts_test: 'Test sesi çal',
270
270
  stt_download_now: 'Şimdi indir / hazırla',
271
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',
272
278
  stt_engine_auto: 'Otomatik — anahtar varsa bulut, yoksa yerel Whisper',
273
279
  stt_engine_local: 'Whisper — yerel/offline (large-v3-turbo)',
274
280
  stt_engine_groq: 'Groq Whisper — bulut (hızlı)',
@@ -278,6 +284,9 @@
278
284
  tts_edge_voice: 'Edge sesi',
279
285
  tts_piper_voice: 'Piper sesi',
280
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)',
281
290
  tts_chat_auto: 'Chat\'te ajanın yazdıklarını otomatik seslendir',
282
291
  tts_on: 'TTS açık — cevaplar sesli de gider',
283
292
  tts_off: 'TTS kapalı',
@@ -1000,6 +1009,12 @@
1000
1009
  tts_test: 'Play test sound',
1001
1010
  stt_download_now: 'Download / prepare now',
1002
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',
1003
1018
  stt_engine_auto: 'Auto — cloud if a key exists, otherwise local Whisper',
1004
1019
  stt_engine_local: 'Whisper — local/offline (large-v3-turbo)',
1005
1020
  stt_engine_groq: 'Groq Whisper — cloud (fast)',
@@ -1009,6 +1024,9 @@
1009
1024
  tts_edge_voice: 'Edge voice',
1010
1025
  tts_piper_voice: 'Piper voice',
1011
1026
  tts_piper_download: 'Download / prepare Piper',
1027
+ tts_piper_speed: 'Speaking speed',
1028
+ tts_piper_silence: 'Sentence pause',
1029
+ tts_piper_noise: 'Expressiveness',
1012
1030
  tts_chat_auto: 'Auto-speak agent replies in chat',
1013
1031
  tts_on: 'TTS on — replies also go as voice',
1014
1032
  tts_off: 'TTS off',
@@ -2554,178 +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
- `<div class="form-grid" style="grid-template-columns:auto 1fr;align-items:center;margin-bottom:8px">
2596
- <label style="grid-column:1">${_t('stt_engine')}</label>
2597
- <select id="sttEngine" class="inp" style="grid-column:2">
2598
- <option value="">${_t('stt_engine_auto')}</option>
2599
- <option value="local">${_t('stt_engine_local')}</option>
2600
- <option value="groq">${_t('stt_engine_groq')}</option>
2601
- <option value="openai">${_t('stt_engine_openai')}</option>
2602
- </select>
2603
- </div>` +
2604
- `<button id="sttDlBtn" class="btn ghost" style="margin-bottom:14px">${_t('stt_download_now')}</button>` +
2605
- '<div class="sub" style="margin:10px 0 4px;font-weight:700;color:var(--accent)">TTS (Yazı → Ses)</div>' +
2606
- `<div class="form-grid" style="grid-template-columns:auto 1fr 1fr;align-items:center;margin-top:10px">
2607
- <label class="lock-row"><input type="checkbox" id="ttsOn" /><span>${_t('tts_active')}</span></label>
2608
- <label style="grid-column:1">${_t('tts_engine')}</label>
2609
- <select id="ttsEngine" class="inp" style="grid-column:2/4">
2610
- <option value="edge">Edge TTS — ücretsiz bulut (Ahmet/Emel…)</option>
2611
- <option value="piper">Piper — yerel & offline (Fahrettin/Fettah…)</option>
2612
- <option value="openai">OpenAI-uyumlu API (tts-1, ses: alloy…)</option>
2613
- </select>
2614
- <label style="grid-column:1">${_t('tts_edge_voice')}</label>
2615
- <select id="ttsEdgeVoice" class="inp" style="grid-column:2/4">${edgeOpts}</select>
2616
- <label style="grid-column:1">${_t('tts_piper_voice')}</label>
2617
- <select id="ttsPiperVoice" class="inp" style="grid-column:2/4">${piperOpts}</select>
2618
- <span id="piperStatusRow" style="grid-column:1/4;display:flex;gap:10px;align-items:center">
2619
- <span class="sub" id="piperStatusTxt" style="margin:0"></span>
2620
- <button id="piperDlBtn" class="btn ghost">${_t('tts_piper_download')}</button>
2621
- </span>
2622
- <label class="lock-row" style="grid-column:1/4"><input type="checkbox" id="ttsChatAuto" /><span>${_t('tts_chat_auto')}</span></label>
2623
- <span id="ttsOpenaiWrap" style="display:contents">
2624
- <input id="ttsUrl" class="inp" placeholder="https://api.openai.com/v1" autocomplete="off" />
2625
- <input id="ttsKey" class="inp" type="password" placeholder="API Key" autocomplete="off" />
2626
- <input id="ttsModel" class="inp" placeholder="tts-1" autocomplete="off" />
2627
- <input id="ttsVoice" class="inp" placeholder="ses: alloy" autocomplete="off" />
2628
- </span>
2629
- <button id="ttsSave" class="btn ghost" style="grid-column:1">${_t('tts_save')}</button>
2630
- <button id="ttsTest" class="btn ghost" style="grid-column:2;justify-self:start">${_t('tts_test')}</button>
2631
- </div>` +
2632
- '<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
+ };
2633
2680
 
2634
2681
  const syncEngineUi = () => {
2635
- const eng = $('#ttsEngine').value;
2636
- const isEdge = eng === 'edge';
2682
+ const eng = ttsEng();
2637
2683
  const isPiper = eng === 'piper';
2638
- $('#ttsEdgeVoice').disabled = !isEdge;
2639
- $('#ttsPiperVoice').disabled = !isPiper;
2640
- $('#piperDlBtn').disabled = !isPiper;
2641
- $('#piperStatusTxt').style.opacity = isPiper ? '1' : '0.45';
2642
- $('#ttsOpenaiWrap').style.opacity = eng === 'openai' ? '1' : '0.35';
2643
- $('#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; }
2644
2722
  };
2645
2723
 
2724
+ /* ---------- ayarları yükle ---------- */
2646
2725
  try {
2647
2726
  const tts = await beast.waGetTts();
2648
2727
  $('#ttsOn').checked = !!tts.enabled;
2649
- $('#ttsEngine').value = ['edge', 'piper', 'openai'].includes(tts.engine) ? tts.engine : 'edge';
2650
- $('#ttsEdgeVoice').value = tts.edgeVoice || 'tr-TR-AhmetNeural';
2651
- $('#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;
2652
2733
  $('#ttsChatAuto').checked = !!tts.chatAutoSpeak;
2653
2734
  $('#ttsUrl').value = tts.baseUrl || '';
2654
2735
  $('#ttsKey').value = tts.key || '';
2655
2736
  $('#ttsModel').value = tts.model || 'tts-1';
2656
2737
  $('#ttsVoice').value = tts.voice || 'alloy';
2738
+ setSeg('ttsEngine', ['edge', 'piper', 'openai'].includes(tts.engine) ? tts.engine : 'edge');
2657
2739
  } catch {}
2658
-
2659
- /* STT motoru seçimi: '' = otomatik (bulut anahtarı varsa bulut, yoksa yerel) */
2660
- const sttEngineSel = $('#sttEngine');
2661
- if (sttEngineSel) sttEngineSel.value = (stt && stt.pref) || '';
2662
-
2663
- /* Piper durum satırı: runtime/ses modeli hazır mı (indirilince güncellenir) */
2664
- const refreshPiperStatus = async () => {
2665
- const v = $('#ttsPiperVoice') ? $('#ttsPiperVoice').value : '';
2666
- try {
2667
- const st = await beast.piperStatus(v);
2668
- const el = $('#piperStatusTxt');
2669
- if (el && st) {
2670
- el.textContent = 'Piper: ' + (st.installed
2671
- ? 'hazır — yerel/offline'
2672
- : st.installing
2673
- ? 'indiriliyor…'
2674
- : st.runtime
2675
- ? 'ses modeli indirilecek (ilk kullanımda otomatik)'
2676
- : 'runtime + ses modeli indirilecek (ilk kullanımda otomatik)');
2677
- }
2678
- return st;
2679
- } catch { return null; }
2680
- };
2681
-
2740
+ setSeg('sttEngine', (stt && stt.pref) || '');
2741
+ syncPiperLabels();
2742
+ fillVoices();
2682
2743
  syncEngineUi();
2683
2744
  await refreshPiperStatus();
2684
- $('#ttsEngine').addEventListener('change', () => { syncEngineUi(); refreshPiperStatus(); });
2685
- $('#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
+ });
2686
2773
  $('#piperDlBtn').addEventListener('click', async () => {
2687
- const v = $('#ttsPiperVoice').value;
2774
+ captureVoice();
2688
2775
  toast('Piper indiriliyor… (ilk seferde ~80MB)');
2689
2776
  const poll = setInterval(refreshPiperStatus, 4000);
2690
- 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) }));
2691
2778
  clearInterval(poll);
2692
2779
  await refreshPiperStatus();
2693
2780
  toast(r && r.ok ? 'Piper hazır ✓ — artık çevrimdışı seslendirme yapılır' : 'Piper indirilemedi: ' + ((r && r.error) || '?'));
2694
2781
  });
2695
-
2696
- /* STT durum satırı: yenile + şimdi indir (yüklenirken 3 sn'de bir güncellenir) */
2697
- const refreshSttStatus = async () => {
2698
- try {
2699
- stt = await beast.sttStatus();
2700
- const el = $('#sttStatusTxt');
2701
- if (el && stt) el.textContent = stt.engineLabel + ' · ' + (stateText[stt.state] || stt.state) + (stt.mb ? ' · ' + stt.mb + ' MB' : '');
2702
- return stt;
2703
- } catch { return null; }
2704
- };
2705
2782
  $('#sttDlBtn').addEventListener('click', async () => {
2783
+ const s0 = await refreshSttStatus();
2784
+ if (s0 && s0.state === 'cloud') { toast('Bulut motoru seçili — indirme gerekmez'); return; }
2706
2785
  await beast.sttPrefetchNow().catch(() => {});
2707
2786
  toast('STT modeli indiriliyor/yükleniyor…');
2708
2787
  const poll = setInterval(async () => {
2709
- const s = await refreshSttStatus();
2710
- 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 ✓'); }
2711
2790
  }, 3000);
2712
2791
  });
2713
- /* STT motoru: Whisper yerel / Groq / OpenAI-uyumlu — anında kaydedilir */
2714
- if (sttEngineSel) sttEngineSel.addEventListener('change', async () => {
2715
- const r = await beast.sttProviderSet(sttEngineSel.value).catch(() => null);
2716
- if (r && r.ok) {
2717
- await refreshSttStatus();
2718
- toast('STT motoru: ' + (r.engineLabel || 'otomatik'));
2719
- } else {
2720
- toast('STT motoru değiştirilemedi');
2721
- }
2722
- });
2723
2792
  $('#ttsSave').addEventListener('click', async () => {
2793
+ captureVoice();
2724
2794
  await beast.waSetTts({
2725
2795
  enabled: $('#ttsOn').checked,
2726
- engine: $('#ttsEngine').value,
2727
- edgeVoice: $('#ttsEdgeVoice').value,
2728
- 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),
2729
2802
  chatAutoSpeak: $('#ttsChatAuto').checked,
2730
2803
  baseUrl: $('#ttsUrl').value.trim(),
2731
2804
  key: $('#ttsKey').value.trim(),
@@ -2740,7 +2813,7 @@ async function renderTtsPane() { const pane = $('#tab-tts');
2740
2813
  });
2741
2814
  $('#ttsTest').addEventListener('click', async () => {
2742
2815
  /* motor + IPC + çalma zincirini uçtan uca dener — sorun neredeyse görünür */
2743
- const eng = $('#ttsEngine').value;
2816
+ const eng = ttsEng();
2744
2817
  const line = eng === 'piper'
2745
2818
  ? 'Merhaba kanka, Piper yerel TTS testi. Ben Beast.'
2746
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;