Conversor de text a veu
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<title>Conversor de text a veu</title>
<style>
body {
font-family: sans-serif;
padding: 2em;
background-color: #f4f4f4;
}
input {
font-size: 1.2em;
padding: 0.5em;
margin-top: 1em;
width: 100%;
max-width: 500px;
}
#speakBtn {
font-size: 2em;
background: none;
border: none;
cursor: pointer;
margin-top: 1em;
}
#speakBtn:hover {
color: #0078D7;
}
</style>
</head>
<body>
<h2>Conversor de text a veu</h2>
<input type="text" id="cercar" placeholder="Escriu el text que vols escoltar">
<button id="speakBtn" title="Escolta el text"></button>
<script>
document.addEventListener('DOMContentLoaded', () => {
const inputElement = document.getElementById('cercar');
const speakBtn = document.getElementById('speakBtn');
let voices = [];
function loadVoices() {
voices = speechSynthesis.getVoices();
}
speechSynthesis.onvoiceschanged = loadVoices;
loadVoices();
function speakText() {
const text = inputElement.value.trim();
if (!text) return;
const utterance = new SpeechSynthesisUtterance(text);
const zira = voices.find(v => v.name === 'Microsoft Zira - English (United States)');
if (zira) {
utterance.voice = zira;
utterance.lang = zira.lang;
}
utterance.rate = 1;
utterance.pitch = 1;
speechSynthesis.cancel();
speechSynthesis.speak(utterance);
}
speakBtn.addEventListener('click', speakText);
// Repetir cada 10 segons
setInterval(speakText, 10000);
});
</script>
</body>
</html>
Aquest script, mosta les veus que admet el navegador
```html
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<title>Conversor de text a veu</title>
<style>
body {
font-family: sans-serif;
padding: 2em;
background-color: #f4f4f4;
}
input {
font-size: 1.2em;
padding: 0.5em;
margin-top: 1em;
width: 100%;
max-width: 500px;
box-sizing: border-box;
}
select {
font-size: 1.1em;
padding: 0.5em;
margin-top: 1em;
width: 100%;
max-width: 500px;
}
#languageInfo {
margin-top: 1em;
padding: 0.7em;
background: white;
border: 1px solid #ccc;
border-radius: 6px;
max-width: 500px;
}
#speakBtn {
font-size: 2em;
background: none;
border: none;
cursor: pointer;
margin-top: 1em;
}
#speakBtn:hover {
color: #0078D7;
}
#status {
margin-top: 0.8em;
font-size: 0.95em;
color: #555;
}
</style>
</head>
<body>
<h2>Conversor de text a veu</h2>
<input
type="text"
id="cercar"
placeholder="Escriu el text que vols escoltar"
>
<br>
<select id="voiceSelect">
<option value="">Carregant veus...</option>
</select>
<div id="languageInfo">
<strong>Llengua carregada:</strong>
<span id="currentLanguage">--</span>
</div>
<div id="status"></div>
<button id="speakBtn" title="Escolta el text">🔊</button>
<script>
document.addEventListener('DOMContentLoaded', () => {
const inputElement = document.getElementById('cercar');
const speakBtn = document.getElementById('speakBtn');
const voiceSelect = document.getElementById('voiceSelect');
const currentLanguage = document.getElementById('currentLanguage');
const status = document.getElementById('status');
let voices = [];
/*
* Carrega totes les veus que Firefox posa a disposició
*/
function loadVoices() {
voices = speechSynthesis.getVoices();
console.log("Veus disponibles:", voices);
voiceSelect.innerHTML = '';
if (voices.length === 0) {
const option = document.createElement('option');
option.textContent = 'No s’han trobat veus';
voiceSelect.appendChild(option);
currentLanguage.textContent = 'Cap veu disponible';
return;
}
/*
* Ordenem per llengua i després per nom
*/
voices.sort((a, b) => {
if (a.lang !== b.lang) {
return a.lang.localeCompare(b.lang);
}
return a.name.localeCompare(b.name);
});
/*
* Afegim totes les veus al desplegable
*/
voices.forEach((voice, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent =
`${voice.lang} — ${voice.name}`;
voiceSelect.appendChild(option);
});
/*
* Busquem primer una veu catalana
*
* ca
* ca-ES
* ca-ES-...
*/
let catalanIndex = voices.findIndex(voice =>
voice.lang.toLowerCase() === 'ca' ||
voice.lang.toLowerCase().startsWith('ca-')
);
/*
* Si existeix català, el seleccionem
*/
if (catalanIndex !== -1) {
voiceSelect.value = catalanIndex;
updateLanguage(catalanIndex);
status.textContent =
'✓ S’ha detectat una veu catalana.';
} else {
/*
* Si no hi ha català, agafem la primera veu
*/
voiceSelect.value = 0;
updateLanguage(0);
status.textContent =
'⚠ Firefox no ha detectat cap veu catalana.';
}
}
/*
* Actualitza el botó/informació de llengua
*/
function updateLanguage(index) {
const voice = voices[index];
if (!voice) {
currentLanguage.textContent = '--';
return;
}
currentLanguage.textContent =
`${voice.lang} — ${voice.name}`;
}
/*
* Quan l'usuari canvia la veu
*/
voiceSelect.addEventListener('change', () => {
updateLanguage(voiceSelect.value);
});
/*
* Parla el text
*/
function speakText() {
const text = inputElement.value.trim();
if (!text) return;
const selectedIndex = parseInt(voiceSelect.value);
const voice = voices[selectedIndex];
if (!voice) {
status.textContent =
'No hi ha cap veu seleccionada.';
return;
}
const utterance =
new SpeechSynthesisUtterance(text);
/*
* Utilitzem EXACTAMENT la veu seleccionada
*/
utterance.voice = voice;
utterance.lang = voice.lang;
utterance.rate = 1;
utterance.pitch = 1;
speechSynthesis.cancel();
speechSynthesis.speak(utterance);
/*
* Mostrem què està parlant realment
*/
currentLanguage.textContent =
`${voice.lang} — ${voice.name}`;
}
/*
* Botó de reproducció
*/
speakBtn.addEventListener('click', speakText);
/*
* Les veus poden arribar de manera asíncrona
* a Firefox.
*/
speechSynthesis.onvoiceschanged = loadVoices;
loadVoices();
/*
* Repetir cada 10 segons
*/
setInterval(speakText, 10000);
});
</script>
</body>
</html>
```
Sí. El problema principal del teu codi és que forces Microsoft Zira en anglès, així que Firefox no té cap possibilitat d’escollir català.
Et proposo aquesta versió:
- mostra en un botó quina veu/llengua està carregada;
- afegeix un desplegable amb totes les veus que Firefox detecti;
- permet seleccionar manualment la veu;
- indica si Català (
ca) està disponible; - si hi ha una veu catalana, la selecciona automàticament;
- manté la reproducció cada 10 segons.
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<title>Conversor de text a veu</title>
<style>
body {
font-family: sans-serif;
padding: 2em;
background-color: #f4f4f4;
}
input {
font-size: 1.2em;
padding: 0.5em;
margin-top: 1em;
width: 100%;
max-width: 500px;
box-sizing: border-box;
}
select {
font-size: 1.1em;
padding: 0.5em;
margin-top: 1em;
width: 100%;
max-width: 500px;
}
#languageInfo {
margin-top: 1em;
padding: 0.7em;
background: white;
border: 1px solid #ccc;
border-radius: 6px;
max-width: 500px;
}
#speakBtn {
font-size: 2em;
background: none;
border: none;
cursor: pointer;
margin-top: 1em;
}
#speakBtn:hover {
color: #0078D7;
}
#status {
margin-top: 0.8em;
font-size: 0.95em;
color: #555;
}
</style>
</head>
<body>
<h2>Conversor de text a veu</h2>
<input
type="text"
id="cercar"
placeholder="Escriu el text que vols escoltar"
>
<br>
<select id="voiceSelect">
<option value="">Carregant veus...</option>
</select>
<div id="languageInfo">
<strong>Llengua carregada:</strong>
<span id="currentLanguage">--</span>
</div>
<div id="status"></div>
<button id="speakBtn" title="Escolta el text">🔊</button>
<script>
document.addEventListener('DOMContentLoaded', () => {
const inputElement = document.getElementById('cercar');
const speakBtn = document.getElementById('speakBtn');
const voiceSelect = document.getElementById('voiceSelect');
const currentLanguage = document.getElementById('currentLanguage');
const status = document.getElementById('status');
let voices = [];
/*
* Carrega totes les veus que Firefox posa a disposició
*/
function loadVoices() {
voices = speechSynthesis.getVoices();
console.log("Veus disponibles:", voices);
voiceSelect.innerHTML = '';
if (voices.length === 0) {
const option = document.createElement('option');
option.textContent = 'No s’han trobat veus';
voiceSelect.appendChild(option);
currentLanguage.textContent = 'Cap veu disponible';
return;
}
/*
* Ordenem per llengua i després per nom
*/
voices.sort((a, b) => {
if (a.lang !== b.lang) {
return a.lang.localeCompare(b.lang);
}
return a.name.localeCompare(b.name);
});
/*
* Afegim totes les veus al desplegable
*/
voices.forEach((voice, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent =
`${voice.lang} — ${voice.name}`;
voiceSelect.appendChild(option);
});
/*
* Busquem primer una veu catalana
*
* ca
* ca-ES
* ca-ES-...
*/
let catalanIndex = voices.findIndex(voice =>
voice.lang.toLowerCase() === 'ca' ||
voice.lang.toLowerCase().startsWith('ca-')
);
/*
* Si existeix català, el seleccionem
*/
if (catalanIndex !== -1) {
voiceSelect.value = catalanIndex;
updateLanguage(catalanIndex);
status.textContent =
'✓ S’ha detectat una veu catalana.';
} else {
/*
* Si no hi ha català, agafem la primera veu
*/
voiceSelect.value = 0;
updateLanguage(0);
status.textContent =
'⚠ Firefox no ha detectat cap veu catalana.';
}
}
/*
* Actualitza el botó/informació de llengua
*/
function updateLanguage(index) {
const voice = voices[index];
if (!voice) {
currentLanguage.textContent = '--';
return;
}
currentLanguage.textContent =
`${voice.lang} — ${voice.name}`;
}
/*
* Quan l'usuari canvia la veu
*/
voiceSelect.addEventListener('change', () => {
updateLanguage(voiceSelect.value);
});
/*
* Parla el text
*/
function speakText() {
const text = inputElement.value.trim();
if (!text) return;
const selectedIndex = parseInt(voiceSelect.value);
const voice = voices[selectedIndex];
if (!voice) {
status.textContent =
'No hi ha cap veu seleccionada.';
return;
}
const utterance =
new SpeechSynthesisUtterance(text);
/*
* Utilitzem EXACTAMENT la veu seleccionada
*/
utterance.voice = voice;
utterance.lang = voice.lang;
utterance.rate = 1;
utterance.pitch = 1;
speechSynthesis.cancel();
speechSynthesis.speak(utterance);
/*
* Mostrem què està parlant realment
*/
currentLanguage.textContent =
`${voice.lang} — ${voice.name}`;
}
/*
* Botó de reproducció
*/
speakBtn.addEventListener('click', speakText);
/*
* Les veus poden arribar de manera asíncrona
* a Firefox.
*/
speechSynthesis.onvoiceschanged = loadVoices;
loadVoices();
/*
* Repetir cada 10 segons
*/
setInterval(speakText, 10000);
});
</script>
</body>
</html>
Què veurem ara
El desplegable et mostrarà coses semblants a:
ca-ES — Veu catalana de-DE — German ... en-US — Microsoft Zira ... en-US — ... es-ES — Spanish ... fr-FR — French ...
I a sota:
Llengua carregada: ca-ES — NOM_DE_LA_VEU
Això és especialment útil per al que estem investigant amb Firefox + Speech Dispatcher + Piper/Ona, perquè podrem veure si Firefox realment rep una veu catalana o simplement no la té disponible.
Una cosa important: que Linux tingui una veu catalana instal·lada no vol dir necessàriament que Firefox la vegi a speechSynthesis.getVoices(). Aquest és precisament el test que faria amb aquesta pàgina.
Si vols, el següent pas pot ser fer que el desplegable mostri només les llengües (Català, Espanyol, Anglès, etc.) i, en seleccionar-ne una, triï automàticament la millor veu disponible.
Sí. Ona és un model de veu que es guarda com a fitxers, principalment:
ca_ES-upc_ona-medium.onnx ca_ES-upc_ona-medium.onnx.json
El .onnx és el model neuronal de la veu i el .json conté la configuració/metadades necessàries. Piper funciona precisament amb aquesta parella de fitxers. (GitHub)
I sí, hi ha veus Piper per a molts idiomes. El repositori oficial de veus actual indica 35 idiomes, i inclou català, castellà, anglès, francès, alemany, italià, portuguès, neerlandès, rus, xinès, japonès? etc.; però no tots els idiomes del món tenen una veu Piper disponible. (Hugging Face)
Per exemple, actualment tenim:
| Llengua | Veus Piper |
|---|---|
| 🇨🇦 Català | upc_ona, upc_pau |
| 🇪🇸 Castellà | carlfm, davefx, mls, sharvard… |
| 🇬🇧 Anglès UK | alan, alba, cori… |
| 🇺🇸 Anglès US | amy, lessac, ryan, sam… |
| 🇫🇷 Francès | gilles, siwis, tom, upmc… |
| 🇩🇪 Alemany | thorsten, karlsson, ramona… |
| 🇮🇹 Italià | paola, riccardo |
| 🇵🇹 Portuguès | tugão, cadu, edresson… |
| 🇳🇱 Neerlandès | diverses |
| 🇷🇺 Rus | denis, dmitri, irina, ruslan |
| 🇨🇳 Xinès | huayan |
La llista oficial mostra que Català (ca_ES) està molt ben representat, amb upc_ona i upc_pau. (GitHub)
I això encaixa molt bé amb el que estàvem parlant del Synology
Podríem tenir, per exemple:
/volume2/web/00-sapi_tts/
│
├── voices/
│ ├── ca_ES-upc_ona-medium.onnx
│ ├── ca_ES-upc_ona-medium.onnx.json
│ ├── es_ES-davefx-medium.onnx
│ ├── es_ES-davefx-medium.onnx.json
│ ├── en_US-ryan-medium.onnx
│ ├── en_US-ryan-medium.onnx.json
│ └── ...
│
└── servidor TTS
I el nostre servidor podria tenir un menú:
┌─────────────────────────────┐ │ Veu: │ │ │ │ ▼ Català — Ona │ │ Català — Pau │ │ Castellà — DaveFX │ │ Castellà — Sharvard │ │ English US — Ryan │ │ English US — Amy │ │ Français — Tom │ │ Deutsch — Thorsten │ └─────────────────────────────┘
Aquesta és probablement la millor arquitectura per al teu TTS de la intranet. No depenem de les veus d’Edge/Firefox ni d’Internet: els models .onnx queden al Synology i Piper el
Sí. La diferència que veus entre Windows 11 i Deepin/Linux és real, i en el teu cas explica perfectament el missatge:
⚠️ Firefox no ha detectat cap veu catalana.
Windows 11
A Windows, Firefox pot accedir a les veus TTS disponibles al sistema, com la que veus a la captura:
Microsoft Zira Desktop - English (United States)
Per això el teu JavaScript speechSynthesis.getVoices() rep una llista de veus i el selector les pot mostrar.
Deepin/Linux
Aquí Firefox funciona d’una manera diferent. Firefox per a Linux no porta un conjunt propi de veus. La seva implementació de Web Speech synthesis utilitza Speech Dispatcher com a intermediari del sistema. Mozilla ho documenta explícitament. (Bugzilla)
El camí és aproximadament:
Firefox
↓
Web Speech API
↓
Speech Dispatcher
↓
mòdul TTS
↓
veuI aquí apareix el problema interessant que ja estàvem treballant:
Firefox
↓
Speech Dispatcher
↓
Piper
↓
OnaAquesta arquitectura és perfectament possible, però Firefox no necessàriament veu automàticament totes les veus que tu tens instal·lades a Piper.
De fet, Mozilla té fins i tot una incidència específica sobre això: Piper està instal·lat com a mòdul de Speech Dispatcher però Firefox no incorpora les veus Piper a speechSynthesis.getVoices(). (Bugzilla)
I això explica exactament el que hem estat fent
Tu ja tens:
spd-say -Li hi apareixen centenars/milers de veus.
Això significa que:
Speech Dispatcher sí que té veus.
Però el teu navegador està dient:
Firefox → speechSynthesis.getVoices()
↓
[]Per tant, el problema no és que Deepin no tingui veus.
És que hi ha una diferència entre:
veus disponibles per al sistemai
veus que Firefox Linux exposa a la Web Speech APIMozilla reconeix precisament aquesta fragilitat de la implementació Linux. (Bugzilla)
Això també explica una cosa molt important
La nostra infraestructura:
Firefox → Speech Dispatcher → Piper → Onasí que pot funcionar per fer parlar Firefox, però hem de superar la capa de detecció de veus de speechSynthesis.
Per això jo faria ara una prova molt concreta al teu Deepin:
speechSynthesis.getVoices().map(v => ({
name: v.name,
lang: v.lang,
localService: v.localService
}))Si retorna [], ja tenim confirmat que el problema està entre Firefox i Speech Dispatcher, no entre Piper i Ona.
I això és important perquè no cal abandonar Piper/Ona ni tornar a eSpeak. Podem intentar fer que Firefox vegi explícitament la veu que ja tens funcionant.
Incidència de Mozilla sobre Piper i Web Speech API
s utilitza localment.I una cosa important: no cal instal·lar totes les veus. Podem començar amb, per exemple, Ona + una veu castellana + anglès + francès + alemany, i anar afegint-ne quan les necessitis.
Sí. La diferència que veus entre Windows 11 i Deepin/Linux és real, i en el teu cas explica perfectament el missatge:
⚠️ Firefox no ha detectat cap veu catalana.
Windows 11
A Windows, Firefox pot accedir a les veus TTS disponibles al sistema, com la que veus a la captura:
Microsoft Zira Desktop - English (United States)
Per això el teu JavaScript speechSynthesis.getVoices() rep una llista de veus i el selector les pot mostrar.
Deepin/Linux
Aquí Firefox funciona d’una manera diferent. Firefox per a Linux no porta un conjunt propi de veus. La seva implementació de Web Speech synthesis utilitza Speech Dispatcher com a intermediari del sistema. Mozilla ho documenta explícitament.
El camí és aproximadament:
Firefox ↓ Web Speech API ↓ Speech Dispatcher ↓ mòdul TTS ↓ veu
I aquí apareix el problema interessant que ja estàvem treballant:
Firefox ↓ Speech Dispatcher ↓ Piper ↓ Ona
Aquesta arquitectura és perfectament possible, però Firefox no necessàriament veu automàticament totes les veus que tu tens instal·lades a Piper.
De fet, Mozilla té fins i tot una incidència específica sobre això: Piper està instal·lat com a mòdul de Speech Dispatcher però Firefox no incorpora les veus Piper a speechSynthesis.getVoices().
I això explica exactament el que hem estat fent
Tu ja tens:
spd-say -L
i hi apareixen centenars/milers de veus.
Això significa que:
Speech Dispatcher sí que té veus.
Però el teu navegador està dient:
Firefox → speechSynthesis.getVoices()
↓
[]
Per tant, el problema no és que Deepin no tingui veus.
És que hi ha una diferència entre:
veus disponibles per al sistema
i
veus que Firefox Linux exposa a la Web Speech API
Mozilla reconeix precisament aquesta fragilitat de la implementació Linux.
Això també explica una cosa molt important
La nostra infraestructura:
Firefox → Speech Dispatcher → Piper → Ona
sí que pot funcionar per fer parlar Firefox, però hem de superar la capa de detecció de veus de speechSynthesis.
Per això jo faria ara una prova molt concreta al teu Deepin:
speechSynthesis.getVoices().map(v => ({
name: v.name,
lang: v.lang,
localService: v.localService
}))
Si retorna [], ja tenim confirmat que el problema està entre Firefox i Speech Dispatcher, no entre Piper i Ona.
I això és important perquè no cal abandonar Piper/Ona ni tornar a eSpeak. Podem intentar fer que Firefox vegi explícitament la veu que ja tens funcionant.