{"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"}}, "macos-app/VoxOver/ProcessingFormState.swift": {"e5eef33b279c532cd1fc4563d686419f79355c57": {"t": 1786068434.317613, "n": 6646, "h": "b9c4ccd2e54106b7", "c": "// ProcessingFormState.swift\n// VoxOver — Observable state for the processing form (bound to SettingsSidebarView)\n\nimport SwiftUI\nimport Combine\n\n@MainActor\nfinal class ProcessingFormState: ObservableObject {\n\n // ── ASR ───────────────────────────────────────────────────────────────────\n @Published var whisperModel = \"medium\"\n @Published var sourceLang = \"Chinese\"\n\n // ── Translation ───────────────────────────────────────────────────────────\n @Published var targetLang = \"English\"\n @Published var translationBackend = \"deepseek\"\n\n // DeepSeek\n @Published var deepseekApiKey = \"\"\n @Published var deepseekModel = \"deepseek-chat\"\n @Published var deepseekBaseUrl = \"https://api.deepseek.com/v1\"\n\n // OpenAI\n @Published var openaiApiKey = \"\"\n @Published var openaiModel = \"gpt-4o-mini\"\n @Published var openaiBaseUrl = \"https://api.openai.com/v1\"\n\n // Anthropic\n @Published var anthropicApiKey = \"\"\n @Published var anthropicModel = \"claude-3-5-haiku-20241022\"\n\n // Ollama\n @Published var ollamaHost = \"http://localhost:11434\"\n @Published var ollamaModel = \"qwen2.5:7b\"\n @Published var ollamaAutoPull = true\n\n // ── Subtitle ──────────────────────────────────────────────────────────────\n @Published var softSubtitles = true\n @Published var burnSubtitles = true\n @Published var fontName = \"Arial\"\n @Published var fontSize = 24\n @Published var fontColor = \"white\"\n @Published var subtitleAlign = \"bottom-center\"\n @Published var marginV = 20\n\n // ── Parallel ──────────────────────────────────────────────────────────────\n @Published var parallelTranslation = true\n @Published var maxWorkers = 4\n @Published var autoGenerateContext = false\n @Published var translationContext = \"\"\n\n // ── Available models (fetched dynamically) ────────────────────────────────\n @Published var availableOllamaModels: [String] = []\n @Published var availableDeepseekModels: [String] = []\n\n // ── Helpers ───────────────────────────────────────────────────────────────\n\n static let whisperModels = [\"tiny\", \"base\", \"small\", \"medium\", \"large\"]\n static let sourceLangs = [\"Auto\", \"Chinese\", \"English\", \"Japanese\", \"Korean\",\n \"French\", \"German\", \"Spanish\", \"Russian\", \"Portuguese\"]\n static let targetLangs = [\"English\", \"Chinese\", \"Japanese\", \"Korean\",\n \"French\", \"German\", \"Spanish\", \"Russian\", \"Portuguese\"]\n static let backends = [\"deepseek\", \"openai\", \"anthropic\", \"ollama\"]\n static let fontColors = [\"white\", \"yellow\", \"black\", \"red\", \"green\", \"blue\"]\n static let alignments = [\"bottom-center\", \"top-center\", \"bottom-left\", \"bottom-right\"]\n static let fontNames = [\n \"Arial\", \"Helvetica Neue\", \"PingFang SC\", \"Hiragino Sans GB\",\n \"STHeiti\", \"STSong\", \"Microsoft YaHei\", \"Songti SC\",\n \"Georgia\", \"Times New Roman\", \"Courier New\",\n \"Impact\", \"Verdana\", \"Tahoma\", \"Trebuchet MS\",\n ]\n\n func toProcessRequest(videoPath: String, videoStem: String) -> ProcessRequest {\n ProcessRequest(\n videoPath: videoPath,\n videoStem: videoStem,\n sourceLang: sourceLang,\n targetLang: targetLang,\n whisperModel: whisperModel,\n translationBackend: translationBackend,\n deepseekApiKey: deepseekApiKey,\n deepseekModel: deepseekModel,\n deepseekBaseUrl: deepseekBaseUrl,\n openaiApiKey: openaiApiKey,\n openaiModel: openaiModel,\n openaiBaseUrl: openaiBaseUrl,\n anthropicApiKey: anthropicApiKey,\n anthropicModel: anthropicModel,\n ollamaHost: ollamaHost,\n ollamaModel: ollamaModel,\n ollamaAutoPull: ollamaAutoPull,\n softSubtitles: softSubtitles,\n burnSubtitles: burnSubtitles,\n fontName: fontName,\n fontSize: fontSize,\n fontColor: fontColor,\n subtitleAlign: subtitleAlign,\n marginV: marginV,\n parallelTranslation: parallelTranslation,\n maxWorkers: maxWorkers,\n translationContext: autoGenerateContext ? \"\" : translationContext,\n useTranslationContext: autoGenerateContext\n )\n }\n\n func apply(config: AppConfig) {\n whisperModel = config.whisperModel\n sourceLang = config.sourceLang\n targetLang = config.targetLang\n translationBackend = config.translationBackend\n deepseekApiKey = config.deepseekApiKey\n deepseekModel = config.deepseekModel\n deepseekBaseUrl = config.deepseekBaseUrl\n openaiApiKey = config.openaiApiKey\n openaiModel = config.openaiModel\n openaiBaseUrl = config.openaiBaseUrl\n anthropicApiKey = config.anthropicApiKey\n anthropicModel = config.anthropicModel\n ollamaHost = config.ollamaHost\n ollamaModel = config.ollamaModel\n ollamaAutoPull = config.ollamaAutoPull\n softSubtitles = config.softSubtitles\n burnSubtitles = config.burnSubtitles\n fontName = config.fontName\n fontSize = config.fontSize\n fontColor = config.fontColor\n subtitleAlign = config.subtitleAlign\n marginV = config.marginV\n parallelTranslation = config.parallelTranslation\n maxWorkers = config.maxWorkers\n autoGenerateContext = config.autoGenerateContext\n translationContext = config.translationContext\n }\n\n func toAppConfig() -> AppConfig {\n AppConfig(\n whisperModel: whisperModel,\n sourceLang: sourceLang,\n targetLang: targetLang,\n translationBackend: translationBackend,\n deepseekApiKey: deepseekApiKey,\n deepseekModel: deepseekModel,\n deepseekBaseUrl: deepseekBaseUrl,\n openaiApiKey: openaiApiKey,\n openaiModel: openaiModel,\n openaiBaseUrl: openaiBaseUrl,\n anthropicApiKey: anthropicApiKey,\n anthropicModel: anthropicModel,\n ollamaHost: ollamaHost,\n ollamaModel: ollamaModel,\n ollamaAutoPull: ollamaAutoPull,\n softSubtitles: softSubtitles,\n burnSubtitles: burnSubtitles,\n fontName: fontName,\n fontSize: fontSize,\n fontColor: fontColor,\n subtitleAlign: subtitleAlign,\n marginV: marginV,\n parallelTranslation: parallelTranslation,\n maxWorkers: maxWorkers,\n autoGenerateContext: autoGenerateContext,\n translationContext: translationContext\n )\n }\n}\n"}}}}