You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI解析WebVTT文件并实现音频同步交互转录技术问询

实现SwiftUI交互式转录文档(音频+WebVTT同步)

一、WebVTT解析实现

首先定义字幕数据模型,再编写本地VTT文件解析逻辑,处理时间戳和多行字幕场景:

import Foundation

struct Caption: Identifiable {
    let id = UUID()
    let startTime: TimeInterval
    let endTime: TimeInterval
    let text: String
}

// 解析本地WebVTT文件
func parseWebVTT(from url: URL) -> [Caption] {
    var captions = [Caption]()
    guard let content = try? String(contentsOf: url) else { return captions }
    
    let lines = content.components(separatedBy: .newlines)
    var currentStart: TimeInterval?
    var currentEnd: TimeInterval?
    var currentText = ""
    
    for line in lines {
        let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
        // 跳过无关行(空行、WebVTT头部、注释)
        if trimmed.isEmpty || trimmed.starts(with: "WEBVTT") || trimmed.starts(with: "NOTE") {
            continue
        }
        
        // 匹配时间戳格式行
        let timePattern = #"^(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})$"#
        if trimmed.range(of: timePattern, options: .regularExpression) != nil {
            // 保存上一段已收集好的字幕
            if let start = currentStart, let end = currentEnd, !currentText.isEmpty {
                captions.append(Caption(startTime: start, endTime: end, text: currentText))
                currentText = ""
            }
            // 解析时间戳为TimeInterval
            let timeParts = trimmed.components(separatedBy: " --> ")
            currentStart = parseTime(timeParts[0])
            currentEnd = parseTime(timeParts[1])
        } else {
            // 拼接多行字幕文本
            currentText += (currentText.isEmpty ? "" : " ") + trimmed
        }
    }
    // 处理最后一段未保存的字幕
    if let start = currentStart, let end = currentEnd, !currentText.isEmpty {
        captions.append(Caption(startTime: start, endTime: end, text: currentText))
    }
    return captions
}

// 将VTT时间字符串转换为TimeInterval
private func parseTime(_ timeStr: String) -> TimeInterval {
    let parts = timeStr.components(separatedBy: ":")
    guard parts.count == 3 else { return 0 }
    let hours = Double(parts[0]) ?? 0
    let minutes = Double(parts[1]) ?? 0
    let seconds = Double(parts[2]) ?? 0
    return hours * 3600 + minutes * 60 + seconds
}

二、SwiftUI视图与音频同步逻辑

使用AVPlayer处理音频播放,监听播放进度实现字幕高亮,同时支持点击字幕跳转对应音频时间:

import AVKit
import SwiftUI

struct TranscriptView: View {
    @State private var captions: [Caption] = []
    @State private var player: AVPlayer?
    @State private var currentTime: TimeInterval = 0
    @State private var highlightedCaptionId: UUID?
    // 保存时间观察者token,用于页面销毁时移除监听
    private var timeObserverToken: Any?

    var body: some View {
        VStack(spacing: 16) {
            // 播放控制栏
            HStack {
                Button(action: togglePlay) {
                    Image(systemName: player?.timeControlStatus == .playing ? "pause.circle.fill" : "play.circle.fill")
                        .font(.system(size: 44))
                        .foregroundColor(.blue)
                }
                
                Slider(
                    value: $currentTime,
                    in: 0...(player?.currentItem?.duration.seconds ?? 0),
                    onEditingChanged: { isEditing in
                        if !isEditing {
                            player?.seek(to: CMTime(seconds: currentTime, preferredTimescale: 1000))
                        }
                    }
                )
            }
            .padding(.horizontal)

            // 转录文本展示区域
            ScrollView {
                VStack(alignment: .leading, spacing: 12) {
                    ForEach(captions) { caption in
                        Text(caption.text)
                            .padding(10)
                            .background(highlightedCaptionId == caption.id ? Color.blue.opacity(0.2) : Color.clear)
                            .cornerRadius(6)
                            .onTapGesture {
                                player?.seek(to: CMTime(seconds: caption.startTime, preferredTimescale: 1000))
                            }
                    }
                }
                .padding()
            }
        }
        .onAppear {
            // 加载本地音频和VTT文件(替换为你的实际文件名)
            guard let audioURL = Bundle.main.url(forResource: "your-audio", withExtension: "mp3"),
                  let vttURL = Bundle.main.url(forResource: "your-transcript", withExtension: "vtt") else {
                print("未找到本地音频或VTT文件")
                return
            }
            
            player = AVPlayer(url: audioURL)
            captions = parseWebVTT(from: vttURL)
            
            // 监听播放进度,0.1秒间隔平衡同步精度与性能
            let interval = CMTime(seconds: 0.1, preferredTimescale: 1000)
            timeObserverToken = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
                guard let self = self else { return }
                self.currentTime = time.seconds
                // 匹配当前播放时间对应的字幕
                self.highlightedCaptionId = self.captions.first {
                    self.currentTime >= $0.startTime && self.currentTime <= $0.endTime
                }?.id
            }
        }
        .onDisappear {
            player?.pause()
            if let token = timeObserverToken {
                player?.removeTimeObserver(token)
            }
        }
    }

    private func togglePlay() {
        guard let player = player else { return }
        player.timeControlStatus == .playing ? player.pause() : player.play()
    }
}

三、关键注意事项

  1. 多行字幕处理:WebVTT允许单段字幕跨多行,解析时需将连续的文本行拼接成完整的字幕内容
  2. 时间精度:解析时间戳时保留毫秒级精度,避免音频与字幕出现同步偏移
  3. 性能优化:AVPlayer的时间监听间隔不要设置过小,0.1秒足够满足视觉同步需求
  4. 资源配置:确保MP3和VTT文件已添加到Xcode项目目标中(勾选Copy items if needed)

内容的提问来源于stack exchange,提问作者Jason G

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 07:24:57