using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SaasMediaWorker.Configuration; using SaasMediaWorker.Models; namespace SaasMediaWorker.Services; /// /// AudioCraft Service — Meta MusicGen + AudioGen entegrasyonu. /// /// HuggingFace Inference API üzerinden çalışır (self-hosted model gerekmez). /// Raspberry Pi'da çalıştırılabilir — model inference cloud'da yapılır. /// /// Yetenekler: /// MusicGen: Text-to-music (müzik üretimi) /// - Genre, BPM, enstrüman, mood bazlı prompt /// - Stereo çıktı desteği /// - 30 saniyeye kadar üretim /// AudioGen: Text-to-sound (ses efekti üretimi) /// - Ortam sesleri: yağmur, rüzgâr, deniz dalgaları /// - Foley sesleri: ayak sesleri, kapı gıcırtısı /// - Sahne bazlı ambient ses katmanları /// /// multimodal-audiocraft skill'inden elde edilen bilgilerle tasarlandı. /// public class AudioCraftService { private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly ApiSettings _settings; // HuggingFace Inference API endpoints private const string MUSICGEN_MODEL = "facebook/musicgen-small"; private const string AUDIOGEN_MODEL = "facebook/audiogen-medium"; private const string HF_API_BASE = "https://api-inference.huggingface.co/models"; public AudioCraftService( HttpClient httpClient, ILogger logger, IOptions settings) { _httpClient = httpClient; _logger = logger; _settings = settings.Value; _httpClient.DefaultRequestHeaders.Add( "Authorization", $"Bearer {_settings.HuggingFaceApiKey}"); _httpClient.Timeout = TimeSpan.FromMinutes(5); } /// /// MusicGen ile text-to-music üretimi. /// Proje için background müzik oluşturur. /// public async Task GenerateMusicAsync( string musicPrompt, MusicTechnicalParams? technicalParams, int targetDurationSeconds, string outputDirectory, CancellationToken ct) { // Prompt'u teknik parametrelerle zenginleştir var enrichedPrompt = EnrichMusicPrompt(musicPrompt, technicalParams); _logger.LogInformation( "🎵 MusicGen müzik üretimi — Prompt: \"{Prompt}\", Süre: {Duration}s", enrichedPrompt[..Math.Min(100, enrichedPrompt.Length)], targetDurationSeconds); var audioBytes = await CallHuggingFaceInference( MUSICGEN_MODEL, enrichedPrompt, ct); if (audioBytes == null || audioBytes.Length == 0) { throw new InvalidOperationException("MusicGen boş yanıt döndü"); } // WAV dosyasını kaydet var outputPath = Path.Combine(outputDirectory, "background_music_audiocraft.wav"); await File.WriteAllBytesAsync(outputPath, audioBytes, ct); var fileInfo = new FileInfo(outputPath); _logger.LogInformation( "✅ MusicGen müzik üretildi: {Path} ({Size:N0} bytes)", outputPath, fileInfo.Length); return new GeneratedMediaFile { SceneId = string.Empty, SceneOrder = 0, Type = MediaFileType.AudioMusic, LocalPath = outputPath, FileSizeBytes = fileInfo.Length, DurationSeconds = targetDurationSeconds, MimeType = "audio/wav", AiProvider = "audiocraft-musicgen" }; } /// /// AudioGen ile sahne bazlı ambient ses efekti üretimi. /// Her sahne için farklı bir ortam sesi oluşturulabilir. /// public async Task GenerateAmbientSoundAsync( string ambientPrompt, int sceneOrder, double durationSeconds, string outputDirectory, CancellationToken ct) { if (string.IsNullOrWhiteSpace(ambientPrompt)) return null; _logger.LogInformation( "🔊 AudioGen ses efekti — Sahne: {Order}, Prompt: \"{Prompt}\"", sceneOrder, ambientPrompt[..Math.Min(80, ambientPrompt.Length)]); try { var audioBytes = await CallHuggingFaceInference( AUDIOGEN_MODEL, ambientPrompt, ct); if (audioBytes == null || audioBytes.Length == 0) { _logger.LogWarning("AudioGen boş yanıt — sahne {Order} için ambient atlanıyor", sceneOrder); return null; } var fileName = $"ambient_scene_{sceneOrder:D3}.wav"; var outputPath = Path.Combine(outputDirectory, fileName); await File.WriteAllBytesAsync(outputPath, audioBytes, ct); _logger.LogInformation( "✅ Ambient ses üretildi: {FileName} ({Size:N0} bytes)", fileName, audioBytes.Length); return new GeneratedMediaFile { SceneId = string.Empty, SceneOrder = sceneOrder, Type = MediaFileType.AudioAmbient, LocalPath = outputPath, FileSizeBytes = audioBytes.Length, DurationSeconds = durationSeconds, MimeType = "audio/wav", AiProvider = "audiocraft-audiogen" }; } catch (Exception ex) { _logger.LogWarning(ex, "Ambient ses üretimi başarısız (sahne {Order}) — devam ediliyor", sceneOrder); return null; } } /// /// Projenin tüm sahneleri için batch ambient ses üretimi. /// Paralel çalışır — Raspberry Pi'da bellek dostu. /// public async Task> GenerateAllAmbientSoundsAsync( List scenes, string outputDirectory, CancellationToken ct) { var results = new List(); // Sıralı üret (HuggingFace rate limit + Pi bellek optimizasyonu) foreach (var scene in scenes) { var ambientPrompt = scene.AmbientSoundPrompt; if (string.IsNullOrWhiteSpace(ambientPrompt)) continue; var result = await GenerateAmbientSoundAsync( ambientPrompt, scene.Order, scene.Duration, outputDirectory, ct); if (result != null) results.Add(result); // Rate limit koruma — 1 saniye bekle await Task.Delay(1000, ct); } _logger.LogInformation( "🔊 Toplam {Count} sahne için ambient ses üretildi", results.Count); return results; } // ── Private: HuggingFace Inference API çağrısı ────────────────── private async Task CallHuggingFaceInference( string modelId, string prompt, CancellationToken ct) { var url = $"{HF_API_BASE}/{modelId}"; var payload = new { inputs = prompt }; var content = new StringContent( JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); var maxRetries = 3; for (var attempt = 1; attempt <= maxRetries; attempt++) { try { var response = await _httpClient.PostAsync(url, content, ct); // Model yükleniyor (cold start) if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable || (int)response.StatusCode == 503) { var json = await response.Content.ReadAsStringAsync(ct); _logger.LogInformation( "Model yükleniyor ({Model}), deneme {Attempt}/{Max}...", modelId, attempt, maxRetries); // Model yüklenme süresi bekleme var waitTime = ExtractEstimatedTime(json); await Task.Delay(TimeSpan.FromSeconds(waitTime), ct); continue; } response.EnsureSuccessStatusCode(); // Audio binary yanıt return await response.Content.ReadAsByteArrayAsync(ct); } catch (HttpRequestException ex) when (attempt < maxRetries) { _logger.LogWarning(ex, "HuggingFace API hatası, deneme {Attempt}/{Max}", attempt, maxRetries); await Task.Delay(3000 * attempt, ct); } } throw new InvalidOperationException( $"HuggingFace API {maxRetries} deneme sonrası başarısız — Model: {modelId}"); } /// /// MusicGen prompt'unu teknik parametrelerle zenginleştirir. /// AudioCraft skill'den öğrenilen best practice'lere göre optimize eder. /// private string EnrichMusicPrompt(string basePrompt, MusicTechnicalParams? technical) { if (technical == null) return basePrompt; var enriched = basePrompt; // BPM ekle (prompt'ta yoksa) if (!enriched.Contains("BPM", StringComparison.OrdinalIgnoreCase) && technical.Bpm > 0) { enriched += $", {technical.Bpm} BPM"; } // Key ekle if (!string.IsNullOrEmpty(technical.Key) && !enriched.Contains(technical.Key, StringComparison.OrdinalIgnoreCase)) { enriched += $", {technical.Key}"; } // Emotional arc ekle if (!string.IsNullOrEmpty(technical.EmotionalArc)) { enriched += $", {technical.EmotionalArc.Replace("-", " ")} energy"; } return enriched; } private int ExtractEstimatedTime(string json) { try { var doc = JsonSerializer.Deserialize(json); if (doc.TryGetProperty("estimated_time", out var time)) return Math.Max(10, (int)time.GetDouble()); } catch { } return 20; // Default: 20 saniye bekle } } /// /// MusicGen teknik parametreleri — AI senaryo çıktısından parse edilir. /// public class MusicTechnicalParams { public int Bpm { get; set; } public string? Key { get; set; } public List Instruments { get; set; } = new(); public string EmotionalArc { get; set; } = string.Empty; }