Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4d3449e3a | |||
| 44d2c6b4fe | |||
| 0309c95aa5 | |||
| 2aa2cc70c9 | |||
| 9d0776c819 |
@@ -380,6 +380,7 @@ API-Endpoint fuer andere Services: `GET http://localhost:3001/api/session`
|
||||
- Text-Chat mit ARIA
|
||||
- **Sprachaufnahme**: Push-to-Talk (halten) oder Tap-to-Talk (tippen, Auto-Stop bei Stille)
|
||||
- **Gespraechsmodus** (Ohr-Button): Nach jeder ARIA-Antwort startet automatisch die Aufnahme — wie ein natuerliches Gespraech hin und her
|
||||
- **Wake-Word** (optional, Picovoice Porcupine on-device): "Jarvis", "Computer" usw. — Mikrofon hoert passiv mit, Konversation startet beim Schluesselwort. Eigene Wake-Words ueber die Picovoice Console moeglich. Ohne API-Key faellt der Ohr-Button auf Direkt-Aufnahme zurueck.
|
||||
- **VAD (Voice Activity Detection)**: Konfigurierbare Stille-Toleranz (1.0–8.0s, Default 2.8s) bevor Auto-Stop greift. Max-Aufnahme 120s.
|
||||
- **Speech Gate**: Aufnahme wird verworfen wenn keine Sprache erkannt
|
||||
- **STT (Speech-to-Text)**: 16kHz mono → Bridge → Gamebox-Whisper (CUDA) → Text im Chat. Fast in Echtzeit.
|
||||
@@ -398,6 +399,49 @@ API-Endpoint fuer andere Services: `GET http://localhost:3001/api/session`
|
||||
- GPS-Position (optional)
|
||||
- QR-Code Scanner fuer Token-Pairing
|
||||
|
||||
### Wake-Word einrichten (Picovoice Porcupine)
|
||||
|
||||
Das Wake-Word laeuft komplett **on-device** in der App — kein Audio verlaesst dein Telefon
|
||||
fuer die Erkennung. Picovoice bietet aktuell einen **7-Tage Free Trial** ohne Kreditkarte
|
||||
und ohne Auto-Renewal an, danach kostenpflichtig (siehe [picovoice.ai/pricing](https://picovoice.ai/pricing)).
|
||||
Wer das Wake-Word ueberspringen will: der Ohr-Button funktioniert auch ohne AccessKey
|
||||
(Direkt-Aufnahme statt passivem Lauschen — siehe unten).
|
||||
|
||||
**1) AccessKey holen** (einmalig, ~2 Minuten):
|
||||
|
||||
1. Auf [console.picovoice.ai](https://console.picovoice.ai) registrieren (Email + Passwort, keine Kreditkarte fuer den Trial).
|
||||
2. Nach dem Login auf dem Dashboard → **AccessKey** kopieren (langer Base64-String).
|
||||
|
||||
**2) AccessKey in der App eintragen:**
|
||||
|
||||
- App → **Einstellungen** → Abschnitt **Wake-Word**
|
||||
- AccessKey einfuegen, **Keyword** auswaehlen (Default: `jarvis`)
|
||||
- Speichern → die App initialisiert Porcupine automatisch
|
||||
|
||||
**Eingebaute Keywords** (sofort verfuegbar, kein Training noetig):
|
||||
`jarvis`, `computer`, `picovoice`, `porcupine`, `bumblebee`, `terminator`,
|
||||
`alexa`, `hey google`, `ok google`, `hey siri`
|
||||
|
||||
**3) Eigenes Wake-Word erstellen** ("ARIA", "Hey Stefan", was du willst):
|
||||
|
||||
1. [console.picovoice.ai](https://console.picovoice.ai) → **Porcupine** → **Train Wake Word**
|
||||
2. Wort eingeben (z.B. `ARIA`), Sprache `German` waehlen, Plattform `Android`
|
||||
3. **Train** druecken — Picovoice trainiert das Modell in ~1–2 Minuten
|
||||
4. Die fertige `.ppn`-Datei runterladen
|
||||
5. *(Custom-Upload in der App ist Phase 2 — aktuell nur eingebaute Keywords.
|
||||
`.ppn`-Dateien koennen schon manuell ins App-Bundle gelegt werden, die UI
|
||||
dafuer kommt mit dem naechsten Diagnostic-Update.)*
|
||||
|
||||
**Bedienung:**
|
||||
- **Ohr-Button (👂)** in der Statusleiste tippen → Wake-Word ist scharf, App hoert passiv mit
|
||||
- Wake-Word sagen → Symbol wechselt auf 🎙️, normale Konversation laeuft
|
||||
- Nach jeder ARIA-Antwort oeffnet sich das Mikro nochmal — Stille → zurueck zu 👂
|
||||
- Erneut tippen → Ohr aus (🔇)
|
||||
|
||||
**Ohne AccessKey:** Der Ohr-Button startet stattdessen die Direkt-Aufnahme (Mikro
|
||||
ist sofort aktiv, kein passives Lauschen). Auch ein gueltiger Modus, nur halt ohne
|
||||
"Hands-free" via Schluesselwort.
|
||||
|
||||
### Ersteinrichtung (Dev-Maschine, einmalig)
|
||||
|
||||
```bash
|
||||
@@ -744,8 +788,9 @@ docker exec aria-core ssh aria-wohnung hostname
|
||||
- **Proxy Cold Start**: Jede Nachricht spawnt einen neuen `claude --print` Prozess.
|
||||
Dadurch ist ARIA langsamer als die direkte Claude CLI. Timeout ist auf 900s (15 Min).
|
||||
- **Kein Streaming zur App**: Die App zeigt erst die fertige Antwort, keine Streaming-Tokens.
|
||||
- **Wake Word nur auf VM**: Die Bridge hoert auf "ARIA" ueber das lokale Mikrofon der VM.
|
||||
In der App gibt es Energy-basierte Erkennung (Phase 1). On-device "ARIA"-Keyword (Porcupine) ist Phase 2.
|
||||
- **Wake-Word in der App nur eingebaute Keywords**: `jarvis`, `computer` etc. funktionieren
|
||||
sofort, eigene Wake-Words (`.ppn` aus der Picovoice Console) muessen aktuell noch manuell
|
||||
ins App-Bundle. Die Upload-UI in Diagnostic ist Phase 2.
|
||||
- **Audio-Format**: App nimmt AAC/MP4 auf, Bridge konvertiert via FFmpeg zu 16kHz PCM.
|
||||
- **RVS Zombie-Connections**: WebSocket-Verbindungen sterben gelegentlich ohne Fehlermeldung.
|
||||
Bridge hat Ping-Check (5s), Diagnostic nutzt frische Verbindungen pro Request.
|
||||
@@ -800,6 +845,7 @@ docker exec aria-core ssh aria-wohnung hostname
|
||||
- [x] Audio-Pause statt Ducking (TRANSIENT statt MAY_DUCK) + release-Timing fix
|
||||
- [x] VAD-Stille-Toleranz und Max-Aufnahme einstellbar (1-8s, 120s)
|
||||
- [x] Disk-Voll Banner in Diagnostic mit copy-baren Cleanup-Befehlen
|
||||
- [x] Porcupine Wake-Word on-device in der App (eingebaute Keywords + State-Icon)
|
||||
|
||||
### Phase 2 — ARIA wird produktiv
|
||||
|
||||
@@ -815,5 +861,5 @@ docker exec aria-core ssh aria-wohnung hostname
|
||||
- [ ] STARFACE Telefonie-Skill
|
||||
- [ ] Desktop Client (Tauri)
|
||||
- [ ] bKVM Remote IT-Support
|
||||
- [ ] Porcupine Wake Word (on-device "ARIA" in der App)
|
||||
- [ ] Custom-`.ppn`-Upload fuer Wake-Word ueber Diagnostic (eigene Trigger-Worte)
|
||||
- [ ] Claude Vision direkt (Bildanalyse ohne Dateipfad-Umweg)
|
||||
|
||||
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 603
|
||||
versionName "0.0.6.3"
|
||||
versionCode 605
|
||||
versionName "0.0.6.5"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.0.6.3",
|
||||
"version": "0.0.6.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -72,13 +72,28 @@ interface Props {
|
||||
const MessageText: React.FC<Props> = ({ text, style }) => {
|
||||
const segments = React.useMemo(() => tokenize(text), [text]);
|
||||
return (
|
||||
<Text style={style} selectable>
|
||||
<Text
|
||||
style={style}
|
||||
selectable
|
||||
// dataDetectorType ist Android-only und macht Phone/URL/Email zusaetzlich
|
||||
// ueber System-Detection klickbar — als Fallback falls unsere Regex-
|
||||
// Tokens nicht passen.
|
||||
dataDetectorType="all"
|
||||
>
|
||||
{segments.map((seg, i) => {
|
||||
if (seg.kind === 'text') {
|
||||
return <Text key={i}>{seg.text}</Text>;
|
||||
return <Text key={i} selectable>{seg.text}</Text>;
|
||||
}
|
||||
return (
|
||||
<Text key={i} style={LINK_STYLE} onPress={() => onPress(seg)}>
|
||||
<Text
|
||||
key={i}
|
||||
selectable
|
||||
style={LINK_STYLE}
|
||||
onPress={() => onPress(seg)}
|
||||
// Long-Press soll an den Parent durch fuer Selection
|
||||
onLongPress={undefined}
|
||||
suppressHighlighting={false}
|
||||
>
|
||||
{seg.text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -191,6 +191,13 @@ class AudioService {
|
||||
private pcmBytesCollected: number = 0;
|
||||
private readonly PCM_MAX_CACHE_BYTES = 30 * 1024 * 1024; // 30MB
|
||||
|
||||
// AudioFocus wird verzoegert freigegeben — wenn ARIA eine zweite Antwort
|
||||
// direkt hinterherschickt (oder ein neuer Stream startet), bleibt Spotify
|
||||
// pausiert. Ohne diese Verzoegerung springt Spotify im Mikro-Sekunden-Gap
|
||||
// zwischen zwei Streams kurz wieder an.
|
||||
private focusReleaseTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private readonly FOCUS_RELEASE_DELAY_MS = 800;
|
||||
|
||||
// VAD State
|
||||
private vadEnabled: boolean = false;
|
||||
private lastSpeechTime: number = 0;
|
||||
@@ -205,6 +212,24 @@ class AudioService {
|
||||
this.recorder.setSubscriptionDuration(0.1); // 100ms Metering-Updates
|
||||
}
|
||||
|
||||
/** AudioFocus mit kleiner Verzoegerung freigeben — Spotify/YouTube
|
||||
* springen sonst im Gap zwischen zwei TTS-Streams (oder wenn ARIA
|
||||
* eine zweite Antwort direkt hinterherschickt) kurz wieder an. */
|
||||
private _releaseFocusDeferred(): void {
|
||||
this._cancelDeferredFocusRelease();
|
||||
this.focusReleaseTimer = setTimeout(() => {
|
||||
this.focusReleaseTimer = null;
|
||||
AudioFocus?.release().catch(() => {});
|
||||
}, this.FOCUS_RELEASE_DELAY_MS);
|
||||
}
|
||||
|
||||
private _cancelDeferredFocusRelease(): void {
|
||||
if (this.focusReleaseTimer) {
|
||||
clearTimeout(this.focusReleaseTimer);
|
||||
this.focusReleaseTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Berechtigungen ---
|
||||
|
||||
async requestMicrophonePermission(): Promise<boolean> {
|
||||
@@ -305,6 +330,7 @@ class AudioService {
|
||||
this.setState('recording');
|
||||
|
||||
// Andere Apps waehrend der Aufnahme pausieren (Musik, Videos etc.)
|
||||
this._cancelDeferredFocusRelease();
|
||||
AudioFocus?.requestExclusive().catch(() => {});
|
||||
|
||||
// VAD aktivieren — Stille-Dauer aus AsyncStorage (Settings-konfigurierbar).
|
||||
@@ -387,8 +413,9 @@ class AudioService {
|
||||
await this.recorder.stopRecorder();
|
||||
this.recorder.removeRecordBackListener();
|
||||
|
||||
// Audio-Focus freigeben — andere Apps duerfen wieder
|
||||
AudioFocus?.release().catch(() => {});
|
||||
// Audio-Focus verzoegert freigeben — gleich kommt die TTS-Antwort,
|
||||
// im Gap soll Spotify nicht hochkommen.
|
||||
this._releaseFocusDeferred();
|
||||
|
||||
const durationMs = Date.now() - this.recordingStartTime;
|
||||
const hadSpeech = this.speechDetected;
|
||||
@@ -535,6 +562,7 @@ class AudioService {
|
||||
this.pcmStreamActive = false;
|
||||
return '';
|
||||
}
|
||||
this._cancelDeferredFocusRelease();
|
||||
AudioFocus?.requestDuck().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -553,11 +581,12 @@ class AudioService {
|
||||
if (isFinal) {
|
||||
if (!silent) {
|
||||
// end() resolved jetzt erst wenn der native Writer-Thread fertig
|
||||
// ist (alle Samples ausgespielt) — danach erst AudioFocus freigeben,
|
||||
// damit Spotify/YouTube nicht waehrend des Pre-Roll-Ausklangs
|
||||
// wieder aufdrehen.
|
||||
// ist (alle Samples ausgespielt) — danach AudioFocus verzoegert
|
||||
// freigeben, damit Spotify/YouTube nicht im Mikro-Gap zwischen zwei
|
||||
// ARIA-Antworten wieder hochdrehen. Wenn ein neuer Stream innerhalb
|
||||
// FOCUS_RELEASE_DELAY_MS startet, wird das Release abgebrochen.
|
||||
try { await PcmStreamPlayer!.end(); } catch {}
|
||||
AudioFocus?.release().catch(() => {});
|
||||
this._releaseFocusDeferred();
|
||||
}
|
||||
this.pcmStreamActive = false;
|
||||
|
||||
@@ -661,8 +690,9 @@ class AudioService {
|
||||
private async _playNext(): Promise<void> {
|
||||
if (this.audioQueue.length === 0) {
|
||||
this.isPlaying = false;
|
||||
// Audio-Focus abgeben → andere Apps volle Lautstaerke
|
||||
AudioFocus?.release().catch(() => {});
|
||||
// Audio-Focus verzoegert abgeben → wenn gleich noch eine Antwort kommt,
|
||||
// bleibt Spotify pausiert.
|
||||
this._releaseFocusDeferred();
|
||||
// Alle Audio-Teile abgespielt → Listener benachrichtigen
|
||||
this.playbackFinishedListeners.forEach(cb => cb());
|
||||
return;
|
||||
@@ -670,6 +700,7 @@ class AudioService {
|
||||
|
||||
// Beim ersten Playback-Start: andere Apps ducken
|
||||
if (!this.isPlaying) {
|
||||
this._cancelDeferredFocusRelease();
|
||||
AudioFocus?.requestDuck().catch(() => {});
|
||||
}
|
||||
this.isPlaying = true;
|
||||
@@ -755,7 +786,8 @@ class AudioService {
|
||||
this.pcmBytesCollected = 0;
|
||||
this.pcmMessageId = '';
|
||||
}
|
||||
// Audio-Focus freigeben
|
||||
// Audio-Focus sofort freigeben — User hat explizit abgebrochen
|
||||
this._cancelDeferredFocusRelease();
|
||||
AudioFocus?.release().catch(() => {});
|
||||
}
|
||||
|
||||
|
||||
@@ -239,6 +239,8 @@ class F5Runner:
|
||||
|
||||
def _infer_blocking(self, gen_text: str, ref_wav: str, ref_text: str,
|
||||
speed: float = 1.0) -> tuple[np.ndarray, int]:
|
||||
logger.info("infer() text=%d chars, speed=%.2f, cfg=%.2f, nfe=%d",
|
||||
len(gen_text), speed, self.cfg_strength, self.nfe_step)
|
||||
wav, sr, _ = self.model.infer(
|
||||
ref_file=ref_wav,
|
||||
ref_text=ref_text,
|
||||
|
||||
Reference in New Issue
Block a user