from flask import Flask, request, jsonify, send_from_directory from piper import PiperVoice import os, uuid, wave app = Flask(__name__, static_folder="static") BASE = os.path.dirname(__file__) MODELS = os.path.join(BASE, "models") OUTPUT = os.path.join(BASE, "output") os.makedirs(OUTPUT, exist_ok=True) print("Loading Piper…") voice = PiperVoice.load( os.path.join(MODELS, "en_US-lessac-medium.onnx"), os.path.join(MODELS, "en_US-lessac-medium.onnx.json") ) print("Piper ready") @app.route("/") def index(): return send_from_directory("static", "index.html") @app.route("/speak", methods=["POST"]) def speak(): data = request.json text = data.get("text", "").strip() if not text: return jsonify({"error": "Empty text"}), 400 uid = str(uuid.uuid4()) wav_path = os.path.join(OUTPUT, f"{uid}.wav") with wave.open(wav_path, "wb") as wf: wf.setnchannels(1) wf.setsampwidth(2) # PCM16 wf.setframerate(22050) audio_stream = voice.synthesize(text) for chunk in audio_stream: # CASE 1: chunk IS bytes if isinstance(chunk, (bytes, bytearray)): wf.writeframes(chunk) # CASE 2: chunk is numpy array elif hasattr(chunk, "dtype"): wf.writeframes(chunk.astype("int16").tobytes()) # CASE 3: chunk is object → extract safely elif hasattr(chunk, "__dict__"): for v in chunk.__dict__.values(): if isinstance(v, (bytes, bytearray)): wf.writeframes(v) break if hasattr(v, "dtype"): wf.writeframes(v.astype("int16").tobytes()) break else: raise RuntimeError(f"Unsupported AudioChunk: {chunk}") else: raise RuntimeError(f"Unknown audio chunk type: {type(chunk)}") return jsonify({ "audio": f"/audio/{uid}.wav" }) @app.route("/audio/") def audio(name): return send_from_directory(OUTPUT, name) @app.route("/") def static_files(path): return send_from_directory("static", path) if __name__ == "__main__": app.run(host="0.0.0.0", port=7860)