- TTS 속도: 1.2x → 1.4x - 자막 타이밍: 장면의 90% → 75% 구간에 압축 (음성과 싱크) - 쇼츠 트렌드에 맞춘 빠른 템포 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
161 lines
5.4 KiB
PHP
161 lines
5.4 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-A';
|
|
$speakingRate = $options['speaking_rate'] ?? 1.4;
|
|
$pitch = $options['pitch'] ?? 0.0;
|
|
|
|
// TTS 전송 전 이모지/특수문자 제거 (읽어버리는 문제 방지)
|
|
$cleanText = $this->stripNonSpeechChars($text);
|
|
|
|
$response = Http::withToken($token)
|
|
->timeout(30)
|
|
->post('https://texttospeech.googleapis.com/v1/text:synthesize', [
|
|
'input' => [
|
|
'text' => $cleanText,
|
|
],
|
|
'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;
|
|
}
|
|
|
|
/**
|
|
* TTS에 보내기 전 이모지, 특수문자, 동작 표현 제거
|
|
*/
|
|
private function stripNonSpeechChars(string $text): string
|
|
{
|
|
// 이모지 유니코드 블록 제거
|
|
$text = preg_replace('/[\x{1F600}-\x{1F64F}]/u', '', $text); // 이모티콘
|
|
$text = preg_replace('/[\x{1F300}-\x{1F5FF}]/u', '', $text); // 기호/픽토그래프
|
|
$text = preg_replace('/[\x{1F680}-\x{1F6FF}]/u', '', $text); // 교통/지도
|
|
$text = preg_replace('/[\x{1F900}-\x{1F9FF}]/u', '', $text); // 보충 이모지
|
|
$text = preg_replace('/[\x{1FA00}-\x{1FA6F}]/u', '', $text); // 체스/확장
|
|
$text = preg_replace('/[\x{1FA70}-\x{1FAFF}]/u', '', $text); // 심볼 확장
|
|
$text = preg_replace('/[\x{2600}-\x{27BF}]/u', '', $text); // 기타 심볼
|
|
$text = preg_replace('/[\x{2700}-\x{27BF}]/u', '', $text); // Dingbats
|
|
$text = preg_replace('/[\x{FE00}-\x{FE0F}]/u', '', $text); // 변형 선택자
|
|
$text = preg_replace('/[\x{200D}]/u', '', $text); // Zero Width Joiner
|
|
$text = preg_replace('/[\x{20E3}]/u', '', $text); // 키캡
|
|
$text = preg_replace('/[\x{E0020}-\x{E007F}]/u', '', $text); // 태그
|
|
|
|
// *강조* 스타일 텍스트에서 별표만 제거 (내용은 유지)
|
|
$text = str_replace('*', '', $text);
|
|
|
|
// (효과음), [동작] 등 괄호 표현 제거
|
|
$text = preg_replace('/\([^)]*\)/', '', $text);
|
|
$text = preg_replace('/\[[^\]]*\]/', '', $text);
|
|
|
|
// ○, ●, ◎ 등 특수 기호 제거
|
|
$text = preg_replace('/[○●◎◇◆□■△▲▽▼★☆♡♥]/u', '', $text);
|
|
|
|
// 연속 공백 정리
|
|
$text = preg_replace('/\s+/', ' ', $text);
|
|
|
|
return trim($text);
|
|
}
|
|
}
|