{"repo": "badakonpro/ai-translate-dub", "files": {"macos-app/VoxOver/Services/PipelineAPIClient.swift": {"e5eef33b279c532cd1fc4563d686419f79355c57": {"t": 1785745896.8130412, "n": 14104, "h": "fb6629949b8cb8e1", "c": "// PipelineAPIClient.swift\n// VoxOver — HTTP client for the FastAPI backend (api_server.py)\n\nimport Foundation\n\nstruct JobResult: Decodable {\n let status: String\n let subtitlePath: String?\n let videoPath: String?\n let softVideoPath: String?\n let hardVideoPath: String?\n let outputDir: String?\n let error: String?\n}\n\n@MainActor\nfinal class PipelineAPIClient: ObservableObject {\n\n // ── Configuration ─────────────────────────────────────────────────────────\n\n private let port: Int\n private var base: URL { URL(string: \"http://127.0.0.1:\\(port)\")! }\n\n private lazy var encoder: JSONEncoder = {\n let e = JSONEncoder()\n e.keyEncodingStrategy = .convertToSnakeCase\n return e\n }()\n\n private lazy var decoder: JSONDecoder = {\n let d = JSONDecoder()\n d.keyDecodingStrategy = .convertFromSnakeCase\n return d\n }()\n\n init(port: Int = 7861) {\n self.port = port\n }\n\n // ── Health ────────────────────────────────────────────────────────────────\n\n func health() async throws -> Bool {\n let (_, resp) = try await URLSession.shared.data(from: base.appending(path: \"/health\"))\n return (resp as? HTTPURLResponse)?.statusCode == 200\n }\n\n // ── Process ───────────────────────────────────────────────────────────────\n\n private func isBrokenPipeError(_ error: Error) -> Bool {\n let text = error.localizedDescription.lowercased()\n return text.contains(\"broken pipe\") || text.contains(\"errno 32\")\n }\n\n /// Submit a job and return the job ID.\n func submitJob(request: ProcessRequest) async throws -> String {\n let attempts = 3\n for attempt in 1...attempts {\n do {\n var urlReq = URLRequest(url: base.appending(path: \"/process\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n // Avoid stale keep-alive socket reuse that may trigger EPIPE.\n urlReq.setValue(\"close\", forHTTPHeaderField: \"Connection\")\n urlReq.httpBody = try encoder.encode(request)\n\n let (data, resp) = try await URLSession.shared.data(for: urlReq)\n guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {\n let body = String(data: data, encoding: .utf8) ?? \"\"\n throw VoxOverError.apiError(\"submitJob failed: \\(body)\")\n }\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n guard let jobId = json?[\"job_id\"] as? String else {\n throw VoxOverError.apiError(\"No job_id in response\")\n }\n return jobId\n } catch {\n if attempt < attempts && isBrokenPipeError(error) {\n try await Task.sleep(for: .milliseconds(300 * attempt))\n continue\n }\n throw error\n }\n }\n\n throw VoxOverError.apiError(\"submitJob failed after retries\")\n }\n\n /// SSE stream for a job. Yields ProgressEvent values until done/error.\n func jobProgressStream(jobId: String) -> AsyncThrowingStream {\n AsyncThrowingStream { continuation in\n Task {\n let url = self.base.appending(path: \"/jobs/\\(jobId)/events\")\n do {\n let (asyncBytes, response) = try await URLSession.shared.bytes(from: url)\n guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {\n continuation.finish(throwing: VoxOverError.sseStreamFailed(\"HTTP \\((response as? HTTPURLResponse)?.statusCode ?? -1)\"))\n return\n }\n for try await line in asyncBytes.lines {\n guard line.hasPrefix(\"data: \") else { continue }\n let payload = String(line.dropFirst(6))\n guard let data = payload.data(using: .utf8) else { continue }\n let event = try self.decoder.decode(ProgressEvent.self, from: data)\n continuation.yield(event)\n if event.type == \"done\" || event.type == \"error\" {\n continuation.finish()\n return\n }\n }\n continuation.finish()\n } catch {\n continuation.finish(throwing: error)\n }\n }\n }\n }\n\n /// Poll-based fallback for job status when SSE connection is interrupted.\n func jobResult(jobId: String) async throws -> JobResult {\n let url = base.appending(path: \"/jobs/\\(jobId)/result\")\n let (data, resp) = try await URLSession.shared.data(from: url)\n guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {\n throw VoxOverError.apiError(\"jobResult failed\")\n }\n return try decoder.decode(JobResult.self, from: data)\n }\n\n // ── Settings ──────────────────────────────────────────────────────────────\n\n func loadSettings() async throws -> AppConfig {\n let (data, _) = try await URLSession.shared.data(from: base.appending(path: \"/settings\"))\n let json = (try JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:]\n\n let defaults = json[\"defaults\"] as? [String: Any] ?? [:]\n let deepseek = json[\"deepseek\"] as? [String: Any] ?? [:]\n let openai = json[\"openai\"] as? [String: Any] ?? [:]\n let anthropic = json[\"anthropic\"] as? [String: Any] ?? [:]\n let ollama = json[\"ollama\"] as? [String: Any] ?? [:]\n let translation = json[\"translation\"] as? [String: Any] ?? [:]\n let ui = json[\"ui\"] as? [String: Any] ?? [:]\n\n return AppConfig(\n whisperModel: defaults[\"whisper_model\"] as? String ?? \"medium\",\n sourceLang: defaults[\"source_lang\"] as? String ?? \"Chinese\",\n targetLang: defaults[\"target_lang\"] as? String ?? \"English\",\n translationBackend: defaults[\"translation_backend\"] as? String ?? \"deepseek\",\n deepseekApiKey: deepseek[\"api_key\"] as? String ?? \"\",\n deepseekModel: deepseek[\"model\"] as? String ?? \"deepseek-chat\",\n deepseekBaseUrl: deepseek[\"base_url\"] as? String ?? \"https://api.deepseek.com/v1\",\n openaiApiKey: openai[\"api_key\"] as? String ?? \"\",\n openaiModel: openai[\"model\"] as? String ?? \"gpt-4o-mini\",\n openaiBaseUrl: openai[\"base_url\"] as? String ?? \"https://api.openai.com/v1\",\n anthropicApiKey: anthropic[\"api_key\"] as? String ?? \"\",\n anthropicModel: anthropic[\"model\"] as? String ?? \"claude-3-5-haiku-20241022\",\n ollamaHost: ollama[\"host\"] as? String ?? \"http://localhost:11434\",\n ollamaModel: ollama[\"model\"] as? String ?? \"qwen2.5:7b\",\n ollamaAutoPull: ollama[\"auto_pull\"] as? Bool ?? true,\n softSubtitles: ui[\"soft_subtitles\"] as? Bool ?? true,\n burnSubtitles: ui[\"burn_subtitles\"] as? Bool ?? true,\n fontName: ui[\"font_name\"] as? String ?? \"Arial\",\n fontSize: ui[\"font_size\"] as? Int ?? 24,\n fontColor: ui[\"font_color\"] as? String ?? \"white\",\n subtitleAlign: ui[\"subtitle_align\"] as? String ?? \"bottom-center\",\n marginV: ui[\"margin_v\"] as? Int ?? 20,\n parallelTranslation: translation[\"parallel_enabled\"] as? Bool ?? true,\n maxWorkers: translation[\"parallel_workers\"] as? Int ?? 4,\n autoGenerateContext: translation[\"context_enabled\"] as? Bool ?? false,\n translationContext: translation[\"manual_context\"] as? String ?? \"\"\n )\n }\n\n func saveSettings(_ config: AppConfig) async throws {\n var urlReq = URLRequest(url: base.appending(path: \"/settings\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n let payload: [String: Any] = [\n \"whisper_model\": config.whisperModel,\n \"source_lang\": config.sourceLang,\n \"target_lang\": config.targetLang,\n \"translation_backend\": config.translationBackend,\n \"deepseek_api_key\": config.deepseekApiKey,\n \"deepseek_model\": config.deepseekModel,\n \"deepseek_base_url\": config.deepseekBaseUrl,\n \"openai_api_key\": config.openaiApiKey,\n \"openai_model\": config.openaiModel,\n \"openai_base_url\": config.openaiBaseUrl,\n \"anthropic_api_key\": config.anthropicApiKey,\n \"anthropic_model\": config.anthropicModel,\n \"ollama_host\": config.ollamaHost,\n \"ollama_model\": config.ollamaModel,\n \"ollama_auto_pull\": config.ollamaAutoPull,\n \"parallel_enabled\": config.parallelTranslation,\n \"parallel_workers\": config.maxWorkers,\n \"context_enabled\": config.autoGenerateContext,\n \"translation_context\": config.translationContext,\n \"soft_subtitles\": config.softSubtitles,\n \"burn_subtitles\": config.burnSubtitles,\n \"font_name\": config.fontName,\n \"font_size\": config.fontSize,\n \"font_color\": config.fontColor,\n \"subtitle_align\": config.subtitleAlign,\n \"margin_v\": config.marginV,\n ]\n urlReq.httpBody = try JSONSerialization.data(withJSONObject: payload)\n _ = try await URLSession.shared.data(for: urlReq)\n }\n\n // ── Models ────────────────────────────────────────────────────────────────\n\n func ollamaModels(host: String) async throws -> [String] {\n var components = URLComponents(url: base.appending(path: \"/models/ollama\"), resolvingAgainstBaseURL: false)\n components?.queryItems = [URLQueryItem(name: \"host\", value: host)]\n var urlReq = URLRequest(url: components?.url ?? base.appending(path: \"/models/ollama\"))\n urlReq.httpMethod = \"GET\"\n let (data, _) = try await URLSession.shared.data(for: urlReq)\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n return (json?[\"models\"] as? [String]) ?? []\n }\n\n func deepseekModels(apiKey: String, baseUrl: String) async throws -> [String] {\n var urlReq = URLRequest(url: base.appending(path: \"/models/deepseek\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n urlReq.httpBody = try JSONSerialization.data(withJSONObject: [\"api_key\": apiKey, \"base_url\": baseUrl])\n let (data, _) = try await URLSession.shared.data(for: urlReq)\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n return (json?[\"models\"] as? [String]) ?? []\n }\n\n func generateTranslationContext(formState: ProcessingFormState) async throws -> String {\n let body: [String: Any] = [\n \"translation_backend\": formState.translationBackend,\n \"source_lang\": formState.sourceLang,\n \"target_lang\": formState.targetLang,\n \"video_description\": formState.translationContext,\n \"deepseek_api_key\": formState.deepseekApiKey,\n \"deepseek_model\": formState.deepseekModel,\n \"deepseek_base_url\": formState.deepseekBaseUrl,\n \"openai_api_key\": formState.openaiApiKey,\n \"openai_model\": formState.openaiModel,\n \"openai_base_url\": formState.openaiBaseUrl,\n \"anthropic_api_key\": formState.anthropicApiKey,\n \"anthropic_model\": formState.anthropicModel,\n \"ollama_host\": formState.ollamaHost,\n \"ollama_model\": formState.ollamaModel,\n ]\n var urlReq = URLRequest(url: base.appending(path: \"/generate_context\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n urlReq.httpBody = try JSONSerialization.data(withJSONObject: body)\n let (data, resp) = try await URLSession.shared.data(for: urlReq)\n guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {\n let detail = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?[\"detail\"] as? String ?? \"Unknown error\"\n throw VoxOverError.apiError(detail)\n }\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n return (json?[\"context\"] as? String) ?? \"\"\n }\n\n /// Stream Ollama model pull progress. Yields (pct, message) tuples.\n func pullOllamaModel(host: String, model: String) -> AsyncThrowingStream<(Double, String), Error> {\n AsyncThrowingStream { continuation in\n Task {\n var urlReq = URLRequest(url: self.base.appending(path: \"/ollama/pull\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n urlReq.httpBody = try JSONSerialization.data(withJSONObject: [\"host\": host, \"model\": model])\n do {\n let (asyncBytes, _) = try await URLSession.shared.bytes(for: urlReq)\n for try await line in asyncBytes.lines {\n guard line.hasPrefix(\"data: \"),\n let data = String(line.dropFirst(6)).data(using: .utf8),\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n else { continue }\n let pct = json[\"pct\"] as? Double ?? 0\n let msg = json[\"msg\"] as? String ?? \"\"\n continuation.yield((pct, msg))\n if json[\"type\"] as? String == \"done\" {\n continuation.finish(); return\n }\n }\n continuation.finish()\n } catch {\n continuation.finish(throwing: error)\n }\n }\n }\n }\n}\n"}, "0ceb0a9d4aa4224fab06c893fba40f3930a4820b": {"t": 1785757756.046515, "n": 14104, "h": "fb6629949b8cb8e1", "c": "// PipelineAPIClient.swift\n// VoxOver — HTTP client for the FastAPI backend (api_server.py)\n\nimport Foundation\n\nstruct JobResult: Decodable {\n let status: String\n let subtitlePath: String?\n let videoPath: String?\n let softVideoPath: String?\n let hardVideoPath: String?\n let outputDir: String?\n let error: String?\n}\n\n@MainActor\nfinal class PipelineAPIClient: ObservableObject {\n\n // ── Configuration ─────────────────────────────────────────────────────────\n\n private let port: Int\n private var base: URL { URL(string: \"http://127.0.0.1:\\(port)\")! }\n\n private lazy var encoder: JSONEncoder = {\n let e = JSONEncoder()\n e.keyEncodingStrategy = .convertToSnakeCase\n return e\n }()\n\n private lazy var decoder: JSONDecoder = {\n let d = JSONDecoder()\n d.keyDecodingStrategy = .convertFromSnakeCase\n return d\n }()\n\n init(port: Int = 7861) {\n self.port = port\n }\n\n // ── Health ────────────────────────────────────────────────────────────────\n\n func health() async throws -> Bool {\n let (_, resp) = try await URLSession.shared.data(from: base.appending(path: \"/health\"))\n return (resp as? HTTPURLResponse)?.statusCode == 200\n }\n\n // ── Process ───────────────────────────────────────────────────────────────\n\n private func isBrokenPipeError(_ error: Error) -> Bool {\n let text = error.localizedDescription.lowercased()\n return text.contains(\"broken pipe\") || text.contains(\"errno 32\")\n }\n\n /// Submit a job and return the job ID.\n func submitJob(request: ProcessRequest) async throws -> String {\n let attempts = 3\n for attempt in 1...attempts {\n do {\n var urlReq = URLRequest(url: base.appending(path: \"/process\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n // Avoid stale keep-alive socket reuse that may trigger EPIPE.\n urlReq.setValue(\"close\", forHTTPHeaderField: \"Connection\")\n urlReq.httpBody = try encoder.encode(request)\n\n let (data, resp) = try await URLSession.shared.data(for: urlReq)\n guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {\n let body = String(data: data, encoding: .utf8) ?? \"\"\n throw VoxOverError.apiError(\"submitJob failed: \\(body)\")\n }\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n guard let jobId = json?[\"job_id\"] as? String else {\n throw VoxOverError.apiError(\"No job_id in response\")\n }\n return jobId\n } catch {\n if attempt < attempts && isBrokenPipeError(error) {\n try await Task.sleep(for: .milliseconds(300 * attempt))\n continue\n }\n throw error\n }\n }\n\n throw VoxOverError.apiError(\"submitJob failed after retries\")\n }\n\n /// SSE stream for a job. Yields ProgressEvent values until done/error.\n func jobProgressStream(jobId: String) -> AsyncThrowingStream {\n AsyncThrowingStream { continuation in\n Task {\n let url = self.base.appending(path: \"/jobs/\\(jobId)/events\")\n do {\n let (asyncBytes, response) = try await URLSession.shared.bytes(from: url)\n guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {\n continuation.finish(throwing: VoxOverError.sseStreamFailed(\"HTTP \\((response as? HTTPURLResponse)?.statusCode ?? -1)\"))\n return\n }\n for try await line in asyncBytes.lines {\n guard line.hasPrefix(\"data: \") else { continue }\n let payload = String(line.dropFirst(6))\n guard let data = payload.data(using: .utf8) else { continue }\n let event = try self.decoder.decode(ProgressEvent.self, from: data)\n continuation.yield(event)\n if event.type == \"done\" || event.type == \"error\" {\n continuation.finish()\n return\n }\n }\n continuation.finish()\n } catch {\n continuation.finish(throwing: error)\n }\n }\n }\n }\n\n /// Poll-based fallback for job status when SSE connection is interrupted.\n func jobResult(jobId: String) async throws -> JobResult {\n let url = base.appending(path: \"/jobs/\\(jobId)/result\")\n let (data, resp) = try await URLSession.shared.data(from: url)\n guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {\n throw VoxOverError.apiError(\"jobResult failed\")\n }\n return try decoder.decode(JobResult.self, from: data)\n }\n\n // ── Settings ──────────────────────────────────────────────────────────────\n\n func loadSettings() async throws -> AppConfig {\n let (data, _) = try await URLSession.shared.data(from: base.appending(path: \"/settings\"))\n let json = (try JSONSerialization.jsonObject(with: data) as? [String: Any]) ?? [:]\n\n let defaults = json[\"defaults\"] as? [String: Any] ?? [:]\n let deepseek = json[\"deepseek\"] as? [String: Any] ?? [:]\n let openai = json[\"openai\"] as? [String: Any] ?? [:]\n let anthropic = json[\"anthropic\"] as? [String: Any] ?? [:]\n let ollama = json[\"ollama\"] as? [String: Any] ?? [:]\n let translation = json[\"translation\"] as? [String: Any] ?? [:]\n let ui = json[\"ui\"] as? [String: Any] ?? [:]\n\n return AppConfig(\n whisperModel: defaults[\"whisper_model\"] as? String ?? \"medium\",\n sourceLang: defaults[\"source_lang\"] as? String ?? \"Chinese\",\n targetLang: defaults[\"target_lang\"] as? String ?? \"English\",\n translationBackend: defaults[\"translation_backend\"] as? String ?? \"deepseek\",\n deepseekApiKey: deepseek[\"api_key\"] as? String ?? \"\",\n deepseekModel: deepseek[\"model\"] as? String ?? \"deepseek-chat\",\n deepseekBaseUrl: deepseek[\"base_url\"] as? String ?? \"https://api.deepseek.com/v1\",\n openaiApiKey: openai[\"api_key\"] as? String ?? \"\",\n openaiModel: openai[\"model\"] as? String ?? \"gpt-4o-mini\",\n openaiBaseUrl: openai[\"base_url\"] as? String ?? \"https://api.openai.com/v1\",\n anthropicApiKey: anthropic[\"api_key\"] as? String ?? \"\",\n anthropicModel: anthropic[\"model\"] as? String ?? \"claude-3-5-haiku-20241022\",\n ollamaHost: ollama[\"host\"] as? String ?? \"http://localhost:11434\",\n ollamaModel: ollama[\"model\"] as? String ?? \"qwen2.5:7b\",\n ollamaAutoPull: ollama[\"auto_pull\"] as? Bool ?? true,\n softSubtitles: ui[\"soft_subtitles\"] as? Bool ?? true,\n burnSubtitles: ui[\"burn_subtitles\"] as? Bool ?? true,\n fontName: ui[\"font_name\"] as? String ?? \"Arial\",\n fontSize: ui[\"font_size\"] as? Int ?? 24,\n fontColor: ui[\"font_color\"] as? String ?? \"white\",\n subtitleAlign: ui[\"subtitle_align\"] as? String ?? \"bottom-center\",\n marginV: ui[\"margin_v\"] as? Int ?? 20,\n parallelTranslation: translation[\"parallel_enabled\"] as? Bool ?? true,\n maxWorkers: translation[\"parallel_workers\"] as? Int ?? 4,\n autoGenerateContext: translation[\"context_enabled\"] as? Bool ?? false,\n translationContext: translation[\"manual_context\"] as? String ?? \"\"\n )\n }\n\n func saveSettings(_ config: AppConfig) async throws {\n var urlReq = URLRequest(url: base.appending(path: \"/settings\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n let payload: [String: Any] = [\n \"whisper_model\": config.whisperModel,\n \"source_lang\": config.sourceLang,\n \"target_lang\": config.targetLang,\n \"translation_backend\": config.translationBackend,\n \"deepseek_api_key\": config.deepseekApiKey,\n \"deepseek_model\": config.deepseekModel,\n \"deepseek_base_url\": config.deepseekBaseUrl,\n \"openai_api_key\": config.openaiApiKey,\n \"openai_model\": config.openaiModel,\n \"openai_base_url\": config.openaiBaseUrl,\n \"anthropic_api_key\": config.anthropicApiKey,\n \"anthropic_model\": config.anthropicModel,\n \"ollama_host\": config.ollamaHost,\n \"ollama_model\": config.ollamaModel,\n \"ollama_auto_pull\": config.ollamaAutoPull,\n \"parallel_enabled\": config.parallelTranslation,\n \"parallel_workers\": config.maxWorkers,\n \"context_enabled\": config.autoGenerateContext,\n \"translation_context\": config.translationContext,\n \"soft_subtitles\": config.softSubtitles,\n \"burn_subtitles\": config.burnSubtitles,\n \"font_name\": config.fontName,\n \"font_size\": config.fontSize,\n \"font_color\": config.fontColor,\n \"subtitle_align\": config.subtitleAlign,\n \"margin_v\": config.marginV,\n ]\n urlReq.httpBody = try JSONSerialization.data(withJSONObject: payload)\n _ = try await URLSession.shared.data(for: urlReq)\n }\n\n // ── Models ────────────────────────────────────────────────────────────────\n\n func ollamaModels(host: String) async throws -> [String] {\n var components = URLComponents(url: base.appending(path: \"/models/ollama\"), resolvingAgainstBaseURL: false)\n components?.queryItems = [URLQueryItem(name: \"host\", value: host)]\n var urlReq = URLRequest(url: components?.url ?? base.appending(path: \"/models/ollama\"))\n urlReq.httpMethod = \"GET\"\n let (data, _) = try await URLSession.shared.data(for: urlReq)\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n return (json?[\"models\"] as? [String]) ?? []\n }\n\n func deepseekModels(apiKey: String, baseUrl: String) async throws -> [String] {\n var urlReq = URLRequest(url: base.appending(path: \"/models/deepseek\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n urlReq.httpBody = try JSONSerialization.data(withJSONObject: [\"api_key\": apiKey, \"base_url\": baseUrl])\n let (data, _) = try await URLSession.shared.data(for: urlReq)\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n return (json?[\"models\"] as? [String]) ?? []\n }\n\n func generateTranslationContext(formState: ProcessingFormState) async throws -> String {\n let body: [String: Any] = [\n \"translation_backend\": formState.translationBackend,\n \"source_lang\": formState.sourceLang,\n \"target_lang\": formState.targetLang,\n \"video_description\": formState.translationContext,\n \"deepseek_api_key\": formState.deepseekApiKey,\n \"deepseek_model\": formState.deepseekModel,\n \"deepseek_base_url\": formState.deepseekBaseUrl,\n \"openai_api_key\": formState.openaiApiKey,\n \"openai_model\": formState.openaiModel,\n \"openai_base_url\": formState.openaiBaseUrl,\n \"anthropic_api_key\": formState.anthropicApiKey,\n \"anthropic_model\": formState.anthropicModel,\n \"ollama_host\": formState.ollamaHost,\n \"ollama_model\": formState.ollamaModel,\n ]\n var urlReq = URLRequest(url: base.appending(path: \"/generate_context\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n urlReq.httpBody = try JSONSerialization.data(withJSONObject: body)\n let (data, resp) = try await URLSession.shared.data(for: urlReq)\n guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {\n let detail = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?[\"detail\"] as? String ?? \"Unknown error\"\n throw VoxOverError.apiError(detail)\n }\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n return (json?[\"context\"] as? String) ?? \"\"\n }\n\n /// Stream Ollama model pull progress. Yields (pct, message) tuples.\n func pullOllamaModel(host: String, model: String) -> AsyncThrowingStream<(Double, String), Error> {\n AsyncThrowingStream { continuation in\n Task {\n var urlReq = URLRequest(url: self.base.appending(path: \"/ollama/pull\"))\n urlReq.httpMethod = \"POST\"\n urlReq.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n urlReq.httpBody = try JSONSerialization.data(withJSONObject: [\"host\": host, \"model\": model])\n do {\n let (asyncBytes, _) = try await URLSession.shared.bytes(for: urlReq)\n for try await line in asyncBytes.lines {\n guard line.hasPrefix(\"data: \"),\n let data = String(line.dropFirst(6)).data(using: .utf8),\n let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]\n else { continue }\n let pct = json[\"pct\"] as? Double ?? 0\n let msg = json[\"msg\"] as? String ?? \"\"\n continuation.yield((pct, msg))\n if json[\"type\"] as? String == \"done\" {\n continuation.finish(); return\n }\n }\n continuation.finish()\n } catch {\n continuation.finish(throwing: error)\n }\n }\n }\n }\n}\n"}}}}