🤖✍️ Robot Poeta Interactiu

Tutorial pas a pas · Reconeixement de veu · Anàlisi de sentiments · Síntesi de parla · Responsiu

Aprendrem a crear un Robot Poeta que escolta les teves paraules mitjançant la Web Speech API (reconeixement de veu), n'analitza el sentiment amb un diccionari de paraules, genera un poema personalitzat i el recita en veu alta amb síntesi de parla. Funciona tant en ordinadors com en dispositius mòbils (cal un navegador compatible amb les API de veu, com Chrome).

Pas 1: Estructura HTML bàsica

Comencem amb l'esquelet del document. Incloem la meta etiqueta viewport per a la responsivitat. Dins del body situem una icona del robot, un títol, controls amb botons, i diverses caixes (div) per mostrar l'estat, el text reconegut, l'anàlisi de sentiments i el poema generat.

<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Robot Poeta Interactiu</title>
</head>
<body>
    <div class="robot-icon">🤖✍️</div>
    <h1>Robot Poeta Interactiu</h1>
    <div class="subtitle">Cada paraula respira mentre la recito</div>

    <div class="controls">
        <button onclick="start()">🎤 Parla amb mi</button>
        <button onclick="reprodueixPoema()">🔊 Escolta el poema</button>
        <br>
        <label for="estilPoema">Estil de poema:</label>
        <select id="estilPoema">
            <option value="clàssic">Clàssic</option>
            <option value="modern">Modern</option>
            <option value="minimalista">Minimalista</option>
        </select>
    </div>

    <div class="box">
        <p><strong>Estat:</strong></p>
        <div id="estat">🌬️ Escoltant el teu cor…</div>
    </div>

    <div class="box">
        <p><strong>Les teves paraules:</strong></p>
        <div id="resultat">Aquí apareixerà el que diguis...</div>
    </div>

    <div class="box">
        <p><strong>Anàlisi de sentiments:</strong></p>
        <div class="sentiment-box neutral" id="sentiment">Esperant les teves paraules...</div>
    </div>

    <div class="box">
        <p><strong>Poema generat:</strong></p>
        <div class="poema" id="poema"></div>
    </div>
</body>
</html>

Observa que els botons criden les funcions start() i reprodueixPoema() directament des de l'HTML. El selector #estilPoema permetrà (en una futura ampliació) triar diferents formats de poema.

Pas 2: Estils CSS responsius i temàtica

Utilitzem una paleta de colors càlids (marrons i cremes) per evocar un ambient poètic. El cos té max-width: 800px i padding perquè s'adapti a qualsevol pantalla. Les caixes (.box) tenen ombra i una vora esquerra decorativa. Afegim classes per als sentiments (.positiu, .negatiu, .neutral) que canvien els colors de fons i text, i transicions suaus.

body {
    font-family: 'Georgia', serif;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
    background: #f5f0e6;
    color: #333;
}
h1 {
    text-align: center;
    color: #5d4037;
    font-size: 2.5em;
    margin-bottom: 10px;
}
.subtitle {
    text-align: center;
    color: #795548;
    font-style: italic;
    margin-bottom: 20px;
}
.robot-icon {
    text-align: center;
    font-size: 3em;
    margin: 20px 0;
    transition: transform 0.3s;
}
.robot-icon.talking {
    transform: scale(1.2) rotate(-5deg);
}
button {
    background: #8d6e63;
    color: white;
    border: none;
    padding: 12px 24px;
    font-size: 18px;
    border-radius: 5px;
    cursor: pointer;
    display: block;
    margin: 20px auto;
    font-family: 'Georgia', serif;
    transition: background 0.3s;
}
button:hover {
    background: #6d4c41;
}
#estat {
    padding: 12px;
    border-radius: 8px;
    text-align: center;
    font-weight: bold;
    margin: 20px 0;
    font-style: italic;
    background: #fff3e0;
    transition: background 0.8s, color 0.8s;
}
.box {
    background: white;
    border-radius: 12px;
    padding: 20px;
    margin: 20px 0;
    border-left: 5px solid #8d6e63;
    box-shadow: 0 4px 10px rgba(0,0,0,0.08);
}
.sentiment-box {
    text-align: center;
    font-size: 1.2em;
    font-weight: bold;
    padding: 15px;
    border-radius: 10px;
    margin: 20px 0;
    transition: background 0.8s, color 0.8s, border-color 0.8s;
}
.positiu {
    background: #e8f5e9;
    color: #2e7d32;
    border: 2px solid #81c784;
}
.negatiu {
    background: #ffebee;
    color: #c62828;
    border: 2px solid #ef9a9a;
}
.neutral {
    background: #f3e5f5;
    color: #7b1fa2;
    border: 2px solid #ce93d8;
}
.poema {
    font-style: italic;
    line-height: 1.8;
    text-align: center;
    padding: 20px;
    background: #fff;
    border-radius: 12px;
    border: 2px dashed #8d6e63;
    margin: 20px 0;
}
.poema .estrofa { margin: 15px 0; }
.poema .vers {
    margin: 6px 0;
    opacity: 0;
    transition: opacity 0.3s;
}
.poema .signatura {
    text-align: right;
    font-style: normal;
    font-weight: bold;
    color: #5d4037;
    margin-top: 20px;
}
.controls {
    text-align: center;
    margin: 20px 0;
}
.controls button {
    display: inline-block;
    margin: 0 10px;
    padding: 10px 20px;
}
#estilPoema {
    margin-top: 10px;
    padding: 5px 10px;
    font-size: 16px;
}
💡 Consell: Les transicions (transition) fan que els canvis de color i opacitat siguin suaus, millorant l'experiència d'usuari.

Pas 3: Inicialització de variables globals

Definim un petit diccionari de sentiments (diccionariSentiments) on cada paraula té un valor numèric: positiu si és bona, negatiu si és dolenta. També declarem poemaActual per emmagatzemar el darrer poema generat, i sintesisVeu (no utilitzat directament, però es podria usar per desar la instància de síntesi).

const diccionariSentiments = {
    "bo": 2, "bé": 2, "content": 2, "feliç": 3, "fantàstic": 3,
    "dolent": -2, "malament": -2, "trist": -2, "horrible": -3, "problema": -1
};
let poemaActual = "";
let sintesisVeu = null;

Pas 4: Configuració del reconeixement de veu (SpeechRecognition)

La Web Speech API proporciona SpeechRecognition (o webkitSpeechRecognition en navegadors més antics). Creem una instància, li assignem l'idioma català (ca-ES) i desactivem els resultats intermedis. La funció start() actualitza l'estat i inicia l'escolta.

const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.lang = "ca-ES";
recognition.continuous = false;
recognition.interimResults = false;

function start() {
    document.getElementById("estat").innerText = "🎧 Escoltant el teu cor…";
    recognition.start();
}
⚠️ Important: El reconeixement de veu només funciona en pàgines servides via HTTPS o en localhost, i requereix permís de l'usuari per accedir al micròfon.

Pas 5: Anàlisi de sentiments

La funció analitzarSentiment(text) converteix el text a minúscules, el divideix en paraules i acumula la puntuació de cada paraula segons el diccionari. Retorna un nombre enter: positiu per a emocions bones, negatiu per a dolentes, zero si és neutre.

function analitzarSentiment(text) {
    return text.toLowerCase()
        .split(/\s+/)
        .reduce((acc, paraula) => acc + (diccionariSentiments[paraula] || 0), 0);
}

Pas 6: Generació del poema segons el sentiment

A partir de la puntuació del sentiment, generarPoema(text, puntuacio) retorna un objecte amb un títol, un prefix (emoji) i un array d'estrofes. Cada estrofa és un array de versos. Com més alta és la puntuació, més alegre és el poema; com més baixa, més malenconiós.

function generarPoema(text, puntuacio) {
    let estrofes = [], titulo = "", prefix = "";
    if (puntuacio > 2) {
        titulo = "Oda a l'Alegria";
        estrofes = [["El sol dansa al teu cor", "el vent canta amb tu"], ["Riu de llum s'estén", "i el teu cor brilla"]];
        prefix = "☀️ ";
    } else if (puntuacio > 0) {
        titulo = "Sonet del Benestar";
        estrofes = [["La calma flueix", "un sospir suau recorre el dia"], ["Tot sembla més lleu", "el moment és música"]];
    } else if (puntuacio > -1) {
        titulo = "Balada del Present";
        estrofes = [["Les paraules flueixen suaus", "ni gran goig ni clots"], ["Equilibri precís", "en el silenci amable"]];
    } else if (puntuacio > -3) {
        titulo = "Elegia del Desassossec";
        estrofes = [["Sento una ombra en el teu dir", "fred suau que toca el cor"], ["Però la llum espera", "torna a guiar el teu pas"]];
    } else {
        titulo = "Càntic de la Melangia";
        estrofes = [["Com pluja persistent", "la tristesa mulla el parlar"], ["Però el sol sempre torna", "i amb ell, l'esperança brilla"]];
        prefix = "🌧️ ";
    }
    return { prefix, titulo, estrofes };
}

Pas 7: Síntesi de veu i reproducció del poema

La funció reprodueixPoema() agafa tots els versos (elements .vers) i els va llegint un per un amb SpeechSynthesisUtterance. Mentre es llegeix, la icona del robot s'anima (classe .talking). Cada vers es fa visible (opacitat 1) en el moment que comença a ser pronunciat, creant un efecte de sincronització visual i auditiu.

function reprodueixPoema() {
    if (!poemaActual) {
        parla("Encara no he sentit les teves paraules per crear un poema.");
        return;
    }
    const versosEls = document.querySelectorAll('.vers');
    let delay = 0;
    function parlarVers(i) {
        if (i >= versosEls.length) return;
        const text = versosEls[i].innerText;
        const utter = new SpeechSynthesisUtterance(text);
        utter.lang = "ca-ES";
        utter.rate = 0.9;
        utter.pitch = 1.0;
        utter.onstart = () => document.querySelector('.robot-icon').classList.add('talking');
        utter.onend = () => {
            document.querySelector('.robot-icon').classList.remove('talking');
            setTimeout(() => parlarVers(i + 1), 200);
        };
        window.speechSynthesis.speak(utter);
        versosEls[i].style.opacity = 1;
    }
    parlarVers(0);
}

// Funció auxiliar per si encara no hi ha poema
function parla(missatge) {
    const utter = new SpeechSynthesisUtterance(missatge);
    utter.lang = "ca-ES";
    window.speechSynthesis.speak(utter);
}

Pas 8: Gestió del resultat del reconeixement

Quan el reconeixement obté un resultat (esdeveniment onresult), recuperem el text transcrit, l'analitzem, actualitzem la interfície amb la puntuació i la classe CSS corresponent, generem el poema i el mostrem al DOM. Tot seguit, amb un petit retard (setTimeout), cridem reprodueixPoema() perquè el robot reciti automàticament el poema.

recognition.onresult = function(e) {
    const text = e.results[0][0].transcript;
    document.getElementById("resultat").innerText = text;

    const puntuacio = analitzarSentiment(text);
    const elemS = document.getElementById("sentiment");
    let cls = "neutral", txt = "😐 Sentiment neutral";
    if (puntuacio > 3) { cls = "positiu"; txt = "✨ Sentiment molt positiu"; }
    else if (puntuacio > 0) { cls = "positiu"; txt = "😊 Sentiment positiu"; }
    else if (puntuacio > -3) { cls = "negatiu"; txt = "😔 Sentiment negatiu"; }
    else { cls = "negatiu"; txt = "😢 Sentiment molt negatiu"; }
    txt += ` (Puntuació: ${puntuacio})`;
    elemS.innerText = txt;
    elemS.className = "sentiment-box " + cls;

    const poema = generarPoema(text, puntuacio);
    poemaActual = poema.prefix + poema.titulo + "\n" + poema.estrofes.flat().join("\n") + "\n\n— Robot Poeta";

    const elemP = document.getElementById("poema");
    elemP.innerHTML = "";
    poema.estrofes.forEach(estrofa => {
        const estDiv = document.createElement("div");
        estDiv.className = "estrofa";
        estrofa.forEach(v => {
            const vDiv = document.createElement("div");
            vDiv.className = "vers";
            vDiv.innerText = v;
            estDiv.appendChild(vDiv);
        });
        elemP.appendChild(estDiv);
    });
    const sig = document.createElement("div");
    sig.className = "signatura";
    sig.innerText = "— Robot Poeta";
    elemP.appendChild(sig);

    // Amaguem tots els versos per a l'animació de lectura
    elemP.querySelectorAll('.vers').forEach(v => v.style.opacity = 0);

    // Reproduir automàticament amb un petit retard
    setTimeout(() => reprodueixPoema(), 300);
};

recognition.onerror = function(e) {
    document.getElementById("estat").innerText = "❌ Error: " + e.error;
};
🎯 El poema es genera al moment, basant-se en la puntuació del sentiment, i es recita immediatament. La icona del robot s'anima durant la lectura.

Pas 9: Codi complet del Robot Poeta

Aquí tens el codi complet. Desa'l en un fitxer .html i obre'l amb un navegador compatible (Chrome, Edge) per provar-lo. Recorda que cal donar permís al micròfon i servir la pàgina via HTTPS o localhost.

<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Robot Poeta Interactiu</title>
<style>
body { font-family:'Georgia',serif; max-width:800px; margin:0 auto; padding:20px; background:#f5f0e6; color:#333;}
h1{text-align:center;color:#5d4037;font-size:2.5em;margin-bottom:10px;}
.subtitle{text-align:center;color:#795548;font-style:italic;margin-bottom:20px;}
.robot-icon{text-align:center;font-size:3em;margin:20px 0;transition:transform 0.3s;}
.robot-icon.talking{transform:scale(1.2) rotate(-5deg);}
button{background:#8d6e63;color:white;border:none;padding:12px 24px;font-size:18px;border-radius:5px;cursor:pointer;display:block;margin:20px auto;font-family:'Georgia',serif;transition:background 0.3s;}
button:hover{background:#6d4c41;}
#estat{padding:12px;border-radius:8px;text-align:center;font-weight:bold;margin:20px 0;font-style:italic;background:#fff3e0;transition:background 0.8s,color 0.8s;}
.box{background:white;border-radius:12px;padding:20px;margin:20px 0;border-left:5px solid #8d6e63;box-shadow:0 4px 10px rgba(0,0,0,0.08);}
.sentiment-box{text-align:center;font-size:1.2em;font-weight:bold;padding:15px;border-radius:10px;margin:20px 0;transition:background 0.8s,color 0.8s,border-color 0.8s;}
.positiu{background:#e8f5e9;color:#2e7d32;border:2px solid #81c784;}
.negatiu{background:#ffebee;color:#c62828;border:2px solid #ef9a9a;}
.neutral{background:#f3e5f5;color:#7b1fa2;border:2px solid #ce93d8;}
.poema{font-style:italic;line-height:1.8;text-align:center;padding:20px;background:#fff;border-radius:12px;border:2px dashed #8d6e63;margin:20px 0;}
.poema .estrofa{margin:15px 0;}
.poema .vers{margin:6px 0;opacity:0;transition:opacity 0.3s;}
.poema .signatura{text-align:right;font-style:normal;font-weight:bold;color:#5d4037;margin-top:20px;}
.controls{text-align:center;margin:20px 0;}
.controls button{display:inline-block;margin:0 10px;padding:10px 20px;}
#estilPoema{margin-top:10px;padding:5px 10px;font-size:16px;}
</style>
</head>
<body>
<div class="robot-icon">🤖✍️</div>
<h1>Robot Poeta Interactiu</h1>
<div class="subtitle">Cada paraula respira mentre la recito</div>
<div class="controls">
    <button onclick="start()">🎤 Parla amb mi</button>
    <button onclick="reprodueixPoema()">🔊 Escolta el poema</button>
    <br>
    <label for="estilPoema">Estil de poema:</label>
    <select id="estilPoema">
        <option value="clàssic">Clàssic</option>
        <option value="modern">Modern</option>
        <option value="minimalista">Minimalista</option>
    </select>
</div>
<div class="box">
    <p><strong>Estat:</strong></p>
    <div id="estat">🌬️ Escoltant el teu cor…</div>
</div>
<div class="box">
    <p><strong>Les teves paraules:</strong></p>
    <div id="resultat">Aquí apareixerà el que diguis...</div>
</div>
<div class="box">
    <p><strong>Anàlisi de sentiments:</strong></p>
    <div class="sentiment-box neutral" id="sentiment">Esperant les teves paraules...</div>
</div>
<div class="box">
    <p><strong>Poema generat:</strong></p>
    <div class="poema" id="poema"></div>
</div>
<script>
const diccionariSentiments={"bo":2,"bé":2,"content":2,"feliç":3,"fantàstic":3,"dolent":-2,"malament":-2,"trist":-2,"horrible":-3,"problema":-1};
let poemaActual="", sintesisVeu=null;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.lang="ca-ES"; recognition.continuous=false; recognition.interimResults=false;
function start(){document.getElementById("estat").innerText="🎧 Escoltant el teu cor…";recognition.start();}
function analitzarSentiment(text){return text.toLowerCase().split(/\s+/).reduce((acc,p)=>acc+(diccionariSentiments[p]||0),0);}
function generarPoema(text,puntuacio){
    let estrofes=[], titulo="", prefix="";
    if(puntuacio>2){titulo="Oda a l'Alegria"; estrofes=[["El sol dansa al teu cor","el vent canta amb tu"],["Riu de llum s'estén","i el teu cor brilla"]]; prefix="☀️ ";}
    else if(puntuacio>0){titulo="Sonet del Benestar"; estrofes=[["La calma flueix","un sospir suau recorre el dia"],["Tot sembla més lleu","el moment és música"]];}
    else if(puntuacio>-1){titulo="Balada del Present"; estrofes=[["Les paraules flueixen suaus","ni gran goig ni clots"],["Equilibri precís","en el silenci amable"]];}
    else if(puntuacio>-3){titulo="Elegia del Desassossec"; estrofes=[["Sento una ombra en el teu dir","fred suau que toca el cor"],["Però la llum espera","torna a guiar el teu pas"]];}
    else{titulo="Càntic de la Melangia"; estrofes=[["Com pluja persistent","la tristesa mulla el parlar"],["Però el sol sempre torna","i amb ell, l'esperança brilla"]]; prefix="🌧️ ";}
    return {prefix,titulo,estrofes};
}
function reprodueixPoema(){
    if(!poemaActual){parla("Encara no he sentit les teves paraules per crear un poema.");return;}
    const versosEls=document.querySelectorAll('.vers');
    let delay=0;
    function parlarVers(i){
        if(i>=versosEls.length) return;
        const text=versosEls[i].innerText;
        const utter=new SpeechSynthesisUtterance(text);
        utter.lang="ca-ES"; utter.rate=0.9; utter.pitch=1.0;
        utter.onstart=()=>document.querySelector('.robot-icon').classList.add('talking');
        utter.onend=()=>{
            document.querySelector('.robot-icon').classList.remove('talking');
            setTimeout(()=>parlarVers(i+1),200);
        };
        window.speechSynthesis.speak(utter);
        versosEls[i].style.opacity=1;
    }
    parlarVers(0);
}
function parla(missatge){
    const utter = new SpeechSynthesisUtterance(missatge);
    utter.lang="ca-ES";
    window.speechSynthesis.speak(utter);
}
recognition.onresult=function(e){
    const text=e.results[0][0].transcript;
    document.getElementById("resultat").innerText=text;
    const puntuacio=analitzarSentiment(text);
    const elemS=document.getElementById("sentiment");
    let cls="neutral", txt="😐 Sentiment neutral";
    if(puntuacio>3){cls="positiu"; txt="✨ Sentiment molt positiu";}
    else if(puntuacio>0){cls="positiu"; txt="😊 Sentiment positiu";}
    else if(puntuacio>-3){cls="negatiu"; txt="😔 Sentiment negatiu";}
    else{cls="negatiu"; txt="😢 Sentiment molt negatiu";}
    txt+=` (Puntuació: ${puntuacio})`;
    elemS.innerText=txt; elemS.className="sentiment-box "+cls;
    const poema=generarPoema(text,puntuacio);
    poemaActual=poema.prefix+poema.titulo+"\n"+poema.estrofes.flat().join("\n")+"\n\n— Robot Poeta";
    const elemP=document.getElementById("poema");
    elemP.innerHTML="";
    poema.estrofes.forEach(estrofa=>{
        const estDiv=document.createElement("div"); estDiv.className="estrofa";
        estrofa.forEach(v=>{
            const vDiv=document.createElement("div"); vDiv.className="vers"; vDiv.innerText=v; estDiv.appendChild(vDiv);
        });
        elemP.appendChild(estDiv);
    });
    const sig=document.createElement("div"); sig.className="signatura"; sig.innerText="— Robot Poeta"; elemP.appendChild(sig);
    elemP.querySelectorAll('.vers').forEach(v=>v.style.opacity=0);
    setTimeout(()=>reprodueixPoema(),300);
};
recognition.onerror=function(e){document.getElementById("estat").innerText="❌ Error: "+e.error;};
</script>
</body>
</html>

🎤 Prova el Robot Poeta aquí mateix!

Fes clic a 🎤 Parla amb mi, digues unes paraules en català (p. ex. "estic molt feliç") i mira com el robot n'analitza el sentiment, genera un poema i el recita. També pots fer clic a 🔊 Escolta el poema per tornar-lo a sentir. Funciona amb micròfon; dona permís quan el navegador ho demani.