- GeminiScriptService: 트렌딩 제목/시나리오 생성 - VeoVideoService: Veo 3.1 영상 클립 생성 - TtsService: Google TTS 나레이션 생성 - BgmService: 분위기별 BGM 선택 - VideoAssemblyService: FFmpeg 영상 합성 - VideoGenerationJob: 백그라운드 처리 - Veo3Controller: API 엔드포인트 - React 프론트엔드 (5단계 위저드) - GoogleCloudService.getAccessToken() public 변경 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
3.7 KiB
PHP
137 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Video;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class BgmService
|
|
{
|
|
/**
|
|
* 분위기별 BGM 매핑 (로열티프리 BGM 파일 풀)
|
|
* storage/app/bgm/ 디렉토리에 미리 준비
|
|
*/
|
|
private array $moodMap = [
|
|
'upbeat' => ['upbeat_01.mp3', 'upbeat_02.mp3'],
|
|
'energetic' => ['energetic_01.mp3', 'energetic_02.mp3'],
|
|
'exciting' => ['exciting_01.mp3', 'exciting_02.mp3'],
|
|
'calm' => ['calm_01.mp3', 'calm_02.mp3'],
|
|
'dramatic' => ['dramatic_01.mp3', 'dramatic_02.mp3'],
|
|
'happy' => ['happy_01.mp3', 'happy_02.mp3'],
|
|
'sad' => ['sad_01.mp3', 'sad_02.mp3'],
|
|
'mysterious' => ['mysterious_01.mp3', 'mysterious_02.mp3'],
|
|
'inspiring' => ['inspiring_01.mp3', 'inspiring_02.mp3'],
|
|
];
|
|
|
|
/**
|
|
* 분위기에 맞는 BGM 선택
|
|
*
|
|
* @return string|null BGM 파일 경로
|
|
*/
|
|
public function select(string $mood, string $savePath): ?string
|
|
{
|
|
$bgmDir = storage_path('app/bgm');
|
|
|
|
// 분위기 키워드 매칭 (부분 일치 지원)
|
|
$matchedFiles = [];
|
|
$moodLower = strtolower($mood);
|
|
|
|
foreach ($this->moodMap as $key => $files) {
|
|
if (str_contains($moodLower, $key)) {
|
|
$matchedFiles = array_merge($matchedFiles, $files);
|
|
}
|
|
}
|
|
|
|
// 매칭되는 분위기가 없으면 기본값
|
|
if (empty($matchedFiles)) {
|
|
$matchedFiles = $this->moodMap['upbeat'] ?? ['default.mp3'];
|
|
}
|
|
|
|
// 랜덤 선택
|
|
$selectedFile = $matchedFiles[array_rand($matchedFiles)];
|
|
$sourcePath = "{$bgmDir}/{$selectedFile}";
|
|
|
|
// BGM 파일 존재 확인
|
|
if (! file_exists($sourcePath)) {
|
|
Log::warning('BgmService: BGM 파일 없음', [
|
|
'path' => $sourcePath,
|
|
'mood' => $mood,
|
|
]);
|
|
|
|
// BGM 디렉토리에서 아무 파일이나 선택
|
|
return $this->selectFallback($bgmDir, $savePath);
|
|
}
|
|
|
|
$dir = dirname($savePath);
|
|
if (! is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
|
|
copy($sourcePath, $savePath);
|
|
|
|
Log::info('BgmService: BGM 선택 완료', [
|
|
'mood' => $mood,
|
|
'file' => $selectedFile,
|
|
]);
|
|
|
|
return $savePath;
|
|
}
|
|
|
|
/**
|
|
* 폴백: BGM 디렉토리에서 아무 MP3 선택
|
|
*/
|
|
private function selectFallback(string $bgmDir, string $savePath): ?string
|
|
{
|
|
if (! is_dir($bgmDir)) {
|
|
Log::error('BgmService: BGM 디렉토리 없음', ['dir' => $bgmDir]);
|
|
|
|
return null;
|
|
}
|
|
|
|
$files = glob("{$bgmDir}/*.mp3");
|
|
|
|
if (empty($files)) {
|
|
Log::error('BgmService: BGM 파일이 하나도 없음');
|
|
|
|
return null;
|
|
}
|
|
|
|
$selected = $files[array_rand($files)];
|
|
|
|
$dir = dirname($savePath);
|
|
if (! is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
|
|
copy($selected, $savePath);
|
|
|
|
return $savePath;
|
|
}
|
|
|
|
/**
|
|
* 무음 BGM 생성 (FFmpeg 사용, BGM 파일이 없을 때 폴백)
|
|
*/
|
|
public function generateSilence(int $durationSec, string $savePath): ?string
|
|
{
|
|
$dir = dirname($savePath);
|
|
if (! is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
|
|
$cmd = sprintf(
|
|
'ffmpeg -y -f lavfi -i anullsrc=r=44100:cl=stereo -t %d -c:a libmp3lame -q:a 9 %s 2>&1',
|
|
$durationSec,
|
|
escapeshellarg($savePath)
|
|
);
|
|
|
|
exec($cmd, $output, $returnCode);
|
|
|
|
if ($returnCode !== 0) {
|
|
Log::error('BgmService: 무음 BGM 생성 실패', ['output' => implode("\n", $output)]);
|
|
|
|
return null;
|
|
}
|
|
|
|
return $savePath;
|
|
}
|
|
}
|