import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; import '../../cekirdek/log.dart'; import '../../modeller/wangp_ayarlari.dart'; /// WanGP üretim istemcisi — **CLI mode** (`wgp.py --process settings.json`). /// /// Gradio API yerine WanGP'nin resmi headless `--process` argümanını kullanır. /// Bu yol: /// - Gradio session/state karmaşası yok /// - Net subprocess kontrolü (kill, exit code, stdout) /// - Server'ın açık olmasına gerek yok (CLI bağımsız çalışır) /// - stdout'tan ilerleme parse'lanabilir /// /// Akış: /// 1. Tüm parametreleri tek bir settings JSON olarak temp dosyaya yaz. /// 2. WanGP venv'inden `python wgp.py --process --output-dir ` /// subprocess başlat (cwd = Wan2GP klasörü). /// 3. stdout/stderr satır satır okuyup `ilerlemeCagrisi` ile yansıt. /// 4. Exit code = 0 ise output-dir'den en yeni .mp4'ü al, yoksa hata. class WanGpClient { /// Wan2GP kök klasörü (wgp.py burada). static const _wgpDir = r'D:\__sistem__\Masaustu\FikoYT.Yeni.Cag\video-uretim\ltx-desktop-wangp\LTX-Desktop-WanGP\Wan2GP'; /// WanGP'nin kendi sanal ortamı. Yoksa "python" fallback. static const _venvPython = r'D:\__sistem__\Masaustu\FikoYT.Yeni.Cag\video-uretim\ltx-desktop-wangp\LTX-Desktop-WanGP\Wan2GP\env_venv\Scripts\python.exe'; /// Mevcut çalışan subprocess (iptal için). Process? _aktifSurec; /// `iptalEt()` çağrıldığında `true` olur — exitCode != 0 dönen subprocess'in /// "iptal edildi" mi yoksa "hata" mı olduğunu ayırt etmek için. bool _iptalEdildi = false; /// CLI mod: dış URL'ye gerek yok. baseUrl yalnızca log/UI uyumu için. final String baseUrl; WanGpClient({this.baseUrl = 'http://127.0.0.1:7861'}); /// Hızlı sağlık kontrolü — Python ve wgp.py mevcut mu? Future sagliklimi() async { if (!await File(p.join(_wgpDir, 'wgp.py')).exists()) return false; return true; } /// Üretim — CLI subprocess'i çalıştırır, video yolunu döndürür. /// Tek-task mode (settings dict). Çoklu task için ileride genişletilir. Future> uret({ required String prompt, String modelType = 'ltx2_22B_distilled', String resolution = '1280x704', int? videoLength, int durationSeconds = 5, int fps = 24, int numInferenceSteps = 8, double? guidanceScale, double? embeddedGuidanceScale, int? seed, String? negativePrompt, String? imageStart, String? audioGuide, List? loraSecimleri, List? loraMultipliers, String cameraMotion = 'none', WanGpAyarlari? gelismis, Map ekstra = const {}, void Function(double oran, String? aciklama)? ilerlemeCagrisi, String? outputDir, /// VRAM güvenlik katsayısı — `--vram-safety-coefficient` CLI argümanı. /// Düşük (0.55-0.7) = GPU daha az coşar, daha az ısı, hız azalır. /// Default 0.8 (WanGP'nin kendi default'u). double? vramSafety, }) async { if (!await File(p.join(_wgpDir, 'wgp.py')).exists()) { throw Exception( 'WanGP bulunamadı: $_wgpDir — kurulumu kontrol et'); } // ── LoRA UYUMLULUK FİLTRESİ ─────────────────────────────────────── // Mimari uyumsuz LoRA'lar mmgp/offload.py:1553'te AssertionError verir // (loras_module_data is not None). Önce uyumsuz olanları temizle. final uyumluLoraSecimleri = _loraUyumlulukFiltresi(loraSecimleri, modelType); final uyumluMultipliers = loraMultipliers != null && loraSecimleri != null ? _multiplerleriEsle( loraSecimleri, uyumluLoraSecimleri, loraMultipliers) : loraMultipliers; final settings = _ayarlarBirlestir( prompt: prompt, modelType: modelType, resolution: resolution, videoLength: videoLength, durationSeconds: durationSeconds, fps: fps, numInferenceSteps: numInferenceSteps, guidanceScale: guidanceScale, embeddedGuidanceScale: embeddedGuidanceScale, seed: seed, negativePrompt: negativePrompt, imageStart: imageStart, audioGuide: audioGuide, loraSecimleri: uyumluLoraSecimleri, loraMultipliers: uyumluMultipliers, cameraMotion: cameraMotion, gelismis: gelismis, ekstra: ekstra, ); final tempDir = Directory.systemTemp.createTempSync('wangp_cli_'); final settingsPath = p.join(tempDir.path, 'settings.json'); // UTF-8 encoding zorla — Türkçe karakterli prompt'larda Windows sistem // encoding (cp1254) Python 3'ün UTF-8 okuyucusunda UnicodeDecodeError verir. await File(settingsPath).writeAsString( const JsonEncoder.withIndent(' ').convert(settings), encoding: utf8, ); final cikti = outputDir ?? p.join(tempDir.path, 'output'); await Directory(cikti).create(recursive: true); Log.bilgi( 'WanGpClient', 'CLI üretim: $modelType $resolution ${durationSeconds}s ' 'steps=$numInferenceSteps cfg=$guidanceScale → $cikti', ); final pythonExe = await File(_venvPython).exists() ? _venvPython : 'python'; final args = [ 'wgp.py', '--process', settingsPath, '--output-dir', cikti, // GPU coşturma azaltma: kullanıcı belirtmişse VRAM safety katsayısı. if (vramSafety != null) ...[ '--vram-safety-coefficient', vramSafety.toStringAsFixed(2), ], ]; ilerlemeCagrisi?.call(0.02, 'WanGP başlıyor'); final cikisOncesi = await _mp4Topla(cikti); Process surec; try { surec = await Process.start( pythonExe, args, workingDirectory: _wgpDir, environment: { 'PYTHONIOENCODING': 'utf-8', 'PYTHONUNBUFFERED': '1', }, runInShell: false, ); } catch (e) { tempDir.deleteSync(recursive: true); throw Exception('Python başlatılamadı: $e'); } _aktifSurec = surec; final hataLog = StringBuffer(); final cikisLog = StringBuffer(); // stdout: ilerleme & log final stdoutFut = surec.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((satir) { cikisLog.writeln(satir); _ilerlemeAyikla(satir, ilerlemeCagrisi); }).asFuture(); // stderr: tqdm progress + warnings final stderrFut = surec.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((satir) { hataLog.writeln(satir); _ilerlemeAyikla(satir, ilerlemeCagrisi); }).asFuture(); final exitKodu = await surec.exitCode; await stdoutFut; await stderrFut; _aktifSurec = null; if (exitKodu != 0) { tempDir.deleteSync(recursive: true); // İptal edildiyse net mesaj — kullanıcı "hata" değil "iptal" görsün if (_iptalEdildi) { _iptalEdildi = false; throw const _IptalEdildiException(); } final hamHata = hataLog.toString(); // settings.json'a yazılan LoRA listesini hata mesajına ekle final aktifLoralar = settings['activated_loras']; final loraOzet = aktifLoralar is List && aktifLoralar.isNotEmpty ? '\nKullanılan LoRA: ${aktifLoralar.join(", ")}' : ''; // Bilinen hata kalıpları için kullanıcı dostu mesaj if (hamHata.contains('loras_module_data is not None') || hamHata.contains('AssertionError') && hamHata.contains('load_loras_into_model')) { throw Exception( 'LoRA dosyası modelle uyumsuz.$loraOzet\n\n' 'Çözüm: Inspector → WanGP Gelişmiş Panel → Çoklu LoRA → ' 'tüm seçimleri kaldır. Veya Sihirbaz açıp şablon listesinden ' '"Son Durum" şablonu yerine boş başla. Model: $modelType'); } if (hamHata.contains('CUDA out of memory') || hamHata.contains('OutOfMemoryError')) { throw Exception( 'GPU belleği yetmedi. "Sessiz Mod" profili dene veya ' 'çözünürlük 480p\'ye düşür.'); } if (hamHata.contains('Sampler') && hamHata.contains('not supported')) { throw Exception( 'Bu sampler model tarafından desteklenmiyor. ' 'LTX-2 sadece "euler" veya "res2s" kabul eder.'); } final hataOzet = hamHata.split('\n').reversed .where((l) => l.trim().isNotEmpty) .take(5) .toList() .reversed .join('\n'); throw Exception( 'WanGP CLI exit=$exitKodu\nSon satırlar:\n$hataOzet'); } // Çıktıyı bul: çıkış öncesinden sonra eklenmiş .mp4 final cikisSonrasi = await _mp4Topla(cikti); final yeniler = cikisSonrasi.where((f) => !cikisOncesi.contains(f)).toList(); yeniler.sort((a, b) => File(b) .lastModifiedSync() .compareTo(File(a).lastModifiedSync())); if (yeniler.isEmpty) { tempDir.deleteSync(recursive: true); throw Exception( 'WanGP CLI 0 dönmesine rağmen video çıktısı bulunamadı: $cikti'); } final videoYolu = yeniler.first; Log.bilgi('WanGpClient', 'Üretim tamam: $videoYolu'); ilerlemeCagrisi?.call(1.0, 'Tamam'); // settings JSON'u sakla (debug), çıktı klasörünü temizleme return { 'video_path': videoYolu, 'settings_path': settingsPath, 'log_tail': cikisLog.toString().split('\n').reversed.take(20).toList(), }; } /// Çalışan üretimi iptal et — process tree dahil (CUDA worker'lar serbest kalır). /// /// **Windows kritik**: Dart'ın `Process.kill()` Windows'ta sadece üst seviye /// python.exe'yi öldürür — CUDA child process'leri ayakta kalır ve VRAM tutmaya /// devam eder. `taskkill /T /F /PID` → tüm tree'yi force kill, CUDA driver /// process sonlandığında VRAM otomatik serbest kalır. Future iptalEt() async { final s = _aktifSurec; if (s == null) return; final pid = s.pid; _aktifSurec = null; _iptalEdildi = true; // exit code yorumlanırken kullanılacak try { if (Platform.isWindows) { // /T = tree (alt process'ler dahil), /F = force final r = await Process.run( 'taskkill', ['/T', '/F', '/PID', '$pid'], ); if (r.exitCode == 0) { Log.bilgi('WanGpClient', 'taskkill /T /F /PID $pid → tamam'); } else { Log.uyari('WanGpClient', 'taskkill exit=${r.exitCode}: ${r.stderr}'); } } else { // Linux/Mac: önce SIGTERM (graceful), 2sn sonra SIGKILL s.kill(ProcessSignal.sigterm); await Future.delayed(const Duration(seconds: 2)); s.kill(ProcessSignal.sigkill); } } catch (e) { Log.uyari('WanGpClient', 'iptal hatası: $e'); } } /// stdout/stderr satırından ilerleme oranı ve faz adı parse et. /// WanGP/diffusers genelde tqdm üzerinden "X/Y [time> _mp4Topla(String klasor) async { final d = Directory(klasor); if (!await d.exists()) return const []; // Recursive: bazı modeller alt klasör (output/batch_0/x.mp4) oluşturur return d .listSync(recursive: true, followLinks: false) .whereType() .where((f) => f.path.toLowerCase().endsWith('.mp4') || f.path.toLowerCase().endsWith('.webm') || f.path.toLowerCase().endsWith('.mkv')) .map((f) => f.path) .toList(); } /// Video dosyasını hedef klasöre kopyala (Gradio API'siyle uyumluluk için). /// CLI mode'da video direkt diskte, indirme gerek yok — sadece kopyalanır. Future dosyaIndir(String uzakYol, String hedefYol) async { final kaynak = File(uzakYol); if (!await kaynak.exists()) { throw Exception('WanGP CLI çıktısı bulunamadı: $uzakYol'); } final hedef = File(hedefYol); await hedef.parent.create(recursive: true); await kaynak.copy(hedefYol); return hedef; } // ── Settings birleştirici (gelişmiş + standart) ───────────────────────── Map _ayarlarBirlestir({ required String prompt, required String modelType, required String resolution, int? videoLength, required int durationSeconds, required int fps, required int numInferenceSteps, double? guidanceScale, double? embeddedGuidanceScale, int? seed, String? negativePrompt, String? imageStart, String? audioGuide, List? loraSecimleri, List? loraMultipliers, required String cameraMotion, WanGpAyarlari? gelismis, required Map ekstra, }) { // video_length = toplam istenen kare sayısı. WanGP `duration_seconds`'ı // sadece UI etiket için kullanır; üretimde `video_length`'i okur. // Bu yüzden videoLength null olsa bile MUTLAKA hesaplanmış değeri gönderiyoruz. final hesaplananFrame = videoLength ?? _frameHesapla(durationSeconds, fps); final settings = { 'model_type': modelType, 'prompt': prompt, 'resolution': resolution, 'num_inference_steps': numInferenceSteps, 'duration_seconds': durationSeconds, // WanGP'de `force_fps` string olarak bekleniyor — "control", "source", // boş "", veya sayı stringi (`"24"`). int gönderilirse `len()` patlar. 'force_fps': fps.toString(), 'video_length': hesaplananFrame, // sliding_window_size: göndermiyoruz — model defaults'tan al. // Kullanıcı gelişmiş ayardan açıkça verirse `gelismis.slidingWindowSize` // alttaki blokta override eder. if (negativePrompt != null && negativePrompt.isNotEmpty) 'negative_prompt': negativePrompt, if (seed != null) 'seed': seed, if (guidanceScale != null) 'guidance_scale': guidanceScale, if (embeddedGuidanceScale != null) 'embedded_guidance_scale': embeddedGuidanceScale, if (imageStart != null && imageStart.isNotEmpty) ...{ 'image_prompt_type': 'S', 'image_start': imageStart, }, if (audioGuide != null && audioGuide.isNotEmpty) ...{ 'audio_prompt_type': 'A', 'audio_guide': audioGuide, }, if (loraSecimleri != null && loraSecimleri.isNotEmpty) 'activated_loras': loraSecimleri, if (loraMultipliers != null && loraMultipliers.isNotEmpty) 'loras_multipliers': loraMultipliers.map((m) => m.toString()).join(' '), }; if (gelismis != null) { _ekleEgerNull(settings, 'sample_solver', gelismis.sampleSolver); _ekleEgerNull(settings, 'flow_shift', gelismis.flowShift); if (guidanceScale == null) { _ekleEgerNull(settings, 'guidance_scale', gelismis.guidanceScale); } _ekleEgerNull(settings, 'guidance2_scale', gelismis.guidance2Scale); _ekleEgerNull(settings, 'guidance3_scale', gelismis.guidance3Scale); _ekleEgerNull(settings, 'switch_threshold', gelismis.switchThreshold); _ekleEgerNull(settings, 'switch_threshold2', gelismis.switchThreshold2); _ekleEgerNull(settings, 'guidance_phases', gelismis.guidancePhases); _ekleEgerNull(settings, 'model_switch_phase', gelismis.modelSwitchPhase); _ekleEgerNull(settings, 'alt_guidance_scale', gelismis.altGuidanceScale); _ekleEgerNull(settings, 'alt_scale', gelismis.altScale); _ekleEgerNull( settings, 'audio_guidance_scale', gelismis.audioGuidanceScale); _ekleEgerNull(settings, 'audio_scale', gelismis.audioScale); if (embeddedGuidanceScale == null) { _ekleEgerNull(settings, 'embedded_guidance_scale', gelismis.embeddedGuidanceScale); } _ekleEgerNull( settings, 'skip_steps_cache_type', gelismis.skipStepsCacheType); _ekleEgerNull( settings, 'skip_steps_multiplier', gelismis.skipStepsMultiplier); _ekleEgerNull(settings, 'skip_steps_start_step_perc', gelismis.skipStepsStartStepPerc); _ekleEgerNull( settings, 'override_attention', gelismis.overrideAttention); _ekleEgerNull(settings, 'override_profile', gelismis.overrideProfile); _ekleEgerNull( settings, 'sliding_window_size', gelismis.slidingWindowSize); _ekleEgerNull( settings, 'sliding_window_overlap', gelismis.slidingWindowOverlap); _ekleEgerNull(settings, 'sliding_window_color_correction_strength', gelismis.slidingWindowColorCorrectionStrength); _ekleEgerNull(settings, 'sliding_window_overlap_noise', gelismis.slidingWindowOverlapNoise); _ekleEgerNull(settings, 'sliding_window_discard_last_frames', gelismis.slidingWindowDiscardLastFrames); _ekleEgerNull(settings, 'RIFLEx_setting', gelismis.riflexSetting); _ekleEgerNull(settings, 'NAG_scale', gelismis.nagScale); _ekleEgerNull(settings, 'NAG_tau', gelismis.nagTau); _ekleEgerNull(settings, 'NAG_alpha', gelismis.nagAlpha); _ekleEgerNull(settings, 'apg_switch', gelismis.apgSwitch); _ekleEgerNull(settings, 'cfg_star_switch', gelismis.cfgStarSwitch); _ekleEgerNull(settings, 'cfg_zero_step', gelismis.cfgZeroStep); _ekleEgerNull( settings, 'perturbation_switch', gelismis.perturbationSwitch); _ekleEgerNull( settings, 'perturbation_layers', gelismis.perturbationLayers); _ekleEgerNull(settings, 'perturbation_start_perc', gelismis.perturbationStartPerc); _ekleEgerNull( settings, 'perturbation_end_perc', gelismis.perturbationEndPerc); _ekleEgerNull( settings, 'temporal_upsampling', gelismis.temporalUpsampling); _ekleEgerNull( settings, 'spatial_upsampling', gelismis.spatialUpsampling); _ekleEgerNull( settings, 'film_grain_intensity', gelismis.filmGrainIntensity); _ekleEgerNull( settings, 'film_grain_saturation', gelismis.filmGrainSaturation); _ekleEgerNull(settings, 'MMAudio_setting', gelismis.mmAudioSetting); _ekleEgerNull(settings, 'MMAudio_prompt', gelismis.mmAudioPrompt); _ekleEgerNull(settings, 'MMAudio_neg_prompt', gelismis.mmAudioNegPrompt); _ekleEgerNull(settings, 'image_prompt_type', gelismis.imagePromptType); _ekleEgerNull(settings, 'image_end', gelismis.imageEnd); _ekleEgerNull(settings, 'video_source', gelismis.videoSource); _ekleEgerNull(settings, 'keep_frames_video_source', gelismis.keepFramesVideoSource); _ekleEgerNull( settings, 'input_video_strength', gelismis.inputVideoStrength); _ekleEgerNull(settings, 'video_prompt_type', gelismis.videoPromptType); _ekleEgerNull(settings, 'image_refs_relative_size', gelismis.imageRefsRelativeSize); _ekleEgerNull(settings, 'remove_background_images_ref', gelismis.removeBackgroundImagesRef); _ekleEgerNull(settings, 'control_net_weight', gelismis.controlNetWeight); _ekleEgerNull( settings, 'control_net_weight2', gelismis.controlNetWeight2); _ekleEgerNull( settings, 'control_net_weight_alt', gelismis.controlNetWeightAlt); _ekleEgerNull(settings, 'motion_amplitude', gelismis.motionAmplitude); _ekleEgerNull(settings, 'mask_expand', gelismis.maskExpand); _ekleEgerNull(settings, 'denoising_strength', gelismis.denoisingStrength); _ekleEgerNull(settings, 'masking_strength', gelismis.maskingStrength); _ekleEgerNull(settings, 'audio_guide2', gelismis.audioGuide2); _ekleEgerNull(settings, 'audio_source', gelismis.audioSource); _ekleEgerNull(settings, 'audio_prompt_type', gelismis.audioPromptType); _ekleEgerNull(settings, 'speakers_locations', gelismis.speakersLocations); _ekleEgerNull( settings, 'self_refiner_setting', gelismis.selfRefinerSetting); _ekleEgerNull(settings, 'self_refiner_plan', gelismis.selfRefinerPlan); _ekleEgerNull(settings, 'self_refiner_f_uncertainty', gelismis.selfRefinerFUncertainty); _ekleEgerNull(settings, 'self_refiner_certain_percentage', gelismis.selfRefinerCertainPercentage); _ekleEgerNull(settings, 'batch_size', gelismis.batchSize); _ekleEgerNull(settings, 'repeat_generation', gelismis.repeatGeneration); _ekleEgerNull( settings, 'multi_prompts_gen_type', gelismis.multiPromptsGenType); _ekleEgerNull( settings, 'multi_images_gen_type', gelismis.multiImagesGenType); _ekleEgerNull(settings, 'min_frames_if_references', gelismis.minFramesIfReferences); _ekleEgerNull(settings, 'pause_seconds', gelismis.pauseSeconds); _ekleEgerNull(settings, 'prompt_enhancer', gelismis.promptEnhancer); _ekleEgerNull(settings, 'temperature', gelismis.temperature); _ekleEgerNull(settings, 'top_p', gelismis.topP); _ekleEgerNull(settings, 'top_k', gelismis.topK); if ((loraSecimleri == null || loraSecimleri.isEmpty) && gelismis.loraDosyalari.isNotEmpty) { settings['activated_loras'] = gelismis.loraDosyalari; if (gelismis.loraMultipliers.isNotEmpty) { settings['loras_multipliers'] = gelismis.loraMultipliers.map((m) => m.toString()).join(' '); } } // Meta key'ler — settings.json'a yazılmaz, CLI argümanı veya orchestrator // mantığı için kullanılır. const metaKeyler = { 'vram_safety_coefficient', // → CLI: --vram-safety-coefficient }; gelismis.ozelAlanlar.forEach((k, v) { if (v != null && !metaKeyler.contains(k)) { settings[k] = v; } }); } settings.addAll(ekstra); // ── MODEL-BAZLI SAMPLER DÜZELTMESİ ───────────────────────────────── // LTX-2 modelleri sadece 'euler' ve 'res2s' kabul eder, 'unipc' reddedilir. // Wan2.1 ise 'unipc'/'euler'/'dpm++' kabul eder. // Profillerde global olarak 'unipc' set edildiği için, model LTX-2 ise // otomatik 'euler'a düşür (kanıt: ltx2_handler.py:457). final modelTipi = settings['model_type']?.toString().toLowerCase() ?? ''; final ltx2Mi = modelTipi.contains('ltx2') || modelTipi.contains('ltx-2') || modelTipi.startsWith('ltx2_'); if (ltx2Mi) { final ss = settings['sample_solver']?.toString().toLowerCase() ?? ''; if (ss.isNotEmpty && !{'euler', 'res2s'}.contains(ss)) { Log.bilgi('WanGpClient', 'LTX-2 sampler düzeltme: $ss → euler (LTX-2 unipc desteklemiyor)'); settings['sample_solver'] = 'euler'; } } // ── TİP NORMALİZASYONU (WanGP UI Gradio component'lerine göre) ── // 1) String bekleyen sayısal alanlar (Gradio Textbox) — int gelirse `len()` patlar. const stringBekliyor = [ 'force_fps', 'keep_frames_video_source', 'keep_frames_video_guide', ]; for (final k in stringBekliyor) { final v = settings[k]; if (v != null && v is! String) { settings[k] = v.toString(); } } // 2) "OFF=0 / ON=1" Dropdown'lar — Dart bool gönderiyor ama WanGP int bekliyor. // `if apg_switch == 0:` gibi int kıyaslar var, bool truthy karşılığı yok. const boolUntoInt = [ 'apg_switch', 'cfg_star_switch', 'perturbation_switch', 'remove_background_images_ref', // Gradio Checkbox → Python `if x == 1` ]; for (final k in boolUntoInt) { final v = settings[k]; if (v is bool) { settings[k] = v ? 1 : 0; } } return settings; } static void _ekleEgerNull( Map harita, String anahtar, Object? deger) { if (deger != null) harita[anahtar] = deger; } /// Bilinen modeller — defaults klasöründen okumak için fallback. static const List bilinenModeller = [ 'ltx2_22B_distilled', 'ltx2_22B', 'ltx2_22B_1_1', 'ltx2_22B_distilled_1_1', 'ltx2_22B_distilled_gguf_q4_k_m', 'ltx2_22B_distilled_gguf_q6_k', 'ltx2_22B_distilled_gguf_q8_0', 'ltx2_22B_nvfp4', 'ltx2_19B', 'ltx2_19B_nvfp4', 'ltx2_distilled', 'ltx2_distilled_gguf_q4_k_m', 'ltx2_distilled_gguf_q6_k', 'ltx2_distilled_gguf_q8_0', 'ltxv_13B', 'ltxv_distilled', ]; static int _frameHesapla(int sure, int fps) => ((sure * fps) ~/ 8) * 8 + 1; /// LoRA dosya adıyla model mimarisi uyumlu mu? /// `loras_module_data is not None` AssertionError'ını engellemek için /// ön-filtre. Yanlış mimari LoRA atlanır + log uyarı. /// /// Kurallar: /// - 22B LoRA + 19B model → REDDED (boyut uyumsuz) /// - 19B LoRA + 22B model → REDDED /// - Wan2.1 LoRA + LTX-2 model → REDDED (mimari uyumsuz) /// - Belirsiz LoRA (boyut prefix yok) → kabul (kullanıcı sorumluluğu) static List? _loraUyumlulukFiltresi( List? loralar, String modelType) { if (loralar == null || loralar.isEmpty) return loralar; final mt = modelType.toLowerCase(); final ltx2Model = mt.contains('ltx2') || mt.contains('ltx-2'); final wanModel = mt.contains('wan2.1') || mt.contains('wan2_1'); final hangiBoyut = mt.contains('19b') ? '19b' : mt.contains('22b') ? '22b' : mt.contains('14b') ? '14b' : null; final uyumlular = []; for (final l in loralar) { final ll = l.toLowerCase(); // 1. Mimari kontrolü (LTX-2 vs Wan2.1) if (ltx2Model && (ll.contains('wan2.1') || ll.contains('wan_'))) { Log.uyari('WanGpClient', 'LoRA atlandı (mimari uyumsuz): $l → $modelType (Wan→LTX2)'); continue; } if (wanModel && (ll.contains('ltx2') || ll.contains('ltx-2'))) { Log.uyari('WanGpClient', 'LoRA atlandı (mimari uyumsuz): $l → $modelType (LTX2→Wan)'); continue; } // 2. Boyut kontrolü (19B/22B/14B) — sürüm prefix dahil // ltx-2.3-22b-* gibi sürüm-bazlı isimleri de yakala if (hangiBoyut != null) { final loraBoyut = ll.contains('22b') ? '22b' : ll.contains('19b') ? '19b' : ll.contains('14b') ? '14b' : null; if (loraBoyut != null && loraBoyut != hangiBoyut) { Log.uyari('WanGpClient', 'LoRA atlandı (boyut uyumsuz): $l ($loraBoyut) → $modelType ($hangiBoyut)'); continue; } } // 3. LTX-2 sürüm uyumluluğu: 2.0 vs 2.3 // ltx-2-19b-* → LTX-2 2.0 (19B distilled = 2.0 distilled) // ltx-2.3-22b-* → LTX-2 2.3 (22B = 2.3) if (ltx2Model && hangiBoyut == '19b' && ll.contains('2.3')) { Log.uyari('WanGpClient', 'LoRA atlandı (sürüm uyumsuz): $l (2.3) → $modelType (2.0/19B)'); continue; } // 4. Dosya gerçekten var mı? (loras/, loras/ltx2/, loras/wan/, loras/wan_i2v/) bool bulundu = false; for (final altKlasor in [ '', 'ltx2', 'wan', 'wan_i2v', ]) { final yol = altKlasor.isEmpty ? p.join(_wgpDir, 'loras', l) : p.join(_wgpDir, 'loras', altKlasor, l); if (File(yol).existsSync()) { bulundu = true; break; } } if (!bulundu) { Log.uyari('WanGpClient', 'LoRA atlandı (dosya bulunamadı): $l'); continue; } uyumlular.add(l); } if (uyumlular.length < loralar.length) { Log.bilgi( 'WanGpClient', 'LoRA filtresi: ${loralar.length} → ${uyumlular.length} ' '(${loralar.length - uyumlular.length} uyumsuz atlandı)'); } return uyumlular.isEmpty ? null : uyumlular; } /// LoRA listesi filtre sonrası küçüldüğünde multiplier'ları paralel kes. static List? _multiplerleriEsle( List? orijLoralar, List? uyumluLoralar, List orijMultipliers, ) { if (orijLoralar == null || uyumluLoralar == null) return null; if (uyumluLoralar.isEmpty) return null; final yeni = []; for (var i = 0; i < orijLoralar.length; i++) { if (uyumluLoralar.contains(orijLoralar[i])) { yeni.add(i < orijMultipliers.length ? orijMultipliers[i] : 1.0); } } return yeni; } } /// İptal sinyali yutulduğunda fırlatılan tip — orchestrator bunu yakalayıp /// "iptal edildi" mesajı ile job durumunu kapatır (hata yerine). class _IptalEdildiException implements Exception { const _IptalEdildiException(); @override String toString() => 'WanGP üretimi iptal edildi'; }