using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SaasMediaWorker.Configuration; using SaasMediaWorker.Models; namespace SaasMediaWorker.Services; /// /// Video render orchestrator — Tüm medya üretim ve birleştirme pipeline'ını yönetir. /// /// Pipeline Adımları: /// 1. Temp dizin oluştur /// 2. Her sahne için: Higgsfield (video klip) + TTS (narration) /// 3. AudioCraft MusicGen (background müzik) — fallback: Suno /// 4. AudioCraft AudioGen (sahne bazlı ambient sesler) /// 5. Her sahne için: video + narration + ambient merge + altyazı /// 6. Tüm sahneleri birleştir + müzik mix (3 katmanlı ses) /// 7. S3'e yükle /// 8. Temp dizini temizle /// public class VideoRenderPipeline { private readonly ILogger _logger; private readonly HiggsFieldService _higgsField; private readonly TtsService _tts; private readonly SunoMusicService _sunoMusic; private readonly AudioCraftService _audioCraft; private readonly FFmpegService _ffmpeg; private readonly S3StorageService _s3; private readonly FFmpegSettings _ffmpegSettings; public VideoRenderPipeline( ILogger logger, HiggsFieldService higgsField, TtsService tts, SunoMusicService sunoMusic, AudioCraftService audioCraft, FFmpegService ffmpeg, S3StorageService s3, IOptions ffmpegSettings) { _logger = logger; _higgsField = higgsField; _tts = tts; _sunoMusic = sunoMusic; _audioCraft = audioCraft; _ffmpeg = ffmpeg; _s3 = s3; _ffmpegSettings = ffmpegSettings.Value; } /// /// Tam render pipeline'ını çalıştırır. /// Giriş: VideoGenerationJob (Redis'ten alınan) /// Çıkış: Final video'nun S3 URL'i /// public async Task ExecuteAsync( VideoGenerationJob job, Func progressCallback, CancellationToken ct) { var projectDir = Path.Combine(_ffmpegSettings.TempDirectory, job.ProjectId); Directory.CreateDirectory(projectDir); var allMediaFiles = new List(); try { var scenes = job.Scenes.OrderBy(s => s.Order).ToList(); var totalSteps = scenes.Count * 3 + 4; // (video+tts+ambient per scene) + music + merge + upload + finalize var completedSteps = 0; // ═══════════════════════════════════════ // ADIM 1: Her sahne için video klip üret // ═══════════════════════════════════════ _logger.LogInformation( "📹 Adım 1/5: Video klip üretimi — {Count} sahne", scenes.Count); var videoTasks = new List>(); foreach (var scene in scenes) { videoTasks.Add(GenerateVideoClipWithProgress( scene, projectDir, job.AspectRatio, () => { completedSteps++; var progress = (int)(completedSteps / (double)totalSteps * 60); // Video %0-60 return progressCallback(Math.Min(progress, 60), "VIDEO_GENERATION"); }, ct)); } var videoResults = await Task.WhenAll(videoTasks); allMediaFiles.AddRange(videoResults); // ═══════════════════════════════════════ // ADIM 2: Her sahne için TTS narration üret // ═══════════════════════════════════════ _logger.LogInformation("🎙️ Adım 2/5: TTS narration üretimi"); var voiceStyle = job.ScriptJson?.VoiceStyle ?? "Deep authoritative male voice"; var ttsTasks = new List>(); foreach (var scene in scenes) { ttsTasks.Add(GenerateTtsWithProgress( scene, projectDir, voiceStyle, () => { completedSteps++; var progress = 60 + (int)(completedSteps / (double)totalSteps * 10); // TTS %60-70 return progressCallback(Math.Min(progress, 70), "TTS_GENERATION"); }, ct)); } var ttsResults = await Task.WhenAll(ttsTasks); allMediaFiles.AddRange(ttsResults); // ═══════════════════════════════════════ // ADIM 3: Background müzik üret // ═══════════════════════════════════════ _logger.LogInformation("🎵 Adım 3/6: Background müzik üretimi (AudioCraft MusicGen)"); await progressCallback(72, "MUSIC_GENERATION"); var musicPrompt = job.ScriptJson?.MusicPrompt ?? "Cinematic orchestral, mysterious, slow build, 80 BPM, strings and piano"; // Teknik parametreleri ScriptPayload'dan al MusicTechnicalParams? musicTechnical = null; if (job.ScriptJson?.MusicTechnical != null) { musicTechnical = new MusicTechnicalParams { Bpm = job.ScriptJson.MusicTechnical.Bpm, Key = job.ScriptJson.MusicTechnical.Key, Instruments = job.ScriptJson.MusicTechnical.Instruments, EmotionalArc = job.ScriptJson.MusicTechnical.EmotionalArc }; } GeneratedMediaFile? musicFile = null; // Önce AudioCraft MusicGen dene, başarısız olursa Suno fallback try { musicFile = await _audioCraft.GenerateMusicAsync( musicPrompt, musicTechnical, job.TargetDuration, projectDir, ct); allMediaFiles.Add(musicFile); _logger.LogInformation("✅ MusicGen başarılı — AudioCraft ile müzik üretildi"); } catch (Exception ex) { _logger.LogWarning(ex, "MusicGen başarısız — Suno fallback deneniyor"); try { musicFile = await _sunoMusic.GenerateMusicAsync( musicPrompt, job.TargetDuration, projectDir, ct); allMediaFiles.Add(musicFile); _logger.LogInformation("✅ Suno fallback başarılı"); } catch (Exception sunoEx) { _logger.LogWarning(sunoEx, "Suno da başarısız — müziksiz devam ediliyor"); } } await progressCallback(78, "MUSIC_GENERATION"); // ═══════════════════════════════════════ // ADIM 4: AudioGen — Sahne bazlı ambient sesler // ═══════════════════════════════════════ _logger.LogInformation("🔊 Adım 4/6: AudioGen ambient ses efektleri"); await progressCallback(79, "AMBIENT_GENERATION"); var ambientFiles = new List(); try { ambientFiles = await _audioCraft.GenerateAllAmbientSoundsAsync( scenes, projectDir, ct); allMediaFiles.AddRange(ambientFiles); } catch (Exception ex) { _logger.LogWarning(ex, "Ambient ses üretimi başarısız — ambientsiz devam ediliyor"); } await progressCallback(82, "AMBIENT_GENERATION"); // ═══════════════════════════════════════ // ADIM 5: FFmpeg — Birleştirme, ambient overlay ve altyazı // ═══════════════════════════════════════ _logger.LogInformation("🎬 Adım 5/6: FFmpeg render — merge + ambient + subtitle + finalize"); await progressCallback(83, "MEDIA_MERGE"); var mergedScenePaths = new List(); foreach (var scene in scenes) { var videoFile = videoResults.FirstOrDefault(v => v.SceneOrder == scene.Order && v.Type == MediaFileType.VideoClip); var ttsFile = ttsResults.FirstOrDefault(t => t.SceneOrder == scene.Order && t.Type == MediaFileType.AudioNarration); if (videoFile == null) { _logger.LogWarning("Sahne {Order} için video bulunamadı, atlanıyor", scene.Order); continue; } var currentPath = videoFile.LocalPath; // Video + Narration merge if (ttsFile != null) { currentPath = await _ffmpeg.MergeVideoWithNarrationAsync( currentPath, ttsFile.LocalPath, scene.Order, projectDir, ct); } // Ambient ses overlay (AudioGen) var ambientFile = ambientFiles.FirstOrDefault(a => a.SceneOrder == scene.Order && a.Type == MediaFileType.AudioAmbient); if (ambientFile != null) { currentPath = await _ffmpeg.OverlayAmbientAudioAsync( currentPath, ambientFile.LocalPath, scene.Order, projectDir, ct); } // Altyazı ekle if (!string.IsNullOrEmpty(scene.SubtitleText)) { currentPath = await _ffmpeg.AddSubtitlesAsync( currentPath, scene.SubtitleText, scene.Order, projectDir, ct); } mergedScenePaths.Add(currentPath); } await progressCallback(88, "MEDIA_MERGE"); // Final concatenation + music mix var finalLocalPath = await _ffmpeg.ConcatenateAndFinalize( mergedScenePaths, musicFile?.LocalPath, projectDir, job.ProjectId, job.TargetDuration, ct); allMediaFiles.Add(new GeneratedMediaFile { Type = MediaFileType.FinalVideo, LocalPath = finalLocalPath, FileSizeBytes = new FileInfo(finalLocalPath).Length, DurationSeconds = job.TargetDuration, MimeType = "video/mp4" }); await progressCallback(92, "MEDIA_MERGE"); // ═══════════════════════════════════════ // ADIM 6: S3'e yükle // ═══════════════════════════════════════ _logger.LogInformation("☁️ Adım 6/6: S3 yükleme"); await progressCallback(94, "UPLOAD"); // Tüm medya dosyalarını yükle await _s3.UploadAllMediaAsync(job.ProjectId, allMediaFiles, ct); // Final videoyu yükle var finalVideoUrl = await _s3.UploadFinalVideoAsync( finalLocalPath, job.ProjectId, ct); await progressCallback(98, "FINALIZATION"); _logger.LogInformation( "🎉 Pipeline tamamlandı — Project: {ProjectId}, URL: {Url}", job.ProjectId, finalVideoUrl); return finalVideoUrl; } finally { // Temp dizini temizle (bellek/disk tasarrufu) CleanupTempDirectory(projectDir); } } private async Task GenerateVideoClipWithProgress( ScenePayload scene, string outputDir, string aspectRatio, Func onComplete, CancellationToken ct) { var result = await _higgsField.GenerateVideoClipAsync( scene, outputDir, aspectRatio, ct); await onComplete(); return result; } private async Task GenerateTtsWithProgress( ScenePayload scene, string outputDir, string voiceStyle, Func onComplete, CancellationToken ct) { var result = await _tts.GenerateNarrationAsync( scene, outputDir, voiceStyle, ct); await onComplete(); return result; } private void CleanupTempDirectory(string path) { try { if (Directory.Exists(path)) { Directory.Delete(path, recursive: true); _logger.LogInformation("Temp dizin temizlendi: {Path}", path); } } catch (Exception ex) { _logger.LogWarning(ex, "Temp dizin temizlenemedi: {Path}", path); } } }