fix:TTS 속도 1.5x + Neural2 음성 변경 + 자막 문장 단위 싱크
TTS 개선: - 음성: ko-KR-Wavenet-A → ko-KR-Neural2-C (남성, 자연스럽고 개성있는 음성) - 속도: 1.0x → 1.5x (기존 대비 50% 빠르게) - 피치: 0.0 → 2.0 (더 에너지 있는 톤) 자막 싱크 버그 수정: - 장면 전체 나레이션을 한 블록으로 표시 → 문장 단위로 분리 표시 - 각 문장 타이밍을 글자 수 비례로 자동 계산 - 문장 분리 로직: 마침표/느낌표/물음표 기준, 폴백으로 쉼표 분리 - 장면 끝 0.3초 여백으로 자연스러운 전환 시나리오 프롬프트: - 나레이션 문장 길이 규칙 추가 (한 문장 15~25자) - 반드시 마침표/느낌표/물음표로 문장 구분하도록 명시 - 장면당 글자 수 60~100자로 밀도 향상 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -184,7 +184,7 @@ public function concatNarrations(array $audioPaths, array $scenes, string $outpu
|
||||
}
|
||||
|
||||
/**
|
||||
* ASS 자막 파일 생성
|
||||
* ASS 자막 파일 생성 (한 문장 단위로 분리, 음성 싱크)
|
||||
*
|
||||
* @param array $scenes [{scene_number, narration, duration}, ...]
|
||||
*/
|
||||
@@ -221,14 +221,42 @@ public function generateAssSubtitle(array $scenes, string $outputPath): string
|
||||
continue;
|
||||
}
|
||||
|
||||
$startTime = $this->formatAssTime($currentTime);
|
||||
$endTime = $this->formatAssTime($currentTime + $duration);
|
||||
// 나레이션을 문장 단위로 분리
|
||||
$sentences = $this->splitIntoSentences($narration);
|
||||
|
||||
// 긴 텍스트는 줄바꿈
|
||||
$text = $this->wrapText($narration, 12);
|
||||
$text = str_replace("\n", "\\N", $text);
|
||||
// 총 글자 수 기준으로 각 문장의 시간 비율 계산
|
||||
$totalChars = array_sum(array_map('mb_strlen', $sentences));
|
||||
// 나레이션이 차지하는 시간 (장면 끝에 0.3초 여백)
|
||||
$activeTime = max($duration - 0.3, $duration * 0.9);
|
||||
$offset = 0.0;
|
||||
|
||||
$ass .= "Dialogue: 0,{$startTime},{$endTime},Default,,0,0,0,,{$text}\n";
|
||||
foreach ($sentences as $sentence) {
|
||||
$sentence = trim($sentence);
|
||||
if (empty($sentence)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ratio = mb_strlen($sentence) / max($totalChars, 1);
|
||||
$sentDuration = max($activeTime * $ratio, 0.8);
|
||||
|
||||
// 장면 경계 초과 방지
|
||||
if ($offset + $sentDuration > $duration) {
|
||||
$sentDuration = $duration - $offset;
|
||||
}
|
||||
if ($sentDuration <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$startTime = $this->formatAssTime($currentTime + $offset);
|
||||
$endTime = $this->formatAssTime($currentTime + $offset + $sentDuration);
|
||||
|
||||
$text = $this->wrapText($sentence, 14);
|
||||
$text = str_replace("\n", "\\N", $text);
|
||||
|
||||
$ass .= "Dialogue: 0,{$startTime},{$endTime},Default,,0,0,0,,{$text}\n";
|
||||
|
||||
$offset += $sentDuration;
|
||||
}
|
||||
|
||||
$currentTime += $duration;
|
||||
}
|
||||
@@ -238,6 +266,22 @@ public function generateAssSubtitle(array $scenes, string $outputPath): string
|
||||
return $outputPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 나레이션 텍스트를 문장 단위로 분리
|
||||
*/
|
||||
private function splitIntoSentences(string $text): array
|
||||
{
|
||||
// 마침표, 느낌표, 물음표, 물결표 뒤에서 분리
|
||||
$sentences = preg_split('/(?<=[.!?~。!?])\s*/', $text, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
// 분리 안 되고 30자 이상이면 쉼표에서 분리
|
||||
if (count($sentences) <= 1 && mb_strlen($text) > 30) {
|
||||
$sentences = preg_split('/(?<=[,,])\s*/', $text, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
return ! empty($sentences) ? $sentences : [$text];
|
||||
}
|
||||
|
||||
/**
|
||||
* 최종 합성: 영상 + 나레이션 + BGM + 자막
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user