generated from fahricansecer/boilerplate-be
303 lines
10 KiB
C#
303 lines
10 KiB
C#
using System.Text;
|
||
using System.Text.Json;
|
||
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
using SaasMediaWorker.Configuration;
|
||
using SaasMediaWorker.Models;
|
||
|
||
namespace SaasMediaWorker.Services;
|
||
|
||
/// <summary>
|
||
/// 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ı.
|
||
/// </summary>
|
||
public class AudioCraftService
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
private readonly ILogger<AudioCraftService> _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<AudioCraftService> logger,
|
||
IOptions<ApiSettings> settings)
|
||
{
|
||
_httpClient = httpClient;
|
||
_logger = logger;
|
||
_settings = settings.Value;
|
||
|
||
_httpClient.DefaultRequestHeaders.Add(
|
||
"Authorization", $"Bearer {_settings.HuggingFaceApiKey}");
|
||
_httpClient.Timeout = TimeSpan.FromMinutes(5);
|
||
}
|
||
|
||
/// <summary>
|
||
/// MusicGen ile text-to-music üretimi.
|
||
/// Proje için background müzik oluşturur.
|
||
/// </summary>
|
||
public async Task<GeneratedMediaFile> 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"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// AudioGen ile sahne bazlı ambient ses efekti üretimi.
|
||
/// Her sahne için farklı bir ortam sesi oluşturulabilir.
|
||
/// </summary>
|
||
public async Task<GeneratedMediaFile?> 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Projenin tüm sahneleri için batch ambient ses üretimi.
|
||
/// Paralel çalışır — Raspberry Pi'da bellek dostu.
|
||
/// </summary>
|
||
public async Task<List<GeneratedMediaFile>> GenerateAllAmbientSoundsAsync(
|
||
List<ScenePayload> scenes,
|
||
string outputDirectory,
|
||
CancellationToken ct)
|
||
{
|
||
var results = new List<GeneratedMediaFile>();
|
||
|
||
// 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<byte[]?> 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}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// MusicGen prompt'unu teknik parametrelerle zenginleştirir.
|
||
/// AudioCraft skill'den öğrenilen best practice'lere göre optimize eder.
|
||
/// </summary>
|
||
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<JsonElement>(json);
|
||
if (doc.TryGetProperty("estimated_time", out var time))
|
||
return Math.Max(10, (int)time.GetDouble());
|
||
}
|
||
catch { }
|
||
|
||
return 20; // Default: 20 saniye bekle
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// MusicGen teknik parametreleri — AI senaryo çıktısından parse edilir.
|
||
/// </summary>
|
||
public class MusicTechnicalParams
|
||
{
|
||
public int Bpm { get; set; }
|
||
public string? Key { get; set; }
|
||
public List<string> Instruments { get; set; } = new();
|
||
public string EmotionalArc { get; set; } = string.Empty;
|
||
}
|