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 +1 -1
- package/src/agent/piper.js +64 -28
- package/src/main.js +28 -2
- package/src/preload.js +1 -0
- package/src/renderer/i18n.js +28 -0
- package/src/renderer/renderer.js +198 -102
- package/src/renderer/style.css +57 -0
package/package.json
CHANGED
package/src/agent/piper.js
CHANGED
|
@@ -195,41 +195,76 @@ async function install(voiceId) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
/* ---------- sentez
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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'),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -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',
|
package/src/renderer/renderer.js
CHANGED
|
@@ -2554,155 +2554,251 @@ function updateInstallPct(ev) {
|
|
|
2554
2554
|
} catch {}
|
|
2555
2555
|
}
|
|
2556
2556
|
|
|
2557
|
-
async function renderTtsPane() {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
<
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
<
|
|
2609
|
-
|
|
2610
|
-
<
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
<
|
|
2619
|
-
</
|
|
2620
|
-
<
|
|
2621
|
-
<
|
|
2622
|
-
|
|
2623
|
-
|
|
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 =
|
|
2627
|
-
const isEdge = eng === 'edge';
|
|
2682
|
+
const eng = ttsEng();
|
|
2628
2683
|
const isPiper = eng === 'piper';
|
|
2629
|
-
$('#
|
|
2630
|
-
$('#
|
|
2631
|
-
$('#
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
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
|
-
|
|
2641
|
-
|
|
2642
|
-
$('#
|
|
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
|
-
|
|
2651
|
-
|
|
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
|
-
|
|
2672
|
-
|
|
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
|
-
|
|
2774
|
+
captureVoice();
|
|
2675
2775
|
toast('Piper indiriliyor… (ilk seferde ~80MB)');
|
|
2676
2776
|
const poll = setInterval(refreshPiperStatus, 4000);
|
|
2677
|
-
const r = await beast.piperInstall(
|
|
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
|
|
2697
|
-
if (
|
|
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:
|
|
2704
|
-
edgeVoice
|
|
2705
|
-
piperVoice
|
|
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 =
|
|
2816
|
+
const eng = ttsEng();
|
|
2721
2817
|
const line = eng === 'piper'
|
|
2722
2818
|
? 'Merhaba kanka, Piper yerel TTS testi. Ben Beast.'
|
|
2723
2819
|
: eng === 'openai'
|
package/src/renderer/style.css
CHANGED
|
@@ -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;
|