beast-agent 2.3.2 → 2.3.4
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/mem0.js +6 -1
- package/src/agent/progressbus.js +41 -0
- package/src/agent/tools.js +10 -3
- package/src/agent/whatsapp.js +97 -2
- package/src/main.js +188 -2
- package/src/preload.js +2 -0
- package/src/renderer/i18n.js +4 -2
- package/src/renderer/index.html +2 -0
- package/src/renderer/renderer.js +107 -3
package/package.json
CHANGED
package/src/agent/mem0.js
CHANGED
|
@@ -101,11 +101,16 @@ function loadPipeline() {
|
|
|
101
101
|
_pipeLoading = (async () => {
|
|
102
102
|
try {
|
|
103
103
|
const { pipeline, env } = require('@xenova/transformers');
|
|
104
|
+
const bus = require('./progressbus');
|
|
104
105
|
const modelsDir = process.env.BEAST_MODELS_DIR || path.join(beastRoot(), 'models');
|
|
105
106
|
fs.mkdirSync(modelsDir, { recursive: true });
|
|
106
107
|
env.cacheDir = modelsDir;
|
|
107
108
|
env.allowLocalModels = false;
|
|
108
|
-
const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
|
|
109
|
+
const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
|
|
110
|
+
quantized: true,
|
|
111
|
+
progress_callback: bus.fileProgressAggregator('emb'),
|
|
112
|
+
});
|
|
113
|
+
bus.emitInstallProgress('emb', { pct: 100 });
|
|
109
114
|
_pipe = async (texts) => {
|
|
110
115
|
const out = [];
|
|
111
116
|
for (const t of texts) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Kurulum progress bus — agent modülleri (whisper / mem0 / tools) yüzde üretir,
|
|
4
|
+
main abone olup renderer'a 'install-progress' agent:event olarak aktarır.
|
|
5
|
+
transformers.js progress_callback'i dosya bazlı geldiği için per-dosya
|
|
6
|
+
loaded/total toplamından genel yüzde hesaplayan aggregator da burada. */
|
|
7
|
+
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
|
|
10
|
+
function onInstallProgress(fn) {
|
|
11
|
+
if (typeof fn === 'function') listeners.add(fn);
|
|
12
|
+
return () => listeners.delete(fn);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function emitInstallProgress(id, data) {
|
|
16
|
+
for (const fn of listeners) {
|
|
17
|
+
try { fn(id, data); } catch {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* transformers.js benzeri dosya bazlı akışlar için: per-dosya loaded/total'dan
|
|
22
|
+
genel yüzde üreten progress_callback üretir. */
|
|
23
|
+
function fileProgressAggregator(id) {
|
|
24
|
+
const files = new Map(); // file -> { loaded, total }
|
|
25
|
+
return (d) => {
|
|
26
|
+
try {
|
|
27
|
+
const key = d && (d.file || d.name);
|
|
28
|
+
if (!key || typeof d.loaded !== 'number' || typeof d.total !== 'number' || d.total <= 0) return;
|
|
29
|
+
files.set(String(key), { loaded: d.loaded, total: d.total });
|
|
30
|
+
let loaded = 0;
|
|
31
|
+
let total = 0;
|
|
32
|
+
for (const v of files.values()) {
|
|
33
|
+
loaded += v.loaded;
|
|
34
|
+
total += v.total;
|
|
35
|
+
}
|
|
36
|
+
if (total > 0) emitInstallProgress(id, { pct: (loaded / total) * 100, loaded, total });
|
|
37
|
+
} catch {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { onInstallProgress, emitInstallProgress, fileProgressAggregator };
|
package/src/agent/tools.js
CHANGED
|
@@ -695,21 +695,25 @@ async function findSystemPython() {
|
|
|
695
695
|
return null;
|
|
696
696
|
}
|
|
697
697
|
|
|
698
|
-
function httpsGetBuffer(url, redirectsLeft = 4, signal) {
|
|
698
|
+
function httpsGetBuffer(url, redirectsLeft = 4, signal, onProgress) {
|
|
699
699
|
return new Promise((resolve, reject) => {
|
|
700
700
|
const req = https.get(url, { headers: { 'User-Agent': 'BeastAgent/1.0 (+python bootstrap)' } }, (res) => {
|
|
701
701
|
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectsLeft > 0) {
|
|
702
702
|
res.resume();
|
|
703
|
-
return resolve(httpsGetBuffer(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal));
|
|
703
|
+
return resolve(httpsGetBuffer(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal, onProgress));
|
|
704
704
|
}
|
|
705
705
|
if (res.statusCode !== 200) {
|
|
706
706
|
res.resume();
|
|
707
707
|
return reject(new Error(`indirme başarısız: HTTP ${res.statusCode}`));
|
|
708
708
|
}
|
|
709
|
+
const total = parseInt(res.headers['content-length'] || '0', 10) || 0;
|
|
709
710
|
const chunks = [];
|
|
710
711
|
let size = 0;
|
|
711
712
|
res.on('data', (c) => {
|
|
712
713
|
size += c.length;
|
|
714
|
+
if (typeof onProgress === 'function') {
|
|
715
|
+
try { onProgress(size, total); } catch {}
|
|
716
|
+
}
|
|
713
717
|
if (size > 80 * 1024 * 1024) {
|
|
714
718
|
req.destroy(new Error('dosya çok büyük'));
|
|
715
719
|
return;
|
|
@@ -731,7 +735,10 @@ async function installEmbeddedPython(signal) {
|
|
|
731
735
|
const dest = path.join(beastAppDir(), 'py');
|
|
732
736
|
fs.mkdirSync(dest, { recursive: true });
|
|
733
737
|
const zipPath = path.join(os.tmpdir(), 'beast-py-embed.zip');
|
|
734
|
-
const
|
|
738
|
+
const bus = require('./progressbus');
|
|
739
|
+
const buf = await httpsGetBuffer(PYTHON_EMBED_URL, 4, signal, (size, total) => {
|
|
740
|
+
if (total > 0) bus.emitInstallProgress('python', { pct: (size / total) * 100, loaded: size, total });
|
|
741
|
+
});
|
|
735
742
|
fs.writeFileSync(zipPath, buf);
|
|
736
743
|
const r = await runCommand(
|
|
737
744
|
`Expand-Archive -LiteralPath "${zipPath}" -DestinationPath "${dest}" -Force`,
|
package/src/agent/whatsapp.js
CHANGED
|
@@ -36,6 +36,83 @@ function waLogSafe(line) {
|
|
|
36
36
|
|
|
37
37
|
const TRACK_CAP = 500;
|
|
38
38
|
|
|
39
|
+
/* ---------- TTS → WhatsApp sesli not (OGG/Opus) ---------- */
|
|
40
|
+
|
|
41
|
+
let _ffmpegPath = null;
|
|
42
|
+
function getFfmpegPath() {
|
|
43
|
+
if (_ffmpegPath !== null) return _ffmpegPath;
|
|
44
|
+
try { _ffmpegPath = require('ffmpeg-static') || ''; } catch { _ffmpegPath = ''; }
|
|
45
|
+
return _ffmpegPath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runFfmpeg(args, input) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
try {
|
|
51
|
+
const { spawn } = require('child_process');
|
|
52
|
+
const p = spawn(getFfmpegPath(), args, { windowsHide: true });
|
|
53
|
+
const out = [];
|
|
54
|
+
let err = '';
|
|
55
|
+
p.stdout.on('data', (c) => out.push(c));
|
|
56
|
+
p.stderr.on('data', (c) => { if (err.length < 400) err += c.toString(); });
|
|
57
|
+
p.on('error', reject);
|
|
58
|
+
p.on('close', (code) => {
|
|
59
|
+
if (code !== 0) return reject(new Error(`ffmpeg ${code}: ${err.slice(0, 200)}`));
|
|
60
|
+
resolve(Buffer.concat(out));
|
|
61
|
+
});
|
|
62
|
+
p.stdin.on('error', () => {});
|
|
63
|
+
p.stdin.write(input);
|
|
64
|
+
p.stdin.end();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
reject(e);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Herhangi bir ses buffer'ı → OGG/Opus (WhatsApp sesli not formatı). Zaten OggS ise olduğu gibi döner. */
|
|
72
|
+
async function toVoiceOggOpus(buf) {
|
|
73
|
+
if (buf.length > 4 && buf.subarray(0, 4).toString('latin1') === 'OggS') return buf;
|
|
74
|
+
const ff = getFfmpegPath();
|
|
75
|
+
if (!ff) throw new Error('ffmpeg bulunamadı');
|
|
76
|
+
return runFfmpeg([
|
|
77
|
+
'-hide_banner', '-loglevel', 'error',
|
|
78
|
+
'-i', 'pipe:0',
|
|
79
|
+
'-c:a', 'libopus', '-b:a', '48k', '-ar', '24000', '-ac', '1',
|
|
80
|
+
'-application', 'voip', '-vbr', 'on',
|
|
81
|
+
'-f', 'ogg', 'pipe:1',
|
|
82
|
+
], buf);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Sesli not dalga formu: 64 segmentlik normalize genlik histogramı (Uint8Array) */
|
|
86
|
+
async function audioWaveform(buf) {
|
|
87
|
+
try {
|
|
88
|
+
const ff = getFfmpegPath();
|
|
89
|
+
if (!ff) return undefined;
|
|
90
|
+
const pcm = await runFfmpeg([
|
|
91
|
+
'-hide_banner', '-loglevel', 'error',
|
|
92
|
+
'-i', 'pipe:0', '-f', 's16le', '-ar', '16000', '-ac', '1', 'pipe:1',
|
|
93
|
+
], buf);
|
|
94
|
+
const SEG = 64;
|
|
95
|
+
const step = Math.max(1, Math.floor(pcm.length / 2 / SEG));
|
|
96
|
+
const wave = new Uint8Array(SEG);
|
|
97
|
+
let max = 1;
|
|
98
|
+
for (let i = 0; i < SEG; i++) {
|
|
99
|
+
let sum = 0;
|
|
100
|
+
const start = i * step;
|
|
101
|
+
for (let j = 0; j < step; j++) {
|
|
102
|
+
const off = (start + j) * 2;
|
|
103
|
+
if (off + 1 < pcm.length) sum += Math.abs(pcm.readInt16LE(off));
|
|
104
|
+
}
|
|
105
|
+
const v = Math.round(sum / step);
|
|
106
|
+
wave[i] = v;
|
|
107
|
+
if (v > max) max = v;
|
|
108
|
+
}
|
|
109
|
+
for (let i = 0; i < SEG; i++) wave[i] = Math.min(255, Math.round((wave[i] / max) * 255));
|
|
110
|
+
return wave;
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
39
116
|
/* lastKnownPresence etiketleri */
|
|
40
117
|
const PRESENCE_LABELS = {
|
|
41
118
|
available: 'çevrimiçi',
|
|
@@ -470,11 +547,29 @@ class WhatsAppBridge {
|
|
|
470
547
|
return id ? { id } : true;
|
|
471
548
|
}
|
|
472
549
|
|
|
473
|
-
/* Sesli not olarak yanıtla (TTS çıktısı mp3 buffer
|
|
550
|
+
/* Sesli not olarak yanıtla (TTS çıktısı mp3 buffer → WhatsApp sesli not formatı OGG/Opus'a çevrilir;
|
|
551
|
+
ptt sesli notlar mp3 ile açılmaz, mutlaka audio/ogg; codecs=opus olmalı) */
|
|
474
552
|
async sendAudio(jid, audioBuf) {
|
|
475
553
|
if (!this.sock || !this.connected || !audioBuf) return false;
|
|
476
554
|
try {
|
|
477
|
-
|
|
555
|
+
let voice = audioBuf;
|
|
556
|
+
let mime = 'audio/mpeg';
|
|
557
|
+
let ptt = false;
|
|
558
|
+
let waveform;
|
|
559
|
+
try {
|
|
560
|
+
voice = await toVoiceOggOpus(audioBuf);
|
|
561
|
+
mime = 'audio/ogg; codecs=opus';
|
|
562
|
+
ptt = true;
|
|
563
|
+
waveform = await audioWaveform(voice);
|
|
564
|
+
} catch {
|
|
565
|
+
/* ffmpeg dönüşümü başarısızsa mp3'ü ptt'siz ses mesajı olarak dene */
|
|
566
|
+
voice = audioBuf;
|
|
567
|
+
mime = 'audio/mpeg';
|
|
568
|
+
ptt = false;
|
|
569
|
+
}
|
|
570
|
+
const msg = { audio: voice, ptt, mimetype: mime };
|
|
571
|
+
if (waveform) msg.waveform = waveform;
|
|
572
|
+
const ret = await this.sock.sendMessage(jid, msg);
|
|
478
573
|
this._trackOutgoing(jid, '[sesli yanıt]', ret);
|
|
479
574
|
return true;
|
|
480
575
|
} catch (e) {
|
package/src/main.js
CHANGED
|
@@ -655,11 +655,16 @@ async function ensureStt() {
|
|
|
655
655
|
hf.env.cacheDir = modelsDir;
|
|
656
656
|
hf.env.allowLocalModels = false;
|
|
657
657
|
let lastErr = null;
|
|
658
|
+
const bus = require('./agent/progressbus');
|
|
658
659
|
for (const dtype of ['q4f16', 'q4', 'q8']) {
|
|
659
660
|
try {
|
|
660
|
-
const p = await hf.pipeline('automatic-speech-recognition', wanted, {
|
|
661
|
+
const p = await hf.pipeline('automatic-speech-recognition', wanted, {
|
|
662
|
+
dtype,
|
|
663
|
+
progress_callback: bus.fileProgressAggregator('stt'),
|
|
664
|
+
});
|
|
661
665
|
sttPipeline = p;
|
|
662
666
|
sttModel = wanted;
|
|
667
|
+
bus.emitInstallProgress('stt', { pct: 100 });
|
|
663
668
|
waLog('STT hazır: ' + wanted + ' (dtype ' + dtype + ')');
|
|
664
669
|
return sttPipeline;
|
|
665
670
|
} catch (e) {
|
|
@@ -676,6 +681,37 @@ async function ensureStt() {
|
|
|
676
681
|
return sttLoading;
|
|
677
682
|
}
|
|
678
683
|
|
|
684
|
+
/* ---------- Kurulum yüzde göstergesi: agent modüllerinden gelen progress
|
|
685
|
+
renderer'a 'install-progress' event'i olarak akıtılır (throttle'lı);
|
|
686
|
+
installPctState'i install:status da okur (sekme sonradan açılırsa ilk
|
|
687
|
+
çizimde yüzde zaten dolu gelir). ---------- */
|
|
688
|
+
const installPctState = {}; // id -> { pct, loaded, total, ts }
|
|
689
|
+
{
|
|
690
|
+
const bus = require('./agent/progressbus');
|
|
691
|
+
const lastSent = new Map(); // id -> { t, pct }
|
|
692
|
+
bus.onInstallProgress((id, d) => {
|
|
693
|
+
try {
|
|
694
|
+
if (!id || !d || typeof d.pct !== 'number' || !isFinite(d.pct)) return;
|
|
695
|
+
const cur = {
|
|
696
|
+
pct: Math.max(0, Math.min(100, Math.round(d.pct))),
|
|
697
|
+
loaded: d.loaded || 0,
|
|
698
|
+
total: d.total || 0,
|
|
699
|
+
ts: Date.now(),
|
|
700
|
+
};
|
|
701
|
+
installPctState[id] = cur;
|
|
702
|
+
const now = Date.now();
|
|
703
|
+
const prev = lastSent.get(id) || { t: 0, pct: -1 };
|
|
704
|
+
/* aynı yüzde tekrarını ve <500ms'lik küçük sıçramaları yut (event fırtınası olmasın) */
|
|
705
|
+
if (cur.pct === prev.pct && now - prev.t < 3000) return;
|
|
706
|
+
if (now - prev.t < 500 && Math.abs(cur.pct - prev.pct) < 2) return;
|
|
707
|
+
lastSent.set(id, { t: now, pct: cur.pct });
|
|
708
|
+
if (win && !win.isDestroyed()) {
|
|
709
|
+
win.webContents.send('agent:event', { type: 'install-progress', id, pct: cur.pct, loaded: cur.loaded, total: cur.total });
|
|
710
|
+
}
|
|
711
|
+
} catch {}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
679
715
|
/* ogg/opus/mp3 → mono 16kHz Float32 PCM (ffmpeg ile) */
|
|
680
716
|
function decodeAudioToPcm16k(buf) {
|
|
681
717
|
return new Promise((resolve, reject) => {
|
|
@@ -3248,6 +3284,15 @@ app.whenReady().then(() => {
|
|
|
3248
3284
|
waLog('STT aktif: ' + sttEngineLabel() + ' — yerel model kullanılmayacak');
|
|
3249
3285
|
}
|
|
3250
3286
|
|
|
3287
|
+
/* EMBEDDING modeli (hafıza semantik arama) — açılışta otomatik indir */
|
|
3288
|
+
setTimeout(() => {
|
|
3289
|
+
try {
|
|
3290
|
+
require('./agent/mem0').search('beast', 'warmup')
|
|
3291
|
+
.then(() => waLog('embedding modeli hazır (all-MiniLM-L6-v2)'))
|
|
3292
|
+
.catch(() => {});
|
|
3293
|
+
} catch {}
|
|
3294
|
+
}, 20000);
|
|
3295
|
+
|
|
3251
3296
|
// WhatsApp köprüsünü otomatik başlat (eşleme varsa direkt bağlanır)
|
|
3252
3297
|
ensureWa().start().catch((e) => waLog('autostart failed: ' + (e && e.message)));
|
|
3253
3298
|
|
|
@@ -3909,7 +3954,18 @@ async function ocrRead({ image, lang = 'tur+eng' } = {}) {
|
|
|
3909
3954
|
if (!worker) {
|
|
3910
3955
|
const tessDir = path.join(APP_DIR, 'tessdata');
|
|
3911
3956
|
fs.mkdirSync(tessDir, { recursive: true });
|
|
3912
|
-
|
|
3957
|
+
const bus = require('./agent/progressbus');
|
|
3958
|
+
worker = await t.createWorker(langKey, 1, {
|
|
3959
|
+
cachePath: tessDir,
|
|
3960
|
+
logger: (m) => {
|
|
3961
|
+
try {
|
|
3962
|
+
/* dil verisi (.traineddata) inerken yüzde üret — OCR çalışma anını kirletme */
|
|
3963
|
+
if (m && /traineddata/i.test(String(m.status || '')) && typeof m.progress === 'number' && isFinite(m.progress)) {
|
|
3964
|
+
bus.emitInstallProgress('ocr', { pct: m.progress * 100 });
|
|
3965
|
+
}
|
|
3966
|
+
} catch {}
|
|
3967
|
+
},
|
|
3968
|
+
});
|
|
3913
3969
|
_ocrWorkers.set(langKey, worker);
|
|
3914
3970
|
}
|
|
3915
3971
|
let input = image;
|
|
@@ -5825,6 +5881,136 @@ ipcMain.handle('stt:prefetch', () => {
|
|
|
5825
5881
|
.catch((e) => waLog('STT prefetch hata: ' + String((e && e.message) || e)));
|
|
5826
5882
|
return { ok: true, loading: true };
|
|
5827
5883
|
});
|
|
5884
|
+
|
|
5885
|
+
/* KURULUM SEKMESİ: gerekli bileşenlerin durum taraması */
|
|
5886
|
+
ipcMain.handle('install:status', async () => {
|
|
5887
|
+
const rows = [];
|
|
5888
|
+
const pkgOk = (id) => { try { require.resolve(id); return true; } catch { return false; } };
|
|
5889
|
+
/* progress bus'tan canlı yüzde (10 dk taze ise güvenilir sayılır) */
|
|
5890
|
+
const pctOf = (id) => {
|
|
5891
|
+
const s = installPctState[id];
|
|
5892
|
+
return s && Date.now() - s.ts < 10 * 60 * 1000 ? s : null;
|
|
5893
|
+
};
|
|
5894
|
+
const pctFields = (id) => {
|
|
5895
|
+
const s = pctOf(id);
|
|
5896
|
+
if (!s) return {};
|
|
5897
|
+
return {
|
|
5898
|
+
pct: s.pct,
|
|
5899
|
+
loadedMb: s.loaded ? Math.round(s.loaded / 1048576) : undefined,
|
|
5900
|
+
totalMb: s.total ? Math.round(s.total / 1048576) : undefined,
|
|
5901
|
+
};
|
|
5902
|
+
};
|
|
5903
|
+
const scanModel = (rel) => {
|
|
5904
|
+
const dir = path.join(APP_DIR, 'models', ...rel.split('/'));
|
|
5905
|
+
let files = [];
|
|
5906
|
+
try {
|
|
5907
|
+
files = fs.readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
5908
|
+
.filter((d) => d.isFile())
|
|
5909
|
+
.map((d) => path.join(d.parentPath || d.path, d.name));
|
|
5910
|
+
} catch {}
|
|
5911
|
+
let bytes = 0, tmp = 0, onnx = 0, hasCfg = false;
|
|
5912
|
+
for (const p of files) {
|
|
5913
|
+
try {
|
|
5914
|
+
bytes += fs.statSync(p).size;
|
|
5915
|
+
if (/\.tmp/i.test(p)) tmp++;
|
|
5916
|
+
else if (/\.onnx$/i.test(p)) onnx++;
|
|
5917
|
+
else if (/config\.json$/i.test(p)) hasCfg = true;
|
|
5918
|
+
} catch {}
|
|
5919
|
+
}
|
|
5920
|
+
return { files, bytes, tmp, onnx, hasCfg };
|
|
5921
|
+
};
|
|
5922
|
+
|
|
5923
|
+
/* 1) STT modeli */
|
|
5924
|
+
if (sttProvider() !== 'local') {
|
|
5925
|
+
rows.push({ id: 'stt', name: 'STT modeli — ' + sttModelName(), state: 'cloud', detail: 'bulut motoru — indirme gerekmez' });
|
|
5926
|
+
} else {
|
|
5927
|
+
const s = scanModel(sttModelName());
|
|
5928
|
+
let state;
|
|
5929
|
+
if (sttPipeline) state = 'ok';
|
|
5930
|
+
else if (sttLoading) state = 'loading';
|
|
5931
|
+
else if (s.hasCfg && s.onnx >= 2 && s.tmp === 0) state = 'downloaded';
|
|
5932
|
+
else if (s.files.length) state = 'partial';
|
|
5933
|
+
else state = 'missing';
|
|
5934
|
+
rows.push({
|
|
5935
|
+
id: 'stt',
|
|
5936
|
+
name: 'STT modeli — whisper-large-v3-turbo',
|
|
5937
|
+
state,
|
|
5938
|
+
detail: sttEngineLabel(),
|
|
5939
|
+
mb: Math.round(s.bytes / 1048576),
|
|
5940
|
+
...(['missing', 'partial', 'loading'].includes(state) ? pctFields('stt') : {}),
|
|
5941
|
+
});
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5944
|
+
/* 2) Embedding modeli (hafıza semantik arama) */
|
|
5945
|
+
{
|
|
5946
|
+
const s = scanModel('Xenova/all-MiniLM-L6-v2');
|
|
5947
|
+
const state = s.hasCfg && s.onnx >= 1 && s.tmp === 0 ? 'ok' : (s.files.length ? 'partial' : 'missing');
|
|
5948
|
+
rows.push({
|
|
5949
|
+
id: 'emb',
|
|
5950
|
+
name: 'Embedding modeli — all-MiniLM-L6-v2',
|
|
5951
|
+
state,
|
|
5952
|
+
detail: 'hafıza semantik arama',
|
|
5953
|
+
mb: Math.round(s.bytes / 1048576),
|
|
5954
|
+
...(['missing', 'partial', 'loading'].includes(state) ? pctFields('emb') : {}),
|
|
5955
|
+
});
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5958
|
+
/* 3) ffmpeg */
|
|
5959
|
+
let ffOk = false;
|
|
5960
|
+
try { const p = require('ffmpeg-static'); ffOk = !!p && fs.existsSync(p); } catch {}
|
|
5961
|
+
rows.push({ id: 'ffmpeg', name: 'ffmpeg — ses/video dönüşüm', state: ffOk ? 'ok' : 'missing', detail: ffOk ? 'kurulu' : 'npm paketi eksik' });
|
|
5962
|
+
|
|
5963
|
+
/* 4) çalışma zamanı npm paketleri */
|
|
5964
|
+
rows.push({ id: 'hf', name: 'Transformers.js — STT çalışma zamanı', state: pkgOk('@huggingface/transformers') ? 'ok' : 'missing', detail: 'npm paketi' });
|
|
5965
|
+
rows.push({ id: 'ort', name: 'ONNX Runtime — model motoru', state: pkgOk('onnxruntime-node') ? 'ok' : 'missing', detail: 'npm paketi' });
|
|
5966
|
+
|
|
5967
|
+
/* 5) OCR */
|
|
5968
|
+
{
|
|
5969
|
+
const po = pctOf('ocr');
|
|
5970
|
+
const dl = pkgOk('tesseract.js') && po && po.pct < 100; // dil verisi şu an iniyor
|
|
5971
|
+
rows.push({
|
|
5972
|
+
id: 'ocr',
|
|
5973
|
+
name: 'OCR — Tesseract (ekran okuma)',
|
|
5974
|
+
state: dl ? 'loading' : (pkgOk('tesseract.js') ? 'ok' : 'missing'),
|
|
5975
|
+
detail: dl ? 'dil verisi iniyor — ilk OCR kullanımında' : (pkgOk('tesseract.js') ? 'kurulu — dil verisi ilk kullanımda iner' : 'npm paketi eksik'),
|
|
5976
|
+
...(dl ? pctFields('ocr') : {}),
|
|
5977
|
+
});
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5980
|
+
/* 6) Python (opsiyonel — betikler) */
|
|
5981
|
+
let pyVer = '';
|
|
5982
|
+
for (const cmd of ['python -c "import sys;print(sys.version.split()[0])"', 'py -3 -c "import sys;print(sys.version.split()[0])"']) {
|
|
5983
|
+
try {
|
|
5984
|
+
pyVer = String(require('child_process').execSync(cmd, { timeout: 4000, stdio: 'pipe', encoding: 'utf8' })).trim();
|
|
5985
|
+
if (pyVer) break;
|
|
5986
|
+
} catch {}
|
|
5987
|
+
}
|
|
5988
|
+
if (!pyVer) {
|
|
5989
|
+
const ps = pctOf('python');
|
|
5990
|
+
if (ps && ps.pct < 100) { // gömülü python zip'i şu an iniyor
|
|
5991
|
+
rows.push({ id: 'python', name: 'Python — betikler / web arama', state: 'loading', detail: 'gömülü python indiriliyor', ...pctFields('python') });
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
if (pyVer || !rows.some((r) => r.id === 'python')) {
|
|
5995
|
+
rows.push({ id: 'python', name: 'Python — betikler / web arama', state: pyVer ? 'ok' : 'optional', detail: pyVer ? 'v' + pyVer : 'sistemde bulunamadı — opsiyonel' });
|
|
5996
|
+
}
|
|
5997
|
+
|
|
5998
|
+
/* 7) Edge TTS (bulut) */
|
|
5999
|
+
rows.push({ id: 'edge', name: 'Edge TTS — seslendirme', state: 'cloud', detail: 'bulut — kurulum gerekmez' });
|
|
6000
|
+
|
|
6001
|
+
return rows;
|
|
6002
|
+
});
|
|
6003
|
+
|
|
6004
|
+
/* embedding modelini şimdi indir (mem0 arama yolu ısıtılır) */
|
|
6005
|
+
ipcMain.handle('embed:prefetch', () => {
|
|
6006
|
+
try {
|
|
6007
|
+
const mem0 = require('./agent/mem0');
|
|
6008
|
+
mem0.search('beast', 'warmup')
|
|
6009
|
+
.then(() => waLog('embedding modeli hazır'))
|
|
6010
|
+
.catch(() => {});
|
|
6011
|
+
} catch {}
|
|
6012
|
+
return { ok: true, loading: true };
|
|
6013
|
+
});
|
|
5828
6014
|
ipcMain.handle('stt:lang:set', (_e, lang) => {
|
|
5829
6015
|
const v = ['auto', 'tr', 'en'].includes(String(lang)) ? String(lang) : 'tr';
|
|
5830
6016
|
settings.sttLang = v;
|
package/src/preload.js
CHANGED
|
@@ -55,6 +55,8 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
55
55
|
sttLangGet: () => ipcRenderer.invoke('stt:lang:get'),
|
|
56
56
|
sttStatus: () => ipcRenderer.invoke('stt:status'),
|
|
57
57
|
sttPrefetchNow: () => ipcRenderer.invoke('stt:prefetch'),
|
|
58
|
+
installStatus: () => ipcRenderer.invoke('install:status'),
|
|
59
|
+
embedPrefetch: () => ipcRenderer.invoke('embed:prefetch'),
|
|
58
60
|
sttLangSet: (lang) => ipcRenderer.invoke('stt:lang:set', lang),
|
|
59
61
|
updateStatus: () => ipcRenderer.invoke('update:status'),
|
|
60
62
|
updateCheck: () => ipcRenderer.invoke('update:check'),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
tab_notes: 'Oturum Notları',
|
|
70
70
|
tab_history: 'Oturum Geçmişi',
|
|
71
71
|
tab_agents: 'Paralel Ajanlar',
|
|
72
|
-
tab_tts: 'Sesli Yanıt',
|
|
72
|
+
tab_tts: 'Sesli Yanıt',
|
|
73
|
+
tab_install: 'Kurulum',
|
|
73
74
|
tab_email: 'E-posta',
|
|
74
75
|
tab_integrations: 'Entegrasyonlar',
|
|
75
76
|
tab_websearch: 'Web Arama',
|
|
@@ -645,7 +646,8 @@
|
|
|
645
646
|
tab_notes: 'Session Notes',
|
|
646
647
|
tab_history: 'Session History',
|
|
647
648
|
tab_agents: 'Parallel Agents',
|
|
648
|
-
tab_tts: 'Voice Reply',
|
|
649
|
+
tab_tts: 'Voice Reply',
|
|
650
|
+
tab_install: 'Setup',
|
|
649
651
|
tab_email: 'E-mail',
|
|
650
652
|
tab_integrations: 'Integrations',
|
|
651
653
|
tab_websearch: 'Web Search',
|
package/src/renderer/index.html
CHANGED
|
@@ -233,6 +233,7 @@
|
|
|
233
233
|
<button class="tab" data-tab="skills" data-i18n="tab_skills">Skills</button>
|
|
234
234
|
<button class="tab" data-tab="agents" data-i18n="tab_agents">Paralel Ajanlar</button>
|
|
235
235
|
<button class="tab" data-tab="tts" data-i18n="tab_tts">Sesli Yanıt</button>
|
|
236
|
+
<button class="tab" data-tab="install" data-i18n="tab_install">Kurulum</button>
|
|
236
237
|
<button class="tab" data-tab="email" data-i18n="tab_email">E-posta</button>
|
|
237
238
|
<button class="tab" data-tab="integrations" data-i18n="tab_integrations">Entegrasyonlar</button>
|
|
238
239
|
<button class="tab" data-tab="websearch" data-i18n="tab_websearch">Web Arama</button>
|
|
@@ -257,6 +258,7 @@
|
|
|
257
258
|
<div id="tab-skills" class="pane" hidden></div>
|
|
258
259
|
<div id="tab-agents" class="pane" hidden></div>
|
|
259
260
|
<div id="tab-tts" class="pane" hidden></div>
|
|
261
|
+
<div id="tab-install" class="pane" hidden></div>
|
|
260
262
|
<div id="tab-email" class="pane" hidden></div>
|
|
261
263
|
<div id="tab-integrations" class="pane" hidden></div>
|
|
262
264
|
<div id="tab-websearch" class="pane" hidden></div>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -1060,6 +1060,7 @@ async function renderActiveSettingsTab() {
|
|
|
1060
1060
|
case 'fallout': await refreshFalloutPane(); break;
|
|
1061
1061
|
case 'skills': await renderSkillsPane(); break;
|
|
1062
1062
|
case 'tts': await renderTtsPane(); break;
|
|
1063
|
+
case 'install': await renderInstallPane(); break;
|
|
1063
1064
|
case 'email': await renderEmailPane(); break;
|
|
1064
1065
|
case 'integrations': await renderIntegrationsPane(); break;
|
|
1065
1066
|
case 'websearch': await renderWebSearchPane(); break;
|
|
@@ -1150,13 +1151,14 @@ function switchTab(name) {
|
|
|
1150
1151
|
document.querySelectorAll('#setTabs .tab').forEach((b) =>
|
|
1151
1152
|
b.classList.toggle('active', b.dataset.tab === name)
|
|
1152
1153
|
);
|
|
1153
|
-
for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'email', 'integrations', 'websearch', 'mcp', 'events', 'cron', 'usage', 'logs', 'dash', 'sec', 'update']) {
|
|
1154
|
+
for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'cron', 'usage', 'logs', 'dash', 'sec', 'update']) {
|
|
1154
1155
|
const el = $('#tab-' + p);
|
|
1155
1156
|
if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
|
|
1156
1157
|
}
|
|
1157
1158
|
if (name === 'lang') renderLangPane();
|
|
1158
1159
|
if (name === 'cron') openCron();
|
|
1159
1160
|
if (name === 'usage') renderUsagePane();
|
|
1161
|
+
if (name === 'install') renderInstallPane();
|
|
1160
1162
|
if (name === 'events') renderEventsPane();
|
|
1161
1163
|
if (name === 'logs') renderLogPane();
|
|
1162
1164
|
if (name === 'dash') renderDashboardPane();
|
|
@@ -2097,8 +2099,109 @@ function ttsFlushTail() {
|
|
|
2097
2099
|
if (rest) ttsEnqueueSentence(rest);
|
|
2098
2100
|
else ttsKick();
|
|
2099
2101
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
+
/* ---------------- KURULUM SEKMESİ: bileşenler OTOMATİK kurulur ----------------
|
|
2103
|
+
Eksik/kısmen inmiş bileşen sekme açılınca kendiliğinden indirilir; kullanıcı
|
|
2104
|
+
hiçbir düğmeye basmaz. Durum canlı güncellenir (4 sn'de bir). */
|
|
2105
|
+
async function renderInstallPane() {
|
|
2106
|
+
const pane = $('#tab-install');
|
|
2107
|
+
if (!pane) return;
|
|
2108
|
+
pane.innerHTML =
|
|
2109
|
+
'<h2>Kurulum</h2><div class="sub">Gerekli bileşenler otomatik indirilir ve kurulur — eksik varsa aşağıda İNİYOR olarak görürsün.</div>' +
|
|
2110
|
+
'<div id="instRows" style="margin-top:10px">Taranıyor…</div>';
|
|
2111
|
+
|
|
2112
|
+
const autoStarted = new Set(); // bu pane oturumunda otomatik başlatılanlar
|
|
2113
|
+
let refreshTimer = null;
|
|
2114
|
+
|
|
2115
|
+
const badge = (st) => {
|
|
2116
|
+
const map = {
|
|
2117
|
+
ok: ['KURULU', 'var(--ok)'],
|
|
2118
|
+
downloaded: ['İNDİRİLDİ', 'var(--ok)'],
|
|
2119
|
+
loading: ['İNİYOR…', '#d9a441'],
|
|
2120
|
+
partial: ['İNİYOR… (devam)', '#d9a441'],
|
|
2121
|
+
missing: ['İNDİRİLİYOR…', '#d9a441'],
|
|
2122
|
+
cloud: ['BULUT', 'var(--muted)'],
|
|
2123
|
+
optional: ['OPSİYONEL', 'var(--muted)'],
|
|
2124
|
+
};
|
|
2125
|
+
const [txt, color] = map[st] || [st, 'var(--muted)'];
|
|
2126
|
+
return '<span style="color:' + color + ';font-weight:700;font-size:11px;flex:none">' + txt + '</span>';
|
|
2127
|
+
};
|
|
2128
|
+
|
|
2129
|
+
/* eksik bileşenleri OTOMATİK başlat (bileşen başına bir kez) */
|
|
2130
|
+
const autoStart = (rows) => {
|
|
2131
|
+
for (const r of rows) {
|
|
2132
|
+
if (autoStarted.has(r.id)) continue;
|
|
2133
|
+
if (r.id === 'stt' && ['missing', 'partial', 'loading'].includes(r.state)) {
|
|
2134
|
+
autoStarted.add('stt');
|
|
2135
|
+
beast.sttPrefetchNow().catch(() => {});
|
|
2136
|
+
}
|
|
2137
|
+
if (r.id === 'emb' && ['missing', 'partial'].includes(r.state)) {
|
|
2138
|
+
autoStarted.add('emb');
|
|
2139
|
+
beast.embedPrefetch().catch(() => {});
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
|
|
2144
|
+
const render = (rows) => {
|
|
2145
|
+
const el = $('#instRows');
|
|
2146
|
+
if (!el) return;
|
|
2147
|
+
if (!rows || !rows.length) { el.innerHTML = '<div class="sub">Durum alınamadı</div>'; return; }
|
|
2148
|
+
autoStart(rows);
|
|
2149
|
+
const hasPct = (r) => typeof r.pct === 'number' && ['missing', 'partial', 'loading'].includes(r.state);
|
|
2150
|
+
const pctLabel = (r) => {
|
|
2151
|
+
if (!hasPct(r)) return '<span id="inst-pct-' + r.id + '" style="display:none"></span>';
|
|
2152
|
+
const mb = (r.loadedMb && r.totalMb) ? ' · ' + r.loadedMb + '/' + r.totalMb + ' MB' : '';
|
|
2153
|
+
return '<span id="inst-pct-' + r.id + '" style="color:#d9a441;font-weight:700;font-size:11px;flex:none">%' + Math.round(r.pct) + mb + '</span>';
|
|
2154
|
+
};
|
|
2155
|
+
const bar = (r) => {
|
|
2156
|
+
if (!hasPct(r)) return '<div id="inst-bar-' + r.id + '" style="display:none"></div>';
|
|
2157
|
+
return '<div id="inst-bar-' + r.id + '" style="margin-top:7px;height:6px;border-radius:3px;background:var(--border);overflow:hidden">' +
|
|
2158
|
+
'<div id="inst-fill-' + r.id + '" style="height:100%;width:' + Math.max(2, Math.round(r.pct)) + '%;background:#d9a441;border-radius:3px;transition:width .4s ease"></div></div>';
|
|
2159
|
+
};
|
|
2160
|
+
el.innerHTML = rows.map((r) => (
|
|
2161
|
+
'<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--border)">' +
|
|
2162
|
+
'<div style="flex:1;min-width:0">' +
|
|
2163
|
+
'<div style="font-size:13px">' + escapeHtml(r.name) + '</div>' +
|
|
2164
|
+
'<div class="sub" style="font-size:11px">' + escapeHtml(r.detail || '') + (r.mb ? ' · ' + r.mb + ' MB' : '') + '</div>' +
|
|
2165
|
+
bar(r) +
|
|
2166
|
+
'</div>' +
|
|
2167
|
+
badge(r.state) +
|
|
2168
|
+
pctLabel(r) +
|
|
2169
|
+
'</div>'
|
|
2170
|
+
)).join('');
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
const refresh = async () => {
|
|
2174
|
+
if (pane.hidden) return;
|
|
2175
|
+
const rows = await beast.installStatus().catch(() => []);
|
|
2176
|
+
render(rows);
|
|
2177
|
+
};
|
|
2178
|
+
|
|
2179
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
2180
|
+
await refresh();
|
|
2181
|
+
refreshTimer = setInterval(refresh, 4000);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/* Kurulum progress event'i: sekme açıksa bar'ı anında güncelle (4 sn polling beklemeden).
|
|
2185
|
+
Sekme kapalıysa yok say — açılınca install:status zaten kayıtlı yüzeyi getirir. */
|
|
2186
|
+
function updateInstallPct(ev) {
|
|
2187
|
+
try {
|
|
2188
|
+
if (!ev || !ev.id) return;
|
|
2189
|
+
const pane = document.getElementById('tab-install');
|
|
2190
|
+
if (!pane || pane.hidden) return;
|
|
2191
|
+
const bar = document.getElementById('inst-bar-' + ev.id);
|
|
2192
|
+
const fill = document.getElementById('inst-fill-' + ev.id);
|
|
2193
|
+
const label = document.getElementById('inst-pct-' + ev.id);
|
|
2194
|
+
if (!bar || !fill || !label) return;
|
|
2195
|
+
const pct = Math.max(0, Math.min(100, Math.round(ev.pct || 0)));
|
|
2196
|
+
const mb = (ev.loaded && ev.total) ? ' · ' + Math.round(ev.loaded / 1048576) + '/' + Math.round(ev.total / 1048576) + ' MB' : '';
|
|
2197
|
+
bar.style.display = '';
|
|
2198
|
+
fill.style.width = Math.max(2, pct) + '%';
|
|
2199
|
+
label.style.display = '';
|
|
2200
|
+
label.textContent = '%' + pct + mb;
|
|
2201
|
+
} catch {}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
async function renderTtsPane() { const pane = $('#tab-tts');
|
|
2102
2205
|
if (!pane) return;
|
|
2103
2206
|
/* STT DURUMU: model indirildi mi / iniyor mu / hangi motor */
|
|
2104
2207
|
let stt = null;
|
|
@@ -4602,6 +4705,7 @@ function onEvent(ev) {
|
|
|
4602
4705
|
showApprovalCard(ev);
|
|
4603
4706
|
return;
|
|
4604
4707
|
}
|
|
4708
|
+
if (ev.type === 'install-progress') { updateInstallPct(ev); return; }
|
|
4605
4709
|
if (ev.type === 'update') {
|
|
4606
4710
|
if (ev.downloaded) toast(_t('up_downloaded') + ' (v' + (ev.version || '?') + ') — /update now');
|
|
4607
4711
|
else if (ev.available && ev.version && ev.version !== ev.current) toast(_t('up_available') + ' (v' + ev.version + ')');
|