C:\
├── tts\
| ├──tts-to-wav.py ← Script Python per generar l’àudio
│ ├── tts-to-wav.ps1 ← Script PowerShell per generar l’àudio
│ └── list-voices.ps1 ← Script PowerShell per llistar veus
└── AppServ\
└── www\
└── tts\
├── index.php ← Interfície web
└── tts.wav ← Fitxer d’àudio generat
Index.php
--------
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Executa PowerShell per obtenir la llista de veus disponibles
$voiceListCmd = "powershell -ExecutionPolicy Bypass -File C:\\tts\\list-voices.ps1";
exec($voiceListCmd, $voices, $voiceStatus);
// Si falla, utilitza una llista per defecte
if ($voiceStatus !== 0 || empty($voices)) {
$voices = [
"SAPI5|Microsoft Helena Desktop",
"SAPI5|Microsoft Hazel Desktop",
"SAPI5|Microsoft Zira Desktop"
];
}else
//-----------provisional fins la sol·lucio definitiva----
{
// Afegim manualment Herena a OneCore (només si no està ja)
$herena = "ONECORE|Microsoft Herena - Catalan (Catalan)";
if (!in_array($herena, $voices)) {
$voices[] = $herena;
}
}
//------fi del provisional ❌ Error generant l’àudio.---
$text = $_GET['text'] ?? '';
$voice = $_GET['voice'] ?? $voices[0]; // 👈 AIXÒ ÉS ON TOCA
$audioFile = '/tts/tts.wav';
$audioTag = '';
if ($text) {
$escapedText = escapeshellarg($text);
$escapedVoice = escapeshellarg($voice);
$escapedOutput = escapeshellarg(__DIR__ . DIRECTORY_SEPARATOR . 'tts.wav');
//$cmd = "powershell -ExecutionPolicy Bypass -File C:\\tts\\tts-to-wav.ps1 -text $escapedText -voiceName $escapedVoice -outputPath $escapedOutput";
$cmd = "python C:\\tts\\tts-to-wav.py $escapedText $escapedVoice $escapedOutput";
exec($cmd, $outputLines, $status);
if ($status === 0 && file_exists(__DIR__ . '/tts.wav')) {
$audioTag = "<audio controls autoplay style='margin-top:20px;'>
<source src=\"$audioFile\" type=\"audio/wav\">
El teu navegador no suporta àudio.
</audio>";
} else {
$audioTag = "<p style='color:red;'>❌ Error generant l’àudio.</p>";
}
}
?>
<!-- Interfície HTML -->
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<title>Conversió TTS</title>
</head>
<body>
<form method="get">
<label for="text">Text:</label><br>
<textarea name="text" rows="4" cols="50"><?= htmlspecialchars($text) ?></textarea><br><br>
<label for="voice">Veu:</label>
<select name="voice">
<?php foreach ($voices as $v): ?>
<option value="<?= htmlspecialchars($v) ?>" <?= $v === $voice ? 'selected' : '' ?>><?= htmlspecialchars($v) ?></option>
<?php endforeach; ?>
</select><br><br>
<button type="submit">Generar àudio</button>
</form>
<?= $audioTag ?>
</body>
</html>
=========
tts-to-wav.ps1 (usarem tts-to-wav.py)
param (
[string]$text = "Hola món",
[string]$voiceName,
[string]$outputPath = "C:\tts\tts.wav"
)
try {
if ($voiceName -like "ONECORE|*") {
$realVoice = $voiceName.Substring(8)
Add-Type -AssemblyName Windows.Media.SpeechSynthesis
$synth = New-Object Windows.Media.SpeechSynthesis.SpeechSynthesizer
$voice = $synth.AllVoices | Where-Object { $_.DisplayName -eq $realVoice }
if (-not $voice) {
Write-Error "Veu '$realVoice' no trobada a OneCore."
exit 1
}
$synth.Voice = $voice
$streamOp = $synth.SynthesizeTextToStreamAsync($text)
$streamOp.Wait()
$stream = $streamOp.GetResults()
# Llegir el stream
$reader = New-Object Windows.Storage.Streams.DataReader($stream)
$reader.LoadAsync($stream.Size).AsTask().Wait()
$data = New-Object byte[] $stream.Size
$reader.ReadBytes($data)
[System.IO.File]::WriteAllBytes($outputPath, $data)
}
elseif ($voiceName -like "SAPI5|*") {
$realVoice = $voiceName.Substring(6)
Add-Type -AssemblyName System.Speech
$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
$synth.SelectVoice($realVoice)
$synth.SetOutputToWaveFile($outputPath)
$synth.Speak($text)
}
else {
Write-Error "Format de veu desconegut: $voiceName"
exit 1
}
}
catch {
Write-Error "Error inesperat: $_"
exit 1
}
========================
tts-to-wav.py
import sys
import os
import pyttsx3
text = sys.argv[1]
voice_name = sys.argv[2]
output_path = sys.argv[3]
engine = pyttsx3.init()
# Buscar la veu exacta
found = False
for voice in engine.getProperty('voices'):
if voice.name.strip() == voice_name.strip():
engine.setProperty('voice', voice.id)
found = True
break
if not found:
print(f"❌ Veu '{voice_name}' no trobada.")
sys.exit(1)
engine.save_to_file(text, output_path)
engine.runAndWait()
print(f"✅ Àudio generat a {output_path}")
======================
list-voices.ps1
# Llista de veus SAPI5
Add-Type -AssemblyName System.Speech
$sapi = New-Object System.Speech.Synthesis.SpeechSynthesizer
$sapiVoices = $sapi.GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }
# Llista de veus OneCore
Add-Type -AssemblyName Windows.Media.SpeechSynthesis
$core = New-Object Windows.Media.SpeechSynthesis.SpeechSynthesizer
$coreVoices = $core.AllVoices | ForEach-Object { $_.DisplayName }
# Sortida combinada amb etiquetes
$sapiVoices | ForEach-Object { "SAPI5|$_" }
$coreVoices | ForEach-Object { "ONECORE|$_" }