// Sperre für schwere Hintergrundarbeit, solange eine Aufnahme läuft. // // Warum: whisper, die sherpa-Diarisierung und rkllm dürfen nicht neben einer // laufenden Aufnahme arbeiten. Auf diesem lüfterlosen 8-GB-Board hält rkllm // allein ~6,7 GB (RssShmem 4,79 GB + RssAnon 1,93 GB von 7,74 GB MemTotal), es // bleibt also ~1 GB Rest. Am 2026-08-06 lief die Analyse von #96 gleichzeitig // mit der Transkription von #97 an — der globale OOM-Killer erschlug // rkllm_server, systemd nahm den ganzen Dienst mit ('oom-kill'), und die GUI // zeigte "failed to fetch". Dazu treibt die Zusatzlast die A76-Kerne auf ~80 °C, // während das Mikrofon offen ist, und kostet Audioqualität. // // Der Recorder ist die einzige Wahrheit über den Aufnahmezustand; server.js // hängt die Abfrage beim Start ein. Eigenes Modul, weil recorder.js bereits // llm.js importiert — llm.js darf recorder.js deshalb nicht zurückimportieren // (Zyklus). let probe = () => false; // Beim Start aus server.js: gate.setProbe(() => rec.isRecording()). export function setProbe(fn) { probe = typeof fn === 'function' ? fn : (() => false); } export function busy() { try { return !!probe(); } catch { return false; } } // Parken, bis der Recorder wieder still ist. Ein- und Austritt werden geloggt, // damit eine lange Pause im Journal als Pause erkennbar ist und nicht wie ein // hängender Job aussieht. export async function waitIdle(label) { if (!busy()) return 0; const t0 = Date.now(); console.log(`gate: ${label} pausiert — Aufnahme läuft`); while (busy()) await new Promise(r => setTimeout(r, 2000)); const s = Math.round((Date.now() - t0) / 1000); console.log(`gate: ${label} fortgesetzt nach ${s} s Pause`); return s; } // --- Speicher freiräumen, bevor ein großes Modell lädt ----------------------- // // Auf diesem 7,74-GB-Board darf nur EIN großes Modell resident sein. rkllm_server // braucht allein ~6,5 GB (davon 4,79 GB unevictable NPU-Shmem), und daneben halten // die faul gestarteten Dauerhelfer in der Spitze (gemessen, VmHWM): // e5-small (embed_text.py) 1,15 GB // CT2-Whisper (transcribe_daemon) 0,90 GB // TitaNet (spk_embed) 0,70 GB // Am 2026-08-07 kostete das die Analyse von #101: die 2-Sekunden-Aufnahme #100 // davor hatte den warmen CT2-Daemon gestartet (Kurz-Clip-Pfad, bleibt per Design // resident), danach wurde rkllm in ALLEN 5 Startversuchen OOM-getötet → GUI // "KI-Analyse fehlgeschlagen". zram hilft hier NICHT: der komprimierte Swap liegt // selbst im RAM, ausgelagerte 0,9 GB kosten immer noch ~0,3-0,45 GB physisch. // // Jeder Helfer registriert deshalb, wie er sich beenden lässt; wer ein großes // Modell laden will, ruft vorher freeRam(). Alle Helfer starten // beim nächsten Aufruf faul neu — es geht nur Ladezeit verloren, kein Zustand. const releasers = new Map(); export function registerReleaser(name, stop) { releasers.set(name, stop); } // Beendet einen Kindprozess und wartet, bis er wirklich weg ist (sonst hat der // Kernel die Seiten beim Laden des nächsten Modells noch nicht zurück). // Rückgabe: true, wenn etwas beendet wurde. export function killAndWait(proc, timeoutMs = 5000) { if (!proc || proc.exitCode !== null || proc.signalCode) return Promise.resolve(false); return new Promise(resolve => { let settled = false; const done = () => { if (!settled) { settled = true; resolve(true); } }; proc.once('exit', done); try { proc.kill('SIGTERM'); } catch { done(); return; } setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} setTimeout(done, 500); }, timeoutMs); }); } // Alle registrierten Helfer außer `keep` beenden. Wird vor jedem Ladeversuch // eines großen Modells aufgerufen — auch vor einem Retry, denn ein Retry ohne // freien Speicher wird genauso OOM-getötet wie der erste Versuch. export async function freeRam(keep) { const freed = []; for (const [name, stop] of releasers) { if (name === keep) continue; try { if (await stop()) freed.push(name); } catch (e) { console.warn(`ram: ${name} nicht freigegeben — ${e.message}`); } } if (freed.length) { console.log(`ram: für ${keep} freigegeben — ${freed.join(', ')}`); await new Promise(r => setTimeout(r, 1500)); // Kernel-Reclaim abwarten } return freed; }