diff --git a/app/Jobs/TutorialVideoJob.php b/app/Jobs/TutorialVideoJob.php index c300da82..a70addd3 100644 --- a/app/Jobs/TutorialVideoJob.php +++ b/app/Jobs/TutorialVideoJob.php @@ -243,7 +243,7 @@ public function handle( // ASS 자막 생성 $subtitlePath = "{$workDir}/subtitle.ass"; - $videoAssembly->generateAssSubtitle($scenes, $subtitlePath, $narrationDurations); + $videoAssembly->generateAssSubtitle($scenes, $subtitlePath, $narrationDurations, 'landscape'); // 최종 MP4 합성 $finalOutputPath = "{$workDir}/final_tutorial.mp4"; diff --git a/app/Services/Video/VideoAssemblyService.php b/app/Services/Video/VideoAssemblyService.php index 911890a6..644508e7 100644 --- a/app/Services/Video/VideoAssemblyService.php +++ b/app/Services/Video/VideoAssemblyService.php @@ -208,27 +208,55 @@ public function getAudioDuration(string $path): float * @param array $scenes [{scene_number, narration, duration}, ...] * @param array $narrationDurations [scene_number => 실제 오디오 초] (ffprobe 측정값) */ - public function generateAssSubtitle(array $scenes, string $outputPath, array $narrationDurations = []): string + /** + * @param string $layout 'portrait' (9:16 Shorts) 또는 'landscape' (16:9 튜토리얼) + */ + public function generateAssSubtitle(array $scenes, string $outputPath, array $narrationDurations = [], string $layout = 'portrait'): string { $dir = dirname($outputPath); if (! is_dir($dir)) { mkdir($dir, 0755, true); } + $isLandscape = $layout === 'landscape'; + $ass = "[Script Info]\n"; $ass .= "ScriptType: v4.00+\n"; - $ass .= "PlayResX: 1080\n"; - $ass .= "PlayResY: 1920\n"; - $ass .= "WrapStyle: 0\n\n"; + + if ($isLandscape) { + // 가로 영상 (16:9 튜토리얼) + $ass .= "PlayResX: 1920\n"; + $ass .= "PlayResY: 1080\n"; + } else { + // 세로 영상 (9:16 Shorts) + $ass .= "PlayResX: 1080\n"; + $ass .= "PlayResY: 1920\n"; + } + $ass .= "WrapStyle: 2\n\n"; // 2 = no word wrapping (한줄 유지) $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,96,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,5,2,5,40,40,0,1\n\n"; + + if ($isLandscape) { + // 가로 영상: 하단 중앙, 폰트 작게, 여백 넓게, 한줄 최대 + // Alignment=2: 하단 중앙 + // MarginL=20, MarginR=20: 좌우 여백 최소화 → 길게 + // MarginV=30: 하단에서 약간 위 + // FontSize=48: 1920 기준 적절한 크기 + // BorderStyle=3: 반투명 배경 박스 + // BackColour=&H80000000: 반투명 검정 배경 + $ass .= "Style: Default,NanumGothic,48,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,3,2,0,2,20,20,30,1\n\n"; + } else { + // 세로 영상 (기존) + $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"; $currentTime = 0; + // 가로 영상은 줄바꿈 없이 한줄, 세로 영상은 16자 줄바꿈 + $maxCharsPerLine = $isLandscape ? 999 : 16; foreach ($scenes as $scene) { $sceneDuration = $scene['duration'] ?? 8; @@ -276,7 +304,7 @@ public function generateAssSubtitle(array $scenes, string $outputPath, array $na $startTime = $this->formatAssTime($currentTime + $offset); $endTime = $this->formatAssTime($currentTime + $offset + $sentDuration); - $text = $this->wrapText($sentence, 16); + $text = $this->wrapText($sentence, $maxCharsPerLine); $text = str_replace("\n", "\\N", $text); $ass .= "Dialogue: 0,{$startTime},{$endTime},Default,,0,0,0,,{$text}\n";