beast-agent 2.3.3 → 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 +99 -6
- package/src/renderer/renderer.js +34 -0
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) => {
|
|
@@ -3918,7 +3954,18 @@ async function ocrRead({ image, lang = 'tur+eng' } = {}) {
|
|
|
3918
3954
|
if (!worker) {
|
|
3919
3955
|
const tessDir = path.join(APP_DIR, 'tessdata');
|
|
3920
3956
|
fs.mkdirSync(tessDir, { recursive: true });
|
|
3921
|
-
|
|
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
|
+
});
|
|
3922
3969
|
_ocrWorkers.set(langKey, worker);
|
|
3923
3970
|
}
|
|
3924
3971
|
let input = image;
|
|
@@ -5839,6 +5886,20 @@ ipcMain.handle('stt:prefetch', () => {
|
|
|
5839
5886
|
ipcMain.handle('install:status', async () => {
|
|
5840
5887
|
const rows = [];
|
|
5841
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
|
+
};
|
|
5842
5903
|
const scanModel = (rel) => {
|
|
5843
5904
|
const dir = path.join(APP_DIR, 'models', ...rel.split('/'));
|
|
5844
5905
|
let files = [];
|
|
@@ -5870,14 +5931,28 @@ ipcMain.handle('install:status', async () => {
|
|
|
5870
5931
|
else if (s.hasCfg && s.onnx >= 2 && s.tmp === 0) state = 'downloaded';
|
|
5871
5932
|
else if (s.files.length) state = 'partial';
|
|
5872
5933
|
else state = 'missing';
|
|
5873
|
-
rows.push({
|
|
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
|
+
});
|
|
5874
5942
|
}
|
|
5875
5943
|
|
|
5876
5944
|
/* 2) Embedding modeli (hafıza semantik arama) */
|
|
5877
5945
|
{
|
|
5878
5946
|
const s = scanModel('Xenova/all-MiniLM-L6-v2');
|
|
5879
5947
|
const state = s.hasCfg && s.onnx >= 1 && s.tmp === 0 ? 'ok' : (s.files.length ? 'partial' : 'missing');
|
|
5880
|
-
rows.push({
|
|
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
|
+
});
|
|
5881
5956
|
}
|
|
5882
5957
|
|
|
5883
5958
|
/* 3) ffmpeg */
|
|
@@ -5890,7 +5965,17 @@ ipcMain.handle('install:status', async () => {
|
|
|
5890
5965
|
rows.push({ id: 'ort', name: 'ONNX Runtime — model motoru', state: pkgOk('onnxruntime-node') ? 'ok' : 'missing', detail: 'npm paketi' });
|
|
5891
5966
|
|
|
5892
5967
|
/* 5) OCR */
|
|
5893
|
-
|
|
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
|
+
}
|
|
5894
5979
|
|
|
5895
5980
|
/* 6) Python (opsiyonel — betikler) */
|
|
5896
5981
|
let pyVer = '';
|
|
@@ -5900,7 +5985,15 @@ ipcMain.handle('install:status', async () => {
|
|
|
5900
5985
|
if (pyVer) break;
|
|
5901
5986
|
} catch {}
|
|
5902
5987
|
}
|
|
5903
|
-
|
|
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
|
+
}
|
|
5904
5997
|
|
|
5905
5998
|
/* 7) Edge TTS (bulut) */
|
|
5906
5999
|
rows.push({ id: 'edge', name: 'Edge TTS — seslendirme', state: 'cloud', detail: 'bulut — kurulum gerekmez' });
|
package/src/renderer/renderer.js
CHANGED
|
@@ -2146,13 +2146,26 @@ async function renderInstallPane() {
|
|
|
2146
2146
|
if (!el) return;
|
|
2147
2147
|
if (!rows || !rows.length) { el.innerHTML = '<div class="sub">Durum alınamadı</div>'; return; }
|
|
2148
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
|
+
};
|
|
2149
2160
|
el.innerHTML = rows.map((r) => (
|
|
2150
2161
|
'<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--border)">' +
|
|
2151
2162
|
'<div style="flex:1;min-width:0">' +
|
|
2152
2163
|
'<div style="font-size:13px">' + escapeHtml(r.name) + '</div>' +
|
|
2153
2164
|
'<div class="sub" style="font-size:11px">' + escapeHtml(r.detail || '') + (r.mb ? ' · ' + r.mb + ' MB' : '') + '</div>' +
|
|
2165
|
+
bar(r) +
|
|
2154
2166
|
'</div>' +
|
|
2155
2167
|
badge(r.state) +
|
|
2168
|
+
pctLabel(r) +
|
|
2156
2169
|
'</div>'
|
|
2157
2170
|
)).join('');
|
|
2158
2171
|
};
|
|
@@ -2168,6 +2181,26 @@ async function renderInstallPane() {
|
|
|
2168
2181
|
refreshTimer = setInterval(refresh, 4000);
|
|
2169
2182
|
}
|
|
2170
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
|
+
|
|
2171
2204
|
async function renderTtsPane() { const pane = $('#tab-tts');
|
|
2172
2205
|
if (!pane) return;
|
|
2173
2206
|
/* STT DURUMU: model indirildi mi / iniyor mu / hangi motor */
|
|
@@ -4672,6 +4705,7 @@ function onEvent(ev) {
|
|
|
4672
4705
|
showApprovalCard(ev);
|
|
4673
4706
|
return;
|
|
4674
4707
|
}
|
|
4708
|
+
if (ev.type === 'install-progress') { updateInstallPct(ev); return; }
|
|
4675
4709
|
if (ev.type === 'update') {
|
|
4676
4710
|
if (ev.downloaded) toast(_t('up_downloaded') + ' (v' + (ev.version || '?') + ') — /update now');
|
|
4677
4711
|
else if (ev.available && ev.version && ev.version !== ev.current) toast(_t('up_available') + ' (v' + ev.version + ')');
|