fix: [sound-logo] 음성 카테고리 기반 선택으로 재구성

- 여성/남성/아이 카테고리 탭으로 1차 분류 (성별 확실한 전달)
- 공식 문서 기반 음성 성별 정보 수정 (Gacrux=여성, Sadachbia=남성 등)
- 아이 카테고리: 젊은 음성 + 'young child, high-pitched' Director's Note 지시문
- 스타일 옵션에서 아이/청소년 제거 (카테고리로 이동)
- 프롬프트 형식을 Director's Note 형식으로 개선
This commit is contained in:
김보곤
2026-03-08 14:12:57 +09:00
parent 0e86636354
commit 9b7362fa4f
2 changed files with 94 additions and 49 deletions

View File

@@ -486,6 +486,7 @@ public function soundLogoTts(Request $request): JsonResponse
$request->validate([
'text' => 'required|string|max:200',
'voice_name' => 'nullable|string|max:30',
'voice_category' => 'nullable|string|in:female,male,child',
'voice_style' => 'nullable|string|max:100',
'voice_speed' => 'nullable|integer|min:1|max:5',
]);
@@ -498,6 +499,7 @@ public function soundLogoTts(Request $request): JsonResponse
}
$voiceName = $request->voice_name ?: 'Kore';
$voiceCategory = $request->voice_category ?: 'female';
$voiceStyle = $request->voice_style ?: '';
$voiceSpeed = $request->voice_speed ?: 3;
@@ -505,23 +507,31 @@ public function soundLogoTts(Request $request): JsonResponse
$speedDirectives = [
1 => '아주 천천히 또박또박 말해주세요.',
2 => '조금 느린 속도로 말해주세요.',
3 => '', // 보통 — 지시 없음
3 => '',
4 => '조금 빠른 속도로 말해주세요.',
5 => '아주 빠른 속도로 말해주세요.',
];
// TTS에 전달할 텍스트 구성
$directives = [];
if ($voiceStyle) {
$directives[] = $voiceStyle;
}
if (! empty($speedDirectives[$voiceSpeed])) {
$directives[] = $speedDirectives[$voiceSpeed];
// TTS 프롬프트 구성 — Director's Note 형식
$ttsText = $request->text;
$notes = [];
// 아이 카테고리: 높은 톤으로 어린이처럼 연기하도록 강한 지시문
if ($voiceCategory === 'child') {
$notes[] = 'Speak as a young child with a high-pitched, innocent voice';
}
$ttsText = $request->text;
if (! empty($directives)) {
$ttsText = implode(' ', $directives)." \n\n{$ttsText}";
if ($voiceStyle) {
$notes[] = $voiceStyle;
}
if (! empty($speedDirectives[$voiceSpeed])) {
$notes[] = $speedDirectives[$voiceSpeed];
}
if (! empty($notes)) {
$direction = implode('. ', $notes);
$ttsText = "[{$direction}]\n\n{$ttsText}";
}
// 짧은 텍스트는 TTS 모델이 텍스트 생성으로 인식할 수 있으므로 발화 컨텍스트 추가

View File

@@ -355,24 +355,45 @@
<div style="margin-bottom: 6px;">
<input class="sl-input" x-model="voiceText" placeholder='예: 우리들의 솔루션 쌤' style="margin-bottom: 6px;">
<!-- 성별/연령 카테고리 -->
<div style="display: flex; gap: 2px; margin-bottom: 6px; background: var(--sl-bg); border-radius: 6px; padding: 2px;">
<button style="flex:1; padding: 4px 0; border-radius: 5px; border: none; font-size: 10px; font-weight: 600; cursor: pointer; transition: all .15s;"
:style="voiceCategory === 'female' ? 'background: #ec4899; color: #fff;' : 'background: transparent; color: var(--sl-text2);'"
@click="voiceCategory = 'female'; voiceName = voiceGroups.female[0].value;">
<i class="ri-women-line"></i> 여성
</button>
<button style="flex:1; padding: 4px 0; border-radius: 5px; border: none; font-size: 10px; font-weight: 600; cursor: pointer; transition: all .15s;"
:style="voiceCategory === 'male' ? 'background: #3b82f6; color: #fff;' : 'background: transparent; color: var(--sl-text2);'"
@click="voiceCategory = 'male'; voiceName = voiceGroups.male[0].value;">
<i class="ri-men-line"></i> 남성
</button>
<button style="flex:1; padding: 4px 0; border-radius: 5px; border: none; font-size: 10px; font-weight: 600; cursor: pointer; transition: all .15s;"
:style="voiceCategory === 'child' ? 'background: #f59e0b; color: #fff;' : 'background: transparent; color: var(--sl-text2);'"
@click="voiceCategory = 'child'; voiceName = voiceGroups.child[0].value;">
<i class="ri-emotion-happy-line"></i> 아이
</button>
</div>
<!-- 음성 선택 -->
<select class="sl-select" x-model="voiceName" style="margin-bottom: 6px;">
<template x-for="opt in voiceOptions" :key="opt.value">
<option :value="opt.value" x-text="opt.label + ' (' + opt.gender + ', ' + opt.desc + ')'"></option>
<template x-for="opt in voiceGroups[voiceCategory]" :key="opt.value">
<option :value="opt.value" x-text="opt.label"></option>
</template>
</select>
<!-- 스타일 선택 -->
<select class="sl-select" x-model="voiceStyle" style="margin-bottom: 6px;">
<template x-for="s in voiceStyleOptions" :key="s.value">
<option :value="s.value" x-text="s.label"></option>
</template>
</select>
<!-- /스타일 + 속도 -->
<div style="display: flex; gap: 4px; margin-bottom: 6px;">
<select class="sl-select" x-model="voiceStyle" style="flex: 1;">
<template x-for="s in voiceStyleOptions" :key="s.value">
<option :value="s.value" x-text="s.label"></option>
</template>
</select>
</div>
<!-- 말하기 속도 -->
<div class="sl-param" style="margin-bottom: 8px;">
<div class="sl-param-label">
<span><i class="ri-speed-line"></i> 말하기 속도</span>
<span><i class="ri-speed-line"></i> 속도</span>
<span x-text="voiceSpeedLabels[voiceSpeed - 1]" style="font-size: 10px;"></span>
</div>
<input type="range" class="sl-slider" min="1" max="5" step="1"
@@ -1076,38 +1097,51 @@ function soundLogo() {
voiceDelay: 0.0,
voiceVolume: 0.8,
voiceBuffer: null,
voiceCategory: 'female',
voiceName: 'Kore',
voiceStyle: '',
voiceSpeed: 3, // 1~5 (1=매우느림, 3=보통, 5=매우빠름)
voiceOptions: [
{ value: 'Kore', label: '코어', desc: '단정하고 명확한', gender: '여성' },
{ value: 'Aoede', label: '아오이데', desc: '산뜻하고 가벼운', gender: '여성' },
{ value: 'Leda', label: '레다', desc: '젊고 발랄한', gender: '여성' },
{ value: 'Achernar', label: '아케르나르', desc: '부드럽고 섬세한', gender: '여성' },
{ value: 'Sulafat', label: '술라파트', desc: '따뜻하고 포근한', gender: '여성' },
{ value: 'Sadachbia', label: '사다키비아', desc: '활기차고 생동감', gender: '여성' },
{ value: 'Vindemiatrix', label: '빈데미아트릭스', desc: '부드럽고 차분한', gender: '여성' },
{ value: 'Puck', label: '퍽', desc: '밝고 활발한', gender: '남성' },
{ value: 'Charon', label: '카론', desc: '정보적이고 안정적', gender: '남성' },
{ value: 'Fenrir', label: '펜리르', desc: '에너지 넘치는', gender: '남성' },
{ value: 'Orus', label: '오루스', desc: '무게감 있는', gender: '남성' },
{ value: 'Perseus', label: '페르세우스', desc: '차분하고 침착한', gender: '남성' },
{ value: 'Gacrux', label: '가크룩스', desc: '성숙하고 중후한', gender: '남성' },
{ value: 'Achird', label: '아키르드', desc: '친근하고 다정한', gender: '남성' },
{ value: 'Algieba', label: '알기에바', desc: '매끄럽고 세련된', gender: '남성' },
{ value: 'Zephyr', label: '제피르', desc: '밝고 경쾌한', gender: '중성' },
],
voiceSpeed: 3,
voiceGroups: {
female: [
{ value: 'Kore', label: '코어 — 단정하고 명확한' },
{ value: 'Aoede', label: '아오이데 — 산뜻하고 가벼운' },
{ value: 'Leda', label: '레다 — 젊고 발랄한' },
{ value: 'Zephyr', label: '제피르 — 밝고 경쾌한' },
{ value: 'Achernar', label: '아케르나르 — 부드럽고 섬세한' },
{ value: 'Sulafat', label: '술라파트 — 따뜻하고 포근한' },
{ value: 'Gacrux', label: '가크룩스 — 성숙하고 중후한' },
{ value: 'Vindemiatrix', label: '빈데미아트릭스 — 차분한' },
{ value: 'Pulcherrima', label: '풀체리마 — 또렷하고 앞선' },
],
male: [
{ value: 'Puck', label: '퍽 — 밝고 활발한' },
{ value: 'Charon', label: '카론 — 안정적이고 신뢰감' },
{ value: 'Fenrir', label: '펜리르 — 에너지 넘치는' },
{ value: 'Orus', label: '오루스 — 무게감 있는' },
{ value: 'Achird', label: '아키르드 — 친근하고 다정한' },
{ value: 'Algieba', label: '알기에바 — 매끄럽고 세련된' },
{ value: 'Schedar', label: '쉐다르 — 균일하고 안정적' },
{ value: 'Alnilam', label: '알닐람 — 단단하고 힘 있는' },
{ value: 'Sadaltager', label: '사달타게르 — 지적이고 차분한' },
],
child: [
{ value: 'Leda', label: '여자아이 (레다 기반) — 발랄한' },
{ value: 'Zephyr', label: '여자아이 (제피르 기반) — 경쾌한' },
{ value: 'Achernar', label: '여자아이 (아케르나르 기반) — 섬세한' },
{ value: 'Puck', label: '남자아이 (퍽 기반) — 활발한' },
{ value: 'Fenrir', label: '남자아이 (펜리르 기반) — 에너지' },
],
},
voiceStyleOptions: [
{ value: '', label: '기본 스타일' },
{ value: '밝고 활기차게', label: '밝고 활기차게' },
{ value: '차분하고 신뢰감 있게', label: '차분하고 신뢰감 있게' },
{ value: '따뜻하고 부드럽게', label: '따뜻하고 부드럽게' },
{ value: '힘 있고 당당하게', label: '힘 있고 당당하게' },
{ value: '귀엽고 사랑스럽게', label: '귀엽고 사랑스럽게' },
{ value: '진지하고 전문적으로', label: '진지하고 전문적으로' },
{ value: '어린 아이의 목소리로', label: '어린 아이 (5~7세)' },
{ value: '초등학생 아이의 밝은 목소리로', label: '초등학생 (8~12세)' },
{ value: '10대 청소년의 목소리로', label: '청소년 (13~18세)' },
{ value: '', label: '기본 ' },
{ value: '밝고 활기차게 말해주세요', label: '밝고 활기차게' },
{ value: '차분하고 신뢰감 있게 말해주세요', label: '차분하고 신뢰감 있게' },
{ value: '따뜻하고 부드럽게 말해주세요', label: '따뜻하고 부드럽게' },
{ value: '힘 있고 당당하게 말해주세요', label: '힘 있고 당당하게' },
{ value: '귀엽고 사랑스럽게 말해주세요', label: '귀엽고 사랑스럽게' },
{ value: '진지하고 전문적으로 말해주세요', label: '진지하고 전문적으로' },
{ value: '속삭이듯 조용히 말해주세요', label: '속삭이듯 조용히' },
{ value: '흥분하고 신나게 말해주세요', label: '흥분하고 신나게' },
],
voiceSpeedLabels: ['매우 느리게', '느리게', '보통', '빠르게', '매우 빠르게'],
@@ -1681,6 +1715,7 @@ function soundLogo() {
body: JSON.stringify({
text: this.voiceText,
voice_name: this.voiceName,
voice_category: this.voiceCategory,
voice_style: this.voiceStyle,
voice_speed: this.voiceSpeed,
}),