import Foundation struct SentenceBoundary: Identifiable, Equatable, Decodable { let index: Int let startMs: Int64 let endMs: Int64 let text: String? let language: String? let referenceSpeechDurationMs: Int64? var id: Int { index } private enum CodingKeys: String, CodingKey { case index, text, language case startMs = "start_ms" case endMs = "end_ms" case referenceSpeechDurationMs = "reference_speech_duration_ms" } } struct SentenceBoundaryDocument: Decodable { let videoHash: String let durationMs: Int64 let algorithmVersion: String let sentences: [SentenceBoundary] private enum CodingKeys: String, CodingKey { case videoHash = "video_hash" case durationMs = "duration_ms" case algorithmVersion = "algorithm_version" case sentences } } struct TrainingVideoSummary: Identifiable, Decodable { let videoHash: String let title: String let streamUrl: String let durationMs: Int64? let sizeBytes: Int64 let language: String? let sentenceCount: Int let status: String var id: String { videoHash } private enum CodingKeys: String, CodingKey { case title, language, status case videoHash = "video_hash" case streamUrl = "stream_url" case durationMs = "duration_ms" case sizeBytes = "size_bytes" case sentenceCount = "sentence_count" } /// 与安卓端一致:相对路径基于服务地址解析;服务地址为 http 时强制使用 http 流地址。 func resolvedStreamURL(baseURL: String) -> URL? { guard let base = URL(string: baseURL) else { return nil } var absolute = URL(string: streamUrl, relativeTo: base)?.absoluteString ?? streamUrl if base.scheme == "http" { absolute = absolute.replacingOccurrences( of: "https://", with: "http://", options: [.anchored] ) } return URL(string: absolute) } } struct TrainingMediaItem: Identifiable, Equatable { let id: String let title: String let url: URL let sentences: [SentenceBoundary] let isLocalFile: Bool init( id: String, title: String, url: URL, sentences: [SentenceBoundary] = [], isLocalFile: Bool = false ) { self.id = id self.title = title self.url = url self.sentences = sentences self.isLocalFile = isLocalFile } static func == (lhs: TrainingMediaItem, rhs: TrainingMediaItem) -> Bool { lhs.id == rhs.id } }