import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import '../../cekirdek/log.dart'; class LtxStatus { final String durum; // idle/running/complete/error/cancelled final String faz; final double ilerleme; const LtxStatus({ required this.durum, required this.faz, required this.ilerleme, }); static LtxStatus fromJson(Map j) => LtxStatus( durum: (j['status'] ?? 'unknown').toString(), faz: (j['phase'] ?? '').toString(), ilerleme: (j['progress'] is num) ? (j['progress'] as num).toDouble() : 0.0, ); } class LtxGenerateResult { final String durum; final String? videoPath; final Map ham; const LtxGenerateResult({ required this.durum, this.videoPath, this.ham = const {}, }); } class LtxClient { final String url; final String? token; late final Dio _dio; LtxClient({required this.url, this.token}) { _dio = Dio( BaseOptions( baseUrl: url, connectTimeout: const Duration(seconds: 10), sendTimeout: const Duration(minutes: 30), receiveTimeout: const Duration(minutes: 30), headers: { 'Content-Type': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }, ), ); } Future ilerleme() async { final r = await _dio.get('/api/generation/progress'); return LtxStatus.fromJson(r.data as Map); } Future> modellerDurum() async { try { final r = await _dio.get('/api/models/status'); return Map.from(r.data as Map); } catch (_) { return {}; } } Future uret({ required String prompt, String? imagePath, String? audioPath, required String model, // fast/pro required String duration, // "5"/"6"/... required String resolution, // 540p/720p/... required String aspectRatio, // 16:9 / 9:16 required String fps, // "24" required String cameraMotion, // "none" required bool audioAcik, String? negativePrompt, double? guidanceScale, int? numInferenceSteps, int? seed, String? referenceFacePath, // WanGP/InfiniteTalk için String? loraPath, }) async { final body = { 'prompt': prompt, 'model': model, 'duration': duration, 'resolution': resolution, 'fps': fps, 'audio': audioAcik ? 'true' : 'false', 'cameraMotion': cameraMotion, 'aspectRatio': aspectRatio, }; if (imagePath != null && imagePath.isNotEmpty) { body['imagePath'] = imagePath; } if (audioAcik && audioPath != null && audioPath.isNotEmpty) { body['audioPath'] = audioPath; } if (negativePrompt != null) body['negativePrompt'] = negativePrompt; if (guidanceScale != null) body['guidanceScale'] = guidanceScale; if (numInferenceSteps != null) { body['numInferenceSteps'] = numInferenceSteps; } // LTX FastAPI ekstra alanları reddediyor (422). Bu alanları // sadece WanGP modu seçildiğinde gönder (referans yüz/lora) ya da // LTX'in kabul ettiği şema güncellenince ekle. // Şimdilik seed: LTX zaten her çağrıda rastgele seed kullanıyor, // böyle 5-seed batch otomatik çeşitlilik sağlıyor. Log.bilgi( 'LtxClient', 'Üretim: süre=${duration}s · çöz=$resolution · model=$model · ' 'kamera=$cameraMotion · oran=$aspectRatio · audio=$audioAcik · ' 'fps=$fps · neg=${negativePrompt?.isNotEmpty == true ? "var" : "yok"}', ); Response r; try { r = await _dio.post('/api/generate', data: body); } on DioException catch (e) { // 422 Unprocessable Entity gibi durumlarda FastAPI detail JSON'unu hata // mesajına aktar — kullanıcı hangi alanın reddedildiğini görebilsin. final ham = e.response?.data; final detay = ham is Map ? (ham['detail'] ?? ham).toString() : ham.toString(); Log.hata('LtxClient', 'POST /api/generate ${e.response?.statusCode}: $detay\nGönderilen body: $body'); throw Exception( 'LTX ${e.response?.statusCode ?? "?"}: $detay'); } final data = r.data as Map; Log.bilgi('LtxClient', 'Yanıt durum=${data['status']} video=${data['video_path']}'); return LtxGenerateResult( durum: (data['status'] ?? '').toString(), videoPath: data['video_path'] as String?, ham: Map.from(data), ); } /// LTX Desktop settings.json dosyasının yolu (Windows). /// `%LOCALAPPDATA%\LTXDesktop\settings.json` static String get _settingsDosyasi { final localAppData = Platform.environment['LOCALAPPDATA'] ?? '${Platform.environment['USERPROFILE']}\\AppData\\Local'; return '$localAppData\\LTXDesktop\\settings.json'; } /// LTX Desktop ayarlarını "GPU rahat" moduna çek. /// **Doğrudan settings.json dosyasını düzenler** — auth token gerekmiyor. /// LTX Desktop bir sonraki render'da yeni ayarları okur (restart gerekirse). /// /// Etkisi: /// - `fast_model.use_upscaler: false` → 2× spatial upscale yok (%50 az iş) /// - `pro_model.use_upscaler: false` /// - `prompt_enhancer_enabled_t2v: false` → Qwen yüklenmez (RAM tasarrufu) /// - `prompt_enhancer_enabled_i2v: false` /// - `use_torch_compile: false` → derleme overhead'i yok Future rahatModuUygula() async { return _ayarDegistir({ 'fast_model': {'use_upscaler': false}, 'pro_model': {'use_upscaler': false}, 'prompt_enhancer_enabled_t2v': false, 'prompt_enhancer_enabled_i2v': false, }); } /// Varsayılan kalite moduna döndür (upscaler+enhancer açılır). Future kaliteModuUygula() async { return _ayarDegistir({ 'fast_model': {'use_upscaler': true}, 'pro_model': {'use_upscaler': true}, 'prompt_enhancer_enabled_t2v': true, 'prompt_enhancer_enabled_i2v': true, }); } /// settings.json'da belirli alanları değiştir, geri kalanı korur. /// İç içe nested map'leri merge eder (deep merge). Future _ayarDegistir(Map degisiklikler) async { try { final f = File(_settingsDosyasi); if (!await f.exists()) { Log.uyari('LtxClient', 'settings.json yok: $_settingsDosyasi — LTX Desktop hiç açılmadı?'); return false; } final mevcut = jsonDecode(await f.readAsString()) as Map; // Deep merge void birlestir(Map hedef, Map kaynak) { kaynak.forEach((k, v) { if (v is Map && hedef[k] is Map) { birlestir(hedef[k] as Map, v); } else { hedef[k] = v; } }); } birlestir(mevcut, degisiklikler); // Atomik yazma (önce .tmp, sonra rename) final tmp = File('${f.path}.tmp'); await tmp.writeAsString( const JsonEncoder.withIndent(' ').convert(mevcut)); await tmp.rename(f.path); Log.bilgi('LtxClient', 'settings.json güncellendi: $degisiklikler'); return true; } catch (e) { Log.hata('LtxClient', 'settings dosya hata: $e'); return false; } } /// Mevcut settings.json içeriğini döner (upscaler/enhancer durumu). Future?> ayarlariOku() async { try { final f = File(_settingsDosyasi); if (!await f.exists()) return null; return jsonDecode(await f.readAsString()) as Map; } catch (_) { return null; } } /// Mevcut LTX Desktop ayarlarını çek (upscaler/enhancer durumu). Future?> ayarlariCek() async { try { final r = await _dio.get('/api/settings'); return r.data is Map ? Map.from(r.data as Map) : null; } catch (_) { return null; } } /// LTX boşa düşene kadar bekler. Future idleBekle({Duration zaman = const Duration(minutes: 5)}) async { final son = DateTime.now().add(zaman); while (DateTime.now().isBefore(son)) { try { final s = await ilerleme(); if (const ['idle', 'complete', 'error', 'cancelled'].contains(s.durum)) { return true; } } catch (_) {} await Future.delayed(const Duration(seconds: 2)); } return false; } }