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.