EINSATZ-FELD (User-Wunsch): Laufzeitwert MANUAL_MARGIN in core/config.py (set/get_manual_margin, Muster wie set_margin_buffer). 0 = automatisch wie bisher (margin_buffer_pct % der freien Margin), > 0 = Position wird auf genau diesen Margin-Einsatz gerechnet. - calc_lots setzt es um und DECKELT auf freie Margin x Buffer; ein zu hoher Wunschwert wird begrenzt und geloggt statt an den Broker durchgereicht. Reicht der Betrag nicht fuers Mindestlot -> 0.0 + Warnung, Trade sauber abgelehnt. - Vorrang vor dem Risiko-Modus in trader._send_locked: eine ausdrueckliche Groessenvorgabe schlaegt die Rechenregel. - Wirkt auf ALLE neuen Positionen, auch die autonomen. - UI-Feld "Einsatz" in der ORDER-Leiste (nicht in der Trade-Leiste: die ist nur bei offener Position sichtbar, der Einsatz muss vorher einstellbar sein), bernstein umrandet solange gesetzt. Enter blurrt nur, change sendet einmal. - POST /api/manualmargin, Snapshot manual_margin, neustart-fest ueber runtime_state.json. Ende-zu-Ende getestet (250 -> Snapshot -> 0 -> persistiert). - Lot-Logik isoliert geprueft: 200 EUR -> 0,58 Lots; 5000 EUR -> gedeckelt. SPRACHAUSGABE: Piper lokal (tools/speak.py). Windows-TTS funktioniert zwar, aber es ist KEINE deutsche Stimme installiert - weder SAPI5 noch OneCore (nur David/Zira/Mark, en-US); deutscher Text kaeme mit englischer Aussprache. Add-WindowsCapability scheitert ohne Adminrechte. Piper gewaehlt: laeuft lokal (passt zum Rest - WireGuard-only, Secrets in der ini, keine Cloud), kostet nichts, aus PowerShell/Python aufrufbar. Stimme thorsten-medium (de_DE), Real-Time-Faktor 0,08 (4,3 s Audio in 0,36 s). Das ZIP entpackt eine Ebene tiefer als erwartet (tools/piper/piper/piper.exe) - speak.py SUCHT Binary und Modell statt Pfade fest zu verdrahten. Der erste Entwurf scheiterte daran; der Fail-safe meldete die Pfade sauber statt stumm zu bleiben. Drei Modi getestet (Argument, Pipe, --wav). tools/piper/ ist gitignored (Binaries + 60-MB-Modell), speak.py versioniert. v=136. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
98 lines
3.6 KiB
Python
98 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""speak.py — deutsche Sprachausgabe über Piper (lokal, offline, kostenlos).
|
|
|
|
Warum Piper und nicht die Windows-Sprachausgabe: auf diesem Rechner ist KEINE
|
|
deutsche SAPI-/OneCore-Stimme installiert (nur David/Zira/Mark, alle en-US) —
|
|
deutscher Text käme mit englischer Aussprache heraus. Piper bringt eine eigene
|
|
deutsche Stimme mit, läuft komplett lokal (passt zum Rest des Setups: WireGuard-
|
|
only, Secrets in der ini, keine Cloud-Abhängigkeit) und kostet nichts.
|
|
|
|
Aufruf:
|
|
python tools/speak.py "Der Text"
|
|
echo "Der Text" | python tools/speak.py
|
|
python tools/speak.py --datei meldung.txt
|
|
|
|
Rückgabe 0 = gesprochen, 1 = Fehler (fehlt Piper/Modell → Hinweis auf stderr).
|
|
Fail-safe gedacht: ein Aufrufer im Bot soll bei fehlender Sprachausgabe
|
|
weiterlaufen, nicht abstürzen.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _finde(name: str, *starts: str) -> str:
|
|
"""Erste Datei `name` unterhalb der Startpfade. ⚠ Das Piper-ZIP entpackt eine
|
|
Ebene tiefer als erwartet (`tools/piper/piper/piper.exe`) — fest verdrahtete
|
|
Pfade brechen deshalb, sobald jemand anders entpackt oder das Layout sich mit
|
|
einer neuen Version ändert. Suchen ist hier robuster als raten."""
|
|
for s in starts:
|
|
if os.path.isfile(s):
|
|
return s
|
|
for wurzel, _dirs, dateien in os.walk(s):
|
|
if name in dateien:
|
|
return os.path.join(wurzel, name)
|
|
return ""
|
|
|
|
|
|
PIPER = _finde("piper.exe", os.path.join(HERE, "piper"))
|
|
MODELL = _finde("de_DE-thorsten-medium.onnx", os.path.join(HERE, "piper"))
|
|
|
|
|
|
def sprich(text: str, behalten: str | None = None) -> int:
|
|
text = (text or "").strip()
|
|
if not text:
|
|
return 0
|
|
if not PIPER or not MODELL:
|
|
print(f"Piper oder Stimme nicht gefunden unter {os.path.join(HERE, 'piper')}\n"
|
|
f" piper.exe: {PIPER or 'FEHLT'}\n Stimme: {MODELL or 'FEHLT'}",
|
|
file=sys.stderr)
|
|
return 1
|
|
wav = behalten or os.path.join(tempfile.gettempdir(), "speak_out.wav")
|
|
try:
|
|
subprocess.run([PIPER, "--model", MODELL, "--output_file", wav],
|
|
input=text.encode("utf-8"),
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
check=True, timeout=120)
|
|
except Exception as e:
|
|
print(f"Piper-Aufruf fehlgeschlagen: {e}", file=sys.stderr)
|
|
return 1
|
|
if behalten: # nur erzeugen, nicht abspielen
|
|
return 0
|
|
# Abspielen über den .NET-SoundPlayer — kein zusätzliches Paket nötig.
|
|
ps = (f"(New-Object Media.SoundPlayer '{wav}').PlaySync()")
|
|
try:
|
|
subprocess.run(["powershell", "-NoProfile", "-Command", ps],
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
check=True, timeout=300)
|
|
except Exception as e:
|
|
print(f"Wiedergabe fehlgeschlagen: {e}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def main() -> None:
|
|
p = argparse.ArgumentParser(description="Deutsche Sprachausgabe via Piper")
|
|
p.add_argument("text", nargs="*", help="zu sprechender Text")
|
|
p.add_argument("--datei", help="Text aus Datei lesen")
|
|
p.add_argument("--wav", help="nur WAV erzeugen, nicht abspielen")
|
|
a = p.parse_args()
|
|
if a.datei:
|
|
with open(a.datei, encoding="utf-8") as f:
|
|
txt = f.read()
|
|
elif a.text:
|
|
txt = " ".join(a.text)
|
|
else:
|
|
txt = sys.stdin.read() # Pipe-Betrieb
|
|
sys.exit(sprich(txt, a.wav))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|