servidor local Python,

I tant. La manera més senzilla és fer que el navegador enviï el text a un servidor local Python, i aquest executi Docker/Piper. Així no cal donar al navegador accés directe al terminal.

1. Servidor Python

Crea servidor.py: servidor.py

from http.server import BaseHTTPRequestHandler, HTTPServer
import subprocess
import json

HOST = "127.0.0.1"
PORT = 8765

class Handler(BaseHTTPRequestHandler):

    def do_POST(self):
        if self.path != "/parlar":
            self.send_error(404)
            return

        try:
            length = int(self.headers.get("Content-Length", 0))
            data = json.loads(self.rfile.read(length))
            text = data.get("text", "").strip()

            if not text:
                self.send_error(400, "Text buit")
                return

            # Executem Piper dins del contenidor.
            # El text entra per stdin, evitant problemes amb cometes.
            cmd = [
                "docker", "exec", "-i", "piper",
                "piper",
                "--model", "/config/ca_ES-upc_ona-medium.onnx",
                "--output_file", "-"
            ]

            piper = subprocess.Popen(
                cmd,
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
            )

            audio, error = piper.communicate(
                text.encode("utf-8"),
                timeout=60
            )

            if piper.returncode != 0:
                raise RuntimeError(error.decode("utf-8", errors="replace"))

            # Reproduïm el PCM generat per Piper.
            subprocess.run(
                ["aplay", "-f", "S16_LE", "-r", "22050", "-c", "1"],
                input=audio,
                check=True
            )

            response = {"ok": True}

            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            self.wfile.write(json.dumps(response).encode())

        except Exception as e:
            print("ERROR:", e)

            self.send_response(500)
            self.send_header("Content-Type", "application/json")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            self.wfile.write(
                json.dumps({"ok": False, "error": str(e)}).encode()
            )

    def do_OPTIONS(self):
        self.send_response(204)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()


print(f"Ona preparada a http://{HOST}:{PORT}")
HTTPServer((HOST, PORT), Handler).serve_forever()

2. La pàgina web

Crea ona.html: ona.html

<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Ona · UPC</title>

    <style>
        body {
            margin: 0;
            background: #111827;
            color: white;
            font-family: system-ui, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
        }

        .caixa {
            width: min(600px, 90%);
            background: #1f2937;
            padding: 30px;
            border-radius: 20px;
            box-shadow: 0 15px 40px #0008;
        }

        h1 {
            margin-top: 0;
        }

        textarea {
            width: 100%;
            height: 140px;
            box-sizing: border-box;
            resize: vertical;
            padding: 15px;
            border-radius: 12px;
            border: 1px solid #4b5563;
            background: #111827;
            color: white;
            font-size: 18px;
        }

        button {
            margin-top: 15px;
            width: 100%;
            padding: 16px;
            border: 0;
            border-radius: 12px;
            background: #2563eb;
            color: white;
            font-size: 18px;
            cursor: pointer;
        }

        button:hover {
            background: #1d4ed8;
        }

        button:disabled {
            background: #4b5563;
            cursor: wait;
        }

        #estat {
            margin-top: 15px;
            color: #9ca3af;
        }
    </style>
</head>

<body>

<div class="caixa">

    <h1>🎙️ Ona</h1>

    <p>Escriu el que vols que digui:</p>

    <textarea id="text">Hola, sóc l'Ona, la meva veu s'ha desenvolupat a la UPC de Barcelona.</textarea>

    <button id="parlar">
        🔊 Parla
    </button>

    <div id="estat">Preparada</div>

</div>

<script>
const text = document.getElementById("text");
const boto = document.getElementById("parlar");
const estat = document.getElementById("estat");

async function parlar() {

    const frase = text.value.trim();

    if (!frase) {
        estat.textContent = "Escriu alguna cosa.";
        return;
    }

    boto.disabled = true;
    estat.textContent = "🗣️ L'Ona està parlant...";

    try {

        const resposta = await fetch("http://127.0.0.1:8765/parlar", {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify({
                text: frase
            })
        });

        const resultat = await resposta.json();

        if (!resultat.ok) {
            throw new Error(resultat.error || "Error desconegut");
        }

        estat.textContent = "✅ Fet";

    } catch (error) {

        console.error(error);

        estat.textContent =
            "❌ No s'ha pogut contactar amb el servidor: " +
            error.message;

    } finally {

        boto.disabled = false;
    }
}

boto.addEventListener("click", parlar);

text.addEventListener("keydown", function(e) {
    if (e.ctrlKey && e.key === "Enter") {
        parlar();
    }
});
</script>

</body>
</html>
:::

## 3. Posar-ho en marxa

Al Deepin, obre un terminal a la carpeta dels dos fitxers:

```bash
python3 servidor.py

Hauries de veure:

Ona preparada a http://127.0.0.1:8765

Després obre ona.html amb Firefox o Chromium.

Escrius:

Bon dia! Sóc l’Ona i avui us acompanyaré.

i prems 🔊 Parla.

El recorregut serà:

ona.html
   ↓
http://127.0.0.1:8765/parlar
   ↓
docker exec -i piper piper ...
   ↓
ca_ES-upc_ona-medium.onnx
   ↓
PCM 22050 Hz
   ↓
aplay
   ↓
🔊 altaveus

Una diferència important respecte al teu comandament

Tu tenies:

docker exec piper bash -c "echo \"Hola...\" | piper ..."

Jo he fet servir:

docker exec -i piper piper ...

i envio el text per stdin. És millor perquè frases amb apòstrofs, cometes, accents, signes d’interrogació, etc. no trenquen el comandament.

Si vols, el següent pas pot ser fer-te una versió molt més bonica, tipus consola d’assistent, amb botó 🎙️, historial de converses, indicador que l’Ona està parlant i resposta automàtica, mantenint Piper dins del teu Docker.

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *

Aquest lloc utilitza Akismet per reduir els comentaris brossa. Apreneu com es processen les dades dels comentaris.