import 'dart:async'; import 'dart:convert'; import 'dart:io'; import '../../cekirdek/log.dart'; /// Yerel Whisper (resmi `whisper` CLI veya `whisper.cpp` uyumlu) sarmalayıcısı. /// Ses dosyasından zaman damgalı transkript çıkarır. class WhisperLirik { /// Whisper executable yolu (PATH'te ise sadece komut adı yeterli). final String whisperKomut; /// tiny / base / small / medium / large-v3 final String model; WhisperLirik({ this.whisperKomut = 'whisper', this.model = 'small', }); /// `whisper --help` çağırarak kurulu olup olmadığını kontrol eder. Future kuruluMu() async { try { final r = await Process.run(whisperKomut, ['--help']); return r.exitCode == 0; } catch (e) { Log.uyari('WhisperLirik', 'kurulu değil: $e'); return false; } } /// Ses dosyasından zaman damgalı transkript çıkarır. /// /// Çıktı: her eleman `{basla:double, bit:double, metin:String}` (saniye). Future>> transkriptCikar( String sesYolu, { String dil = 'tr', void Function(double yuzde, String faz)? ilerleme, }) async { final dosya = File(sesYolu); if (!await dosya.exists()) { throw Exception('Ses dosyası bulunamadı: $sesYolu'); } final temp = Directory.systemTemp.createTempSync('whisper_'); Log.bilgi('WhisperLirik', 'başlatılıyor: $sesYolu (model=$model, dil=$dil)'); ilerleme?.call(0.02, 'başlatılıyor'); final argumanlar = [ sesYolu, '--language', dil, '--model', model, '--output_format', 'json', '--output_dir', temp.path, '--verbose', 'True', ]; Process surec; try { surec = await Process.start( whisperKomut, argumanlar, runInShell: true, ); } catch (e) { throw Exception('Whisper başlatılamadı: $e'); } // Stdout/stderr ilerleme satırlarını izle (whisper "[00:01.000 --> 00:03.500] ..." üretir). // Yüzde tahmini: en son zaman damgası / ses süresi yerine satır sayısı tabanlı yumuşatma. final yuzdeRegex = RegExp(r'(\d{1,3})\s*%'); final zamanRegex = RegExp(r'\[(\d{1,2}):(\d{2}(?:\.\d+)?)\s*-->'); double sonYuzde = 0.05; void satirIsle(String satir) { final my = yuzdeRegex.firstMatch(satir); if (my != null) { final v = double.tryParse(my.group(1)!); if (v != null) { sonYuzde = (v / 100.0).clamp(0.0, 0.99); ilerleme?.call(sonYuzde, 'işleniyor'); return; } } final mz = zamanRegex.firstMatch(satir); if (mz != null) { // Kaba ilerleme: zaman damgası arttıkça %30..%95 arası ilerle. sonYuzde = (sonYuzde + 0.005).clamp(0.05, 0.95); ilerleme?.call(sonYuzde, 'transkript'); } } final stdoutSub = surec.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen(satirIsle); final stderrSub = surec.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen(satirIsle); final cikis = await surec.exitCode; await stdoutSub.cancel(); await stderrSub.cancel(); if (cikis != 0) { _temizle(temp); throw Exception('Whisper başarısız (exit=$cikis)'); } ilerleme?.call(0.97, 'json okunuyor'); // Çıktı: .json (uzantısız taban isim kullanılır). final tabanAd = _tabanAd(sesYolu); final jsonDosya = File('${temp.path}${Platform.pathSeparator}$tabanAd.json'); if (!await jsonDosya.exists()) { // Bazı sürümler farklı uzantı kullanabilir; klasördeki ilk .json'u dene. final alt = temp .listSync() .whereType() .where((f) => f.path.toLowerCase().endsWith('.json')) .toList(); if (alt.isEmpty) { _temizle(temp); throw Exception('Whisper JSON çıktısı bulunamadı: ${jsonDosya.path}'); } return _jsonOku(alt.first, temp); } final sonuc = await _jsonOku(jsonDosya, temp); ilerleme?.call(1.0, 'tamam'); return sonuc; } Future>> _jsonOku(File f, Directory temp) async { try { final ham = await f.readAsString(); final data = jsonDecode(ham); final segments = (data is Map) ? data['segments'] : null; if (segments is! List) { _temizle(temp); Log.uyari('WhisperLirik', 'segments alanı bulunamadı'); return const []; } final liste = >[]; for (final s in segments) { if (s is! Map) continue; final basla = (s['start'] as num?)?.toDouble() ?? 0; final bit = (s['end'] as num?)?.toDouble() ?? 0; final metin = (s['text'] ?? '').toString().trim(); liste.add({'basla': basla, 'bit': bit, 'metin': metin}); } _temizle(temp); return liste; } catch (e) { _temizle(temp); Log.hata('WhisperLirik', 'JSON okuma hatası: $e'); rethrow; } } void _temizle(Directory d) { try { if (d.existsSync()) d.deleteSync(recursive: true); } catch (_) {} } String _tabanAd(String yol) { final ayrac = yol.contains('\\') ? '\\' : '/'; final dosyaAdi = yol.split(ayrac).last; final nokta = dosyaAdi.lastIndexOf('.'); return nokta > 0 ? dosyaAdi.substring(0, nokta) : dosyaAdi; } }