import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import 'package:path/path.dart' as p; import '../veri/isar_servisi.dart'; import '../veri/modeller/uretim_kaydi.dart'; import 'comfy_istemci.dart'; import 'comfy_ws.dart'; class AktifIs { const AktifIs({ required this.kayitId, required this.promptId, this.ilerleme = 0, this.nodeAdi = '', }); final int kayitId; final String promptId; final double ilerleme; final String nodeAdi; AktifIs kopyala({double? ilerleme, String? nodeAdi}) => AktifIs( kayitId: kayitId, promptId: promptId, ilerleme: ilerleme ?? this.ilerleme, nodeAdi: nodeAdi ?? this.nodeAdi, ); } final uretimKuyruguProvider = NotifierProvider>(UretimKuyrugu.new); /// Kaydı + fiziksel çıktı dosyalarını BİRLİKTE siler (disk şişmesini önler — /// eskiden sadece Isar kaydı siliniyor, GB'larca dosya diskte yetim kalıyordu). Future kayitVeDosyalariSil(int kayitId) async { final k = await isar.uretimKaydis.get(kayitId); if (k != null) { for (final yol in k.ciktiDosyalari) { try { final f = File(yol); if (f.existsSync()) await f.delete(); } catch (_) {} } } await isar.writeTxn(() => isar.uretimKaydis.delete(kayitId)); } /// Tüm üretim kayıtları (galeri + kuyruk panelleri izler). final uretimKayitlariProvider = StreamProvider>((ref) { return isar.uretimKaydis .where() .sortByOlusturmaZamaniDesc() .watch(fireImmediately: true); }); /// Üretim kuyruğu: iş gönder → WS'ten izle → çıktıyı indir → Isar'a işle. class UretimKuyrugu extends Notifier> { StreamSubscription? _eventAbone; @override Map build() { _eventAbone?.cancel(); _eventAbone = ref.watch(comfyWsProvider).eventAkisi.listen(_eventIsle); ref.onDispose(() => _eventAbone?.cancel()); return {}; } ComfyIstemci get _istemci => ref.read(comfyIstemciProvider); /// STARTUP RECONCILE — app kapanınca "asılı" kalmış işleri kurtarır. /// Isar'da kuyrukta/uretiliyor/indiriliyor/bekliyor olan kayıtları ComfyUI /// geçmişiyle mutabık kılar: bitmişse indir, hatalıysa işaretle, hâlâ /// çalışıyorsa WS takibine geri al (clientId kalıcı olduğu için event'ler gelir). Future kurtar() async { List asili; try { asili = await isar.uretimKaydis .filter() .durumEqualTo(UretimDurumu.kuyrukta) .or() .durumEqualTo(UretimDurumu.uretiliyor) .or() .durumEqualTo(UretimDurumu.indiriliyor) .or() .durumEqualTo(UretimDurumu.bekliyor) .findAll(); } catch (_) { return; } for (final k in asili) { final pid = k.promptId ?? ''; if (pid.isEmpty) { await _kayitGuncelle(k.id, (x) { x ..durum = UretimDurumu.hata ..hataMesaji = 'Uygulama kapandığı için iş gönderilemedi.' ..bitisZamani = DateTime.now(); }); continue; } final aktif = AktifIs(kayitId: k.id, promptId: pid); try { final hist = await _istemci.history(pid); if (hist == null) { // ComfyUI'de henüz/halen yok → izlemeye al, WS yakalasın. _isiGuncelle(aktif); continue; } final st = hist['status'] as Map?; if (st?['completed'] == true) { _isiGuncelle(aktif); await _tamamlandi(aktif); // çıktıları indirip tamamlar } else if (st?['status_str'] == 'error') { await _kayitGuncelle(k.id, (x) { x ..durum = UretimDurumu.hata ..hataMesaji = 'Üretim hatayla sonuçlanmış (kurtarma).' ..bitisZamani = DateTime.now(); }); } else { _isiGuncelle(aktif); // hâlâ çalışıyor → izlemeye geri al } } catch (_) { // ComfyUI kapalı olabilir; izlemeye al, açılınca/WS dönünce yakalanır. _isiGuncelle(aktif); } } } Future uretimEkle({ required UretimTipi tip, required String baslik, required String parametrelerJson, required Map workflow, String sihirbazJson = '', int varyantGrubu = 0, }) async { final kayit = UretimKaydi() ..tip = tip ..baslik = baslik ..parametrelerJson = parametrelerJson ..sihirbazJson = sihirbazJson ..workflowJson = jsonEncode(workflow) ..durum = UretimDurumu.bekliyor ..varyantGrubu = varyantGrubu ..olusturmaZamani = DateTime.now(); kayit.id = await isar.writeTxn(() => isar.uretimKaydis.put(kayit)); try { final promptId = await _istemci.promptGonder(workflow); kayit ..promptId = promptId ..durum = UretimDurumu.kuyrukta; await isar.writeTxn(() => isar.uretimKaydis.put(kayit)); state = { ...state, promptId: AktifIs(kayitId: kayit.id, promptId: promptId), }; } catch (e) { kayit ..durum = UretimDurumu.hata ..hataMesaji = _hataTurkce(e); await isar.writeTxn(() => isar.uretimKaydis.put(kayit)); } return kayit.id; } /// İşi DIŞARIDA (özel endpoint) zaten gönderdiyse, mevcut prompt_id ile /// kaydı oluşturup WS takibine alır. MSR/LTX gibi /prompt dışı motorlar için. Future hazirPromptEkle({ required UretimTipi tip, required String baslik, required String parametrelerJson, required String promptId, String workflowJson = '', }) async { final kayit = UretimKaydi() ..tip = tip ..baslik = baslik ..parametrelerJson = parametrelerJson ..workflowJson = workflowJson ..promptId = promptId ..durum = UretimDurumu.kuyrukta ..olusturmaZamani = DateTime.now(); kayit.id = await isar.writeTxn(() => isar.uretimKaydis.put(kayit)); state = { ...state, promptId: AktifIs(kayitId: kayit.id, promptId: promptId), }; return kayit.id; } /// Aynısını tekrar üretir — eski kaydın VARYANTI olarak gruplanır. /// MSR/LTX gibi /prompt-dışı motorların workflowJson'u boştur → tekrar edilemez. Future yenidenDene(UretimKaydi eski) async { if (eski.workflowJson.isEmpty) return; Map wf; try { wf = Map.from(jsonDecode(eski.workflowJson) as Map); } catch (_) { return; } await uretimEkle( tip: eski.tip, baslik: eski.baslik, parametrelerJson: eski.parametrelerJson, sihirbazJson: eski.sihirbazJson, workflow: wf, varyantGrubu: eski.etkinGrup, ); } Future iptalEt(String promptId) async { final aktif = state[promptId]; if (aktif == null) return; // İkisini de çağır: interrupt çalışanı, kuyruktanSil bekleyeni keser // (ilerleme=0 olan iş aslında çalışıyor olabilir → hayalet iş önlenir). try { await _istemci.interrupt(); } catch (_) {} try { await _istemci.kuyruktanSil(promptId); } catch (_) {} await _kayitGuncelle(aktif.kayitId, (k) { k ..durum = UretimDurumu.iptal ..bitisZamani = DateTime.now(); }); _isiKaldir(promptId); } void _eventIsle(ComfyEvent e) { final promptId = e.promptId; if (promptId == null || !state.containsKey(promptId)) return; final aktif = state[promptId]!; switch (e.tip) { case 'progress': final deger = (e.veri['value'] as num?)?.toDouble() ?? 0; final maks = (e.veri['max'] as num?)?.toDouble() ?? 1; _isiGuncelle(aktif.kopyala(ilerleme: maks == 0 ? 0 : deger / maks)); _kayitGuncelle(aktif.kayitId, (k) => k.durum = UretimDurumu.uretiliyor); case 'executing': final node = e.veri['node']; if (node == null) { _tamamlandi(aktif); } else { _isiGuncelle(aktif.kopyala(nodeAdi: node.toString())); } case 'execution_success': _tamamlandi(aktif); case 'execution_error': final ham = e.veri['exception_message']?.toString() ?? 'Üretim sırasında hata oluştu'; _kayitGuncelle(aktif.kayitId, (k) { k ..durum = UretimDurumu.hata ..hataMesaji = comfyHataTurkce(ham) ..bitisZamani = DateTime.now(); }); _isiKaldir(promptId); case 'execution_interrupted': _kayitGuncelle(aktif.kayitId, (k) { k ..durum = UretimDurumu.iptal ..bitisZamani = DateTime.now(); }); _isiKaldir(promptId); } } /// Çift "bitti" sinyaline (executing node=null + execution_success) karşı /// idempotent koruma — aynı işi iki kez indirme yarışını engeller. final Set _tamamlananlar = {}; Future _tamamlandi(AktifIs aktif) async { if (!state.containsKey(aktif.promptId)) return; if (!_tamamlananlar.add(aktif.promptId)) return; // zaten işleniyor _isiGuncelle(aktif.kopyala(ilerleme: 1)); await _kayitGuncelle( aktif.kayitId, (k) => k.durum = UretimDurumu.indiriliyor); try { // Yeni biten iş henüz history'e yazılmamış olabilir → kısa retry. Map? hist; for (var deneme = 0; deneme < 3 && hist == null; deneme++) { if (deneme > 0) { await Future.delayed(const Duration(milliseconds: 800)); } try { hist = await _istemci.history(aktif.promptId); } catch (_) {/* retry */} } if (hist == null) throw ComfyHatasi('Üretim geçmişi bulunamadı'); final ciktilar = ComfyIstemci.ciktilariAyikla(hist); if (ciktilar.isEmpty) throw ComfyHatasi('Çıktı dosyası üretilmedi'); final klasor = ref.read(ayarlarProvider).ciktiKlasoru; final indirilenler = []; for (final c in ciktilar) { final hedef = p.join( klasor, '${aktif.kayitId}_${_dosyaAdiTemizle(c.dosyaAdi)}'); await _istemci.dosyaIndir(c, hedef); indirilenler.add(hedef); } await _kayitGuncelle(aktif.kayitId, (k) { k ..durum = UretimDurumu.tamamlandi ..ciktiDosyalari = indirilenler ..bitisZamani = DateTime.now(); }); } catch (e) { await _kayitGuncelle(aktif.kayitId, (k) { k ..durum = UretimDurumu.hata ..hataMesaji = _hataTurkce(e) ..bitisZamani = DateTime.now(); }); } _isiKaldir(aktif.promptId); } void _isiGuncelle(AktifIs is_) => state = {...state, is_.promptId: is_}; void _isiKaldir(String promptId) { final yeni = Map.from(state)..remove(promptId); state = yeni; } Future _kayitGuncelle( int kayitId, void Function(UretimKaydi k) degistir) async { await isar.writeTxn(() async { final k = await isar.uretimKaydis.get(kayitId); if (k == null) return; degistir(k); await isar.uretimKaydis.put(k); }); } static String _dosyaAdiTemizle(String ad) => ad.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_'); static String _hataTurkce(Object e) { final m = e.toString(); if (m.contains('connection') || m.contains('SocketException')) { return 'ComfyUI sunucusuna ulaşılamıyor. Sunucunun açık olduğundan emin olun.'; } return comfyHataTurkce(m); } }