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? let zhText: String? 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" case zhText = "zh_text" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) index = try container.decode(Int.self, forKey: .index) startMs = try container.decode(Int64.self, forKey: .startMs) endMs = try container.decode(Int64.self, forKey: .endMs) text = try container.decodeIfPresent(String.self, forKey: .text) language = try container.decodeIfPresent(String.self, forKey: .language) referenceSpeechDurationMs = try container.decodeIfPresent(Int64.self, forKey: .referenceSpeechDurationMs) zhText = try container.decodeIfPresent(String.self, forKey: .zhText) } init( index: Int, startMs: Int64, endMs: Int64, text: String?, language: String?, referenceSpeechDurationMs: Int64?, zhText: String? = nil ) { self.index = index self.startMs = startMs self.endMs = endMs self.text = text self.language = language self.referenceSpeechDurationMs = referenceSpeechDurationMs self.zhText = zhText } func withZhText(_ zhText: String?) -> SentenceBoundary { SentenceBoundary( index: index, startMs: startMs, endMs: endMs, text: text, language: language, referenceSpeechDurationMs: referenceSpeechDurationMs, zhText: zhText ) } } 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) } func copy(durationMs: Int64? = self.durationMs, sentenceCount: Int = self.sentenceCount) -> Self { TrainingVideoSummary( videoHash: videoHash, title: title, streamUrl: streamUrl, durationMs: durationMs, sizeBytes: sizeBytes, language: language, sentenceCount: sentenceCount, status: status ) } } 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 } }