import 'dart:io'; import 'dart:math' as math; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; import 'package:url_launcher/url_launcher.dart'; import '../../durum/ace_step_sablon_durumu.dart'; import '../../durum/ace_step_uretim_durumu.dart'; import '../../durum/proje_durumu.dart'; import '../../servisler/ace_step_aktarici.dart'; import '../../servisler/ace_step_api_istemci.dart'; import '../../servisler/ace_step_baslatici.dart'; import '../../servisler/kaynak_kopyalayici.dart'; import '../../servisler/proje_deposu.dart'; import '../../servisler/segment_olusturucu.dart'; import '../ortak/lora_yoneticisi_modal.dart'; import '../ortak/mini_ses_oynatici.dart'; import '../ortak/ses_aralik_oynatici.dart'; import 'ace_step_sihirbaz.dart'; /// ACE-Step için tam native Flutter UI — REST API'yi kullanır. class AceStepSayfasi extends ConsumerStatefulWidget { const AceStepSayfasi({super.key}); @override ConsumerState createState() => _AceStepSayfasiState(); } class _AceStepSayfasiState extends ConsumerState { // Temel final _promptCtrl = TextEditingController(); final _globalCaptionCtrl = TextEditingController(); final _lyricsCtrl = TextEditingController(); final _sampleQueryCtrl = TextEditingController(); final _bpmCtrl = TextEditingController(); final _keyCtrl = TextEditingController(); final _timeSignatureCtrl = TextEditingController(); final _seedCtrl = TextEditingController(); final _instructionCtrl = TextEditingController(); final _customTimestepsCtrl = TextEditingController(); final _trackNameCtrl = TextEditingController(); // LM (Language Model) — lirik üretimi final _lmNegativePromptCtrl = TextEditingController(); double _lmTemperature = 0.85; double _lmCfgScale = 2.5; double _lmTopP = 0.9; double _lmRepetitionPenalty = 1.0; // Decoder bool _useTiledDecode = true; bool _constrainedDecoding = true; // Caption bool _useCotCaption = true; bool _isFormatCaption = false; bool _allowLmBatch = true; bool _lmAyarlariAcik = false; double _sure = 60.0; bool _sureBelirsiz = false; int _steps = 8; double _cfg = 7.0; String _dil = 'tr'; bool _dilOtomatik = true; // Otomatik dil algılama (CoT) String? _model; bool _thinking = false; bool _sampleMode = false; bool _useFormat = false; bool _useRandomSeed = true; // Görev tipi String _taskType = 'text2music'; // Gelişmiş DiT String _inferMethod = 'ode'; double _shift = 3.0; bool _useAdg = false; double _cfgIntervalStart = 0.0; double _cfgIntervalEnd = 1.0; // Repaint / audio2audio String? _referenceAudioPath; String? _srcAudioPath; double _repaintingStart = 0.0; double? _repaintingEnd; String _repaintMode = 'balanced'; double _repaintStrength = 0.5; double _audioCoverStrength = 1.0; double _coverNoiseStrength = 0.0; bool _gelismisAcik = false; // Üretim sayısı (batch) — varsayılan 1, server default 2 idi. int _batchSize = 1; // Çıkış kontrolleri String _audioFormat = 'mp3'; // wav | mp3 | flac int _mp3Bitrate = 192; // dengeli kalite/boyut int _mp3SampleRate = 44100; bool _enableNormalization = false; double _normalizationDb = -14.0; double _fadeInDuration = 0.0; double _fadeOutDuration = 0.0; double _scoreScale = 1.0; String? _loraPath; String _samplerMode = 'euler'; bool _ciktiAcik = false; List> _modeller = []; bool _modelleryukleniyor = false; bool _gonderiyor = false; // Form scroll controller — "Kopya olarak edit" tıklamasında yukarı kaydırmak için final ScrollController _formScrollCtrl = ScrollController(); @override void initState() { super.initState(); // Text controller değişimlerini de auto-save'e bağla for (final c in [ _promptCtrl, _globalCaptionCtrl, _lyricsCtrl, _sampleQueryCtrl, _bpmCtrl, _keyCtrl, _timeSignatureCtrl, _seedCtrl, _instructionCtrl, _customTimestepsCtrl, _trackNameCtrl, _lmNegativePromptCtrl, ]) { c.addListener(_otomatikKaydet); } WidgetsBinding.instance.addPostFrameCallback((_) async { await _modelleriYukle(); await _sonAyarlariYukle(); }); } /// Açılışta son kaydedilmiş ayarları geri yükler (proje öncelikli). Future _sonAyarlariYukle() async { try { final aktifProje = ref.read(projeProvider); final ayarlar = await AceStepSonAyar.yukle(projeKlasoru: aktifProje?.klasorYolu); if (ayarlar == null || !mounted) return; _formuMaptanDoldur(ayarlar); } catch (_) { // sessiz geç — başlangıçta hata loglarsak UI'yi rahatsız etmeyelim } } /// formSnapshot benzeri Map'ten formu doldurur. void _formuMaptanDoldur(Map fs) { setState(() { if (fs['prompt'] is String) _promptCtrl.text = fs['prompt']; if (fs['globalCaption'] is String) { _globalCaptionCtrl.text = fs['globalCaption']; } if (fs['lyrics'] is String) _lyricsCtrl.text = fs['lyrics']; if (fs['sure'] is num) { _sure = (fs['sure'] as num).toDouble(); _sureBelirsiz = false; } _sureBelirsiz = fs['sureBelirsiz'] as bool? ?? _sureBelirsiz; _steps = (fs['steps'] as num?)?.toInt() ?? _steps; _cfg = (fs['cfg'] as num?)?.toDouble() ?? _cfg; if (fs['bpm'] is String) _bpmCtrl.text = fs['bpm']; if (fs['keyScale'] is String) _keyCtrl.text = fs['keyScale']; if (fs['timeSignature'] is String) { _timeSignatureCtrl.text = fs['timeSignature']; } _dil = fs['dil'] as String? ?? _dil; _dilOtomatik = fs['dilOtomatik'] as bool? ?? _dilOtomatik; _model = fs['model'] as String? ?? _model; _thinking = fs['thinking'] as bool? ?? _thinking; _sampleMode = fs['sampleMode'] as bool? ?? _sampleMode; if (fs['sampleQuery'] is String) _sampleQueryCtrl.text = fs['sampleQuery']; _useFormat = fs['useFormat'] as bool? ?? _useFormat; if (fs['seed'] is String) _seedCtrl.text = fs['seed']; _useRandomSeed = fs['kullaniciRandomIstedi'] as bool? ?? fs['useRandomSeed'] as bool? ?? _useRandomSeed; _taskType = fs['taskType'] as String? ?? _taskType; _inferMethod = fs['inferMethod'] as String? ?? _inferMethod; _shift = (fs['shift'] as num?)?.toDouble() ?? _shift; _useAdg = fs['useAdg'] as bool? ?? _useAdg; _cfgIntervalStart = (fs['cfgIntervalStart'] as num?)?.toDouble() ?? _cfgIntervalStart; _cfgIntervalEnd = (fs['cfgIntervalEnd'] as num?)?.toDouble() ?? _cfgIntervalEnd; if (fs['customTimesteps'] is String) { _customTimestepsCtrl.text = fs['customTimesteps']; } _referenceAudioPath = fs['referenceAudioPath'] as String? ?? _referenceAudioPath; _srcAudioPath = fs['srcAudioPath'] as String? ?? _srcAudioPath; _repaintingStart = (fs['repaintingStart'] as num?)?.toDouble() ?? _repaintingStart; _repaintingEnd = (fs['repaintingEnd'] as num?)?.toDouble(); _repaintMode = fs['repaintMode'] as String? ?? _repaintMode; _repaintStrength = (fs['repaintStrength'] as num?)?.toDouble() ?? _repaintStrength; _audioCoverStrength = (fs['audioCoverStrength'] as num?)?.toDouble() ?? _audioCoverStrength; _coverNoiseStrength = (fs['coverNoiseStrength'] as num?)?.toDouble() ?? _coverNoiseStrength; if (fs['instruction'] is String) _instructionCtrl.text = fs['instruction']; _audioFormat = fs['audioFormat'] as String? ?? _audioFormat; _mp3Bitrate = (fs['mp3Bitrate'] as num?)?.toInt() ?? _mp3Bitrate; _mp3SampleRate = (fs['mp3SampleRate'] as num?)?.toInt() ?? _mp3SampleRate; _enableNormalization = fs['enableNormalization'] as bool? ?? _enableNormalization; _normalizationDb = (fs['normalizationDb'] as num?)?.toDouble() ?? _normalizationDb; _fadeInDuration = (fs['fadeInDuration'] as num?)?.toDouble() ?? _fadeInDuration; _fadeOutDuration = (fs['fadeOutDuration'] as num?)?.toDouble() ?? _fadeOutDuration; _scoreScale = (fs['scoreScale'] as num?)?.toDouble() ?? _scoreScale; _loraPath = fs['loraPath'] as String? ?? _loraPath; _samplerMode = fs['samplerMode'] as String? ?? _samplerMode; _batchSize = (fs['batchSize'] as num?)?.toInt() ?? _batchSize; // LM if (fs['lmNegativePrompt'] is String) { _lmNegativePromptCtrl.text = fs['lmNegativePrompt']; } _lmTemperature = (fs['lmTemperature'] as num?)?.toDouble() ?? _lmTemperature; _lmCfgScale = (fs['lmCfgScale'] as num?)?.toDouble() ?? _lmCfgScale; _lmTopP = (fs['lmTopP'] as num?)?.toDouble() ?? _lmTopP; _lmRepetitionPenalty = (fs['lmRepetitionPenalty'] as num?)?.toDouble() ?? _lmRepetitionPenalty; _useTiledDecode = fs['useTiledDecode'] as bool? ?? _useTiledDecode; _constrainedDecoding = fs['constrainedDecoding'] as bool? ?? _constrainedDecoding; _useCotCaption = fs['useCotCaption'] as bool? ?? _useCotCaption; _isFormatCaption = fs['isFormatCaption'] as bool? ?? _isFormatCaption; _allowLmBatch = fs['allowLmBatch'] as bool? ?? _allowLmBatch; if (fs['trackName'] is String) _trackNameCtrl.text = fs['trackName']; }); } /// Tüm form alanlarını snapshot olarak topla. Hem üretim için hem /// otomatik kaydetme/şablon için kullanılır. Map _tumFormuTopla() { return { 'prompt': _promptCtrl.text.trim(), 'globalCaption': _globalCaptionCtrl.text.trim(), 'lyrics': _lyricsCtrl.text.trim(), 'sure': _sureBelirsiz ? null : _sure, 'sureBelirsiz': _sureBelirsiz, 'steps': _steps, 'cfg': _cfg, 'bpm': _bpmCtrl.text, 'keyScale': _keyCtrl.text, 'timeSignature': _timeSignatureCtrl.text, 'dil': _dil, 'dilOtomatik': _dilOtomatik, 'model': _model, 'thinking': _thinking, 'sampleMode': _sampleMode, 'sampleQuery': _sampleQueryCtrl.text, 'useFormat': _useFormat, 'seed': _seedCtrl.text, 'useRandomSeed': _useRandomSeed, 'taskType': _taskType, 'inferMethod': _inferMethod, 'shift': _shift, 'useAdg': _useAdg, 'cfgIntervalStart': _cfgIntervalStart, 'cfgIntervalEnd': _cfgIntervalEnd, 'customTimesteps': _customTimestepsCtrl.text, 'referenceAudioPath': _referenceAudioPath, 'srcAudioPath': _srcAudioPath, 'repaintingStart': _repaintingStart, 'repaintingEnd': _repaintingEnd, 'repaintMode': _repaintMode, 'repaintStrength': _repaintStrength, 'audioCoverStrength': _audioCoverStrength, 'coverNoiseStrength': _coverNoiseStrength, 'instruction': _instructionCtrl.text, 'audioFormat': _audioFormat, 'mp3Bitrate': _mp3Bitrate, 'mp3SampleRate': _mp3SampleRate, 'enableNormalization': _enableNormalization, 'normalizationDb': _normalizationDb, 'fadeInDuration': _fadeInDuration, 'fadeOutDuration': _fadeOutDuration, 'scoreScale': _scoreScale, 'loraPath': _loraPath, 'samplerMode': _samplerMode, 'batchSize': _batchSize, 'lmNegativePrompt': _lmNegativePromptCtrl.text, 'lmTemperature': _lmTemperature, 'lmCfgScale': _lmCfgScale, 'lmTopP': _lmTopP, 'lmRepetitionPenalty': _lmRepetitionPenalty, 'useTiledDecode': _useTiledDecode, 'constrainedDecoding': _constrainedDecoding, 'useCotCaption': _useCotCaption, 'isFormatCaption': _isFormatCaption, 'allowLmBatch': _allowLmBatch, 'trackName': _trackNameCtrl.text, }; } /// Form değiştiğinde otomatik kaydet (debounced 800ms). void _otomatikKaydet() { final aktifProje = ref.read(projeProvider); AceStepSonAyar.kaydetDebounced( ayarlar: _tumFormuTopla(), projeKlasoru: aktifProje?.klasorYolu, ); } @override void setState(VoidCallback fn) { super.setState(fn); // Her form değişikliği sonrası otomatik kaydı tetikle _otomatikKaydet(); } @override void dispose() { _promptCtrl.dispose(); _globalCaptionCtrl.dispose(); _lyricsCtrl.dispose(); _sampleQueryCtrl.dispose(); _bpmCtrl.dispose(); _keyCtrl.dispose(); _timeSignatureCtrl.dispose(); _seedCtrl.dispose(); _instructionCtrl.dispose(); // Son ayarları anında kaydet (dispose anında debounce çalışmayabilir) try { final aktifProje = ref.read(projeProvider); AceStepSonAyar.kaydetHemen( ayarlar: _tumFormuTopla(), projeKlasoru: aktifProje?.klasorYolu, ); } catch (_) {} _customTimestepsCtrl.dispose(); _trackNameCtrl.dispose(); _lmNegativePromptCtrl.dispose(); _formScrollCtrl.dispose(); super.dispose(); } /// Görev kartından gelen "Forma yükle" / "Kopya olarak edit" sinyalini uygula. void _formdanYukle(AceStepGorev g) { setState(() { // Temel alanlar (her durumda) _promptCtrl.text = g.prompt; _lyricsCtrl.text = g.lyrics; _globalCaptionCtrl.text = g.globalCaption ?? ''; _bpmCtrl.text = g.bpm?.toString() ?? ''; _keyCtrl.text = g.keyScale ?? ''; if (g.sure != null) { _sure = g.sure!.clamp(1, 240).toDouble(); _sureBelirsiz = false; } else { _sureBelirsiz = true; } _steps = g.steps; _cfg = g.cfg; _dil = g.dil; _dilOtomatik = g.dilOtomatik; _model = g.model; _thinking = g.thinking; _sampleMode = g.sampleMode; // ✨ Tam form snapshot varsa TÜM alanları geri yükle // (timeSignature, sampleQuery, useFormat, repaintMode, audioFormat, vb.) final fs = g.formSnapshot; if (fs != null) { // Controller alanları if (fs['timeSignature'] is String) { _timeSignatureCtrl.text = fs['timeSignature'] as String; } if (fs['sampleQuery'] is String) { _sampleQueryCtrl.text = fs['sampleQuery'] as String; } if (fs['seed'] is String) { _seedCtrl.text = fs['seed'] as String; } if (fs['customTimesteps'] is String) { _customTimestepsCtrl.text = fs['customTimesteps'] as String; } if (fs['instruction'] is String) { _instructionCtrl.text = fs['instruction'] as String; } // Bool / String / num alanları _useFormat = fs['useFormat'] as bool? ?? _useFormat; _useRandomSeed = fs['useRandomSeed'] as bool? ?? _useRandomSeed; _useAdg = fs['useAdg'] as bool? ?? _useAdg; _enableNormalization = fs['enableNormalization'] as bool? ?? _enableNormalization; _taskType = fs['taskType'] as String? ?? _taskType; _inferMethod = fs['inferMethod'] as String? ?? _inferMethod; _repaintMode = fs['repaintMode'] as String? ?? _repaintMode; _samplerMode = fs['samplerMode'] as String? ?? _samplerMode; _audioFormat = fs['audioFormat'] as String? ?? _audioFormat; _referenceAudioPath = fs['referenceAudioPath'] as String? ?? _referenceAudioPath; _srcAudioPath = fs['srcAudioPath'] as String? ?? _srcAudioPath; _loraPath = fs['loraPath'] as String? ?? _loraPath; _shift = (fs['shift'] as num?)?.toDouble() ?? _shift; _cfgIntervalStart = (fs['cfgIntervalStart'] as num?)?.toDouble() ?? _cfgIntervalStart; _cfgIntervalEnd = (fs['cfgIntervalEnd'] as num?)?.toDouble() ?? _cfgIntervalEnd; _repaintingStart = (fs['repaintingStart'] as num?)?.toDouble() ?? _repaintingStart; _repaintingEnd = (fs['repaintingEnd'] as num?)?.toDouble(); _repaintStrength = (fs['repaintStrength'] as num?)?.toDouble() ?? _repaintStrength; _audioCoverStrength = (fs['audioCoverStrength'] as num?)?.toDouble() ?? _audioCoverStrength; _coverNoiseStrength = (fs['coverNoiseStrength'] as num?)?.toDouble() ?? _coverNoiseStrength; _normalizationDb = (fs['normalizationDb'] as num?)?.toDouble() ?? _normalizationDb; _fadeInDuration = (fs['fadeInDuration'] as num?)?.toDouble() ?? _fadeInDuration; _fadeOutDuration = (fs['fadeOutDuration'] as num?)?.toDouble() ?? _fadeOutDuration; _scoreScale = (fs['scoreScale'] as num?)?.toDouble() ?? _scoreScale; _mp3Bitrate = (fs['mp3Bitrate'] as num?)?.toInt() ?? _mp3Bitrate; _mp3SampleRate = (fs['mp3SampleRate'] as num?)?.toInt() ?? _mp3SampleRate; _batchSize = (fs['batchSize'] as num?)?.toInt() ?? _batchSize; // LM if (fs['lmNegativePrompt'] is String) { _lmNegativePromptCtrl.text = fs['lmNegativePrompt'] as String; } _lmTemperature = (fs['lmTemperature'] as num?)?.toDouble() ?? _lmTemperature; _lmCfgScale = (fs['lmCfgScale'] as num?)?.toDouble() ?? _lmCfgScale; _lmTopP = (fs['lmTopP'] as num?)?.toDouble() ?? _lmTopP; _lmRepetitionPenalty = (fs['lmRepetitionPenalty'] as num?)?.toDouble() ?? _lmRepetitionPenalty; _useTiledDecode = fs['useTiledDecode'] as bool? ?? _useTiledDecode; _constrainedDecoding = fs['constrainedDecoding'] as bool? ?? _constrainedDecoding; _useCotCaption = fs['useCotCaption'] as bool? ?? _useCotCaption; _isFormatCaption = fs['isFormatCaption'] as bool? ?? _isFormatCaption; _allowLmBatch = fs['allowLmBatch'] as bool? ?? _allowLmBatch; if (fs['trackName'] is String) { _trackNameCtrl.text = fs['trackName'] as String; } } // Seed önceliği: bitenSeed (server'ın gerçek seed'i) > seed (kullanıcı) > // formSnapshot['seed']. Tıpa tıp aynı sonuç için bu sıra önemli. if (g.bitenSeed != null && g.bitenSeed! >= 0) { _seedCtrl.text = g.bitenSeed!.toString(); _useRandomSeed = false; } else if (g.seed != null && g.seed! >= 0) { _seedCtrl.text = g.seed!.toString(); _useRandomSeed = false; } }); if (_formScrollCtrl.hasClients) { _formScrollCtrl.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Form değerleri yüklendi'), duration: Duration(seconds: 2), ), ); } } Future _modelleriYukle() async { if (!await AceStepApiIstemci.portAcikMi()) return; setState(() => _modelleryukleniyor = true); final api = AceStepApiIstemci(); final m = await api.modelListesi(); if (mounted) { setState(() { _modeller = m; _modelleryukleniyor = false; }); } } Future _uret() async { final durum = ref.read(aceStepProvider); if (!durum.apiCalisiyor) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Önce API server\'ı başlat (yan butondan)'), backgroundColor: Colors.red, ), ); return; } if (!_sampleMode && _promptCtrl.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Prompt veya sample mode gerekli')), ); return; } setState(() => _gonderiyor = true); try { final api = AceStepApiIstemci(); // CLIENT-SIDE SEED ÜRETİMİ — server seed döndürmese bile her zaman // gerçek seed'i biliriz. Kullanıcı seed yazdıysa onu kullan; yoksa // (random toggle veya boş) burada üret. Server'a sabit gönder. int kesinSeed; final yazilanSeed = int.tryParse(_seedCtrl.text); if (!_useRandomSeed && yazilanSeed != null && yazilanSeed >= 0) { kesinSeed = yazilanSeed; } else { // 1 .. 2^31-1 arası — tüm framework'lerde güvenli aralık kesinSeed = math.Random().nextInt(2147483646) + 1; } // UI'a yansıt — kullanıcı görsün if (_seedCtrl.text != kesinSeed.toString()) { _seedCtrl.text = kesinSeed.toString(); } // TÜM form alanlarını snapshot olarak topla — Forma Yükle'de eksiksiz final formSnapshot = { 'prompt': _promptCtrl.text.trim(), 'globalCaption': _globalCaptionCtrl.text.trim(), 'lyrics': _lyricsCtrl.text.trim(), 'sure': _sureBelirsiz ? null : _sure, 'sureBelirsiz': _sureBelirsiz, 'steps': _steps, 'cfg': _cfg, 'bpm': _bpmCtrl.text, 'keyScale': _keyCtrl.text, 'timeSignature': _timeSignatureCtrl.text, 'dil': _dil, 'dilOtomatik': _dilOtomatik, 'model': _model, 'thinking': _thinking, 'sampleMode': _sampleMode, 'sampleQuery': _sampleQueryCtrl.text, 'useFormat': _useFormat, 'seed': kesinSeed.toString(), // Kullanıcının orijinal "random" tercihini sakla (UI'da göstermek için). // Server'a useRandomSeed=false gidiyor (kesin seed'le). 'useRandomSeed': false, 'kullaniciRandomIstedi': _useRandomSeed, 'taskType': _taskType, 'inferMethod': _inferMethod, 'shift': _shift, 'useAdg': _useAdg, 'cfgIntervalStart': _cfgIntervalStart, 'cfgIntervalEnd': _cfgIntervalEnd, 'customTimesteps': _customTimestepsCtrl.text, 'referenceAudioPath': _referenceAudioPath, 'srcAudioPath': _srcAudioPath, 'repaintingStart': _repaintingStart, 'repaintingEnd': _repaintingEnd, 'repaintMode': _repaintMode, 'repaintStrength': _repaintStrength, 'audioCoverStrength': _audioCoverStrength, 'coverNoiseStrength': _coverNoiseStrength, 'instruction': _instructionCtrl.text, 'audioFormat': _audioFormat, 'mp3Bitrate': _mp3Bitrate, 'mp3SampleRate': _mp3SampleRate, 'enableNormalization': _enableNormalization, 'normalizationDb': _normalizationDb, 'fadeInDuration': _fadeInDuration, 'fadeOutDuration': _fadeOutDuration, 'scoreScale': _scoreScale, 'loraPath': _loraPath, 'samplerMode': _samplerMode, 'batchSize': _batchSize, // LM 'lmNegativePrompt': _lmNegativePromptCtrl.text, 'lmTemperature': _lmTemperature, 'lmCfgScale': _lmCfgScale, 'lmTopP': _lmTopP, 'lmRepetitionPenalty': _lmRepetitionPenalty, 'useTiledDecode': _useTiledDecode, 'constrainedDecoding': _constrainedDecoding, 'useCotCaption': _useCotCaption, 'isFormatCaption': _isFormatCaption, 'allowLmBatch': _allowLmBatch, 'trackName': _trackNameCtrl.text, }; final yeniGorevler = await ref.read(aceStepUretimProvider.notifier).uretimEkle( api: api, prompt: _promptCtrl.text.trim(), globalCaption: _globalCaptionCtrl.text.trim(), lyrics: _lyricsCtrl.text.trim(), sure: _sureBelirsiz ? null : _sure, steps: _steps, cfg: _cfg, bpm: int.tryParse(_bpmCtrl.text), keyScale: _keyCtrl.text.trim(), timeSignature: _timeSignatureCtrl.text.trim(), dil: _dil, dilOtomatik: _dilOtomatik, model: _model, thinking: _thinking, sampleMode: _sampleMode, sampleQuery: _sampleQueryCtrl.text.trim(), useFormat: _useFormat, // Client-side üretilen kesin seed — server her zaman bunu kullanır seed: kesinSeed, useRandomSeed: false, taskType: _taskType, inferMethod: _inferMethod, shift: _shift, useAdg: _useAdg, cfgIntervalStart: _cfgIntervalStart, cfgIntervalEnd: _cfgIntervalEnd, customTimesteps: _customTimestepsCtrl.text.trim().isEmpty ? null : _customTimestepsCtrl.text.trim(), referenceAudioPath: _referenceAudioPath, srcAudioPath: _srcAudioPath, repaintingStart: _repaintingStart, repaintingEnd: _repaintingEnd, repaintMode: _repaintMode, repaintStrength: _repaintStrength, audioCoverStrength: _audioCoverStrength, coverNoiseStrength: _coverNoiseStrength, instruction: _instructionCtrl.text.trim().isEmpty ? null : _instructionCtrl.text.trim(), audioFormat: _audioFormat, mp3Bitrate: _audioFormat == 'mp3' ? _mp3Bitrate : null, mp3SampleRate: _audioFormat == 'mp3' ? _mp3SampleRate : null, enableNormalization: _enableNormalization, normalizationDb: _enableNormalization ? _normalizationDb : null, fadeInDuration: _fadeInDuration > 0 ? _fadeInDuration : null, fadeOutDuration: _fadeOutDuration > 0 ? _fadeOutDuration : null, scoreScale: _scoreScale != 1.0 ? _scoreScale : null, loraPath: _loraPath, samplerMode: _samplerMode != 'euler' ? _samplerMode : null, batchSize: _batchSize, // LM parametreleri lmNegativePrompt: _lmNegativePromptCtrl.text.trim().isEmpty ? null : _lmNegativePromptCtrl.text.trim(), lmTemperature: _lmTemperature != 0.85 ? _lmTemperature : null, lmCfgScale: _lmCfgScale != 2.5 ? _lmCfgScale : null, lmTopP: _lmTopP != 0.9 ? _lmTopP : null, lmRepetitionPenalty: _lmRepetitionPenalty != 1.0 ? _lmRepetitionPenalty : null, useTiledDecode: !_useTiledDecode ? false : null, constrainedDecoding: !_constrainedDecoding ? false : null, useCotCaption: !_useCotCaption ? false : null, isFormatCaption: _isFormatCaption ? true : null, allowLmBatch: !_allowLmBatch ? false : null, trackName: _trackNameCtrl.text.trim().isEmpty ? null : _trackNameCtrl.text.trim(), projeId: ref.read(projeProvider)?.id, projeKlasoru: ref.read(projeProvider)?.klasorYolu, formSnapshot: formSnapshot, ); if (mounted) { final n = yeniGorevler.length; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(n > 1 ? '$n varyant kuyruğa eklendi (batch)' : 'Üretim kuyruğa eklendi'), backgroundColor: n > 1 ? Colors.deepPurple : null, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hata: $e'), backgroundColor: Colors.red), ); } } finally { if (mounted) setState(() => _gonderiyor = false); } } Future _rastgeleSample() async { if (!await AceStepApiIstemci.portAcikMi()) return; final api = AceStepApiIstemci(); final sonuc = await api.rastgeleSampleOlustur( description: _sampleQueryCtrl.text, vocalLanguage: _dil, ); if (sonuc.isNotEmpty && mounted) { _promptCtrl.text = sonuc['prompt']?.toString() ?? sonuc['caption']?.toString() ?? _promptCtrl.text; _lyricsCtrl.text = sonuc['lyrics']?.toString() ?? _lyricsCtrl.text; setState(() {}); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('LLM ile prompt + sözler dolduruldu')), ); } } @override Widget build(BuildContext context) { final durum = ref.watch(aceStepProvider); final theme = Theme.of(context); // Görev kartından "forma yükle" sinyali geldiğinde formu doldur ref.listen(aceStepFormaYukleProvider, (eski, yeni) { if (yeni != null) { _formdanYukle(yeni); // Sinyali tüket ref.read(aceStepFormaYukleProvider.notifier).state = null; } }); return Column( children: [ _UstBar(durum: durum), Expanded( child: durum.apiCalisiyor ? Row( children: [ Expanded( flex: 3, child: _UretimFormu(state: this), ), Container( width: 1, color: theme.dividerColor, ), const Expanded( flex: 2, child: _GorevListesi(), ), ], ) : _ApiBaslamaEkrani(durum: durum), ), ], ); } } class _UstBar extends ConsumerWidget { final AceStepDurum durum; const _UstBar({required this.durum}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: theme.colorScheme.surface, border: Border(bottom: BorderSide(color: theme.dividerColor)), ), child: Row( children: [ Icon(Icons.music_note, color: theme.colorScheme.primary, size: 28), const SizedBox(width: 8), Text('ACE-Step', style: theme.textTheme.titleLarge), const SizedBox(width: 4), Text('Müzik Üretici', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, )), const SizedBox(width: 12), Consumer(builder: (ctx, wref, _) { final aktifProje = wref.watch(projeProvider); if (aktifProje == null) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(14), border: Border.all(color: theme.colorScheme.primary, width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.movie_creation_outlined, size: 12, color: theme.colorScheme.primary), const SizedBox(width: 4), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 220), child: Text( aktifProje.isim, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: theme.colorScheme.primary), ), ), ], ), ); }), const SizedBox(width: 12), // API durum _DurumChipi( etiket: 'API', calisiyor: durum.apiCalisiyor, port: 8001, onTikla: () => ref.read(aceStepProvider.notifier).apiBaslat(), ), const SizedBox(width: 6), _DurumChipi( etiket: 'Gradio', calisiyor: durum.gradioCalisiyor, port: 7860, onTikla: () => ref.read(aceStepProvider.notifier).gradioBaslat(), ), const Spacer(), IconButton( icon: const Icon(Icons.bookmark_add_outlined, size: 20), tooltip: 'Müzik LoRA Yöneticisi (indir/yönet)', onPressed: () => LoraYoneticisiModal.goster(context, LoraModulu.muzik), ), if (durum.gradioCalisiyor) IconButton( icon: const Icon(Icons.web_outlined, size: 20), tooltip: 'Gradio UI tarayıcıda aç', onPressed: () => launchUrl(Uri.parse(durum.gradioUrl)), ), if (durum.apiCalisiyor) IconButton( icon: const Icon(Icons.api_outlined, size: 20), tooltip: 'API Docs (Swagger)', onPressed: () => launchUrl(Uri.parse('${durum.apiUrl}/docs')), ), ], ), ); } } class _DurumChipi extends StatelessWidget { final String etiket; final bool calisiyor; final int port; final VoidCallback onTikla; const _DurumChipi({ required this.etiket, required this.calisiyor, required this.port, required this.onTikla, }); @override Widget build(BuildContext context) { return InkWell( onTap: calisiyor ? null : onTikla, borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: calisiyor ? Colors.green.withValues(alpha: 0.2) : Colors.grey.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(10), border: Border.all( color: calisiyor ? Colors.green : Colors.grey, width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: BoxDecoration( shape: BoxShape.circle, color: calisiyor ? Colors.green : Colors.grey, ), ), const SizedBox(width: 6), Text( calisiyor ? '$etiket :$port' : '$etiket başlat', style: const TextStyle(fontSize: 11), ), ], ), ), ); } } class _ApiBaslamaEkrani extends ConsumerWidget { final AceStepDurum durum; const _ApiBaslamaEkrani({required this.durum}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: Padding( padding: const EdgeInsets.all(40), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.cloud_off, size: 72, color: theme.colorScheme.outline) .animate() .fadeIn(duration: 300.ms), const SizedBox(height: 16), Text( 'ACE-Step API Server kapalı', style: theme.textTheme.titleLarge, ), const SizedBox(height: 8), Text( 'Native müzik üretimi için ACE-Step REST API server\'ı (port 8001) çalışmalı. ' 'Bu, Gradio UI\'sından farklı — daha hızlı, daha kontrollü, doğrudan API.', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium, ), if (durum.hata != null) ...[ const SizedBox(height: 8), Text(durum.hata!, style: const TextStyle(color: Colors.red, fontSize: 12)), ], const SizedBox(height: 24), FilledButton.icon( icon: const Icon(Icons.play_arrow), label: const Text('API Server\'ı Başlat'), onPressed: () => ref.read(aceStepProvider.notifier).apiBaslat(), ), const SizedBox(height: 8), OutlinedButton.icon( icon: const Icon(Icons.web), label: const Text('Gradio UI Başlat (alternatif)'), onPressed: () => ref.read(aceStepProvider.notifier).gradioBaslat(), ), const SizedBox(height: 24), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.info_outline, size: 14, color: theme.colorScheme.outline), const SizedBox(width: 6), Text( 'İlk başlatmada model indirme ~5-10 dk sürebilir', style: theme.textTheme.labelSmall, ), ]), const SizedBox(height: 4), Text( 'Konum: video-uretim\\ace-step\\ACE-Step-1.5\\start_api_server.bat', style: theme.textTheme.labelSmall?.copyWith( fontFamily: 'monospace'), ), ], ), ), ], ), ), ), ); } } class _UretimFormu extends StatelessWidget { final _AceStepSayfasiState state; const _UretimFormu({required this.state}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return SingleChildScrollView( controller: state._formScrollCtrl, padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Sihirbaz + şablon barı _SihirbazVeSablonBar(state: state), const SizedBox(height: 12), // Görev tipi seçici _GorevTipiSecici(state: state), const SizedBox(height: 12), // Sample mode Card( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: SwitchListTile( contentPadding: EdgeInsets.zero, dense: true, title: const Text('Sample Mode (LLM otomatik üret)'), subtitle: const Text( 'Açıkken: kısa açıklama yaz, LLM caption + lyrics + her şeyi yapsın', style: TextStyle(fontSize: 11), ), value: state._sampleMode, onChanged: (v) => state.setState(() => state._sampleMode = v), ), ), ), const SizedBox(height: 12), if (state._sampleMode) _Bolum( etiket: 'AÇIKLAMA (sample query)', icon: Icons.text_snippet, child: TextField( controller: state._sampleQueryCtrl, maxLines: 2, decoration: const InputDecoration( hintText: 'Örn: lofi rap with melancholic piano about lost love', ), ), ) else ...[ _Bolum( etiket: 'PROMPT (caption / stil)', icon: Icons.short_text, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ TextButton.icon( icon: const Icon(Icons.auto_awesome, size: 14), label: const Text('LLM doldur', style: TextStyle(fontSize: 11)), onPressed: state._rastgeleSample, ), IconButton( icon: const Icon(Icons.clear, size: 14), tooltip: 'Promptu temizle', padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 24, minHeight: 24), onPressed: () => state._promptCtrl.clear(), ), ], ), child: TextField( controller: state._promptCtrl, maxLines: 3, decoration: const InputDecoration( hintText: 'Örn: trap, dark melodic, 808 bass, female vocals', ), ), ), const SizedBox(height: 12), _Bolum( etiket: 'SÖZLER (lyrics, opsiyonel)', icon: Icons.lyrics_outlined, child: TextField( controller: state._lyricsCtrl, maxLines: 6, decoration: const InputDecoration( hintText: '[verse]\n...\n[chorus]\n...\n\nBoş bırakırsan enstrümantal', ), ), ), const SizedBox(height: 8), CheckboxListTile( contentPadding: EdgeInsets.zero, dense: true, title: const Text('use_format (LLM ile lirik düzelt)', style: TextStyle(fontSize: 12)), value: state._useFormat, onChanged: (v) => state.setState(() => state._useFormat = v ?? false), ), ], const SizedBox(height: 12), // Kaynak ses dosyası (görev tipine göre değişir) _KaynakSesAlani(state: state), const SizedBox(height: 12), // Text2music modunda OPSIYONEL stil referansı if (state._taskType == 'text2music') _Bolum( etiket: 'STİL REFERANS SES (opsiyonel — bu sese benzetsin)', icon: Icons.style, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _DosyaSecici( yol: state._referenceAudioPath, tip: 'audio', onSec: (yol) => state.setState( () => state._referenceAudioPath = yol), onTemizle: () => state.setState( () => state._referenceAudioPath = null), ), if (state._referenceAudioPath != null) ...[ const SizedBox(height: 6), Row( children: [ const SizedBox( width: 100, child: Text('Benzetme gücü', style: TextStyle(fontSize: 11)), ), Expanded( child: Slider( value: state._audioCoverStrength, min: 0, max: 1, divisions: 20, label: state._audioCoverStrength .toStringAsFixed(2), onChanged: (v) => state.setState( () => state._audioCoverStrength = v), ), ), SizedBox( width: 40, child: Text( state._audioCoverStrength.toStringAsFixed(2), style: const TextStyle( fontSize: 11, fontFamily: 'monospace', ), ), ), ], ), Text( '0=hiç benzetme · 1=tamamen taklit · 0.5 önerilen', style: Theme.of(context).textTheme.labelSmall, ), ], ], ), ), if (state._taskType == 'text2music' && state._referenceAudioPath == null) const SizedBox(height: 12) else if (state._taskType == 'text2music') const SizedBox(height: 12), // Süre _Bolum( etiket: 'SÜRE', icon: Icons.timer_outlined, child: Column( children: [ Row( children: [ Switch( value: !state._sureBelirsiz, onChanged: (v) => state.setState(() => state._sureBelirsiz = !v), ), const SizedBox(width: 8), Expanded( child: Text( state._sureBelirsiz ? 'Otomatik süre (LLM karar verir)' : 'Sabit: ${state._sure.toInt()}s (${(state._sure / 60).toStringAsFixed(1)}dk)', style: const TextStyle(fontSize: 12), ), ), ], ), if (!state._sureBelirsiz) Slider( value: state._sure, min: 10, max: 300, divisions: 290, label: '${state._sure.toInt()}s', onChanged: (v) => state.setState(() => state._sure = v), ), ], ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _Bolum( etiket: 'STEPS', icon: Icons.linear_scale, child: Column( children: [ Slider( value: state._steps.toDouble(), min: 4, max: 50, divisions: 46, label: state._steps.toString(), onChanged: (v) => state.setState(() => state._steps = v.round()), ), Text( '${state._steps} adım · ${state._steps < 10 ? "hızlı" : state._steps < 25 ? "dengeli" : "kaliteli"}', style: theme.textTheme.labelSmall, ), ], ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'CFG', icon: Icons.compass_calibration, child: Column( children: [ Slider( value: state._cfg, min: 1, max: 15, divisions: 28, label: state._cfg.toStringAsFixed(1), onChanged: (v) => state.setState(() => state._cfg = v), ), Text( '${state._cfg.toStringAsFixed(1)} · ${state._cfg < 4 ? "serbest" : state._cfg < 8 ? "dengeli" : "bağımlı"}', style: theme.textTheme.labelSmall, ), ], ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'VARYANT (BATCH)', icon: Icons.layers, child: Column( children: [ Slider( value: state._batchSize.toDouble(), min: 1, max: 8, divisions: 7, label: state._batchSize.toString(), onChanged: (v) => state .setState(() => state._batchSize = v.round()), ), Text( '${state._batchSize} varyant · aynı promptla farklı seed', style: theme.textTheme.labelSmall, ), ], ), ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _Bolum( etiket: 'BPM', icon: Icons.speed, child: TextField( controller: state._bpmCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration(hintText: 'Otomatik'), ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'KEY', icon: Icons.piano, child: TextField( controller: state._keyCtrl, decoration: const InputDecoration(hintText: 'Cmaj/Amin'), ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'TIME SIG', icon: Icons.alarm, child: TextField( controller: state._timeSignatureCtrl, decoration: const InputDecoration(hintText: '4/4'), ), ), ), const SizedBox(width: 12), Expanded( flex: 2, child: _Bolum( etiket: 'VOCAL DİLİ', icon: Icons.language, trailing: Tooltip( message: state._dilOtomatik ? 'Otomatik: LLM her satır için dili kendisi tespit eder ' '(çok dilli şarkılar için ideal)\nKapatmak için tıkla' : 'Manuel: sabit dil kullanılır\nOtomatik için tıkla', child: FilterChip( visualDensity: VisualDensity.compact, label: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(state._dilOtomatik ? Icons.auto_awesome : Icons.translate, size: 11), const SizedBox(width: 3), Text(state._dilOtomatik ? 'Oto' : 'Manuel', style: const TextStyle(fontSize: 10)), ], ), selected: state._dilOtomatik, onSelected: (v) => state.setState(() => state._dilOtomatik = v), ), ), child: DropdownButtonFormField( initialValue: state._dil, decoration: InputDecoration( isDense: true, helperText: state._dilOtomatik ? 'Fallback dil (otomatik tespit başarısızsa)' : 'Tüm vokal bu dilde söylenir', helperMaxLines: 2, ), items: const [ DropdownMenuItem(value: 'tr', child: Text('Türkçe')), DropdownMenuItem(value: 'en', child: Text('İngilizce')), DropdownMenuItem(value: 'es', child: Text('İspanyolca')), DropdownMenuItem(value: 'fr', child: Text('Fransızca')), DropdownMenuItem(value: 'de', child: Text('Almanca')), DropdownMenuItem(value: 'it', child: Text('İtalyanca')), DropdownMenuItem(value: 'ja', child: Text('Japonca')), DropdownMenuItem(value: 'ko', child: Text('Korece')), DropdownMenuItem(value: 'zh', child: Text('Çince')), DropdownMenuItem(value: 'ru', child: Text('Rusça')), DropdownMenuItem(value: 'ar', child: Text('Arapça')), DropdownMenuItem(value: 'pt', child: Text('Portekizce')), DropdownMenuItem(value: 'hi', child: Text('Hintçe')), ], onChanged: (v) => v != null ? state.setState(() => state._dil = v) : null, ), ), ), ], ), const SizedBox(height: 12), // Model _Bolum( etiket: 'MODEL', icon: Icons.memory, trailing: IconButton( icon: const Icon(Icons.refresh, size: 14), tooltip: 'Yenile', padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 24, minHeight: 24), onPressed: state._modelleriYukle, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (state._modelleryukleniyor) const LinearProgressIndicator(minHeight: 2) else if (state._modeller.isEmpty) Text( 'Model listesi alınamadı (varsayılan kullanılır)', style: theme.textTheme.labelSmall, ) else DropdownButtonFormField( initialValue: state._model, decoration: const InputDecoration(hintText: 'Varsayılan'), items: [ const DropdownMenuItem( value: null, child: Text('Varsayılan')), ...state._modeller.map((m) { final id = m['id']?.toString() ?? m['model_id']?.toString() ?? m['name']?.toString() ?? m['model']?.toString() ?? m.values.firstOrNull?.toString() ?? '?'; return DropdownMenuItem(value: id, child: Text(id)); }), ], onChanged: (v) => state.setState(() => state._model = v), ), CheckboxListTile( contentPadding: EdgeInsets.zero, dense: true, title: const Text('thinking (LM ile audio code üret)', style: TextStyle(fontSize: 12)), subtitle: const Text( 'Açıkken: 5Hz LM ile yüksek kalite audio code (yavaş)', style: TextStyle(fontSize: 10), ), value: state._thinking, onChanged: (v) => state.setState(() => state._thinking = v ?? false), ), ], ), ), const SizedBox(height: 12), // Seed _Bolum( etiket: 'SEED', icon: Icons.casino, child: Row( children: [ Expanded( child: TextField( controller: state._seedCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration(hintText: 'Rastgele'), ), ), const SizedBox(width: 8), Row( mainAxisSize: MainAxisSize.min, children: [ Checkbox( value: state._useRandomSeed, onChanged: (v) => state.setState( () => state._useRandomSeed = v ?? true), ), const Text('Rastgele', style: TextStyle(fontSize: 11)), ], ), ], ), ), const SizedBox(height: 12), // Gelişmiş ayarlar Card( child: ExpansionTile( tilePadding: const EdgeInsets.symmetric(horizontal: 12), title: const Row( children: [ Icon(Icons.tune, size: 18), SizedBox(width: 8), Text('Gelişmiş Ayarlar'), ], ), subtitle: const Text( 'Diffusion / inference / repaint parametreleri', style: TextStyle(fontSize: 11), ), initiallyExpanded: state._gelismisAcik, onExpansionChanged: (v) => state.setState(() => state._gelismisAcik = v), children: [ Padding( padding: const EdgeInsets.all(12), child: _GelismisAyarlar(state: state), ), ], ), ), const SizedBox(height: 8), // LM (Language Model) / Lirik ayarları Card( child: ExpansionTile( tilePadding: const EdgeInsets.symmetric(horizontal: 12), title: const Row( children: [ Icon(Icons.auto_stories, size: 18), SizedBox(width: 8), Text('LM / Lirik Ayarları'), ], ), subtitle: const Text( 'Negative prompt / temperature / CFG / decoder', style: TextStyle(fontSize: 11), ), initiallyExpanded: state._lmAyarlariAcik, onExpansionChanged: (v) => state.setState(() => state._lmAyarlariAcik = v), children: [ Padding( padding: const EdgeInsets.all(12), child: _LmAyarlari(state: state), ), ], ), ), const SizedBox(height: 8), // Çıkış ayarları Card( child: ExpansionTile( tilePadding: const EdgeInsets.symmetric(horizontal: 12), title: const Row( children: [ Icon(Icons.output, size: 18), SizedBox(width: 8), Text('Çıkış Ayarları'), ], ), subtitle: const Text( 'Format / bitrate / normalization / fade / LoRA', style: TextStyle(fontSize: 11), ), initiallyExpanded: state._ciktiAcik, onExpansionChanged: (v) => state.setState(() => state._ciktiAcik = v), children: [ Padding( padding: const EdgeInsets.all(12), child: _CikisAyarlari(state: state), ), ], ), ), const SizedBox(height: 16), FilledButton.icon( icon: state._gonderiyor ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.music_note), label: const Text('Müzik Üret'), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), textStyle: const TextStyle(fontSize: 14), ), onPressed: state._gonderiyor ? null : state._uret, ), ], ), ); } } class _KaynakSesAlani extends StatelessWidget { final _AceStepSayfasiState state; const _KaynakSesAlani({required this.state}); bool get _useSrc => state._taskType == 'audio2audio' || state._taskType == 'extend' || state._taskType == 'complete'; bool get _aralikSecimiVar => state._taskType == 'repaint' || state._taskType == 'extend'; String? _kaynakYol() => _useSrc ? state._srcAudioPath : state._referenceAudioPath; void _kaynakAyarla(String? yol) { if (_useSrc) { state._srcAudioPath = yol; } else { state._referenceAudioPath = yol; } } @override Widget build(BuildContext context) { final tip = state._taskType; if (tip == 'text2music') return const SizedBox.shrink(); final etiket = switch (tip) { 'audio2audio' => 'KAYNAK SES (audio2audio)', 'cover' => 'KAYNAK ŞARKI (cover edilecek)', 'repaint' => 'KAYNAK ŞARKI · değiştirilecek bölümü slider ile seç', 'lego' => 'MEVCUT TRACK\'LER (yeni track eklenecek)', 'extend' => 'KAYNAK ŞARKI · uzatma başlangıç noktasını slider ile belirle', 'complete' => 'YARIM ŞARKI (tamamlanacak)', 'extract' => 'ANALİZ EDİLECEK SES', _ => 'KAYNAK SES', }; final yol = _kaynakYol(); final yolDoluVeVar = yol != null && File(yol).existsSync(); return _Bolum( etiket: etiket, icon: Icons.audiotrack, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _DosyaSecici( yol: yol, tip: 'audio', onSec: (y) => state.setState(() => _kaynakAyarla(y)), onTemizle: () => state.setState(() => _kaynakAyarla(null)), ), // Range slider'lı oynatıcı (sadece repaint + extend gibi aralık gerektirenlerde) if (yolDoluVeVar && _aralikSecimiVar) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), border: Border.all(color: Theme.of(context).dividerColor), ), child: SesAralikOynatici( key: ValueKey('kaynak-aralik-$yol'), dosyaYolu: yol, basla: state._repaintingStart, bit: state._repaintingEnd ?? 30.0, onAralikDegisti: (b, e) { state.setState(() { state._repaintingStart = b; state._repaintingEnd = e; }); }, ), ), ], ], ), ); } } class _GorevTipiSecici extends StatelessWidget { final _AceStepSayfasiState state; const _GorevTipiSecici({required this.state}); String _aciklama(String tip) => switch (tip) { 'text2music' => 'Sadece prompt+sözlerden müzik üret', 'cover' => 'Mevcut şarkıyı farklı stilde yeniden seslendir', 'repaint' => 'Mevcut şarkının bir bölümünü değiştir (start/end aralık)', 'lego' => 'Mevcut track\'lere yeni track ekle (sadece base model)', 'extract' => 'Sadece audio code/analiz çıkar (üretim yok)', 'complete' => 'Yarım şarkıyı tamamla', 'audio2audio' => 'Mevcut sesi başka bir versiyona dönüştür', 'extend' => 'Mevcut şarkıyı uzat (devamını yaz)', _ => '', }; bool _kaynakSesGerek(String tip) => tip != 'text2music'; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('GÖREV TİPİ', style: theme.textTheme.labelSmall), const SizedBox(height: 4), Wrap( spacing: 6, runSpacing: 6, children: [ _gorevChipi(context, 'text2music', 'Text → Müzik', Icons.text_format), _gorevChipi(context, 'cover', 'Cover', Icons.album), _gorevChipi(context, 'repaint', 'Repaint', Icons.edit), _gorevChipi(context, 'lego', 'Lego (track ekle)', Icons.extension), _gorevChipi(context, 'extend', 'Uzat', Icons.add_box), _gorevChipi(context, 'complete', 'Tamamla', Icons.check_box), _gorevChipi(context, 'audio2audio', 'Audio → Audio', Icons.swap_horiz), _gorevChipi(context, 'extract', 'Sadece Analiz', Icons.analytics), ], ), const SizedBox(height: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ Icon(Icons.info_outline, size: 14, color: theme.colorScheme.primary), const SizedBox(width: 6), Expanded( child: Text( _aciklama(state._taskType) + (_kaynakSesGerek(state._taskType) ? ' · Kaynak ses gerekir' : ''), style: theme.textTheme.labelSmall, ), ), ], ), ), ], ); } Widget _gorevChipi( BuildContext ctx, String deger, String etiket, IconData icon) { final secili = state._taskType == deger; final theme = Theme.of(ctx); return InkWell( onTap: () => state.setState(() => state._taskType = deger), borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: secili ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), border: Border.all( color: secili ? theme.colorScheme.primary : theme.dividerColor, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: secili ? Colors.black : null), const SizedBox(width: 6), Text( etiket, style: TextStyle( fontSize: 11, color: secili ? Colors.black : null, fontWeight: secili ? FontWeight.bold : null, ), ), ], ), ), ); } } /// Sihirbaz + şablon bar — formun en üstünde gözükür. /// - "Sihirbaz" butonu: AceStepSihirbazDialog açar, sonucu form'a uygular /// - "Şablonlar" dropdown: kaydedilmiş şablonları listeler, seçince yükler /// - "Şablon olarak kaydet" butonu: mevcut form ayarlarını şablon yapar class _SihirbazVeSablonBar extends ConsumerWidget { final _AceStepSayfasiState state; const _SihirbazVeSablonBar({required this.state}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final sablonlar = ref.watch(aceStepSablonProvider); return Card( color: theme.colorScheme.primaryContainer.withValues(alpha: 0.25), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ // Sihirbaz FilledButton.icon( icon: const Icon(Icons.auto_fix_high, size: 16), label: const Text('Sihirbaz'), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), onPressed: () => _sihirbaziAc(context, ref), ), const SizedBox(width: 8), // Şablonlar dropdown PopupMenuButton( tooltip: 'Şablon yükle', offset: const Offset(0, 36), onSelected: (id) => _sablonYukle(context, ref, id), itemBuilder: (ctx) { if (sablonlar.isEmpty) { return [ const PopupMenuItem( enabled: false, child: Text('Henüz şablon yok', style: TextStyle(fontSize: 12)), ), ]; } return sablonlar.map((s) { return PopupMenuItem( value: s.id, child: Row( children: [ Icon( s.yerlesik ? Icons.bookmark : Icons.bookmark_border, size: 14, color: s.yerlesik ? theme.colorScheme.primary : null, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(s.isim, style: const TextStyle(fontSize: 13)), if (s.aciklama != null) Text(s.aciklama!, style: TextStyle( fontSize: 10, color: theme.colorScheme.outline, )), ], ), ), if (!s.yerlesik) IconButton( icon: const Icon(Icons.delete_outline, size: 14), padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 24, minHeight: 24), onPressed: () { Navigator.of(ctx).pop(); _sablonSil(context, ref, s.id, s.isim); }, ), ], ), ); }).toList(); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(6), border: Border.all(color: theme.dividerColor), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.bookmarks_outlined, size: 16), const SizedBox(width: 6), Text( 'Şablonlar (${sablonlar.length})', style: const TextStyle(fontSize: 12), ), const Icon(Icons.arrow_drop_down, size: 18), ], ), ), ), const SizedBox(width: 8), // Şablon olarak kaydet OutlinedButton.icon( icon: const Icon(Icons.bookmark_add_outlined, size: 14), label: const Text('Şablona kaydet', style: TextStyle(fontSize: 12)), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), ), onPressed: () => _mevcutFormuSablonYap(context, ref), ), const Spacer(), // Bilgi Text( '💾 Son ayarlar otomatik kaydedilir', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.outline, fontStyle: FontStyle.italic, ), ), ], ), ), ); } Future _sihirbaziAc(BuildContext context, WidgetRef ref) async { final sonuc = await showDialog( context: context, useRootNavigator: true, builder: (_) => const AceStepSihirbazDialog(), ); if (sonuc != null) { _sihirbazSonucuFormaUygula(sonuc); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('✨ Sihirbaz ayarları forma uygulandı')), ); } } } void _sihirbazSonucuFormaUygula(SihirbazSonuc sonuc) { state.setState(() { state._promptCtrl.text = sonuc.prompt; state._lmNegativePromptCtrl.text = sonuc.negatifPrompt; state._bpmCtrl.text = sonuc.bpm.toString(); state._dil = sonuc.dil; state._cfg = sonuc.cfg; state._lmTemperature = sonuc.lmTemperature; // Lyrics taslağı sadece boşsa doldur (kullanıcıyı yazdığını silme) if (state._lyricsCtrl.text.trim().isEmpty && sonuc.onerilenLyricsTaslak.isNotEmpty) { state._lyricsCtrl.text = sonuc.onerilenLyricsTaslak; } }); // Anında kaydet — kullanıcı hemen kapatırsa ayarlar kaybolmasın final aktifProje = state.ref.read(projeProvider); AceStepSonAyar.kaydetHemen( ayarlar: state._tumFormuTopla(), projeKlasoru: aktifProje?.klasorYolu, ); } Future _sablonYukle( BuildContext context, WidgetRef ref, String id) async { final sablon = ref.read(aceStepSablonProvider.notifier).bul(id); if (sablon == null) return; final ay = sablon.ayarlar; state.setState(() { if (ay['prompt'] is String) state._promptCtrl.text = ay['prompt']; if (ay['negatifPrompt'] is String) { state._lmNegativePromptCtrl.text = ay['negatifPrompt']; } if (ay['bpm'] is num) { state._bpmCtrl.text = (ay['bpm'] as num).toInt().toString(); } if (ay['dil'] is String) state._dil = ay['dil']; if (ay['cfg'] is num) state._cfg = (ay['cfg'] as num).toDouble(); if (ay['lmTemperature'] is num) { state._lmTemperature = (ay['lmTemperature'] as num).toDouble(); } if (ay['onerilenLyricsTaslak'] is String && state._lyricsCtrl.text.trim().isEmpty) { state._lyricsCtrl.text = ay['onerilenLyricsTaslak']; } }); // Anında kaydet final aktifProje = ref.read(projeProvider); await AceStepSonAyar.kaydetHemen( ayarlar: state._tumFormuTopla(), projeKlasoru: aktifProje?.klasorYolu, ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('"${sablon.isim}" şablonu yüklendi')), ); } } Future _sablonSil( BuildContext context, WidgetRef ref, String id, String isim) async { final onay = await showDialog( context: context, useRootNavigator: true, builder: (ctx) => AlertDialog( title: const Text('Şablonu sil?'), content: Text('"$isim" şablonu kalıcı olarak silinecek.'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('İptal')), FilledButton( style: FilledButton.styleFrom(backgroundColor: Colors.red), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Sil'), ), ], ), ); if (onay == true) { await ref.read(aceStepSablonProvider.notifier).sil(id); } } Future _mevcutFormuSablonYap( BuildContext context, WidgetRef ref) async { final isimCtrl = TextEditingController(); final aciklamaCtrl = TextEditingController(); final onay = await showDialog( context: context, useRootNavigator: true, builder: (ctx) => AlertDialog( title: const Text('Mevcut formu şablon olarak kaydet'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: isimCtrl, autofocus: true, decoration: const InputDecoration( labelText: 'Şablon adı', isDense: true, ), ), const SizedBox(height: 8), TextField( controller: aciklamaCtrl, maxLines: 2, decoration: const InputDecoration( labelText: 'Açıklama (opsiyonel)', isDense: true, ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('İptal')), FilledButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Kaydet')), ], ), ); if (onay == true && isimCtrl.text.trim().isNotEmpty) { final ayarlar = state._tumFormuTopla(); await ref.read(aceStepSablonProvider.notifier).kaydet( isim: isimCtrl.text.trim(), aciklama: aciklamaCtrl.text.trim().isEmpty ? null : aciklamaCtrl.text.trim(), ayarlar: ayarlar, ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('"${isimCtrl.text.trim()}" şablonu kaydedildi')), ); } } } } class _LmAyarlari extends StatelessWidget { final _AceStepSayfasiState state; const _LmAyarlari({required this.state}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Track adı _Bolum( etiket: 'TRACK ADI (opsiyonel)', icon: Icons.label_outline, child: TextField( controller: state._trackNameCtrl, style: const TextStyle(fontSize: 12), decoration: const InputDecoration( isDense: true, hintText: 'Ör: Çay Şarkısı', hintStyle: TextStyle(fontSize: 11), contentPadding: EdgeInsets.all(8), border: OutlineInputBorder(), ), ), ), const SizedBox(height: 12), // LM Negative Prompt — kritik! _Bolum( etiket: 'LM NEGATIVE PROMPT', icon: Icons.block, child: TextField( controller: state._lmNegativePromptCtrl, maxLines: 2, style: const TextStyle(fontSize: 12), decoration: const InputDecoration( isDense: true, hintText: 'Ör: profanity, depressing tone, generic phrases', hintStyle: TextStyle(fontSize: 11), contentPadding: EdgeInsets.all(8), border: OutlineInputBorder(), ), ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _Bolum( etiket: 'TEMPERATURE: ${state._lmTemperature.toStringAsFixed(2)}', icon: Icons.thermostat, child: Slider( value: state._lmTemperature, min: 0.0, max: 2.0, divisions: 40, label: state._lmTemperature.toStringAsFixed(2), onChanged: (v) => state.setState(() => state._lmTemperature = v), ), ), ), const SizedBox(width: 8), Expanded( child: _Bolum( etiket: 'LM CFG: ${state._lmCfgScale.toStringAsFixed(1)}', icon: Icons.tune, child: Slider( value: state._lmCfgScale, min: 1.0, max: 10.0, divisions: 90, label: state._lmCfgScale.toStringAsFixed(1), onChanged: (v) => state.setState(() => state._lmCfgScale = v), ), ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _Bolum( etiket: 'TOP P: ${state._lmTopP.toStringAsFixed(2)}', icon: Icons.scatter_plot, child: Slider( value: state._lmTopP, min: 0.0, max: 1.0, divisions: 100, label: state._lmTopP.toStringAsFixed(2), onChanged: (v) => state.setState(() => state._lmTopP = v), ), ), ), const SizedBox(width: 8), Expanded( child: _Bolum( etiket: 'REPETITION: ${state._lmRepetitionPenalty.toStringAsFixed(2)}', icon: Icons.repeat, child: Slider( value: state._lmRepetitionPenalty, min: 0.5, max: 2.0, divisions: 30, label: state._lmRepetitionPenalty.toStringAsFixed(2), onChanged: (v) => state.setState(() => state._lmRepetitionPenalty = v), ), ), ), ], ), const Divider(height: 24), Text('Decoder & Caption', style: theme.textTheme.labelMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, )), const SizedBox(height: 4), SwitchListTile( dense: true, contentPadding: EdgeInsets.zero, title: const Text('Tiled VAE Decode', style: TextStyle(fontSize: 12)), subtitle: const Text('VRAM tasarrufu — büyük modellerde önerilir', style: TextStyle(fontSize: 10)), value: state._useTiledDecode, onChanged: (v) => state.setState(() => state._useTiledDecode = v), ), SwitchListTile( dense: true, contentPadding: EdgeInsets.zero, title: const Text('Constrained Decoding', style: TextStyle(fontSize: 12)), subtitle: const Text( 'Codec çıkışını sınırlandır (kalite için önerilir)', style: TextStyle(fontSize: 10)), value: state._constrainedDecoding, onChanged: (v) => state.setState(() => state._constrainedDecoding = v), ), SwitchListTile( dense: true, contentPadding: EdgeInsets.zero, title: const Text('Chain-of-Thought Caption', style: TextStyle(fontSize: 12)), subtitle: const Text( 'LLM caption üretirken adım adım düşünür (kalite ↑)', style: TextStyle(fontSize: 10)), value: state._useCotCaption, onChanged: (v) => state.setState(() => state._useCotCaption = v), ), SwitchListTile( dense: true, contentPadding: EdgeInsets.zero, title: const Text('Format Caption', style: TextStyle(fontSize: 12)), subtitle: const Text('Caption formatlamayı zorla', style: TextStyle(fontSize: 10)), value: state._isFormatCaption, onChanged: (v) => state.setState(() => state._isFormatCaption = v), ), SwitchListTile( dense: true, contentPadding: EdgeInsets.zero, title: const Text('LM Batch İşlem', style: TextStyle(fontSize: 12)), subtitle: const Text( 'LLM\'i batch modunda çalıştır (hız ↑, VRAM ↑)', style: TextStyle(fontSize: 10)), value: state._allowLmBatch, onChanged: (v) => state.setState(() => state._allowLmBatch = v), ), ], ); } } class _GelismisAyarlar extends StatelessWidget { final _AceStepSayfasiState state; const _GelismisAyarlar({required this.state}); @override Widget build(BuildContext context) { return Column( children: [ // Diffusion method Row( children: [ Expanded( child: _Bolum( etiket: 'INFER METHOD', icon: Icons.functions, child: SegmentedButton( segments: const [ ButtonSegment(value: 'ode', label: Text('ODE')), ButtonSegment(value: 'sde', label: Text('SDE')), ], selected: {state._inferMethod}, onSelectionChanged: (s) => state.setState(() => state._inferMethod = s.first), ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'SHIFT (timestep)', icon: Icons.timeline, child: Column( children: [ Slider( value: state._shift, min: 1.0, max: 5.0, divisions: 40, label: state._shift.toStringAsFixed(2), onChanged: (v) => state.setState(() => state._shift = v), ), Text(state._shift.toStringAsFixed(2), style: const TextStyle( fontSize: 10, fontFamily: 'monospace')), ], ), ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _Bolum( etiket: 'CFG INTERVAL START', icon: Icons.start, child: Slider( value: state._cfgIntervalStart, min: 0, max: 1, divisions: 20, label: state._cfgIntervalStart.toStringAsFixed(2), onChanged: (v) => state.setState( () => state._cfgIntervalStart = v), ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'CFG INTERVAL END', icon: Icons.stop, child: Slider( value: state._cfgIntervalEnd, min: 0, max: 1, divisions: 20, label: state._cfgIntervalEnd.toStringAsFixed(2), onChanged: (v) => state.setState(() => state._cfgIntervalEnd = v), ), ), ), ], ), CheckboxListTile( contentPadding: EdgeInsets.zero, dense: true, title: const Text('use_adg (Adaptive Diffusion Guidance)', style: TextStyle(fontSize: 12)), subtitle: const Text( 'Açıkken CFG dinamik olarak ayarlanır', style: TextStyle(fontSize: 10), ), value: state._useAdg, onChanged: (v) => state.setState(() => state._useAdg = v ?? false), ), const SizedBox(height: 8), _Bolum( etiket: 'CUSTOM TIMESTEPS (virgülle ayır)', icon: Icons.list_alt, child: TextField( controller: state._customTimestepsCtrl, decoration: const InputDecoration( hintText: '0.97,0.76,0.615,0.5,0.395,0.28,0.18,0.085,0', ), ), ), const SizedBox(height: 12), // Repaint parametreleri (kaynak + range slider üst alanda gösterilir) if (state._taskType == 'repaint') ...[ const Divider(), Row( children: [ Expanded( child: _Bolum( etiket: 'REPAINT MODE', icon: Icons.draw, child: DropdownButtonFormField( initialValue: state._repaintMode, items: const [ DropdownMenuItem( value: 'conservative', child: Text('Conservative')), DropdownMenuItem( value: 'balanced', child: Text('Balanced')), DropdownMenuItem( value: 'aggressive', child: Text('Aggressive')), ], onChanged: (v) => v != null ? state.setState(() => state._repaintMode = v) : null, ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'REPAINT STRENGTH', icon: Icons.opacity, child: Slider( value: state._repaintStrength, min: 0, max: 1, divisions: 20, label: state._repaintStrength.toStringAsFixed(2), onChanged: (v) => state.setState( () => state._repaintStrength = v), ), ), ), ], ), const SizedBox(height: 4), Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ Icon(Icons.info_outline, size: 14, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 6), const Expanded( child: Text( 'Hangi bölümün değiştirileceğini üstteki kaynak ses oynatıcısında range slider ile seç (yeşil tutamaklar).', style: TextStyle(fontSize: 11), ), ), ], ), ), ], // Audio cover (audio2audio için) if (state._taskType == 'audio2audio') ...[ const Divider(), Row( children: [ Expanded( child: _Bolum( etiket: 'AUDIO COVER STRENGTH', icon: Icons.volume_up, child: Slider( value: state._audioCoverStrength, min: 0, max: 1, divisions: 20, label: state._audioCoverStrength.toStringAsFixed(2), onChanged: (v) => state.setState( () => state._audioCoverStrength = v), ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'COVER NOISE STRENGTH', icon: Icons.blur_on, child: Slider( value: state._coverNoiseStrength, min: 0, max: 1, divisions: 20, label: state._coverNoiseStrength.toStringAsFixed(2), onChanged: (v) => state.setState( () => state._coverNoiseStrength = v), ), ), ), ], ), ], const SizedBox(height: 8), _Bolum( etiket: 'INSTRUCTION (DiT system prompt, opsiyonel)', icon: Icons.assignment, child: TextField( controller: state._instructionCtrl, maxLines: 2, decoration: const InputDecoration( hintText: 'Boş = varsayılan instruction kullan', ), ), ), const SizedBox(height: 8), _Bolum( etiket: 'GLOBAL CAPTION (full song context)', icon: Icons.subject, child: TextField( controller: state._globalCaptionCtrl, maxLines: 2, decoration: const InputDecoration( hintText: 'Lego SFT görevleri için tüm şarkı bağlamı', ), ), ), ], ); } } class _CikisAyarlari extends StatelessWidget { final _AceStepSayfasiState state; const _CikisAyarlari({required this.state}); @override Widget build(BuildContext context) { return Column( children: [ // Audio format _Bolum( etiket: 'AUDIO FORMAT', icon: Icons.audio_file, child: SegmentedButton( segments: const [ ButtonSegment(value: 'wav', label: Text('WAV')), ButtonSegment(value: 'mp3', label: Text('MP3')), ButtonSegment(value: 'flac', label: Text('FLAC')), ], selected: {state._audioFormat}, onSelectionChanged: (s) => state.setState(() => state._audioFormat = s.first), ), ), if (state._audioFormat == 'mp3') ...[ const SizedBox(height: 8), Row( children: [ Expanded( child: _Bolum( etiket: 'MP3 BITRATE', icon: Icons.high_quality, child: DropdownButtonFormField( initialValue: state._mp3Bitrate, items: const [ DropdownMenuItem(value: 128, child: Text('128 kbps')), DropdownMenuItem(value: 192, child: Text('192 kbps')), DropdownMenuItem(value: 256, child: Text('256 kbps')), DropdownMenuItem(value: 320, child: Text('320 kbps')), ], onChanged: (v) => v != null ? state.setState(() => state._mp3Bitrate = v) : null, ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'SAMPLE RATE', icon: Icons.graphic_eq, child: DropdownButtonFormField( initialValue: state._mp3SampleRate, items: const [ DropdownMenuItem(value: 22050, child: Text('22050 Hz')), DropdownMenuItem(value: 44100, child: Text('44100 Hz')), DropdownMenuItem(value: 48000, child: Text('48000 Hz')), ], onChanged: (v) => v != null ? state.setState(() => state._mp3SampleRate = v) : null, ), ), ), ], ), ], const SizedBox(height: 8), // Normalization SwitchListTile( contentPadding: EdgeInsets.zero, dense: true, title: const Text('Loudness Normalize', style: TextStyle(fontSize: 12)), subtitle: const Text( 'Çıkışı belirli LUFS\'a eşitle (YouTube/Spotify standartı)', style: TextStyle(fontSize: 10), ), value: state._enableNormalization, onChanged: (v) => state.setState(() => state._enableNormalization = v), ), if (state._enableNormalization) _Bolum( etiket: 'NORMALIZATION TARGET (LUFS)', icon: Icons.equalizer, child: Row( children: [ Expanded( child: Slider( value: state._normalizationDb, min: -30, max: 0, divisions: 60, label: '${state._normalizationDb.toStringAsFixed(1)} dB', onChanged: (v) => state.setState( () => state._normalizationDb = v), ), ), SizedBox( width: 60, child: Text( '${state._normalizationDb.toStringAsFixed(1)} dB', style: const TextStyle( fontSize: 11, fontFamily: 'monospace'), ), ), ], ), ), const SizedBox(height: 8), // Fade Row( children: [ Expanded( child: _Bolum( etiket: 'FADE IN (s)', icon: Icons.gradient, child: Slider( value: state._fadeInDuration, min: 0, max: 5, divisions: 50, label: state._fadeInDuration.toStringAsFixed(1), onChanged: (v) => state.setState(() => state._fadeInDuration = v), ), ), ), const SizedBox(width: 12), Expanded( child: _Bolum( etiket: 'FADE OUT (s)', icon: Icons.gradient_outlined, child: Slider( value: state._fadeOutDuration, min: 0, max: 10, divisions: 100, label: state._fadeOutDuration.toStringAsFixed(1), onChanged: (v) => state.setState(() => state._fadeOutDuration = v), ), ), ), ], ), const SizedBox(height: 8), // Score scale _Bolum( etiket: 'SCORE SCALE (kalite ölçeği)', icon: Icons.straighten, child: Slider( value: state._scoreScale, min: 0.5, max: 2.0, divisions: 30, label: state._scoreScale.toStringAsFixed(2), onChanged: (v) => state.setState(() => state._scoreScale = v), ), ), const SizedBox(height: 8), // Sampler mode _Bolum( etiket: 'SAMPLER MODE', icon: Icons.tune, child: DropdownButtonFormField( initialValue: state._samplerMode, items: const [ DropdownMenuItem(value: 'euler', child: Text('Euler')), DropdownMenuItem( value: 'euler_a', child: Text('Euler Ancestral')), DropdownMenuItem(value: 'heun', child: Text('Heun')), DropdownMenuItem(value: 'dpm++', child: Text('DPM++')), DropdownMenuItem(value: 'dpm++_2m', child: Text('DPM++ 2M')), ], onChanged: (v) => v != null ? state.setState(() => state._samplerMode = v) : null, ), ), const SizedBox(height: 8), // LoRA _Bolum( etiket: 'LoRA PATH (opsiyonel)', icon: Icons.extension, child: Row( children: [ Expanded( child: Text( state._loraPath != null ? p.basename(state._loraPath!) : '— LoRA seçilmedi —', overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, color: state._loraPath == null ? Theme.of(context).colorScheme.outline : null, ), ), ), if (state._loraPath != null) IconButton( icon: const Icon(Icons.clear, size: 14), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 24, minHeight: 24), onPressed: () => state.setState(() => state._loraPath = null), ), TextButton.icon( icon: const Icon(Icons.folder_open, size: 14), label: Text( state._loraPath != null ? 'Değiştir' : 'Seç', style: const TextStyle(fontSize: 11)), onPressed: () async { final r = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['safetensors', 'pt', 'ckpt'], ); if (r != null && r.files.single.path != null) { state.setState( () => state._loraPath = r.files.single.path); } }, ), ], ), ), ], ); } } class _DosyaSecici extends StatelessWidget { final String? yol; final String tip; final ValueChanged onSec; final VoidCallback onTemizle; const _DosyaSecici({ required this.yol, required this.tip, required this.onSec, required this.onTemizle, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), border: Border.all(color: theme.dividerColor), ), child: Row( children: [ Icon( yol != null ? Icons.audio_file : Icons.upload_file, size: 18, color: yol != null ? theme.colorScheme.primary : theme.colorScheme.outline, ), const SizedBox(width: 8), Expanded( child: Text( yol != null ? p.basename(yol!) : '— dosya seçilmedi —', overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, color: yol != null ? null : theme.colorScheme.outline, ), ), ), if (yol != null) IconButton( icon: const Icon(Icons.clear, size: 16), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 24, minHeight: 24), onPressed: onTemizle, ), TextButton.icon( icon: const Icon(Icons.folder_open, size: 14), label: Text(yol != null ? 'Değiştir' : 'Seç', style: const TextStyle(fontSize: 11)), onPressed: () async { final r = await FilePicker.platform.pickFiles( type: FileType.audio, ); if (r != null && r.files.single.path != null) { onSec(r.files.single.path!); } }, ), ], ), ); } } class _Bolum extends StatelessWidget { final String etiket; final IconData icon; final Widget? trailing; final Widget child; const _Bolum({ required this.etiket, required this.icon, this.trailing, required this.child, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon(icon, size: 14, color: theme.colorScheme.outline), const SizedBox(width: 6), Text(etiket, style: theme.textTheme.labelSmall), const Spacer(), if (trailing != null) trailing!, ], ), const SizedBox(height: 4), child, ], ); } } /// "Sadece aktif projenin görevleri" filtresi — proje bağlamında varsayılan açık final aceStepSadeceProjeFiltresiProvider = StateProvider((ref) => true); /// Bir görevin form değerlerini formaya yüklemek için sinyal sağlayıcı. /// Görev kartından "Forma yükle" / "Kopya olarak edit" basıldığında set edilir. /// Sayfa state'i bunu dinler, alanları doldurur ve geri null'a çeker. final aceStepFormaYukleProvider = StateProvider((ref) => null); class _GorevListesi extends ConsumerWidget { const _GorevListesi(); @override Widget build(BuildContext context, WidgetRef ref) { final tumGorevler = ref.watch(aceStepUretimProvider); final sadeceProje = ref.watch(aceStepSadeceProjeFiltresiProvider); final aktifProje = ref.watch(projeProvider); final gorevler = (sadeceProje && aktifProje != null) ? tumGorevler.where((g) => g.projeId == aktifProje.id).toList() : tumGorevler; final theme = Theme.of(context); return Column( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: theme.colorScheme.surface, border: Border(bottom: BorderSide(color: theme.dividerColor)), ), child: Row( children: [ const Icon(Icons.queue_music, size: 18), const SizedBox(width: 8), Text('ÜRETİM GEÇMİŞİ', style: theme.textTheme.labelMedium), const SizedBox(width: 8), if (aktifProje != null) FilterChip( avatar: Icon( sadeceProje ? Icons.filter_alt : Icons.filter_alt_off_outlined, size: 12, ), label: Text( 'Sadece bu proje', style: const TextStyle(fontSize: 10), ), selected: sadeceProje, visualDensity: VisualDensity.compact, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, onSelected: (v) => ref .read(aceStepSadeceProjeFiltresiProvider.notifier) .state = v, ), const Spacer(), if (gorevler.isNotEmpty) IconButton( icon: const Icon(Icons.delete_sweep, size: 18), tooltip: 'Geçmişi temizle', onPressed: () async { final onay = await showDialog( context: context, builder: (c) => AlertDialog( title: const Text('Geçmişi temizle?'), content: const Text('Tüm üretim kayıtları silinecek (ses dosyaları diskte kalır).'), actions: [ TextButton( onPressed: () => Navigator.pop(c, false), child: const Text('İptal')), FilledButton( onPressed: () => Navigator.pop(c, true), child: const Text('Sil')), ], ), ); if (onay == true) { await ref .read(aceStepUretimProvider.notifier) .tumGecmisiTemizle(); } }, ), ], ), ), Expanded( child: gorevler.isEmpty ? Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.music_off, size: 48, color: theme.colorScheme.outline), const SizedBox(height: 8), Text('Henüz üretim yok', style: theme.textTheme.bodyMedium), ], ), ), ) : ListView.builder( itemCount: gorevler.length, itemBuilder: (c, i) => _GorevKarti(gorev: gorevler[i]) .animate() .fadeIn(duration: 200.ms), ), ), ], ); } } class _GorevKarti extends ConsumerStatefulWidget { final AceStepGorev gorev; const _GorevKarti({required this.gorev}); @override ConsumerState<_GorevKarti> createState() => _GorevKartiState(); } class _GorevKartiState extends ConsumerState<_GorevKarti> { bool _genis = false; Color _renk(BuildContext c) { final t = Theme.of(c); return switch (widget.gorev.durum) { 1 => Colors.green, 2 => Colors.red, _ => t.colorScheme.primary, }; } IconData _ikon() => switch (widget.gorev.durum) { 1 => Icons.check_circle, 2 => Icons.error_outline, _ => Icons.sync, }; /// Üretim türü rozeti — orijinal/tekrar/varyant/kopya ({String emoji, String etiket, Color renk}) _turBilgi() { final t = widget.gorev.uretimTuru; switch (t) { case 'tekrar': return (emoji: '🔄', etiket: 'Tekrar', renk: Colors.blueGrey); case 'varyant': return (emoji: '🎲', etiket: 'Varyant', renk: Colors.deepPurple); case 'kopya': return (emoji: '📋', etiket: 'Kopya', renk: Colors.teal); default: return (emoji: '🎵', etiket: 'Orijinal', renk: Colors.indigo); } } Future _yenidenUret({required bool yeniSeed}) async { final durum = ref.read(aceStepProvider); if (!durum.apiCalisiyor) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Önce API server\'ı başlat'), backgroundColor: Colors.red), ); return; } final api = AceStepApiIstemci(); final yeni = await ref.read(aceStepUretimProvider.notifier).yenidenUret( api, widget.gorev, yeniSeed: yeniSeed, tur: yeniSeed ? 'varyant' : 'tekrar', projeId: ref.read(projeProvider)?.id, projeKlasoru: ref.read(projeProvider)?.klasorYolu, ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(yeni != null ? (yeniSeed ? 'Varyant kuyruğa eklendi' : 'Yeniden üretim kuyruğa eklendi') : 'Hata: yeniden üretim başlatılamadı'), backgroundColor: yeni != null ? null : Colors.red, ), ); } } void _formaYukle({bool kopyaModu = false}) { // Kopya görevini işaretlemek için klon oluştur (kaynakGorevId set ederek) final kaynak = widget.gorev; final hedef = kopyaModu ? AceStepGorev( id: kaynak.id, taskId: kaynak.taskId, prompt: kaynak.prompt, lyrics: kaynak.lyrics, sure: kaynak.sure, steps: kaynak.steps, cfg: kaynak.cfg, bpm: kaynak.bpm, keyScale: kaynak.keyScale, dil: kaynak.dil, dilOtomatik: kaynak.dilOtomatik, model: kaynak.model, thinking: kaynak.thinking, sampleMode: kaynak.sampleMode, baslama: kaynak.baslama, projeId: kaynak.projeId, projeKlasoru: kaynak.projeKlasoru, globalCaption: kaynak.globalCaption, kaynakGorevId: kaynak.id, uretimTuru: 'kopya', formSnapshot: kaynak.formSnapshot, seed: kaynak.seed, bitenSeed: kaynak.bitenSeed, useRandomSeed: kaynak.useRandomSeed, ) : kaynak; ref.read(aceStepFormaYukleProvider.notifier).state = hedef; } Future _projeyeAktar() async { final yol = widget.gorev.sesYerelYol; if (yol == null) return; final aktif = ref.read(projeProvider); if (aktif == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Önce bir proje aç')), ); return; } final hedef = await KaynakKopyalayici.tekDosyaEkle( aktif.klasorYolu, yol, 'acestep', ); var yeni = aktif.copyWith(kaynak: aktif.kaynak.copyWith(sesPath: hedef)); if (yeni.kaynak.girisGorseliPath.isNotEmpty && yeni.segmentler.isEmpty) { yeni = await SegmentOlusturucu.otomatikSegmentleriOlustur(yeni); } ref.read(projeProvider.notifier).ayarla(yeni); await ProjeDeposu.kaydet(yeni); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Aktif projeye eklendi: ${aktif.isim}')), ); } } Future _disaAktar() async { final yol = widget.gorev.sesYerelYol; if (yol == null) return; final r = await FilePicker.platform.saveFile( dialogTitle: 'Ses dosyasını kaydet', fileName: 'acestep_${widget.gorev.id.substring(widget.gorev.id.length - 8)}${p.extension(yol)}', ); if (r != null) { await File(yol).copy(r); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Kaydedildi: $r')), ); } } } @override Widget build(BuildContext context) { final g = widget.gorev; final theme = Theme.of(context); final renk = _renk(context); return Card( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Column( children: [ ListTile( leading: g.durum == 0 ? SizedBox( width: 32, height: 32, child: CircularProgressIndicator( strokeWidth: 2, value: g.ilerleme / 100, color: renk, ), ) : Icon(_ikon(), color: renk, size: 28), title: Row( children: [ // Tür rozeti Builder(builder: (_) { final t = _turBilgi(); return Tooltip( message: t.etiket, child: Container( margin: const EdgeInsets.only(right: 6), padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 1), decoration: BoxDecoration( color: t.renk.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), border: Border.all( color: t.renk.withValues(alpha: 0.5), width: 0.6), ), child: Text( t.emoji, style: const TextStyle(fontSize: 11), ), ), ); }), Expanded( child: Text( g.prompt.isEmpty ? '(sample mode)' : g.prompt, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12), ), ), ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${g.steps} steps · CFG ${g.cfg.toStringAsFixed(1)} · ${g.dil}' '${g.sure != null ? " · ${g.sure!.toInt()}s" : ""}' '${g.bpm != null ? " · ${g.bpm} BPM" : ""}' '${(g.bitenSeed ?? g.seed) != null && (g.bitenSeed ?? g.seed)! >= 0 ? " · 🌱 ${g.bitenSeed ?? g.seed}" : ""}', style: const TextStyle(fontSize: 10), ), if (g.durum == 0 && g.ilerlemeMetni != null) Text(g.ilerlemeMetni!, style: const TextStyle(fontSize: 10, color: Colors.grey)), if (g.durum == 2 && g.hata != null) Text('Hata: ${g.hata}', style: const TextStyle(fontSize: 10, color: Colors.red)), ], ), trailing: IconButton( icon: Icon(_genis ? Icons.expand_less : Icons.expand_more), onPressed: () => setState(() => _genis = !_genis), ), ), if (g.durum == 1 && g.sesYerelYol != null && File(g.sesYerelYol!).existsSync()) Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: MiniSesOynatici( key: ValueKey('ses-${g.id}'), dosyaYolu: g.sesYerelYol!, ), ), if (_genis) ...[ const Divider(height: 1), Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (g.lyrics.isNotEmpty) ...[ Text('Sözler:', style: theme.textTheme.labelSmall), SelectableText( g.lyrics, style: const TextStyle(fontSize: 11), ), const SizedBox(height: 8), ], // Seed bilgisi — kopyalanabilir, "Forma Yükle"de tıpa tıp aynı sonuç için Builder(builder: (_) { final s = g.bitenSeed ?? g.seed; if (s == null || s < 0) { return const Padding( padding: EdgeInsets.only(bottom: 6), child: Text( '🌱 Seed: rastgele (sunucu seed döndürmedi)', style: TextStyle(fontSize: 11, color: Colors.grey), ), ); } final etiket = g.bitenSeed != null && g.bitenSeed! >= 0 ? 'gerçek' : 'kullanıcı'; return Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Text('🌱 ', style: TextStyle(fontSize: 12)), SelectableText( 'Seed: $s', style: const TextStyle( fontSize: 11, fontFamily: 'monospace', fontWeight: FontWeight.w600), ), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 1), decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(4), ), child: Text(etiket, style: const TextStyle( fontSize: 9, color: Colors.green)), ), const SizedBox(width: 4), IconButton( tooltip: 'Seed kopyala', icon: const Icon(Icons.copy, size: 13), padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 24, minHeight: 24), onPressed: () { Clipboard.setData( ClipboardData(text: s.toString())); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Seed kopyalandı'), duration: Duration(seconds: 1)), ); }, ), ], ), ); }), Wrap( spacing: 8, runSpacing: 4, children: [ if (g.sesYerelYol != null) ...[ OutlinedButton.icon( icon: const Icon(Icons.movie_creation, size: 14), label: const Text('Aktif projeye aktar', style: TextStyle(fontSize: 11)), onPressed: _projeyeAktar, ), OutlinedButton.icon( icon: const Icon(Icons.save_alt, size: 14), label: const Text('Dışa aktar', style: TextStyle(fontSize: 11)), onPressed: _disaAktar, ), OutlinedButton.icon( icon: const Icon(Icons.folder_open, size: 14), label: const Text('Klasörde göster', style: TextStyle(fontSize: 11)), onPressed: () => launchUrl( Uri.file(File(g.sesYerelYol!).parent.path)), ), ], // Forma yükle (sadece form alanlarını doldur) Tooltip( message: 'Form değerlerini bu üretimle doldur', child: OutlinedButton.icon( icon: const Icon(Icons.input, size: 14), label: const Text('Forma yükle', style: TextStyle(fontSize: 11)), onPressed: () => _formaYukle(kopyaModu: false), ), ), // Yeniden üret (aynı parametreler — random seed) Tooltip( message: 'Aynı parametrelerle yeniden üret', child: OutlinedButton.icon( icon: const Icon(Icons.refresh, size: 14), label: const Text('Yeniden üret', style: TextStyle(fontSize: 11)), onPressed: () => _yenidenUret(yeniSeed: false), ), ), // Varyant (rastgele seed) Tooltip( message: 'Rastgele seed ile varyant üret', child: OutlinedButton.icon( icon: const Icon(Icons.shuffle, size: 14), label: const Text('Varyant', style: TextStyle(fontSize: 11)), onPressed: () => _yenidenUret(yeniSeed: true), ), ), // Kopya olarak edit (formu doldur + kopya işareti) Tooltip( message: 'Bu üretimin kopyasını forma yükleyip edit et', child: OutlinedButton.icon( icon: const Icon(Icons.content_copy, size: 14), label: const Text('Kopya olarak edit', style: TextStyle(fontSize: 11)), onPressed: () => _formaYukle(kopyaModu: true), ), ), OutlinedButton.icon( icon: const Icon(Icons.delete_outline, size: 14, color: Colors.red), label: const Text('Geçmişten sil', style: TextStyle(fontSize: 11, color: Colors.red)), onPressed: () => ref .read(aceStepUretimProvider.notifier) .sil(g.id), ), ], ), ], ), ), ], ], ), ); } }