import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pasteboard/pasteboard.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import '../../durum/ace_lora_durumu.dart'; import '../../durum/karakter_durumu.dart'; import '../../durum/lora_durumu.dart'; import '../../servisler/ace/ace_lora_api_istemci.dart'; import 'lora_paneli.dart'; import 'yuz_seti_dialog.dart'; /// Karakter seçici/yönetici panel — Görsel formunun en üstünde. class KarakterPaneli extends ConsumerWidget { /// Aktif karakter ayarlandığında form'a yüz ref + tanım uygulamak için final void Function(Karakter karakter) onKarakterUygula; const KarakterPaneli({super.key, required this.onKarakterUygula}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final karakterler = ref.watch(karakterlerProvider); final aktifId = ref.watch(aktifKarakterIdProvider); return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.deepPurple.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(10), border: Border.all( color: Colors.deepPurple.withValues(alpha: 0.25), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.person_pin_circle, size: 16, color: Colors.deepPurple), const SizedBox(width: 6), Text('Karakter', style: theme.textTheme.labelMedium?.copyWith( color: Colors.deepPurple, fontWeight: FontWeight.w600, )), const SizedBox(width: 6), Tooltip( message: 'Bir karakter (yüz + tanım) kaydet, sonra istediğin ' 'kıyafet/saç/mekan ile farklı görsellerde kullan.', child: Icon(Icons.info_outline, size: 12, color: theme.colorScheme.outline), ), const Spacer(), if (karakterler.isNotEmpty) Text('${karakterler.length} karakter', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, fontSize: 10, )), TextButton.icon( icon: const Icon(Icons.add, size: 14), label: const Text('Yeni', style: TextStyle(fontSize: 11)), style: TextButton.styleFrom( visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 6), ), onPressed: () => _karakterDuzenleDialog(context, ref, null), ), ], ), const SizedBox(height: 6), if (karakterler.isEmpty) Padding( padding: const EdgeInsets.all(8), child: Text( 'Henüz karakter yok. "Yeni" ile bir karakter oluştur ' '(isim + yüz fotoğrafı + kısa tanım).', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, ), ), ) else SizedBox( height: 90, child: ListView.separated( scrollDirection: Axis.horizontal, separatorBuilder: (_, __) => const SizedBox(width: 6), itemCount: karakterler.length, itemBuilder: (_, i) { final k = karakterler[i]; final aktif = k.id == aktifId; return _KarakterKarti( karakter: k, aktif: aktif, onSec: () { if (aktif) { ref.read(aktifKarakterIdProvider.notifier).state = null; } else { ref.read(aktifKarakterIdProvider.notifier).state = k.id; onKarakterUygula(k); } }, onDuzenle: () => _karakterDuzenleDialog(context, ref, k), ); }, ), ), ], ), ); } Future _karakterDuzenleDialog( BuildContext context, WidgetRef ref, Karakter? mevcut, ) async { final isimCtrl = TextEditingController(text: mevcut?.isim ?? ''); final tanimCtrl = TextEditingController(text: mevcut?.tanim ?? ''); final notCtrl = TextEditingController(text: mevcut?.notlar ?? ''); String? yuzYolu = mevcut?.yuzRefYolu; String cinsiyet = mevcut?.cinsiyet ?? 'kişi'; // Ses alanları — RVC voice clone String? sesRefYolu = mevcut?.sesRefYolu; String? sesModelYolu = mevcut?.sesModelYolu; String? sesIndexYolu = mevcut?.sesIndexYolu; bool sesRefSilindi = false; bool sesModelIndexSilindi = false; final sonuc = await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setS) => AlertDialog( title: Row( children: [ const Icon(Icons.person_pin_circle, color: Colors.deepPurple), const SizedBox(width: 8), Text(mevcut == null ? 'Yeni Karakter' : 'Karakter Düzenle'), ], ), content: SizedBox( width: 460, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Yüz fotoğrafı Row( children: [ Container( width: 80, height: 80, decoration: BoxDecoration( border: Border.all( color: Theme.of(ctx).dividerColor), borderRadius: BorderRadius.circular(40), color: Theme.of(ctx) .colorScheme .surfaceContainerHighest, ), clipBehavior: Clip.antiAlias, child: yuzYolu != null && File(yuzYolu!).existsSync() ? Image.file(File(yuzYolu!), fit: BoxFit.cover) : Icon(Icons.person, size: 40, color: Theme.of(ctx).colorScheme.outline), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Yüz Fotoğrafı', style: Theme.of(ctx) .textTheme .labelMedium), const SizedBox(height: 4), Wrap( spacing: 6, children: [ OutlinedButton.icon( icon: const Icon(Icons.folder_open, size: 14), label: const Text('Dosyadan', style: TextStyle(fontSize: 11)), onPressed: () async { final r = await FilePicker.platform .pickFiles( type: FileType.custom, allowedExtensions: const [ 'png', 'jpg', 'jpeg', 'webp', ], ); final yol = r?.files.firstOrNull?.path; if (yol != null) { setS(() => yuzYolu = yol); } }, ), OutlinedButton.icon( icon: const Icon(Icons.content_paste, size: 14), label: const Text('Panodan', style: TextStyle(fontSize: 11)), onPressed: () async { final bytes = await Pasteboard.image; if (bytes == null) return; final docs = await getApplicationDocumentsDirectory(); final klasor = Directory(p.join( docs.path, 'FikoYT-VideoStudyo', 'panodan')); if (!await klasor.exists()) { await klasor.create( recursive: true); } final hedef = p.join(klasor.path, 'pano_${DateTime.now().millisecondsSinceEpoch}.png'); await File(hedef) .writeAsBytes(bytes); setS(() => yuzYolu = hedef); }, ), ], ), ], ), ), ], ), const SizedBox(height: 12), // Ses Referansı bölümü Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.teal.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(8), border: Border.all( color: Colors.teal.withValues(alpha: 0.35)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.mic_none, size: 16, color: Colors.teal), const SizedBox(width: 6), Text('Ses Referansı (10-30sn vokal)', style: Theme.of(ctx) .textTheme .labelMedium ?.copyWith( fontWeight: FontWeight.w600, color: Colors.teal.shade800, )), const Spacer(), if (sesRefYolu != null && !sesRefSilindi) const Icon(Icons.check_circle, size: 14, color: Colors.green), ], ), const SizedBox(height: 6), if (sesRefYolu != null && !sesRefSilindi) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Theme.of(ctx) .colorScheme .surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: Row( children: [ const Icon(Icons.audiotrack, size: 14, color: Colors.teal), const SizedBox(width: 6), Expanded( child: Text( p.basename(sesRefYolu!), style: const TextStyle(fontSize: 11), overflow: TextOverflow.ellipsis, ), ), ], ), ), const SizedBox(height: 6), Wrap( spacing: 6, children: [ OutlinedButton.icon( icon: const Icon(Icons.folder_open, size: 14), label: const Text('Dosyadan seç', style: TextStyle(fontSize: 11)), onPressed: () async { final r = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: const [ 'mp3', 'wav', 'flac', 'ogg', 'm4a', ], ); final yol = r?.files.firstOrNull?.path; if (yol != null) { setS(() { sesRefYolu = yol; sesRefSilindi = false; }); } }, ), if (sesRefYolu != null && !sesRefSilindi) OutlinedButton.icon( icon: const Icon(Icons.delete_outline, size: 14, color: Colors.red), label: const Text('Sil', style: TextStyle( fontSize: 11, color: Colors.red)), onPressed: () { setS(() { sesRefSilindi = true; sesRefYolu = null; }); }, ), ], ), ], ), ), const SizedBox(height: 10), // RVC Ses Modeli (opsiyonel — eğitilmiş) Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.indigo.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(8), border: Border.all( color: Colors.indigo.withValues(alpha: 0.35)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.smart_toy_outlined, size: 16, color: Colors.indigo), const SizedBox(width: 6), Text('RVC Ses Modeli (opsiyonel — eğitilmiş)', style: Theme.of(ctx) .textTheme .labelMedium ?.copyWith( fontWeight: FontWeight.w600, color: Colors.indigo.shade800, )), ], ), const SizedBox(height: 6), if (sesModelYolu != null && !sesModelIndexSilindi) Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ const Icon(Icons.layers, size: 12, color: Colors.indigo), const SizedBox(width: 4), Expanded( child: Text( '.pth: ${p.basename(sesModelYolu!)}', style: const TextStyle(fontSize: 10), overflow: TextOverflow.ellipsis, ), ), ], ), ), if (sesIndexYolu != null && !sesModelIndexSilindi) Padding( padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ const Icon(Icons.dataset_outlined, size: 12, color: Colors.indigo), const SizedBox(width: 4), Expanded( child: Text( '.index: ${p.basename(sesIndexYolu!)}', style: const TextStyle(fontSize: 10), overflow: TextOverflow.ellipsis, ), ), ], ), ), Wrap( spacing: 6, children: [ OutlinedButton.icon( icon: const Icon(Icons.upload_file, size: 14), label: const Text('.pth seç', style: TextStyle(fontSize: 11)), onPressed: () async { final r = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: const ['pth'], ); final yol = r?.files.firstOrNull?.path; if (yol != null) { setS(() { sesModelYolu = yol; sesModelIndexSilindi = false; }); } }, ), OutlinedButton.icon( icon: const Icon(Icons.upload_file, size: 14), label: const Text('.index seç', style: TextStyle(fontSize: 11)), onPressed: () async { final r = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: const ['index'], ); final yol = r?.files.firstOrNull?.path; if (yol != null) { setS(() { sesIndexYolu = yol; sesModelIndexSilindi = false; }); } }, ), if ((sesModelYolu != null || sesIndexYolu != null) && !sesModelIndexSilindi) OutlinedButton.icon( icon: const Icon(Icons.delete_outline, size: 14, color: Colors.red), label: const Text('İkisini de Sil', style: TextStyle( fontSize: 11, color: Colors.red)), onPressed: () { setS(() { sesModelIndexSilindi = true; sesModelYolu = null; sesIndexYolu = null; }); }, ), ], ), ], ), ), const SizedBox(height: 10), // ACE-Step Ses LoRA bölümü (şarkı/vokal eğitimi) Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.cyan.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(8), border: Border.all( color: Colors.cyan.withValues(alpha: 0.4)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.music_note, size: 16, color: Colors.cyan), const SizedBox(width: 6), Text('ACE-Step Ses LoRA (Şarkı/Vokal)', style: Theme.of(ctx) .textTheme .labelMedium ?.copyWith( fontWeight: FontWeight.w600, color: Colors.cyan.shade800, )), const Spacer(), OutlinedButton.icon( icon: const Icon(Icons.graphic_eq, size: 14), label: const Text('Yeni LoRA Eğit', style: TextStyle(fontSize: 11)), onPressed: mevcut == null ? null : () => AceLoraEgitDialog.goster( ctx, karakter: mevcut, ), ), ], ), const SizedBox(height: 6), Text( 'Bu karakterin sesinde şarkı/vokal üretmek için ' 'LoRA eğit. Eğitim ~30-90 dk sürer (port 8003).', style: Theme.of(ctx).textTheme.labelSmall?.copyWith( color: Theme.of(ctx).colorScheme.outline, fontSize: 10, ), ), if (mevcut != null) ...[ const SizedBox(height: 10), _KarakterAceLoraListesi(karakterId: mevcut.id), ], ], ), ), const SizedBox(height: 16), TextField( controller: isimCtrl, autofocus: mevcut == null, decoration: const InputDecoration( labelText: 'İsim', border: OutlineInputBorder(), isDense: true, hintText: 'Ayşe, Ahmet, Karakter 1...', ), ), const SizedBox(height: 12), TextField( controller: tanimCtrl, minLines: 2, maxLines: 4, decoration: const InputDecoration( labelText: 'Tanım (İngilizce öneriniyor)', helperText: 'Prompt\'a otomatik eklenir. Görünüm tasviri.', border: OutlineInputBorder(), isDense: true, hintText: 'young woman, brown eyes, slim, fair skin, mid-twenties', ), ), const SizedBox(height: 12), DropdownButtonFormField( value: cinsiyet, decoration: const InputDecoration( labelText: 'Cinsiyet', border: OutlineInputBorder(), isDense: true, ), items: const [ DropdownMenuItem(value: 'kadın', child: Text('Kadın')), DropdownMenuItem( value: 'erkek', child: Text('Erkek')), DropdownMenuItem(value: 'kişi', child: Text('Kişi')), ], onChanged: (v) => setS(() => cinsiyet = v ?? 'kişi'), ), const SizedBox(height: 12), TextField( controller: notCtrl, minLines: 1, maxLines: 3, decoration: const InputDecoration( labelText: 'Notlar (opsiyonel)', border: OutlineInputBorder(), isDense: true, ), ), const SizedBox(height: 16), // LoRA eğitimi Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.amber.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), border: Border.all( color: Colors.amber.withValues(alpha: 0.4)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.school_outlined, size: 16, color: Colors.amber), const SizedBox(width: 6), Text('LoRA Eğitimi', style: Theme.of(ctx) .textTheme .labelMedium ?.copyWith( fontWeight: FontWeight.w600, color: Colors.amber.shade800, )), const Spacer(), OutlinedButton.icon( icon: const Icon(Icons.add_photo_alternate, size: 14), label: const Text('Yüz Seti', style: TextStyle(fontSize: 11)), onPressed: mevcut == null ? null : () => _yuzSetiYonet(ctx, mevcut), ), const SizedBox(width: 4), OutlinedButton.icon( icon: const Icon(Icons.school, size: 14), label: const Text('LoRA Eğit', style: TextStyle(fontSize: 11)), onPressed: mevcut == null ? null : () => _loraEgitDialog(ctx, mevcut), ), ], ), const SizedBox(height: 6), Text( 'Yüz seti hazırla → "LoRA Eğit" ile karakter LoRA\'sı ' 'eğit (~30-60 dk). Birden fazla LoRA eğitebilirsin ' '(farklı versiyonlar). Çıktı: gorsel-uretim/loralar/', style: Theme.of(ctx).textTheme.labelSmall?.copyWith( color: Theme.of(ctx).colorScheme.outline, fontSize: 10, ), ), // Bu karaktere ait LoRA'lar if (mevcut != null) ...[ const SizedBox(height: 10), _KarakterLoraListesi(karakterId: mevcut.id), ], ], ), ), ], ), ), ), actions: [ if (mevcut != null) TextButton.icon( icon: const Icon(Icons.delete_outline, size: 16, color: Colors.red), label: const Text('Sil', style: TextStyle(color: Colors.red)), onPressed: () async { await ref .read(karakterlerProvider.notifier) .sil(mevcut.id); if (ctx.mounted) Navigator.pop(ctx, true); }, ), TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('İptal'), ), FilledButton( onPressed: () async { if (isimCtrl.text.trim().isEmpty) return; if (mevcut == null) { final yeni = await ref .read(karakterlerProvider.notifier) .ekle( isim: isimCtrl.text.trim(), tanim: tanimCtrl.text.trim(), yuzKaynagi: yuzYolu, cinsiyet: cinsiyet, notlar: notCtrl.text.trim().isEmpty ? null : notCtrl.text.trim(), ); // Ses alanları → guncelle ile (ekle ses kabul etmiyor) if (sesRefYolu != null || sesModelYolu != null || sesIndexYolu != null) { await ref.read(karakterlerProvider.notifier).guncelle( yeni, yeniSesRefKaynagi: sesRefYolu, yeniSesModelKaynagi: sesModelYolu, yeniSesIndexKaynagi: sesIndexYolu, ); } } else { mevcut ..isim = isimCtrl.text.trim() ..tanim = tanimCtrl.text.trim() ..cinsiyet = cinsiyet ..notlar = notCtrl.text.trim().isEmpty ? null : notCtrl.text.trim(); await ref.read(karakterlerProvider.notifier).guncelle( mevcut, yeniYuzKaynagi: yuzYolu != mevcut.yuzRefYolu ? yuzYolu : null, yeniSesRefKaynagi: (!sesRefSilindi && sesRefYolu != mevcut.sesRefYolu) ? sesRefYolu : null, sesRefSil: sesRefSilindi, yeniSesModelKaynagi: (!sesModelIndexSilindi && sesModelYolu != mevcut.sesModelYolu) ? sesModelYolu : null, yeniSesIndexKaynagi: (!sesModelIndexSilindi && sesIndexYolu != mevcut.sesIndexYolu) ? sesIndexYolu : null, sesModelIndexSil: sesModelIndexSilindi, ); } if (ctx.mounted) Navigator.pop(ctx, true); }, child: const Text('Kaydet'), ), ], ), ), ); } Future _loraEgitDialog(BuildContext ctx, Karakter k) async { final docs = await getApplicationDocumentsDirectory(); final veriKlasoru = p.join( docs.path, 'FikoYT-VideoStudyo', 'karakterler', '${k.id}_set'); if (!ctx.mounted) return; // Backend'den base model listesini al — basit hard-coded liste const modeller = [ (repo: 'SG161222/RealVisXL_V4.0', etiket: 'RealVisXL v4.0 (önerilir)'), (repo: 'SG161222/RealVisXL_V5.0', etiket: 'RealVisXL v5.0'), (repo: 'RunDiffusion/Juggernaut-XL-v9', etiket: 'Juggernaut XL v9'), ( repo: 'stabilityai/stable-diffusion-xl-base-1.0', etiket: 'SDXL Base 1.0' ), ]; await LoraEgitDialog.goster( ctx, karakter: k, veriKlasoruYolu: veriKlasoru, modelSecenekleri: modeller, ); } Future _yuzSetiYonet(BuildContext ctx, Karakter k) async { await yuzSetiDialogGoster( ctx, karakterId: k.id, karakterIsmi: k.isim, ); } } /// Karakter düzenleme dialog'u içinde gösterilen — bu karaktere ait LoRA'ların /// canlı listesi (eğitim ilerlemesi dahil). class _KarakterLoraListesi extends ConsumerWidget { final String karakterId; const _KarakterLoraListesi({required this.karakterId}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final tumLoralar = ref.watch(loraListesiProvider); final tumIsler = ref.watch(loraIsleriProvider); final benimLoralar = tumLoralar .where((l) => l.karakterId == karakterId) .toList(); final benimIsler = tumIsler.values .where((i) => i.karakterId == karakterId) .toList(); if (benimLoralar.isEmpty && benimIsler.isEmpty) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest .withValues(alpha: 0.4), borderRadius: BorderRadius.circular(4), ), child: Text( 'Bu karaktere ait henüz LoRA yok.', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, ), ), ); } return Container( decoration: BoxDecoration( border: Border.all(color: theme.dividerColor), borderRadius: BorderRadius.circular(6), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.amber.withValues(alpha: 0.1), borderRadius: const BorderRadius.vertical(top: Radius.circular(6)), ), child: Row( children: [ const Icon(Icons.school, size: 12, color: Colors.amber), const SizedBox(width: 4), Text( 'LoRA\'lar (${benimLoralar.length}' '${benimIsler.where((i) => i.durum == 0).isNotEmpty ? " + ${benimIsler.where((i) => i.durum == 0).length} eğitim" : ""})', style: theme.textTheme.labelSmall?.copyWith( fontWeight: FontWeight.w600, color: Colors.amber.shade800, ), ), ], ), ), // Aktif eğitimler for (final isi in benimIsler.where((i) => i.durum == 0)) Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), child: Row( children: [ const SizedBox( width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: 6), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isi.loraIsim, style: theme.textTheme.labelSmall ?.copyWith(fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), Text( '${isi.faz} · %${isi.ilerleme}', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, fontSize: 10, ), overflow: TextOverflow.ellipsis, ), ], ), ), Text('${isi.adim}/${isi.toplamAdim}', style: theme.textTheme.labelSmall?.copyWith( fontFamily: 'monospace', fontSize: 10, )), ], ), ), // Tamamlanmış LoRA'lar for (final l in benimLoralar) Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), child: Row( children: [ const Icon(Icons.check_circle, size: 12, color: Colors.green), const SizedBox(width: 6), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l.meta['isim'] as String? ?? l.id, style: theme.textTheme.labelSmall ?.copyWith(fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), Text( 'base: ${l.baseModel.split("/").last} · ' 'tetik: ${l.tetikleyici} · ${l.adim} adım · ' '${l.boyutMb} MB', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, fontSize: 9, ), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ), // Hatalı işler for (final isi in benimIsler.where((i) => i.durum == 2)) Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), child: Row( children: [ const Icon(Icons.error_outline, size: 12, color: Colors.red), const SizedBox(width: 6), Expanded( child: Text( '${isi.loraIsim}: ${isi.hata ?? "hata"}', style: theme.textTheme.labelSmall?.copyWith( color: Colors.red, fontSize: 10, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), const SizedBox(height: 4), ], ), ); } } class _KarakterKarti extends StatelessWidget { final Karakter karakter; final bool aktif; final VoidCallback onSec; final VoidCallback onDuzenle; const _KarakterKarti({ required this.karakter, required this.aktif, required this.onSec, required this.onDuzenle, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final yuz = karakter.yuzRefYolu; return SizedBox( width: 110, child: Stack( children: [ Material( color: aktif ? Colors.deepPurple : theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: onSec, onLongPress: onDuzenle, onSecondaryTap: onDuzenle, // sağ tık → düzenle child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all( color: aktif ? Colors.deepPurple : theme.dividerColor, width: aktif ? 2 : 1, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Stack( alignment: Alignment.center, children: [ Container( width: 44, height: 44, decoration: BoxDecoration( shape: BoxShape.circle, color: theme.colorScheme.surface, border: Border.all( color: aktif ? Colors.white : theme.dividerColor, width: 2, ), ), clipBehavior: Clip.antiAlias, child: yuz != null && File(yuz).existsSync() ? Image.file(File(yuz), fit: BoxFit.cover) : Icon(Icons.person, size: 24, color: theme.colorScheme.outline), ), if (aktif) const Positioned( right: 0, bottom: 0, child: Icon(Icons.check_circle, size: 16, color: Colors.white), ), ], ), const SizedBox(height: 4), Text( karakter.isim, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: aktif ? Colors.white : null, ), ), Text( karakter.cinsiyet, style: TextStyle( fontSize: 9, color: aktif ? Colors.white.withValues(alpha: 0.85) : theme.colorScheme.outline, ), ), ], ), ), ), ), // Düzenleme butonu — sağ üst köşe Positioned( top: 2, right: 2, child: Material( color: aktif ? Colors.white.withValues(alpha: 0.25) : theme.colorScheme.surface, shape: const CircleBorder(), child: InkWell( customBorder: const CircleBorder(), onTap: onDuzenle, child: Tooltip( message: 'Düzenle / Sil', child: Padding( padding: const EdgeInsets.all(3), child: Icon( Icons.edit_outlined, size: 12, color: aktif ? Colors.white : theme.colorScheme.outline, ), ), ), ), ), ), ], ), ); } } /// Karakter dialog'da gösterilen — bu karaktere ait ACE-Step Ses LoRA listesi. class _KarakterAceLoraListesi extends ConsumerWidget { final String karakterId; const _KarakterAceLoraListesi({required this.karakterId}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final tumLoralar = ref.watch(aceLoraListesiProvider); final tumIsler = ref.watch(aceLoraIsleriProvider); final benimLoralar = tumLoralar .where((l) => l.karakterId == karakterId) .toList(); final benimIsler = tumIsler.values .where((i) => i.karakterId == karakterId) .toList(); if (benimLoralar.isEmpty && benimIsler.isEmpty) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest .withValues(alpha: 0.4), borderRadius: BorderRadius.circular(4), ), child: Text( 'Bu karaktere ait henüz ACE-Step LoRA yok.', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, ), ), ); } return Container( decoration: BoxDecoration( border: Border.all(color: theme.dividerColor), borderRadius: BorderRadius.circular(6), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.cyan.withValues(alpha: 0.1), borderRadius: const BorderRadius.vertical(top: Radius.circular(6)), ), child: Row( children: [ const Icon(Icons.music_note, size: 12, color: Colors.cyan), const SizedBox(width: 4), Text( 'ACE-Step LoRA\'lar (${benimLoralar.length}' '${benimIsler.where((i) => i.durum == 0).isNotEmpty ? " + ${benimIsler.where((i) => i.durum == 0).length} eğitim" : ""})', style: theme.textTheme.labelSmall?.copyWith( fontWeight: FontWeight.w600, color: Colors.cyan.shade800, ), ), ], ), ), // Aktif eğitimler for (final isi in benimIsler.where((i) => i.durum == 0)) Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( children: [ const SizedBox( width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: 6), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isi.loraIsim, style: theme.textTheme.labelSmall ?.copyWith(fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), Text( '${isi.faz} · %${isi.ilerleme}', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, fontSize: 10, ), overflow: TextOverflow.ellipsis, ), ], ), ), IconButton( icon: const Icon(Icons.cancel, size: 14, color: Colors.red), tooltip: 'İptal', visualDensity: VisualDensity.compact, onPressed: () => ref .read(aceLoraIsleriProvider.notifier) .iptal(isi.id), ), Text('${isi.epoch}/${isi.toplamEpoch}', style: theme.textTheme.labelSmall?.copyWith( fontFamily: 'monospace', fontSize: 10, )), ], ), ), // Tamamlanmış LoRA'lar for (final l in benimLoralar) Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( children: [ const Icon(Icons.check_circle, size: 12, color: Colors.green), const SizedBox(width: 6), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l.isim ?? l.id, style: theme.textTheme.labelSmall ?.copyWith(fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), Text( 'base: ${l.baseVariant} · ' 'tarz: ${l.tarz} · ' 'tetik: ${l.tetikleyici} · ' '${l.epoch} ep · ${l.boyutMb.toStringAsFixed(1)} MB', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, fontSize: 9, ), overflow: TextOverflow.ellipsis, ), ], ), ), // Devam et butonu IconButton( icon: const Icon(Icons.play_arrow, size: 14, color: Colors.cyan), tooltip: 'Eğitime devam et', visualDensity: VisualDensity.compact, onPressed: () { // mevcut LoRA üzerine devam — karakter context'i lazım final karakterler = ref.read(karakterlerProvider); final k = karakterler .where((x) => x.id == karakterId) .firstOrNull; if (k != null) { AceLoraEgitDialog.goster( context, karakter: k, mevcutLoraYolu: l.yol, ); } }, ), IconButton( icon: const Icon(Icons.delete_outline, size: 14, color: Colors.red), tooltip: 'Sil', visualDensity: VisualDensity.compact, onPressed: () => ref .read(aceLoraListesiProvider.notifier) .sil(l.id), ), ], ), ), // Hatalı işler for (final isi in benimIsler.where((i) => i.durum == 2)) Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( children: [ const Icon(Icons.error_outline, size: 12, color: Colors.red), const SizedBox(width: 6), Expanded( child: Text( '${isi.loraIsim}: ${isi.hata ?? "hata"}', style: theme.textTheme.labelSmall?.copyWith( color: Colors.red, fontSize: 10, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), const SizedBox(height: 4), ], ), ); } } /// ACE-Step Ses LoRA eğitim dialog'u — port 8003 backend'ine eğitim başlatır. class AceLoraEgitDialog extends ConsumerStatefulWidget { final Karakter karakter; final String? mevcutLoraYolu; const AceLoraEgitDialog({ super.key, required this.karakter, this.mevcutLoraYolu, }); static Future goster( BuildContext context, { required Karakter karakter, String? mevcutLoraYolu, }) async { await showDialog( context: context, builder: (_) => AceLoraEgitDialog( karakter: karakter, mevcutLoraYolu: mevcutLoraYolu, ), ); } @override ConsumerState createState() => _AceLoraEgitDialogState(); } class _AceLoraEgitDialogState extends ConsumerState { late TextEditingController _isimCtrl; late TextEditingController _tetikleyiciCtrl; String? _datasetKlasoru; String _baseVariant = 'turbo'; // turbo / sft / base String _trainMode = 'fixed'; // fixed / vanilla int _epoch = 500; int _batchSize = 1; double _lr = 1e-4; String _tarz = 'pop'; bool _voiceCloneEtkin = false; @override void initState() { super.initState(); final devamMi = widget.mevcutLoraYolu != null; _isimCtrl = TextEditingController( text: devamMi ? '' : '${widget.karakter.isim}_ace_v1', ); final tetik = widget.karakter.isim .replaceAll(RegExp(r'[^A-Za-z0-9]'), '') .toLowerCase(); _tetikleyiciCtrl = TextEditingController(text: '${tetik}_voice'); } @override void dispose() { _isimCtrl.dispose(); _tetikleyiciCtrl.dispose(); super.dispose(); } Future _klasorSec() async { final yol = await FilePicker.platform.getDirectoryPath(); if (yol != null) { setState(() => _datasetKlasoru = yol); } } int _veriDosyaSayisi() { if (_datasetKlasoru == null) return 0; final d = Directory(_datasetKlasoru!); if (!d.existsSync()) return 0; return d .listSync(recursive: true) .whereType() .where((f) { final ext = f.path.toLowerCase(); return ext.endsWith('.mp3') || ext.endsWith('.wav') || ext.endsWith('.flac') || ext.endsWith('.ogg') || ext.endsWith('.m4a'); }) .length; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final devamMi = widget.mevcutLoraYolu != null; final veriSayisi = _veriDosyaSayisi(); final yeterliVeri = veriSayisi >= 3 || devamMi; return AlertDialog( title: Row( children: [ const Icon(Icons.music_note, color: Colors.cyan), const SizedBox(width: 8), Expanded( child: Text( devamMi ? 'ACE-Step LoRA — Devam Et' : 'ACE-Step Ses LoRA Eğit', overflow: TextOverflow.ellipsis, ), ), ], ), content: SizedBox( width: 520, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Karakter bilgisi Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.cyan.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(6), ), child: Row( children: [ const Icon(Icons.person_pin_circle, size: 14, color: Colors.cyan), const SizedBox(width: 6), Text('Karakter: ${widget.karakter.isim}', style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.w600)), if (devamMi) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.cyan, borderRadius: BorderRadius.circular(4), ), child: const Text( 'DEVAM', style: TextStyle( fontSize: 9, color: Colors.white, fontWeight: FontWeight.w700), ), ), ], ], ), ), const SizedBox(height: 12), TextField( controller: _isimCtrl, decoration: const InputDecoration( labelText: 'LoRA İsmi', border: OutlineInputBorder(), isDense: true, hintText: 'Ayse_ace_v1', ), ), const SizedBox(height: 10), TextField( controller: _tetikleyiciCtrl, decoration: const InputDecoration( labelText: 'Tetikleme kelimesi', border: OutlineInputBorder(), isDense: true, hintText: 'ayse_voice', ), ), const SizedBox(height: 12), // Dataset klasör seçimi Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( border: Border.all(color: theme.dividerColor), borderRadius: BorderRadius.circular(6), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.folder_open, size: 14, color: Colors.cyan), const SizedBox(width: 6), Expanded( child: Text( _datasetKlasoru ?? 'Eğitim veri klasörü seç (.wav/.mp3/.flac)', style: theme.textTheme.labelSmall?.copyWith( color: _datasetKlasoru != null ? null : theme.colorScheme.outline, ), overflow: TextOverflow.ellipsis, ), ), OutlinedButton.icon( icon: const Icon(Icons.folder, size: 14), label: const Text('Klasör seç', style: TextStyle(fontSize: 11)), onPressed: _klasorSec, ), ], ), if (_datasetKlasoru != null) Padding( padding: const EdgeInsets.only(top: 4), child: Text( '$veriSayisi ses dosyası bulundu ' '${veriSayisi < 3 ? "(min 3 önerilir)" : "✓"}', style: theme.textTheme.labelSmall?.copyWith( color: veriSayisi >= 3 ? Colors.green : Colors.orange, fontSize: 10, ), ), ), ], ), ), const SizedBox(height: 14), // Base model seçimi Text('Base Model', style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 4), SegmentedButton( segments: const [ ButtonSegment( value: 'turbo', label: Text('Turbo'), icon: Icon(Icons.bolt, size: 14)), ButtonSegment( value: 'sft', label: Text('SFT'), icon: Icon(Icons.tune, size: 14)), ButtonSegment( value: 'base', label: Text('Base'), icon: Icon(Icons.layers, size: 14)), ], selected: {_baseVariant}, onSelectionChanged: (s) => setState(() => _baseVariant = s.first), style: const ButtonStyle( visualDensity: VisualDensity.compact, ), ), const SizedBox(height: 12), // Train mode Text('Eğitim Modu', style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 4), SegmentedButton( segments: const [ ButtonSegment( value: 'fixed', label: Text('Fixed (önerilir)')), ButtonSegment(value: 'vanilla', label: Text('Vanilla')), ], selected: {_trainMode}, onSelectionChanged: (s) => setState(() => _trainMode = s.first), style: const ButtonStyle( visualDensity: VisualDensity.compact, ), ), const SizedBox(height: 12), // Tarz seçimi DropdownButtonFormField( value: _tarz, decoration: const InputDecoration( labelText: 'Müzik tarzı', border: OutlineInputBorder(), isDense: true, ), items: const [ DropdownMenuItem(value: 'pop', child: Text('Pop')), DropdownMenuItem(value: 'rock', child: Text('Rock')), DropdownMenuItem(value: 'klasik', child: Text('Klasik')), DropdownMenuItem( value: 'turkhalk', child: Text('Türk Halk Müziği')), DropdownMenuItem(value: 'arabesk', child: Text('Arabesk')), DropdownMenuItem( value: 'electronic', child: Text('Electronic')), DropdownMenuItem(value: 'jazz', child: Text('Jazz')), ], onChanged: (v) => setState(() => _tarz = v ?? 'pop'), ), const SizedBox(height: 12), // Epoch slider Row( children: [ Text('Epoch: $_epoch', style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.w600)), const Spacer(), Text( _epoch < 200 ? 'hızlı' : _epoch < 700 ? 'orta' : 'derin', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, ), ), ], ), Slider( value: _epoch.toDouble(), min: 100, max: 2000, divisions: 19, label: '$_epoch', onChanged: (v) => setState(() => _epoch = v.round()), ), // Batch size Row( children: [ Text('Batch:', style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(width: 8), ...[1, 2, 3, 4].map((b) => Padding( padding: const EdgeInsets.only(right: 4), child: ChoiceChip( label: Text('$b', style: const TextStyle(fontSize: 11)), selected: _batchSize == b, onSelected: (_) => setState(() => _batchSize = b), visualDensity: VisualDensity.compact, ), )), ], ), const SizedBox(height: 8), // Learning rate Row( children: [ Text('LR:', style: theme.textTheme.labelMedium ?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(width: 8), ...[ (0.5e-4, '0.5e-4'), (1e-4, '1e-4'), (2e-4, '2e-4'), ].map((p) => Padding( padding: const EdgeInsets.only(right: 4), child: ChoiceChip( label: Text(p.$2, style: const TextStyle(fontSize: 11)), selected: (_lr - p.$1).abs() < 1e-9, onSelected: (_) => setState(() => _lr = p.$1), visualDensity: VisualDensity.compact, ), )), ], ), const SizedBox(height: 8), // Voice clone CheckboxListTile( value: _voiceCloneEtkin, onChanged: (v) => setState(() => _voiceCloneEtkin = v ?? false), contentPadding: EdgeInsets.zero, dense: true, visualDensity: VisualDensity.compact, controlAffinity: ListTileControlAffinity.leading, title: Text('Voice clone aktif', style: theme.textTheme.labelMedium), subtitle: Text( 'RVC ile karakter sesini şarkıya bindir', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.outline, fontSize: 10, ), ), ), const SizedBox(height: 8), if (!yeterliVeri) Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ const Icon(Icons.warning_amber, size: 14, color: Colors.orange), const SizedBox(width: 6), Expanded( child: Text( 'En az 3 ses dosyası gerekli (önerilen: 10+).', style: theme.textTheme.labelSmall?.copyWith( color: Colors.orange.shade800, ), ), ), ], ), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('İptal'), ), FilledButton.icon( icon: const Icon(Icons.play_arrow, size: 16), label: Text(devamMi ? 'Devam Et' : 'Eğitime Başla'), style: FilledButton.styleFrom( backgroundColor: Colors.cyan.shade700, ), onPressed: (_isimCtrl.text.trim().isEmpty || _datasetKlasoru == null || !yeterliVeri) ? null : () async { // Port kontrolü final hazir = await AceLoraApiIstemci.portAcikMi(); if (!hazir) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'ACE-Step backend (port 8003) bulunamadı.'), backgroundColor: Colors.red, ), ); } return; } final jobId = await ref .read(aceLoraIsleriProvider.notifier) .egitBaslat( karakterId: widget.karakter.id, loraIsim: _isimCtrl.text.trim(), veriKlasoru: _datasetKlasoru!, baseVariant: _baseVariant, tarz: _tarz, tetikleyici: _tetikleyiciCtrl.text.trim().isEmpty ? null : _tetikleyiciCtrl.text.trim(), epoch: _epoch, rank: 16, mevcutLoraYolu: widget.mevcutLoraYolu, ); if (context.mounted) { if (jobId != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Eğitim başladı: $jobId'), backgroundColor: Colors.cyan, ), ); Navigator.pop(context); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Eğitim başlatılamadı'), backgroundColor: Colors.red, ), ); } } }, ), ], ); } }