Implement Stage 1: command generator, CLI, TTS and LLM clients
- lexicon.py: exact forms from docs/LEXICON.md (Polish diacritics preserved)
- generator.py: Clause/Command dataclasses, render(), random_command(level 1-3)
- llm.py: OpenAI-compatible client via urllib, JSON-only contract, fallback to random
- tts.py: Piper via subprocess, defensive (prints [TTS off: reason] on any failure)
- cli.py: argparse interface per SPEC §9
- tests/test_generator.py: 23 tests covering all genders, locations, level-3 joining,
capitalisation; all pass
DoD verified: pytest 23/23, python -m ipin_vr --level 3 --count 6 --no-audio OK.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:03:50 +02:00
|
|
|
import os
|
2026-06-10 14:01:30 +02:00
|
|
|
import shutil
|
Implement Stage 1: command generator, CLI, TTS and LLM clients
- lexicon.py: exact forms from docs/LEXICON.md (Polish diacritics preserved)
- generator.py: Clause/Command dataclasses, render(), random_command(level 1-3)
- llm.py: OpenAI-compatible client via urllib, JSON-only contract, fallback to random
- tts.py: Piper via subprocess, defensive (prints [TTS off: reason] on any failure)
- cli.py: argparse interface per SPEC §9
- tests/test_generator.py: 23 tests covering all genders, locations, level-3 joining,
capitalisation; all pass
DoD verified: pytest 23/23, python -m ipin_vr --level 3 --count 6 --no-audio OK.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:03:50 +02:00
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
|
2026-06-10 14:01:30 +02:00
|
|
|
_LINUX_PLAYERS = ["pw-play", "paplay", "aplay"]
|
|
|
|
|
|
Implement Stage 1: command generator, CLI, TTS and LLM clients
- lexicon.py: exact forms from docs/LEXICON.md (Polish diacritics preserved)
- generator.py: Clause/Command dataclasses, render(), random_command(level 1-3)
- llm.py: OpenAI-compatible client via urllib, JSON-only contract, fallback to random
- tts.py: Piper via subprocess, defensive (prints [TTS off: reason] on any failure)
- cli.py: argparse interface per SPEC §9
- tests/test_generator.py: 23 tests covering all genders, locations, level-3 joining,
capitalisation; all pass
DoD verified: pytest 23/23, python -m ipin_vr --level 3 --count 6 --no-audio OK.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:03:50 +02:00
|
|
|
|
|
|
|
|
def speak(text: str, voice_path: str) -> None:
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tf:
|
|
|
|
|
wav_path = tf.name
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
proc = subprocess.run(
|
|
|
|
|
["piper", "--model", voice_path, "--output_file", wav_path],
|
|
|
|
|
input=text.encode(),
|
|
|
|
|
capture_output=True,
|
|
|
|
|
timeout=15,
|
|
|
|
|
)
|
|
|
|
|
if proc.returncode != 0:
|
|
|
|
|
stderr = proc.stderr.decode(errors="replace").strip()
|
|
|
|
|
print(f"[TTS off: piper błąd {proc.returncode}: {stderr}]")
|
|
|
|
|
return
|
|
|
|
|
_play(wav_path)
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
print("[TTS off: piper nie znaleziony — zainstaluj piper-tts]")
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
print("[TTS off: piper timeout]")
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print(f"[TTS off: {exc}]")
|
|
|
|
|
finally:
|
|
|
|
|
try:
|
|
|
|
|
os.unlink(wav_path)
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _play(wav_path: str) -> None:
|
|
|
|
|
if sys.platform.startswith("linux"):
|
2026-06-10 14:01:30 +02:00
|
|
|
player = next((p for p in _LINUX_PLAYERS if shutil.which(p)), None)
|
|
|
|
|
if player is None:
|
|
|
|
|
print("[TTS: brak odtwarzacza audio]")
|
|
|
|
|
return
|
|
|
|
|
_run_player(player, wav_path)
|
Implement Stage 1: command generator, CLI, TTS and LLM clients
- lexicon.py: exact forms from docs/LEXICON.md (Polish diacritics preserved)
- generator.py: Clause/Command dataclasses, render(), random_command(level 1-3)
- llm.py: OpenAI-compatible client via urllib, JSON-only contract, fallback to random
- tts.py: Piper via subprocess, defensive (prints [TTS off: reason] on any failure)
- cli.py: argparse interface per SPEC §9
- tests/test_generator.py: 23 tests covering all genders, locations, level-3 joining,
capitalisation; all pass
DoD verified: pytest 23/23, python -m ipin_vr --level 3 --count 6 --no-audio OK.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:03:50 +02:00
|
|
|
elif sys.platform == "darwin":
|
|
|
|
|
_run_player("afplay", wav_path)
|
|
|
|
|
elif sys.platform == "win32":
|
|
|
|
|
try:
|
|
|
|
|
import winsound
|
|
|
|
|
winsound.PlaySound(wav_path, winsound.SND_FILENAME)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print(f"[TTS off: winsound: {exc}]")
|
|
|
|
|
else:
|
|
|
|
|
print(f"[TTS off: nieobsługiwana platforma {sys.platform}]")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_player(player: str, wav_path: str) -> None:
|
|
|
|
|
try:
|
2026-06-10 14:01:30 +02:00
|
|
|
proc = subprocess.run([player, wav_path], capture_output=True, timeout=30)
|
|
|
|
|
if proc.returncode != 0:
|
|
|
|
|
stderr = proc.stderr.decode(errors="replace").strip()
|
|
|
|
|
print(f"[TTS: {player} błąd {proc.returncode}: {stderr}]")
|
Implement Stage 1: command generator, CLI, TTS and LLM clients
- lexicon.py: exact forms from docs/LEXICON.md (Polish diacritics preserved)
- generator.py: Clause/Command dataclasses, render(), random_command(level 1-3)
- llm.py: OpenAI-compatible client via urllib, JSON-only contract, fallback to random
- tts.py: Piper via subprocess, defensive (prints [TTS off: reason] on any failure)
- cli.py: argparse interface per SPEC §9
- tests/test_generator.py: 23 tests covering all genders, locations, level-3 joining,
capitalisation; all pass
DoD verified: pytest 23/23, python -m ipin_vr --level 3 --count 6 --no-audio OK.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:03:50 +02:00
|
|
|
except FileNotFoundError:
|
|
|
|
|
print(f"[TTS off: {player} nie znaleziony]")
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print(f"[TTS off: {exc}]")
|