generated from fahricansecer/boilerplate-be
This commit is contained in:
323
media-worker/Services/VideoRenderPipeline.cs
Normal file
323
media-worker/Services/VideoRenderPipeline.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user