Files
ContentGen_BE/media-worker/Services/VideoRenderPipeline.cs
Harun CAN 85c35c73e8
Some checks failed
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled
main
2026-03-29 12:43:49 +03:00

324 lines
13 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 SunoMusicService _sunoMusic;
private readonly AudioCraftService _audioCraft;
private readonly FFmpegService _ffmpeg;
private readonly S3StorageService _s3;
private readonly FFmpegSettings _ffmpegSettings;
public VideoRenderPipeline(
ILogger<VideoRenderPipeline> logger,
HiggsFieldService higgsField,
TtsService tts,
SunoMusicService sunoMusic,
AudioCraftService audioCraft,
FFmpegService ffmpeg,
S3StorageService s3,
IOptions<FFmpegSettings> ffmpegSettings)
{
_logger = logger;
_higgsField = higgsField;
_tts = tts;
_sunoMusic = sunoMusic;
_audioCraft = audioCraft;
_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 * 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<Task<GeneratedMediaFile>>();
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<Task<GeneratedMediaFile>>();
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<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(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<string>();
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<GeneratedMediaFile> GenerateVideoClipWithProgress(
ScenePayload scene, string outputDir, string aspectRatio,
Func<Task> onComplete, CancellationToken ct)
{
var result = await _higgsField.GenerateVideoClipAsync(
scene, outputDir, aspectRatio, ct);
await onComplete();
return result;
}
private async Task<GeneratedMediaFile> GenerateTtsWithProgress(
ScenePayload scene, string outputDir, string voiceStyle,
Func<Task> 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);
}
}
}