import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import '../veri/isar_servisi.dart'; final comfyIstemciProvider = Provider((ref) { final url = ref.watch(ayarlarProvider.select((a) => a.comfyUrl)); return ComfyIstemci(url); }); /// ComfyUI HTTP API istemcisi (/prompt, /history, /view, /object_info...) class ComfyIstemci { ComfyIstemci(this.baseUrl) : _dio = Dio(BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(minutes: 5), )); final String baseUrl; final Dio _dio; /// WS/HTTP üretimlerinde kullanılan sabit istemci kimliği. /// [clientIdHazirla] ile diskten yüklenir; restart sonrası AYNI kalır ki /// uçuştaki işlerin WS event'leri kaybolmasın. static String clientId = 'fikoyt-gecici'; static bool _idYuklendi = false; /// İstemci kimliğini diskten okur (yoksa üretip kalıcı yazar). main()'de çağrılmalı. static Future clientIdHazirla() async { if (_idYuklendi) return; try { final dir = await getApplicationSupportDirectory(); final f = File(p.join(dir.path, 'client_id.txt')); if (await f.exists()) { final v = (await f.readAsString()).trim(); if (v.isNotEmpty) { clientId = v; _idYuklendi = true; return; } } clientId = 'fikoyt-${DateTime.now().millisecondsSinceEpoch}'; await f.writeAsString(clientId); } catch (_) { clientId = 'fikoyt-${DateTime.now().millisecondsSinceEpoch}'; } _idYuklendi = true; } Future erisilebilirMi() async { try { await _dio.get('/system_stats', options: Options(receiveTimeout: const Duration(seconds: 4))); return true; } catch (_) { return false; } } /// Tüm node tanımları; model/LoRA listeleri buradan çekilir. Future> objectInfo([String? nodeAdi]) async { final yol = nodeAdi == null ? '/object_info' : '/object_info/$nodeAdi'; final cevap = await _dio.get(yol); final d = cevap.data; if (d is! Map) return {}; return Map.from(d); } /// Bir node'un COMBO input seçeneklerini döndürür (ör. checkpoint listesi). Future> comboSecenekleri(String nodeAdi, String inputAdi) async { final bilgi = await objectInfo(nodeAdi); final node = bilgi[nodeAdi] as Map?; if (node == null) return []; final girdiler = (node['input'] as Map?)?['required'] as Map?; final tanim = girdiler?[inputAdi]; if (tanim is List && tanim.isNotEmpty && tanim.first is List) { return List.from(tanim.first as List); } return []; } /// Workflow'u kuyruğa gönderir, prompt_id döndürür. Future promptGonder(Map workflow) async { final cevap = await _dio.post('/prompt', data: { 'prompt': workflow, 'client_id': clientId, }); final veri = cevap.data; if (veri is! Map) throw ComfyHatasi('Sunucu geçersiz yanıt verdi'); if (veri['error'] != null) { throw ComfyHatasi(veri['error'].toString()); } final pid = veri['prompt_id']; if (pid is! String) { // Geçersiz workflow → 200 + node_errors (prompt_id yok). throw ComfyHatasi(veri['node_errors']?.toString().isNotEmpty == true ? 'Workflow hatası: ${veri['node_errors']}' : 'Sunucu prompt_id döndürmedi (model/node eksik olabilir)'); } return pid; } Future?> history(String promptId) async { final cevap = await _dio.get('/history/$promptId'); final veri = cevap.data as Map; return veri[promptId] == null ? null : Map.from(veri[promptId] as Map); } /// /history çıktısından üretilen dosyaları (filename, subfolder, type) listeler. static List ciktilariAyikla(Map historyKaydi) { final sonuc = []; final outputs = historyKaydi['outputs'] as Map? ?? {}; for (final nodeCiktisi in outputs.values) { if (nodeCiktisi is! Map) continue; final m = Map.from(nodeCiktisi); for (final anahtar in ['audio', 'images', 'gifs', 'video']) { final liste = m[anahtar]; if (liste is List) { for (final oge in liste) { if (oge is Map && oge['filename'] != null) { sonuc.add(ComfyCikti( dosyaAdi: oge['filename'] as String, altKlasor: (oge['subfolder'] as String?) ?? '', tip: (oge['type'] as String?) ?? 'output', )); } } } } } return sonuc; } /// /view ile dosyayı indirir, hedef yola yazar. Future dosyaIndir(ComfyCikti cikti, String hedefYol) async { final cevap = await _dio.get>( '/view', queryParameters: { 'filename': cikti.dosyaAdi, 'subfolder': cikti.altKlasor, 'type': cikti.tip, }, // Büyük video çıktıları 5dk'yı aşabilir. options: Options( responseType: ResponseType.bytes, receiveTimeout: const Duration(minutes: 30)), ); final veri = cevap.data; if (veri == null || veri.isEmpty) { throw ComfyHatasi('Çıktı dosyası indirilemedi (boş yanıt)'); } final dosya = File(hedefYol); await dosya.parent.create(recursive: true); await dosya.writeAsBytes(veri, flush: true); return dosya; } /// Yerel resmi ComfyUI input klasörüne yükler, dosya adını döndürür (I2V/edit). Future resimYukle(String yerelYol) async { final dosya = File(yerelYol); final form = FormData.fromMap({ 'image': await MultipartFile.fromFile(yerelYol, filename: dosya.uri.pathSegments.last), 'overwrite': 'true', }); final cevap = await _dio.post('/upload/image', data: form); final d = cevap.data; if (d is! Map || d['name'] is! String) { throw ComfyHatasi('Dosya yüklenemedi (geçersiz yanıt)'); } return d['name'] as String; } /// Workflow'u gönderir, bitene kadar bekler, çıktıları döndürür (sıralı orkestrasyon). Future> uretVeBekle( Map workflow, { Duration zamanAsimi = const Duration(minutes: 10), void Function(double ilerleme)? onIlerleme, }) async { final promptId = await promptGonder(workflow); final bitis = DateTime.now().add(zamanAsimi); while (DateTime.now().isBefore(bitis)) { await Future.delayed(const Duration(milliseconds: 1500)); final hist = await history(promptId); if (hist == null) continue; final st = hist['status'] as Map?; if (st?['status_str'] == 'error') { final mesaj = _hataMesaji(hist); throw ComfyHatasi(mesaj); } if (st?['completed'] == true) { return ciktilariAyikla(hist); } } throw ComfyHatasi('Üretim zaman aşımına uğradı'); } /// Workflow'u çalıştırır, ham history outputs map'ini döndürür /// (analiz node'larının json/metin çıktıları için). Future> hamCiktiCalistir( Map workflow, { Duration zamanAsimi = const Duration(minutes: 8), }) async { final promptId = await promptGonder(workflow); final bitis = DateTime.now().add(zamanAsimi); while (DateTime.now().isBefore(bitis)) { await Future.delayed(const Duration(milliseconds: 1500)); final hist = await history(promptId); if (hist == null) continue; final st = hist['status'] as Map?; if (st?['status_str'] == 'error') throw ComfyHatasi(_hataMesaji(hist)); if (st?['completed'] == true) { return (hist['outputs'] as Map?)?.cast() ?? {}; } } throw ComfyHatasi('Analiz zaman aşımına uğradı'); } static String _hataMesaji(Map hist) { final st = hist['status'] as Map?; final msgs = st?['messages'] as List?; if (msgs != null) { for (final m in msgs) { if (m is List && m.isNotEmpty && m[0] == 'execution_error') { final d = m[1] as Map?; return d?['exception_message']?.toString() ?? 'Üretim hatası'; } } } return 'Üretim hatası'; } Future interrupt() => _dio.post('/interrupt'); Future kuyruktanSil(String promptId) => _dio.post('/queue', data: {'delete': [promptId]}); } class ComfyCikti { const ComfyCikti( {required this.dosyaAdi, required this.altKlasor, required this.tip}); final String dosyaAdi; final String altKlasor; final String tip; } class ComfyHatasi implements Exception { ComfyHatasi(this.mesaj); final String mesaj; @override String toString() => mesaj; } /// ComfyUI ham hata mesajını kullanıcı dostu Türkçeye çevirir. String comfyHataTurkce(String ham) { final m = ham.toLowerCase(); if (m.contains('tokenize') || m.contains('tokenizer')) { return 'Bu model metin kodlayıcısını (CLIP) içermiyor. ACE text encoder ' 'ayrı yükleniyor; sorun sürerse "aio" (all-in-one) modeli seçin.'; } if (m.contains("'nonetype' object has no attribute 'shape'") || m.contains('nonetype') && m.contains('shape')) { return 'Bu model (XL/SFT) mevcut ACE-Step node\'larıyla uyumsuz görünüyor. ' 'Lütfen "ace_step_1.5_turbo_aio" modelini kullanın.'; } if (m.contains('out of memory') || m.contains('cuda') && m.contains('memory')) { return 'GPU belleği yetersiz. Süreyi/çözünürlüğü düşürün veya daha hafif ' 'bir model seçin.'; } if (m.contains('size') && m.contains('match')) { return 'Model/VAE boyut uyuşmazlığı. Seçili model ile VAE uyumlu değil.'; } return ham.replaceFirst('Exception: ', '').trim(); }