import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../cekirdek/log.dart'; import '../servisler/gorsel/gorsel_api_istemci.dart'; /// Sunucu tarafında bulunan bir LoRA dosyası class Lora { final String id; // dosya adı (uzantısız) final String yol; // sunucu local path final double boyutMb; final Map meta; Lora({ required this.id, required this.yol, required this.boyutMb, required this.meta, }); /// LoRA hangi base model ile eğitildi String get baseModel => (meta['base_model'] as String?) ?? ''; /// Tetikleme kelimesi String get tetikleyici => (meta['tetikleyici'] as String?) ?? ''; /// Sınıf (woman/man/person) String get cins => (meta['cins'] as String?) ?? 'person'; /// Karakter ismi (eğitim sırasında verilen isim) String? get isim => meta['isim'] as String?; /// Eğitim adımı sayısı int get adim => (meta['adim'] as int?) ?? 0; /// Karakter ID (varsa) String? get karakterId => meta['karakter_id'] as String?; static Lora fromJson(Map j) => Lora( id: j['id'] as String, yol: j['yol'] as String, boyutMb: (j['boyut_mb'] as num?)?.toDouble() ?? 0, meta: Map.from(j['meta'] as Map? ?? {}), ); } /// Eğitim işi (sunucu) class LoraEgitimIsi { final String id; final String? karakterId; final String loraIsim; final String baseModel; final int durum; // 0=calisiyor 1=tamam 2=hata final int ilerleme; final int adim; final int toplamAdim; final String faz; final String? hata; final DateTime baslama; LoraEgitimIsi({ required this.id, this.karakterId, required this.loraIsim, required this.baseModel, required this.durum, required this.ilerleme, required this.adim, required this.toplamAdim, required this.faz, this.hata, required this.baslama, }); static LoraEgitimIsi fromJson(Map j) => LoraEgitimIsi( id: j['id'] as String, karakterId: j['karakter_id'] as String?, loraIsim: j['lora_isim'] as String? ?? '', baseModel: j['base_model'] as String? ?? '', durum: (j['durum'] as num?)?.toInt() ?? 0, ilerleme: (j['ilerleme'] as num?)?.toInt() ?? 0, adim: (j['adim'] as num?)?.toInt() ?? 0, toplamAdim: (j['toplam_adim'] as num?)?.toInt() ?? 0, faz: j['faz'] as String? ?? '', hata: j['hata'] as String?, baslama: DateTime.fromMillisecondsSinceEpoch( ((j['baslama'] as num?)?.toDouble() ?? 0).round() * 1000, ), ); } class LoraServisi { final Dio _dio = Dio(BaseOptions( baseUrl: 'http://127.0.0.1:8002', connectTimeout: const Duration(seconds: 8), receiveTimeout: const Duration(minutes: 2), headers: {'Content-Type': 'application/json'}, )); Future> liste() async { try { final r = await _dio.get('/lora/list'); final liste = r.data['loralar'] as List? ?? []; return liste .map((e) => Lora.fromJson(Map.from(e as Map))) .toList(); } catch (e) { Log.uyari('Lora', 'list hata: $e'); return []; } } /// Eğitim başlat. veri_klasoru lokal yol; sunucu da aynı makinede olduğu için /// doğrudan path geçiriyoruz. Future egitBaslat({ required String karakterId, required String loraIsim, required String veriKlasoru, required String baseModelRepo, String? tetikleyici, String cins = 'person', int adim = 1500, int rank = 16, double lr = 1e-4, int boyut = 1024, String? mevcutLoraYolu, bool ayniDosyayaYaz = false, bool useDora = false, double minSnrGamma = 0.0, bool smartCaption = false, bool autoTest = false, }) async { try { final r = await _dio.post('/lora/egit', data: { 'karakter_id': karakterId, 'lora_isim': loraIsim, 'veri_klasoru': veriKlasoru, 'base_model_repo': baseModelRepo, 'tetikleyici': tetikleyici, 'cins': cins, 'adim': adim, 'rank': rank, 'lr': lr, 'boyut': boyut, 'mevcut_lora_yolu': mevcutLoraYolu, 'ayni_dosyaya_yaz': ayniDosyayaYaz, 'use_dora': useDora, 'min_snr_gamma': minSnrGamma, 'smart_caption': smartCaption, 'auto_test': autoTest, }); return r.data['job_id'] as String?; } catch (e) { Log.uyari('Lora', 'egit baslat hata: $e'); return null; } } Future isDurumu(String jobId) async { try { final r = await _dio.get('/lora/job/$jobId'); return LoraEgitimIsi.fromJson(Map.from(r.data as Map)); } catch (_) { return null; } } Future isIptal(String jobId) async { try { final r = await _dio.delete('/lora/job/$jobId'); return r.data['iptal'] as bool? ?? false; } catch (_) { return false; } } Future sil(String loraId) async { try { final r = await _dio.delete('/lora/$loraId'); return r.data['silindi'] as bool? ?? false; } catch (_) { return false; } } Future metaGuncelle( String loraId, Map guncellemeler) async { try { final r = await _dio.patch( '/lora/$loraId/meta', data: guncellemeler, ); return r.data['basarili'] as bool? ?? false; } catch (e) { Log.uyari('Lora', 'meta guncelle hata: $e'); return false; } } } /// LoRA listesi state — sunucudan periyodik çeker class LoraListesiNotifier extends StateNotifier> { LoraListesiNotifier() : super([]) { yenile(); } final servis = LoraServisi(); Future yenile() async { final l = await servis.liste(); state = l; } Future sil(String id) async { await servis.sil(id); await yenile(); } Future karakterAta(String loraId, String karakterId) async { final ok = await servis.metaGuncelle(loraId, { 'karakter_id': karakterId, }); if (ok) await yenile(); return ok; } } final loraListesiProvider = StateNotifierProvider>( (ref) => LoraListesiNotifier(), ); /// Aktif eğitim işleri (job_id → durum) class LoraIslerNotifier extends StateNotifier> { LoraIslerNotifier(this.ref) : super({}) { _periyodik(); } final Ref ref; Timer? _z; final servis = LoraServisi(); void _periyodik() { _z = Timer.periodic(const Duration(seconds: 2), (_) async { await _sorgula(); }); } Future _sorgula() async { final aktifler = state.values.where((i) => i.durum == 0).toList(); if (aktifler.isEmpty) return; if (!await GorselApiIstemci.portAcikMi()) return; final yeniState = Map.from(state); bool degisti = false; for (final i in aktifler) { final guncel = await servis.isDurumu(i.id); if (guncel != null) { yeniState[i.id] = guncel; if (guncel.durum != 0) { degisti = true; // Tamamlandı → liste yenile if (guncel.durum == 1) { ref.read(loraListesiProvider.notifier).yenile(); } } else { degisti = true; } } } if (degisti) state = yeniState; } Future egitBaslat({ required String karakterId, required String loraIsim, required String veriKlasoru, required String baseModelRepo, String? tetikleyici, String cins = 'person', int adim = 1500, int rank = 16, String? mevcutLoraYolu, bool ayniDosyayaYaz = false, bool useDora = false, double minSnrGamma = 0.0, bool smartCaption = false, bool autoTest = false, }) async { final jobId = await servis.egitBaslat( karakterId: karakterId, loraIsim: loraIsim, veriKlasoru: veriKlasoru, baseModelRepo: baseModelRepo, tetikleyici: tetikleyici, cins: cins, adim: adim, rank: rank, mevcutLoraYolu: mevcutLoraYolu, ayniDosyayaYaz: ayniDosyayaYaz, useDora: useDora, minSnrGamma: minSnrGamma, smartCaption: smartCaption, autoTest: autoTest, ); if (jobId == null) return null; // İlk durum sorgusu final durum = await servis.isDurumu(jobId); if (durum != null) { state = {...state, jobId: durum}; } return jobId; } Future iptal(String jobId) async { await servis.isIptal(jobId); } @override void dispose() { _z?.cancel(); super.dispose(); } } final loraIsleriProvider = StateNotifierProvider>( (ref) => LoraIslerNotifier(ref), ); /// Form'da seçili aktif LoRA'lar — generation'a gönderilir class AktifLoraSecimi { final String yol; double olcek; final String? tetikleyici; AktifLoraSecimi({ required this.yol, this.olcek = 0.8, this.tetikleyici, }); Map toJson() => { 'yol': yol, 'olcek': olcek, 'tetikleyici': tetikleyici, }; } /// Aktif seçili LoRA listesi (form için) final aktifLoralarProvider = StateProvider>((ref) => []);