# Flutter Video Üretim Stüdyosu - Implementation Plan

> **Not:** Kullanıcı CLAUDE.md tercihleri gereği test minimum (sadece kritik pipeline mantığı: LtxClient, FfmpegService, JobOrchestrator, KaynakKopyalayici). UI manuel doğrulanır.

**Goal:** Mevcut `Muzik-Video-Uret.bat` pipeline'ını profesyonel Flutter desktop stüdyosuna dönüştür - per-segment tam kontrol, multi-output export, resumable job kuyruğu.

**Architecture:** Flutter desktop → LTX HTTP API + ffmpeg/FaceFusion/WanGP subprocess. Riverpod + Hive state. 4 tema, 3 görünüm modu.

**Tech Stack:** Flutter 3.41, Riverpod, Freezed, Dio, Hive, ffmpeg, video_player_win, fluent_ui, forui.

---

## Faz 0: İskelet (1 gün)

### Task 0.1: Flutter desktop init + bağımlılıklar

**Files:**
- Create: tüm proje (`flutter create`)
- Modify: `pubspec.yaml`

- [ ] **Step 1:** Flutter create

```bash
cd "D:/__sistem__/Masaustu/FikoYT.Yeni.Cag/video-uretim"
flutter create --platforms=windows --org tr.fikoyt --project-name fikoyt_video_uret flutter-video-uret
```

- [ ] **Step 2:** pubspec.yaml bağımlılıkları ekle

```yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5
  freezed_annotation: ^2.4.4
  json_annotation: ^4.9.0
  dio: ^5.7.0
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  path_provider: ^2.1.4
  path: ^1.9.0
  go_router: ^14.6.0
  window_manager: ^0.4.3
  tray_manager: ^0.3.0
  video_player: ^2.9.2
  video_player_win: ^3.1.1
  file_picker: ^8.1.4
  desktop_drop: ^0.5.0
  fluent_ui: ^4.9.2
  forui: ^0.7.0
  crypto: ^3.0.5
  uuid: ^4.5.1
  process_run: ^1.2.0
  flutter_acrylic: ^1.1.4
  fl_chart: ^0.69.0
  flutter_animate: ^4.5.0
  collection: ^1.18.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  build_runner: ^2.4.13
  riverpod_generator: ^2.4.3
  freezed: ^2.5.7
  json_serializable: ^6.8.0
```

- [ ] **Step 3:** `flutter pub get`

- [ ] **Step 4:** Commit
```bash
git init
git add .
git commit -m "Faz 0.1: Flutter desktop iskelet"
```

### Task 0.2: Tema token sistemi (Özel Fiko ana, diğer 3 stub)

**Files:**
- Create: `lib/cekirdek/sabitler.dart`
- Create: `lib/ui/tema/tema_kaydedici.dart`
- Create: `lib/ui/tema/ozel_fiko_tema.dart`
- Create: `lib/ui/tema/material_dark_tema.dart`
- Create: `lib/ui/tema/fluent_tema.dart`
- Create: `lib/ui/tema/shadcn_minimal_tema.dart`
- Create: `lib/durum/tema_durumu.dart`

- [ ] **Step 1:** `lib/cekirdek/sabitler.dart`

```dart
class Sabitler {
  static const String uygulamaIsmi = 'FikoYT Video Stüdyo';
  static const Map<String, int> cozunurlukMaxSure = {
    '512p': 20, '540p': 20, '720p': 10, '1080p': 5,
  };
  static const List<int> izinliSureler = [5, 6, 8, 10, 20];
  static const List<String> modeller = ['fast', 'pro'];
  static const List<String> oranlar = ['16:9', '9:16'];
  static const List<String> kameraHareketleri = [
    'none', 'zoom-in', 'zoom-out', 'pan-left', 'pan-right',
  ];
}
```

- [ ] **Step 2:** `lib/ui/tema/tema_kaydedici.dart` - tema enum + abstract

```dart
import 'package:flutter/material.dart';

enum TemaTuru { ozelFiko, materialDark, fluent, shadcnMinimal }

class TemaTanimi {
  final ThemeData light, dark;
  const TemaTanimi(this.light, this.dark);
}
```

- [ ] **Step 3:** `lib/ui/tema/ozel_fiko_tema.dart` - ana glassmorphic tema

```dart
import 'package:flutter/material.dart';

class OzelFikoTema {
  static const _aksan = Color(0xFFFBBF24);
  static const _arkaplan = Color(0xFF0F1115);
  static const _yuzey = Color(0xFF161A22);
  static const _kenar = Color(0xFF2A2F3A);
  static const _metin = Color(0xFFCDD3DF);
  static const _metinIkincil = Color(0xFF8A93A4);

  static ThemeData karanlik() => ThemeData(
    useMaterial3: true,
    brightness: Brightness.dark,
    scaffoldBackgroundColor: _arkaplan,
    colorScheme: const ColorScheme.dark(
      primary: _aksan,
      surface: _yuzey,
      onSurface: _metin,
      outline: _kenar,
    ),
    cardTheme: const CardTheme(
      color: _yuzey,
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(10)),
        side: BorderSide(color: _kenar, width: 1),
      ),
    ),
    textTheme: const TextTheme(
      bodyMedium: TextStyle(color: _metin, fontSize: 13),
      labelSmall: TextStyle(color: _metinIkincil, fontSize: 11),
    ),
  );

  static ThemeData aydinlik() => karanlik().copyWith(
    brightness: Brightness.light,
    scaffoldBackgroundColor: const Color(0xFFFAFAFA),
  );
}
```

- [ ] **Step 4:** Diğer 3 tema (material_dark/fluent/shadcn) — stub: ThemeData.dark/light, accent override.

- [ ] **Step 5:** `lib/durum/tema_durumu.dart`

```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../ui/tema/tema_kaydedici.dart';
import '../ui/tema/ozel_fiko_tema.dart';
import '../ui/tema/material_dark_tema.dart';
import '../ui/tema/fluent_tema.dart';
import '../ui/tema/shadcn_minimal_tema.dart';

class TemaAyari {
  final TemaTuru tur;
  final bool aydinlik;
  final Color aksan;
  TemaAyari({required this.tur, required this.aydinlik, required this.aksan});

  ThemeData get tema {
    final t = switch (tur) {
      TemaTuru.ozelFiko => aydinlik ? OzelFikoTema.aydinlik() : OzelFikoTema.karanlik(),
      TemaTuru.materialDark => MaterialDarkTema.olustur(aydinlik),
      TemaTuru.fluent => FluentTema.olustur(aydinlik),
      TemaTuru.shadcnMinimal => ShadcnMinimalTema.olustur(aydinlik),
    };
    return t.copyWith(colorScheme: t.colorScheme.copyWith(primary: aksan));
  }

  TemaAyari kopyala({TemaTuru? tur, bool? aydinlik, Color? aksan}) =>
    TemaAyari(tur: tur ?? this.tur, aydinlik: aydinlik ?? this.aydinlik, aksan: aksan ?? this.aksan);
}

class TemaNotifier extends StateNotifier<TemaAyari> {
  TemaNotifier() : super(TemaAyari(tur: TemaTuru.ozelFiko, aydinlik: false, aksan: const Color(0xFFFBBF24))) {
    _yukle();
  }
  Box? _kutu;
  Future<void> _yukle() async {
    _kutu = await Hive.openBox('ayarlar');
    final t = _kutu!.get('tema_tur', defaultValue: 'ozelFiko');
    final a = _kutu!.get('tema_aydinlik', defaultValue: false);
    final r = _kutu!.get('tema_aksan', defaultValue: 0xFFFBBF24);
    state = TemaAyari(
      tur: TemaTuru.values.firstWhere((e) => e.name == t),
      aydinlik: a,
      aksan: Color(r),
    );
  }
  void degistir(TemaAyari yeni) {
    state = yeni;
    _kutu?.put('tema_tur', yeni.tur.name);
    _kutu?.put('tema_aydinlik', yeni.aydinlik);
    _kutu?.put('tema_aksan', yeni.aksan.value);
  }
}

final temaProvider = StateNotifierProvider<TemaNotifier, TemaAyari>((ref) => TemaNotifier());
```

- [ ] **Step 6:** Commit `git commit -m "Faz 0.2: Tema sistemi (4 tema)"`

### Task 0.3: Ana pencere shell + go_router

**Files:**
- Create: `lib/main.dart`
- Create: `lib/app.dart`
- Create: `lib/yonlendirici.dart`
- Create: `lib/ui/ana_pencere.dart`
- Create: `lib/ui/sayfalar/projeler_sayfasi.dart` (stub)
- Create: `lib/ui/sayfalar/studyo_sayfasi.dart` (stub)
- Create: `lib/ui/sayfalar/render_sayfasi.dart` (stub)
- Create: `lib/ui/sayfalar/gecmis_sayfasi.dart` (stub)
- Create: `lib/ui/sayfalar/ayarlar_sayfasi.dart` (stub)

- [ ] **Step 1:** `lib/main.dart`

```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:video_player_win/video_player_win_plugin.dart';
import 'app.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  await Window.initialize();
  await windowManager.ensureInitialized();
  WindowsVideoPlayer.registerWith();
  await windowManager.waitUntilReadyToShow(
    const WindowOptions(
      size: Size(1480, 920),
      minimumSize: Size(1200, 760),
      center: true,
      backgroundColor: Colors.transparent,
      titleBarStyle: TitleBarStyle.normal,
      title: 'FikoYT Video Stüdyo',
    ),
    () async {
      await Window.setEffect(effect: WindowEffect.acrylic, dark: true);
      await windowManager.show();
      await windowManager.focus();
    },
  );
  runApp(const ProviderScope(child: VideoStudyoApp()));
}
```

- [ ] **Step 2:** `lib/app.dart`

```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'durum/tema_durumu.dart';
import 'yonlendirici.dart';

class VideoStudyoApp extends ConsumerWidget {
  const VideoStudyoApp({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final tema = ref.watch(temaProvider);
    return MaterialApp.router(
      title: 'FikoYT Video Stüdyo',
      debugShowCheckedModeBanner: false,
      theme: tema.tema,
      routerConfig: yonlendirici,
    );
  }
}
```

- [ ] **Step 3:** `lib/yonlendirici.dart`

```dart
import 'package:go_router/go_router.dart';
import 'ui/ana_pencere.dart';
import 'ui/sayfalar/projeler_sayfasi.dart';
import 'ui/sayfalar/studyo_sayfasi.dart';
import 'ui/sayfalar/render_sayfasi.dart';
import 'ui/sayfalar/gecmis_sayfasi.dart';
import 'ui/sayfalar/ayarlar_sayfasi.dart';

final yonlendirici = GoRouter(
  initialLocation: '/projeler',
  routes: [
    ShellRoute(
      builder: (ctx, state, child) => AnaPencere(child: child),
      routes: [
        GoRoute(path: '/projeler', builder: (c, s) => const ProjelerSayfasi()),
        GoRoute(path: '/studyo/:projeId', builder: (c, s) => StudyoSayfasi(projeId: s.pathParameters['projeId']!)),
        GoRoute(path: '/render', builder: (c, s) => const RenderSayfasi()),
        GoRoute(path: '/gecmis', builder: (c, s) => const GecmisSayfasi()),
        GoRoute(path: '/ayarlar', builder: (c, s) => const AyarlarSayfasi()),
      ],
    ),
  ],
);
```

- [ ] **Step 4:** `lib/ui/ana_pencere.dart`

```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class AnaPencere extends StatelessWidget {
  final Widget child;
  const AnaPencere({super.key, required this.child});
  @override
  Widget build(BuildContext context) {
    final loc = GoRouterState.of(context).uri.path;
    int idx = 0;
    if (loc.startsWith('/studyo')) idx = 1;
    else if (loc.startsWith('/render')) idx = 2;
    else if (loc.startsWith('/gecmis')) idx = 3;
    else if (loc.startsWith('/ayarlar')) idx = 4;
    return Scaffold(
      body: Row(
        children: [
          NavigationRail(
            extended: false,
            selectedIndex: idx,
            onDestinationSelected: (i) {
              const yollar = ['/projeler', '/studyo/_son_', '/render', '/gecmis', '/ayarlar'];
              context.go(yollar[i]);
            },
            destinations: const [
              NavigationRailDestination(icon: Icon(Icons.folder_outlined), label: Text('Projeler')),
              NavigationRailDestination(icon: Icon(Icons.movie_outlined), label: Text('Stüdyo')),
              NavigationRailDestination(icon: Icon(Icons.queue_outlined), label: Text('Render')),
              NavigationRailDestination(icon: Icon(Icons.history), label: Text('Geçmiş')),
              NavigationRailDestination(icon: Icon(Icons.settings_outlined), label: Text('Ayarlar')),
            ],
          ),
          const VerticalDivider(width: 1),
          Expanded(child: child),
        ],
      ),
    );
  }
}
```

- [ ] **Step 5:** Stub sayfalar — her biri `Center(child: Text('Sayfa adı'))` döner.

- [ ] **Step 6:** `flutter run -d windows` — uygulama açılmalı, 5 nav sekmesi.

- [ ] **Step 7:** Commit `git commit -m "Faz 0.3: Ana pencere shell + router"`

---

## Faz 1: Veri Modeli + Proje Deposu (1-2 gün)

### Task 1.1: Tüm freezed model'leri yaz

**Files:**
- Create: `lib/modeller/segment.dart`
- Create: `lib/modeller/segment_grubu.dart`
- Create: `lib/modeller/preset.dart`
- Create: `lib/modeller/global_ayarlar.dart`
- Create: `lib/modeller/kaynak_dosyalar.dart`
- Create: `lib/modeller/cikis_ayarlari.dart`
- Create: `lib/modeller/analiz_sonuclari.dart`
- Create: `lib/modeller/render_gecmisi.dart`
- Create: `lib/modeller/proje.dart`
- Create: `lib/modeller/job.dart`

- [ ] **Step 1:** `lib/modeller/segment.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'segment.freezed.dart';
part 'segment.g.dart';

enum SegmentDurum { bos, kuyruk, uretiliyor, tamam, hata, atlandi }

@freezed
class AnalizMetadata with _$AnalizMetadata {
  const factory AnalizMetadata({
    String? enerjiSeviyesi,    // silence/low/medium/high/climax
    double? rmsDb,
    String? transkriptSnippet,
    @Default([]) List<double> beats,
  }) = _AnalizMetadata;
  factory AnalizMetadata.fromJson(Map<String, dynamic> j) => _$AnalizMetadataFromJson(j);
}

@freezed
class Segment with _$Segment {
  const factory Segment({
    required String id,
    required int sira,
    required double basla,
    required double bit,
    String? prompt,
    String? negatifPrompt,
    String? model,
    String? cozunurluk,
    String? oran,
    int? sure,
    String? kameraHareketi,
    double? guidanceScale,
    int? steps,
    int? seed,
    bool? ses,
    String? sesOverridePath,
    String? girisGorseliOverridePath,
    bool? faceSwapAcik,
    String? grupId,
    @Default(SegmentDurum.bos) SegmentDurum durum,
    String? videoYolu,
    @Default([]) List<String> varyantYollari,
    String? sonKareYolu,
    AnalizMetadata? analizMeta,
    String? hataMesaji,
    int? uretimSuresiMs,
  }) = _Segment;
  factory Segment.fromJson(Map<String, dynamic> j) => _$SegmentFromJson(j);
}
```

- [ ] **Step 2:** `lib/modeller/segment_grubu.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'segment_grubu.freezed.dart';
part 'segment_grubu.g.dart';

@freezed
class SegmentGrubu with _$SegmentGrubu {
  const factory SegmentGrubu({
    required String id,
    required String isim,
    required String renk,
    @Default({}) Map<String, dynamic> paylasilanAyarlar,
  }) = _SegmentGrubu;
  factory SegmentGrubu.fromJson(Map<String, dynamic> j) => _$SegmentGrubuFromJson(j);
}
```

- [ ] **Step 3:** `lib/modeller/preset.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'preset.freezed.dart';
part 'preset.g.dart';

@freezed
class Preset with _$Preset {
  const factory Preset({
    required String id,
    required String isim,
    required Map<String, dynamic> ayarlar,
    @Default('') String aciklama,
  }) = _Preset;
  factory Preset.fromJson(Map<String, dynamic> j) => _$PresetFromJson(j);
}
```

- [ ] **Step 4:** `lib/modeller/global_ayarlar.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'global_ayarlar.freezed.dart';
part 'global_ayarlar.g.dart';

@freezed
class GlobalAyarlar with _$GlobalAyarlar {
  const factory GlobalAyarlar({
    @Default('cinematic slow motion, beautiful scenery, atmospheric lighting, high quality 4k') String varsayilanPrompt,
    @Default('blurry, low quality, distorted face, extra fingers, multiple people, cartoon, animated') String varsayilanNegatifPrompt,
    @Default('fast') String model,
    @Default('540p') String cozunurluk,
    @Default('16:9') String oran,
    @Default(20) int segmentSuresi,
    @Default(3.5) double guidanceScale,
    @Default(30) int steps,
    @Default(true) bool ses,
    @Default(false) bool infiniteTalk,
    @Default(false) bool faceSwapAcik,
    String? faceSwapKaynakPath,
  }) = _GlobalAyarlar;
  factory GlobalAyarlar.fromJson(Map<String, dynamic> j) => _$GlobalAyarlarFromJson(j);
}
```

- [ ] **Step 5:** `lib/modeller/kaynak_dosyalar.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'kaynak_dosyalar.freezed.dart';
part 'kaynak_dosyalar.g.dart';

@freezed
class KaynakDosyalar with _$KaynakDosyalar {
  const factory KaynakDosyalar({
    required String sesPath,
    required String girisGorseliPath,
    String? faceSwapPath,
    String? watermarkPath,
    String? introPath,
    String? outroPath,
    String? lutPath,
    String? srtPath,
  }) = _KaynakDosyalar;
  factory KaynakDosyalar.fromJson(Map<String, dynamic> j) => _$KaynakDosyalarFromJson(j);
}
```

- [ ] **Step 6:** `lib/modeller/cikis_ayarlari.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'cikis_ayarlari.freezed.dart';
part 'cikis_ayarlari.g.dart';

enum SesKarisimiModu { tamOrijinal, segmentSesleri, mix, sessiz }
enum GecisTipi { yok, crossfade, dissolve, kesim }

@freezed
class CikisPreset with _$CikisPreset {
  const factory CikisPreset({
    required String id,
    required String isim,
    @Default('1080p') String cozunurluk,
    @Default('16:9') String oran,
    @Default('libx264') String codec,
    @Default(20) int crf,
    @Default('') String dosyaIsmiSablon,  // {{stem}}_{{preset}}.mp4
  }) = _CikisPreset;
  factory CikisPreset.fromJson(Map<String, dynamic> j) => _$CikisPresetFromJson(j);
}

@freezed
class GecisAyari with _$GecisAyari {
  const factory GecisAyari({
    required int boundaryIdx,
    @Default(GecisTipi.kesim) GecisTipi tip,
    @Default(0.5) double sureSn,
  }) = _GecisAyari;
  factory GecisAyari.fromJson(Map<String, dynamic> j) => _$GecisAyariFromJson(j);
}

@freezed
class RenkDuzeltme with _$RenkDuzeltme {
  const factory RenkDuzeltme({
    String? lutPath,
    @Default(0.0) double parlaklik,
    @Default(1.0) double kontrast,
    @Default(1.0) double doygunluk,
  }) = _RenkDuzeltme;
  factory RenkDuzeltme.fromJson(Map<String, dynamic> j) => _$RenkDuzeltmeFromJson(j);
}

@freezed
class Watermark with _$Watermark {
  const factory Watermark({
    required String path,
    @Default('top-right') String konum,
    @Default(0.5) double opaklik,
    @Default(0.15) double genislikOran,
  }) = _Watermark;
  factory Watermark.fromJson(Map<String, dynamic> j) => _$WatermarkFromJson(j);
}

@freezed
class Altyazi with _$Altyazi {
  const factory Altyazi({
    String? srtPath,
    @Default(false) bool whisperOtomatik,
    @Default('Inter') String font,
    @Default(28) int boyut,
    @Default('#FFFFFF') String renk,
  }) = _Altyazi;
  factory Altyazi.fromJson(Map<String, dynamic> j) => _$AltyaziFromJson(j);
}

@freezed
class HizRampasi with _$HizRampasi {
  const factory HizRampasi({
    @Default({}) Map<String, double> segmentHizleri,  // segmentId -> 0.5..2.0
  }) = _HizRampasi;
  factory HizRampasi.fromJson(Map<String, dynamic> j) => _$HizRampasiFromJson(j);
}

@freezed
class CikisAyarlari with _$CikisAyarlari {
  const factory CikisAyarlari({
    @Default([]) List<CikisPreset> presetler,
    @Default([]) List<GecisAyari> gecisler,
    @Default(SesKarisimiModu.tamOrijinal) SesKarisimiModu sesModu,
    @Default(true) bool loudnessNormalize,
    RenkDuzeltme? renkDuzeltme,
    Watermark? watermark,
    Altyazi? altyazi,
    String? introPath,
    String? outroPath,
    HizRampasi? hizRampasi,
    @Default(false) bool beatSyncSnap,
    @Default(0.3) double beatSyncTolerans,
  }) = _CikisAyarlari;
  factory CikisAyarlari.fromJson(Map<String, dynamic> j) => _$CikisAyarlariFromJson(j);
}
```

- [ ] **Step 7:** `lib/modeller/analiz_sonuclari.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'analiz_sonuclari.freezed.dart';
part 'analiz_sonuclari.g.dart';

@freezed
class TranskriptParca with _$TranskriptParca {
  const factory TranskriptParca({
    required double basla, required double bit, required String metin,
  }) = _TranskriptParca;
  factory TranskriptParca.fromJson(Map<String, dynamic> j) => _$TranskriptParcaFromJson(j);
}

@freezed
class AnalizSonuclari with _$AnalizSonuclari {
  const factory AnalizSonuclari({
    @Default([]) List<double> rmsZamanlari,
    @Default([]) List<double> rmsDegerleri,
    @Default([]) List<TranskriptParca> transkript,
    @Default([]) List<double> beats,
    @Default(0) double bpm,
    DateTime? guncellendi,
  }) = _AnalizSonuclari;
  factory AnalizSonuclari.fromJson(Map<String, dynamic> j) => _$AnalizSonuclariFromJson(j);
}
```

- [ ] **Step 8:** `lib/modeller/render_gecmisi.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'render_gecmisi.freezed.dart';
part 'render_gecmisi.g.dart';

@freezed
class RenderGecmisi with _$RenderGecmisi {
  const factory RenderGecmisi({
    required String id,
    required DateTime tarih,
    required String presetId,
    required String ciktiPath,
    required int dosyaBoyutuByte,
    required int sureSaniye,
    @Default([]) List<String> kullanilanSegmentVaryantlari,
  }) = _RenderGecmisi;
  factory RenderGecmisi.fromJson(Map<String, dynamic> j) => _$RenderGecmisiFromJson(j);
}
```

- [ ] **Step 9:** `lib/modeller/proje.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'segment.dart';
import 'segment_grubu.dart';
import 'preset.dart';
import 'global_ayarlar.dart';
import 'kaynak_dosyalar.dart';
import 'cikis_ayarlari.dart';
import 'analiz_sonuclari.dart';
import 'render_gecmisi.dart';
part 'proje.freezed.dart';
part 'proje.g.dart';

@freezed
class Proje with _$Proje {
  const factory Proje({
    required String id,
    required String isim,
    @Default(1) int versiyon,
    required DateTime olusturmaTarihi,
    required DateTime guncellemeTarihi,
    required String klasorYolu,           // disk klasör
    required KaynakDosyalar kaynak,
    @Default(GlobalAyarlar()) GlobalAyarlar global,
    @Default([]) List<Segment> segmentler,
    @Default([]) List<SegmentGrubu> gruplar,
    @Default([]) List<Preset> presetler,
    @Default(AnalizSonuclari()) AnalizSonuclari analiz,
    @Default(CikisAyarlari()) CikisAyarlari cikis,
    @Default([]) List<RenderGecmisi> renderGecmisi,
  }) = _Proje;
  factory Proje.fromJson(Map<String, dynamic> j) => _$ProjeFromJson(j);
}
```

- [ ] **Step 10:** `lib/modeller/job.dart`

```dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'job.freezed.dart';
part 'job.g.dart';

enum JobTipi { analiziYap, segmentUret, infiniteTalkUret, faceSwapUygula, enIyiKareyiCikar, birlestir, cikisOlustur, topluUret }
enum JobDurum { kuyruk, calisiyor, tamam, hata, iptal, durakli }

@freezed
class Job with _$Job {
  const factory Job({
    required String id,
    required JobTipi tip,
    required String projeId,
    @Default({}) Map<String, dynamic> params,
    @Default(JobDurum.kuyruk) JobDurum durum,
    @Default(0.0) double ilerleme,
    @Default('') String fazAciklama,
    @Default([]) List<String> log,
    DateTime? baslama,
    DateTime? bitis,
    String? hata,
    @Default(0) int oncelik,
  }) = _Job;
  factory Job.fromJson(Map<String, dynamic> j) => _$JobFromJson(j);
}
```

- [ ] **Step 11:** Build runner çalıştır

```bash
dart run build_runner build --delete-conflicting-outputs
```

Beklenen: tüm `.freezed.dart` ve `.g.dart` dosyaları oluşur.

- [ ] **Step 12:** Commit `git commit -m "Faz 1.1: Veri modelleri (freezed)"`

### Task 1.2: Kimlik üretici + dosya yardımcısı

**Files:**
- Create: `lib/cekirdek/kimlik.dart`
- Create: `lib/cekirdek/dosya_yardimcisi.dart`
- Create: `lib/cekirdek/log.dart`

- [ ] **Step 1:** `lib/cekirdek/kimlik.dart`

```dart
import 'dart:math';

class FikoYTId {
  static const _karakterler = '0123456789abcdef';
  static final _rng = Random.secure();
  static String yeni() {
    final ts = DateTime.now().millisecondsSinceEpoch.toRadixString(16).padLeft(12, '0');
    final rastgele = List.generate(12, (_) => _karakterler[_rng.nextInt(16)]).join();
    return '$ts$rastgele';
  }
}
```

- [ ] **Step 2:** `lib/cekirdek/dosya_yardimcisi.dart`

```dart
import 'dart:io';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as p;

class DosyaYardimcisi {
  static Future<String> hashlaSha256(File dosya) async {
    final akis = dosya.openRead();
    final ozet = await sha256.bind(akis).first;
    return ozet.toString().substring(0, 16);
  }

  static Future<File> kopyalaHashli(File kaynak, Directory hedefKlasor, {String? prefix}) async {
    if (!await hedefKlasor.exists()) await hedefKlasor.create(recursive: true);
    final hash = await hashlaSha256(kaynak);
    final ext = p.extension(kaynak.path);
    final ad = '${prefix ?? p.basenameWithoutExtension(kaynak.path)}_$hash$ext';
    final hedef = File(p.join(hedefKlasor.path, ad));
    if (!await hedef.exists()) await kaynak.copy(hedef.path);
    return hedef;
  }

  static Future<void> atomikYaz(File hedef, String icerik) async {
    final tmp = File('${hedef.path}.tmp');
    await tmp.writeAsString(icerik, encoding: utf8, flush: true);
    await tmp.rename(hedef.path);
  }
}
```

- [ ] **Step 3:** `lib/cekirdek/log.dart`

```dart
import 'dart:collection';
import 'dart:io';
import 'package:flutter/foundation.dart';

enum LogSeviye { bilgi, uyari, hata, debug }

class Log {
  static final _halka = Queue<String>();
  static const _maxSatir = 2000;
  static File? _dosya;

  static Future<void> baslat(File dosya) async {
    _dosya = dosya;
    if (!await dosya.parent.exists()) await dosya.parent.create(recursive: true);
  }

  static void _yaz(LogSeviye s, String etiket, String mesaj) {
    final t = DateTime.now().toIso8601String();
    final satir = '$t [${s.name.toUpperCase()}] $etiket: $mesaj';
    if (kDebugMode) debugPrint(satir);
    _halka.add(satir);
    if (_halka.length > _maxSatir) _halka.removeFirst();
    _dosya?.writeAsString('$satir\n', mode: FileMode.append);
  }

  static void bilgi(String etiket, String m) => _yaz(LogSeviye.bilgi, etiket, m);
  static void uyari(String etiket, String m) => _yaz(LogSeviye.uyari, etiket, m);
  static void hata(String etiket, String m) => _yaz(LogSeviye.hata, etiket, m);
  static void debug(String etiket, String m) => _yaz(LogSeviye.debug, etiket, m);
  static List<String> sonSatirlar({int n = 100}) => _halka.toList().reversed.take(n).toList().reversed.toList();
}
```

- [ ] **Step 4:** Test

```dart
// test/dosya_yardimcisi_test.dart
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:fikoyt_video_uret/cekirdek/dosya_yardimcisi.dart';

void main() {
  test('Aynı dosya iki kez kopyalanmaz', () async {
    final tmp = await Directory.systemTemp.createTemp('fy_test_');
    final kaynak = File('${tmp.path}/k.txt')..writeAsStringSync('merhaba');
    final hedef = Directory('${tmp.path}/hedef');
    final k1 = await DosyaYardimcisi.kopyalaHashli(kaynak, hedef);
    final k2 = await DosyaYardimcisi.kopyalaHashli(kaynak, hedef);
    expect(k1.path, k2.path);
    await tmp.delete(recursive: true);
  });
}
```

Run: `flutter test test/dosya_yardimcisi_test.dart` — PASS.

- [ ] **Step 5:** Commit `git commit -m "Faz 1.2: Kimlik + dosya yardımcısı + log"`

### Task 1.3: KaynakKopyalayici servisi

**Files:**
- Create: `lib/servisler/kaynak_kopyalayici.dart`

- [ ] **Step 1:**

```dart
import 'dart:io';
import 'package:path/path.dart' as p;
import '../cekirdek/dosya_yardimcisi.dart';
import '../modeller/kaynak_dosyalar.dart';

class KaynakKopyalayici {
  /// Verilen kaynak yollarını proje/kaynak klasörüne kopyalar, yeni KaynakDosyalar döner.
  static Future<KaynakDosyalar> projeyeKopyala({
    required String projeKlasoru,
    required String sesPath,
    required String girisGorseliPath,
    String? faceSwapPath,
    String? watermarkPath,
    String? introPath,
    String? outroPath,
    String? lutPath,
    String? srtPath,
  }) async {
    final hedef = Directory(p.join(projeKlasoru, 'kaynak'));
    Future<String> kop(String? path, String prefix) async {
      if (path == null) return '';
      final f = await DosyaYardimcisi.kopyalaHashli(File(path), hedef, prefix: prefix);
      return f.path;
    }
    return KaynakDosyalar(
      sesPath: await kop(sesPath, 'ses'),
      girisGorseliPath: await kop(girisGorseliPath, 'giris'),
      faceSwapPath: faceSwapPath != null ? await kop(faceSwapPath, 'yuz') : null,
      watermarkPath: watermarkPath != null ? await kop(watermarkPath, 'wm') : null,
      introPath: introPath != null ? await kop(introPath, 'intro') : null,
      outroPath: outroPath != null ? await kop(outroPath, 'outro') : null,
      lutPath: lutPath != null ? await kop(lutPath, 'lut') : null,
      srtPath: srtPath != null ? await kop(srtPath, 'srt') : null,
    );
  }

  /// Tek dosya ekle (proje açıkken kaynak güncelleme).
  static Future<String> tekDosyaEkle(String projeKlasoru, String kaynakPath, String prefix) async {
    final hedef = Directory(p.join(projeKlasoru, 'kaynak'));
    final f = await DosyaYardimcisi.kopyalaHashli(File(kaynakPath), hedef, prefix: prefix);
    return f.path;
  }
}
```

- [ ] **Step 2:** Commit `git commit -m "Faz 1.3: KaynakKopyalayici"`

### Task 1.4: ProjeDeposu - Hive + atomic JSON

**Files:**
- Create: `lib/servisler/proje_deposu.dart`
- Create: `lib/durum/proje_durumu.dart`

- [ ] **Step 1:** `lib/servisler/proje_deposu.dart`

```dart
import 'dart:convert';
import 'dart:io';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../modeller/proje.dart';
import '../cekirdek/dosya_yardimcisi.dart';
import '../cekirdek/kimlik.dart';
import '../cekirdek/log.dart';
import '../modeller/kaynak_dosyalar.dart';
import 'kaynak_kopyalayici.dart';

class ProjeOzeti {
  final String id, isim, klasorYolu;
  final DateTime guncellendi;
  final String? thumbPath;
  ProjeOzeti({required this.id, required this.isim, required this.klasorYolu, required this.guncellendi, this.thumbPath});
  Map<String, dynamic> toJson() => {'id': id, 'isim': isim, 'klasorYolu': klasorYolu, 'guncellendi': guncellendi.toIso8601String(), 'thumbPath': thumbPath};
  static ProjeOzeti fromJson(Map j) => ProjeOzeti(id: j['id'], isim: j['isim'], klasorYolu: j['klasorYolu'], guncellendi: DateTime.parse(j['guncellendi']), thumbPath: j['thumbPath']);
}

class ProjeDeposu {
  static Box? _kutu;
  static Future<Box> _box() async => _kutu ??= await Hive.openBox('projeler');

  static Future<Directory> _projelerKok() async {
    final docs = await getApplicationDocumentsDirectory();
    final kok = Directory(p.join(docs.path, 'FikoYT-VideoStudyo', 'projeler'));
    if (!await kok.exists()) await kok.create(recursive: true);
    return kok;
  }

  static Future<List<ProjeOzeti>> sonProjeler() async {
    final b = await _box();
    final liste = <ProjeOzeti>[];
    for (final v in b.values) {
      try { liste.add(ProjeOzeti.fromJson(Map<String, dynamic>.from(v as Map))); } catch (_) {}
    }
    liste.sort((a, b) => b.guncellendi.compareTo(a.guncellendi));
    return liste;
  }

  static Future<Proje> projeOlustur({
    required String isim,
    required String sesPath,
    required String girisGorseliPath,
    String? faceSwapPath,
  }) async {
    final id = FikoYTId.yeni();
    final kok = await _projelerKok();
    final klasor = Directory(p.join(kok.path, '${isim}_$id'.replaceAll(RegExp(r'[^\w-]'), '_')));
    await klasor.create(recursive: true);
    for (final alt in ['kaynak', 'ses_segmentleri', 'kareler', 'segmentler', 'analiz', 'on_izleme', 'cikti']) {
      await Directory(p.join(klasor.path, alt)).create();
    }
    await Log.baslat(File(p.join(klasor.path, 'proje.log')));
    final kaynak = await KaynakKopyalayici.projeyeKopyala(
      projeKlasoru: klasor.path,
      sesPath: sesPath,
      girisGorseliPath: girisGorseliPath,
      faceSwapPath: faceSwapPath,
    );
    final simdi = DateTime.now();
    final proje = Proje(
      id: id, isim: isim,
      olusturmaTarihi: simdi, guncellemeTarihi: simdi,
      klasorYolu: klasor.path,
      kaynak: kaynak,
    );
    await kaydet(proje);
    return proje;
  }

  static Future<void> kaydet(Proje proje) async {
    final yeni = proje.copyWith(guncellemeTarihi: DateTime.now());
    final dosya = File(p.join(yeni.klasorYolu, 'proje.json'));
    await DosyaYardimcisi.atomikYaz(dosya, const JsonEncoder.withIndent('  ').convert(yeni.toJson()));
    final b = await _box();
    await b.put(yeni.id, ProjeOzeti(
      id: yeni.id, isim: yeni.isim, klasorYolu: yeni.klasorYolu, guncellendi: yeni.guncellemeTarihi,
    ).toJson());
    Log.bilgi('ProjeDeposu', 'Kaydedildi: ${yeni.isim}');
  }

  static Future<Proje?> yukle(String klasorYolu) async {
    final dosya = File(p.join(klasorYolu, 'proje.json'));
    if (!await dosya.exists()) return null;
    final j = jsonDecode(await dosya.readAsString()) as Map<String, dynamic>;
    return Proje.fromJson(j);
  }

  static Future<void> sil(String projeId) async {
    final b = await _box();
    final ozet = b.get(projeId);
    if (ozet != null) {
      final klasor = Directory(ozet['klasorYolu']);
      if (await klasor.exists()) await klasor.delete(recursive: true);
      await b.delete(projeId);
    }
  }
}
```

- [ ] **Step 2:** `lib/durum/proje_durumu.dart`

```dart
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../modeller/proje.dart';
import '../servisler/proje_deposu.dart';

class ProjeNotifier extends StateNotifier<Proje?> {
  ProjeNotifier() : super(null);
  Timer? _kayitDebounce;

  void ayarla(Proje p) {
    state = p;
    _kayitDebounce?.cancel();
    _kayitDebounce = Timer(const Duration(milliseconds: 800), () {
      if (state != null) ProjeDeposu.kaydet(state!);
    });
  }

  void guncelle(Proje Function(Proje) fn) {
    if (state != null) ayarla(fn(state!));
  }

  @override
  void dispose() {
    _kayitDebounce?.cancel();
    if (state != null) ProjeDeposu.kaydet(state!);
    super.dispose();
  }
}

final projeProvider = StateNotifierProvider<ProjeNotifier, Proje?>((ref) => ProjeNotifier());
final projelerListesiProvider = FutureProvider((ref) => ProjeDeposu.sonProjeler());
```

- [ ] **Step 3:** Commit `git commit -m "Faz 1.4: ProjeDeposu + Hive + atomic save"`

### Task 1.5: Yeni proje wizard + projeler sayfası

**Files:**
- Modify: `lib/ui/sayfalar/projeler_sayfasi.dart`
- Create: `lib/ui/proje_olustur/yeni_proje_dialog.dart`

- [ ] **Step 1:** `lib/ui/proje_olustur/yeni_proje_dialog.dart` — dosya seçici (file_picker), ses+görsel zorunlu, ses ve resim path'i alır, `ProjeDeposu.projeOlustur` çağırır, yeni projeyi `projeProvider`'a ata, `/studyo/$id`'ye git.

```dart
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../servisler/proje_deposu.dart';
import '../../durum/proje_durumu.dart';

class YeniProjeDialog extends ConsumerStatefulWidget {
  const YeniProjeDialog({super.key});
  @override
  ConsumerState<YeniProjeDialog> createState() => _YeniProjeDialogState();
}

class _YeniProjeDialogState extends ConsumerState<YeniProjeDialog> {
  final _isimCtrl = TextEditingController(text: 'Yeni Proje');
  String? _ses, _gorsel, _yuz;
  bool _olusturuluyor = false;

  Future<void> _seç(String tip) async {
    final r = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: tip == 'ses' ? ['mp3', 'wav', 'flac', 'm4a'] : ['png', 'jpg', 'jpeg', 'webp'],
    );
    if (r != null) setState(() {
      if (tip == 'ses') _ses = r.files.single.path;
      else if (tip == 'gorsel') _gorsel = r.files.single.path;
      else _yuz = r.files.single.path;
    });
  }

  Future<void> _olustur() async {
    if (_ses == null || _gorsel == null) return;
    setState(() => _olusturuluyor = true);
    try {
      final p = await ProjeDeposu.projeOlustur(
        isim: _isimCtrl.text.trim().isEmpty ? 'Yeni Proje' : _isimCtrl.text.trim(),
        sesPath: _ses!, girisGorseliPath: _gorsel!, faceSwapPath: _yuz,
      );
      ref.read(projeProvider.notifier).ayarla(p);
      ref.invalidate(projelerListesiProvider);
      if (mounted) {
        Navigator.of(context).pop();
        context.go('/studyo/${p.id}');
      }
    } finally {
      if (mounted) setState(() => _olusturuluyor = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Yeni Proje'),
      content: SizedBox(
        width: 480,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(controller: _isimCtrl, decoration: const InputDecoration(labelText: 'Proje ismi')),
            const SizedBox(height: 16),
            _DosyaSatiri(etiket: 'Şarkı (mp3/wav)', deger: _ses, sec: () => _seç('ses')),
            _DosyaSatiri(etiket: 'Başlangıç görseli', deger: _gorsel, sec: () => _seç('gorsel')),
            _DosyaSatiri(etiket: 'Yüz (face swap, opsiyonel)', deger: _yuz, sec: () => _seç('yuz')),
          ],
        ),
      ),
      actions: [
        TextButton(onPressed: _olusturuluyor ? null : () => Navigator.pop(context), child: const Text('İptal')),
        FilledButton(
          onPressed: (_ses != null && _gorsel != null && !_olusturuluyor) ? _olustur : null,
          child: _olusturuluyor ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Oluştur'),
        ),
      ],
    );
  }
}

class _DosyaSatiri extends StatelessWidget {
  final String etiket;
  final String? deger;
  final VoidCallback sec;
  const _DosyaSatiri({required this.etiket, this.deger, required this.sec});
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Row(children: [
        Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
          Text(etiket, style: Theme.of(context).textTheme.labelSmall),
          Text(deger ?? '— seçilmedi —', overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12)),
        ])),
        TextButton(onPressed: sec, child: const Text('Seç')),
      ]),
    );
  }
}
```

- [ ] **Step 2:** `lib/ui/sayfalar/projeler_sayfasi.dart`

```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../durum/proje_durumu.dart';
import '../../servisler/proje_deposu.dart';
import '../proje_olustur/yeni_proje_dialog.dart';

class ProjelerSayfasi extends ConsumerWidget {
  const ProjelerSayfasi({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final liste = ref.watch(projelerListesiProvider);
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Row(children: [
          Text('Projeler', style: Theme.of(context).textTheme.headlineMedium),
          const Spacer(),
          FilledButton.icon(
            icon: const Icon(Icons.add),
            label: const Text('Yeni Proje'),
            onPressed: () => showDialog(context: context, builder: (_) => const YeniProjeDialog()),
          ),
        ]),
        const SizedBox(height: 16),
        Expanded(child: liste.when(
          data: (l) => l.isEmpty
            ? const Center(child: Text('Henüz proje yok. "Yeni Proje" ile başla.'))
            : ListView.builder(
                itemCount: l.length,
                itemBuilder: (ctx, i) {
                  final o = l[i];
                  return Card(
                    child: ListTile(
                      leading: const Icon(Icons.movie),
                      title: Text(o.isim),
                      subtitle: Text('${o.klasorYolu}\nGüncelleme: ${o.guncellendi.toLocal()}'),
                      isThreeLine: true,
                      onTap: () async {
                        final p = await ProjeDeposu.yukle(o.klasorYolu);
                        if (p != null) {
                          ref.read(projeProvider.notifier).ayarla(p);
                          if (ctx.mounted) ctx.go('/studyo/${p.id}');
                        }
                      },
                      trailing: IconButton(
                        icon: const Icon(Icons.delete_outline),
                        onPressed: () async {
                          final onay = await showDialog<bool>(context: ctx, builder: (_) => AlertDialog(
                            title: const Text('Projeyi sil?'),
                            content: const Text('Disk klasörü dahil tamamen silinecek.'),
                            actions: [
                              TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('İptal')),
                              FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Sil')),
                            ],
                          ));
                          if (onay == true) {
                            await ProjeDeposu.sil(o.id);
                            ref.invalidate(projelerListesiProvider);
                          }
                        },
                      ),
                    ),
                  );
                },
              ),
          loading: () => const Center(child: CircularProgressIndicator()),
          error: (e, _) => Text('Hata: $e'),
        )),
      ]),
    );
  }
}
```

- [ ] **Step 3:** `flutter run -d windows` → "Yeni Proje" dialog'u test et, ses+görsel seç, oluştur. Kontrol: kullanıcı Documents/FikoYT-VideoStudyo/projeler/ altında klasör + proje.json + kaynak/ kopyaları.

- [ ] **Step 4:** Commit `git commit -m "Faz 1.5: Yeni proje wizard + projeler sayfası"`

---

## Faz 2: LTX Bağlantısı (1 gün)

### Task 2.1: LTX port keşif servisi

**Files:**
- Create: `lib/servisler/ltx/ltx_port_kesfedici.dart`

- [ ] **Step 1:**

```dart
import 'dart:io';
import '../../cekirdek/log.dart';

class LtxPortKesfedici {
  /// netstat + tasklist ile ltx2_server.py PID'inin dinlediği portu bul.
  static Future<int?> portuBul() async {
    try {
      // 1. ltx2_server.py PID'lerini bul
      final wmic = await Process.run('wmic', [
        'process', 'where', "CommandLine like '%ltx2_server.py%'",
        'get', 'ProcessId', '/format:list',
      ]);
      final pidler = <int>[];
      for (final s in wmic.stdout.toString().split('\n')) {
        final t = s.trim();
        if (t.startsWith('ProcessId=')) {
          final v = int.tryParse(t.split('=')[1]);
          if (v != null) pidler.add(v);
        }
      }
      if (pidler.isEmpty) {
        Log.uyari('LTX', 'ltx2_server.py süreci bulunamadı');
        return null;
      }
      // 2. netstat ile dinlenen port
      final ns = await Process.run('netstat', ['-ano']);
      for (final s in ns.stdout.toString().split('\n')) {
        final t = s.trim();
        if (t.contains('LISTENING')) {
          for (final pid in pidler) {
            if (t.endsWith(' $pid') || t.contains(' $pid ') || t.contains('\t$pid')) {
              final parts = t.split(RegExp(r'\s+'));
              if (parts.length >= 2) {
                final adr = parts[1];
                if (adr.contains(':')) {
                  final port = int.tryParse(adr.split(':').last);
                  if (port != null && port > 1024) {
                    Log.bilgi('LTX', 'Port bulundu: $port (PID $pid)');
                    return port;
                  }
                }
              }
            }
          }
        }
      }
    } catch (e) {
      Log.hata('LTX', 'Port keşif hatası: $e');
    }
    // 3. fallback: bilinen portları tara
    for (final p in [57835, 62106, 62725, 8000, 8080]) {
      try {
        final s = await Socket.connect('127.0.0.1', p, timeout: const Duration(milliseconds: 300));
        s.destroy();
        Log.bilgi('LTX', 'Port bulundu (tarama): $p');
        return p;
      } catch (_) {}
    }
    return null;
  }
}
```

- [ ] **Step 2:** Commit `git commit -m "Faz 2.1: LTX port keşfi"`

### Task 2.2: Token yönetici + LtxClient

**Files:**
- Create: `lib/servisler/ltx/ltx_token_yonetici.dart`
- Create: `lib/servisler/ltx/ltx_client.dart`

- [ ] **Step 1:** `lib/servisler/ltx/ltx_token_yonetici.dart`

```dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../cekirdek/log.dart';

class LtxTokenYonetici {
  static Future<File> _tokenDosyasi() async {
    final docs = await getApplicationDocumentsDirectory();
    final dir = Directory(p.join(docs.path, 'FikoYT-VideoStudyo'));
    if (!await dir.exists()) await dir.create(recursive: true);
    return File(p.join(dir.path, 'ltx_token.txt'));
  }

  static Future<String?> kaydedilenOku() async {
    final f = await _tokenDosyasi();
    if (await f.exists()) return (await f.readAsString()).trim();
    return null;
  }

  static Future<void> kaydet(String token) async {
    final f = await _tokenDosyasi();
    await f.writeAsString(token.trim());
  }

  /// Token'ın geçerli olup olmadığını /api/generation/progress ile kontrol.
  static Future<bool> dogrula(String url, String? token) async {
    final dio = Dio(BaseOptions(connectTimeout: const Duration(seconds: 5)));
    try {
      final r = await dio.get('$url/api/generation/progress',
        options: Options(headers: token != null ? {'Authorization': 'Bearer $token'} : null, validateStatus: (_) => true),
      );
      if (r.statusCode == 200) return true;
      if (r.statusCode == 401) return false;
    } catch (e) {
      Log.uyari('LTX', 'Doğrulama hatası: $e');
    }
    return false;
  }
}
```

- [ ] **Step 2:** `lib/servisler/ltx/ltx_client.dart`

```dart
import 'package:dio/dio.dart';
import '../../cekirdek/log.dart';

class LtxStatus {
  final String durum;     // idle/running/complete/error/cancelled
  final String faz;
  final double ilerleme;  // 0-100
  LtxStatus({required this.durum, required this.faz, required this.ilerleme});
  static LtxStatus fromJson(Map j) => LtxStatus(
    durum: j['status'] ?? 'unknown',
    faz: j['phase'] ?? '',
    ilerleme: (j['progress'] ?? 0).toDouble(),
  );
}

class LtxGenerateResult {
  final String durum;
  final String? videoPath;
  LtxGenerateResult({required this.durum, this.videoPath});
}

class LtxClient {
  final String url;
  final String? token;
  late final Dio _dio;
  LtxClient({required this.url, this.token}) {
    _dio = Dio(BaseOptions(
      baseUrl: url,
      connectTimeout: const Duration(seconds: 10),
      headers: {
        'Content-Type': 'application/json',
        if (token != null) 'Authorization': 'Bearer $token',
      },
    ));
  }

  Future<LtxStatus> ilerleme() async {
    final r = await _dio.get('/api/generation/progress');
    return LtxStatus.fromJson(r.data);
  }

  Future<LtxGenerateResult> uret({
    required String prompt,
    required String imagePath,
    required String audioPath,
    required String model,           // fast/pro
    required String duration,        // "5"/"6"/"8"/"10"/"20"
    required String resolution,      // 540p/720p/...
    required String aspectRatio,     // 16:9
    required String fps,             // "24"
    required String cameraMotion,    // "none"
    required bool audioAcik,
    String? negativePrompt,
    double? guidanceScale,
    int? numInferenceSteps,
  }) async {
    final body = {
      'prompt': prompt,
      'model': model,
      'duration': duration,
      'resolution': resolution,
      'fps': fps,
      'audio': audioAcik ? 'true' : 'false',
      'cameraMotion': cameraMotion,
      'aspectRatio': aspectRatio,
      'imagePath': imagePath,
      if (audioAcik) 'audioPath': audioPath,
      if (negativePrompt != null) 'negativePrompt': negativePrompt,
      if (guidanceScale != null) 'guidanceScale': guidanceScale,
      if (numInferenceSteps != null) 'numInferenceSteps': numInferenceSteps,
    };
    Log.bilgi('LtxClient', 'Üretim isteği: $duration s, $resolution, model=$model');
    final r = await _dio.post('/api/generate', data: body, options: Options(receiveTimeout: const Duration(minutes: 20)));
    return LtxGenerateResult(
      durum: r.data['status'] ?? '',
      videoPath: r.data['video_path'],
    );
  }

  Future<bool> idleBekle({Duration zaman = const Duration(minutes: 5)}) async {
    final son = DateTime.now().add(zaman);
    while (DateTime.now().isBefore(son)) {
      final s = await ilerleme();
      if (['idle', 'complete', 'error', 'cancelled'].contains(s.durum)) return true;
      await Future.delayed(const Duration(seconds: 2));
    }
    return false;
  }
}
```

- [ ] **Step 3:** Commit `git commit -m "Faz 2.2: LtxClient + token yönetici"`

### Task 2.3: LTX bağlantı paneli (Ayarlar sayfası)

**Files:**
- Modify: `lib/ui/sayfalar/ayarlar_sayfasi.dart`

`AyarlarSayfasi`'na "LTX Bağlantı" kartı ekle: "Bağlantıyı Test Et" butonu → port keşfi + token doğrulama → durum metni. Token geçersizse `TextField` ile yapıştır + kaydet.

- [ ] **Step 1:** Implement, çalıştır, gerçek LTX Desktop açıkken test et.

- [ ] **Step 2:** Commit `git commit -m "Faz 2.3: LTX bağlantı paneli"`

---

## Faz 3: ffmpeg Servisleri (1-2 gün)

### Task 3.1: FfmpegServis - probe/split/concat

**Files:**
- Create: `lib/servisler/ffmpeg/ffmpeg_servis.dart`

- [ ] **Step 1:**

```dart
import 'dart:io';
import 'package:path/path.dart' as p;
import '../../cekirdek/log.dart';

class FfmpegServis {
  static String ffmpegYolu = 'ffmpeg';
  static String ffprobeYolu = 'ffprobe';

  static Future<double> sesSuresi(String yol) async {
    final r = await Process.run(ffprobeYolu, [
      '-v', 'error', '-show_entries', 'format=duration',
      '-of', 'default=noprint_wrappers=1:nokey=1', yol,
    ]);
    return double.parse((r.stdout as String).trim());
  }

  static Future<void> sesParcala({
    required String girisYolu,
    required String hedefKlasor,
    required double parcaSure,
    required double toplamSure,
  }) async {
    int sayi = (toplamSure ~/ parcaSure);
    final kalan = toplamSure - (sayi * parcaSure);
    if (kalan >= 5.0) sayi += 1;
    await Directory(hedefKlasor).create(recursive: true);
    for (int i = 0; i < sayi; i++) {
      final basla = i * parcaSure;
      final bit = (i + 1) * parcaSure > toplamSure ? toplamSure : (i + 1) * parcaSure;
      final sure = bit - basla;
      final cikis = p.join(hedefKlasor, 'audio_segment_${i.toString().padLeft(3, '0')}.mp3');
      final r = await Process.run(ffmpegYolu, [
        '-y', '-i', girisYolu, '-ss', basla.toString(), '-t', sure.toString(),
        '-acodec', 'libmp3lame', '-b:a', '192k', cikis,
      ]);
      if (r.exitCode != 0) {
        Log.hata('Ffmpeg', 'sesParcala segment $i hata: ${r.stderr}');
        throw Exception('ffmpeg sesParcala başarısız: segment $i');
      }
    }
    Log.bilgi('Ffmpeg', '$sayi ses parçası oluşturuldu');
  }

  static Future<void> videolariBirlestir(List<String> videolar, String cikis) async {
    final tmp = File(p.join(p.dirname(cikis), '_concat_${DateTime.now().millisecondsSinceEpoch}.txt'));
    await tmp.writeAsString(videolar.map((v) => "file '${v.replaceAll("'", "'\\''")}'").join('\n'));
    final r = await Process.run(ffmpegYolu, [
      '-y', '-f', 'concat', '-safe', '0', '-i', tmp.path, '-c', 'copy', cikis,
    ]);
    await tmp.delete();
    if (r.exitCode != 0) throw Exception('Birleştirme hatası: ${r.stderr}');
  }

  static Future<void> sesEkle(String videoYolu, String sesYolu, String cikis) async {
    final r = await Process.run(ffmpegYolu, [
      '-y', '-i', videoYolu, '-i', sesYolu,
      '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k',
      '-map', '0:v:0', '-map', '1:a:0', '-shortest', cikis,
    ]);
    if (r.exitCode != 0) throw Exception('sesEkle hatası: ${r.stderr}');
  }
}
```

- [ ] **Step 2:** Commit `git commit -m "Faz 3.1: FfmpegServis temel ops"`

### Task 3.2: KareCikarici - son kare + Laplacian

**Files:**
- Create: `lib/servisler/ffmpeg/kare_cikarici.dart`

- [ ] **Step 1:**

```dart
import 'dart:io';
import 'dart:typed_data';
import 'package:path/path.dart' as p;
import 'ffmpeg_servis.dart';
import '../../cekirdek/log.dart';

class KareCikarici {
  /// Son ~num_candidates kareyi çıkarıp Laplacian variance ile en keskinini seçer.
  static Future<String> enKesinKare({
    required String videoYolu,
    required String hedefYolu,
    int adaySayisi = 8,
    int sondanAtla = 2,
  }) async {
    // Süre + fps al
    final pr = await Process.run(FfmpegServis.ffprobeYolu, [
      '-v', 'error',
      '-show_entries', 'stream=r_frame_rate,duration',
      '-select_streams', 'v:0', '-of', 'default=noprint_wrappers=1', videoYolu,
    ]);
    double sure = 0, fps = 24;
    for (final s in (pr.stdout as String).split('\n')) {
      if (s.startsWith('duration=')) sure = double.tryParse(s.split('=')[1]) ?? 0;
      else if (s.startsWith('r_frame_rate=')) {
        final fr = s.split('=')[1];
        if (fr.contains('/')) {
          final parts = fr.split('/');
          fps = double.parse(parts[0]) / double.parse(parts[1]);
        } else fps = double.parse(fr);
      }
    }
    if (sure <= 0) {
      await _sonKare(videoYolu, hedefYolu, sure: 0);
      return hedefYolu;
    }
    final tmpDir = await Directory.systemTemp.createTemp('en_kesin_');
    final adaylar = <String>[];
    for (int i = 0; i < adaySayisi; i++) {
      final ofs = sondanAtla + adaySayisi - 1 - i;
      final seek = (sure - (ofs / fps) - 0.01).clamp(0, sure);
      final ay = p.join(tmpDir.path, 'aday_${i.toString().padLeft(2, '0')}.jpg');
      final r = await Process.run(FfmpegServis.ffmpegYolu, [
        '-y', '-ss', seek.toString(), '-i', videoYolu, '-frames:v', '1', '-q:v', '1', ay,
      ]);
      if (r.exitCode == 0 && await File(ay).exists()) adaylar.add(ay);
    }
    if (adaylar.isEmpty) {
      await tmpDir.delete(recursive: true);
      await _sonKare(videoYolu, hedefYolu, sure: sure);
      return hedefYolu;
    }
    String enIyi = adaylar.last;
    double enYuksekSkor = -1;
    for (final ay in adaylar) {
      final skor = await _laplacianSkor(ay);
      if (skor > enYuksekSkor) { enYuksekSkor = skor; enIyi = ay; }
    }
    await File(enIyi).copy(hedefYolu);
    await tmpDir.delete(recursive: true);
    Log.bilgi('Kare', 'En keskin kare seçildi (skor: ${enYuksekSkor.toStringAsFixed(0)})');
    return hedefYolu;
  }

  static Future<void> _sonKare(String video, String cikis, {required double sure}) async {
    final seek = (sure - 0.05).clamp(0, double.infinity);
    await Process.run(FfmpegServis.ffmpegYolu, [
      '-y', '-ss', seek.toString(), '-i', video, '-frames:v', '1', '-q:v', '1', cikis,
    ]);
  }

  static Future<double> _laplacianSkor(String resimYolu) async {
    final pr = await Process.start(FfmpegServis.ffmpegYolu, [
      '-y', '-i', resimYolu, '-pix_fmt', 'gray', '-f', 'rawvideo', 'pipe:1',
    ]);
    final bytes = BytesBuilder();
    pr.stdout.listen(bytes.add);
    await pr.exitCode;
    final raw = bytes.toBytes();
    if (raw.length < 100) return 0;
    int toplam = 0, sayim = 0;
    for (int j = 0; j < raw.length - 1; j++) {
      final d = (raw[j] - raw[j + 1]).abs();
      toplam += d * d;
      sayim++;
    }
    return sayim > 0 ? toplam / sayim : 0;
  }
}
```

- [ ] **Step 2:** Commit `git commit -m "Faz 3.2: KareCikarici Laplacian"`

### Task 3.3: FfmpegFiltre - xfade/loudnorm/lut/overlay

**Files:**
- Create: `lib/servisler/ffmpeg/ffmpeg_filtre.dart`

Karmaşık filter_complex builder. Bu task büyük, alt-adımları implementation sırasında detaylandırılır:

- xfade geçişi: `xfade=transition=fade:duration=0.5:offset=N`
- loudnorm: `-af loudnorm=I=-14:TP=-1.5:LRA=11`
- LUT: `lut3d=file=path.cube`
- watermark: `[1:v]format=rgba,colorchannelmixer=aa=0.5[wm];[0:v][wm]overlay=W-w-20:20`
- altyazı: `subtitles=path.srt:force_style='FontName=Inter,FontSize=28'`

- [ ] **Step 1-N:** Implementation. Test: küçük örnek video + watermark uygulayıp output kontrol.

- [ ] **Final Step:** Commit `git commit -m "Faz 3.3: FfmpegFiltre kompozisyonu"`

---

## Faz 4: Job Orchestrator (2 gün)

### Task 4.1: Job veri persistence + kuyruk

**Files:**
- Create: `lib/servisler/job_orkestratoru.dart`
- Create: `lib/durum/job_durumu.dart`

Hive kutusu `joblar` (key: jobId, value: JSON). Her enqueue/update'de yaz.
- `enqueue(Job)` — kuyruğa ekle, event yay
- `aktifJob()` — durum=calisiyor olanı dön
- `bootstrap()` — açılışta calisiyor job'ları kuyruk'a geri at
- `iptal(jobId)` — cancelToken trip
- `event stream` — UI subscribe

### Task 4.2: SegmentUret job tipi

LtxClient + KareCikarici akışı. Her ilerleme adımında job.ilerleme update + Hive flush.

### Task 4.3: TopluUret zincir job

Tüm segmentleri sırayla SegmentUret + EnIyiKareyiCikar + (opt) FaceSwap → bir sonraki segmentin imagePath'i.

### Task 4.4: Multi-LTX-instance slot havuzu

Periyodik (5sn) netstat tarama → slot ekle/kaldır. Toggle ile etkin.

Detaylı kod implementation sırasında yazılır (Faz 3 kadar uzun ama benzer yapı).

---

## Faz 5: Studio Görünümü (3 gün)

### Task 5.1: Segment listesi widget'ı

`ReorderableListView` + segment kartı (mini thumb + durum badge + grup renk şeridi).

### Task 5.2: Inspector paneli + tüm field'lar

Her field tek dosya: `prompt_alani.dart`, `sure_secici.dart`, `cfg_slider.dart`, `cozunurluk_secici.dart`, `oran_secici.dart`, `model_secici.dart`, `kamera_secici.dart`, `ses_kontrol.dart`, `seed_alani.dart`, `negatif_prompt_alani.dart`, `gorsel_override_alani.dart`, `face_swap_kontrol.dart`, `grup_secici.dart`. Her field "global'den miras" placeholder'ı, override edince segment.field set.

### Task 5.3: Önizleme oynatıcı + varyant seçici

`video_player_win` ile mp4 oynat, varyant dropdown.

### Task 5.4: Mini timeline + waveform widget

`fl_chart` ile RMS bar grafiği veya custom CustomPainter, segment ribbon overlay, sürükle ile sınır kaydır.

### Task 5.5: Studio görünümü compose

Sol-orta-sağ layout, hepsini bir araya getir.

### Task 5.6: Toplu üret butonu (Studio header)

`JobOrkestratoru.enqueue(TopluUret)` → tüm üretilmemiş segmentler sıraya girer.

---

## Faz 6-13 (özet — implementation sırasında detaylanır)

- **Faz 6:** Timeline (NLE) ve Storyboard görünümleri (Studio'daki state'i farklı render eder).
- **Faz 7:** SesEnerjisiAnalizci (RMS RDS hesabı, mevcut Python mantığı port), WhisperRunner (whisper.cpp subprocess), BeatDetector (ffprobe + onset).
- **Faz 8:** OllamaClient + 4 katman koordinatör + AI Öneri modali.
- **Faz 9:** FaceFusionRunner + WanGPRunner (`tools/infinitetalk_runner.py`).
- **Faz 10:** Çıkış pipeline'ı (geçiş, ses karışım, loudnorm, LUT, watermark, altyazı, intro/outro, hız rampası, beat-sync, çoklu preset paralel render).
- **Faz 11:** Render sayfası (job tablosu + canlı log + iptal), tray icon, mini render bar.
- **Faz 12:** Geçmiş sayfası (eski render'ları geri yükle), multi-LTX auto-detect.
- **Faz 13:** Polish (animasyonlar, kısayollar, hata UI'ları, crash-recovery test).

Her faz kendi içinde Faz 0-5 tarzında bite-sized task'lara bölünür, implementation sırasında.

---

## Kabul Kriterleri (spec'ten)

- [ ] Bat dosyasındaki tüm parametreler UI'dan ayarlanabiliyor
- [ ] Her segment ayrı prompt + ayrı tüm parametreler alabiliyor
- [ ] Bir segment "ses kapalı" üretebiliyor
- [ ] Tek segment yeniden üretilebiliyor, varyant olarak saklanıyor
- [ ] Uygulama kapanıp açılınca render kaldığı yerden devam ediyor
- [ ] LTX yeniden başlatılınca yeni port/token bulunuyor
- [ ] InfiniteTalk modu çalışıyor
- [ ] Face swap zinciri çalışıyor
- [ ] Çoklu çıkış preset'i tek seferde üretiliyor (paralel)
- [ ] Tray icon arka plan render
- [ ] 4 tema runtime geçiş kayıpsız

## Self-Review Notları

- Spec coverage: tüm 11 kabul kriteri Faz'lara map'lendi.
- Type consistency: tüm model property isimleri Bölüm 1.1'de freezed olarak tek noktada tanımlanıyor.
- Faz 5+ task'ları daha az detayda — bu kasıtlı, kullanıcı CLAUDE.md tercihi gereği UI işlerinde TDD/detay zorlama yapmıyoruz; pipeline core'unda (Faz 0-4) tam detay var.
