import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; import '../comfy/comfy_istemci.dart'; import '../comfy/uretim_kuyrugu.dart'; import '../cekirdek/ffmpeg_servisi.dart'; import '../karakter/karakter_servisi.dart'; import '../msr/msr_servisi.dart'; import '../veri/isar_servisi.dart'; import '../veri/modeller/bolum.dart'; import '../veri/modeller/film_proje.dart'; import '../veri/modeller/karakter.dart'; import '../veri/modeller/sahne.dart'; import '../veri/modeller/uretim_kaydi.dart'; import 'film_servisi.dart'; /// Sahne → MSR motoru üretim orkestrasyonu. /// /// Sahnenin karakter referanslarını + prompt/süre/çözünürlüğünü workflow /// params'ına yazar, MSR'a gönderir, prompt_id'yi sahneye bağlar. Üretim /// kuyruğu (uretim_kuyrugu) çıktıyı indirip kaydı tamamlayınca bu servis /// uretilenVideoYolu + keyframeYolu'nu doldurup durum=hazir yapar. final sahneUretimServisiProvider = Provider((ref) => SahneUretimServisi(ref)); class SahneUretimServisi { SahneUretimServisi(this._ref) { // Tamamlanan üretim kayıtlarını dinle — bekleyen sahnelere bağla. _abone = _ref.listen>>( uretimKayitlariProvider, (_, sonraki) => _kayitlariIsle(sonraki.value ?? const []), fireImmediately: true, ); _ref.onDispose(() => _abone.close()); } final Ref _ref; late final ProviderSubscription _abone; static const varsayilanWorkflow = 'FikoYT_LTX23_MSR_Hizli.json'; /// Sahneyi MSR motoruna gönderir. Hata varsa [SahneUretimHatasi] (Türkçe) fırlatır. Future uret(Sahne sahne) async { final msr = _ref.read(msrServisiProvider); // 1) Workflow seç (sahnede yoksa varsayılan) + tanımını bul. final dosya = sahne.kullanilanWorkflow.isNotEmpty ? sahne.kullanilanWorkflow : varsayilanWorkflow; final workflowlar = await _workflowlar(msr); final wf = workflowlar.firstWhere( (w) => w.dosya == dosya || w.ad == dosya, orElse: () => workflowlar.isNotEmpty ? workflowlar.first : (throw SahneUretimHatasi( 'MSR motorunda workflow bulunamadı. ComfyUI ve FikoYT MSR ' 'eklentisinin açık olduğundan emin olun.')), ); // 2) Sahne karakterlerinin referanslarını topla (zaten ComfyUI input adı). final karakterler = _ref.read(_filmKarakterleriProvider); final refAdlari = [ for (final id in sahne.karakterIdler) if (karakterler[id]?.anaRef.isNotEmpty == true) karakterler[id]!.anaRef, ]; // 3) Params: workflow varsayılanları + sahne alanları (heuristik eşleme). final proje = await _sahneProjesi(sahne); final params = Map.from(wf.varsayilanlar()); _sahneyiParamsaYaz(wf, params, sahne, refAdlari, proje); // 4) Gönder → prompt_id. final String promptId; try { promptId = await msr.uret(wf.dosya, params, _clientId); } on MsrHatasi catch (e) { throw SahneUretimHatasi(e.toString()); } catch (e) { throw SahneUretimHatasi(_baglantiHatasi(e.toString())); } // 5) Kuyruk takibine al (galeri + WS ilerleme) + sahneyi işaretle. await _ref.read(uretimKuyruguProvider.notifier).hazirPromptEkle( tip: UretimTipi.video, baslik: _baslik(sahne), parametrelerJson: '', promptId: promptId, ); await FilmServisi.sahneDurumGuncelle( sahne.id, durum: SahneDurumu.uretiliyor, promptId: promptId, ); } // --- Tamamlanan üretimi sahneye bağla --- final Set _islenmis = {}; Future _kayitlariIsle(List kayitlar) async { final sahneler = _ref.read(_bekleyenSahnelerProvider); if (sahneler.isEmpty) return; final promptHaritasi = { for (final s in sahneler) if (s.uretimPromptId.isNotEmpty) s.uretimPromptId: s, }; for (final k in kayitlar) { final pid = k.promptId; if (pid == null || !promptHaritasi.containsKey(pid)) continue; if (k.durum == UretimDurumu.tamamlandi && k.ciktiDosyalari.isNotEmpty) { if (!_islenmis.add(pid)) continue; final video = k.ciktiDosyalari.firstWhere( (f) => _videoMu(f), orElse: () => k.ciktiDosyalari.first); // Gerçek resim çıktısı yoksa videodan SON kareyi çıkar (thumbnail + // sonraki sahneye süreklilik rehberi). 'keyframe=video' bug'ı giderildi. var keyframe = k.ciktiDosyalari.firstWhere( (f) => _resimMi(f), orElse: () => ''); if (keyframe.isEmpty) { keyframe = (_videoMu(video) ? await FfmpegServisi.videoSonKare(video) : null) ?? video; } await FilmServisi.sahneDurumGuncelle( promptHaritasi[pid]!.id, durum: SahneDurumu.hazir, videoYolu: video, keyframeYolu: keyframe, ); } else if (k.durum == UretimDurumu.hata || k.durum == UretimDurumu.iptal) { if (!_islenmis.add(pid)) continue; await FilmServisi.sahneDurumGuncelle( promptHaritasi[pid]!.id, durum: SahneDurumu.taslak, ); } } } /// Sahnenin ait olduğu projeyi (oran/stil için) çözer. Future _sahneProjesi(Sahne sahne) async { final bolum = await isar.bolums.get(sahne.bolumId); if (bolum == null) return null; return isar.filmProjes.get(bolum.projeId); } /// En-boy oranını model-dostu (genişlik, yükseklik) çözünürlüğe çevirir. static (int, int) _oranBoyut(String oran) => switch (oran) { '9:16' => (768, 1344), '1:1' => (1024, 1024), '4:3' => (1152, 896), '2.39:1' => (1536, 640), _ => (1344, 768), // 16:9 varsayılan }; // --- Heuristik param eşleme --- void _sahneyiParamsaYaz( MsrWorkflow wf, Map params, Sahne sahne, List refAdlari, FilmProje? proje, ) { var ozneSayaci = 0; final (genislik, yukseklik) = _oranBoyut(proje?.enBoyOrani ?? '16:9'); for (final par in wf.parametreler) { final k = par.key.toLowerCase(); switch (par.tip) { case MsrTip.metin: if (_iceriyorMu(k, ['negatif', 'negative'])) { if (sahne.negatifPrompt.isNotEmpty) { params[par.key] = sahne.negatifPrompt; } } else if (_iceriyorMu(k, ['prompt', 'metin', 'aciklama', 'text'])) { final metin = _sahnePrompt(sahne, proje?.stil ?? ''); if (metin.isNotEmpty) params[par.key] = metin; } case MsrTip.sayi: // Süre + ÇÖZÜNÜRLÜK (proje oranı) parametrelerini eşle. if (_iceriyorMu(k, ['sure', 'duration', 'length', 'saniye', 'frame'])) { params[par.key] = _sureDeger(par, sahne.sureSn); } else if (_iceriyorMu(k, ['genislik', 'genis', 'width', 'en'])) { params[par.key] = _boyutDeger(par, genislik); } else if (_iceriyorMu(k, ['yukseklik', 'yuksek', 'height', 'boy'])) { params[par.key] = _boyutDeger(par, yukseklik); } case MsrTip.gorsel: // Sırayla özne slotlarına karakter referanslarını ata. if (ozneSayaci < refAdlari.length) { params[par.key] = refAdlari[ozneSayaci++]; } default: break; } } } String _sahnePrompt(Sahne s, String stil) { final stilEk = stil.trim().isNotEmpty ? stil.trim() : 'cinematic'; if (s.prompt.isNotEmpty) { // Elle yazılmış prompt'a proje stili zaten yoksa ekle (görsel tutarlılık). return s.prompt.toLowerCase().contains(stilEk.toLowerCase()) ? s.prompt : '${s.prompt}, $stilEk'; } return [ if (s.aciklama.isNotEmpty) s.aciklama, if (s.mekan.isNotEmpty) s.mekan, if (s.zaman.isNotEmpty) s.zaman, if (s.ruhHali.isNotEmpty) s.ruhHali, stilEk, ].join(', '); } num _sureDeger(MsrParametre par, double sn) { final v = par.adim != null && par.adim! >= 1 ? sn.round() : sn; if (par.min != null && v < par.min!) return par.min!; if (par.max != null && v > par.max!) return par.max!; return v; } /// Çözünürlük değerini parametre sınırlarına kıstırır. num _boyutDeger(MsrParametre par, int hedef) { num v = hedef; if (par.min != null && v < par.min!) v = par.min!; if (par.max != null && v > par.max!) v = par.max!; return v; } String _baslik(Sahne s) => s.aciklama.isNotEmpty ? s.aciklama : 'Sahne ${s.sira + 1}'; Future> _workflowlar(MsrServisi msr) async { final cache = _ref.read(msrWorkflowlariProvider).valueOrNull; if (cache != null && cache.isNotEmpty) return cache; return msr.workflowListesi(); } static bool _iceriyorMu(String k, List kelimeler) => kelimeler.any(k.contains); static bool _videoMu(String f) => const ['.mp4', '.webm', '.mov', '.gif', '.mkv'].contains(_uzanti(f)); static bool _resimMi(String f) => const ['.png', '.jpg', '.jpeg', '.webp'].contains(_uzanti(f)); static String _uzanti(String f) => p.extension(f).toLowerCase(); // comfy client id ile aynı kanal (WS ilerleme bu id ile gelir). String get _clientId => ComfyIstemci.clientId; static String _baglantiHatasi(String m) { if (m.contains('connection') || m.contains('SocketException') || m.contains('Connection refused')) { return 'MSR motoruna ulaşılamıyor. ComfyUI ve FikoYT MSR eklentisinin ' 'açık olduğundan emin olun.'; } return m.replaceFirst('Exception: ', '').trim(); } } /// Sahne id → Karakter haritası (üret akışında referans çözümü için). final _filmKarakterleriProvider = Provider>((ref) { final liste = ref.watch(karakterlerProvider).valueOrNull ?? const []; return {for (final k in liste) k.id: k}; }); /// Üretiliyor durumundaki sahneler (tüm bölümler) — tamamlanma takibi için. final _bekleyenSahnelerProvider = Provider>((ref) { return ref.watch(uretiliyorSahnelerProvider).valueOrNull ?? const []; }); class SahneUretimHatasi implements Exception { SahneUretimHatasi(this.mesaj); final String mesaj; @override String toString() => mesaj; }