import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:fikoyt_video_uret/modeller/cikis_ayarlari.dart' hide GecisAyari, GecisTipi; import 'package:fikoyt_video_uret/servisler/ffmpeg/ffmpeg_filtre.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path/path.dart' as p; import '../cekirdek/kimlik.dart'; import '../cekirdek/log.dart'; import '../cekirdek/sabitler.dart'; import '../modeller/job.dart'; import '../modeller/proje.dart'; import '../modeller/segment.dart'; import 'face_fusion_runner.dart'; import 'ffmpeg/ffmpeg_servis.dart'; import 'ffmpeg/kare_cikarici.dart'; import 'ltx/ltx_baglanti_servisi.dart'; import 'ltx/ltx_client.dart'; import 'analiz/ses_enerjisi.dart'; import 'proje_deposu.dart'; import 'segment_olusturucu.dart'; import 'wangp/wangp_baslatici.dart'; import 'wangp/wangp_client.dart'; /// Tüm job'ları yöneten merkezi orchestrator. /// Hive'da kalıcı, resume destekli, paralelizasyon yetenekli. class JobOrkestratoru { static JobOrkestratoru? _ornek; static JobOrkestratoru get ornek => _ornek ??= JobOrkestratoru._(); JobOrkestratoru._(); Box? _kutu; final _events = StreamController.broadcast(); Stream get events => _events.stream; bool _isleyiciCalisiyor = false; bool _duraklatildi = false; final Map> _iptalTokenleri = {}; /// Anlık LTX bağlantı durumunu döndüren callback (Riverpod provider'a bağlanır). LtxBaglanti Function()? _ltxOku; /// LTX bağlantı kaynağını ayarla (uygulama başlatınca çağrılır). void ltxKaynagiAyarla(LtxBaglanti Function() okuyucu) { _ltxOku = okuyucu; } /// İzleme modu — açıksa biten segmentin ardından sıradakini otomatik kuyruğa al. bool Function()? _izlemeModuOku; void izlemeModuKaynagiAyarla(bool Function() okuyucu) { _izlemeModuOku = okuyucu; } /// %85 bildirimi gönderilmiş job'lar (toplama bir kere için). final Set _bildirilenJoblar85 = {}; Future _trayBildir(String baslik, String mesaj) async { if (!Platform.isWindows) return; try { final guvenliBaslik = baslik.replaceAll("'", "''"); final guvenliMesaj = mesaj.replaceAll("'", "''"); await Process.run('powershell', [ '-WindowStyle', 'Hidden', '-Command', "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] | Out-Null; " "\$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02); " "\$nodes = \$t.GetElementsByTagName('text'); " "[void]\$nodes.Item(0).AppendChild(\$t.CreateTextNode('$guvenliBaslik')); " "[void]\$nodes.Item(1).AppendChild(\$t.CreateTextNode('$guvenliMesaj')); " "\$toast = [Windows.UI.Notifications.ToastNotification]::new(\$t); " "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('FikoYT').Show(\$toast)" ]); } catch (_) {} } Future baslat({LtxBaglanti? ltxBaglanti}) async { _kutu ??= await Hive.openBox('joblar'); // Bootstrap: çalışan job'ları kuyruğa geri at final guncellemeler = {}; for (final entry in _kutu!.toMap().entries) { try { final j = Job.fromJson(Map.from(entry.value as Map)); if (j.durum == JobDurum.calisiyor) { guncellemeler[j.id] = j .copyWith(durum: JobDurum.kuyruk, fazAciklama: 'Resume') .toJson(); } } catch (_) {} } for (final e in guncellemeler.entries) { await _kutu!.put(e.key, e.value); } _isleyiciDondur(ltxBaglanti); } Future> tumJoblar() async { _kutu ??= await Hive.openBox('joblar'); final liste = []; for (final v in _kutu!.values) { try { liste.add(Job.fromJson(Map.from(v as Map))); } catch (_) {} } liste.sort((a, b) { final cmp = a.durum.index.compareTo(b.durum.index); if (cmp != 0) return cmp; return b.oncelik.compareTo(a.oncelik); }); return liste; } Future ekle(Job j) async { _kutu ??= await Hive.openBox('joblar'); await _kutu!.put(j.id, j.toJson()); _events.add(j); return j; } Future guncelle(Job j) async { _kutu ??= await Hive.openBox('joblar'); await _kutu!.put(j.id, j.toJson()); _events.add(j); } Future iptal(String jobId) async { final c = _iptalTokenleri.remove(jobId); c?.complete(); final j = await _jobOku(jobId); if (j != null) { await guncelle(j.copyWith(durum: JobDurum.iptal, bitis: DateTime.now())); } } void duraklat(bool durum) => _duraklatildi = durum; Future _jobOku(String id) async { _kutu ??= await Hive.openBox('joblar'); final v = _kutu!.get(id); if (v == null) return null; return Job.fromJson(Map.from(v as Map)); } void _isleyiciDondur(LtxBaglanti? ltx) async { if (_isleyiciCalisiyor) return; _isleyiciCalisiyor = true; while (_isleyiciCalisiyor) { if (_duraklatildi) { await Future.delayed(const Duration(seconds: 1)); continue; } final tumu = await tumJoblar(); final siradaki = tumu.firstWhere( (j) => j.durum == JobDurum.kuyruk, orElse: () => Job(id: '', tip: JobTipi.segmentUret, projeId: ''), ); if (siradaki.id.isEmpty) { await Future.delayed(const Duration(seconds: 1)); continue; } try { await _jobYurut(siradaki, _ltxOku?.call() ?? ltx); } catch (e, st) { Log.hata('JobOrk', 'Yürütme hatası: $e\n$st'); await guncelle(siradaki.copyWith( durum: JobDurum.hata, hata: e.toString(), bitis: DateTime.now(), )); } } } Future _jobYurut(Job j, LtxBaglanti? ltx) async { final iptal = Completer(); _iptalTokenleri[j.id] = iptal; await guncelle(j.copyWith( durum: JobDurum.calisiyor, baslama: DateTime.now(), ilerleme: 0, fazAciklama: 'Başlatılıyor', )); final proje = await _projeYukle(j.projeId); if (proje == null) { await guncelle(j.copyWith( durum: JobDurum.hata, hata: 'Proje bulunamadı', bitis: DateTime.now(), )); return; } switch (j.tip) { case JobTipi.segmentUret: await _segmentUret(j, proje, ltx, iptal); case JobTipi.topluUret: await _topluUret(j, proje, ltx, iptal); case JobTipi.analiziYap: await _analiziYap(j, proje, iptal); case JobTipi.enIyiKareyiCikar: await _enIyiKareyiCikarJob(j, proje, iptal); case JobTipi.birlestir: await _birlestir(j, proje, iptal); default: await guncelle(j.copyWith( durum: JobDurum.hata, hata: 'Bu job tipi henüz implement edilmedi: ${j.tip.name}', bitis: DateTime.now(), )); } } Future _projeYukle(String projeId) async { final liste = await ProjeDeposu.sonProjeler(); final ozet = liste.firstWhere( (o) => o.id == projeId, orElse: () => ProjeOzeti( id: '', isim: '', klasorYolu: '', guncellendi: DateTime.now(), ), ); if (ozet.id.isEmpty) return null; return ProjeDeposu.yukle(ozet.klasorYolu); } // ─── Job Yürütücüler ────────────────────────────────────────────────────── Future _segmentUret( Job j, Proje proje, LtxBaglanti? ltx, Completer iptal, ) async { final segId = j.params['segmentId'] as String; final seg = proje.segmentler.firstWhere( (s) => s.id == segId, orElse: () => Segment(id: '', sira: -1, basla: 0, bit: 0), ); if (seg.id.isEmpty) { await guncelle(j.copyWith( durum: JobDurum.hata, hata: 'Segment bulunamadı', bitis: DateTime.now(), )); return; } // Motor seçim mantığı (auto-fallback): // - Segment WanGP istiyor VE WanGP açık → WanGP'ye git // - Segment LTX istiyor VE LTX bağlı → LTX'e git // - Segment LTX istiyor AMA LTX kapalı + WanGP açık → otomatik WanGP'ye yönlendir // - Segment WanGP istiyor AMA WanGP kapalı + LTX bağlı → otomatik LTX'e yönlendir // - İkisi de kapalı → uygun hata mesajı final wangpAcik = await WanGpBaslatici.calisiyorMu(); final ltxAcik = ltx != null && ltx.baglandi && ltx.client != null; if (seg.motor == SegmentMotor.wangp) { if (wangpAcik) { await _segmentUretWanGp(j, proje, seg, iptal); return; } if (!ltxAcik) { await guncelle(j.copyWith( durum: JobDurum.hata, hata: 'WanGP Standalone (port 7861) kapalı. Topbar\'daki ' '"WanGP başlat" rozetinden başlat veya ' 'WanGP-Standalone-Baslat.bat çalıştır.', bitis: DateTime.now(), )); return; } Log.bilgi('JobOrk', 'WanGP kapalı, LTX açık → segment LTX\'e otomatik yönlendirildi'); // LTX akışına devam et (aşağıdaki kod) } else { // motor == ltx if (!ltxAcik) { if (wangpAcik) { Log.bilgi('JobOrk', 'LTX kapalı, WanGP açık → segment WanGP\'ye otomatik yönlendirildi'); await _segmentUretWanGp(j, proje, seg, iptal); return; } await guncelle(j.copyWith( durum: JobDurum.hata, hata: 'Hiçbir motor açık değil. LTX-Desktop-WanGP-Baslat.bat ' 'veya WanGP-Standalone-Baslat.bat çalıştır.', bitis: DateTime.now(), )); return; } } // Parametreleri global'den miras alarak hazırla final g = proje.global; final cozunurluk = seg.cozunurluk ?? g.cozunurluk; final istenenSure = seg.sure ?? g.segmentSuresi; final sure = Sabitler.izinliSureyeYuvarla(istenenSure, cozunurluk); Log.bilgi( 'JobOrk', 'S${seg.sira + 1}: istenen=${istenenSure}s → gönderilecek=${sure}s ' '(çözünürlük=$cozunurluk, segment=${seg.sure}, global=${g.segmentSuresi})', ); // Giriş görseli öncelik sırası: // 1. Segment'in manuel override'ı (Inspector'dan seçilen özel görsel) // 2. Önceki segment'in son karesi (zincir devamı — segmentler arası geçiş) // 3. Job param 'girisGorseli' (eski uyumluluk) // 4. Projenin genel başlangıç görseli String? imagePath = seg.girisGorseliOverridePath; if (imagePath == null || imagePath.isEmpty) { // Önceki segmentin son karesini ara if (seg.sira > 0) { try { final onceki = proje.segmentler.firstWhere( (x) => x.sira == seg.sira - 1, ); if (onceki.sonKareYolu != null && onceki.sonKareYolu!.isNotEmpty && await File(onceki.sonKareYolu!).exists()) { imagePath = onceki.sonKareYolu; Log.bilgi('JobOrk', 'S${seg.sira + 1} giriş = S${onceki.sira + 1} son karesi (zincir)'); } } catch (_) {} } } imagePath ??= (j.params['girisGorseli'] as String?) ?? proje.kaynak.girisGorseliPath; if (imagePath != null && (imagePath.isEmpty || !await File(imagePath).exists())) { imagePath = null; } // sesYolu (job param) range ile kesilmiş audio dosyasıdır — bu öncelikli. // Yoksa raw override veya proje sesini kullan (range uygulanmaz, son çare). String? audioPath = (j.params['sesYolu'] as String?) ?? seg.sesOverridePath ?? proje.kaynak.sesPath; if (audioPath != null && (audioPath.isEmpty || !await File(audioPath).exists())) { audioPath = null; } // Segmenti "uretiliyor" durumuna geçir (UI'da badge için) { final liste = proje.segmentler .map((s) => s.id == seg.id ? s.copyWith(durum: SegmentDurum.uretiliyor, hataMesaji: null) : s) .toList(); await ProjeDeposu.kaydet(proje.copyWith(segmentler: liste)); } // idleBekle kaldırıldı: LTX-Desktop-WanGP zaten meşgulse 409 döner, // bizim ayrı beklememize gerek yok. Direkt üretime geç. await guncelle(j.copyWith(ilerleme: 10, fazAciklama: 'Üretim başlıyor')); final aciksesi = (seg.ses ?? g.ses) && audioPath != null; // Seed: önce job override (5-seed batch'ten), yoksa segment seed'i final seedKullan = (j.params['seedOverride'] as int?) ?? seg.seed; // Motor wangp ise LTX'in kendi InfiniteTalk modeli kullanılır. final modelAdi = seg.motor == SegmentMotor.wangp ? 'wangp' : (seg.model ?? g.model); // WanGP modeli için referans yüz: önce segment alanı, yoksa face swap havuzu String? referansYuz; if (seg.motor == SegmentMotor.wangp) { referansYuz = (seg.wangpReferansYuzPath != null && seg.wangpReferansYuzPath!.isNotEmpty) ? seg.wangpReferansYuzPath : (seg.faceSwapKaynakYollari.isNotEmpty ? seg.faceSwapKaynakYollari.first : (proje.global.faceSwapKaynakYollari.isNotEmpty ? proje.global.faceSwapKaynakYollari.first : proje.kaynak.faceSwapPath)); } // Eski projelerden kalan kamera hareketi değerlerini LTX'in beklediği // şemaya çevir (LTX 422 vermesin diye). final hamKamera = seg.kameraHareketi ?? g.kameraHareketi; const kameraEslestirme = { 'zoom-in': 'dolly_in', 'zoom-out': 'dolly_out', 'pan-left': 'dolly_left', 'pan-right': 'dolly_right', 'tilt-up': 'jib_up', 'tilt-down': 'jib_down', }; final kameraHareketi = kameraEslestirme[hamKamera] ?? hamKamera; // Üretim isteğini başlat (await etme — paralel pollama yapacağız) final uretimFuture = ltx.client!.uret( prompt: seg.prompt ?? g.varsayilanPrompt, negativePrompt: seg.negatifPrompt ?? g.varsayilanNegatifPrompt, imagePath: imagePath, audioPath: audioPath, model: modelAdi, duration: sure.toString(), resolution: cozunurluk, aspectRatio: seg.oran ?? g.oran, fps: Sabitler.varsayilanFps, cameraMotion: kameraHareketi, audioAcik: aciksesi, guidanceScale: seg.guidanceScale ?? g.guidanceScale, numInferenceSteps: seg.steps ?? g.steps, seed: seedKullan, referenceFacePath: referansYuz, loraPath: seg.wangpLoraPath, ); // Paralel olarak LTX progress'ini polla, job'a yansıt Timer? pollTimer; bool tamamlandi = false; pollTimer = Timer.periodic(const Duration(milliseconds: 1200), (_) async { if (tamamlandi) return; try { final s = await ltx.client!.ilerleme(); if (tamamlandi) return; // Progress 0-100 arası, biz 10-80 aralığında map'liyoruz final mapped = (10 + (s.ilerleme * 0.7)).clamp(10.0, 80.0); final faz = s.faz.isNotEmpty ? s.faz : 'üretiliyor'; await guncelle(j.copyWith( ilerleme: mapped, fazAciklama: 'LTX: $faz · %${s.ilerleme.toStringAsFixed(0)}', )); // %85 yaklaşma bildirimi (her job için bir kere) if (s.ilerleme >= 85 && !_bildirilenJoblar85.contains(j.id)) { _bildirilenJoblar85.add(j.id); unawaited(_trayBildir( 'FikoYT · S${seg.sira + 1}', 'Segment yakında bitecek (%${s.ilerleme.toStringAsFixed(0)})', )); } } catch (_) { // Pollama hatası göz ardı } }); LtxGenerateResult result; try { result = await uretimFuture; } finally { tamamlandi = true; pollTimer.cancel(); } if (result.videoPath == null) { await guncelle(j.copyWith( durum: JobDurum.hata, hata: 'Video yolu boş döndü: ${result.durum}', bitis: DateTime.now(), )); return; } await guncelle(j.copyWith(ilerleme: 80, fazAciklama: 'Video kopyalanıyor')); // Hedef yola kopyala (varyant olarak). Job id'nin random kısmı + ts ile // benzersiz isim — yakın iki üretim aynı dosya ismini almaz. final benzersiz = '${j.id.substring(j.id.length - 8)}_${DateTime.now().millisecondsSinceEpoch.toRadixString(16).substring(6)}'; final hedefAd = 'segment_${seg.sira.toString().padLeft(3, '0')}_$benzersiz.mp4'; final hedefYol = p.join(proje.klasorYolu, 'segmentler', hedefAd); final hedefDosya = File(hedefYol); if (await hedefDosya.exists()) { await hedefDosya.delete(); } await File(result.videoPath!).copy(hedefYol); // Son kareyi çıkar final kareYol = p.join( proje.klasorYolu, 'kareler', 'frame_${seg.sira.toString().padLeft(3, '0')}_$benzersiz.jpg', ); await KareCikarici.enKesinKare( videoYolu: hedefYol, hedefYolu: kareYol, ); // Face swap (opsiyonel) — son kareye uygulanır, sonraki segmentin girişi olur String sonKareYol = kareYol; final faceSwapAcik = seg.faceSwapAcik ?? proje.global.faceSwapAcik; if (faceSwapAcik) { // Yüz listesi öncelik: segment.faceSwapKaynakYollari → // segment.faceSwapKaynakOverridePath → proje.global.faceSwapKaynakYollari → // proje.global.faceSwapKaynakPath → proje.kaynak.faceSwapPath List yuzler = []; if (seg.faceSwapKaynakYollari.isNotEmpty) { yuzler = seg.faceSwapKaynakYollari; } else if (seg.faceSwapKaynakOverridePath != null) { yuzler = [seg.faceSwapKaynakOverridePath!]; } else if (proje.global.faceSwapKaynakYollari.isNotEmpty) { yuzler = proje.global.faceSwapKaynakYollari; } else if (proje.global.faceSwapKaynakPath != null) { yuzler = [proje.global.faceSwapKaynakPath!]; } else if (proje.kaynak.faceSwapPath != null) { yuzler = [proje.kaynak.faceSwapPath!]; } if (yuzler.isNotEmpty) { await guncelle(j.copyWith( ilerleme: 90, fazAciklama: yuzler.length > 1 ? 'Face swap (${yuzler.length} yüz chain)' : 'Face swap uygulanıyor', )); final swappedYol = p.join( proje.klasorYolu, 'kareler', 'frame_${seg.sira.toString().padLeft(3, '0')}_${benzersiz}_swap.jpg', ); final ok = await FaceFusionRunner.resimUzerindeUygula( kaynakYuzler: yuzler, hedefResim: kareYol, cikis: swappedYol, ); if (ok) { sonKareYol = swappedYol; Log.bilgi('JobOrk', 'Face swap uygulandı (${yuzler.length} yüz): $swappedYol'); } else { Log.uyari('JobOrk', 'Face swap başarısız, orijinal kare kullanılacak'); } } else { Log.uyari('JobOrk', 'Face swap açık ama yüz dosyası seçilmemiş'); } } // Segmenti güncelle final guncel = seg.copyWith( durum: SegmentDurum.tamam, videoYolu: hedefYol, varyantYollari: [...seg.varyantYollari, hedefYol], sonKareYolu: sonKareYol, uretimSuresiMs: DateTime.now() .difference(j.baslama ?? DateTime.now()) .inMilliseconds, ); final yeniSegmentler = proje.segmentler .map((s) => s.id == seg.id ? guncel : s) .toList(); await ProjeDeposu.kaydet(proje.copyWith(segmentler: yeniSegmentler)); await guncelle(j.copyWith( durum: JobDurum.tamam, ilerleme: 100, fazAciklama: 'Tamamlandı', bitis: DateTime.now(), )); _bildirilenJoblar85.remove(j.id); await _izlemeModuTetikle(seg); } /// İzleme modu açıksa, biten segmentin sonrasındaki ilk üretilmemiş segmenti /// otomatik kuyruğa al. Future _izlemeModuTetikle(Segment seg) async { if (_izlemeModuOku?.call() != true) return; try { final guncelOzet = (await ProjeDeposu.sonProjeler()) .firstWhereOrNull((o) => seg.id.isNotEmpty); if (guncelOzet == null) return; // Asıl proje yüklemesi için klasör yolu lazım — son aktif projeden alacağız final liste = await ProjeDeposu.sonProjeler(); Proje? guncelProje; for (final ozet in liste) { final candidate = await ProjeDeposu.yukle(ozet.klasorYolu); if (candidate != null && candidate.segmentler.any((s) => s.id == seg.id)) { guncelProje = candidate; break; } } if (guncelProje == null) return; final sonraki = guncelProje.segmentler.firstWhereOrNull( (s) => s.sira > seg.sira && (s.durum == SegmentDurum.bos || s.durum == SegmentDurum.hata || s.durum == SegmentDurum.atlandi), ); if (sonraki == null) return; final harita = await SegmentOlusturucu.sesParcalariniHazirla(guncelProje); await ekle(Job( id: FikoYTId.yeni(), tip: JobTipi.segmentUret, projeId: guncelProje.id, params: { 'segmentId': sonraki.id, 'sesYolu': harita[sonraki.id], 'girisGorseli': guncelProje.kaynak.girisGorseliPath, }, )); Log.bilgi('JobOrk', 'İzleme modu: S${sonraki.sira + 1} otomatik kuyruğa alındı'); } catch (e) { Log.uyari('JobOrk', 'İzleme modu tetiklemesi başarısız: $e'); } } // ─── WanGP Standalone (port 7861) ────────────────────────────────────── Future _segmentUretWanGp( Job j, Proje proje, Segment seg, Completer iptal, ) async { final g = proje.global; // Giriş görseli zinciri String? imagePath = seg.girisGorseliOverridePath; if (imagePath == null || imagePath.isEmpty) { if (seg.sira > 0) { try { final onceki = proje.segmentler .firstWhere((x) => x.sira == seg.sira - 1); if (onceki.sonKareYolu != null && onceki.sonKareYolu!.isNotEmpty && await File(onceki.sonKareYolu!).exists()) { imagePath = onceki.sonKareYolu; } } catch (_) {} } } imagePath ??= (j.params['girisGorseli'] as String?) ?? proje.kaynak.girisGorseliPath; if (imagePath != null && (imagePath.isEmpty || !await File(imagePath).exists())) { imagePath = null; } final audioPath = (j.params['sesYolu'] as String?) ?? seg.sesOverridePath ?? proje.kaynak.sesPath; final audioVar = audioPath.isNotEmpty && await File(audioPath).exists(); // Segmenti üretiliyor durumuna geçir { final liste = proje.segmentler .map((s) => s.id == seg.id ? s.copyWith(durum: SegmentDurum.uretiliyor, hataMesaji: null) : s) .toList(); await ProjeDeposu.kaydet(proje.copyWith(segmentler: liste)); } final cozunurluk = seg.cozunurluk ?? g.cozunurluk; final oran = seg.oran ?? g.oran; final sure = ((seg.bit - seg.basla).round()).clamp(1, 60); final fps = int.tryParse(Sabitler.varsayilanFps) ?? 24; final seedKullan = (j.params['seedOverride'] as int?) ?? seg.seed; // Çözünürlük dize haritası (WanGP formatına) const cozHarita = { '512p': {'16:9': '832x480', '9:16': '480x832'}, '540p': {'16:9': '960x544', '9:16': '544x960'}, '720p': {'16:9': '1280x704', '9:16': '704x1280'}, '1080p': {'16:9': '1920x1088', '9:16': '1088x1920'}, }; final wgpResolution = cozHarita[cozunurluk]?[oran] ?? '1280x704'; final client = WanGpClient(baseUrl: 'http://127.0.0.1:7861'); await guncelle( j.copyWith(ilerleme: 10, fazAciklama: 'WanGP Standalone başlıyor')); // ── İPTAL DİNLEYİCİSİ ───────────────────────────────────────────────── // İptal Completer set olduğunda subprocess'i tree-kill et — VRAM serbest // kalır. Future.then fire-and-forget; iptal etmezse hiç tetiklenmez. // ignore: unawaited_futures iptal.future.then((_) { Log.bilgi('JobOrk', 'İptal sinyali → WanGP subprocess tree-kill'); // ignore: unawaited_futures client.iptalEt(); }); // Gelişmiş ayarlar: segment > global, ikisi yoksa null final gelismis = seg.wangpAyarlari ?? g.wangpVarsayilan; // LoRA listesi: gelişmişten gelirse onu kullan, yoksa tek LoRA path List? loraListesi; List? loraMultiplierListesi; if (gelismis != null && gelismis.loraDosyalari.isNotEmpty) { loraListesi = gelismis.loraDosyalari; if (gelismis.loraMultipliers.isNotEmpty) { loraMultiplierListesi = gelismis.loraMultipliers; } } else if (seg.wangpLoraPath != null && seg.wangpLoraPath!.isNotEmpty) { loraListesi = [seg.wangpLoraPath!]; } // ── LIPSYNC MODEL OTOMATİK SEÇİMİ ───────────────────────────────────── // Mantık (LTX Desktop paritesi + MultiTalk): // 0. 2. ses varsa → multitalk modeli zorla (çoklu konuşmacı) // 1. Tek ses + profilin model_type'ı zaten lipsync → profil model'i // 2. Tek ses + profil non-lipsync → InfiniteTalk override // 3. Ses yoksa → profil / global / default final ozelModel = gelismis?.ozelAlanlar['model_type']?.toString(); final profilLipsyncMi = ozelModel != null && (ozelModel.contains('infinitetalk') || ozelModel.contains('multitalk') || ozelModel.contains('avatar')); // Çoklu konuşmacı kontrolü final ikinciSesYolu = seg.wangpIkinciSesPath; final cokluKonusmaci = ikinciSesYolu != null && ikinciSesYolu.isNotEmpty && await File(ikinciSesYolu).exists(); String secilenModel; if (cokluKonusmaci) { // 2. ses tespit edildi → MultiTalk zorla // 720p profil set edilmişse multitalk_720p, yoksa multitalk (480p) final cozYuksek = (seg.cozunurluk ?? g.cozunurluk).contains('720') || (seg.cozunurluk ?? g.cozunurluk).contains('1080'); secilenModel = cozYuksek ? 'multitalk_720p' : 'multitalk'; Log.bilgi('JobOrk', 'WanGP: 2. konuşmacı → $secilenModel modeli (audio_guide2 + speakers_locations)'); } else if (audioVar && !profilLipsyncMi) { secilenModel = 'infinitetalk'; Log.bilgi('JobOrk', 'WanGP: ses + non-lipsync profil → InfiniteTalk override (sliding=81)'); } else if (ozelModel != null && ozelModel.isNotEmpty) { secilenModel = ozelModel; } else if (audioVar) { secilenModel = 'infinitetalk'; Log.bilgi('JobOrk', 'WanGP: ses tespit edildi → InfiniteTalk + sliding_window=81'); } else if (seg.model != null && seg.model!.isNotEmpty) { secilenModel = seg.model!; } else if (g.model.isNotEmpty) { secilenModel = g.model; } else { secilenModel = 'ltx2_22B_distilled'; } // VRAM safety: profil set etmişse oradan al, yoksa null (WanGP default 0.8). final vramSafetyKats = gelismis?.ozelAlanlar['vram_safety_coefficient'] is num ? (gelismis!.ozelAlanlar['vram_safety_coefficient'] as num) .toDouble() : null; // InfiniteTalk için sliding_window_size MUTLAKA 81 olmalı // (defaults/infinitetalk.json: "Sliding Window size must be 81 frames"). final infinitetalkMi = secilenModel.contains('infinitetalk') || secilenModel.contains('multitalk'); // InfiniteTalk/MultiTalk için zorunlu lipsync ayarları (LTX Desktop paritesi): // - sliding_window_size = 81 (smooth shot transitions) // - audio_prompt_type = "A" (audio-driven) // - audio_guidance_scale = 4.0 (sıkı dudak senkronu için tatlı nokta) // Kullanıcı `gelismis.slidingWindowSize` veya `audioGuidanceScale` set etmişse // ekstra'ya değil gelismise gider, gelismis sonradan override eder. final lipsyncEkstra = {}; if (infinitetalkMi) { lipsyncEkstra['sliding_window_size'] = gelismis?.slidingWindowSize ?? 81; lipsyncEkstra['audio_prompt_type'] = gelismis?.audioPromptType ?? 'A'; lipsyncEkstra['audio_guidance_scale'] = gelismis?.audioGuidanceScale ?? 4.0; } // ÇOKLU KONUŞMACI — MultiTalk parametreleri if (cokluKonusmaci) { lipsyncEkstra['audio_guide2'] = ikinciSesYolu; // Konuşmacı konumları — kullanıcı set etmediyse "L,R" varsayılan lipsyncEkstra['speakers_locations'] = seg.wangpKonusmaciKonumlari ?? 'L,R'; // 2. ref yüz: image_refs olarak listeye eklenebilir, WanGP'nin // multitalk pipeline'ı bunu konum bilgisiyle eşleştirir. final ilkRefYuz = seg.wangpReferansYuzPath; final ikinciYuz = seg.wangpIkinciReferansYuzPath; if (ikinciYuz != null && ikinciYuz.isNotEmpty) { lipsyncEkstra['image_refs'] = [ if (ilkRefYuz != null && ilkRefYuz.isNotEmpty) ilkRefYuz, ikinciYuz, ]; } // multitalk modelinde sliding_window genelde daha küçük (default 81 yine OK) lipsyncEkstra['sliding_window_size'] = gelismis?.slidingWindowSize ?? 81; lipsyncEkstra['audio_prompt_type'] = 'A'; lipsyncEkstra['audio_guidance_scale'] = gelismis?.audioGuidanceScale ?? 5.0; } Map sonuc; try { sonuc = await client.uret( prompt: seg.prompt ?? g.varsayilanPrompt, modelType: secilenModel, resolution: wgpResolution, durationSeconds: sure, fps: fps, numInferenceSteps: seg.steps ?? g.steps, guidanceScale: seg.guidanceScale ?? g.guidanceScale, seed: seedKullan, negativePrompt: seg.negatifPrompt ?? g.varsayilanNegatifPrompt, imageStart: imagePath, audioGuide: audioVar ? audioPath : null, cameraMotion: seg.kameraHareketi ?? g.kameraHareketi, loraSecimleri: loraListesi, loraMultipliers: loraMultiplierListesi, gelismis: gelismis, ekstra: lipsyncEkstra, vramSafety: vramSafetyKats, ilerlemeCagrisi: (oran, aciklama) { if (iptal.isCompleted) return; // %10 (başlangıç) → %75 (üretim) aralığına eşle final yansit = 10.0 + oran.clamp(0.0, 1.0) * 65.0; // fire-and-forget — UI thread'i bloklama unawaited(guncelle(j.copyWith( ilerleme: yansit, fazAciklama: aciklama ?? 'WanGP üretim', ))); }, ); } catch (e) { // İptal sinyali geldiyse: hata değil, "iptal edildi" durumu final iptalMi = iptal.isCompleted || e.toString().contains('iptal edildi'); await guncelle(j.copyWith( durum: iptalMi ? JobDurum.iptal : JobDurum.hata, hata: iptalMi ? null : 'WanGP hatası: $e', bitis: DateTime.now(), )); return; } final videoPath = (sonuc['video_path'] ?? sonuc['video_url'] ?? '').toString(); if (videoPath.isEmpty) { await guncelle(j.copyWith( durum: JobDurum.hata, hata: 'WanGP video çıktısı boş döndü', bitis: DateTime.now(), )); return; } await guncelle(j.copyWith(ilerleme: 80, fazAciklama: 'Video kopyalanıyor')); final benzersiz = '${j.id.substring(j.id.length - 8)}_${DateTime.now().millisecondsSinceEpoch.toRadixString(16).substring(6)}'; final hedefAd = 'segment_${seg.sira.toString().padLeft(3, '0')}_wgp_$benzersiz.mp4'; final hedefYol = p.join(proje.klasorYolu, 'segmentler', hedefAd); await Directory(p.dirname(hedefYol)).create(recursive: true); if (videoPath.startsWith('http')) { await client.dosyaIndir(videoPath, hedefYol); } else if (await File(videoPath).exists()) { await File(videoPath).copy(hedefYol); } else { // WanGP relative yol döndürebilir — /file= ile dene try { await client.dosyaIndir(videoPath, hedefYol); } catch (_) { await guncelle(j.copyWith( durum: JobDurum.hata, hata: 'WanGP video bulunamadı: $videoPath', bitis: DateTime.now(), )); return; } } final kareYol = p.join( proje.klasorYolu, 'kareler', 'frame_${seg.sira.toString().padLeft(3, '0')}_wgp_$benzersiz.jpg', ); try { await KareCikarici.enKesinKare(videoYolu: hedefYol, hedefYolu: kareYol); } catch (e) { Log.uyari('JobOrk', 'WanGP son kare çıkarılamadı: $e'); } final guncel = seg.copyWith( durum: SegmentDurum.tamam, videoYolu: hedefYol, varyantYollari: [...seg.varyantYollari, hedefYol], sonKareYolu: File(kareYol).existsSync() ? kareYol : null, uretimSuresiMs: DateTime.now() .difference(j.baslama ?? DateTime.now()) .inMilliseconds, ); final yeniSegmentler = proje.segmentler.map((s) => s.id == seg.id ? guncel : s).toList(); await ProjeDeposu.kaydet(proje.copyWith(segmentler: yeniSegmentler)); await guncelle(j.copyWith( durum: JobDurum.tamam, ilerleme: 100, fazAciklama: 'WanGP Standalone tamam', bitis: DateTime.now(), )); _bildirilenJoblar85.remove(j.id); await _izlemeModuTetikle(seg); } Future _topluUret( Job j, Proje proje, LtxBaglanti? ltx, Completer iptal, ) async { final segIdler = (j.params['segmentIdler'] as List).cast(); int i = 0; for (final segId in segIdler) { if (iptal.isCompleted) break; i++; // Alt job kuyruğa atma yerine doğrudan inline çalıştır (orchestrator // tek-thread olduğundan kuyruğa atarsak deadlock olur). final altJob = await ekle(Job( id: FikoYTId.yeni(), tip: JobTipi.segmentUret, projeId: proje.id, params: {'segmentId': segId}, durum: JobDurum.calisiyor, baslama: DateTime.now(), )); try { await _segmentUret(altJob, proje, ltx, Completer()); } catch (e) { await guncelle(altJob.copyWith( durum: JobDurum.hata, hata: e.toString(), bitis: DateTime.now(), )); } await guncelle(j.copyWith( ilerleme: i / segIdler.length * 100, fazAciklama: 'Segment $i / ${segIdler.length}', )); } await guncelle(j.copyWith( durum: JobDurum.tamam, ilerleme: 100, fazAciklama: 'Tüm segmentler tamamlandı', bitis: DateTime.now(), )); } Future _analiziYap( Job j, Proje proje, Completer iptal, ) async { await guncelle(j.copyWith(ilerleme: 10, fazAciklama: 'RMS analizi')); final sinirlar = proje.segmentler .map((s) => (basla: s.basla, bit: s.bit)) .toList(); final seviyeler = await SesEnerjisi.seviyeAnalizi(proje.kaynak.sesPath, sinirlar); final yeniSegmentler = []; for (int i = 0; i < proje.segmentler.length; i++) { final s = proje.segmentler[i]; final sev = i < seviyeler.length ? seviyeler[i] : 'medium'; yeniSegmentler.add(s.copyWith( analizMeta: AnalizMetadata(enerjiSeviyesi: sev), )); } await ProjeDeposu.kaydet(proje.copyWith(segmentler: yeniSegmentler)); await guncelle(j.copyWith( durum: JobDurum.tamam, ilerleme: 100, fazAciklama: 'Analiz tamamlandı', bitis: DateTime.now(), )); } Future _enIyiKareyiCikarJob( Job j, Proje proje, Completer iptal, ) async { final videoYol = j.params['videoYol'] as String; final hedef = j.params['hedef'] as String; await KareCikarici.enKesinKare(videoYolu: videoYol, hedefYolu: hedef); await guncelle(j.copyWith( durum: JobDurum.tamam, ilerleme: 100, bitis: DateTime.now(), )); } Future _birlestir( Job j, Proje proje, Completer iptal, ) async { final videolar = proje.segmentler .where((s) => s.videoYolu != null) .map((s) => s.videoYolu!) .toList(); if (videolar.isEmpty) { await guncelle(j.copyWith( durum: JobDurum.hata, hata: 'Birleştirilecek video yok', bitis: DateTime.now(), )); return; } await guncelle(j.copyWith(ilerleme: 5, fazAciklama: 'Birleştirme başlıyor')); // Job params'tan geçiş ayarlarını oku final gecislerJson = j.params['gecisler'] as List?; final List gecisler = gecislerJson != null ? gecislerJson .map((g) => GecisAyari.fromJson(Map.from(g as Map))) .toList() : []; // Çıktı klasörü final ciktiKlasor = p.join(proje.klasorYolu, 'cikti'); await Directory(ciktiKlasor).create(recursive: true); // Herhangi bir efekt var mı? final efektVar = gecisler.any((g) => g.tip != GecisTipi.kesim); final concatCikis = p.join(ciktiKlasor, 'concat_raw.mp4'); if (efektVar) { Log.bilgi('JobOrk', 'Birleştirme: xfade modu (${gecisler.length} geçiş ayarı)'); await FfmpegFiltre.xfadeBirlestir( videolar: videolar, gecisler: gecisler, cikis: concatCikis, ilerleme: (yuzde, faz) async { await guncelle(j.copyWith( ilerleme: yuzde * 0.85, fazAciklama: 'xfade: $faz', )); }, ); } else { Log.bilgi('JobOrk', 'Birleştirme: concat modu (kesim geçişleri)'); await FfmpegServis.videolariBirlestir(videolar, concatCikis); await guncelle(j.copyWith(ilerleme: 85, fazAciklama: 'Ses ekleniyor')); } // Ses ekle final ts = DateTime.now().millisecondsSinceEpoch; final final_ = p.join(ciktiKlasor, '${proje.isim}_final_$ts.mp4'); await guncelle(j.copyWith(ilerleme: 88, fazAciklama: 'Ses ekleniyor')); await FfmpegServis.sesEkle(concatCikis, proje.kaynak.sesPath, final_); // Ara dosyayı temizle try { await File(concatCikis).delete(); } catch (_) {} await guncelle(j.copyWith( durum: JobDurum.tamam, ilerleme: 100, fazAciklama: final_, bitis: DateTime.now(), )); } } /// Riverpod provider — UI'dan job stream izleme. final joblarProvider = StreamProvider>((ref) async* { final orch = JobOrkestratoru.ornek; yield await orch.tumJoblar(); await for (final _ in orch.events) { yield await orch.tumJoblar(); } });