diff --git a/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt b/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt index 6f5ec5f..9554c1d 100644 --- a/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt +++ b/android/android/app/src/main/java/com/ariacockpit/PcmStreamPlayerModule.kt @@ -33,6 +33,9 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex // Sekunden Audio die VOR play()-Start gepuffert sein muessen. // 2.5s Vorrat = genug um XTTS-Render-Pausen zwischen Chunks zu puffern. private const val PREROLL_SECONDS = 2.5 + // Stille am Stream-Anfang, damit AudioTrack sauber anfaehrt und die + // ersten Samples nicht abgeschnitten werden (XTTS-Warmup + play()-Latenz). + private const val LEADING_SILENCE_SECONDS = 0.2 } override fun getName() = "PcmStreamPlayer" @@ -94,6 +97,18 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex writerThread = Thread({ val t = track ?: return@Thread try { + // Leading-Silence in den Buffer — gibt AudioTrack Zeit anzufahren. + val silenceBytes = ((sampleRate * channels * 2) * LEADING_SILENCE_SECONDS).toInt() and 0x7FFFFFFE + if (silenceBytes > 0) { + val silence = ByteArray(silenceBytes) + var silOff = 0 + while (silOff < silence.size && !writerShouldStop) { + val w = t.write(silence, silOff, silence.size - silOff) + if (w <= 0) break + silOff += w + } + bytesBuffered += silence.size + } while (!writerShouldStop) { val data = queue.poll(50, java.util.concurrent.TimeUnit.MILLISECONDS) ?: run { if (endRequested) { diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 1d5655c..b83ebb4 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -150,6 +150,15 @@ def _small_range_to_words(m): return f"{_num_to_words_de(a)} bis {_num_to_words_de(b)}" +def _decimal_to_words(m): + """'0.1' / '0,1' → 'null komma eins', '1,25' → 'eins komma zwei fuenf'.""" + int_part = int(m.group(1)) + dec_part = m.group(2) + int_word = _num_to_words_de(int_part) if 0 <= int_part <= 59 else str(int_part) + dec_words = " ".join(_num_to_words_de(int(d)) for d in dec_part) + return f"{int_word} komma {dec_words}" + + _UNIT_WORDS = [ (r'\bTB\b', 'Terabyte'), (r'\bGB\b', 'Gigabyte'), @@ -236,6 +245,11 @@ def clean_text_for_tts(text: str) -> str: # Kleine Zahlen-Bereiche ohne "Uhr": "5-6" → "fuenf bis sechs" t = _re_tts.sub(r'\b(\d{1,2})\s*[-–]\s*(\d{1,2})\b', _small_range_to_words, t) + # Dezimalzahlen: "0.1" / "0,5" / "1,25" → "null komma eins" / "null komma fuenf" / ... + # Muss vor "Zahl+Einheit" laufen, sonst frisst die Unit-Regel den Nachkommaanteil. + # Lookahead verhindert Match auf IP-artigen Strings wie 192.168.1.1. + t = _re_tts.sub(r'\b(\d+)[.,](\d+)(?![.,\d])', _decimal_to_words, t) + # Zahlen + Einheit: "22GB" → "22 Gigabyte" (Leerzeichen einfuegen) t = _re_tts.sub(r'(\d+)([A-Za-z]{1,4})\b', r'\1 \2', t)