Skip to content

feat: OpenAI Cloud-TTS als optionale Vorlese-Engine - #10

Merged
TimInTech merged 3 commits into
mainfrom
feature/openai-cloud-tts
Jun 19, 2026
Merged

feat: OpenAI Cloud-TTS als optionale Vorlese-Engine#10
TimInTech merged 3 commits into
mainfrom
feature/openai-cloud-tts

Conversation

@TimInTech

Copy link
Copy Markdown
Owner

Ziel

Optionale Cloud-basierte Vorlese-Engine (OpenAI TTS) als Alternative zum lokalen Piper. Piper bleibt Default; Cloud-TTS ist opt-in und erst nach ausdrücklicher Datenschutz-Zustimmung aktiv.

Änderungen

  • app/config.py: drei nicht-geheime Felder + Validierung — tts_provider (piper|openai, Default piper), tts_openai_model (Default gpt-4o-mini-tts), tts_openai_voice (Default marin, Whitelist von 13 Stimmen), tts_openai_consent (bool, Default false).
  • app/tts_window.py:
    • CloudTtsService — Synthese via openai.audio.speech.create(..., timeout=30), Wiedergabe über die bestehende _playback_command()-Pipeline (paplay/aplay).
    • _CloudTtsWorker (QThread) — nicht-blockierende Synthese; Status „Wiedergabe…" mit aktiven Pause/Stopp/Nochmal-Aktionen.
    • Einmaliger Datenschutz-Dialog mit Persistenz (_ensure_openai_consent); bei Ablehnung Revert auf Piper.
    • Secret-sichere Fehlerausgabe (_scrub_secret).
    • QThread-Lifetime-Fix: detachte, noch laufende Threads werden in _detached_cloud_threads referenziert und erst bei finished via _on_detached_thread_finisheddeleteLater freigegeben (verhindert „QThread destroyed while running").
  • README.md / ROADMAP.md: Doku zur Cloud-TTS-Option.

Secret-Modell

Wie bei OpenRouter/OpenAI-LLM: nur der Env-Var-Name steht in config.json; der Key liegt in ~/.config/blitztext-linux/secrets.env (mode 0600). Keine Secrets im Repo.

Datenschutz

Cloud-TTS sendet den vorzulesenden Text an OpenAI. Aktivierung ausschließlich nach explizitem Consent-Dialog; Zustimmung wird persistiert, Ablehnung fällt auf Piper zurück.

Testplan

  • Vollsuite offscreen grün: 220 passed (QT_QPA_PLATFORM=offscreen WHISPER_GUI_TESTS=1)
  • Unit-Tests: fehlender Key / fehlendes openai-Paket / Thread-Detach bei hängendem Netzaufruf
  • Live-Test (echter Wayland-Desktop, kein Mock): Consent-Gate, echte Synthese gpt-4o-mini-tts / Stimme nova / Tempo „Schnell", Playback + Pause/Stopp/Nochmal verifiziert
  • CI grün (Push triggert Workflow)

Bekannte Nachzieher (separat, nicht in diesem PR)

is_available() ignoriert fehlendes openai-Paket (UI „bereit" irreführend); tts_openai_model ohne Whitelist; Install-Hint trennt Key-fehlt/Paket-fehlt nicht; OpenAI-Client-Init-Timeout. Sind als eigene Roadmap-Items erfasst.

🤖 Generated with Claude Code

TimInTech and others added 2 commits June 19, 2026 17:21
Ergaenzt das Vorlese-Fenster um einen optionalen OpenAI-Cloud-TTS-Anbieter
neben dem lokalen Piper (Default). Synthese laeuft non-blocking in einem
QThread mit 30s-Timeout; Wiedergabe ueber die bestehende paplay/aplay-Kette.

- config: tts_provider (piper|openai), tts_openai_model, tts_openai_voice,
  tts_openai_consent inkl. Validierung + Sanitize
- tts_window: CloudTtsService, _CloudTtsWorker, Anbieter-/Modell-UI,
  einmaliger Datenschutz-Dialog (persistente Zustimmung), secret-sichere
  Fehlertexte, robuster Thread-Abbruch mit referenzgehaltenem Detach
  (kein "QThread destroyed while running")
- Tests: +14 (Speed-Mapping, Synthese, Consent-Gate, Secret-Scrub,
  Detach-Timeout-Pfad)
- README: Anbieterwahl und neue Config-Felder dokumentiert

Key kommt ausschliesslich aus OPENAI_API_KEY (secrets.env), nie aus config.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHu5E1iQWEa6LViTcdo3o7
- Audio-Export transkribierter Texte als versandfreundliche Datei
  (OGG/Opus bzw. MP3) fuer Sprachnachrichten (WhatsApp o.Ae.)
- Leichter Launch-Smoke-Test, damit CI den realen App-Start bestaetigt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHu5E1iQWEa6LViTcdo3o7

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c24bd669b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/config.py
self._data["tts_openai_model"] = DEFAULTS["tts_openai_model"]
if self._data.get("tts_openai_voice") not in VALID_OPENAI_TTS_VOICES:
self._data["tts_openai_voice"] = DEFAULTS["tts_openai_voice"]
self._data["tts_openai_consent"] = bool(self._data.get("tts_openai_consent", False))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat non-boolean TTS consent as false

When a hand-edited or migrated config contains a string such as "false" or "no", bool(...) converts it to True, so _ensure_openai_consent() will skip the privacy dialog and Cloud-TTS can send text to OpenAI without an explicit opt-in. Please only preserve the consent flag when the stored value is the literal JSON boolean true and reset all other types to False.

Useful? React with 👍 / 👎.

Comment thread app/tts_window.py
Comment on lines +692 to +695
def _on_cloud_finished(self, wav_path: str) -> None:
self._cleanup_cloud_state()
self._status_label.setText("Wiedergabe…")
program, args = _playback_command(wav_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore stale cloud completions after Stop

If the user clicks Stop just after the worker emits finished but before this queued slot runs, _detach_cloud_thread() has already cleared the active cloud state, yet this handler still starts playback of the generated WAV. That makes Stop unreliable at the end of cloud synthesis; guard this slot against completions from a cancelled/non-current worker before switching to Wiedergabe….

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant