fix:여성 음성 변경 + 속도 20% 감속 + 자막 한글자 버그 수정

TTS 설정:
- 음성: Neural2-C (남성) → Neural2-A (여성)
- 속도: 1.5x → 1.2x (20% 감속)
- 피치: 2.0 → 0.0 (자연스러운 여성 톤)

자막 한글자/한단어 버그 수정:
- 최소 청크 길이 10자 보장 (짧은 조각 인접 청크에 병합)
- 전체 25자 이하면 분리하지 않고 한 블록으로 표시
- 남은 짧은 버퍼는 마지막 청크에 합치기
- 최소 표시 시간 0.8초 → 1.5초로 증가
- 줄바꿈 기준 14자 → 16자 (가독성 향상)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-15 13:18:47 +09:00
parent ed1967405c
commit a8d87e2b92
2 changed files with 58 additions and 12 deletions

View File

@@ -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)

View File

@@ -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];
}
/**