using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SaasMediaWorker.Configuration; using SaasMediaWorker.Models; namespace SaasMediaWorker.Services; /// /// Suno AI API Client — Background müzik üretimi. /// Projenin musicPrompt'unu kullanarak AI müzik üretir. /// public class SunoMusicService { private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly ApiSettings _settings; public SunoMusicService( HttpClient httpClient, ILogger logger, IOptions settings) { _httpClient = httpClient; _logger = logger; _settings = settings.Value; _httpClient.BaseAddress = new Uri(_settings.SunoBaseUrl); _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_settings.SunoApiKey}"); _httpClient.Timeout = TimeSpan.FromMinutes(5); } /// /// Proje için background müzik üretir ve dosyaya kaydeder. /// public async Task GenerateMusicAsync( string musicPrompt, int targetDurationSeconds, string outputDirectory, CancellationToken ct) { _logger.LogInformation( "🎵 Müzik üretimi — Prompt: \"{Prompt}\", Süre: {Duration}s", musicPrompt[..Math.Min(80, musicPrompt.Length)], targetDurationSeconds); var requestBody = new { prompt = musicPrompt, duration = Math.Min(120, targetDurationSeconds + 5), // Biraz fazla üret (fade-out için) make_instrumental = true, // Vokal olmadan model = "chirp-v3" }; var content = new StringContent( JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync("/generations", content, ct); response.EnsureSuccessStatusCode(); var responseJson = await response.Content.ReadAsStringAsync(ct); var result = JsonSerializer.Deserialize(responseJson); // Generation ID al var generationId = result.GetProperty("id").GetString() ?? throw new InvalidOperationException("Suno generation ID alınamadı"); _logger.LogInformation("Suno generation başlatıldı: {Id}", generationId); // Polling — müzik hazır olana kadar bekle var musicUrl = await PollForCompletionAsync(generationId, ct); // Müzik dosyasını indir var outputPath = Path.Combine(outputDirectory, "background_music.mp3"); await DownloadFileAsync(musicUrl, outputPath, ct); var fileInfo = new FileInfo(outputPath); _logger.LogInformation("Müzik indirildi: {Path} ({Size} bytes)", outputPath, fileInfo.Length); return new GeneratedMediaFile { SceneId = string.Empty, // Proje seviyesi — sahneye bağlı değil SceneOrder = 0, Type = MediaFileType.AudioMusic, LocalPath = outputPath, FileSizeBytes = fileInfo.Length, DurationSeconds = targetDurationSeconds, MimeType = "audio/mpeg", AiProvider = "suno" }; } private async Task PollForCompletionAsync(string generationId, CancellationToken ct) { var maxAttempts = 60; // 5 dakika (5s × 60) for (var i = 0; i < maxAttempts; i++) { ct.ThrowIfCancellationRequested(); await Task.Delay(5000, ct); var response = await _httpClient.GetAsync($"/generations/{generationId}", ct); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(ct); var result = JsonSerializer.Deserialize(json); var status = result.GetProperty("status").GetString(); if (status == "completed" || status == "complete") { return result.GetProperty("audio_url").GetString() ?? throw new InvalidOperationException("Müzik URL bulunamadı"); } if (status == "failed" || status == "error") { var errorMsg = result.TryGetProperty("error", out var err) ? err.GetString() : "Bilinmeyen hata"; throw new InvalidOperationException($"Suno müzik üretimi başarısız: {errorMsg}"); } } throw new TimeoutException($"Suno müzik üretimi zaman aşımına uğradı: {generationId}"); } private async Task DownloadFileAsync(string url, string outputPath, CancellationToken ct) { using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct); response.EnsureSuccessStatusCode(); await using var fileStream = File.Create(outputPath); await response.Content.CopyToAsync(fileStream, ct); } }