import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; import '../../cekirdek/log.dart'; /// Wav2Lip post-pass servisi — render edilmiş bir videoyu alır, /// **sadece dudak hareketlerini** ses ile senkronize eder. Karakter, /// kıyafet, kompozisyon, ışık değişmez — yalnızca ağız bölgesi. /// /// **Kurulum (PC tarafında, bir kerelik):** /// 1. Python venv: `D:\__sistem__\...\video-uretim\wav2lip\.venv` /// 2. `pip install -r requirements.txt` (rudrabha/Wav2Lip veya /// anothermartz/Easy-Wav2Lip) /// 3. Model indir: `wav2lip.pth` veya `wav2lip_gan.pth` → `checkpoints/` /// 4. (Opsiyonel) GFPGAN ile yüz iyileştirme — `easy_wav2lip` desteğinde var /// /// Kurulum yoksa servis `kuruluMu()` false döner, UI uyarı gösterir. class Wav2LipServisi { static const _kok = r'D:\__sistem__\Masaustu\FikoYT.Yeni.Cag\video-uretim\wav2lip'; static String get pythonExe => p.join(_kok, '.venv', 'Scripts', 'python.exe'); static String get inferenceScript => p.join(_kok, 'inference.py'); static String get checkpointsDizini => p.join(_kok, 'checkpoints'); /// Wav2Lip kurulu mu? static Future kuruluMu() async { if (!await File(pythonExe).exists()) return false; if (!await File(inferenceScript).exists()) return false; return true; } /// Mevcut checkpoint dosyaları (.pth). static Future> mevcutCheckpointler() async { final dir = Directory(checkpointsDizini); if (!await dir.exists()) return const []; return dir .listSync(followLinks: false) .whereType() .where((f) => f.path.toLowerCase().endsWith('.pth')) .map((f) => p.basename(f.path)) .toList() ..sort(); } /// Wav2Lip ile lipsync düzeltmesi yap. /// /// [videoYolu]: orijinal render edilmiş video (input) /// [sesYolu]: kullanılacak ses dosyası (segment'in audio'su) /// [hedefYol]: çıktı dosyası /// [checkpointAd]: 'wav2lip.pth' veya 'wav2lip_gan.pth' (kalite > hız) /// [resize]: yüz tespit/işleme için resize faktörü (1=full, 2=yarı) /// [pads]: yüz çevresi padding (top, bottom, left, right) — "0,10,0,0" /// [gfpgan]: GFPGAN ile yüz iyileştirme aktif mi (Easy-Wav2Lip özelliği) static Future uygula({ required String videoYolu, required String sesYolu, required String hedefYol, String checkpointAd = 'wav2lip_gan.pth', int resize = 1, String pads = '0,10,0,0', bool gfpgan = false, void Function(double oran, String? aciklama)? ilerleme, }) async { if (!await kuruluMu()) { throw Exception( 'Wav2Lip kurulu değil. docs/lipsync-kurulum.md\'ı incele.'); } if (!await File(videoYolu).exists()) { throw Exception('Video bulunamadı: $videoYolu'); } if (!await File(sesYolu).exists()) { throw Exception('Ses bulunamadı: $sesYolu'); } final ckpt = p.join(checkpointsDizini, checkpointAd); if (!await File(ckpt).exists()) { throw Exception('Checkpoint yok: $ckpt'); } await File(hedefYol).parent.create(recursive: true); final args = [ inferenceScript, '--checkpoint_path', ckpt, '--face', videoYolu, '--audio', sesYolu, '--outfile', hedefYol, '--resize_factor', '$resize', '--pads', pads, if (gfpgan) ...[ '--enhance_face', 'gfpgan', ], // Kalite için varsayılan '--nosmooth', ]; Log.bilgi('Wav2Lip', 'Subprocess: $pythonExe ${args.skip(1).take(3).join(" ")} ...'); ilerleme?.call(0.05, 'Wav2Lip başlıyor'); final surec = await Process.start( pythonExe, args, workingDirectory: _kok, environment: { 'PYTHONIOENCODING': 'utf-8', 'PYTHONUNBUFFERED': '1', }, ); final stdoutFut = surec.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((satir) { Log.bilgi('Wav2Lip', satir); // Progress parse — Wav2Lip tqdm: " 35%|███▌| 7/20" final tqdm = RegExp(r'(\d+)%\|').firstMatch(satir); if (tqdm != null) { final yuzde = int.parse(tqdm.group(1)!) / 100.0; ilerleme?.call(yuzde.clamp(0.05, 0.95), 'İşleniyor'); } else if (satir.contains('Number of frames')) { ilerleme?.call(0.10, 'Kareler tespit edildi'); } else if (satir.contains('Face detection')) { ilerleme?.call(0.20, 'Yüz tespiti'); } else if (satir.contains('Lip syncing')) { ilerleme?.call(0.40, 'Dudak senkron'); } }).asFuture(); final stderrLog = StringBuffer(); final stderrFut = surec.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((satir) { stderrLog.writeln(satir); }).asFuture(); final exitCode = await surec.exitCode; await stdoutFut; await stderrFut; if (exitCode != 0) { throw Exception( 'Wav2Lip exit=$exitCode\n${stderrLog.toString().split("\n").reversed.take(5).toList().reversed.join("\n")}'); } if (!await File(hedefYol).exists()) { throw Exception('Çıktı bulunamadı: $hedefYol'); } Log.bilgi('Wav2Lip', 'Tamam: $hedefYol'); ilerleme?.call(1.0, 'Tamam'); return File(hedefYol); } }