Files
ContentGen_BE/media-worker/Services/VideoRenderPipeline.cs
T
Harun CAN a40619ef33
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled
main
2026-05-06 10:48:07 +02:00

305 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SaasMediaWorker.Configuration;
using SaasMediaWorker.Models;
namespace SaasMediaWorker.Services;
/// <summary>
/// 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
/// </summary>
public class VideoRenderPipeline
{
private readonly ILogger<VideoRenderPipeline> _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<VideoRenderPipeline> logger,
HiggsFieldService higgsField,
TtsService tts,
OpenAiTtsService openAiTts,
MinimaxTtsService minimaxTts,
SunoMusicService sunoMusic,
AudioCraftService audioCraft,
RemotionService remotion,
FFmpegService ffmpeg,
S3StorageService s3,
IOptions<FFmpegSettings> ffmpegSettings)
{
_logger = logger;
_higgsField = higgsField;
_tts = tts;
_openAiTts = openAiTts;
_minimaxTts = minimaxTts;
_sunoMusic = sunoMusic;
_audioCraft = audioCraft;
_remotion = remotion;
_ffmpeg = ffmpeg;
_s3 = s3;
_ffmpegSettings = ffmpegSettings.Value;
}
/// <summary>
/// Tam render pipeline'ını çalıştırır.
/// Giriş: VideoGenerationJob (Redis'ten alınan)
/// Çıkış: Final video'nun S3 URL'i
/// </summary>
public async Task<string> ExecuteAsync(
VideoGenerationJob job,
Func<int, string, Task> progressCallback,
CancellationToken ct)
{
var projectDir = Path.Combine(_ffmpegSettings.TempDirectory, job.ProjectId);
Directory.CreateDirectory(projectDir);
var allMediaFiles = new List<GeneratedMediaFile>();
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<Task<GeneratedMediaFile>>();
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<GeneratedMediaFile>();
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<string>();
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<GeneratedMediaFile> GenerateTtsWithProgress(
ScenePayload scene, string outputDir, string voiceStyle,
Func<Task> 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);
}
}
}