import Foundation /// Response von `POST /api/v1/media/upload`. struct MediaUploadResponse: Decodable { let id: String let url: String let mimeType: String let kind: MediaKind let sizeBytes: Int let originalFilename: String? enum CodingKeys: String, CodingKey { case id case url case mimeType = "mime_type" case kind case sizeBytes = "size_bytes" case originalFilename = "original_filename" } } enum MediaKind: String, Codable { case image case audio case video case other } /// Image-Occlusion-Mask-Region. /// `mask_regions`-Feld ist ein JSON-Array-**String** in `fields`, /// nicht ein Object — Server-Schema-Constraint (`fields: Record`). struct MaskRegion: Codable, Hashable, Identifiable { let id: String let x: Double // 0..1 relativ let y: Double let w: Double let h: Double let label: String? init(id: String, x: Double, y: Double, w: Double, h: Double, label: String? = nil) { self.id = id self.x = x self.y = y self.w = w self.h = h self.label = label } } /// Helpers zum Parsen/Serialisieren von `mask_regions` als JSON-String. enum MaskRegions { /// 1:1-Port aus `cards-domain/image-occlusion.ts:parseMaskRegions`. /// Bei Parse- oder Schema-Fehler: leere Liste. Sortiert nach ID /// (lexikographisch, gleich wie Server-Sortierung). static func parse(_ json: String) -> [MaskRegion] { let data = Data(json.utf8) guard let regions = try? JSONDecoder().decode([MaskRegion].self, from: data) else { return [] } return regions.sorted { $0.id < $1.id } } /// Sub-Index → Region (Sortier-Reihenfolge). static func region(for json: String, subIndex: Int) -> MaskRegion? { let all = parse(json) return all.indices.contains(subIndex) ? all[subIndex] : nil } /// Anzahl Regionen → Anzahl Sub-Index-Reviews. static func count(_ json: String) -> Int { parse(json).count } /// Serialisiert eine Liste zu einem JSON-Array-String fürs `fields`-Feld. static func encode(_ regions: [MaskRegion]) -> String { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys] guard let data = try? encoder.encode(regions), let json = String(bytes: data, encoding: .utf8) else { return "[]" } return json } } extension CardFieldsBuilder { /// `image-occlusion`-Fields: `image_ref` (media_id) + /// `mask_regions` (stringified JSON-Array) + optional `note`. static func imageOcclusion( imageRef: String, regions: [MaskRegion], note: String? = nil ) -> [String: String] { var fields: [String: String] = [ "image_ref": imageRef, "mask_regions": MaskRegions.encode(regions) ] if let note, !note.isEmpty { fields["note"] = note } return fields } /// `audio-front`-Fields: `audio_ref` (media_id) + `back` (Antwort-Text). static func audioFront(audioRef: String, back: String) -> [String: String] { ["audio_ref": audioRef, "back": back] } }