{"id":19089,"date":"2026-09-16T08:07:46","date_gmt":"2026-09-16T06:07:46","guid":{"rendered":"https:\/\/www.beseit.net\/?p=19089"},"modified":"2026-09-16T08:38:23","modified_gmt":"2026-09-16T06:38:23","slug":"reproductor","status":"publish","type":"post","link":"https:\/\/www.beseit.net\/?p=19089","title":{"rendered":"reproductor"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>vull adaptar aquest comanda del servidor local TTS de Deepin ( que funciona perfectament )al format que mostra la imatge en html: una entrada de text i un bot\u00f3 en lloc d&#8217;una gravadora. \u00c9s possible?<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">S\u00ed, \u00e9s totalment possible! Et proporcionar\u00e9 una soluci\u00f3 completa amb HTML, CSS i un backend senzill.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Soluci\u00f3 completa:<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. <strong>Backend amb Node.js<\/strong> (recomanat):<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Crea un fitxer <code>server.js<\/code>:<\/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=\"\">const express = require('express');\nconst { exec } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst app = express();\nconst PORT = 3000;\n\napp.use(express.json());\napp.use(express.static('public'));\n\n\/\/ Endpoint per generar TTS\napp.post('\/tts', (req, res) => {\n    const text = req.body.text;\n    const outputFile = `public\/audio\/tts_${Date.now()}.wav`;\n    \n    \/\/ Comanda TTS (adapta-la al teu sistema Deepin)\n    \/\/ Opci\u00f3 1: amb espeak\n    const command = `espeak -w \"${outputFile}\" \"${text}\"`;\n    \n    \/\/ Opci\u00f3 2: amb festival (descomenta si ho prefereixes)\n    \/\/ const command = `echo \"${text}\" | festival --tts --output=\"${outputFile}\"`;\n    \n    exec(command, (error) => {\n        if (error) {\n            return res.status(500).json({ error: error.message });\n        }\n        res.json({ audioUrl: outputFile });\n    });\n});\n\napp.listen(PORT, () => {\n    console.log(`Servidor TTS executant-se a http:\/\/localhost:${PORT}`);\n});<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. <strong>Frontend HTML<\/strong> (fitxer <code>public\/index.html<\/code>):<\/h3>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\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=\"\">&lt;!DOCTYPE html>\n&lt;html lang=\"ca\">\n&lt;head>\n    &lt;meta charset=\"UTF-8\">\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    &lt;title>TTS Deepin&lt;\/title>\n    &lt;style>\n        body {\n            font-family: Arial, sans-serif;\n            max-width: 800px;\n            margin: 50px auto;\n            padding: 20px;\n            background-color: #f5f5f5;\n        }\n        .container {\n            background: white;\n            padding: 30px;\n            border-radius: 8px;\n            box-shadow: 0 2px 10px rgba(0,0,0,0.1);\n        }\n        h1 {\n            color: #333;\n            margin-bottom: 20px;\n        }\n        textarea {\n            width: 100%;\n            height: 150px;\n            padding: 12px;\n            border: 2px solid #ddd;\n            border-radius: 4px;\n            font-size: 16px;\n            resize: vertical;\n            box-sizing: border-box;\n        }\n        textarea:focus {\n            outline: none;\n            border-color: #4CAF50;\n        }\n        button {\n            margin-top: 15px;\n            padding: 12px 30px;\n            background-color: #4CAF50;\n            color: white;\n            border: none;\n            border-radius: 4px;\n            font-size: 16px;\n            cursor: pointer;\n            transition: background 0.3s;\n        }\n        button:hover {\n            background-color: #45a049;\n        }\n        button:disabled {\n            background-color: #cccccc;\n            cursor: not-allowed;\n        }\n        .player-container {\n            margin-top: 30px;\n            padding: 20px;\n            background: #f9f9f9;\n            border-radius: 4px;\n            display: none;\n        }\n        .player-container.show {\n            display: block;\n        }\n        audio {\n            width: 100%;\n            margin-bottom: 15px;\n        }\n        .download-link {\n            color: #4CAF50;\n            text-decoration: none;\n            font-weight: bold;\n        }\n        .download-link:hover {\n            text-decoration: underline;\n        }\n        .loading {\n            display: none;\n            margin-top: 15px;\n            color: #666;\n        }\n        .loading.show {\n            display: block;\n        }\n    &lt;\/style>\n&lt;\/head>\n&lt;body>\n    &lt;div class=\"container\">\n        &lt;h1>Text a Veu (TTS)&lt;\/h1>\n        \n        &lt;textarea id=\"textInput\" placeholder=\"Escriu el text aqu\u00ed...\">&lt;\/textarea>\n        &lt;br>\n        &lt;button id=\"generateBtn\" onclick=\"generateTTS()\">Generar \u00c0udio&lt;\/button>\n        \n        &lt;div class=\"loading\" id=\"loading\">\n            Generant \u00e0udio...\n        &lt;\/div>\n        \n        &lt;div class=\"player-container\" id=\"playerContainer\">\n            &lt;audio id=\"audioPlayer\" controls>&lt;\/audio>\n            &lt;a id=\"downloadLink\" class=\"download-link\" download=\"tts.wav\">\n                 Descarregar \u00e0udio\n            &lt;\/a>\n        &lt;\/div>\n    &lt;\/div>\n\n    &lt;script>\n        async function generateTTS() {\n            const text = document.getElementById('textInput').value;\n            const btn = document.getElementById('generateBtn');\n            const loading = document.getElementById('loading');\n            const playerContainer = document.getElementById('playerContainer');\n            const audioPlayer = document.getElementById('audioPlayer');\n            const downloadLink = document.getElementById('downloadLink');\n            \n            if (!text.trim()) {\n                alert('Si us plau, escriu un text');\n                return;\n            }\n            \n            \/\/ Mostrar estat de c\u00e0rrega\n            btn.disabled = true;\n            loading.classList.add('show');\n            playerContainer.classList.remove('show');\n            \n            try {\n                const response = await fetch('\/tts', {\n                    method: 'POST',\n                    headers: {\n                        'Content-Type': 'application\/json'\n                    },\n                    body: JSON.stringify({ text: text })\n                });\n                \n                if (!response.ok) {\n                    throw new Error('Error en generar l\\'\u00e0udio');\n                }\n                \n                const data = await response.json();\n                \n                \/\/ Configurar el reproductor\n                audioPlayer.src = data.audioUrl;\n                downloadLink.href = data.audioUrl;\n                \n                \/\/ Mostrar el reproductor\n                playerContainer.classList.add('show');\n                audioPlayer.play();\n                \n            } catch (error) {\n                alert('Error: ' + error.message);\n            } finally {\n                btn.disabled = false;\n                loading.classList.remove('show');\n            }\n        }\n    &lt;\/script>\n&lt;\/body>\n&lt;\/html><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. <strong>Instal\u00b7laci\u00f3 i execuci\u00f3<\/strong>:<\/h3>\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=\"\"># Instal\u00b7lar depend\u00e8ncies\nnpm init -y\nnpm install express\n\n# Crear carpeta per als \u00e0udios\nmkdir -p public\/audio\n\n# Executar el servidor\nnode server.js<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">4. <strong>Comandes TTS alternatives<\/strong> (adapta al teu sistema):<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Amb espeak<\/strong> (el m\u00e9s com\u00fa a Linux):<\/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=\"\">espeak -w output.wav \"text\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Amb festival<\/strong>:<\/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=\"\">echo \"text\" | festival --tts --output=output.wav<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Amb speech-dispatcher<\/strong> (natiu de Deepin):<\/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=\"\">spd-say -o output.wav \"text\"<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Caracter\u00edstiques:<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Perfecte! Si ja tens <strong>Piper TTS<\/strong> funcionant a <code>127.0.0.1:10200<\/code>, la soluci\u00f3 \u00e9s molt m\u00e9s senzilla: nom\u00e9s cal fer de pont entre el navegador i el teu servidor Piper.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Soluci\u00f3 adaptada a Piper TTS<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. <strong><code>server.js<\/code><\/strong> (servidor intermediari Node.js)<\/h3>\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=\"\">const express = require('express');\nconst path = require('path');\nconst app = express();\nconst PORT = 3000;\nconst PIPER_URL = 'http:\/\/127.0.0.1:10200\/api\/tts';\n\napp.use(express.json());\napp.use(express.static('public'));\n\n\/\/ Endpoint que consulta Piper TTS\napp.post('\/tts', async (req, res) => {\n    const text = req.body.text;\n    \n    if (!text || !text.trim()) {\n        return res.status(400).json({ error: 'Text buit' });\n    }\n\n    try {\n        const response = await fetch(PIPER_URL, {\n            method: 'POST',\n            headers: { 'Content-Type': 'application\/json' },\n            body: JSON.stringify({ text: text })\n        });\n\n        if (!response.ok) {\n            throw new Error(`Piper TTS ha retornat ${response.status}`);\n        }\n\n        \/\/ Piper retorna l'\u00e0udio WAV directament\n        const audioBuffer = await response.arrayBuffer();\n        \n        res.set({\n            'Content-Type': 'audio\/wav',\n            'Content-Length': audioBuffer.byteLength\n        });\n        res.send(Buffer.from(audioBuffer));\n        \n    } catch (error) {\n        console.error('Error amb Piper TTS:', error.message);\n        res.status(500).json({ error: error.message });\n    }\n});\n\napp.listen(PORT, () => {\n    console.log(`\u2705 Interf\u00edcie TTS disponible a http:\/\/localhost:${PORT}`);\n    console.log(`\ud83d\udd0a Piper TTS a ${PIPER_URL}`);\n});<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">2. <strong><code>public\/index.html<\/code><\/strong> (la interf\u00edcie que has demanat)<\/h3>\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>TTS - Veu Ona (Piper)&lt;\/title>\n    &lt;style>\n        * { box-sizing: border-box; }\n        body {\n            font-family: 'Segoe UI', Arial, sans-serif;\n            max-width: 800px;\n            margin: 40px auto;\n            padding: 20px;\n            background: #f0f2f5;\n        }\n        .card {\n            background: white;\n            padding: 30px;\n            border-radius: 10px;\n            box-shadow: 0 2px 12px rgba(0,0,0,0.08);\n        }\n        h1 { color: #1a1a1a; margin-top: 0; }\n        .subtitle { color: #666; font-size: 14px; margin-bottom: 20px; }\n        textarea {\n            width: 100%;\n            height: 140px;\n            padding: 12px;\n            border: 2px solid #e0e0e0;\n            border-radius: 6px;\n            font-size: 16px;\n            resize: vertical;\n            font-family: inherit;\n        }\n        textarea:focus {\n            outline: none;\n            border-color: #4a90e2;\n        }\n        button {\n            margin-top: 15px;\n            padding: 12px 28px;\n            background: #4a90e2;\n            color: white;\n            border: none;\n            border-radius: 6px;\n            font-size: 15px;\n            font-weight: 600;\n            cursor: pointer;\n            transition: background 0.2s;\n        }\n        button:hover:not(:disabled) { background: #357abd; }\n        button:disabled {\n            background: #b0c4de;\n            cursor: not-allowed;\n        }\n        .status {\n            margin-top: 15px;\n            color: #666;\n            font-size: 14px;\n            min-height: 20px;\n        }\n        .player-box {\n            margin-top: 25px;\n            padding: 20px;\n            background: #f8f9fa;\n            border-radius: 8px;\n            display: none;\n        }\n        .player-box.visible { display: block; }\n        audio { width: 100%; margin-bottom: 12px; }\n        .download-link {\n            color: #4a90e2;\n            text-decoration: none;\n            font-weight: 600;\n            font-size: 14px;\n        }\n        .download-link:hover { text-decoration: underline; }\n    &lt;\/style>\n&lt;\/head>\n&lt;body>\n    &lt;div class=\"card\">\n        &lt;h1>\ud83d\udd0a Text a Veu&lt;\/h1>\n        &lt;div class=\"subtitle\">Veu: Ona (catal\u00e0) \u00b7 Motor: Piper TTS&lt;\/div>\n\n        &lt;textarea id=\"textInput\" placeholder=\"Escriu aqu\u00ed el text que vols escoltar...\">L'arc de Sant Mart\u00ed o arc del cel \u00e9s un fenomen meteorol\u00f2gic \u00f2ptic produ\u00eft per la reflexi\u00f3, refracci\u00f3 i dispersi\u00f3 de la llum.&lt;\/textarea>\n        &lt;br>\n        &lt;button id=\"btn\" onclick=\"speak()\">\u25b6 Generar i reproduir&lt;\/button>\n        &lt;div class=\"status\" id=\"status\">&lt;\/div>\n\n        &lt;div class=\"player-box\" id=\"playerBox\">\n            &lt;audio id=\"audio\" controls>&lt;\/audio>\n            &lt;a id=\"download\" class=\"download-link\" download=\"tts.wav\"> Descarregar \u00e0udio&lt;\/a>\n        &lt;\/div>\n    &lt;\/div>\n\n    &lt;script>\n        async function speak() {\n            const text = document.getElementById('textInput').value.trim();\n            const btn = document.getElementById('btn');\n            const status = document.getElementById('status');\n            const playerBox = document.getElementById('playerBox');\n            const audio = document.getElementById('audio');\n            const download = document.getElementById('download');\n\n            if (!text) {\n                status.textContent = '\u26a0 Escriu un text primer.';\n                return;\n            }\n\n            btn.disabled = true;\n            status.textContent = '\u23f3 Generant \u00e0udio amb Piper TTS...';\n            playerBox.classList.remove('visible');\n\n            try {\n                const res = await fetch('\/tts', {\n                    method: 'POST',\n                    headers: { 'Content-Type': 'application\/json' },\n                    body: JSON.stringify({ text })\n                });\n\n                if (!res.ok) throw new Error(`Error ${res.status}`);\n\n                const blob = await res.blob();\n                const url = URL.createObjectURL(blob);\n\n                audio.src = url;\n                download.href = url;\n                playerBox.classList.add('visible');\n                status.textContent = '\u2705 \u00c0udio generat correctament.';\n                audio.play();\n\n            } catch (err) {\n                status.textContent = '\u274c Error: ' + err.message;\n            } finally {\n                btn.disabled = false;\n            }\n        }\n    &lt;\/script>\n&lt;\/body>\n&lt;\/html><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. <strong>Execuci\u00f3<\/strong><\/h3>\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=\"\">mkdir -p public\n# (posa index.html dins de public\/)\nnpm init -y\nnpm install express\nnode server.js<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Obre <strong><a href=\"http:\/\/localhost:3000\">http:\/\/localhost:3000<\/a><\/strong> i ja ho tindr\u00e0s.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Com funciona el flux<\/h3>\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=\"\">[Navegador] \u2500POST \/tts\u2500\u25b6 [Node.js :3000] \u2500POST \/api\/tts\u2500\u25b6 [Piper TTS :10200]\n       \u25b2                        \u2502                                    \u2502\n       \u2500\u2500\u2500\u2500 audio\/wav \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 audio\/wav \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Notes importants<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Si el teu Piper TTS usa un endpoint diferent<\/strong> (per exemple <code>GET \/api\/tts?text=...<\/code> en lloc de POST), nom\u00e9s has de canviar la l\u00ednia del <code>fetch<\/code> al <code>server.js<\/code>. Pots comprovar-ho amb: <code>curl -X POST http:\/\/127.0.0.1:10200\/api\/tts -H \"Content-Type: application\/json\" -d '{\"text\":\"Hola\"}' --output test.wav<\/code>Si aix\u00f2 funciona, l&#8217;endpoint \u00e9s correcte.<\/li>\n\n\n\n<li><strong>CORS<\/strong>: com que el Node.js fa la petici\u00f3 al Piper des del servidor (no des del navegador), no hi ha problemes de CORS.<\/li>\n\n\n\n<li><strong>Sense fitxers temporals<\/strong>: l&#8217;\u00e0udio es mant\u00e9 en mem\u00f2ria (Blob URL), aix\u00ed que no omple el disc.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Vols que afegeixi alguna cosa m\u00e9s, com selector de veu (si en tens m\u00e9s d&#8217;una), control de velocitat, o historial de frases?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u2705 Input de text amb textarea<br>\u2705 Bot\u00f3 per generar<br>\u2705 Reproductor d&#8217;\u00e0udio amb controls (play, pausa, volum)<br>\u2705 Barra de progr\u00e9s temporal<br>\u2705 Bot\u00f3 de desc\u00e0rrega<br>\u2705 Disseny responsive<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Accedeix a <code>http:\/\/localhost:3000<\/code> i prova-ho! \ud83c\udfb5<\/p>\n","protected":false},"excerpt":{"rendered":"<p>vull adaptar aquest comanda del servidor local TTS de Deepin ( que funciona perfectament )al format que mostra la imatge en html: una entrada de text i un bot\u00f3 en lloc d&#8217;una gravadora. \u00c9s possible? S\u00ed, \u00e9s totalment possible! Et &hellip; <a href=\"https:\/\/www.beseit.net\/?p=19089\">Continua llegint <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-19089","post","type-post","status-publish","format-standard","hentry","category-bloc-de-notes"],"_links":{"self":[{"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/posts\/19089","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=19089"}],"version-history":[{"count":3,"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/posts\/19089\/revisions"}],"predecessor-version":[{"id":19094,"href":"https:\/\/www.beseit.net\/index.php?rest_route=\/wp\/v2\/posts\/19089\/revisions\/19094"}],"wp:attachment":[{"href":"https:\/\/www.beseit.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=19089"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.beseit.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=19089"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.beseit.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=19089"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}