<# Text-to-Speech WAV File Generator Uses Windows Speech Platform API to convert text to speech and save as WAV Author: Claude AI from prompts by Mike Shellim. With input from Paul Grey. Requirements ============ Windows 10, 11 (optional) FFMpeg To install this script ====================== 1. Change the file extention of this script file to .ps1 (note numeral '1'). 2. Create a folder to hold script file and output file 3. Right click script file and choose 'Run with Powershell' 4. At the prompt, type in the text to be converted Stripping silences (optional) ============================= The script offers to strip leading and trailing silences, usesful for time critical setups. This feature requires FFmpeg to be installed. https://ffmpeg.org/download.html#build-windows The 'essentials' pack is sufficient Troubleshooting =============== If the script flashes briefly then closes, it means the script has failed. Cause is almost certainly an overly restrictive PowerShell execution policy. This is easy to fix, see: https://powershellfaqs.com/powershell-running-scripts-is-disabled-on-this-system/ Method 2 is recommended as a good balance between utility and security. #> # Load required assemblies Add-Type -AssemblyName System.Speech Add-Type -AssemblyName System.Windows.Forms function Get-InstalledVoices { $synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer $voices = $synthesizer.GetInstalledVoices() $voiceList = @() foreach ($voice in $voices) { $voiceInfo = [PSCustomObject]@{ Name = $voice.VoiceInfo.Name Gender = $voice.VoiceInfo.Gender Age = $voice.VoiceInfo.Age Culture = $voice.VoiceInfo.Culture.Name Enabled = $voice.Enabled } $voiceList += $voiceInfo } $synthesizer.Dispose() return $voiceList } function Show-VoiceMenu { param($voices) Write-Host "`nAvailable Voices:" -ForegroundColor Green Write-Host "=================" -ForegroundColor Green for ($i = 0; $i -lt $voices.Count; $i++) { $voice = $voices[$i] $status = if ($voice.Enabled) { "[Available]" } else { "[Disabled]" } Write-Host "$($i + 1). $($voice.Name) - $($voice.Gender), $($voice.Culture) $status" } Write-Host "" } function Generate-SpeechWav { param( [string]$text, [string]$voiceName, [string]$outputPath ) try { # Create speech synthesizer $synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer # Set the voice $synthesizer.SelectVoice($voiceName) # Set volume to maximum (100) for loudest output without distortion $synthesizer.Volume = 100 # Set speaking rate to normal $synthesizer.Rate = 0 # Configure WAV output with 16kHz sample rate $format = New-Object System.Speech.AudioFormat.SpeechAudioFormatInfo(16000, [System.Speech.AudioFormat.AudioBitsPerSample]::Sixteen, [System.Speech.AudioFormat.AudioChannel]::Mono) $synthesizer.SetOutputToWaveFile($outputPath, $format) # Speak the text (this generates the WAV file) $synthesizer.Speak($text) # Clean up $synthesizer.Dispose() return $true } catch { Write-Error "Error generating speech: $($_.Exception.Message)" if ($synthesizer) { $synthesizer.Dispose() } return $false } } function Test-FFmpegAvailable { try { $null = & ffmpeg -version 2>$null return $true } catch { return $false } } function Strip-AudioSilence { param( [string]$inputPath, [string]$outputPath ) try { # FFmpeg command to strip leading and trailing silence # silencedetect: detect silence periods # silenceremove: remove silence from beginning and end $tempo = 1.0 # atempo value. Alters speed without altering pitch. Value in range 0.5 to 2.0 $silence = 0.01 $ffmpegArgs = @( "-i", $inputPath, "-af", "silenceremove=start_periods=1:start_silence=$silence :start_threshold=-50dB,areverse,silenceremove=start_periods=1:start_silence=$silence :start_threshold=-50dB,areverse,atempo=$tempo,volume=4dB", "-ar", "16000", "-ac", "1", "-y", $outputPath ) Write-Host "Stripping silence using FFmpeg..." -ForegroundColor Yellow & ffmpeg @ffmpegArgs 2>$null if (Test-Path $outputPath) { return $true } else { return $false } } catch { Write-Error "Error stripping silence: $($_.Exception.Message)" return $false } } function Get-SafeFileName { param([string]$text) # Remove invalid filename characters and limit length to 40 characters $safeText = $text -replace '[^\w\s-]', '' -replace '\s+', '_' if ($safeText.Length -gt 40) { $safeText = $safeText.Substring(0, 40) } return $safeText } # Main script execution Write-Host "==================================" -ForegroundColor Cyan Write-Host "Text-to-Speech WAV File Generator" -ForegroundColor Cyan Write-Host "==================================" -ForegroundColor Cyan Write-Host "" # Get available voices Write-Host "Loading available voices..." -ForegroundColor Yellow $voices = Get-InstalledVoices | Where-Object { $_.Enabled -eq $true } if ($voices.Count -eq 0) { Write-Error "No enabled voices found on this system." exit 1 } # Prompt for text input do { $text = Read-Host "`nEnter the text to convert to speech" if ([string]::IsNullOrWhiteSpace($text)) { Write-Host "Text cannot be empty. Please try again." -ForegroundColor Red } } while ([string]::IsNullOrWhiteSpace($text)) # Show voice menu and get selection Show-VoiceMenu -voices $voices do { $voiceSelection = Read-Host "Select a voice (1-$($voices.Count))" if ($voiceSelection -match '^\d+$' -and [int]$voiceSelection -ge 1 -and [int]$voiceSelection -le $voices.Count) { $selectedVoice = $voices[[int]$voiceSelection - 1] break } else { Write-Host "Invalid selection. Please enter a number between 1 and $($voices.Count)." -ForegroundColor Red } } while ($true) # Generate output filename $safeFileName = Get-SafeFileName -text $text $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $outputFileName = "${safeFileName}_${timestamp}.wav" $outputPath = Join-Path -Path (Get-Location) -ChildPath $outputFileName # Display generation info Write-Host "`nGeneration Details:" -ForegroundColor Green Write-Host "==================" -ForegroundColor Green Write-Host "Text: $text" Write-Host "Voice: $($selectedVoice.Name)" Write-Host "Output: $outputFileName" Write-Host "Sample Rate: 16,000 Hz" Write-Host "Format: 16-bit Mono WAV" Write-Host "" # Generate the WAV file Write-Host "Generating speech..." -ForegroundColor Yellow if (Generate-SpeechWav -text $text -voiceName $selectedVoice.Name -outputPath $outputPath) { Write-Host "Success! WAV file generated: $outputFileName" -ForegroundColor Green # Show file info if (Test-Path $outputPath) { $fileInfo = Get-Item $outputPath Write-Host "File size: $([math]::Round($fileInfo.Length / 1KB, 2)) KB" -ForegroundColor Cyan Write-Host "Full path: $($fileInfo.FullName)" -ForegroundColor Cyan # Prompt for silence stripping Write-Host "" $stripSilence = Read-Host "Would you like to strip leading and trailing silences? (y/N)" if ($stripSilence -match '^[Yy]') { # Check if FFmpeg is available if (Test-FFmpegAvailable) { # Create output filename with _short suffix using reliable method $baseName = [System.IO.Path]::GetFileNameWithoutExtension($outputFileName) $shortFileName = "${baseName}_short.wav" $shortOutputPath = Join-Path -Path (Get-Location) -ChildPath $shortFileName if (Strip-AudioSilence -inputPath $outputPath -outputPath $shortOutputPath) { Write-Host "Success! Silence-stripped - file generated: $shortFileName" -ForegroundColor Green # Show comparison info $shortFileInfo = Get-Item $shortOutputPath Write-Host "Original file size: $([math]::Round($fileInfo.Length / 1KB, 2)) KB" -ForegroundColor Cyan Write-Host "Stripped file size: $([math]::Round($shortFileInfo.Length / 1KB, 2)) KB" -ForegroundColor Cyan $savings = [math]::Round((($fileInfo.Length - $shortFileInfo.Length) / $fileInfo.Length) * 100, 1) Write-Host "Size reduction: $savings%" -ForegroundColor Cyan } else { Write-Host "Failed to strip silence from audio file." -ForegroundColor Red } } else { Write-Host "FFmpeg is not available on this system." -ForegroundColor Red Write-Host "Please install FFmpeg to use the silence stripping feature." -ForegroundColor Yellow Write-Host "You can download FFmpeg from: https://ffmpeg.org/download.html" -ForegroundColor Yellow } } # Ask if user wants to play the file $play = Read-Host "`nWould you like to play the generated file? (y/N)" if ($play -match '^[Yy]') { try { Start-Process -FilePath $outputPath -Wait:$false Write-Host "Playing audio file..." -ForegroundColor Green } catch { Write-Host "Could not play the file automatically. You can play it manually: $outputFileName" -ForegroundColor Yellow } } } } else { Write-Host "Failed to generate WAV file." -ForegroundColor Red exit 1 } Write-Host "`nScript completed successfully!" -ForegroundColor Green