{"id":19194,"date":"2026-09-20T19:02:32","date_gmt":"2026-09-20T17:02:32","guid":{"rendered":"https:\/\/www.beseit.net\/?p=19194"},"modified":"2026-09-20T19:05:26","modified_gmt":"2026-09-20T17:05:26","slug":"19194","status":"publish","type":"post","link":"https:\/\/www.beseit.net\/?p=19194","title":{"rendered":"Crear contenidor entrada text de html-sortida text a Peper"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Aqu\u00ed tens l&#8217;entrada de blog completa, llesta per publicar (en Markdown):<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">---\ntitle: \"Text del navegador a veu amb Piper TTS en Docker\"\ndate: \"2026-09-20\"\ntags: [\"docker\", \"piper\", \"tts\", \"python\", \"flask\"]\n---\n\n# Text del navegador a veu amb Piper TTS en Docker\n\nVols escriure text en una p\u00e0gina web i que es converteixi en \u00e0udio (WAV) amb una veu sint\u00e8tica? En aquest post et mostro com muntar un **contenidor Docker** que fa exactament aix\u00f2: un HTML envia el text, i el contenidor el passa al **Piper** (motor de text-a-veu) per retornar-te el fitxer WAV directament al navegador.\n\n## Arquitectura\n\nEl flux \u00e9s senzill:\n\n1. L'usuari escriu text a un formulari HTML.\n2. JavaScript envia el text a una API (Flask) mitjan\u00e7ant `POST \/api\/tts`.\n3. Flask executa `piper` dins del contenidor amb el text rebut.\n4. Piper genera un fitxer `.wav`.\n5. L'\u00e0udio es retorna al navegador i es reprodueix autom\u00e0ticament.\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Navegador (HTML) \u2500\u2500POST text\u2500\u2500\u25b6 Flask API \u2500\u2500\u25b6 Piper CLI \u2500\u2500\u25b6 WAV \u2500\u2500\u25b6 Navegador<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\n## Requisits\n\n- Docker i Docker Compose instal\u00b7lats.\n- Connexi\u00f3 a internet per baixar les imatges i el model de veu.\n\n## Estructura de fitxers\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">piper-tts\/<br>\u251c\u2500\u2500 app.py<br>\u251c\u2500\u2500 Dockerfile<br>\u251c\u2500\u2500 docker-compose.yml<br>\u2514\u2500\u2500 templates\/<br>\u2514\u2500\u2500 index.html<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\n## Pas 1: l'API amb Flask (`app.py`)\n\nL'aplicaci\u00f3 exposa la p\u00e0gina principal i l'endpoint `\/api\/tts`, que rep el text, l'executa amb Piper i retorna el WAV:\n\n```python\nfrom flask import Flask, request, render_template, send_file\nimport subprocess\n\napp = Flask(__name__)\n\nMODEL = \"\/models\/zh_CN-huayan-medium.onnx\"\nOUTPUT = \"\/app\/output\/resultat.wav\"\n\n@app.route(\"\/\")\ndef index():\n    return render_template(\"index.html\")\n\n@app.route(\"\/api\/tts\", methods=[\"POST\"])\ndef tts():\n    data = request.get_json()\n    text = data.get(\"text\", \"\")\n    if not text:\n        return {\"error\": \"text buit\"}, 400\n\n    subprocess.run(\n        [\"piper\", \"--model\", MODEL, \"--output_file\", OUTPUT],\n        input=text.encode(\"utf-8\")\n    )\n    return send_file(OUTPUT, mimetype=\"audio\/wav\")\n\nif __name__ == \"__main__\":\n    app.run(host=\"0.0.0.0\", port=5000)<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Pas 2: la p\u00e0gina HTML (<code>templates\/index.html<\/code>)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un textarea per escriure, un bot\u00f3 per sintetitzar i un reproductor d&#8217;\u00e0udio:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;!DOCTYPE html>\n&lt;html lang=\"ca\">\n&lt;head>\n    &lt;meta charset=\"UTF-8\">\n    &lt;title>Piper TTS&lt;\/title>\n&lt;\/head>\n&lt;body>\n    &lt;h1>Piper TTS \u2014 text a veu&lt;\/h1>\n    &lt;textarea id=\"text\" rows=\"4\" cols=\"60\">Hola, benvingut al servei de veu!&lt;\/textarea>\n    &lt;br>&lt;br>\n    &lt;button onclick=\"sintetitza()\">Sintetitza&lt;\/button>\n    &lt;br>&lt;br>\n    &lt;audio id=\"audio\" controls style=\"display:none\">&lt;\/audio>\n\n    &lt;script>\n        async function sintetitza() {\n            const text = document.getElementById(\"text\").value;\n            const res = await fetch(\"\/api\/tts\", {\n                method: \"POST\",\n                headers: { \"Content-Type\": \"application\/json\" },\n                body: JSON.stringify({ text: text })\n            });\n            const blob = await res.blob();\n            const url = URL.createObjectURL(blob);\n            const audio = document.getElementById(\"audio\");\n            audio.src = url;\n            audio.style.display = \"block\";\n            audio.play();\n        }\n    &lt;\/script>\n&lt;\/body>\n&lt;\/html><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Pas 3: el <code>Dockerfile<\/code><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Instal\u00b7la Flask i Piper, baixa el model de veu i defineix l&#8217;arrencada:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">FROM python:3.11-slim\n\n# Instal\u00b7la depend\u00e8ncies\nRUN apt-get update &amp;&amp; apt-get install -y wget espeak-ng &amp;&amp; \\\n    pip install flask piper-tts\n\n# Baixa el model de veu (exemple: xin\u00e8s huayan; canvia'l pel que vulguis)\nRUN mkdir -p \/models &amp;&amp; cd \/models &amp;&amp; \\\n    wget https:\/\/huggingface.co\/rhasspy\/piper-voices\/resolve\/main\/zh\/zh_CN\/huayan\/medium\/zh_CN-huayan-medium.onnx &amp;&amp; \\\n    wget https:\/\/huggingface.co\/rhasspy\/piper-voices\/resolve\/main\/zh\/zh_CN\/huayan\/medium\/zh_CN-huayan-medium.onnx.json\n\nWORKDIR \/app\nCOPY app.py .\nCOPY templates .\/templates\nRUN mkdir -p output\n\nEXPOSE 5000\nCMD [\"python\", \"app.py\"]<\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\ud83d\udca1 Per a veus en catal\u00e0 o castell\u00e0, consulta els models a <a href=\"https:\/\/huggingface.co\/rhasspy\/piper-voices\">huggingface.co\/rhasspy\/piper-voices<\/a> i canvia les URLs del Dockerfile.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Pas 4: <code>docker-compose.yml<\/code><\/h2>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">services:\n  piper-tts:\n    build: .\n    container_name: piper-tts\n    ports:\n      - \"5000:5000\"\n    volumes:\n      - .\/models:\/models\n      - .\/output:\/app\/output\n    restart: unless-stopped<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Pas 5: posada en marxa<\/h2>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">cd piper-tts\ndocker compose up -d --build<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Obre <strong><a href=\"http:\/\/localhost:5000\">http:\/\/localhost:5000<\/a><\/strong> al navegador i:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Escriu el text al quadre HTML.<\/li>\n\n\n\n<li>Prem <strong>Sintetitza<\/strong>.<\/li>\n\n\n\n<li>Rep el WAV i el reprodueix autom\u00e0ticament.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Prova r\u00e0pida amb curl<\/h2>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">curl -X POST http:\/\/localhost:5000\/api\/tts \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"text\": \"Hola, aix\u00f2 \u00e9s una prova\"}' \\\n  --output prova.wav<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Alternativa: imatge ja preparada<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Si no vols construir res, existeixen imatges com <code>loderunner84\/piper-rest<\/code> (Flask + Piper). Ara b\u00e9, muntar-ho tu mateix et dona control total sobre el model, els endpoints i la interf\u00edcie.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusi\u00f3<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Amb quatre fitxers tens un servei de text-a-veu complet dins d&#8217;un contenidor Docker: <strong>text des del navegador entra, WAV en surt<\/strong>. Pots ampliar-lo f\u00e0cilment: suport de m\u00faltiples idiomes, cua de peticions o desar historial d&#8217;\u00e0udios. Bon blog i bona sintetitzaci\u00f3! \ud83c\udf99\ufe0f<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\n---\n\nAquest \u00e9s el post complet en Markdown. Si vols, puc:\n\n1. **Adaptar-lo al teu blog** (canviar t\u00edtol, tags, to, afegir captures).\n2. **Traduir-lo al castell\u00e0** o a l'angl\u00e8s.\n3. **Crear la carpeta del post** perqu\u00e8 el desis directament (recorda que per crear fitxers necessito que em diguis com vols fer-ho, o t'obro l'editor).\n\nQu\u00e8 prefereixes?<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Aqu\u00ed tens l&#8217;entrada de blog completa, llesta per publicar (en Markdown): Navegador (HTML) \u2500\u2500POST text\u2500\u2500\u25b6 Flask API \u2500\u2500\u25b6 Piper CLI \u2500\u2500\u25b6 WAV \u2500\u2500\u25b6 Navegador piper-tts\/\u251c\u2500\u2500 app.py\u251c\u2500\u2500 Dockerfile\u251c\u2500\u2500 docker-compose.yml\u2514\u2500\u2500 templates\/\u2514\u2500\u2500 index.html Pas 2: la p\u00e0gina HTML (templates\/index.html) Un textarea per escriure, &hellip; <a href=\"https:\/\/www.beseit.net\/?p=19194\">Continua llegint <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":3167,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[207,1],"tags":[],"class_list":["post-19194","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-deepin-linux","category-bloc-de-notes"],"_links":{"self":[{"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/posts\/19194","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.beseit.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=19194"}],"version-history":[{"count":2,"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/posts\/19194\/revisions"}],"predecessor-version":[{"id":19197,"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/posts\/19194\/revisions\/19197"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/media\/3167"}],"wp:attachment":[{"href":"https:\/\/www.beseit.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=19194"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.beseit.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=19194"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.beseit.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=19194"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}