讯飞语音转文本:定位阅读进度与高亮文本的技术实现

讯飞语音转文本:定位阅读进度与高亮文本的技术实现

前言

在现代应用中,语音转文本技术已成为提升用户体验的关键技术。本文将深入探讨如何基于讯飞语音API实现精准的语音转文本功能,并通过AudioTextMapping数据表实现阅读进度定位和文本高亮显示。

技术架构概览

核心组件

  1. 讯飞语音转文本服务 - 负责音频识别和文本转换
  2. AudioTextMapping数据表 - 存储音频与文本的映射关系
  3. 流式TTS时间戳处理 - 实现精确的音频文本同步
  4. 前端高亮显示 - 基于时间戳的实时文本高亮

数据流程图

graph TD A[音频输入] --> B[讯飞语音识别] B --> C[文本转换] C --> D[时间戳提取] D --> E[AudioTextMapping存储] E --> F[前端播放器] F --> G[实时高亮显示] G --> H[阅读进度追踪]

AudioTextMapping数据表设计

表结构定义

csharp 复制代码
/// <summary>
/// 音频文本映射表,用于记录音频位置和文本的关系
/// </summary>
public class AudioTextMapping
{
    /// <summary>
    /// 主键ID
    /// </summary>
    [Key]
    public int Id { get; set; }

    /// <summary>
    /// 故事ID
    /// </summary>
    [Required]
    public int StoryId { get; set; }

    /// <summary>
    /// 关联的配音ID
    /// </summary>
    public Guid? StoryVoiceId { get; set; }

    /// <summary>
    /// 文本内容
    /// </summary>
    [Required]
    public string TextContent { get; set; }

    /// <summary>
    /// 文本开始位置(字节)
    /// </summary>
    [Required]
    public int TextStartByte { get; set; }

    /// <summary>
    /// 文本结束位置(字节)
    /// </summary>
    [Required]
    public int TextEndByte { get; set; }

    /// <summary>
    /// 音频URL或Base64编码
    /// </summary>
    public string AudioData { get; set; }

    /// <summary>
    /// 音频开始时间(秒)
    /// </summary>
    [Column(TypeName = "decimal(18,10)")]
    public decimal? AudioStartTime { get; set; }

    /// <summary>
    /// 音频结束时间(秒)
    /// </summary>
    [Column(TypeName = "decimal(18,10)")]
    public decimal? AudioEndTime { get; set; }

    /// <summary>
    /// 合成进度,当前合成文本的字节数
    /// </summary>
    public int? Ced { get; set; }

    /// <summary>
    /// 会话ID
    /// </summary>
    public string Sid { get; set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    /// <summary>
    /// 更新时间
    /// </summary>
    public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

关键字段说明

  • TextStartByte/TextEndByte: 文本在完整内容中的字节位置,用于精确定位文本段落
  • AudioStartTime/AudioEndTime: 对应音频片段的时间范围(秒),支持10位小数精度的精确时间同步
  • Ced: 讯飞TTS返回的合成进度,表示当前合成文本的字节数,用于精确映射
  • Sid: 讯飞会话ID,用于标识同一次TTS合成会话
  • AudioData: 音频URL或Base64编码的音频数据
  • StoryVoiceId: 关联的配音ID,支持多种配音版本

AudioStartTime 和 AudioEndTime 计算方式详解

精准计算每个分段音频的 AudioStartTimeAudioEndTime 是实现文本同步高亮的核心。技术实现遵循以下流程:

1. 解析讯飞流式响应数据 (cedaudio)

当向讯飞语音合成服务请求流式TTS时,服务会通过WebSocket返回一系列JSON消息。每个消息代表一个处理片段,核心数据是 cedaudio

  • 数据结构示例

    json 复制代码
    // 单条消息示例
    {
      "code": 0,
      "message": "success",
      "sid": "tts000f1d29@dx1773a0b8d140000100",
      "data": {
        "status": 1, // 1表示合成中,2表示合成结束
        "ced": "15", // Character End aea: 已处理的文本字节数
        "audio": "... (Base64编码的MP3音频数据) ..."
      }
    }
  • 关键字段解析

    • ced: 表示到当前这个数据帧为止,讯飞引擎已经处理了原始文本中的多少个字节。这是一个累积值。
    • audio: Base64编码的MP3音频数据片段,对应本次 ced 增量所覆盖的文本内容。
  • 处理流程 :后端服务在请求过程中会收集所有返回的JSON消息。TTS结束后,ProcessIFlytekTimestampsBatchAsync 方法会遍历这些消息,通过 ced 的变化来切分文本,并将每个文本片段与对应的 audio 数据关联起来,形成一个个独立的"分段"。

2. 从文件头获取比特率

对于每个分段的 audio 数据(解码后的MP3字节流),系统会读取其文件头部信息。通过解析MP3的帧同步字(Frame Sync)及相关元数据,可以直接确定该音频的比特率(Bitrate)。

3. 根据比特率和字节长度计算时长

获取到精确的比特率后,结合音频分段解码后的真实字节长度,使用以下核心公式计算每个分段的播放时长。为保证精度,计算全程采用 decimal 高精度类型。

  • 核心公式

    csharp 复制代码
    // segment.Audio 是Base64编码的音频字符串
    var decodedAudioBytes = Convert.FromBase64String(segment.Audio);
    
    // 应用公式:时长(秒) = (字节数 * 8) / 比特率(bps)
    decimal segmentDuration = (decimal)decodedAudioBytes.Length * 8 / bitRate;
  • 字节长度说明 :公式中的字节长度是Base64字符串解码后的原始二进制数据长度,通过 Convert.FromBase64String(base64String).Length 获取,这避免了因Base64编码本身带来的体积膨胀(约33%)所导致的计算错误。

4. 时间戳的累积生成

在计算出每个分段的精确时长后,系统通过累积的方式生成时间戳:

  • 第一个分段的 AudioStartTime 初始化为 0
  • 后续每个分段的 AudioStartTime 是其前面所有分段 segmentDuration 的总和。
  • 每个分段的 AudioEndTime 则等于其自身的 AudioStartTime + segmentDuration

讯飞语音转文本实现

1. 语音识别服务

csharp 复制代码
public class IFlytekSpeechToTextProvider : ISpeechToTextProvider
{
    private readonly string _appId;
    private readonly string _apiKey;
    private readonly string _apiSecret;

    public async Task<SpeechToTextResponseDto> ConvertSpeechToTextAsync(
        SpeechToTextRequestDto request)
    {
        // 处理音频格式
        var processedAudio = ProcessAudioFormat(request.AudioData, request.AudioFormat);
        
        // 生成WebSocket连接URL
        var url = GenerateWebSocketUrl("/v2/iat");
        
        using var webSocket = new ClientWebSocket();
        await webSocket.ConnectAsync(new Uri(url), CancellationToken.None);
        
        // 发送音频数据并接收识别结果
        var result = await ProcessWebSocketRecognition(webSocket, processedAudio);
        
        return result;
    }
    
    private string GenerateWebSocketUrl(string path)
    {
        var host = "iat-api.xfyun.cn";
        var date = DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'");
        
        // 构建签名
        var signatureOrigin = $"host: {host}\ndate: {date}\nGET {path} HTTP/1.1";
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret));
        var signature = Convert.ToBase64String(
            hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureOrigin)));
        
        // 构建认证头
        var authorizationOrigin = 
            $"api_key=\"{_apiKey}\", algorithm=\"hmac-sha256\", " +
            $"headers=\"host date request-line\", signature=\"{signature}\"";
        var authorization = Convert.ToBase64String(
            Encoding.UTF8.GetBytes(authorizationOrigin));
        
        return $"wss://{host}{path}?authorization={authorization}&date={Uri.EscapeDataString(date)}&host={host}";
    }
}

2. 流式TTS时间戳处理

csharp 复制代码
public async Task<List<AudioTextMapping>> ProcessIFlytekTimestampsBatchAsync(
    int storyId,
    List<string> timestampResponses,
    string fullText,
    Guid? storyVoiceId = null)
{
    var mappings = new List<AudioTextMapping>();
    var cedDataList = new List<(int ced, string audioData, string sid)>();

    // 解析所有时间戳响应
    foreach (var timestampResponse in timestampResponses)
    {
        var response = JsonSerializer.Deserialize<JsonElement>(timestampResponse);
        
        if (response.TryGetProperty("data", out var dataElement))
        {
            // 提取CED值和音频数据
            if (dataElement.TryGetProperty("ced", out var cedElement) &&
                dataElement.TryGetProperty("audio", out var audioElement))
            {
                if (int.TryParse(cedElement.GetString(), out int cedValue))
                {
                    var audioData = audioElement.GetString();
                    var sid = response.TryGetProperty("sid", out var sidElement) 
                        ? sidElement.GetString() : string.Empty;
                    
                    cedDataList.Add((cedValue, audioData, sid));
                }
            }
        }
    }

    // 按CED值排序并创建映射记录
    cedDataList.Sort((a, b) => a.ced.CompareTo(b.ced));
    
    var fullTextBytes = Encoding.UTF8.GetBytes(fullText);
    var previousCed = 0;

    foreach (var (ced, audioData, sid) in cedDataList)
    {
        // 计算文本范围
        var startByte = previousCed;
        var endByte = ced;

        if (startByte < endByte && endByte <= fullTextBytes.Length)
        {
            // 提取对应文本段落
            var textSegmentBytes = new byte[endByte - startByte];
            Array.Copy(fullTextBytes, startByte, textSegmentBytes, 0, textSegmentBytes.Length);
            var textSegment = Encoding.UTF8.GetString(textSegmentBytes);

            // 创建映射记录
            var mapping = new AudioTextMapping
            {
                StoryId = storyId,
                StoryVoiceId = storyVoiceId,
                TextContent = textSegment,
                TextStartByte = startByte,
                TextEndByte = endByte,
                AudioData = audioData,
                Ced = ced,
                Sid = sid,
                CreatedAt = DateTime.UtcNow,
                UpdatedAt = DateTime.UtcNow
            };

            mappings.Add(mapping);
        }

        previousCed = ced;
    }

    // 批量保存到数据库
    if (mappings.Count > 0)
    {
        _context.AudioTextMappings.AddRange(mappings);
        await _context.SaveChangesAsync();
    }

    return mappings;
}
相关推荐
karry_k2 小时前
Java的类加载器
后端
ZZHHWW3 小时前
高性能架构01 -- 开篇
后端·架构
程序员小潘3 小时前
Spring Gateway动态路由实现方案
后端·spring cloud
golang学习记3 小时前
国内完美安装 Rust 环境 + VSCode 编写 Hello World 完整指南(2025 最新)
后端
Undoom3 小时前
解锁超级生产力:手把手教你构建与GitHub深度集成的自动化工作流,让AI成为你的编程副驾驶
后端
我是华为OD~HR~栗栗呀3 小时前
前端面经-高级开发(华为od)
java·前端·后端·python·华为od·华为·面试
菜鸟小九4 小时前
SSM(MybatisPlus)
java·开发语言·spring boot·后端
不爱编程的小九九4 小时前
小九源码-springboot051-智能推荐旅游平台
java·spring boot·后端
数据知道4 小时前
Go基础:常用数学函数处理(主要是math包rand包的处理)
开发语言·后端·golang·go语言