From 01efd990048fdba039879f11ea385e772ff9db6f 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 10:45:22 +0900 Subject: [PATCH] =?UTF-8?q?fix:=EC=9E=90=EB=A7=89=20=EC=A4=91=EC=95=99?= =?UTF-8?q?=EB=B0=B0=EC=B9=98+=ED=81=AC=EA=B8=B02=EB=B0=B0,=20TTS=20?= =?UTF-8?q?=EB=B3=BC=EB=A5=A8=20=EC=A6=9D=EA=B0=80,=20BGM=20=EC=95=B0?= =?UTF-8?q?=EB=B9=84=EC=96=B8=ED=8A=B8=20=EC=9E=90=EB=8F=99=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자막: 하단→중앙(Alignment=5), 48→96pt, 외곽선 5px, 줄바꿈 12자 - TTS: 나레이션 볼륨 1.0→2.0 (2배 증가) - BGM: 무음 대신 분위기별 앰비언트 화음 자동생성 (FFmpeg aevalsrc) - BGM 볼륨: 0.15→0.4 (나레이션 방해 안 하는 수준) Co-Authored-By: Claude Opus 4.6 --- app/Jobs/VideoGenerationJob.php | 4 +- app/Services/Video/BgmService.php | 78 ++++++++++++++++++++- app/Services/Video/VideoAssemblyService.php | 9 +-- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/app/Jobs/VideoGenerationJob.php b/app/Jobs/VideoGenerationJob.php index ac6507f1..8248ccb0 100644 --- a/app/Jobs/VideoGenerationJob.php +++ b/app/Jobs/VideoGenerationJob.php @@ -218,10 +218,10 @@ public function handle( $bgmMood = $scenario['bgm_mood'] ?? 'upbeat'; $bgmPath = $bgm->select($bgmMood, "{$workDir}/bgm.mp3"); - // BGM 파일이 없으면 무음 BGM 생성 + // BGM 파일이 없으면 앰비언트 BGM 자동 생성 if (! $bgmPath) { $totalDuration = array_sum(array_column($activeScenes, 'duration')); - $bgmPath = $bgm->generateSilence($totalDuration, "{$workDir}/bgm.mp3"); + $bgmPath = $bgm->generateAmbient($bgmMood, $totalDuration, "{$workDir}/bgm.mp3"); } // === Step 5: 최종 합성 === diff --git a/app/Services/Video/BgmService.php b/app/Services/Video/BgmService.php index 3f685be1..fd5442a8 100644 --- a/app/Services/Video/BgmService.php +++ b/app/Services/Video/BgmService.php @@ -108,7 +108,83 @@ private function selectFallback(string $bgmDir, string $savePath): ?string } /** - * 무음 BGM 생성 (FFmpeg 사용, BGM 파일이 없을 때 폴백) + * 분위기별 앰비언트 BGM 생성 (FFmpeg 사용, BGM 파일이 없을 때 폴백) + * + * 로컬 BGM 파일이 없을 때 FFmpeg의 오디오 합성으로 간단한 앰비언트 배경음 생성 + */ + public function generateAmbient(string $mood, int $durationSec, string $savePath): ?string + { + $dir = dirname($savePath); + if (! is_dir($dir)) { + mkdir($dir, 0755, true); + } + + // 분위기별 코드 주파수 설정 (근음, 3도, 5도, 옥타브) + $chords = $this->getMoodChord($mood); + + // FFmpeg aevalsrc로 화음 + 볼륨 모듈레이션(호흡 효과) 생성 + $expr = ''; + foreach ($chords as $i => $freq) { + $vol = 0.06 - ($i * 0.01); // 각 음의 볼륨을 점차 줄여 자연스럽게 + $modFreq = 0.08 + ($i * 0.03); // 각 음의 떨림 속도를 다르게 + $expr .= ($expr ? ' + ' : '') . "{$vol}*sin({$freq}*2*PI*t)*((0.5+0.5*sin({$modFreq}*2*PI*t)))"; + } + + // 페이드인(3초) + 페이드아웃(3초) 추가 + $cmd = sprintf( + 'ffmpeg -y -f lavfi -i "aevalsrc=%s:s=44100:d=%d" ' + . '-af "lowpass=f=1500,highpass=f=80,afade=t=in:d=3,afade=t=out:st=%d:d=3" ' + . '-c:a libmp3lame -q:a 4 %s 2>&1', + escapeshellarg($expr), + $durationSec + 2, // 페이드아웃 여유분 + max(0, $durationSec - 2), + escapeshellarg($savePath) + ); + + Log::info('BgmService: 앰비언트 BGM 생성 시작', ['mood' => $mood, 'duration' => $durationSec]); + + exec($cmd, $output, $returnCode); + + if ($returnCode !== 0) { + Log::error('BgmService: 앰비언트 BGM 생성 실패, 무음 폴백', [ + 'output' => implode("\n", array_slice($output, -10)), + ]); + + return $this->generateSilence($durationSec, $savePath); + } + + Log::info('BgmService: 앰비언트 BGM 생성 완료', ['path' => $savePath]); + + return $savePath; + } + + /** + * 분위기별 화음 주파수 반환 + */ + private function getMoodChord(string $mood): array + { + $moodLower = strtolower($mood); + + // 분위기별 코드 (근음 기준 메이저/마이너 코드) + if (str_contains($moodLower, 'sad') || str_contains($moodLower, 'dramatic')) { + // A minor (La-Do-Mi) - 슬프거나 극적인 분위기 + return [220.00, 261.63, 329.63, 440.00]; + } + if (str_contains($moodLower, 'calm') || str_contains($moodLower, 'mysterious')) { + // D minor7 (Re-Fa-La-Do) - 잔잔하거나 신비로운 분위기 + return [146.83, 174.61, 220.00, 261.63]; + } + if (str_contains($moodLower, 'inspiring') || str_contains($moodLower, 'happy')) { + // G major (Sol-Si-Re) - 영감을 주거나 밝은 분위기 + return [196.00, 246.94, 293.66, 392.00]; + } + + // 기본: C major (Do-Mi-Sol) - 밝고 에너지 넘치는 분위기 + return [130.81, 164.81, 196.00, 261.63]; + } + + /** + * 무음 BGM 생성 (FFmpeg 사용, 최종 폴백) */ public function generateSilence(int $durationSec, string $savePath): ?string { diff --git a/app/Services/Video/VideoAssemblyService.php b/app/Services/Video/VideoAssemblyService.php index 2639dba6..56e527c1 100644 --- a/app/Services/Video/VideoAssemblyService.php +++ b/app/Services/Video/VideoAssemblyService.php @@ -202,7 +202,8 @@ public function generateAssSubtitle(array $scenes, string $outputPath): string $ass .= "[V4+ Styles]\n"; $ass .= "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"; - $ass .= "Style: Default,NanumGothic,48,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,3,1,2,40,40,200,1\n\n"; + // Fontsize=96(2배), 흰색 글자+검정 외곽선(5px)+검정 그림자(2px), Alignment=5(화면 중앙) + $ass .= "Style: Default,NanumGothic,96,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,5,2,5,40,40,0,1\n\n"; $ass .= "[Events]\n"; $ass .= "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"; @@ -223,7 +224,7 @@ public function generateAssSubtitle(array $scenes, string $outputPath): string $endTime = $this->formatAssTime($currentTime + $duration); // 긴 텍스트는 줄바꿈 - $text = $this->wrapText($narration, 18); + $text = $this->wrapText($narration, 12); $text = str_replace("\n", "\\N", $text); $ass .= "Dialogue: 0,{$startTime},{$endTime},Default,,0,0,0,,{$text}\n"; @@ -259,14 +260,14 @@ public function assemble( // 나레이션 추가 if ($narrationPath && file_exists($narrationPath)) { $inputs[] = '-i ' . escapeshellarg($narrationPath); - $filterParts[] = "[{$audioIndex}:a]volume=1.0[nar]"; + $filterParts[] = "[{$audioIndex}:a]volume=2.0[nar]"; $audioIndex++; } // BGM 추가 if ($bgmPath && file_exists($bgmPath)) { $inputs[] = '-i ' . escapeshellarg($bgmPath); - $filterParts[] = "[{$audioIndex}:a]volume=0.15[bgm]"; + $filterParts[] = "[{$audioIndex}:a]volume=0.4[bgm]"; $audioIndex++; }