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.