Private/VoiceOutput.ps1

<#
.SYNOPSIS
    Basic Text-to-Speech helper leveraging Azure Speech (if available) or Windows SAPI.
.DESCRIPTION
    Invoke-PSCopilotVoiceOutput will speak text.
      Priority order:
        1. Azure Speech (env AZ_SPEECH_KEY + AZ_SPEECH_REGION) synthesizes to a WAV then plays (needs default Windows audio device).
        2. Windows SAPI.SpVoice COM as quick fallback.
.NOTES
    For Azure Speech you need:
      Set-Item Env:AZ_SPEECH_KEY "<your key>"
      Set-Item Env:AZ_SPEECH_REGION "<region>"
    Optionally set AZ_SPEECH_VOICE (e.g. en-US-JennyNeural). Defaults to en-US-JennyNeural.
#>

function Invoke-PSCopilotVoiceOutput {
    [CmdletBinding()] param(
        [Parameter(Mandatory, Position=0, ValueFromPipeline)] [string] $Text,
        [ValidateSet('auto','azure','local')] [string] $Engine = 'auto',
        [string] $OutFile,
        [switch] $PassThru
    )
    begin { $all = @() }
    process { if ($Text) { $all += $Text } }
    end {
        if (-not $all) { return }
        $fullText = ($all -join ' ')
        $haveAzure = $env:AZ_SPEECH_KEY -and $env:AZ_SPEECH_REGION
        if ($Engine -eq 'azure' -or ($Engine -eq 'auto' -and $haveAzure)) {
            $voice = if ($env:AZ_SPEECH_VOICE) { $env:AZ_SPEECH_VOICE } else { 'en-US-JennyNeural' }
            $ssml = "<speak version='1.0' xml:lang='en-US'><voice name='$voice'>$( [System.Web.HttpUtility]::HtmlEncode($fullText) )</voice></speak>"
            try {
                $endpoint = "https://$($env:AZ_SPEECH_REGION).tts.speech.microsoft.com/cognitiveservices/v1"
                $headers = @{ 'Ocp-Apim-Subscription-Key' = $env:AZ_SPEECH_KEY; 'Content-Type' = 'application/ssml+xml'; 'X-Microsoft-OutputFormat'='riff-16khz-16bit-mono-pcm' }
                $bytes = Invoke-RestMethod -Uri $endpoint -Method POST -Headers $headers -Body $ssml -ErrorAction Stop
                if (-not $OutFile) { $OutFile = Join-Path $env:TEMP ("pscopilot_tts_" + [guid]::NewGuid().ToString() + '.wav') }
                [IO.File]::WriteAllBytes($OutFile, $bytes)
                try { Add-Type -AssemblyName System.Media -ErrorAction SilentlyContinue; (New-Object System.Media.SoundPlayer $OutFile).PlaySync() } catch { Write-Verbose "Playback failed: $_" }
                if ($PassThru) { return $OutFile }
                return
            } catch { Write-Warning "Azure TTS failed: $_. Falling back to local." }
        }
        # Local fallback
        try {
            $voiceObj = New-Object -ComObject SAPI.SpVoice
            $voiceObj.Speak($fullText) | Out-Null
            if ($PassThru) { return $fullText }
        } catch { Write-Error "Local TTS failed: $_" }
    }
}

Export-ModuleMember -Function Invoke-PSCopilotVoiceOutput