虽然RSS的内容已经是聚合精简过的,但有时还是不方便,比如开车、睡前等特殊场景下如果有语音播报,是不是更加完美呢?说干就干。
几个大厂比较了下,Goolge有网络门槛,OpenAI 免费额度太低,最终还是选了
Azure AI 的Speech Studio
image-1728571609805

每月额度50万字符

image-1728572074045

API 服务,这里的路径的坑要注意区分大小写,否则前端会报一个误导性提示(Failed to load because no supported source was found.),而导致无法播放。
public async Task<IActionResult> playArticle(FeedItem textToRead)
{
    IActionResult response = Unauthorized();
    try
    { 
        var fileName = $"{textToRead.Id}.mp3";
        var filePath = Path.Combine("/app/Audio", fileName); 
        if (System.IO.File.Exists(filePath))
        {
            var audioUrl = $"/Audio/{fileName}";
            return Ok(new { url = audioUrl });
        }
        string speechKey = _configuration["MSTTS:SpeechKey"];
        string speechRegion = _configuration["MSTTS:SpeechRegion"];
        var speechConfig = SpeechConfig.FromSubscription(speechKey, speechRegion);

        speechConfig.SpeechSynthesisVoiceName = "zh-CN-XiaoxiaoNeural"; // 选择中文语音 
        speechConfig.SetSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3);

        using (var speechSynthesizer = new SpeechSynthesizer(speechConfig))
        {
            var speechSynthesisResult = await speechSynthesizer.SpeakTextAsync(textToRead.Title+ textToRead.Excerpt);
            if (speechSynthesisResult.Reason == ResultReason.SynthesizingAudioCompleted)
            {    
                await System.IO.File.WriteAllBytesAsync(filePath, speechSynthesisResult.AudioData); 
                var audioUrl = $"/Audio/{fileName}";
                return Ok(new { url = audioUrl });
            }
            else
            {
                return BadRequest($"Speech synthesis failed. Reason: {speechSynthesisResult.Reason}");
            }
        }
    }
    catch (Exception ex)
    {
        return BadRequest($"Speech synthesis failed. Reason: {ex.Message}");
    }
}
Nginx ,增加路径映射
location /Audio/ {
        alias /app/Audio/;
        add_header 'Access-Control-Allow-Origin' '*';
        autoindex on;
    }



前端调用时把文章传过来,就可以返回自动播放的路径。
async playArticle(article) { 
        const token = localStorage.getItem("userToken") 
        if (!token) { 
          this.$router.push('/login');
          return;
        }
        try {
          const response = await apiService.playArticle(article, this.setLoading,token);
          const audioFilePath = response.url; 
          this.$refs.audioPlayer.src = audioFilePath;  
          this.$refs.audioPlayer.play(); 
        } catch (error) {
          console.error("播放音频时发生错误:", error);
        } 
      },

image-1728572862414