Files
ContentGen_BE/media-worker/Services/VideoRenderPipeline.cs
T
Harun CAN 9d8c34b39d
Backend Deploy 🚀 / build-and-deploy (push) Has been cancelled
main
2026-04-25 14:37:46 +02:00

263 lines
11 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 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,
SunoMusicService sunoMusic,
AudioCraftService audioCraft,
RemotionService remotion,
FFmpegService ffmpeg,
S3StorageService s3,
IOptions<FFmpegSettings> ffmpegSettings)
{
_logger = logger;
_higgsField = higgsField;
_tts = tts;
_openAiTts = openAiTts;
_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 — Video render (Ken Burns + Audio Merge + Subtitles)
// ═══════════════════════════════════════
_logger.LogInformation("🎬 Adım 5/6: Remotion render — Ken Burns + audio merge + subtitle");
await progressCallback(75, "MEDIA_MERGE");
var finalLocalPath = await _remotion.RenderVideoAsync(
job.ProjectId, projectDir, scenes, allMediaFiles, musicFile?.LocalPath, 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
{
// 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);
}
}
}