beast-agent 2.44.10 → 2.44.11
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 +246 -0
- package/src/main.js +35 -10
- package/src/preload.js +2 -0
- package/src/renderer/i18n.js +6 -2
- package/src/renderer/renderer.js +67 -8
package/package.json
CHANGED
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
|
|
200
|
+
function synthesize(text, opts = {}) {
|
|
201
|
+
const vid = resolveVoice(opts.voice);
|
|
202
|
+
return (async () => {
|
|
203
|
+
await ensureRuntime();
|
|
204
|
+
await ensureVoice(vid);
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
const args = [
|
|
207
|
+
'--model', voicePath(vid),
|
|
208
|
+
'--config', voiceJsonPath(vid),
|
|
209
|
+
'--output_file', '-',
|
|
210
|
+
'-q',
|
|
211
|
+
];
|
|
212
|
+
const p = spawn(exePath(), args, { windowsHide: true, cwd: binDir() });
|
|
213
|
+
const chunks = [];
|
|
214
|
+
let err = '';
|
|
215
|
+
const timer = setTimeout(() => {
|
|
216
|
+
try { p.kill(); } catch {}
|
|
217
|
+
reject(new Error('piper zaman aşımı'));
|
|
218
|
+
}, 120000);
|
|
219
|
+
p.stdout.on('data', (d) => chunks.push(d));
|
|
220
|
+
p.stderr.on('data', (d) => { err += d.toString(); });
|
|
221
|
+
p.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
222
|
+
p.on('close', (code) => {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
const buf = Buffer.concat(chunks);
|
|
225
|
+
if (code !== 0 || buf.length < 44) {
|
|
226
|
+
return reject(new Error('piper başarısız (kod ' + code + '): ' + err.slice(0, 140)));
|
|
227
|
+
}
|
|
228
|
+
resolve({ audio: buf, mime: 'audio/wav' });
|
|
229
|
+
});
|
|
230
|
+
p.stdin.write(String(text).slice(0, 4000));
|
|
231
|
+
p.stdin.end();
|
|
232
|
+
});
|
|
233
|
+
})();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
VOICES,
|
|
238
|
+
DEFAULT_VOICE,
|
|
239
|
+
synthesize,
|
|
240
|
+
install,
|
|
241
|
+
status,
|
|
242
|
+
ensureRuntime,
|
|
243
|
+
ensureVoice,
|
|
244
|
+
runtimeInstalled,
|
|
245
|
+
voiceInstalled,
|
|
246
|
+
};
|
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');
|
|
@@ -1035,17 +1036,28 @@ async function transcribePcm(audio, langOverride) {
|
|
|
1035
1036
|
}
|
|
1036
1037
|
}
|
|
1037
1038
|
|
|
1039
|
+
/* TTS sentezi: dönen değer { audio: Buffer, mime } — motor seçilebilir.
|
|
1040
|
+
- edge: ücretsiz Microsoft neural (MP3)
|
|
1041
|
+
- piper: tamamen yerel/offline neural (WAV; ilk kullanımda indirilir)
|
|
1042
|
+
- openai: OpenAI-uyumlu /audio/speech API (MP3) */
|
|
1038
1043
|
async function synthesizeSpeech(text) {
|
|
1039
1044
|
const cfg = settings.waTts || {};
|
|
1040
1045
|
if (!cfg.enabled) return null;
|
|
1041
1046
|
try {
|
|
1047
|
+
const engine = cfg.engine || 'edge';
|
|
1042
1048
|
/* EDGE TTS: ücretsiz yerel motor — baseUrl/key GEREKMEZ */
|
|
1043
|
-
if (
|
|
1049
|
+
if (engine === 'edge') {
|
|
1044
1050
|
const edge = require('./agent/edgetts');
|
|
1045
1051
|
const audio = await edge.synthesize(String(text).slice(0, 4000), {
|
|
1046
1052
|
voice: cfg.edgeVoice || 'tr-TR-AhmetNeural',
|
|
1047
1053
|
});
|
|
1048
|
-
return audio;
|
|
1054
|
+
return audio && { audio, mime: 'audio/mpeg' };
|
|
1055
|
+
}
|
|
1056
|
+
/* PIPER: yerel/offline — ilk kullanımda runtime + ses modeli indirilir */
|
|
1057
|
+
if (engine === 'piper') {
|
|
1058
|
+
return await piper.synthesize(String(text).slice(0, 4000), {
|
|
1059
|
+
voice: cfg.piperVoice || piper.DEFAULT_VOICE,
|
|
1060
|
+
});
|
|
1049
1061
|
}
|
|
1050
1062
|
if (!cfg.baseUrl || !cfg.key) return null;
|
|
1051
1063
|
const url = String(cfg.baseUrl).replace(/\/+$/, '') + '/audio/speech';
|
|
@@ -1068,7 +1080,7 @@ async function synthesizeSpeech(text) {
|
|
|
1068
1080
|
waLog(`tts http ${res.status}`);
|
|
1069
1081
|
return null;
|
|
1070
1082
|
}
|
|
1071
|
-
return Buffer.from(await res.arrayBuffer());
|
|
1083
|
+
return { audio: Buffer.from(await res.arrayBuffer()), mime: 'audio/mpeg' };
|
|
1072
1084
|
} catch (e) {
|
|
1073
1085
|
waLog('tts hata: ' + String((e && e.message) || e));
|
|
1074
1086
|
return null;
|
|
@@ -3385,7 +3397,7 @@ function reloadBackend() {
|
|
|
3385
3397
|
return;
|
|
3386
3398
|
}
|
|
3387
3399
|
const voice = await synthesizeSpeech(txt);
|
|
3388
|
-
if (voice) await wa.sendAudio(wajid, voice).catch(() => {});
|
|
3400
|
+
if (voice && voice.audio) await wa.sendAudio(wajid, voice.audio).catch(() => {});
|
|
3389
3401
|
}
|
|
3390
3402
|
}
|
|
3391
3403
|
} catch {}
|
|
@@ -12857,11 +12869,14 @@ ipcMain.handle('wa:sessions', () => [...waChats.values()]);
|
|
|
12857
12869
|
|
|
12858
12870
|
ipcMain.handle('wa:tts:get', () => settings.waTts || {});
|
|
12859
12871
|
ipcMain.handle('wa:tts:set', (_e, cfg) => {
|
|
12872
|
+
const eng = String((cfg && cfg.engine) || 'edge');
|
|
12873
|
+
const pv = String((cfg && cfg.piperVoice) || '').trim();
|
|
12860
12874
|
settings.waTts = {
|
|
12861
12875
|
enabled: !!(cfg && cfg.enabled),
|
|
12862
|
-
/* motor: 'edge' (ücretsiz
|
|
12863
|
-
engine:
|
|
12876
|
+
/* motor: 'edge' (ücretsiz bulut, varsayılan) | 'piper' (yerel/offline) | 'openai' (API) */
|
|
12877
|
+
engine: ['edge', 'piper', 'openai'].includes(eng) ? eng : 'edge',
|
|
12864
12878
|
edgeVoice: String((cfg && cfg.edgeVoice) || 'tr-TR-AhmetNeural').trim(),
|
|
12879
|
+
piperVoice: piper.VOICES[pv] ? pv : piper.DEFAULT_VOICE,
|
|
12865
12880
|
chatAutoSpeak: !!(cfg && cfg.chatAutoSpeak),
|
|
12866
12881
|
baseUrl: String((cfg && cfg.baseUrl) || '').trim(),
|
|
12867
12882
|
key: String((cfg && cfg.key) || '').trim(),
|
|
@@ -12872,6 +12887,15 @@ ipcMain.handle('wa:tts:set', (_e, cfg) => {
|
|
|
12872
12887
|
return settings.waTts;
|
|
12873
12888
|
});
|
|
12874
12889
|
|
|
12890
|
+
/* PIPER (yerel/offline TTS): durum + elle indirme (runtime + ses modeli) */
|
|
12891
|
+
ipcMain.handle('piper:status', (_e, voice) => {
|
|
12892
|
+
try { return piper.status(String(voice || '')); } catch { return { installed: false, runtime: false, voice: false, installing: false }; }
|
|
12893
|
+
});
|
|
12894
|
+
ipcMain.handle('piper:install', async (_e, voice) => {
|
|
12895
|
+
const r = await piper.install(String(voice || ''));
|
|
12896
|
+
return { ...r, status: piper.status(String(voice || '')) };
|
|
12897
|
+
});
|
|
12898
|
+
|
|
12875
12899
|
/* CHAT TTS: masaüstü sohbetinde ajanın son yazısını seslendirir (base64 mp3).
|
|
12876
12900
|
chatAutoSpeak açıkken renderer 'done' olayında bu kanalı çağırır. */
|
|
12877
12901
|
ipcMain.handle('tts:synthesize', async (_e, text) => {
|
|
@@ -12879,13 +12903,14 @@ ipcMain.handle('tts:synthesize', async (_e, text) => {
|
|
|
12879
12903
|
if (!cfg.enabled) return { ok: false, error: 'tts kapalı' };
|
|
12880
12904
|
const t = String(text || '').trim();
|
|
12881
12905
|
if (!t) return { ok: false, error: 'boş metin' };
|
|
12882
|
-
const
|
|
12883
|
-
if (!audio) {
|
|
12906
|
+
const out = await synthesizeSpeech(t.slice(0, 4000));
|
|
12907
|
+
if (!out || !out.audio) {
|
|
12884
12908
|
waLog('tts chat seslendirilemedi — motor: ' + (cfg.engine || 'edge'));
|
|
12885
12909
|
return { ok: false, error: 'seslendirilemedi' };
|
|
12886
12910
|
}
|
|
12887
|
-
|
|
12888
|
-
|
|
12911
|
+
const ses = cfg.engine === 'piper' ? (cfg.piperVoice || piper.DEFAULT_VOICE) : (cfg.edgeVoice || '-');
|
|
12912
|
+
waLog('tts chat ok: ' + out.audio.length + ' bayt, motor: ' + (cfg.engine || 'edge') + ', ses: ' + ses);
|
|
12913
|
+
return { ok: true, audioB64: out.audio.toString('base64'), mime: out.mime || 'audio/mpeg' };
|
|
12889
12914
|
});
|
|
12890
12915
|
|
|
12891
12916
|
/* ---------- Telegram IPC (FEATURE 3) ---------- */
|
package/src/preload.js
CHANGED
|
@@ -203,6 +203,8 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
203
203
|
setWaLockdown: (v) => ipcRenderer.invoke('wa:lockdown:set', v),
|
|
204
204
|
waGetTts: () => ipcRenderer.invoke('wa:tts:get'),
|
|
205
205
|
waSetTts: (cfg) => ipcRenderer.invoke('wa:tts:set', cfg),
|
|
206
|
+
piperStatus: (voice) => ipcRenderer.invoke('piper:status', voice),
|
|
207
|
+
piperInstall: (voice) => ipcRenderer.invoke('piper:install', voice),
|
|
206
208
|
waGetGroups: () => ipcRenderer.invoke('wa:groups:get'),
|
|
207
209
|
waSetGroups: (cfg) => ipcRenderer.invoke('wa:groups:set', cfg),
|
|
208
210
|
tgGetStatus: () => ipcRenderer.invoke('tg:status:get'),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -268,9 +268,11 @@
|
|
|
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
|
|
271
|
+
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
272
|
tts_engine: 'Motor',
|
|
273
273
|
tts_edge_voice: 'Edge sesi',
|
|
274
|
+
tts_piper_voice: 'Piper sesi',
|
|
275
|
+
tts_piper_download: 'Piper\'ı indir / hazırla',
|
|
274
276
|
tts_chat_auto: 'Chat\'te ajanın yazdıklarını otomatik seslendir',
|
|
275
277
|
tts_on: 'TTS açık — cevaplar sesli de gider',
|
|
276
278
|
tts_off: 'TTS kapalı',
|
|
@@ -992,9 +994,11 @@
|
|
|
992
994
|
tts_save: 'Save TTS',
|
|
993
995
|
tts_test: 'Play test sound',
|
|
994
996
|
stt_download_now: 'Download / prepare now',
|
|
995
|
-
tts_note: 'Edge TTS: free
|
|
997
|
+
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
998
|
tts_engine: 'Engine',
|
|
997
999
|
tts_edge_voice: 'Edge voice',
|
|
1000
|
+
tts_piper_voice: 'Piper voice',
|
|
1001
|
+
tts_piper_download: 'Download / prepare Piper',
|
|
998
1002
|
tts_chat_auto: 'Auto-speak agent replies in chat',
|
|
999
1003
|
tts_on: 'TTS on — replies also go as voice',
|
|
1000
1004
|
tts_off: 'TTS off',
|
package/src/renderer/renderer.js
CHANGED
|
@@ -2580,6 +2580,14 @@ 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>' +
|
|
@@ -2590,11 +2598,18 @@ async function renderTtsPane() { const pane = $('#tab-tts');
|
|
|
2590
2598
|
<label class="lock-row"><input type="checkbox" id="ttsOn" /><span>${_t('tts_active')}</span></label>
|
|
2591
2599
|
<label style="grid-column:1">${_t('tts_engine')}</label>
|
|
2592
2600
|
<select id="ttsEngine" class="inp" style="grid-column:2/4">
|
|
2593
|
-
<option value="edge">Edge TTS —
|
|
2601
|
+
<option value="edge">Edge TTS — ücretsiz bulut (Ahmet/Emel…)</option>
|
|
2602
|
+
<option value="piper">Piper — yerel & offline (Fahrettin/Fettah…)</option>
|
|
2594
2603
|
<option value="openai">OpenAI-uyumlu API (tts-1, ses: alloy…)</option>
|
|
2595
2604
|
</select>
|
|
2596
2605
|
<label style="grid-column:1">${_t('tts_edge_voice')}</label>
|
|
2597
2606
|
<select id="ttsEdgeVoice" class="inp" style="grid-column:2/4">${edgeOpts}</select>
|
|
2607
|
+
<label style="grid-column:1">${_t('tts_piper_voice')}</label>
|
|
2608
|
+
<select id="ttsPiperVoice" class="inp" style="grid-column:2/4">${piperOpts}</select>
|
|
2609
|
+
<span id="piperStatusRow" style="grid-column:1/4;display:flex;gap:10px;align-items:center">
|
|
2610
|
+
<span class="sub" id="piperStatusTxt" style="margin:0"></span>
|
|
2611
|
+
<button id="piperDlBtn" class="btn ghost">${_t('tts_piper_download')}</button>
|
|
2612
|
+
</span>
|
|
2598
2613
|
<label class="lock-row" style="grid-column:1/4"><input type="checkbox" id="ttsChatAuto" /><span>${_t('tts_chat_auto')}</span></label>
|
|
2599
2614
|
<span id="ttsOpenaiWrap" style="display:contents">
|
|
2600
2615
|
<input id="ttsUrl" class="inp" placeholder="https://api.openai.com/v1" autocomplete="off" />
|
|
@@ -2608,25 +2623,62 @@ async function renderTtsPane() { const pane = $('#tab-tts');
|
|
|
2608
2623
|
'<div class="sub" style="margin-top:8px">' + _t('tts_note') + '</div>';
|
|
2609
2624
|
|
|
2610
2625
|
const syncEngineUi = () => {
|
|
2611
|
-
const
|
|
2626
|
+
const eng = $('#ttsEngine').value;
|
|
2627
|
+
const isEdge = eng === 'edge';
|
|
2628
|
+
const isPiper = eng === 'piper';
|
|
2612
2629
|
$('#ttsEdgeVoice').disabled = !isEdge;
|
|
2613
|
-
$('#
|
|
2614
|
-
$('#
|
|
2630
|
+
$('#ttsPiperVoice').disabled = !isPiper;
|
|
2631
|
+
$('#piperDlBtn').disabled = !isPiper;
|
|
2632
|
+
$('#piperStatusTxt').style.opacity = isPiper ? '1' : '0.45';
|
|
2633
|
+
$('#ttsOpenaiWrap').style.opacity = eng === 'openai' ? '1' : '0.35';
|
|
2634
|
+
$('#ttsOpenaiWrap').querySelectorAll('input').forEach((i) => (i.disabled = eng !== 'openai'));
|
|
2615
2635
|
};
|
|
2616
2636
|
|
|
2617
2637
|
try {
|
|
2618
2638
|
const tts = await beast.waGetTts();
|
|
2619
2639
|
$('#ttsOn').checked = !!tts.enabled;
|
|
2620
|
-
$('#ttsEngine').value =
|
|
2640
|
+
$('#ttsEngine').value = ['edge', 'piper', 'openai'].includes(tts.engine) ? tts.engine : 'edge';
|
|
2621
2641
|
$('#ttsEdgeVoice').value = tts.edgeVoice || 'tr-TR-AhmetNeural';
|
|
2642
|
+
$('#ttsPiperVoice').value = tts.piperVoice || 'tr_TR-fahrettin-medium';
|
|
2622
2643
|
$('#ttsChatAuto').checked = !!tts.chatAutoSpeak;
|
|
2623
2644
|
$('#ttsUrl').value = tts.baseUrl || '';
|
|
2624
2645
|
$('#ttsKey').value = tts.key || '';
|
|
2625
2646
|
$('#ttsModel').value = tts.model || 'tts-1';
|
|
2626
2647
|
$('#ttsVoice').value = tts.voice || 'alloy';
|
|
2627
2648
|
} catch {}
|
|
2649
|
+
|
|
2650
|
+
/* Piper durum satırı: runtime/ses modeli hazır mı (indirilince güncellenir) */
|
|
2651
|
+
const refreshPiperStatus = async () => {
|
|
2652
|
+
const v = $('#ttsPiperVoice') ? $('#ttsPiperVoice').value : '';
|
|
2653
|
+
try {
|
|
2654
|
+
const st = await beast.piperStatus(v);
|
|
2655
|
+
const el = $('#piperStatusTxt');
|
|
2656
|
+
if (el && st) {
|
|
2657
|
+
el.textContent = 'Piper: ' + (st.installed
|
|
2658
|
+
? 'hazır — yerel/offline'
|
|
2659
|
+
: st.installing
|
|
2660
|
+
? 'indiriliyor…'
|
|
2661
|
+
: st.runtime
|
|
2662
|
+
? 'ses modeli indirilecek (ilk kullanımda otomatik)'
|
|
2663
|
+
: 'runtime + ses modeli indirilecek (ilk kullanımda otomatik)');
|
|
2664
|
+
}
|
|
2665
|
+
return st;
|
|
2666
|
+
} catch { return null; }
|
|
2667
|
+
};
|
|
2668
|
+
|
|
2628
2669
|
syncEngineUi();
|
|
2629
|
-
|
|
2670
|
+
await refreshPiperStatus();
|
|
2671
|
+
$('#ttsEngine').addEventListener('change', () => { syncEngineUi(); refreshPiperStatus(); });
|
|
2672
|
+
$('#ttsPiperVoice').addEventListener('change', refreshPiperStatus);
|
|
2673
|
+
$('#piperDlBtn').addEventListener('click', async () => {
|
|
2674
|
+
const v = $('#ttsPiperVoice').value;
|
|
2675
|
+
toast('Piper indiriliyor… (ilk seferde ~80MB)');
|
|
2676
|
+
const poll = setInterval(refreshPiperStatus, 4000);
|
|
2677
|
+
const r = await beast.piperInstall(v).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
|
|
2678
|
+
clearInterval(poll);
|
|
2679
|
+
await refreshPiperStatus();
|
|
2680
|
+
toast(r && r.ok ? 'Piper hazır ✓ — artık çevrimdışı seslendirme yapılır' : 'Piper indirilemedi: ' + ((r && r.error) || '?'));
|
|
2681
|
+
});
|
|
2630
2682
|
|
|
2631
2683
|
/* STT durum satırı: yenile + şimdi indir (yüklenirken 3 sn'de bir güncellenir) */
|
|
2632
2684
|
const refreshSttStatus = async () => {
|
|
@@ -2650,6 +2702,7 @@ async function renderTtsPane() { const pane = $('#tab-tts');
|
|
|
2650
2702
|
enabled: $('#ttsOn').checked,
|
|
2651
2703
|
engine: $('#ttsEngine').value,
|
|
2652
2704
|
edgeVoice: $('#ttsEdgeVoice').value,
|
|
2705
|
+
piperVoice: $('#ttsPiperVoice').value,
|
|
2653
2706
|
chatAutoSpeak: $('#ttsChatAuto').checked,
|
|
2654
2707
|
baseUrl: $('#ttsUrl').value.trim(),
|
|
2655
2708
|
key: $('#ttsKey').value.trim(),
|
|
@@ -2664,8 +2717,14 @@ async function renderTtsPane() { const pane = $('#tab-tts');
|
|
|
2664
2717
|
});
|
|
2665
2718
|
$('#ttsTest').addEventListener('click', async () => {
|
|
2666
2719
|
/* motor + IPC + çalma zincirini uçtan uca dener — sorun neredeyse görünür */
|
|
2667
|
-
|
|
2668
|
-
const
|
|
2720
|
+
const eng = $('#ttsEngine').value;
|
|
2721
|
+
const line = eng === 'piper'
|
|
2722
|
+
? 'Merhaba kanka, Piper yerel TTS testi. Ben Beast.'
|
|
2723
|
+
: eng === 'openai'
|
|
2724
|
+
? 'Merhaba kanka, OpenAI TTS testi. Ben Beast.'
|
|
2725
|
+
: 'Merhaba kanka, Edge TTS testi. Ben Beast.';
|
|
2726
|
+
toast(eng === 'piper' ? 'TTS test ediliyor… (Piper ilk seferde indiriyorsa uzun sürebilir)' : 'TTS test ediliyor…');
|
|
2727
|
+
const r = await beast.ttsSpeak(line).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
|
|
2669
2728
|
if (!(r && r.ok)) { toast('TTS test HATA: ' + ((r && r.error) || '?')); return; }
|
|
2670
2729
|
try {
|
|
2671
2730
|
await playTtsB64(r);
|