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 OpenAiTtsService _openAiTts;
private readonly MinimaxTtsService _minimaxTts;
private readonly SunoMusicService _sunoMusic;
private readonly AudioCraftService _audioCraft;
private readonly RemotionService _remotion;
private readonly FFmpegService _ffmpeg;
private readonly S3StorageService _s3;
private readonly FFmpegSettings _ffmpegSettings;
public VideoRenderPipeline(
ILogger logger,
HiggsFieldService higgsField,
TtsService tts,
OpenAiTtsService openAiTts,
MinimaxTtsService minimaxTts,
SunoMusicService sunoMusic,
AudioCraftService audioCraft,
RemotionService remotion,
FFmpegService ffmpeg,
S3StorageService s3,
IOptions ffmpegSettings)
{
_logger = logger;
_higgsField = higgsField;
_tts = tts;
_openAiTts = openAiTts;
_minimaxTts = minimaxTts;
_sunoMusic = sunoMusic;
_audioCraft = audioCraft;
_remotion = remotion;
_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 * 2 + 4; // (tts+ambient per scene) + music + remotion + upload + finalize
var completedSteps = 0;
// ═══════════════════════════════════════
// ADIM 1: Video Klip Üretimi (ATLANDI - REMOTION YAPACAK)
// ═══════════════════════════════════════
_logger.LogInformation("📹 Adım 1: Video üretimi atlandı, görseller doğrudan Remotion'da Ken Burns ile işlenecek.");
// ═══════════════════════════════════════
// 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 = (int)(completedSteps / (double)totalSteps * 40); // TTS %0-40
return progressCallback(progress, "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(45, "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(55, "MUSIC_GENERATION");
// ═══════════════════════════════════════
// ADIM 4: AudioGen — Sahne bazlı ambient sesler
// ═══════════════════════════════════════
_logger.LogInformation("🔊 Adım 4/6: AudioGen ambient ses efektleri");
await progressCallback(60, "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(70, "AMBIENT_GENERATION");
// ═══════════════════════════════════════
// ADIM 5: REMOTION — Segmented Video render + FFmpeg Merge
// ═══════════════════════════════════════
_logger.LogInformation("🎬 Adım 5/6: Remotion Segmented Render — Ken Burns + audio merge + subtitle");
await progressCallback(70, "MEDIA_MERGE");
int chunkSize = 20; // 20 scenes per chunk to prevent OOM
var chunkPaths = new List();
var chunkIndex = 0;
for (int i = 0; i < scenes.Count; i += chunkSize)
{
var chunkScenes = scenes.Skip(i).Take(chunkSize).ToList();
chunkIndex++;
_logger.LogInformation("Render Chunk {ChunkIndex} (Scenes {Start} to {End})",
chunkIndex, i + 1, i + chunkScenes.Count);
// Pass null for musicPath so Remotion doesn't add music to each chunk
var chunkPath = await _remotion.RenderVideoAsync(
$"{job.ProjectId}_chunk_{chunkIndex}",
projectDir,
chunkScenes,
allMediaFiles,
null, // No music per chunk
0,
job.VisualEffect,
ct);
chunkPaths.Add(chunkPath);
var progress = 70 + (int)(20.0 * (i + chunkScenes.Count) / scenes.Count);
await progressCallback(progress, "MEDIA_MERGE");
}
_logger.LogInformation("🎬 Chunklar birleştiriliyor ve müzik ekleniyor (FFmpeg)");
var finalLocalPath = await _ffmpeg.ConcatenateAndFinalize(
chunkPaths,
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(90, "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 GenerateTtsWithProgress(
ScenePayload scene, string outputDir, string voiceStyle,
Func onComplete, CancellationToken ct)
{
GeneratedMediaFile result;
if (!string.IsNullOrEmpty(scene.TtsProvider) && scene.TtsProvider.Equals("openai", StringComparison.OrdinalIgnoreCase))
{
result = await _openAiTts.GenerateNarrationAsync(scene, outputDir, voiceStyle, ct);
}
else if (!string.IsNullOrEmpty(scene.TtsProvider) && scene.TtsProvider.Equals("minimax", StringComparison.OrdinalIgnoreCase))
{
result = await _minimaxTts.GenerateNarrationAsync(scene, outputDir, voiceStyle, ct);
}
else
{
// Default: ElevenLabs
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);
}
}
}