Files
sam-manage/app/Services/Video/TtsService.php
김보곤 e093c7b7e7 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>
2026-02-15 12:59:36 +09:00

123 lines
3.5 KiB
PHP

<?php
namespace App\Services\Video;
use App\Services\GoogleCloudService;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class TtsService
{
private GoogleCloudService $googleCloud;
public function __construct(GoogleCloudService $googleCloud)
{
$this->googleCloud = $googleCloud;
}
/**
* 텍스트 → MP3 음성 파일 생성
*
* @return string|null 저장된 파일 경로
*/
public function synthesize(string $text, string $savePath, array $options = []): ?string
{
$token = $this->googleCloud->getAccessToken();
if (! $token) {
Log::error('TtsService: 액세스 토큰 획득 실패');
return null;
}
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;
$response = Http::withToken($token)
->timeout(30)
->post('https://texttospeech.googleapis.com/v1/text:synthesize', [
'input' => [
'text' => $text,
],
'voice' => [
'languageCode' => $languageCode,
'name' => $voiceName,
],
'audioConfig' => [
'audioEncoding' => 'MP3',
'speakingRate' => $speakingRate,
'pitch' => $pitch,
'sampleRateHertz' => 24000,
],
]);
if (! $response->successful()) {
Log::error('TtsService: TTS API 실패', [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
}
$data = $response->json();
$audioContent = $data['audioContent'] ?? null;
if (! $audioContent) {
Log::error('TtsService: 오디오 데이터 없음');
return null;
}
$dir = dirname($savePath);
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($savePath, base64_decode($audioContent));
Log::info('TtsService: 음성 파일 생성 완료', [
'path' => $savePath,
'text_length' => mb_strlen($text),
]);
return $savePath;
} catch (\Exception $e) {
Log::error('TtsService: 예외 발생', ['error' => $e->getMessage()]);
return null;
}
}
/**
* 장면별 일괄 나레이션 생성
*
* @param array $scenes [{narration, scene_number}, ...]
* @return array [scene_number => file_path, ...]
*/
public function synthesizeScenes(array $scenes, string $baseDir): array
{
$results = [];
foreach ($scenes as $scene) {
$sceneNum = $scene['scene_number'] ?? 0;
$narration = $scene['narration'] ?? '';
if (empty($narration)) {
continue;
}
$savePath = "{$baseDir}/narration_{$sceneNum}.mp3";
$result = $this->synthesize($narration, $savePath);
if ($result) {
$results[$sceneNum] = $result;
}
}
return $results;
}
}