From ff80d50c9b02e6256b1978de142306ef760050f6 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 17:39:39 +0900 Subject: [PATCH] =?UTF-8?q?fix:=EC=9E=90=EB=A7=89=20=ED=95=98=EB=8B=A8=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20+=20=EC=9D=B8=ED=8A=B8=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=90=EB=A7=89=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FFmpeg subtitles 필터 → ass 필터 변경 (ASS 스타일 Alignment 완전 보존) - 인트로/아웃트로 씬 자막 제거를 이중 보장: 1. Job에서 자막용 scenes 복사본의 인트로/아웃트로 narration을 빈 문자열로 설정 2. generateAssSubtitle에서 scene_number int 캐스팅 + <= 1 비교로 안전장치 강화 Co-Authored-By: Claude Opus 4.6 --- app/Jobs/TutorialVideoJob.php | 11 +++++++++-- app/Services/Video/VideoAssemblyService.php | 11 ++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/Jobs/TutorialVideoJob.php b/app/Jobs/TutorialVideoJob.php index a70addd3..e6107a15 100644 --- a/app/Jobs/TutorialVideoJob.php +++ b/app/Jobs/TutorialVideoJob.php @@ -241,9 +241,16 @@ public function handle( // === Step 5: 최종 합성 (80%) === $tutorial->updateProgress(TutorialVideo::STATUS_ASSEMBLING, 70, '영상 합성 중...'); - // ASS 자막 생성 + // ASS 자막 생성 (인트로/아웃트로 나레이션은 자막에서 제외) + $subtitleScenes = array_map(function ($scene) { + $sn = (int) ($scene['scene_number'] ?? 0); + if ($sn <= 1 || $sn === 999) { + $scene['narration'] = ''; + } + return $scene; + }, $scenes); $subtitlePath = "{$workDir}/subtitle.ass"; - $videoAssembly->generateAssSubtitle($scenes, $subtitlePath, $narrationDurations, 'landscape'); + $videoAssembly->generateAssSubtitle($subtitleScenes, $subtitlePath, $narrationDurations, 'landscape'); // 최종 MP4 합성 $finalOutputPath = "{$workDir}/final_tutorial.mp4"; diff --git a/app/Services/Video/VideoAssemblyService.php b/app/Services/Video/VideoAssemblyService.php index 644508e7..3530854d 100644 --- a/app/Services/Video/VideoAssemblyService.php +++ b/app/Services/Video/VideoAssemblyService.php @@ -260,11 +260,12 @@ public function generateAssSubtitle(array $scenes, string $outputPath, array $na foreach ($scenes as $scene) { $sceneDuration = $scene['duration'] ?? 8; - $sceneNum = $scene['scene_number'] ?? 0; + $sceneNum = (int) ($scene['scene_number'] ?? 0); $narration = $scene['narration'] ?? ''; - // 인트로/아웃트로 씬은 슬라이드에 텍스트가 포함되어 있으므로 자막 생략 - $isIntroOutro = $sceneNum === 1 || $sceneNum === 999; + // 인트로(1)/아웃트로(999) 씬은 슬라이드에 텍스트가 포함 → 자막 생략 + // 또한 scene_number < 100인 경우도 인트로로 간주 (안전장치) + $isIntroOutro = $sceneNum <= 1 || $sceneNum === 999; if (empty($narration) || $isIntroOutro) { $currentTime += $sceneDuration; @@ -435,8 +436,8 @@ public function assemble( $filterComplex = null; } - // 자막 비디오 필터 - $vf = sprintf("subtitles=%s", escapeshellarg($subtitlePath)); + // 자막 비디오 필터 (ass 필터: ASS 스타일(Alignment 등) 완전 보존) + $vf = sprintf("ass=%s", escapeshellarg($subtitlePath)); // FFmpeg 명령 조립 $cmd = 'ffmpeg -y ' . implode(' ', $inputs);