diff --git a/app/Services/Video/TtsService.php b/app/Services/Video/TtsService.php index 020bebbe..45bea9e7 100644 --- a/app/Services/Video/TtsService.php +++ b/app/Services/Video/TtsService.php @@ -31,9 +31,9 @@ public function synthesize(string $text, string $savePath, array $options = []): try { $languageCode = $options['language_code'] ?? 'ko-KR'; - $voiceName = $options['voice_name'] ?? 'ko-KR-Neural2-C'; - $speakingRate = $options['speaking_rate'] ?? 1.5; - $pitch = $options['pitch'] ?? 2.0; + $voiceName = $options['voice_name'] ?? 'ko-KR-Neural2-A'; + $speakingRate = $options['speaking_rate'] ?? 1.2; + $pitch = $options['pitch'] ?? 0.0; $response = Http::withToken($token) ->timeout(30) diff --git a/app/Services/Video/VideoAssemblyService.php b/app/Services/Video/VideoAssemblyService.php index 6f06ebb4..d505633b 100644 --- a/app/Services/Video/VideoAssemblyService.php +++ b/app/Services/Video/VideoAssemblyService.php @@ -237,7 +237,7 @@ public function generateAssSubtitle(array $scenes, string $outputPath): string } $ratio = mb_strlen($sentence) / max($totalChars, 1); - $sentDuration = max($activeTime * $ratio, 0.8); + $sentDuration = max($activeTime * $ratio, 1.5); // 장면 경계 초과 방지 if ($offset + $sentDuration > $duration) { @@ -250,7 +250,7 @@ public function generateAssSubtitle(array $scenes, string $outputPath): string $startTime = $this->formatAssTime($currentTime + $offset); $endTime = $this->formatAssTime($currentTime + $offset + $sentDuration); - $text = $this->wrapText($sentence, 14); + $text = $this->wrapText($sentence, 16); $text = str_replace("\n", "\\N", $text); $ass .= "Dialogue: 0,{$startTime},{$endTime},Default,,0,0,0,,{$text}\n"; @@ -267,19 +267,65 @@ public function generateAssSubtitle(array $scenes, string $outputPath): string } /** - * 나레이션 텍스트를 문장 단위로 분리 + * 나레이션 텍스트를 자막 청크 단위로 분리 + * + * 핵심 규칙: + * - 한 청크 최소 10자 이상 (한글자/한단어 자막 방지) + * - 장면당 2~3개 청크 목표 + * - 짧은 조각은 인접 청크에 병합 */ private function splitIntoSentences(string $text): array { - // 마침표, 느낌표, 물음표, 물결표 뒤에서 분리 - $sentences = preg_split('/(?<=[.!?~。!?])\s*/', $text, -1, PREG_SPLIT_NO_EMPTY); + $text = trim($text); - // 분리 안 되고 30자 이상이면 쉼표에서 분리 - if (count($sentences) <= 1 && mb_strlen($text) > 30) { - $sentences = preg_split('/(?<=[,,])\s*/', $text, -1, PREG_SPLIT_NO_EMPTY); + // 전체가 25자 이하면 분리하지 않음 + if (mb_strlen($text) <= 25) { + return [$text]; } - return ! empty($sentences) ? $sentences : [$text]; + // 1차: 마침표/느낌표/물음표 기준으로 분리 + $rawParts = preg_split('/(?<=[.!?。!?])\s*/', $text, -1, PREG_SPLIT_NO_EMPTY); + + // 분리가 안 되면 쉼표에서 시도 + if (count($rawParts) <= 1) { + $rawParts = preg_split('/(?<=[,,])\s*/', $text, -1, PREG_SPLIT_NO_EMPTY); + } + + // 그래도 분리 안 되면 원본 반환 + if (count($rawParts) <= 1) { + return [$text]; + } + + // 2차: 짧은 조각을 병합하여 최소 10자 보장 + $minChunkLength = 10; + $merged = []; + $buffer = ''; + + foreach ($rawParts as $part) { + $part = trim($part); + if ($part === '') { + continue; + } + + $buffer .= ($buffer !== '' ? ' ' : '') . $part; + + if (mb_strlen($buffer) >= $minChunkLength) { + $merged[] = $buffer; + $buffer = ''; + } + } + + // 남은 버퍼 처리 + if ($buffer !== '') { + if (! empty($merged)) { + // 마지막 청크에 붙이기 + $merged[count($merged) - 1] .= ' ' . $buffer; + } else { + $merged[] = $buffer; + } + } + + return ! empty($merged) ? $merged : [$text]; } /**