Replicar Dropbox

No. No cal que la carpeta del NAS estigui dins del directori web.

Si el que vols és tenir una carpeta del NAS muntada a Deepin i replicada/sincronitzada com si fos Dropbox, el més important és el tipus d’accés que utilitzarem, no pas que sigui una carpeta web.

Per exemple, al NAS pots tenir:

/volume2/
├── web/
│   └── polidic/
│
├── Documents/
│   └── Miquel/
│
├── Sincronitzacio/
│   └── Deepin/
│
└── altres/

I podríem sincronitzar, per exemple:

NAS
192.168.1.41
/volume2/Sincronitzacio/Deepin
             ↕
Deepin
/home/Miquel/NAS/Deepin

Tens dues coses diferents

1. Muntar la carpeta

Deepin veu directament la carpeta del NAS:

/home/Miquel/NAS/Deepin

però els fitxers físicament continuen estant al NAS.

2. Replicar/sincronitzar-la

Aquí sí que tindries una còpia local:

NAS:    /volume2/Sincronitzacio/Deepin
              ↕ sincronització
PC:     /home/Miquel/Sincronitzacio/Deepin

Això és més semblant a Dropbox: els fitxers existeixen també al PC i al NAS, i el programa manté les dues còpies sincronitzades.

En el teu cas

Com que tens el Synology 192.168.1.41, jo separaria clarament:

/volume2/web/

per a les aplicacions web de Polidic, PHP, etc.

i crearia una carpeta compartida específica, per exemple:

/volume2/Sincronitzacio/

per a les dades que vulguis replicar amb Deepin.

Això té un avantatge important: no barregem les dades de les aplicacions web amb les dades personals que volem sincronitzar.

Si el que busques és exactament una experiència «Dropbox però NAS propi», podem muntar-ho perquè, per exemple:

/home/Miquel/Dropbox-NAS/

es mantingui sincronitzat automàticament amb una carpeta compartida del Synology, incloent canvis en els dos sentits.


Sí. I de fet, si la major part de les vegades només necessites una rèplica NAS → Deepin, jo faria una solució més senzilla que una sincronització bidireccional tipus Dropbox.

La idea seria:

                 RÈPLICA
┌──────────────────────────────┐
│        Synology NAS          │
│        192.168.1.41          │
│                              │
│  /volume2/Dropbox/            │
└──────────────┬───────────────┘
               │
               │  SMB / rsync
               ▼
┌──────────────────────────────┐
│          Deepin              │
│                              │
│ /home/Miquel/Dropbox/        │
└──────────────────────────────┘

La carpeta del NAS no necessita estar dins /volume2/web.

1. Crear la carpeta al Synology

Al NAS crearia una carpeta compartida específica:

Dropbox

que físicament podria ser:

/volume2/Dropbox/

Per exemple:

/volume2/Dropbox/
├── Documents/
├── Llibres/
├── Fotos/
├── Projectes/
└── altres/

A Synology DSM li dones permisos al teu usuari.


2. Compartir-la per SMB

Al Synology:

Panell de control → Serveis de fitxers → SMB

actives SMB.

Així Deepin podrà accedir a:

//192.168.1.41/Dropbox

No cal que sigui una carpeta web.


3. Crear la carpeta local a Deepin

Al PC:

mkdir -p /home/Miquel/Dropbox

Aquí tindríem:

NAS
/volume2/Dropbox/
       │
       │ rèplica
       ▼
Deepin
/home/Miquel/Dropbox/

Però encara no sincronitzaria res.

Primer provaria que Deepin pot llegir correctament el NAS.


4. Per a una rèplica, jo utilitzaria rsync

Això és molt interessant perquè rsync només copia els fitxers nous o modificats.

Per exemple:

rsync -av --delete /origen/ /destinacio/

El comportament seria:

NAS                         Deepin

fitxer1.pdf       ───────►  fitxer1.pdf
foto.jpg          ───────►  foto.jpg
document.odt      ───────►  document.odt

Si demà només canvies document.odt:

NAS                         Deepin

document.odt      ───────►  document.odt
                    només aquest

No torna a copiar tota la carpeta.


5. Una precaució molt important: --delete

Aquesta opció:

--delete

fa que la rèplica sigui exacta.

Per exemple, si al NAS elimines:

NAS
/Dropbox/foto.jpg

la següent rèplica també eliminarà:

Deepin
/home/Miquel/Dropbox/foto.jpg

Això és molt útil per tenir una còpia mirall, però jo no la posaria al principi.

Primer provaria:

rsync -av

sense --delete.

Quan estiguem segurs que tot funciona, ja decidim si volem rèplica estricta.


6. Una qüestió important: NAS → Deepin o bidireccional?

Aquí hi ha una diferència fonamental.

Rèplica NAS → Deepin

NAS
 ↓
Deepin

És la més segura i senzilla.

Si treballes principalment amb els fitxers del NAS, Deepin simplement manté una còpia local.

Sincronització bidireccional

       ┌───────┐
       │       ▼
NAS ◄────────► Deepin

Si modifiques un fitxer al Deepin, el canvi també puja al NAS.

Això ja és més semblant a Dropbox i requereix controlar conflictes, fitxers eliminats, versions, etc.


7. El que jo faria en el teu cas

Començaria amb aquesta estructura:

Synology 192.168.1.41
│
└── /volume2/Dropbox/
       │
       ├── Documents/
       ├── Llibres/
       ├── Projectes/
       └── ...
                │
                │ SMB
                ▼
Deepin
│
└── /home/Miquel/Dropbox/

I faria tres fases:

Fase 1 — Prova manual

Connectar-nos al NAS i comprovar que podem llegir:

//192.168.1.41/Dropbox

Fase 2 — Primera rèplica

Fer una primera còpia:

NAS → Deepin

i comprovar que els fitxers coincideixen.

Fase 3 — Automatitzar

Podríem fer que Deepin executés automàticament, per exemple:

cada hora
     ↓
comprova NAS
     ↓
rsync
     ↓
actualitza /home/Miquel/Dropbox

o fins i tot fer-ho manualment amb un script:

replica_dropbox

I encara hi ha una alternativa molt interessant

Si el que vols és que sigui realment com Dropbox, amb:

  • sincronització automàtica,
  • canvis en els dos sentits,
  • funcionament en segon pla,
  • control de conflictes,

jo miraria Syncthing en lloc de fer-ho simplement amb rsync.

En canvi, si el que vols és «el NAS és el mestre i Deepin en té una còpia local actualitzada», rsync és probablement la solució més neta.

Jo començaria per rsync NAS → Deepin, perquè és molt fàcil de provar i no ens arrisquem a modificar accidentalment els originals del NAS.

⭐📘 Instal·lar dropbox a Deepin (linux) replicant/syncronitzant a /volume2/web/dropbox (xarxa NAS) com si fos una carpeta local

Objectiu

L’objectiu es instal·lar Dropbox com si fos una carpeta local. Això facilitar`l’accés sense consumir espai de memòria. Dropxox, la munta automàticament en cada reinici.


1) Instal·lar suport SMB/CIFS

(Deepin ja ho porta, però ho validem)

Codi

sudo apt install cifs-utils

2) Crear el punt de muntatge



sudo mkdir -p /mnt/dropbox

3) Crear credencials segures

Això evita posar la contrasenya al fstab.

Codi

sudo nano /etc/samba/dropbox.creds

Dins hi poses:

Codi

username=MIQUEL

password=LA_TEVA_CONTRASENYA

Protegeix el fitxer:

Codi

sudo chmod 600 /etc/samba/dropbox.creds

4) Afegir el muntatge permanent al fstab

Obre el fitxer:

Codi

sudo nano /etc/fstab

Afegeix aquesta línia (adaptada per a Dropbox):

Codi

# Carpeta compartida del Synology per a Dropbox

//192.168.1.41/web /mnt/dropbox cifs credentials=/etc/samba/dropbox.creds,uid=1000,gid=1000,file_mode=0777,dir_mode=0777,nobrl,cache=none,iocharset=utf8,vers=3.0,nofail,_netdev,x-systemd.automount 0 0


Recarrega systemd (imprescindible)

sudo systemctl daemon-reload

5) fer el muntatge i provar

Això fa que Deepin i el calibre (local) vegi calibre-library una carpeta local i que funcioni speech-dispatcher.

Codi

sudo mount -a

Si no surt cap error:

comprovar que la carpeta drpbox s’ha creat:

Aquesta carpeta de fet es una carpeta virtual que no ocupa espai del Deepin però visualitza tota la carpeta web del synology. Aquest no era em meu objectiu, però els muntajes només es poden fer amb carpetes compartides i dropbox penja de web/dropbox. Això se soluciona posant un enllaç just de dropbox a la barra lateral


.

afegim un fitge de prova.txt a

Deepin

comprovem que s’incronitza amb Synology

i amb mòbil

⭐📘 Instal·lar Calibre a Deepin (linux) usant /volume2/llibres_deepin (xarxa NAS) com si fos una carpeta local

🎯 Objectiu

L’objectiu es instal·lar calibre a Deepin amb una llibreria compartida com si fos una carpeta local. Això permet que Calibre pugui parlar amb Speech-Dispatcher i també no consumir espai de memòria. Calibre, la munta automàticament en cada reinici.


1) Instal·lar suport SMB/CIFS

(Deepin ja ho porta, però ho validem)

Codi

sudo apt install cifs-utils

2) Crear el punt de muntatge

Escull on vols que Deepin tingui la carpeta del Calibre. Recomanat:

Codi

sudo mkdir -p /mnt/calibre-library

3) Crear credencials segures

Això evita posar la contrasenya al fstab.

Codi

sudo nano /etc/samba/calibre.creds

Dins hi poses:

Codi

username=MIQUEL
password=LA_TEVA_CONTRASENYA

Protegeix el fitxer:

Codi

sudo chmod 600 /etc/samba/calibre.creds

4) Afegir el muntatge permanent al fstab

Obre el fitxer:

Codi

sudo nano /etc/fstab

Afegeix aquesta línia (adaptada per Calibre):

Codi

# Carpeta compartida del Synology per a Calibre
//192.168.1.41/llibres_deepin   /mnt/calibre-library   cifs   credentials=/etc/samba/calibre.creds,uid=1000,gid=1000,file_mode=0777,dir_mode=0777,nobrl,cache=none,iocharset=utf8,vers=3.0,nofail,_netdev,x-systemd.>

Explicació ràpida:

  • //192.168.1.41/llibres_deepin → carpeta compartida del Synology
  • /mnt/calibre-librey → punt de muntatge a Deepin
  • vers=3.0 → SMB3 (el que usa Synology)
  • nofail → Deepin arrenca encara que el NAS estigui apagat
  • uid/gid=1000 → Calibre pot llegir i escriure sense problemes
  • credentials=… → contrasenya segura
  • –

Recarrega systemd (imprescindible)

sudo systemctl daemon-reload

5) fer el muntatge i provar

Això fa que Deepin i el calibre (local) vegi calibre-library una carpeta local i que funcioni speech-dispatcher.

Codi

sudo mount -a

Si no surt cap error:

Codi

ls /mnt/calibre-library

Si hi veus els llibres → muntatge OK i persistent.

6) Configurar Calibre-library

A Calibre:

  1. Preferències → Avançat → Ubicació de la biblioteca
  2. Selecciona: /mnt/calibre-library
  3. Guarda i reinicia Calibre.

7) Validació final

Reinicia Deepin i comprova:

Codi

ls /mnt/calibre-library

Si hi surt la llibreria → ja tens Synology + Deepin + Calibre funcionant de manera automàtica.

Obrim calibre local: /opt/calibre/calibre

Per pasar a la llibreria de xarxa, ara muntada a /mnt/calibre-library:

choose i ja esta:

comprovem la veu:

Hi ha muntanyes d’idiomes a Deepin (linux)

⭐ 📚 Calibre (app taulell) amb la llibreria al Synology, des de Deepin

Guia completa: com obrir el Calibre des del terminal de Deepin (instal·lat des del taulell d’aplicacions)i com instal·lar el Calibre al Synology per tenir la biblioteca centralitzada al NAS.

Part 1 — Obrir el Calibre des del terminal de Deepin

El Calibre de Deepin s’instal·la com a aplicació Linglong (el format d’empaquetatge propi de Deepin). Per tant, no es llança amb una comanda normal, sinó amb ll-cli.

1. Comprovar que el Calibre està instal·lat

ll-cli list | grep -i calibre

Hauries de veure una línia com aquesta:

com.calibre-ebook.calibre   6.29.0.2   main   binary   calibre is a powerful and easy to use e-book manager.

2. Obrir el Calibre des del terminal

ll-cli run com.calibre-ebook.calibre

Si tens més d’una versió instal·lada i vols executar-ne una de concreta:

ll-cli run com.calibre-ebook.calibre/6.29.0.2

3. Muntar la llibreria del Synology

Si la teva biblioteca viu al NAS (com en el meu cas, a la carpeta llibres_deepin), cal muntar-la com a carpeta local perquè el Calibre hi pugui treballar.

Instal·lar les eines de muntatge SMB:

sudo apt install cifs-utils

Crear el fitxer de credencials (/etc/samba/calibre.creds):

username=EL_TEU_USUARI
password=LA_TEVA_CONTRASENYA

Protegir-lo:

sudo chmod 600 /etc/samba/calibre.creds

Afegir el muntatge permanent al /etc/fstab:

//192.168.1.41/llibres_deepin  /mnt/calibre-library  cifs  credentials=/etc/samba/calibre.creds,uid=1000,gid=1000,file_mode=0777,dir_mode=0777,nobrl,cache=none,iocharset=utf8,vers=3.0,nofail  0  0

💡 Les opcions nobrl i cache=none són clau per evitar bloquejos o corrupció de la base de dades SQLite del Calibre quan treballa sobre xarxa.

Muntar i comprovar:

sudo mkdir -p /mnt/calibre-library
sudo mount /mnt/calibre-library
ls /mnt/calibre-library

4. Apuntar el Calibre a la llibreria del NAS

Al Calibre: Canvia/Crea una llibreria → Fes servir una altra llibreria existent i navega fins a /mnt/calibre-library.

A partir d’aquí, el Calibre obre directament la llibreria del Synology, i el muntatge es fa sol a cada reinici gràcies al fstab.


Part 2 — Instal·lar el Calibre al Synology

Per tenir el Calibre funcionant al mateix NAS (com a servidor de contingut, accessible des de qualsevol dispositiu), la manera més neta és amb Docker.

Opció A — Docker (recomanada)

  1. Obre el Centre de paquets del DSM i instal·la Docker (o Container Manager a DSM 7.2+).
  2. Obre Docker → Registre → busca linuxserver/calibre → descarrega la imatge.
  3. A Contenidor → Crea → configura:
ParàmetreValor
Volum/config → carpeta de configuració al NAS
Volum/books → la teva llibreria (llibres_deepin)
Port8080:8080 (HTTP) i 8081:8081 (HTTPS)
VariablesPUID=1024, PGID=100, TZ=Europe/Madrid
  1. Inicia el contenidor i obre http://IP_DEL_SYNOLOGY:8080 des del navegador.

Per línia de comandes (SSH al NAS):

sudo docker run -d \
  --name=calibre \
  -e PUID=1024 -e PGID=100 -e TZ=Europe/Madrid \
  -p 8080:8080 -p 8081:8081 \
  -v /volume1/docker/calibre/config:/config \
  -v /volume1/llibres_deepin:/books \
  --restart unless-stopped \
  linuxserver/calibre

Opció B — Paquet SynoCommunity

  1. A DSM, afegeix el repositori de SynoCommunity (Centre de paquets → Configuració → Fonts de confiança).
  2. Cerca Calibre i instal·la’l.
  3. El servei quedarà actiu i accessible per xarxa.

Accedir-hi des del Deepin

Amb el servidor de contingut del Calibre actiu al Synology, des del Calibre local (Deepin) pots connectar-t’hi:

  • Connectar i compartir → Connecta’t al servidor de contingut → http://IP_DEL_SYNOLOGY:8080
  • O des del navegador de qualsevol dispositiu: http://IP_DEL_SYNOLOGY:8080 (interfície OPDS per a lectors com el Kindle o l’Android).

Resum de comandes útils:

ll-cli list | grep -i calibre        # veure la versió Linglong
ll-cli run com.calibre-ebook.calibre # obrir el Calibre
sudo mount /mnt/calibre-library       # muntar la llibreria del NAS

Si vols, et puc desar aquest post en un fitxer Markdown al teu ordinador (per exemple calibre-synology-deepin.md) perquè el tinguis llest per publicar o adaptar. T’ho faig? 😊

Aquí tens la nota preparada, llesta per enganxar al peu de l’entrada! 📝


📌 Nota d’observacions

a) La comanda calibre no arrenca res — Després d’instal·lar el Calibre natiu a /opt/calibre, la comanda calibre no funciona perquè l’instal·lador no va poder crear els enllaços a /usr/bin. Solució: crear l’enllaç manualment o usar la ruta completa amb la llibreria del Synology:

sudo ln -s /opt/calibre/calibre /usr/local/bin/calibre
/opt/calibre/calibre --with-library /mnt/calibre-library

b) El Calibre local també «recupera» la llibreria del Synology — Tant la versió Linglong com la nativa llegeixen la mateixa configuració (~/.config/calibre) i la llibreria viu al NAS (muntada a /mnt/calibre-library). Per això, amb qualsevol de les dues versions es veuen els mateixos llibres, sense duplicar res.

c) A Deepin (Unix) hi ha moltes més llengües que a Windows — El Calibre a Linux fa servir speech-dispatcher amb el motor espeak-ng, que suporta unes 100 llengües (català, castellà, èuscar, gallec…). A Windows, en canvi, només té les veus SAPI que tinguis instal·lades al sistema, que acostumen a ser poques.

d) El Calibre fa servir speech-dispatcher, no Python — Tot i que el missatge d’error pot fer dubtar, la síntesi de veu del Calibre passa per Qt (QTextToSpeech) → plugin speechd → speech-dispatcher → espeak-ng. Python és el llenguatge en què està escrit el programa, però no intervé en la veu. La confusió amb Firefox/Edge ve de Windows: allà tots dos navegadors fan servir les veus del sistema (SAPI), per això sonen igual; a Linux, Firefox no fa servir speech-dispatcher.


Si la vols amb una mica més d’estil (com una caixa destacada al final de l’article), només cal dir-m’ho i te la maqueto diferent. Gracies a tu! 😊

Entorn virtual source venv/bin/activate

Molt bona pregunta! T’ho explico pas a pas:

Què fa source venv/bin/activate?

Aquesta comanda té dues parts:

1. source (o el seu equivalent .)

És una ordre interna de la terminal (bash) que diu:

«Llegeix aquest fitxer i executa les ordres que hi ha dins, com si les hagués escrit jo mateix a la terminal.»

És a dir, en lloc d’obrir el fitxer com un programa independent, el sistema llegeix les instruccions i les aplica al teu entorn actual.

2. venv/bin/activate

És el fitxer que conté les instruccions per «activar» l’entorn virtual. Quan l’executes, fa diverses coses:

  • Modifica el PATH: Li diu al sistema «quan executis python o pip, fes servir els que hi ha dins de venv/bin/, no els del sistema global».
  • Canvia el prompt: Afegeix (venv) al principi de la línia d’ordres perquè sàpigues que ets dins l’entorn virtual.
  • Defineix variables d’entorn: Configura VIRTUAL_ENV perquè Python sàpiga que està treballant en un entorn aïllat.

Per què és necessari?

Imagina que tens dos projectes Python:

  • Projecte A necessita la versió 1.0 d’una llibreria
  • Projecte B necessita la versió 2.0 de la mateixa llibreria

Si instal·lessis tot al sistema global, hi hauria conflictes. L’entorn virtual crea un «espai aïllat» per a cada projecte, on pots tenir les versions que necessitis sense que es barregin.

Quan fas source venv/bin/activate, li estàs dient al sistema:

«A partir d’ara, tots els paquets de Python que instal·li o utilitzi, fes-los servir d’aquesta carpeta venv, no del sistema global.»


Com crear-lo?

Comencem de zero:

# 1. Esborra l'entorn virtual existent
rm -rf venv

# 2. Verifica que Python3 està disponible
python3 --version

# 3. Crea un nou entorn virtual
python3 -m venv venv

# 4. Activa l'entorn virtual
source venv/bin/activate

# 5. Verifica que està activat (hauries de veure (venv) al prompt)
which python
which pip

Recorda per a futures sessions SSH

Cada vegada que et connectis al Synology per SSH i vulguis treballar amb el projecte, hauràs d’activar l’entorn:

cd /volume2/web/polidic
source venv/bin/activate
cd /volume2/web/polidic
source venv/bin/activate

deactivate

Instal·lar els paquets que necessitis

pip freeze > requirements.txt

Així, si algun dia has de recrear l’entorn, només hauràs de fer:

pip install -r requirements.txt

Com ho desactivo?

Quan acabis de treballar, simplement escrius:

deactivate

I el teu prompt tornarà a la normalitat (sense el (venv) al davant).


Ara que ja tens l’entorn activat (veiem el (venv) al teu prompt), ja pots executar:

./provar_en.sh

I l’script farà servir les llibreries de Python que hi ha dins de venv, no les del sistema global.

hem de matar el port el procés del port 5040:

sudo netstat -tulnp | grep 5040

sudo kill 14313 (el procés proporcionat a dalt)

(venv) urqtejmi@Synology_sec:/volume1/web/python_server$ sudo kill 14313
(venv) urqtejmi@Synology_sec:/volume1/web/python_server$

Instal·la les llibreries necessàries:

pip install gtts flask

./provar_en.sh
ls -lh *.mp3
Idiomalang
Catalàca
Anglèsen
Espanyoles
Francèsfr
Italiàit
Alemanyde
Portuguèspt
Neerlandèsnl

Sí. Aquí hi ha un punt important: Python no s’instal·la dins del venv. El que fem és crear el venv utilitzant el Python 3.12 que acabes d’instal·lar amb SynoCommunity. Així, quan activis venv, el python3 que veuràs serà el 3.12.

Ara mateix tens:

(venv) urqtejmi@synology_vall:/volume2/web/polidic$
which python3
/volume2/web/polidic/venv/bin/python3

Primer, sense tocar res, comprovaria quin Python 3.12 ha instal·lat SynoCommunity:

which python3.12
python3.12 --version

Si surt Python 3.12.x

Jo faria una còpia del venv actual i en crearia un de nou amb Python 3.12:

cd /volume2/web/polidic

Desactiva el venv actual:

deactivate

Fes una còpia de seguretat:

mv venv venv_old

Ara crea el nou venv amb Python 3.12:

python3.12 -m venv venv

Activa’l:

source venv/bin/activate

I comprova:

which python3
python3 --version

Hauria de sortir aproximadament:

/volume2/web/polidic/venv/bin/python3
Python 3.12.x

🎯 Això és exactament el que busquem.

Després instal·larem les llibreries

Una vegada confirmem que funciona:

python3 -m pip --version

i:

python3 -m pip list

Llavors podrem instal·lar al nou entorn Python 3.12 les llibreries que necessites per al teu projecte polidic, per exemple Flask, gTTS, etc., sense barrejar-les amb el Python del sistema.

No eliminaria encara venv_old, perquè així conservem l’entorn anterior per si alguna llibreria del Polidic depèn de la versió antiga.

Fes ara només aquestes dues:

which python3.12
python3.12 --version

i passa’m el resultat. A partir d’aquí et dono les ordres exactes per al Synology.

app.py de TTSG ducumentada

Sí. Ara que ja funciona perfectament, val molt la pena documentar-lo perquè aquest app.py ja té diverses peces interessants: recepció del formulari, gTTS, generació de MP3, escapament HTML, reproducció automàtica, retorn automàtic i servei del fitxer.

Et deixo una versió documentada però sense modificar el funcionament. He procurat que els comentaris expliquin sobretot per què existeix cada part.

```python
# ============================================================
# SERVIDOR TTS AMB FLASK + gTTS
# ============================================================
#
# Funció:
#   1. Rep text des del formulari web.
#   2. Genera un fitxer MP3 amb Google Text-to-Speech (gTTS).
#   3. Mostra una pàgina amb el reproductor d'àudio.
#   4. Reprodueix l'àudio automàticament.
#   5. Quan l'àudio acaba, retorna automàticament al formulari.
#
# Adreça del servidor:
#   http://192.168.1.49:5040
#
# Formulari principal:
#   http://192.168.1.49/python_server/
#
# Endpoint que rep el text:
#   POST /processa
#
# Fitxer d'àudio:
#   /sortida.mp3
#
# ============================================================


# ------------------------------------------------------------
# IMPORTACIONS
# ------------------------------------------------------------

from flask import Flask, request, send_from_directory
from gtts import gTTS
import os
import html


# ------------------------------------------------------------
# CREACIÓ DE L'APLICACIÓ FLASK
# ------------------------------------------------------------

app = Flask(__name__)


# ============================================================
# RUTA /processa
# ============================================================
#
# Aquesta és la ruta principal del sistema TTS.
#
# El formulari web envia el text mitjançant:
#
#     POST /processa
#
# El text arriba dins del camp:
#
#     text
#
# ============================================================

@app.route('/processa', methods=['POST'])
def processa():

    # --------------------------------------------------------
    # RECUPERAR EL TEXT DEL FORMULARI
    # --------------------------------------------------------
    #
    # request.form.get('text', '')
    #
    # Recupera el camp "text" enviat pel formulari.
    #
    # .strip()
    #
    # Elimina espais i salts de línia sobrants al principi
    # i al final del text.
    # --------------------------------------------------------

    text = request.form.get('text', '').strip()


    # --------------------------------------------------------
    # COMPROVACIÓ DE TEXT BUIT
    # --------------------------------------------------------
    #
    # Si l'usuari envia el formulari sense text, no intentem
    # generar cap àudio.
    #
    # El codi HTTP 400 indica una petició incorrecta.
    # --------------------------------------------------------

    if not text:
        return "No s'ha introduit cap text.", 400


    # ========================================================
    # GENERACIÓ DE L'ÀUDIO
    # ========================================================

    try:

        # ----------------------------------------------------
        # gTTS
        # ----------------------------------------------------
        #
        # text=text
        #     Text que volem convertir en veu.
        #
        # lang='ca'
        #     Català.
        #
        # slow=False
        #     Velocitat normal.
        #
        # ----------------------------------------------------

        tts = gTTS(
            text=text,
            lang='ca',
            slow=False
        )


        # ----------------------------------------------------
        # DESAR L'ÀUDIO
        # ----------------------------------------------------
        #
        # El fitxer sempre es genera amb el mateix nom:
        #
        #     sortida.mp3
        #
        # Per tant, una nova petició substitueix l'àudio
        # anterior.
        # ----------------------------------------------------

        tts.save("sortida.mp3")


    # --------------------------------------------------------
    # CONTROL D'ERRORS DE gTTS
    # --------------------------------------------------------

    except Exception as e:

        # Si gTTS falla, mostrem l'error i oferim un enllaç
        # per tornar al formulari.
        #
        # El codi HTTP 500 indica un error del servidor.

        return f'''
        <h1>Error generant l'àudio</h1>
        <p>{e}</p>
        <br>
        <a href="http://192.168.1.49/python_server/"
           style="
           padding: 10px 20px;
           background: #dc3545;
           color: white;
           text-decoration: none;
           border-radius: 5px;">
           Tornar al formulari
        </a>
        ''', 500


    # ========================================================
    # PROTECCIÓ DEL TEXT ABANS D'INSERTAR-LO A L'HTML
    # ========================================================
    #
    # L'usuari ha escrit el text i nosaltres el tornarem a
    # mostrar dins de la pàgina HTML.
    #
    # html.escape() converteix caràcters especials:
    #
    #     <   -> &lt;
    #     >   -> &gt;
    #     &   -> &amp;
    #     "   -> &quot;
    #
    # Això evita que el text introduït per l'usuari sigui
    # interpretat com a codi HTML.
    # --------------------------------------------------------

    text_segur = html.escape(text)


    # ========================================================
    # CONSTRUCCIÓ DE LA PÀGINA HTML DE RETORN
    # ========================================================
    #
    # Utilitzem una f-string perquè podem inserir:
    #
    #     {text_segur}
    #
    # dins de la pàgina.
    #
    # Important:
    #
    # Les claus de CSS i JavaScript apareixen com:
    #
    #     {{ }}
    #
    # perquè estem dins d'una f-string de Python.
    #
    # Python les transforma en:
    #
    #     { }
    #
    # quan genera l'HTML final.
    # ========================================================

    html_retorn = f'''
<!DOCTYPE html>

<html lang="ca">

<head>

    <meta charset="UTF-8">

    <title>Audio TTS</title>


    <!-- =====================================================
         ESTILS DE LA PÀGINA
         ===================================================== -->

    <style>

        body {{
            font-family: Arial, sans-serif;
            text-align: center;
            padding: 40px;
            background-color: #f4f4f9;
        }}


        .container {{
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            max-width: 600px;
            margin: 0 auto;
        }}


        .btn-tornada {{
            display: inline-block;
            margin-top: 20px;
            padding: 12px 30px;
            background-color: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 5px;
            font-weight: bold;
        }}


        .btn-tornada:hover {{
            background-color: #0056b3;
        }}


        audio {{
            width: 100%;
            margin: 20px 0;
        }}

    </style>

</head>


<body>


    <!-- =====================================================
         CONTENIDOR PRINCIPAL
         ===================================================== -->

    <div class="container">


        <!-- TÍTOL -->

        <h1>Audio generat correctament</h1>


        <!-- TEXT QUE HA ESCRIT L'USUARI -->

        <p>
            <strong>Text:</strong> {text_segur}
        </p>


        <!-- =================================================
             REPRODUCTOR D'ÀUDIO
             =================================================
             
             id="audio"
                 Permet localitzar l'element des de JavaScript.
             
             controls
                 Mostra els controls del reproductor.
             
             autoplay
                 Intenta començar la reproducció automàticament.
             ================================================= -->

        <audio id="audio" controls autoplay>

            <source
                src="/sortida.mp3"
                type="audio/mpeg">

            El navegador no pot reproduir aquest àudio.

        </audio>


        <br>


        <!-- =================================================
             BOTÓ DE RETORN MANUAL
             ================================================= -->

        <a
            href="http://192.168.1.49/python_server/"
            class="btn-tornada">

            &larr; Tornar al formulari

        </a>


    </div>


    <!-- =====================================================
         JAVASCRIPT
         =====================================================
         
         La funció d'aquest script és esperar que l'àudio
         finalitzi.
         
         Quan es produeix l'esdeveniment:
         
             ended
         
         el navegador retorna automàticament al formulari.
         ===================================================== -->

    <script>

        // Buscar l'element <audio> pel seu ID.

        const audio = document.getElementById("audio");


        // Comprovar que l'element existeix.

        if (audio) {{

            // ------------------------------------------------
            // EVENT "ended"
            // ------------------------------------------------
            //
            // Aquest esdeveniment es produeix quan el MP3
            // ha arribat al final.
            // ------------------------------------------------

            audio.addEventListener("ended", function() {{


                // Retorn automàtic al formulari.

                window.location.href =
                    "http://192.168.1.49/python_server/";

            }});

        }}

    </script>


</body>

</html>
'''


    # --------------------------------------------------------
    # RETORNAR LA PÀGINA AL NAVEGADOR
    # --------------------------------------------------------

    return html_retorn


# ============================================================
# RUTA /sortida.mp3
# ============================================================
#
# Aquesta ruta permet que el navegador pugui obtenir el fitxer
# MP3 que acabem de generar.
#
# Quan l'HTML conté:
#
#     <source src="/sortida.mp3">
#
# el navegador fa una petició:
#
#     GET /sortida.mp3
#
# i aquesta funció li retorna el fitxer.
# ============================================================

@app.route('/sortida.mp3')
def serve_mp3():

    return send_from_directory(
        os.getcwd(),
        'sortida.mp3',
        mimetype='audio/mpeg'
    )


# ============================================================
# INICI DEL SERVIDOR
# ============================================================
#
# Aquest bloc només s'executa quan iniciem directament
# aquest fitxer Python.
#
# host='0.0.0.0'
#     Permet connexions des d'altres equips de la xarxa.
#
# port=5040
#     Port utilitzat pel servidor Flask.
#
# debug=True
#     Activa el mode de depuració de Flask.
#
# ============================================================

if __name__ == '__main__':

    print(
        "Servidor Flask engegat a "
        "http://0.0.0.0:5040"
    )

    app.run(
        host='0.0.0.0',
        port=5040,
        debug=True
    )
```
# ============================================================
# SERVIDOR TTS AMB FLASK + gTTS
# ============================================================
#
# Funció:
#   1. Rep text des del formulari web.
#   2. Genera un fitxer MP3 amb Google Text-to-Speech (gTTS).
#   3. Mostra una pàgina amb el reproductor d'àudio.
#   4. Reprodueix l'àudio automàticament.
#   5. Quan l'àudio acaba, retorna automàticament al formulari.
#
# Adreça del servidor:
#   http://192.168.1.49:5040
#
# Formulari principal:
#   http://192.168.1.49/python_server/
#
# Endpoint que rep el text:
#   POST /processa
#
# Fitxer d'àudio:
#   /sortida.mp3
#
# ============================================================


# ------------------------------------------------------------
# IMPORTACIONS
# ------------------------------------------------------------

from flask import Flask, request, send_from_directory
from gtts import gTTS
import os
import html


# ------------------------------------------------------------
# CREACIÓ DE L'APLICACIÓ FLASK
# ------------------------------------------------------------

app = Flask(__name__)


# ============================================================
# RUTA /processa
# ============================================================
#
# Aquesta és la ruta principal del sistema TTS.
#
# El formulari web envia el text mitjançant:
#
#     POST /processa
#
# El text arriba dins del camp:
#
#     text
#
# ============================================================

@app.route('/processa', methods=['POST'])
def processa():

    # --------------------------------------------------------
    # RECUPERAR EL TEXT DEL FORMULARI
    # --------------------------------------------------------
    #
    # request.form.get('text', '')
    #
    # Recupera el camp "text" enviat pel formulari.
    #
    # .strip()
    #
    # Elimina espais i salts de línia sobrants al principi
    # i al final del text.
    # --------------------------------------------------------

    text = request.form.get('text', '').strip()


    # --------------------------------------------------------
    # COMPROVACIÓ DE TEXT BUIT
    # --------------------------------------------------------
    #
    # Si l'usuari envia el formulari sense text, no intentem
    # generar cap àudio.
    #
    # El codi HTTP 400 indica una petició incorrecta.
    # --------------------------------------------------------

    if not text:
        return "No s'ha introduit cap text.", 400


    # ========================================================
    # GENERACIÓ DE L'ÀUDIO
    # ========================================================

    try:

        # ----------------------------------------------------
        # gTTS
        # ----------------------------------------------------
        #
        # text=text
        #     Text que volem convertir en veu.
        #
        # lang='ca'
        #     Català.
        #
        # slow=False
        #     Velocitat normal.
        #
        # ----------------------------------------------------

        tts = gTTS(
            text=text,
            lang='ca',
            slow=False
        )


        # ----------------------------------------------------
        # DESAR L'ÀUDIO
        # ----------------------------------------------------
        #
        # El fitxer sempre es genera amb el mateix nom:
        #
        #     sortida.mp3
        #
        # Per tant, una nova petició substitueix l'àudio
        # anterior.
        # ----------------------------------------------------

        tts.save("sortida.mp3")


    # --------------------------------------------------------
    # CONTROL D'ERRORS DE gTTS
    # --------------------------------------------------------

    except Exception as e:

        # Si gTTS falla, mostrem l'error i oferim un enllaç
        # per tornar al formulari.
        #
        # El codi HTTP 500 indica un error del servidor.

        return f'''
        <h1>Error generant l'àudio</h1>
        <p>{e}</p>
        <br>
        <a href="http://192.168.1.49/python_server/"
           style="
           padding: 10px 20px;
           background: #dc3545;
           color: white;
           text-decoration: none;
           border-radius: 5px;">
           Tornar al formulari
        </a>
        ''', 500


    # ========================================================
    # PROTECCIÓ DEL TEXT ABANS D'INSERTAR-LO A L'HTML
    # ========================================================
    #
    # L'usuari ha escrit el text i nosaltres el tornarem a
    # mostrar dins de la pàgina HTML.
    #
    # html.escape() converteix caràcters especials:
    #
    #     <   -> &lt;
    #     >   -> &gt;
    #     &   -> &amp;
    #     "   -> &quot;
    #
    # Això evita que el text introduït per l'usuari sigui
    # interpretat com a codi HTML.
    # --------------------------------------------------------

    text_segur = html.escape(text)


    # ========================================================
    # CONSTRUCCIÓ DE LA PÀGINA HTML DE RETORN
    # ========================================================
    #
    # Utilitzem una f-string perquè podem inserir:
    #
    #     {text_segur}
    #
    # dins de la pàgina.
    #
    # Important:
    #
    # Les claus de CSS i JavaScript apareixen com:
    #
    #     {{ }}
    #
    # perquè estem dins d'una f-string de Python.
    #
    # Python les transforma en:
    #
    #     { }
    #
    # quan genera l'HTML final.
    # ========================================================

    html_retorn = f'''
<!DOCTYPE html>

<html lang="ca">

<head>

    <meta charset="UTF-8">

    <title>Audio TTS</title>


    <!-- =====================================================
         ESTILS DE LA PÀGINA
         ===================================================== -->

    <style>

        body {{
            font-family: Arial, sans-serif;
            text-align: center;
            padding: 40px;
            background-color: #f4f4f9;
        }}


        .container {{
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            max-width: 600px;
            margin: 0 auto;
        }}


        .btn-tornada {{
            display: inline-block;
            margin-top: 20px;
            padding: 12px 30px;
            background-color: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 5px;
            font-weight: bold;
        }}


        .btn-tornada:hover {{
            background-color: #0056b3;
        }}


        audio {{
            width: 100%;
            margin: 20px 0;
        }}

    </style>

</head>


<body>


    <!-- =====================================================
         CONTENIDOR PRINCIPAL
         ===================================================== -->

    <div class="container">


        <!-- TÍTOL -->

        <h1>Audio generat correctament</h1>


        <!-- TEXT QUE HA ESCRIT L'USUARI -->

        <p>
            <strong>Text:</strong> {text_segur}
        </p>


        <!-- =================================================
             REPRODUCTOR D'ÀUDIO
             =================================================
             
             id="audio"
                 Permet localitzar l'element des de JavaScript.
             
             controls
                 Mostra els controls del reproductor.
             
             autoplay
                 Intenta començar la reproducció automàticament.
             ================================================= -->

        <audio id="audio" controls autoplay>

            <source
                src="/sortida.mp3"
                type="audio/mpeg">

            El navegador no pot reproduir aquest àudio.

        </audio>


        <br>


        <!-- =================================================
             BOTÓ DE RETORN MANUAL
             ================================================= -->

        <a
            href="http://192.168.1.49/python_server/"
            class="btn-tornada">

            &larr; Tornar al formulari

        </a>


    </div>


    <!-- =====================================================
         JAVASCRIPT
         =====================================================
         
         La funció d'aquest script és esperar que l'àudio
         finalitzi.
         
         Quan es produeix l'esdeveniment:
         
             ended
         
         el navegador retorna automàticament al formulari.
         ===================================================== -->

    <script>

        // Buscar l'element <audio> pel seu ID.

        const audio = document.getElementById("audio");


        // Comprovar que l'element existeix.

        if (audio) {{

            // ------------------------------------------------
            // EVENT "ended"
            // ------------------------------------------------
            //
            // Aquest esdeveniment es produeix quan el MP3
            // ha arribat al final.
            // ------------------------------------------------

            audio.addEventListener("ended", function() {{


                // Retorn automàtic al formulari.

                window.location.href =
                    "http://192.168.1.49/python_server/";

            }});

        }}

    </script>


</body>

</html>
'''


    # --------------------------------------------------------
    # RETORNAR LA PÀGINA AL NAVEGADOR
    # --------------------------------------------------------

    return html_retorn


# ============================================================
# RUTA /sortida.mp3
# ============================================================
#
# Aquesta ruta permet que el navegador pugui obtenir el fitxer
# MP3 que acabem de generar.
#
# Quan l'HTML conté:
#
#     <source src="/sortida.mp3">
#
# el navegador fa una petició:
#
#     GET /sortida.mp3
#
# i aquesta funció li retorna el fitxer.
# ============================================================

@app.route('/sortida.mp3')
def serve_mp3():

    return send_from_directory(
        os.getcwd(),
        'sortida.mp3',
        mimetype='audio/mpeg'
    )


# ============================================================
# INICI DEL SERVIDOR
# ============================================================
#
# Aquest bloc només s'executa quan iniciem directament
# aquest fitxer Python.
#
# host='0.0.0.0'
#     Permet connexions des d'altres equips de la xarxa.
#
# port=5040
#     Port utilitzat pel servidor Flask.
#
# debug=True
#     Activa el mode de depuració de Flask.
#
# ============================================================

if __name__ == '__main__':

    print(
        "Servidor Flask engegat a "
        "http://0.0.0.0:5040"
    )

    app.run(
        host='0.0.0.0',
        port=5040,
        debug=True
    )

El flux que queda documentat

El sistema ara es pot entendre molt fàcilment com aquest circuit:

┌──────────────────────────────┐
│ http://192.168.1.49/        │
│ python_server/              │
│                              │
│ Formulari HTML               │
└──────────────┬───────────────┘
               │
               │ POST /processa
               │ text
               ▼
┌──────────────────────────────┐
│ Flask :5040                  │
│                              │
│ processa()                   │
│                              │
│ 1. Rep text                  │
│ 2. Comprova text             │
│ 3. gTTS català               │
│ 4. Genera sortida.mp3        │
└──────────────┬───────────────┘
               │
               │ HTML
               ▼
┌──────────────────────────────┐
│ Firefox / navegador          │
│                              │
│ <audio id="audio"            │
│        autoplay>              │
│                              │
│ Reprodueix sortida.mp3       │
└──────────────┬───────────────┘
               │
               │ esdeveniment
               │ "ended"
               ▼
┌──────────────────────────────┐
│ JavaScript                   │
│                              │
│ window.location.href         │
│                              │
│ ↓                            │
│ torna a python_server/       │
└──────────────────────────────┘

Una dada especialment important per conservar en la documentació: el problema que teníem amb {{ }} i }} ara queda explicat. No són un error en aquest app.py: són necessaris perquè tota la pàgina està dins d’una f-string (f'''...'''). Python converteix {{ en { i }} en } abans d’enviar l’HTML al navegador.

I el html.escape(text) també és una peça important: permet mostrar el text introduït sense que caràcters com <, > o & siguin interpretats com HTML.

Flux

Sí. I en el teu cas no cal tocar Flask ni el TTS: el que està impedint que soni automàticament és molt probablement la política d’autoreproducció del Firefox.

A la captura es veu que la pàgina ja genera correctament l’àudio i mostra el reproductor. El que falta és que Firefox faci play() sense que tu premis el botó.

1. Primer: fes que la pàgina HTML intenti reproduir-lo automàticament

Si ara tens alguna cosa semblant a:

<audio controls src="{{ audio_file }}"></audio>

canvia-ho per:

<audio controls autoplay src="{{ audio_file }}"></audio>

O, millor encara:

<audio id="audio" controls autoplay>
    <source src="{{ audio_file }}" type="audio/wav">
</audio>

<script>
window.addEventListener("load", function () {
    const audio = document.getElementById("audio");

    audio.play().catch(function(error) {
        console.log("Autoplay bloquejat:", error);
    });
});
</script>

Això farà que la pàgina demani a Firefox que comenci a reproduir l’àudio tan aviat com es carregui.

2. Però aquí hi ha la clau: Firefox

Firefox bloqueja per defecte la reproducció automàtica amb so en molts casos. Mozilla confirma que es pot permetre l’autoreproducció per a un lloc concret. (Suport de Mozilla)

Per tant, jo faria primer aquesta prova:

Obre:

http://192.168.1.49:5040/processa

i mira a l’esquerra de la barra d’adreces el símbol relacionat amb l’autoreproducció/permisos.

També pots anar a:

Firefox → Ajustos → Privadesa i seguretat → Permisos → Reproducció automàtica → Configuració

i permetre:

Permetre àudio i vídeo

per al teu servidor.

Mozilla permet precisament configurar aquesta excepció per lloc web, de manera que no cal canviar el comportament de tot Internet. (Suport de Mozilla)


3. Per al teu projecte jo faria això

El flux quedaria:

index.html
     │
     │ POST
     ▼
/processa
     │
     ▼
crear_html_i_tts.py
     │
     │ genera WAV
     ▼
pàgina Flask
     │
     │ AUTOPLAY
     ▼
🔊 ÀUDIO

És a dir, no necessitem cap botó «Play».

I això encaixa perfectament amb el que estàvem construint: el servidor genera l’àudio i el navegador simplement el reprodueix.

Una cosa important

Si vols que sigui 100% automàtic fins i tot quan Firefox obre una pestanya nova, hem de configurar dues coses:

  1. el <audio autoplay> / JavaScript de Flask;
  2. donar permís d’autoreproducció a 192.168.1.49:5040 al Firefox.

La segona és la que probablement et falta ara mateix.

Si vols, podem fer-ho pas a pas sobre el teu index.html actual i el Flask, i et preparo exactament el codi que has de substituir perquè quan entri a /processa soni directament l’àudio sense tocar absolutament res.

python3 –version al synology-> /usr/local/bin/python3 –version

/usr/local/bin/python3 --version

/usr/local/bin/python3 no està incorporat al PATH (nano ~/.profile : export PATH=»/usr/local/bin:$PATH») i per tant python3 –version no funciona.

s’ha de posar:

/usr/local/bin/python3 --version
/usr/local/bin/python3 --version

llançar el servidor:

python3 -m http.server 5060

/usr/local/bin/python3 -m http.server 5060

Entrada des del formulari html per al servidor TTS (Ona, UPC) a Deepin del Docker

Perfecte, Miquel — ara entenc exactament què vols:

👉 Que l’entrada del blog expliqui que NO cal obrir cap terminal, ni arrencar Python, ni entrar al servidor Flask. 👉 Només cal obrir el fitxer local TTS Piper (Ona).html, i aquest formulari ja envia el text al servidor Flask que està funcionant en segon pla gràcies a systemd. 👉 I que la URL http://127.0.0.1:5000 només és visible en mode debug, però en l’ús normal no cal ni visitar-la.

Això és molt més elegant i molt més “user‑friendly”.

A continuació et deixo el text revisat perquè l’entrada del blog reflecteixi exactament aquest comportament.

Entrada des de formulari HTML per al servidor TTS (Ona, UPC) a Deepin del Docker

Aquesta entrada complementa la guia principal “Muntatge d’un servidor TTS local amb veu catalana (Ona, UPC) a Deepin” i explica com afegir-hi un formulari HTML extern que permet enviar text directament al servidor TTS sense obrir cap terminal.

L’objectiu és disposar d’un sistema molt simple d’usar: només cal obrir el fitxer TTS Piper (Ona).html i escriure el text. El servidor Flask ja està funcionant en segon pla gràcies a systemd.

🎯 Objectiu

  • Crear un servidor Flask que escolta a 127.0.0.1:5000/processa.
  • Fer que Flask s’iniciï automàticament en cada arrencada del sistema.
  • Crear un fitxer HTML local que envia text al servidor TTS.
  • Permetre que tot el sistema funcioni sense obrir Python, sense activar entorns virtuals, sense obrir cap terminal.
  • Fer que el sistema sigui reproduïble en qualsevol Deepin/Debian.

🗂️ Estructura final del projecte

Codi

~/tts-flask/
 ├── server.py
 └── venv/
~/Desktop/
 └── TTS Piper (Ona).html
/etc/systemd/system/
 └── flask.service

1️⃣ Instal·lació de Flask

bash

mkdir -p ~/tts-flask
cd ~/tts-flask

python3 -m venv venv
source venv/bin/activate

pip install flask

2️⃣ Fitxer server.py

Ubicació:

Codi

~/tts-flask/server.py

Funciona com a pont entre el formulari HTML i el contenidor Docker piper. Rep text, el passa al contenidor i reprodueix l’àudio amb aplay.

3️⃣ Fitxer flask.service (systemd)

Ubicació:

Codi

/etc/systemd/system/flask.service

Aquest servei garanteix que Flask s’inicia automàticament en cada arrencada:

  • No cal executar Python manualment.
  • No cal activar cap entorn virtual.
  • No cal obrir cap terminal.

Només cal tenir el fitxer HTML.

4️⃣ Activació del servei

bash

sudo systemctl daemon-reload
sudo systemctl enable flask.service
sudo systemctl start flask.service

Després d’això, el servidor Flask queda sempre actiu.

5️⃣ Fitxer HTML: TTS Piper (Ona).html

Aquest és el fitxer que l’usuari obre per enviar text al TTS.

Es pot guardar a:

Codi

~/Desktop/TTS Piper (Ona).html

Contingut:

html

<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <title>TTS Piper (Ona)</title>
</head>
<body>
    <h1>TTS Piper (Ona)</h1>
    <form action="http://127.0.0.1:5000/processa" method="post">
        <label for="text">Text:</label><br>
        <textarea id="text" name="text" rows="5" style="width: 80%;"></textarea><br><br>

        <button type="submit" title="Escoltar text" aria-label="Escoltar text"
            style="background: none; border: 1px solid #ccc; border-radius: 6px;
                   cursor: pointer; padding: 8px; display: inline-flex;
                   align-items: center; justify-content: center;
                   transition: background-color 0.2s;"
            onmouseover="this.style.backgroundColor='#f0f0f0'"
            onmouseout="this.style.backgroundColor='none'">

            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
                 viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2"
                 stroke-linecap="round" stroke-linejoin="round">
                <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
                <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
            </svg>
        </button>
    </form>
</body>
</html>

6️⃣ Ús normal del sistema

Un cop configurat:

✔️ Només cal obrir TTS Piper (Ona).html

✔️ Escriure el text

✔️ Fer clic al botó d’altaveu

I el sistema:

  • envia el text al servidor Flask
  • Flask el passa al contenidor Docker piper
  • Piper genera l’àudio
  • aplay el reprodueix

Tot això sense obrir cap terminal.

7️⃣ Sobre http://127.0.0.1:5000

  • La pàgina existeix, però només és útil en mode debug.
  • En l’ús normal no cal visitar-la.
  • El formulari HTML és l’única interfície que cal.

🔗 Enllaç amb l’article principal

Aquesta entrada forma part del projecte complet documentat a:

Muntatge d’un servidor TTS local amb veu catalana (Ona, UPC) a Deepin (tab actual: )

🎓 Conclusions

Amb aquest formulari HTML:

  • El sistema TTS és molt més accessible.
  • No cal cap coneixement de Python ni Docker.
  • Tot funciona automàticament gràcies a systemd.
  • És 100% reproduïble en qualsevol equip Deepin/Debian.
  • L’usuari només interactua amb un fitxer HTML senzill i elegant.

Si vols, puc generar la versió exacta en Markdown per enganxar-la al WordPress (tab 1330389275).

Crear contenidor entrada text de html-sortida text a Peper

Aquí tens l’entrada de blog completa, llesta per publicar (en Markdown):


---
title: "Text del navegador a veu amb Piper TTS en Docker"
date: "2026-09-20"
tags: ["docker", "piper", "tts", "python", "flask"]
---

# Text del navegador a veu amb Piper TTS en Docker

Vols escriure text en una pàgina web i que es converteixi en àudio (WAV) amb una veu sintètica? En aquest post et mostro com muntar un **contenidor Docker** que fa exactament això: 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.

## Arquitectura

El flux és senzill:

1. L'usuari escriu text a un formulari HTML.
2. JavaScript envia el text a una API (Flask) mitjançant `POST /api/tts`.
3. Flask executa `piper` dins del contenidor amb el text rebut.
4. Piper genera un fitxer `.wav`.
5. L'àudio es retorna al navegador i es reprodueix automàticament.

Navegador (HTML) ──POST text──▶ Flask API ──▶ Piper CLI ──▶ WAV ──▶ Navegador

## Requisits

- Docker i Docker Compose instal·lats.
- Connexió a internet per baixar les imatges i el model de veu.

## Estructura de fitxers

piper-tts/
├── app.py
├── Dockerfile
├── docker-compose.yml
└── templates/
└── index.html

## Pas 1: l'API amb Flask (`app.py`)

L'aplicació exposa la pàgina principal i l'endpoint `/api/tts`, que rep el text, l'executa amb Piper i retorna el WAV:

```python
from flask import Flask, request, render_template, send_file
import subprocess

app = Flask(__name__)

MODEL = "/models/zh_CN-huayan-medium.onnx"
OUTPUT = "/app/output/resultat.wav"

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/api/tts", methods=["POST"])
def tts():
    data = request.get_json()
    text = data.get("text", "")
    if not text:
        return {"error": "text buit"}, 400

    subprocess.run(
        ["piper", "--model", MODEL, "--output_file", OUTPUT],
        input=text.encode("utf-8")
    )
    return send_file(OUTPUT, mimetype="audio/wav")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Pas 2: la pàgina HTML (templates/index.html)

Un textarea per escriure, un botó per sintetitzar i un reproductor d’àudio:

<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <title>Piper TTS</title>
</head>
<body>
    <h1>Piper TTS — text a veu</h1>
    <textarea id="text" rows="4" cols="60">Hola, benvingut al servei de veu!</textarea>
    <br><br>
    <button onclick="sintetitza()">Sintetitza</button>
    <br><br>
    <audio id="audio" controls style="display:none"></audio>

    <script>
        async function sintetitza() {
            const text = document.getElementById("text").value;
            const res = await fetch("/api/tts", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ text: text })
            });
            const blob = await res.blob();
            const url = URL.createObjectURL(blob);
            const audio = document.getElementById("audio");
            audio.src = url;
            audio.style.display = "block";
            audio.play();
        }
    </script>
</body>
</html>

Pas 3: el Dockerfile

Instal·la Flask i Piper, baixa el model de veu i defineix l’arrencada:

FROM python:3.11-slim

# Instal·la dependències
RUN apt-get update && apt-get install -y wget espeak-ng && \
    pip install flask piper-tts

# Baixa el model de veu (exemple: xinès huayan; canvia'l pel que vulguis)
RUN mkdir -p /models && cd /models && \
    wget https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx && \
    wget https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx.json

WORKDIR /app
COPY app.py .
COPY templates ./templates
RUN mkdir -p output

EXPOSE 5000
CMD ["python", "app.py"]

💡 Per a veus en català o castellà, consulta els models a huggingface.co/rhasspy/piper-voices i canvia les URLs del Dockerfile.

Pas 4: docker-compose.yml

services:
  piper-tts:
    build: .
    container_name: piper-tts
    ports:
      - "5000:5000"
    volumes:
      - ./models:/models
      - ./output:/app/output
    restart: unless-stopped

Pas 5: posada en marxa

cd piper-tts
docker compose up -d --build

Obre http://localhost:5000 al navegador i:

  1. Escriu el text al quadre HTML.
  2. Prem Sintetitza.
  3. Rep el WAV i el reprodueix automàticament.

Prova ràpida amb curl

curl -X POST http://localhost:5000/api/tts \
  -H "Content-Type: application/json" \
  -d '{"text": "Hola, això és una prova"}' \
  --output prova.wav

Alternativa: imatge ja preparada

Si no vols construir res, existeixen imatges com loderunner84/piper-rest (Flask + Piper). Ara bé, muntar-ho tu mateix et dona control total sobre el model, els endpoints i la interfície.

Conclusió

Amb quatre fitxers tens un servei de text-a-veu complet dins d’un contenidor Docker: text des del navegador entra, WAV en surt. Pots ampliar-lo fàcilment: suport de múltiples idiomes, cua de peticions o desar historial d’àudios. Bon blog i bona sintetització! 🎙️

---

Aquest és el post complet en Markdown. Si vols, puc:

1. **Adaptar-lo al teu blog** (canviar títol, tags, to, afegir captures).
2. **Traduir-lo al castellà** o a l'anglès.
3. **Crear la carpeta del post** perquè el desis directament (recorda que per crear fitxers necessito que em diguis com vols fer-ho, o t'obro l'editor).

Què prefereixes?