From 06c60057718d5d7d60572086a0eb35cb5203ff7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 15 Feb 2026 13:51:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:Google=20Lyria=20API=EB=A1=9C=20AI=20?= =?UTF-8?q?=EB=B0=B0=EA=B2=BD=EC=9D=8C=EC=95=85=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lyria API 연동: - Vertex AI 기반 Google Lyria 음악 생성 API 추가 - 분위기(mood)별 영어 프롬프트 매핑 (upbeat, energetic, calm 등 8종) - 생성된 30초 WAV → MP3 변환 + 영상 길이에 맞춰 루프/트림 - 페이드인(1초) + 페이드아웃(3초) 자동 적용 - 비용: $0.06/30초 BGM 우선순위 변경: - 1순위: Lyria AI 배경음악 (신규) - 2순위: 프리셋 BGM 파일 (storage/app/bgm/) - 3순위: FFmpeg 앰비언트 (기존 폴백) Co-Authored-By: Claude Opus 4.6 --- app/Jobs/VideoGenerationJob.php | 18 +++- app/Services/Video/BgmService.php | 137 ++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/app/Jobs/VideoGenerationJob.php b/app/Jobs/VideoGenerationJob.php index c89f1b30..ab23280c 100644 --- a/app/Jobs/VideoGenerationJob.php +++ b/app/Jobs/VideoGenerationJob.php @@ -216,14 +216,24 @@ public function handle( } // === Step 4: BGM 생성/선택 === - $video->updateProgress(VideoGeneration::STATUS_GENERATING_BGM, 75, 'BGM 준비 중...'); + $video->updateProgress(VideoGeneration::STATUS_GENERATING_BGM, 75, 'AI 배경음악 생성 중...'); $bgmMood = $scenario['bgm_mood'] ?? 'upbeat'; - $bgmPath = $bgm->select($bgmMood, "{$workDir}/bgm.mp3"); + $totalDuration = array_sum(array_column($activeScenes, 'duration')); - // BGM 파일이 없으면 앰비언트 BGM 자동 생성 + // 1순위: Google Lyria AI 배경음악 생성 + $bgmPath = $bgm->generateWithLyria($bgmMood, $totalDuration, "{$workDir}/bgm.mp3"); + if ($bgmPath) { + $totalCost += 0.06; // Lyria 비용 ($0.06/30초) + } + + // 2순위: 프리셋 BGM 파일 + if (! $bgmPath) { + $bgmPath = $bgm->select($bgmMood, "{$workDir}/bgm.mp3"); + } + + // 3순위: FFmpeg 앰비언트 BGM if (! $bgmPath) { - $totalDuration = array_sum(array_column($activeScenes, 'duration')); $bgmPath = $bgm->generateAmbient($bgmMood, $totalDuration, "{$workDir}/bgm.mp3"); } diff --git a/app/Services/Video/BgmService.php b/app/Services/Video/BgmService.php index fd5442a8..c9f6de5d 100644 --- a/app/Services/Video/BgmService.php +++ b/app/Services/Video/BgmService.php @@ -2,10 +2,147 @@ namespace App\Services\Video; +use App\Models\System\AiConfig; +use App\Services\GoogleCloudService; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class BgmService { + private GoogleCloudService $googleCloud; + + public function __construct(GoogleCloudService $googleCloud) + { + $this->googleCloud = $googleCloud; + } + + /** + * Google Lyria API로 AI 배경음악 생성 + * + * @return string|null 생성된 MP3 파일 경로 + */ + public function generateWithLyria(string $mood, int $durationSec, string $savePath): ?string + { + $config = AiConfig::getActiveGemini(); + + if (! $config || ! $config->isVertexAi()) { + Log::info('BgmService: Vertex AI 설정 없음, Lyria 건너뜀'); + + return null; + } + + $token = $this->googleCloud->getAccessToken(); + + if (! $token) { + Log::warning('BgmService: Lyria 액세스 토큰 실패'); + + return null; + } + + try { + $projectId = $config->getProjectId(); + $region = $config->getRegion(); + $prompt = $this->buildMusicPrompt($mood); + + $url = "https://{$region}-aiplatform.googleapis.com/v1/projects/{$projectId}/locations/{$region}/publishers/google/models/lyria:predict"; + + Log::info('BgmService: Lyria API 요청', ['mood' => $mood, 'prompt' => $prompt]); + + $response = Http::withToken($token) + ->timeout(120) + ->post($url, [ + 'instances' => [ + ['prompt' => $prompt], + ], + ]); + + if (! $response->successful()) { + Log::warning('BgmService: Lyria API 실패', [ + 'status' => $response->status(), + 'body' => substr($response->body(), 0, 500), + ]); + + return null; + } + + $data = $response->json(); + $audioContent = $data['predictions'][0]['audioContent'] ?? null; + + if (! $audioContent) { + Log::warning('BgmService: Lyria 응답에 오디오 없음'); + + return null; + } + + $dir = dirname($savePath); + if (! is_dir($dir)) { + mkdir($dir, 0755, true); + } + + // WAV 저장 후 MP3 변환 + 영상 길이에 맞춤 + $wavPath = "{$dir}/bgm_lyria.wav"; + file_put_contents($wavPath, base64_decode($audioContent)); + + // MP3 변환 + 영상 길이 맞춤 (30초 BGM이 짧으면 루프, 길면 트림) + $cmd = sprintf( + 'ffmpeg -y -stream_loop -1 -i %s -t %d -af "afade=t=in:d=1,afade=t=out:st=%d:d=3" -c:a libmp3lame -q:a 2 %s 2>&1', + escapeshellarg($wavPath), + $durationSec, + max(0, $durationSec - 3), + escapeshellarg($savePath) + ); + + exec($cmd, $output, $returnCode); + @unlink($wavPath); + + if ($returnCode !== 0) { + Log::error('BgmService: Lyria WAV→MP3 변환 실패', [ + 'output' => implode("\n", array_slice($output, -5)), + ]); + + return null; + } + + Log::info('BgmService: Lyria BGM 생성 완료', [ + 'path' => $savePath, + 'duration' => $durationSec, + ]); + + return $savePath; + } catch (\Exception $e) { + Log::error('BgmService: Lyria 예외', ['error' => $e->getMessage()]); + + return null; + } + } + + /** + * 분위기 → Lyria 영어 프롬프트 변환 + */ + private function buildMusicPrompt(string $mood): string + { + $prompts = [ + 'upbeat' => 'Upbeat cheerful background music for a short health wellness video. Light electronic beat with positive energy, moderate tempo. Instrumental only, no vocals.', + 'energetic' => 'Energetic motivating background music for a fitness video. Fast-paced electronic beat with driving rhythm. Instrumental only, no vocals.', + 'exciting' => 'Exciting dynamic background music for a surprising facts video. Building tension with electronic elements. Instrumental only, no vocals.', + 'calm' => 'Calm soothing background music for a relaxation wellness video. Gentle piano and ambient pads, slow tempo. Instrumental only, no vocals.', + 'dramatic' => 'Dramatic cinematic background music for a revealing truth video. Orchestral tension with building suspense. Instrumental only, no vocals.', + 'happy' => 'Happy bright background music for a positive health tips video. Cheerful ukulele and light percussion. Instrumental only, no vocals.', + 'mysterious' => 'Mysterious intriguing background music for a health secrets video. Dark ambient with subtle electronic textures. Instrumental only, no vocals.', + 'inspiring' => 'Inspiring uplifting background music for a motivational health video. Warm piano with gentle strings, building progression. Instrumental only, no vocals.', + ]; + + $moodLower = strtolower($mood); + + foreach ($prompts as $key => $prompt) { + if (str_contains($moodLower, $key)) { + return $prompt; + } + } + + return 'Light pleasant background music for a short health video. Moderate tempo, positive mood, gentle electronic beat. Instrumental only, no vocals.'; + } + /** * 분위기별 BGM 매핑 (로열티프리 BGM 파일 풀) * storage/app/bgm/ 디렉토리에 미리 준비