import 'dart:async'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import '../cekirdek/kimlik.dart'; import '../cekirdek/log.dart'; import '../servisler/ace_step_api_istemci.dart'; class AceStepGorev { final String id; // FikoYTId final String taskId; // ACE-Step task_id final String prompt; final String lyrics; final double? sure; final int steps; final double cfg; final int? bpm; final String? keyScale; final String dil; /// Otomatik dil algılama — true ise vokal segmentlerinin dili otomatik /// tespit edilir (TR + EN + ES gibi karışık şarkılar için). final bool dilOtomatik; final String? model; final bool thinking; final bool sampleMode; final DateTime baslama; final String? projeId; // hangi projeye ait (varsa) final String? projeKlasoru; // proje klasörü — çıktı buraya yazılır final String? globalCaption; // Sonuç int durum; // 0=calisiyor, 1=tamam, 2=hata String? sesYerelYol; // indirildikten sonra String? sesUzakYol; // ACE-Step server'da yol String? hata; String? ilerlemeMetni; double ilerleme; // 0-100 // Türetme bilgisi String? kaynakGorevId; // hangi görevden türetildi (yeniden üretim/varyant/kopya için) String? uretimTuru; // 'orijinal' | 'tekrar' | 'varyant' | 'kopya' /// Tam form parametre snapshot'ı (tümü) — "Forma Yükle"de hiç eksik /// alan kalmasın diye saklanır. uretimEkle çağrısında doldurulur. Map? formSnapshot; /// Seed: form'da kullanıcının girdiği değer (-1 ise rastgele istendi) int? seed; /// Server'ın gerçekten kullandığı seed (response'tan parse edilir). /// Forma Yükle'de bu öncelikli kullanılır → tıpa tıp aynı sonuç için. int? bitenSeed; bool useRandomSeed; AceStepGorev({ required this.id, required this.taskId, required this.prompt, required this.lyrics, required this.sure, required this.steps, required this.cfg, required this.bpm, required this.keyScale, required this.dil, this.dilOtomatik = true, required this.model, required this.thinking, required this.sampleMode, required this.baslama, this.projeId, this.projeKlasoru, this.globalCaption, this.durum = 0, this.sesYerelYol, this.sesUzakYol, this.hata, this.ilerlemeMetni, this.ilerleme = 0, this.kaynakGorevId, this.uretimTuru, this.formSnapshot, this.seed, this.bitenSeed, this.useRandomSeed = true, }); Map toJson() => { 'id': id, 'taskId': taskId, 'prompt': prompt, 'lyrics': lyrics, 'sure': sure, 'steps': steps, 'cfg': cfg, 'bpm': bpm, 'keyScale': keyScale, 'dil': dil, 'dilOtomatik': dilOtomatik, 'model': model, 'thinking': thinking, 'sampleMode': sampleMode, 'baslama': baslama.toIso8601String(), 'projeId': projeId, 'projeKlasoru': projeKlasoru, 'globalCaption': globalCaption, 'durum': durum, 'sesYerelYol': sesYerelYol, 'sesUzakYol': sesUzakYol, 'hata': hata, 'ilerlemeMetni': ilerlemeMetni, 'ilerleme': ilerleme, 'kaynakGorevId': kaynakGorevId, 'uretimTuru': uretimTuru, 'formSnapshot': formSnapshot, 'seed': seed, 'bitenSeed': bitenSeed, 'useRandomSeed': useRandomSeed, }; static AceStepGorev fromJson(Map j) => AceStepGorev( id: j['id'] as String, taskId: j['taskId'] as String, prompt: j['prompt'] as String? ?? '', lyrics: j['lyrics'] as String? ?? '', sure: (j['sure'] as num?)?.toDouble(), steps: j['steps'] as int? ?? 8, cfg: (j['cfg'] as num?)?.toDouble() ?? 7.0, bpm: j['bpm'] as int?, keyScale: j['keyScale'] as String?, dil: j['dil'] as String? ?? 'en', dilOtomatik: j['dilOtomatik'] as bool? ?? true, model: j['model'] as String?, thinking: j['thinking'] as bool? ?? false, sampleMode: j['sampleMode'] as bool? ?? false, baslama: DateTime.parse(j['baslama'] as String), projeId: j['projeId'] as String?, projeKlasoru: j['projeKlasoru'] as String?, globalCaption: j['globalCaption'] as String?, durum: j['durum'] as int? ?? 0, sesYerelYol: j['sesYerelYol'] as String?, sesUzakYol: j['sesUzakYol'] as String?, hata: j['hata'] as String?, ilerlemeMetni: j['ilerlemeMetni'] as String?, ilerleme: (j['ilerleme'] as num?)?.toDouble() ?? 0, kaynakGorevId: j['kaynakGorevId'] as String?, uretimTuru: j['uretimTuru'] as String?, formSnapshot: (j['formSnapshot'] as Map?)?.cast(), seed: (j['seed'] as num?)?.toInt(), bitenSeed: (j['bitenSeed'] as num?)?.toInt(), useRandomSeed: j['useRandomSeed'] as bool? ?? true, ); } class AceStepUretimDurumu extends StateNotifier> { AceStepUretimDurumu() : super([]) { _yukle(); _periyodikSorgu(); } Box? _kutu; Timer? _zamanlayici; Future _box() async => _kutu ??= await Hive.openBox('ace_step_gorevler'); Future _yukle() async { final b = await _box(); final liste = []; for (final v in b.values) { try { liste.add(AceStepGorev.fromJson(Map.from(v as Map))); } catch (_) {} } liste.sort((a, b) => b.baslama.compareTo(a.baslama)); state = liste; } Future _kaydet(AceStepGorev g) async { final b = await _box(); await b.put(g.id, g.toJson()); } Future sil(String id) async { final b = await _box(); await b.delete(id); state = state.where((g) => g.id != id).toList(); } Future tumGecmisiTemizle() async { final b = await _box(); await b.clear(); state = []; } /// [batchSize] > 1 ise birden fazla görev oluşur (her task_id için ayrı). /// Tek üretim için tek elemanlı liste döner. Future> uretimEkle({ required AceStepApiIstemci api, required String prompt, String globalCaption = '', String lyrics = '', double? sure, int steps = 8, double cfg = 7.0, int? bpm, String? keyScale, String? timeSignature, String dil = 'en', bool dilOtomatik = true, String? model, bool thinking = false, bool sampleMode = false, String sampleQuery = '', bool useFormat = false, int seed = -1, bool useRandomSeed = true, String taskType = 'text2music', // Gelişmiş String inferMethod = 'ode', double shift = 3.0, bool useAdg = false, double cfgIntervalStart = 0.0, double cfgIntervalEnd = 1.0, String? customTimesteps, // Repaint / audio2audio double repaintingStart = 0.0, double? repaintingEnd, String repaintMode = 'balanced', double repaintStrength = 0.5, String? referenceAudioPath, String? srcAudioPath, double audioCoverStrength = 1.0, double coverNoiseStrength = 0.0, String? instruction, // Çıkış String? audioFormat, int? mp3Bitrate, int? mp3SampleRate, bool? enableNormalization, double? normalizationDb, double? fadeInDuration, double? fadeOutDuration, double? scoreScale, String? loraPath, String? samplerMode, int? batchSize, // LM (Language Model) parametreleri String? lmNegativePrompt, double? lmTemperature, double? lmCfgScale, int? lmTopK, double? lmTopP, double? lmRepetitionPenalty, String? lmModelPath, String? lmBackend, // Decoder bool? useTiledDecode, bool? constrainedDecoding, bool? constrainedDecodingDebug, // Caption bool? useCotCaption, bool? isFormatCaption, bool? allowLmBatch, String? trackName, String? projeId, String? projeKlasoru, String? kaynakGorevId, String? uretimTuru, /// Tam form snapshot — "Forma Yükle"de hiç eksik kalmasın diye Map? formSnapshot, }) async { final taskIds = await api.uretimBaslat( prompt: prompt, globalCaption: globalCaption, lyrics: lyrics, audioDuration: sure, inferenceSteps: steps, guidanceScale: cfg, bpm: bpm, keyScale: keyScale ?? '', timeSignature: timeSignature ?? '', vocalLanguage: dil, useCotLanguage: dilOtomatik, model: model, thinking: thinking, sampleMode: sampleMode, sampleQuery: sampleQuery, useFormat: useFormat, seed: seed, useRandomSeed: useRandomSeed, taskType: taskType, inferMethod: inferMethod, shift: shift, useAdg: useAdg, cfgIntervalStart: cfgIntervalStart, cfgIntervalEnd: cfgIntervalEnd, customTimesteps: customTimesteps, repaintingStart: repaintingStart, repaintingEnd: repaintingEnd, repaintMode: repaintMode, repaintStrength: repaintStrength, referenceAudioPath: referenceAudioPath, srcAudioPath: srcAudioPath, audioCoverStrength: audioCoverStrength, coverNoiseStrength: coverNoiseStrength, instruction: instruction, audioFormat: audioFormat, mp3Bitrate: mp3Bitrate, mp3SampleRate: mp3SampleRate, enableNormalization: enableNormalization, normalizationDb: normalizationDb, fadeInDuration: fadeInDuration, fadeOutDuration: fadeOutDuration, scoreScale: scoreScale, loraPath: loraPath, samplerMode: samplerMode, batchSize: batchSize, lmNegativePrompt: lmNegativePrompt, lmTemperature: lmTemperature, lmCfgScale: lmCfgScale, lmTopK: lmTopK, lmTopP: lmTopP, lmRepetitionPenalty: lmRepetitionPenalty, lmModelPath: lmModelPath, lmBackend: lmBackend, useTiledDecode: useTiledDecode, constrainedDecoding: constrainedDecoding, constrainedDecodingDebug: constrainedDecodingDebug, useCotCaption: useCotCaption, isFormatCaption: isFormatCaption, allowLmBatch: allowLmBatch, trackName: trackName, ); // Batch desteği — her task_id için ayrı bir görev oluştur. // Hepsi aynı parametreleri paylaşır (prompt/lyrics vs.) ama farklı task_id'leri var. final yeniGorevler = []; for (var i = 0; i < taskIds.length; i++) { final tid = taskIds[i]; final etiket = taskIds.length > 1 ? ' [varyant ${i + 1}/${taskIds.length}]' : ''; final gorev = AceStepGorev( id: FikoYTId.yeni(), taskId: tid, prompt: prompt + etiket, lyrics: lyrics, sure: sure, steps: steps, cfg: cfg, bpm: bpm, keyScale: keyScale, dil: dil, dilOtomatik: dilOtomatik, model: model, thinking: thinking, sampleMode: sampleMode, baslama: DateTime.now(), projeId: projeId, projeKlasoru: projeKlasoru, globalCaption: globalCaption, kaynakGorevId: kaynakGorevId, uretimTuru: taskIds.length > 1 ? 'varyant' : uretimTuru, formSnapshot: formSnapshot, seed: seed, // Client-side seed üretildiyse (>=0 ve useRandomSeed=false), bitenSeed // başlangıçta seed ile aynı olur — server döndürmese bile bilinir. bitenSeed: (!useRandomSeed && seed >= 0) ? seed : null, useRandomSeed: useRandomSeed, ); await _kaydet(gorev); yeniGorevler.add(gorev); } // En yeni görevler listenin başına eklenir (ters sırada — son görev en üstte) state = [...yeniGorevler.reversed, ...state]; return yeniGorevler; } /// Mevcut görevin parametreleriyle yeni bir üretim başlatır. /// [yeniSeed] true → rastgele seed (varyant) /// [parametrelerOzelle] doluysa override ('prompt','lyrics','sure','steps','cfg','bpm','keyScale','dil','model' anahtarları) /// [tur] 'tekrar' (aynı parametre) | 'varyant' (rastgele seed) | 'kopya' (form üzerinden edit) Future yenidenUret( AceStepApiIstemci api, AceStepGorev kaynak, { bool yeniSeed = false, Map? parametrelerOzelle, String tur = 'tekrar', String? projeId, String? projeKlasoru, }) async { try { // Seed mantığı: // - 'tekrar' → bitenSeed öncelikli (tıpa tıp aynı sonuç). // bitenSeed yoksa user'ın girdiği seed ya da -1 (rastgele). // - 'varyant' / yeniSeed=true → her zaman -1 (random). // - 'kopya' → kaynak seed (varsa) korunur. int tekrarSeed; bool tekrarRandom; if (yeniSeed || tur == 'varyant') { tekrarSeed = -1; tekrarRandom = true; } else if (tur == 'tekrar') { tekrarSeed = kaynak.bitenSeed ?? kaynak.seed ?? -1; tekrarRandom = tekrarSeed < 0; } else { // kopya tekrarSeed = kaynak.bitenSeed ?? kaynak.seed ?? -1; tekrarRandom = kaynak.useRandomSeed && tekrarSeed < 0; } final liste = await uretimEkle( api: api, prompt: (parametrelerOzelle?['prompt'] as String?) ?? kaynak.prompt, globalCaption: (parametrelerOzelle?['globalCaption'] as String?) ?? (kaynak.globalCaption ?? ''), lyrics: (parametrelerOzelle?['lyrics'] as String?) ?? kaynak.lyrics, sure: (parametrelerOzelle?['sure'] as double?) ?? kaynak.sure, steps: (parametrelerOzelle?['steps'] as int?) ?? kaynak.steps, cfg: (parametrelerOzelle?['cfg'] as double?) ?? kaynak.cfg, bpm: (parametrelerOzelle?['bpm'] as int?) ?? kaynak.bpm, keyScale: (parametrelerOzelle?['keyScale'] as String?) ?? kaynak.keyScale, dil: (parametrelerOzelle?['dil'] as String?) ?? kaynak.dil, dilOtomatik: (parametrelerOzelle?['dilOtomatik'] as bool?) ?? kaynak.dilOtomatik, model: (parametrelerOzelle?['model'] as String?) ?? kaynak.model, thinking: kaynak.thinking, sampleMode: kaynak.sampleMode, seed: tekrarSeed, useRandomSeed: tekrarRandom, projeId: projeId ?? kaynak.projeId, projeKlasoru: projeKlasoru ?? kaynak.projeKlasoru, kaynakGorevId: kaynak.id, uretimTuru: tur, formSnapshot: kaynak.formSnapshot != null ? Map.from(kaynak.formSnapshot!) : null, ); return liste.isEmpty ? null : liste.first; } catch (e) { Log.uyari('AceStepUretim', 'yenidenUret hatası: $e'); return null; } } void _periyodikSorgu() { // 5 saniye: model yükleme/üretim sırasında socket spam'i azalt // (server uvicorn ConnectionResetError log'larını önler). _zamanlayici = Timer.periodic(const Duration(seconds: 5), (_) async { await _aktifleriSorgula(); }); } Future _aktifleriSorgula() async { final aktifler = state.where((g) => g.durum == 0).toList(); if (aktifler.isEmpty) return; if (!await AceStepApiIstemci.portAcikMi()) return; final api = AceStepApiIstemci(); try { final sonuclar = await api.sonucSorgula(aktifler.map((g) => g.taskId).toList()); bool degisti = false; for (final s in sonuclar) { final taskId = s['task_id']?.toString(); if (taskId == null) continue; final idx = state.indexWhere((g) => g.taskId == taskId && g.durum == 0); if (idx < 0) continue; final g = state[idx]; final st = (s['status'] as num?)?.toInt() ?? 0; g.durum = st; g.ilerlemeMetni = s['progress_text']?.toString(); if (st == 1) { // Tamam — ses dosyasını indir await _sesIndir(api, g, s['result']); g.ilerleme = 100; // Server'ın gerçekten kullandığı seed'i parse et — Forma Yükle'de // tıpa tıp aynı sonucu üretmek için bu kullanılır. try { int? bs = _seedAraDerin(s['result']) ?? _seedAraDerin(s); if (bs != null && bs >= 0) { g.bitenSeed = bs; } else if (g.seed != null && g.seed! >= 0) { // Server döndürmediyse: kullanıcı/client seed varsa onu kullan // (client-side üretilen seed bu yola düşer) g.bitenSeed = g.seed; } } catch (_) {} degisti = true; } else if (st == 2) { g.hata = 'Üretim başarısız'; degisti = true; } else { g.ilerleme = (g.ilerleme + 5).clamp(0, 95); degisti = true; } await _kaydet(g); } if (degisti) { state = [...state]; } } catch (e) { Log.uyari('AceStepUretim', 'Sorgu hatası: $e'); } } /// Verilen yapıda (Map/List/String/dynamic) seed-benzeri bir anahtar arar. /// Tanınan anahtarlar: seed, actual_seed, used_seed, random_seed, /// generated_seed, current_seed, sampling_seed. /// Recursive olarak nested map/list içinde de arar. static int? _seedAraDerin(dynamic v) { const seedAnahtarlari = [ 'actual_seed', 'used_seed', 'random_seed', 'generated_seed', 'current_seed', 'sampling_seed', 'seed', ]; if (v == null) return null; if (v is Map) { for (final k in seedAnahtarlari) { final val = v[k]; if (val is num) { final i = val.toInt(); if (i >= 0) return i; } else if (val is String) { final i = int.tryParse(val); if (i != null && i >= 0) return i; } } // Nested for (final inner in v.values) { final r = _seedAraDerin(inner); if (r != null) return r; } } else if (v is List) { for (final el in v) { final r = _seedAraDerin(el); if (r != null) return r; } } else if (v is String) { // Bazen result string olarak JSON gelir → regex ile yakala final m = RegExp(r'"(?:actual_seed|used_seed|random_seed|seed)"\s*:\s*(\d+)') .firstMatch(v); if (m != null) { final i = int.tryParse(m.group(1)!); if (i != null && i >= 0) return i; } } return null; } Future _sesIndir( AceStepApiIstemci api, AceStepGorev g, dynamic result, ) async { try { Log.bilgi( 'AceStepUretim', 'Result tipi: ${result.runtimeType} → $result'); String? uzakYol; // Olası anahtar listesi — server farklı sürümlerde değişik isim kullanır const olasiAnahtarlar = [ 'audio_path', 'output_path', 'path', 'audio_save_path', 'file_path', 'wav_path', 'mp3_path', 'output_audio', 'output', 'file', 'audio_url', 'url', ]; String? mapdanBul(Map m) { for (final k in olasiAnahtarlar) { final v = m[k]; if (v is String && v.isNotEmpty) return v; } // Map içinde nested map/list olabilir — recursive ara for (final v in m.values) { if (v is Map) { final r = mapdanBul(v); if (r != null) return r; } else if (v is List) { for (final el in v) { if (el is Map) { final r = mapdanBul(el); if (r != null) return r; } else if (el is String && (el.endsWith('.mp3') || el.endsWith('.wav') || el.endsWith('.flac'))) { return el; } } } } return null; } if (result is String) { if (result.endsWith('.wav') || result.endsWith('.mp3') || result.endsWith('.flac')) { uzakYol = result; } } else if (result is List && result.isNotEmpty) { for (final el in result) { if (el is Map) { uzakYol = mapdanBul(el); if (uzakYol != null) break; } else if (el is String && (el.endsWith('.mp3') || el.endsWith('.wav') || el.endsWith('.flac'))) { uzakYol = el; break; } } } else if (result is Map) { uzakYol = mapdanBul(result); } // Son çare: result'ın string halinden regex ile uzantılı bir path yakala if (uzakYol == null) { final jstr = result?.toString() ?? ''; final m = RegExp(r'([A-Za-z]:\\[^"\s]+\.(?:mp3|wav|flac))') .firstMatch(jstr) ?? RegExp(r'(/[^"\s]+\.(?:mp3|wav|flac))').firstMatch(jstr) ?? RegExp(r'"([^"]+\.(?:mp3|wav|flac))"').firstMatch(jstr); if (m != null) uzakYol = m.group(1); } // Server bazen "/v1/audio?path=..." URL formatında döndürüyor — // path parametresini çöz, çift encode yapma. if (uzakYol != null && uzakYol.contains('/v1/audio')) { final qIdx = uzakYol.indexOf('?path='); if (qIdx >= 0) { uzakYol = Uri.decodeComponent(uzakYol.substring(qIdx + 6)); } } Log.bilgi('AceStepUretim', 'Çıkarılan uzakYol: $uzakYol'); g.sesUzakYol = uzakYol; // Yerele kopyala — proje klasörüne yaz (yoksa global klasöre) Directory hedefKlasor; if (g.projeKlasoru != null && g.projeKlasoru!.isNotEmpty) { hedefKlasor = Directory(p.join(g.projeKlasoru!, 'ace_step_uretimler')); } else { final docs = await getApplicationDocumentsDirectory(); hedefKlasor = Directory( p.join(docs.path, 'FikoYT-VideoStudyo', 'ace_step_uretimler')); } if (!await hedefKlasor.exists()) await hedefKlasor.create(recursive: true); // Önce: aynı dosya sistemde mı? (lokal path) if (uzakYol != null && await File(uzakYol).exists()) { final yerelHedef = p.join(hedefKlasor.path, 'acestep_${g.id.substring(g.id.length - 8)}${p.extension(uzakYol)}'); await File(uzakYol).copy(yerelHedef); g.sesYerelYol = yerelHedef; Log.bilgi('AceStepUretim', 'Ses kopyalandı: $yerelHedef'); return; } // Değilse REST ile indir — uzantıyı kaynak yoldan al if (uzakYol != null) { try { final ext = p.extension(uzakYol).isNotEmpty ? p.extension(uzakYol) : '.mp3'; final yerelHedef = p.join(hedefKlasor.path, 'acestep_${g.id.substring(g.id.length - 8)}$ext'); final f = await api.sesIndir(uzakYol, yerelHedef); g.sesYerelYol = f.path; Log.bilgi('AceStepUretim', 'Ses indirildi: ${f.path}'); } catch (e) { Log.uyari('AceStepUretim', 'İndirme hatası: $e'); } } } catch (e) { Log.uyari('AceStepUretim', 'Ses indirme genel hatası: $e'); } } @override void dispose() { _zamanlayici?.cancel(); super.dispose(); } } final aceStepUretimProvider = StateNotifierProvider>( (ref) => AceStepUretimDurumu());