Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 213edac3a7 | |||
| acc13aef6b |
@@ -42,6 +42,8 @@ const AUDIO_ENCODING = 'audio/wav';
|
|||||||
// VAD (Voice Activity Detection) — Stille-Erkennung
|
// VAD (Voice Activity Detection) — Stille-Erkennung
|
||||||
const VAD_SILENCE_THRESHOLD_DB = -45; // dB unter dem als "Stille" gilt
|
const VAD_SILENCE_THRESHOLD_DB = -45; // dB unter dem als "Stille" gilt
|
||||||
const VAD_SILENCE_DURATION_MS = 1800; // ms Stille bevor Auto-Stop
|
const VAD_SILENCE_DURATION_MS = 1800; // ms Stille bevor Auto-Stop
|
||||||
|
const VAD_SPEECH_THRESHOLD_DB = -35; // dB ueber dem als "Sprache" gilt (Sprach-Gate)
|
||||||
|
const VAD_SPEECH_MIN_MS = 300; // ms Sprache bevor Aufnahme zaehlt
|
||||||
|
|
||||||
// --- Audio-Service ---
|
// --- Audio-Service ---
|
||||||
|
|
||||||
@@ -61,6 +63,10 @@ class AudioService {
|
|||||||
private preloadedSound: Sound | null = null;
|
private preloadedSound: Sound | null = null;
|
||||||
private preloadedPath: string = '';
|
private preloadedPath: string = '';
|
||||||
|
|
||||||
|
// Sprach-Gate: Aufnahme erst senden wenn tatsaechlich gesprochen wurde
|
||||||
|
private speechDetected: boolean = false;
|
||||||
|
private speechStartTime: number = 0;
|
||||||
|
|
||||||
// VAD State
|
// VAD State
|
||||||
private vadEnabled: boolean = false;
|
private vadEnabled: boolean = false;
|
||||||
private lastSpeechTime: number = 0;
|
private lastSpeechTime: number = 0;
|
||||||
@@ -128,7 +134,21 @@ class AudioService {
|
|||||||
const db = e.currentMetering ?? -160;
|
const db = e.currentMetering ?? -160;
|
||||||
this.meterListeners.forEach(cb => cb(db));
|
this.meterListeners.forEach(cb => cb(db));
|
||||||
|
|
||||||
// VAD: Stille erkennen
|
// Sprach-Gate: Erkennen ob tatsaechlich gesprochen wird
|
||||||
|
if (db > VAD_SPEECH_THRESHOLD_DB) {
|
||||||
|
if (!this.speechDetected && this.speechStartTime === 0) {
|
||||||
|
this.speechStartTime = Date.now();
|
||||||
|
}
|
||||||
|
if (this.speechStartTime > 0 && Date.now() - this.speechStartTime >= VAD_SPEECH_MIN_MS) {
|
||||||
|
this.speechDetected = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!this.speechDetected) {
|
||||||
|
this.speechStartTime = 0; // Reset wenn noch nicht als Sprache erkannt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VAD: Stille erkennen (nur wenn Sprache erkannt wurde)
|
||||||
if (this.vadEnabled) {
|
if (this.vadEnabled) {
|
||||||
if (db > VAD_SILENCE_THRESHOLD_DB) {
|
if (db > VAD_SILENCE_THRESHOLD_DB) {
|
||||||
this.lastSpeechTime = Date.now();
|
this.lastSpeechTime = Date.now();
|
||||||
@@ -138,6 +158,8 @@ class AudioService {
|
|||||||
|
|
||||||
this.recordingStartTime = Date.now();
|
this.recordingStartTime = Date.now();
|
||||||
this.lastSpeechTime = Date.now();
|
this.lastSpeechTime = Date.now();
|
||||||
|
this.speechDetected = false;
|
||||||
|
this.speechStartTime = 0;
|
||||||
this.setState('recording');
|
this.setState('recording');
|
||||||
|
|
||||||
// VAD aktivieren
|
// VAD aktivieren
|
||||||
@@ -180,6 +202,15 @@ class AudioService {
|
|||||||
this.recorder.removeRecordBackListener();
|
this.recorder.removeRecordBackListener();
|
||||||
|
|
||||||
const durationMs = Date.now() - this.recordingStartTime;
|
const durationMs = Date.now() - this.recordingStartTime;
|
||||||
|
const hadSpeech = this.speechDetected;
|
||||||
|
|
||||||
|
// Sprach-Gate: Wenn keine Sprache erkannt → Aufnahme verwerfen
|
||||||
|
if (!hadSpeech) {
|
||||||
|
RNFS.unlink(this.recordingPath).catch(() => {});
|
||||||
|
this.setState('idle');
|
||||||
|
console.log('[Audio] Aufnahme verworfen — keine Sprache erkannt (nur Umgebungsgeraeusche)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Audio-Datei als Base64 lesen
|
// Audio-Datei als Base64 lesen
|
||||||
const base64Data = await RNFS.readFile(this.recordingPath, 'base64');
|
const base64Data = await RNFS.readFile(this.recordingPath, 'base64');
|
||||||
@@ -188,7 +219,7 @@ class AudioService {
|
|||||||
RNFS.unlink(this.recordingPath).catch(() => {});
|
RNFS.unlink(this.recordingPath).catch(() => {});
|
||||||
|
|
||||||
this.setState('idle');
|
this.setState('idle');
|
||||||
console.log(`[Audio] Aufnahme beendet (${durationMs}ms, ${Math.round(base64Data.length / 1024)}KB)`);
|
console.log(`[Audio] Aufnahme beendet (${durationMs}ms, ${Math.round(base64Data.length / 1024)}KB, Sprache erkannt)`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base64: base64Data,
|
base64: base64Data,
|
||||||
|
|||||||
+53
-18
@@ -37,15 +37,41 @@ const state = {
|
|||||||
};
|
};
|
||||||
const SESSION_KEY_FILE = "/data/active-session";
|
const SESSION_KEY_FILE = "/data/active-session";
|
||||||
// /data Verzeichnis sicherstellen (Volume Mount)
|
// /data Verzeichnis sicherstellen (Volume Mount)
|
||||||
try { fs.mkdirSync("/data", { recursive: true }); } catch {}
|
try { fs.mkdirSync("/data", { recursive: true }); } catch (e) {
|
||||||
|
console.error(`[startup] /data mkdir fehlgeschlagen: ${e.message}`);
|
||||||
|
}
|
||||||
|
// sessionFromFile zeigt an, ob der aktive Key aus der Datei kam.
|
||||||
|
// Wenn true, darf resolveActiveSession NICHT mehr auto-picken (Wahl respektieren).
|
||||||
|
let sessionFromFile = false;
|
||||||
let activeSessionKey = (() => {
|
let activeSessionKey = (() => {
|
||||||
try {
|
try {
|
||||||
const saved = fs.readFileSync(SESSION_KEY_FILE, "utf-8").trim();
|
const saved = fs.readFileSync(SESSION_KEY_FILE, "utf-8").trim();
|
||||||
if (saved) { console.log(`[startup] Gespeicherte Session geladen: '${saved}'`); return saved; }
|
if (saved) {
|
||||||
} catch {}
|
console.log(`[startup] Gespeicherte Session geladen: '${saved}'`);
|
||||||
|
sessionFromFile = true;
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[startup] SESSION_KEY_FILE read: ${e.code || e.message}`);
|
||||||
|
}
|
||||||
console.log("[startup] Keine gespeicherte Session — Fallback 'main'");
|
console.log("[startup] Keine gespeicherte Session — Fallback 'main'");
|
||||||
return "main";
|
return "main";
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Atomic write: temp-file + rename, laute Logs bei Fehler.
|
||||||
|
function persistActiveSession(key) {
|
||||||
|
try {
|
||||||
|
const tmp = SESSION_KEY_FILE + ".tmp";
|
||||||
|
fs.writeFileSync(tmp, key);
|
||||||
|
fs.renameSync(tmp, SESSION_KEY_FILE);
|
||||||
|
sessionFromFile = true;
|
||||||
|
console.log(`[session] Aktive Session persistiert: '${key}'`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[session] FEHLER beim Persistieren von '${key}': ${e.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
const logs = [];
|
const logs = [];
|
||||||
let gatewayWs = null;
|
let gatewayWs = null;
|
||||||
let rvsWs = null;
|
let rvsWs = null;
|
||||||
@@ -1662,13 +1688,11 @@ async function handleDeleteSession(clientWs, sessionPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Session-Aufloesung: letzte aktive Session finden ────
|
// ── Session-Aufloesung: letzte aktive Session finden ────
|
||||||
|
// Wird nach Gateway-(Re-)Connect aufgerufen. Darf die explizit gewaehlte
|
||||||
|
// Session NIE ueberschreiben — nur beim absoluten Erststart auto-picken.
|
||||||
async function resolveActiveSession() {
|
async function resolveActiveSession() {
|
||||||
// Nur bei Fallback-Key "main" automatisch aufloesen — gespeicherte Wahl respektieren
|
if (sessionFromFile) {
|
||||||
const hasSavedSession = (() => {
|
log("info", "server", `Session '${activeSessionKey}' aus /data — keine Auto-Wahl`);
|
||||||
try { return !!fs.readFileSync(SESSION_KEY_FILE, "utf-8").trim(); } catch { return false; }
|
|
||||||
})();
|
|
||||||
if (hasSavedSession && activeSessionKey !== "main") {
|
|
||||||
log("info", "server", `Gespeicherte Session '${activeSessionKey}' wird beibehalten`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1687,10 +1711,19 @@ async function resolveActiveSession() {
|
|||||||
const keys = entries.map(e => (e.key || e.sessionKey || e.name || "?").replace(/^agent:main:/, ""));
|
const keys = entries.map(e => (e.key || e.sessionKey || e.name || "?").replace(/^agent:main:/, ""));
|
||||||
log("info", "server", `Verfuegbare Sessions: [${keys.join(", ")}]`);
|
log("info", "server", `Verfuegbare Sessions: [${keys.join(", ")}]`);
|
||||||
|
|
||||||
// Neueste Session nehmen
|
// Neueste Session nehmen — aber user-definierte bevorzugen.
|
||||||
|
// aria-bridge / aria-diagnostic werden von den Services auto-erstellt;
|
||||||
|
// bei erstem Start soll lieber eine "echte" Session gewaehlt werden,
|
||||||
|
// falls vorhanden.
|
||||||
|
const AUTO_KEYS = new Set(["aria-bridge", "aria-diagnostic"]);
|
||||||
|
const normalise = (e) => (e.key || e.sessionKey || e.name || "").replace(/^agent:main:/, "");
|
||||||
|
|
||||||
|
const userEntries = entries.filter(e => !AUTO_KEYS.has(normalise(e)));
|
||||||
|
const pool = userEntries.length > 0 ? userEntries : entries;
|
||||||
|
|
||||||
let newest = null;
|
let newest = null;
|
||||||
let newestTime = 0;
|
let newestTime = 0;
|
||||||
for (const entry of entries) {
|
for (const entry of pool) {
|
||||||
const t = entry.updatedAt || entry.createdAt || 0;
|
const t = entry.updatedAt || entry.createdAt || 0;
|
||||||
if (t >= newestTime) {
|
if (t >= newestTime) {
|
||||||
newestTime = t;
|
newestTime = t;
|
||||||
@@ -1699,12 +1732,11 @@ async function resolveActiveSession() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newest) {
|
if (newest) {
|
||||||
const rawKey = newest.key || newest.sessionKey || newest.name || "";
|
const key = normalise(newest);
|
||||||
const key = rawKey.replace(/^agent:main:/, "");
|
|
||||||
if (key) {
|
if (key) {
|
||||||
activeSessionKey = key;
|
activeSessionKey = key;
|
||||||
try { fs.writeFileSync(SESSION_KEY_FILE, activeSessionKey); } catch {}
|
persistActiveSession(activeSessionKey);
|
||||||
log("info", "server", `Aktive Session auf neueste gewechselt: '${activeSessionKey}'`);
|
log("info", "server", `Auto-Wahl Erststart: '${activeSessionKey}'`);
|
||||||
for (const c of browserClients) {
|
for (const c of browserClients) {
|
||||||
c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
|
c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
|
||||||
}
|
}
|
||||||
@@ -1793,8 +1825,11 @@ function handleSetActiveSession(clientWs, sessionKey) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeSessionKey = sessionKey;
|
activeSessionKey = sessionKey;
|
||||||
try { fs.writeFileSync(SESSION_KEY_FILE, activeSessionKey); } catch {}
|
const ok = persistActiveSession(activeSessionKey);
|
||||||
log("info", "server", `Aktive Session: ${activeSessionKey}`);
|
log("info", "server", `Aktive Session: ${activeSessionKey}${ok ? "" : " (WARN: nicht persistiert!)"}`);
|
||||||
|
if (!ok) {
|
||||||
|
clientWs.send(JSON.stringify({ type: "active_session", ok: false, sessionKey: activeSessionKey, error: "Persistierung fehlgeschlagen — /data Volume pruefen" }));
|
||||||
|
}
|
||||||
// Allen Clients mitteilen
|
// Allen Clients mitteilen
|
||||||
for (const c of browserClients) {
|
for (const c of browserClients) {
|
||||||
c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
|
c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey }));
|
||||||
@@ -1810,7 +1845,7 @@ async function handleCreateSession(clientWs, sessionName) {
|
|||||||
try {
|
try {
|
||||||
// Session wird automatisch erstellt wenn man die erste Nachricht sendet
|
// Session wird automatisch erstellt wenn man die erste Nachricht sendet
|
||||||
activeSessionKey = sessionName;
|
activeSessionKey = sessionName;
|
||||||
try { fs.writeFileSync(SESSION_KEY_FILE, activeSessionKey); } catch {}
|
persistActiveSession(activeSessionKey);
|
||||||
log("info", "server", `Neue Session erstellt und aktiviert: ${sessionName}`);
|
log("info", "server", `Neue Session erstellt und aktiviert: ${sessionName}`);
|
||||||
// Allen Clients mitteilen
|
// Allen Clients mitteilen
|
||||||
for (const c of browserClients) {
|
for (const c of browserClients) {
|
||||||
|
|||||||
Reference in New Issue
Block a user