beast-agent 2.44.10 → 2.44.12

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.10",
4
+ "version": "2.44.12",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -0,0 +1,257 @@
1
+ 'use strict';
2
+
3
+ /* Beast Piper TTS: tamamen YEREL/offline neural seslendirme (rhasspy/piper).
4
+ - Runtime: piper_windows_amd64.zip (~21MB) → %APPDATA%\beast\piper\bin
5
+ (piper.exe + espeak-ng-data + onnxruntime dll'leri birlikte gelir)
6
+ - Sesler: diffusionstudio/piper-voices aynası → %APPDATA%\beast\piper\voices
7
+ (tr_TR fahrettin/fettah medium = CC0; en_US lessac/amy medium)
8
+ - piper.exe stdin metin → stdout WAV (22.05kHz mono 16-bit). mp3/ogg
9
+ dönüşümü gerekmez: WhatsApp sesli not yolu ffmpeg ile ogg/opus'a çevirir,
10
+ masaüstü oynatıcı WAV'ı doğrudan çalar.
11
+ - İndirmeler kilitli: aynı anda iki kez başlamaz; ilerleme progressbus
12
+ üzerinden Kurulum/Ayarlar panesine akar. Hata olursa bir sonraki
13
+ çağrıda yeniden denenir. */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { spawn, execFile } = require('child_process');
18
+ const { Readable } = require('stream');
19
+ const { pipeline } = require('stream/promises');
20
+ const { beastRoot } = require('./memory');
21
+ const log = require('./logger');
22
+ const { emitInstallProgress } = require('./progressbus');
23
+
24
+ const PIPER_VERSION = '2023.11.14-2';
25
+ const RUNTIME_URL = `https://github.com/rhasspy/piper/releases/download/${PIPER_VERSION}/piper_windows_amd64.zip`;
26
+ const VOICE_BASE = 'https://huggingface.co/diffusionstudio/piper-voices/resolve/main';
27
+ const DEFAULT_VOICE = 'tr_TR-fahrettin-medium';
28
+
29
+ /* UI'da listelenen sesler — indirme url'si VOICE_BASE + path + .onnx(.json) */
30
+ const VOICES = {
31
+ 'tr_TR-fahrettin-medium': { name: 'Fahrettin — Türkçe (erkek)', path: 'tr/tr_TR/fahrettin/medium/tr_TR-fahrettin-medium' },
32
+ 'tr_TR-fettah-medium': { name: 'Fettah — Türkçe (erkek)', path: 'tr/tr_TR/fettah/medium/tr_TR-fettah-medium' },
33
+ 'en_US-lessac-medium': { name: 'Lessac — English (male)', path: 'en/en_US/lessac/medium/en_US-lessac-medium' },
34
+ 'en_US-amy-medium': { name: 'Amy — English (female)', path: 'en/en_US/amy/medium/en_US-amy-medium' },
35
+ };
36
+
37
+ function rootDir() {
38
+ return path.join(beastRoot(), 'piper');
39
+ }
40
+ function binDir() {
41
+ return path.join(rootDir(), 'bin');
42
+ }
43
+ function voicesDir() {
44
+ return path.join(rootDir(), 'voices');
45
+ }
46
+ function exePath() {
47
+ return path.join(binDir(), 'piper.exe');
48
+ }
49
+ function voicePath(id) {
50
+ return path.join(voicesDir(), String(id) + '.onnx');
51
+ }
52
+ function voiceJsonPath(id) {
53
+ return voicePath(id) + '.json';
54
+ }
55
+
56
+ function resolveVoice(id) {
57
+ const v = String(id || '').trim();
58
+ return VOICES[v] ? v : DEFAULT_VOICE;
59
+ }
60
+
61
+ function runtimeInstalled() {
62
+ try {
63
+ return fs.existsSync(exePath());
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ function voiceInstalled(id) {
69
+ try {
70
+ return !!VOICES[id] && fs.existsSync(voicePath(id)) && fs.existsSync(voiceJsonPath(id));
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /* ---------- indirme (yüzde akışlı, .part → atomik rename) ---------- */
77
+
78
+ async function download(url, dest, tag) {
79
+ const res = await fetch(url, { redirect: 'follow' });
80
+ if (!res.ok || !res.body) throw new Error('indirilemedi (http ' + res.status + ')');
81
+ const total = Number(res.headers.get('content-length')) || 0;
82
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
83
+ const tmp = dest + '.part';
84
+ let loaded = 0;
85
+ let last = 0;
86
+ const rs = Readable.fromWeb(res.body);
87
+ rs.on('data', (c) => {
88
+ loaded += c.length;
89
+ const now = Date.now();
90
+ if (now - last > 500) {
91
+ last = now;
92
+ emitInstallProgress(tag, { pct: total ? Math.round((loaded / total) * 100) : 0, loaded, total });
93
+ }
94
+ });
95
+ await pipeline(rs, fs.createWriteStream(tmp));
96
+ fs.renameSync(tmp, dest);
97
+ emitInstallProgress(tag, { pct: 100, loaded, total: total || loaded });
98
+ }
99
+
100
+ function unzip(zip, dest) {
101
+ return new Promise((resolve, reject) => {
102
+ const q = (s) => "'" + String(s).replace(/'/g, "''") + "'";
103
+ execFile(
104
+ 'powershell.exe',
105
+ ['-NoProfile', '-NonInteractive', '-Command',
106
+ `Expand-Archive -LiteralPath ${q(zip)} -DestinationPath ${q(dest)} -Force`],
107
+ { timeout: 5 * 60 * 1000, windowsHide: true },
108
+ (err) => (err ? reject(err) : resolve())
109
+ );
110
+ });
111
+ }
112
+
113
+ /* ---------- kurulum (kilitli) ---------- */
114
+
115
+ let runtimePromise = null;
116
+ const voicePromises = new Map();
117
+
118
+ function ensureRuntime() {
119
+ if (runtimeInstalled()) return Promise.resolve(true);
120
+ if (runtimePromise) return runtimePromise;
121
+ runtimePromise = (async () => {
122
+ const zip = path.join(rootDir(), 'piper-' + PIPER_VERSION + '.zip');
123
+ log.info('piper', 'runtime indiriliyor: ' + RUNTIME_URL);
124
+ await download(RUNTIME_URL, zip, 'piper-runtime');
125
+ const tmp = path.join(rootDir(), '.extract');
126
+ fs.rmSync(tmp, { recursive: true, force: true });
127
+ fs.mkdirSync(tmp, { recursive: true });
128
+ await unzip(zip, tmp);
129
+ const inner = path.join(tmp, 'piper');
130
+ const src = fs.existsSync(path.join(inner, 'piper.exe')) ? inner : tmp;
131
+ fs.mkdirSync(binDir(), { recursive: true });
132
+ for (const e of fs.readdirSync(src)) {
133
+ const from = path.join(src, e);
134
+ const to = path.join(binDir(), e);
135
+ fs.rmSync(to, { recursive: true, force: true });
136
+ fs.renameSync(from, to);
137
+ }
138
+ fs.rmSync(tmp, { recursive: true, force: true });
139
+ try { fs.unlinkSync(zip); } catch {}
140
+ if (!runtimeInstalled()) throw new Error('piper.exe çıkarılamadı');
141
+ log.info('piper', 'runtime hazır: ' + exePath());
142
+ return true;
143
+ })().catch((e) => {
144
+ log.error('piper', 'runtime kurulumu başarısız: ' + String((e && e.message) || e));
145
+ throw e;
146
+ }).finally(() => {
147
+ runtimePromise = null; // kilit çözülür; kuruluysa yukarıdaki kısa yol devrede
148
+ });
149
+ return runtimePromise;
150
+ }
151
+
152
+ function ensureVoice(id) {
153
+ const vid = resolveVoice(id);
154
+ if (voiceInstalled(vid)) return Promise.resolve(true);
155
+ if (voicePromises.has(vid)) return voicePromises.get(vid);
156
+ const p = (async () => {
157
+ const base = VOICE_BASE + '/' + VOICES[vid].path;
158
+ log.info('piper', 'ses modeli indiriliyor: ' + vid);
159
+ await download(base + '.onnx', voicePath(vid), 'piper-voice');
160
+ await download(base + '.onnx.json', voiceJsonPath(vid), 'piper-voice');
161
+ if (!voiceInstalled(vid)) throw new Error('ses modeli indirilemedi: ' + vid);
162
+ log.info('piper', 'ses hazır: ' + vid);
163
+ return true;
164
+ })().catch((e) => {
165
+ log.error('piper', 'ses indirme başarısız (' + vid + '): ' + String((e && e.message) || e));
166
+ throw e;
167
+ }).finally(() => {
168
+ voicePromises.delete(vid); // kilit çözülür; kuruluysa yukarıdaki kısa yol devrede
169
+ });
170
+ voicePromises.set(vid, p);
171
+ return p;
172
+ }
173
+
174
+ /* UI/Kurulum için durum: { installed, runtime, voice, voiceId, installing } */
175
+ function status(voiceId) {
176
+ const vid = resolveVoice(voiceId);
177
+ return {
178
+ installed: runtimeInstalled() && voiceInstalled(vid),
179
+ runtime: runtimeInstalled(),
180
+ voice: voiceInstalled(vid),
181
+ voiceId: vid,
182
+ installing: !!runtimePromise || voicePromises.has(vid),
183
+ };
184
+ }
185
+
186
+ /* elle/otomatik kurulum: runtime + ses (hata fırlatmaz, {ok} döner) */
187
+ async function install(voiceId) {
188
+ const vid = resolveVoice(voiceId);
189
+ try {
190
+ await ensureRuntime();
191
+ await ensureVoice(vid);
192
+ return { ok: true, voice: vid };
193
+ } catch (e) {
194
+ return { ok: false, voice: vid, error: String((e && e.message) || e) };
195
+ }
196
+ }
197
+
198
+ /* ---------- sentez: metin → { audio: WAV buffer, mime } ----------
199
+ DİKKAT: piper'ı ASLA stdout'a yazdırmıyoruz (--output_file -). Windows'ta
200
+ binary stdout akışı bozuluyor — çıktı gürültülü/cızırtılı geliyor
201
+ (rhasspy/piper#211: "piping raw output... choppy"). Geçici WAV dosyasına
202
+ yazıp okuyoruz; dosya çıktısı temiz. */
203
+
204
+ function synthesize(text, opts = {}) {
205
+ const vid = resolveVoice(opts.voice);
206
+ return (async () => {
207
+ await ensureRuntime();
208
+ await ensureVoice(vid);
209
+ const tmpDir = path.join(rootDir(), 'tmp');
210
+ fs.mkdirSync(tmpDir, { recursive: true });
211
+ const outFile = path.join(tmpDir, 'out-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8) + '.wav');
212
+ try {
213
+ await new Promise((resolve, reject) => {
214
+ const args = [
215
+ '--model', voicePath(vid),
216
+ '--config', voiceJsonPath(vid),
217
+ '--output_file', outFile,
218
+ '-q',
219
+ ];
220
+ const p = spawn(exePath(), args, { windowsHide: true, cwd: binDir() });
221
+ let err = '';
222
+ const timer = setTimeout(() => {
223
+ try { p.kill(); } catch {}
224
+ reject(new Error('piper zaman aşımı'));
225
+ }, 120000);
226
+ p.stderr.on('data', (d) => { err += d.toString(); });
227
+ p.on('error', (e) => { clearTimeout(timer); reject(e); });
228
+ p.on('close', (code) => {
229
+ clearTimeout(timer);
230
+ if (code !== 0) return reject(new Error('piper başarısız (kod ' + code + '): ' + err.slice(0, 140)));
231
+ resolve();
232
+ });
233
+ p.stdin.write(String(text).slice(0, 4000));
234
+ p.stdin.end();
235
+ });
236
+ const buf = fs.readFileSync(outFile);
237
+ if (buf.length < 44 || buf.slice(0, 4).toString('latin1') !== 'RIFF') {
238
+ throw new Error('geçersiz WAV çıktısı');
239
+ }
240
+ return { audio: buf, mime: 'audio/wav' };
241
+ } finally {
242
+ try { fs.unlinkSync(outFile); } catch {}
243
+ }
244
+ })();
245
+ }
246
+
247
+ module.exports = {
248
+ VOICES,
249
+ DEFAULT_VOICE,
250
+ synthesize,
251
+ install,
252
+ status,
253
+ ensureRuntime,
254
+ ensureVoice,
255
+ runtimeInstalled,
256
+ voiceInstalled,
257
+ };
package/src/main.js CHANGED
@@ -26,6 +26,7 @@ const watchers = require('./agent/watchers');
26
26
  const usageMod = require('./agent/usage');
27
27
  const bus = require('./agent/bus');
28
28
  const computeruse = require('./agent/computeruse');
29
+ const piper = require('./agent/piper');
29
30
  const fsguard = require('./agent/fsguard');
30
31
  const log = require('./agent/logger');
31
32
  const squeeze = require('./agent/squeeze');
@@ -702,7 +703,10 @@ function sttBaseUrl() {
702
703
  }
703
704
  function sttProvider() {
704
705
  const explicit = String(settings.sttProvider || '').trim().toLowerCase();
705
- 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';
706
710
  if (sttGroqKey()) return 'groq';
707
711
  if (sttOpenaiKey() || sttBaseUrl()) return 'openai';
708
712
  return 'local';
@@ -1035,17 +1039,28 @@ async function transcribePcm(audio, langOverride) {
1035
1039
  }
1036
1040
  }
1037
1041
 
1042
+ /* TTS sentezi: dönen değer { audio: Buffer, mime } — motor seçilebilir.
1043
+ - edge: ücretsiz Microsoft neural (MP3)
1044
+ - piper: tamamen yerel/offline neural (WAV; ilk kullanımda indirilir)
1045
+ - openai: OpenAI-uyumlu /audio/speech API (MP3) */
1038
1046
  async function synthesizeSpeech(text) {
1039
1047
  const cfg = settings.waTts || {};
1040
1048
  if (!cfg.enabled) return null;
1041
1049
  try {
1050
+ const engine = cfg.engine || 'edge';
1042
1051
  /* EDGE TTS: ücretsiz yerel motor — baseUrl/key GEREKMEZ */
1043
- if ((cfg.engine || 'edge') === 'edge') {
1052
+ if (engine === 'edge') {
1044
1053
  const edge = require('./agent/edgetts');
1045
1054
  const audio = await edge.synthesize(String(text).slice(0, 4000), {
1046
1055
  voice: cfg.edgeVoice || 'tr-TR-AhmetNeural',
1047
1056
  });
1048
- return audio;
1057
+ return audio && { audio, mime: 'audio/mpeg' };
1058
+ }
1059
+ /* PIPER: yerel/offline — ilk kullanımda runtime + ses modeli indirilir */
1060
+ if (engine === 'piper') {
1061
+ return await piper.synthesize(String(text).slice(0, 4000), {
1062
+ voice: cfg.piperVoice || piper.DEFAULT_VOICE,
1063
+ });
1049
1064
  }
1050
1065
  if (!cfg.baseUrl || !cfg.key) return null;
1051
1066
  const url = String(cfg.baseUrl).replace(/\/+$/, '') + '/audio/speech';
@@ -1068,7 +1083,7 @@ async function synthesizeSpeech(text) {
1068
1083
  waLog(`tts http ${res.status}`);
1069
1084
  return null;
1070
1085
  }
1071
- return Buffer.from(await res.arrayBuffer());
1086
+ return { audio: Buffer.from(await res.arrayBuffer()), mime: 'audio/mpeg' };
1072
1087
  } catch (e) {
1073
1088
  waLog('tts hata: ' + String((e && e.message) || e));
1074
1089
  return null;
@@ -3385,7 +3400,7 @@ function reloadBackend() {
3385
3400
  return;
3386
3401
  }
3387
3402
  const voice = await synthesizeSpeech(txt);
3388
- if (voice) await wa.sendAudio(wajid, voice).catch(() => {});
3403
+ if (voice && voice.audio) await wa.sendAudio(wajid, voice.audio).catch(() => {});
3389
3404
  }
3390
3405
  }
3391
3406
  } catch {}
@@ -6627,12 +6642,21 @@ ipcMain.handle('stt:transcribe', async (_e, b64, lang, priority) => {
6627
6642
 
6628
6643
  ipcMain.handle('stt:lang:get', () => settings.sttLang || 'tr');
6629
6644
 
6645
+ /* STT MOTORU SEÇİMİ: '' = otomatik (anahtar varsa bulut, yoksa yerel Whisper),
6646
+ 'local' = yerel Whisper, 'groq' | 'openai' = bulut */
6647
+ ipcMain.handle('stt:provider:set', (_e, p) => {
6648
+ const v = String(p || '').trim().toLowerCase();
6649
+ settings.sttProvider = ['local', 'groq', 'openai'].includes(v) ? v : '';
6650
+ saveSettings();
6651
+ return { ok: true, pref: settings.sttProvider, provider: sttProvider(), engineLabel: sttEngineLabel() };
6652
+ });
6653
+
6630
6654
  /* STT DURUMU: ayarlar ekranı için — model indirildi mi/in-progress mi,
6631
6655
  hangi motor, ne kadar disk */
6632
6656
  ipcMain.handle('stt:status', () => {
6633
6657
  const provider = sttProvider();
6634
6658
  const model = sttModelName();
6635
- const out = { provider, engineLabel: sttEngineLabel(), model };
6659
+ const out = { provider, engineLabel: sttEngineLabel(), model, pref: String(settings.sttProvider || '') };
6636
6660
  if (provider !== 'local') { out.state = 'cloud'; return out; }
6637
6661
  const dir = path.join(APP_DIR, 'models', ...model.split('/'));
6638
6662
  let files = [];
@@ -12857,11 +12881,14 @@ ipcMain.handle('wa:sessions', () => [...waChats.values()]);
12857
12881
 
12858
12882
  ipcMain.handle('wa:tts:get', () => settings.waTts || {});
12859
12883
  ipcMain.handle('wa:tts:set', (_e, cfg) => {
12884
+ const eng = String((cfg && cfg.engine) || 'edge');
12885
+ const pv = String((cfg && cfg.piperVoice) || '').trim();
12860
12886
  settings.waTts = {
12861
12887
  enabled: !!(cfg && cfg.enabled),
12862
- /* motor: 'edge' (ücretsiz yerel, varsayılan) | 'openai' (OpenAI-uyumlu API) */
12863
- engine: (cfg && cfg.engine) === 'openai' ? 'openai' : 'edge',
12888
+ /* motor: 'edge' (ücretsiz bulut, varsayılan) | 'piper' (yerel/offline) | 'openai' (API) */
12889
+ engine: ['edge', 'piper', 'openai'].includes(eng) ? eng : 'edge',
12864
12890
  edgeVoice: String((cfg && cfg.edgeVoice) || 'tr-TR-AhmetNeural').trim(),
12891
+ piperVoice: piper.VOICES[pv] ? pv : piper.DEFAULT_VOICE,
12865
12892
  chatAutoSpeak: !!(cfg && cfg.chatAutoSpeak),
12866
12893
  baseUrl: String((cfg && cfg.baseUrl) || '').trim(),
12867
12894
  key: String((cfg && cfg.key) || '').trim(),
@@ -12872,6 +12899,15 @@ ipcMain.handle('wa:tts:set', (_e, cfg) => {
12872
12899
  return settings.waTts;
12873
12900
  });
12874
12901
 
12902
+ /* PIPER (yerel/offline TTS): durum + elle indirme (runtime + ses modeli) */
12903
+ ipcMain.handle('piper:status', (_e, voice) => {
12904
+ try { return piper.status(String(voice || '')); } catch { return { installed: false, runtime: false, voice: false, installing: false }; }
12905
+ });
12906
+ ipcMain.handle('piper:install', async (_e, voice) => {
12907
+ const r = await piper.install(String(voice || ''));
12908
+ return { ...r, status: piper.status(String(voice || '')) };
12909
+ });
12910
+
12875
12911
  /* CHAT TTS: masaüstü sohbetinde ajanın son yazısını seslendirir (base64 mp3).
12876
12912
  chatAutoSpeak açıkken renderer 'done' olayında bu kanalı çağırır. */
12877
12913
  ipcMain.handle('tts:synthesize', async (_e, text) => {
@@ -12879,13 +12915,14 @@ ipcMain.handle('tts:synthesize', async (_e, text) => {
12879
12915
  if (!cfg.enabled) return { ok: false, error: 'tts kapalı' };
12880
12916
  const t = String(text || '').trim();
12881
12917
  if (!t) return { ok: false, error: 'boş metin' };
12882
- const audio = await synthesizeSpeech(t.slice(0, 4000));
12883
- if (!audio) {
12918
+ const out = await synthesizeSpeech(t.slice(0, 4000));
12919
+ if (!out || !out.audio) {
12884
12920
  waLog('tts chat seslendirilemedi — motor: ' + (cfg.engine || 'edge'));
12885
12921
  return { ok: false, error: 'seslendirilemedi' };
12886
12922
  }
12887
- waLog('tts chat ok: ' + audio.length + ' bayt, motor: ' + (cfg.engine || 'edge') + ', ses: ' + (cfg.edgeVoice || '-'));
12888
- return { ok: true, audioB64: audio.toString('base64'), mime: 'audio/mpeg' };
12923
+ const ses = cfg.engine === 'piper' ? (cfg.piperVoice || piper.DEFAULT_VOICE) : (cfg.edgeVoice || '-');
12924
+ waLog('tts chat ok: ' + out.audio.length + ' bayt, motor: ' + (cfg.engine || 'edge') + ', ses: ' + ses);
12925
+ return { ok: true, audioB64: out.audio.toString('base64'), mime: out.mime || 'audio/mpeg' };
12889
12926
  });
12890
12927
 
12891
12928
  /* ---------- Telegram IPC (FEATURE 3) ---------- */
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'),
@@ -203,6 +204,8 @@ contextBridge.exposeInMainWorld('beast', {
203
204
  setWaLockdown: (v) => ipcRenderer.invoke('wa:lockdown:set', v),
204
205
  waGetTts: () => ipcRenderer.invoke('wa:tts:get'),
205
206
  waSetTts: (cfg) => ipcRenderer.invoke('wa:tts:set', cfg),
207
+ piperStatus: (voice) => ipcRenderer.invoke('piper:status', voice),
208
+ piperInstall: (voice) => ipcRenderer.invoke('piper:install', voice),
206
209
  waGetGroups: () => ipcRenderer.invoke('wa:groups:get'),
207
210
  waSetGroups: (cfg) => ipcRenderer.invoke('wa:groups:set', cfg),
208
211
  tgGetStatus: () => ipcRenderer.invoke('tg:status:get'),
@@ -268,9 +268,16 @@
268
268
  tts_save: 'TTS Kaydet',
269
269
  tts_test: 'Test sesi çal',
270
270
  stt_download_now: 'Şimdi indir / hazırla',
271
- tts_note: 'Edge TTS: ücretsiz yerel motor — anahtar gerekmez (Ahmet/Emel vb.). OpenAI motoru: uyumlu speech API (tts-1, tts-1-hd; sesler alloy, echo, fable, onyx, nova, shimmer).',
271
+ stt_engine: 'STT motoru',
272
+ stt_engine_auto: 'Otomatik — anahtar varsa bulut, yoksa yerel Whisper',
273
+ stt_engine_local: 'Whisper — yerel/offline (large-v3-turbo)',
274
+ stt_engine_groq: 'Groq Whisper — bulut (hızlı)',
275
+ stt_engine_openai: 'OpenAI-uyumlu Whisper — bulut',
276
+ 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
277
  tts_engine: 'Motor',
273
278
  tts_edge_voice: 'Edge sesi',
279
+ tts_piper_voice: 'Piper sesi',
280
+ tts_piper_download: 'Piper\'ı indir / hazırla',
274
281
  tts_chat_auto: 'Chat\'te ajanın yazdıklarını otomatik seslendir',
275
282
  tts_on: 'TTS açık — cevaplar sesli de gider',
276
283
  tts_off: 'TTS kapalı',
@@ -992,9 +999,16 @@
992
999
  tts_save: 'Save TTS',
993
1000
  tts_test: 'Play test sound',
994
1001
  stt_download_now: 'Download / prepare now',
995
- tts_note: 'Edge TTS: free local engine — no key needed (Ahmet/Emel etc.). OpenAI engine: compatible speech API (tts-1, tts-1-hd; voices alloy, echo, fable, onyx, nova, shimmer).',
1002
+ stt_engine: 'STT engine',
1003
+ stt_engine_auto: 'Auto — cloud if a key exists, otherwise local Whisper',
1004
+ stt_engine_local: 'Whisper — local/offline (large-v3-turbo)',
1005
+ stt_engine_groq: 'Groq Whisper — cloud (fast)',
1006
+ stt_engine_openai: 'OpenAI-compatible Whisper — cloud',
1007
+ 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).',
996
1008
  tts_engine: 'Engine',
997
1009
  tts_edge_voice: 'Edge voice',
1010
+ tts_piper_voice: 'Piper voice',
1011
+ tts_piper_download: 'Download / prepare Piper',
998
1012
  tts_chat_auto: 'Auto-speak agent replies in chat',
999
1013
  tts_on: 'TTS on — replies also go as voice',
1000
1014
  tts_off: 'TTS off',
@@ -2580,21 +2580,45 @@ async function renderTtsPane() { const pane = $('#tab-tts');
2580
2580
  ]
2581
2581
  .map(([v, n]) => `<option value="${v}">${n}</option>`)
2582
2582
  .join('');
2583
+ const piperOpts = [
2584
+ ['tr_TR-fahrettin-medium', 'Fahrettin — Türkçe (erkek)'],
2585
+ ['tr_TR-fettah-medium', 'Fettah — Türkçe (erkek)'],
2586
+ ['en_US-lessac-medium', 'Lessac — English (male)'],
2587
+ ['en_US-amy-medium', 'Amy — English (female)'],
2588
+ ]
2589
+ .map(([v, n]) => `<option value="${v}">${n}</option>`)
2590
+ .join('');
2583
2591
  pane.innerHTML =
2584
2592
  '<h2>' + _t('tts_h2') + '</h2><div class="sub">' + _t('tts_sub') + '</div>' +
2585
2593
  '<div class="sub" style="margin:10px 0 4px;font-weight:700;color:var(--accent)">STT (Ses → Yazı)</div>' +
2586
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>` +
2587
2604
  `<button id="sttDlBtn" class="btn ghost" style="margin-bottom:14px">${_t('stt_download_now')}</button>` +
2588
2605
  '<div class="sub" style="margin:10px 0 4px;font-weight:700;color:var(--accent)">TTS (Yazı → Ses)</div>' +
2589
2606
  `<div class="form-grid" style="grid-template-columns:auto 1fr 1fr;align-items:center;margin-top:10px">
2590
2607
  <label class="lock-row"><input type="checkbox" id="ttsOn" /><span>${_t('tts_active')}</span></label>
2591
2608
  <label style="grid-column:1">${_t('tts_engine')}</label>
2592
2609
  <select id="ttsEngine" class="inp" style="grid-column:2/4">
2593
- <option value="edge">Edge TTS — yerel & ücretsiz (Ahmet/Emel…)</option>
2610
+ <option value="edge">Edge TTS — ücretsiz bulut (Ahmet/Emel…)</option>
2611
+ <option value="piper">Piper — yerel & offline (Fahrettin/Fettah…)</option>
2594
2612
  <option value="openai">OpenAI-uyumlu API (tts-1, ses: alloy…)</option>
2595
2613
  </select>
2596
2614
  <label style="grid-column:1">${_t('tts_edge_voice')}</label>
2597
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>
2598
2622
  <label class="lock-row" style="grid-column:1/4"><input type="checkbox" id="ttsChatAuto" /><span>${_t('tts_chat_auto')}</span></label>
2599
2623
  <span id="ttsOpenaiWrap" style="display:contents">
2600
2624
  <input id="ttsUrl" class="inp" placeholder="https://api.openai.com/v1" autocomplete="off" />
@@ -2608,25 +2632,66 @@ async function renderTtsPane() { const pane = $('#tab-tts');
2608
2632
  '<div class="sub" style="margin-top:8px">' + _t('tts_note') + '</div>';
2609
2633
 
2610
2634
  const syncEngineUi = () => {
2611
- const isEdge = $('#ttsEngine').value === 'edge';
2635
+ const eng = $('#ttsEngine').value;
2636
+ const isEdge = eng === 'edge';
2637
+ const isPiper = eng === 'piper';
2612
2638
  $('#ttsEdgeVoice').disabled = !isEdge;
2613
- $('#ttsOpenaiWrap').style.opacity = isEdge ? '0.35' : '1';
2614
- $('#ttsOpenaiWrap').querySelectorAll('input').forEach((i) => (i.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'));
2615
2644
  };
2616
2645
 
2617
2646
  try {
2618
2647
  const tts = await beast.waGetTts();
2619
2648
  $('#ttsOn').checked = !!tts.enabled;
2620
- $('#ttsEngine').value = tts.engine === 'openai' ? 'openai' : 'edge';
2649
+ $('#ttsEngine').value = ['edge', 'piper', 'openai'].includes(tts.engine) ? tts.engine : 'edge';
2621
2650
  $('#ttsEdgeVoice').value = tts.edgeVoice || 'tr-TR-AhmetNeural';
2651
+ $('#ttsPiperVoice').value = tts.piperVoice || 'tr_TR-fahrettin-medium';
2622
2652
  $('#ttsChatAuto').checked = !!tts.chatAutoSpeak;
2623
2653
  $('#ttsUrl').value = tts.baseUrl || '';
2624
2654
  $('#ttsKey').value = tts.key || '';
2625
2655
  $('#ttsModel').value = tts.model || 'tts-1';
2626
2656
  $('#ttsVoice').value = tts.voice || 'alloy';
2627
2657
  } 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
+
2628
2682
  syncEngineUi();
2629
- $('#ttsEngine').addEventListener('change', syncEngineUi);
2683
+ await refreshPiperStatus();
2684
+ $('#ttsEngine').addEventListener('change', () => { syncEngineUi(); refreshPiperStatus(); });
2685
+ $('#ttsPiperVoice').addEventListener('change', refreshPiperStatus);
2686
+ $('#piperDlBtn').addEventListener('click', async () => {
2687
+ const v = $('#ttsPiperVoice').value;
2688
+ toast('Piper indiriliyor… (ilk seferde ~80MB)');
2689
+ const poll = setInterval(refreshPiperStatus, 4000);
2690
+ const r = await beast.piperInstall(v).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
2691
+ clearInterval(poll);
2692
+ await refreshPiperStatus();
2693
+ toast(r && r.ok ? 'Piper hazır ✓ — artık çevrimdışı seslendirme yapılır' : 'Piper indirilemedi: ' + ((r && r.error) || '?'));
2694
+ });
2630
2695
 
2631
2696
  /* STT durum satırı: yenile + şimdi indir (yüklenirken 3 sn'de bir güncellenir) */
2632
2697
  const refreshSttStatus = async () => {
@@ -2645,11 +2710,22 @@ async function renderTtsPane() { const pane = $('#tab-tts');
2645
2710
  if (s && (s.state === 'ready' || s.state === 'cloud')) { clearInterval(poll); toast('STT hazır'); }
2646
2711
  }, 3000);
2647
2712
  });
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
+ });
2648
2723
  $('#ttsSave').addEventListener('click', async () => {
2649
2724
  await beast.waSetTts({
2650
2725
  enabled: $('#ttsOn').checked,
2651
2726
  engine: $('#ttsEngine').value,
2652
2727
  edgeVoice: $('#ttsEdgeVoice').value,
2728
+ piperVoice: $('#ttsPiperVoice').value,
2653
2729
  chatAutoSpeak: $('#ttsChatAuto').checked,
2654
2730
  baseUrl: $('#ttsUrl').value.trim(),
2655
2731
  key: $('#ttsKey').value.trim(),
@@ -2664,8 +2740,14 @@ async function renderTtsPane() { const pane = $('#tab-tts');
2664
2740
  });
2665
2741
  $('#ttsTest').addEventListener('click', async () => {
2666
2742
  /* motor + IPC + çalma zincirini uçtan uca dener — sorun neredeyse görünür */
2667
- toast('TTS test ediliyor…');
2668
- const r = await beast.ttsSpeak('Merhaba kanka, Edge TTS testi. Ben Beast.').catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
2743
+ const eng = $('#ttsEngine').value;
2744
+ const line = eng === 'piper'
2745
+ ? 'Merhaba kanka, Piper yerel TTS testi. Ben Beast.'
2746
+ : eng === 'openai'
2747
+ ? 'Merhaba kanka, OpenAI TTS testi. Ben Beast.'
2748
+ : 'Merhaba kanka, Edge TTS testi. Ben Beast.';
2749
+ toast(eng === 'piper' ? 'TTS test ediliyor… (Piper ilk seferde indiriyorsa uzun sürebilir)' : 'TTS test ediliyor…');
2750
+ const r = await beast.ttsSpeak(line).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
2669
2751
  if (!(r && r.ok)) { toast('TTS test HATA: ' + ((r && r.error) || '?')); return; }
2670
2752
  try {
2671
2753
  await playTtsB64(r);