Skip to content

Commit 378bad3

Browse files
committed
feat: voice preview
1 parent f626af6 commit 378bad3

17 files changed

+177
-29
lines changed

server/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import random
1414
from utils.config import load_config
1515

16-
from server.handlers import handle_openai_speech, handle_queue_size, handle_static, process_tts_request
16+
from server.handlers import handle_openai_speech, handle_queue_size, handle_static, process_tts_request, handle_voice_sample
1717

1818
# Configure logging
1919
logging.basicConfig(
@@ -72,6 +72,7 @@ def setup_routes(self):
7272
# OpenAI compatible endpoint
7373
self.app.router.add_post('/v1/audio/speech', self._handle_openai_speech)
7474
self.app.router.add_get('/api/queue-size', self._handle_queue_size)
75+
self.app.router.add_get('/api/voice-sample/{voice}', handle_voice_sample)
7576
self.app.router.add_get('/{tail:.*}', handle_static)
7677

7778
async def _handle_openai_speech(self, request):

server/handlers.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
MAX_REQUESTS_PER_WINDOW = config['rate_limit_requests']
3131
ip_request_counts = defaultdict(list)
3232

33+
# Voice samples directory
34+
VOICE_SAMPLES_DIR = Path('voices')
35+
3336
def _get_headers() -> Dict[str, str]:
3437
"""Generate more realistic browser headers with rotation"""
3538
browsers = [
@@ -364,4 +367,39 @@ async def handle_static(request: web.Request) -> web.Response:
364367

365368
except Exception as e:
366369
logger.error(f"Error serving static file: {str(e)}")
367-
return web.Response(text=str(e), status=500)
370+
return web.Response(text=str(e), status=500)
371+
372+
async def handle_voice_sample(request: web.Request) -> web.Response:
373+
"""Handle GET requests for voice samples."""
374+
try:
375+
voice = request.match_info.get('voice')
376+
if not voice:
377+
return web.Response(
378+
text=json.dumps({"error": "Voice parameter is required"}),
379+
status=400,
380+
content_type="application/json"
381+
)
382+
383+
sample_path = VOICE_SAMPLES_DIR / f"{voice}_sample.mp3"
384+
if not sample_path.exists():
385+
return web.Response(
386+
text=json.dumps({"error": f"Sample not found for voice: {voice}"}),
387+
status=404,
388+
content_type="application/json"
389+
)
390+
391+
return web.FileResponse(
392+
path=sample_path,
393+
headers={
394+
"Content-Type": "audio/mpeg",
395+
"Access-Control-Allow-Origin": "*"
396+
}
397+
)
398+
399+
except Exception as e:
400+
logger.error(f"Error serving voice sample: {str(e)}")
401+
return web.Response(
402+
text=json.dumps({"error": str(e)}),
403+
status=500,
404+
content_type="application/json"
405+
)

static/index.html

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ <h2>Try It Out</h2>
8585
<label for="playground-text">Text to Convert</label>
8686
<textarea id="playground-text" rows="4" placeholder="Enter the text you want to convert to speech..."></textarea>
8787
</div>
88+
<div class="form-group">
89+
<label for="playground-instructions">Instructions</label>
90+
<textarea id="playground-instructions" rows="2" placeholder="e.g., Speak in a cheerful and upbeat tone"></textarea>
91+
</div>
8892
<div class="form-group">
8993
<label for="playground-voice">Voice</label>
9094
<select id="playground-voice">
@@ -101,17 +105,20 @@ <h2>Try It Out</h2>
101105
<option value="verse">Verse</option>
102106
</select>
103107
</div>
104-
<div class="form-group">
105-
<label for="playground-instructions">Instructions (Optional)</label>
106-
<textarea id="playground-instructions" rows="2" placeholder="e.g., Speak in a cheerful and upbeat tone"></textarea>
107-
</div>
108108
<button id="playground-submit" class="playground-button">
109109
<i class="fas fa-play"></i> Generate Speech
110110
</button>
111111
</div>
112112
<div class="playground-output">
113-
<div id="playground-status" class="playground-status"></div>
114-
<div id="playground-audio" class="playground-audio"></div>
113+
<div class="audio-section">
114+
<h3>Voice Preview</h3>
115+
<div id="preview-audio" class="audio-player"></div>
116+
</div>
117+
<div class="audio-section">
118+
<h3>Generated Result</h3>
119+
<div id="playground-status" class="playground-status"></div>
120+
<div id="playground-audio" class="audio-player"></div>
121+
</div>
115122
</div>
116123
</div>
117124
</section>

static/index_zh.html

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ <h2>立即体验</h2>
8585
<label for="playground-text">要转换的文本</label>
8686
<textarea id="playground-text" rows="4" placeholder="输入要转换为语音的文本..."></textarea>
8787
</div>
88+
<div class="form-group">
89+
<label for="playground-instructions">指令</label>
90+
<textarea id="playground-instructions" rows="2" placeholder="例如:用欢快和兴奋的语气说话"></textarea>
91+
</div>
8892
<div class="form-group">
8993
<label for="playground-voice">语音</label>
9094
<select id="playground-voice">
@@ -101,17 +105,20 @@ <h2>立即体验</h2>
101105
<option value="verse">Verse</option>
102106
</select>
103107
</div>
104-
<div class="form-group">
105-
<label for="playground-instructions">指令(可选)</label>
106-
<textarea id="playground-instructions" rows="2" placeholder="例如:用欢快和兴奋的语气说话"></textarea>
107-
</div>
108108
<button id="playground-submit" class="playground-button">
109109
<i class="fas fa-play"></i> 生成语音
110110
</button>
111111
</div>
112112
<div class="playground-output">
113-
<div id="playground-status" class="playground-status"></div>
114-
<div id="playground-audio" class="playground-audio"></div>
113+
<div class="audio-section">
114+
<h3>语音预览</h3>
115+
<div id="preview-audio" class="audio-player"></div>
116+
</div>
117+
<div class="audio-section">
118+
<h3>生成结果</h3>
119+
<div id="playground-status" class="playground-status"></div>
120+
<div id="playground-audio" class="audio-player"></div>
121+
</div>
115122
</div>
116123
</div>
117124
</section>

static/script.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,51 @@ document.addEventListener('DOMContentLoaded', function() {
260260
statusDiv.textContent = message;
261261
statusDiv.className = `playground-status ${type}`;
262262
}
263+
});
264+
265+
// Voice sample functionality
266+
let currentSampleAudio = null;
267+
268+
// Function to load and play voice sample
269+
async function loadVoiceSample(voice) {
270+
const previewAudioDiv = document.getElementById('preview-audio');
271+
272+
try {
273+
// Create new audio element
274+
const response = await fetch(`/api/voice-sample/${voice}`);
275+
if (!response.ok) {
276+
throw new Error(`Failed to load voice sample: ${response.statusText}`);
277+
}
278+
279+
const blob = await response.blob();
280+
const audioUrl = URL.createObjectURL(blob);
281+
282+
// Create and configure audio element
283+
currentSampleAudio = document.createElement('audio');
284+
currentSampleAudio.controls = true;
285+
currentSampleAudio.src = audioUrl;
286+
287+
// Clear previous audio and add new one
288+
previewAudioDiv.innerHTML = '';
289+
previewAudioDiv.appendChild(currentSampleAudio);
290+
291+
} catch (error) {
292+
console.error('Error loading voice sample:', error);
293+
// Show error in status
294+
const statusDiv = document.getElementById('playground-status');
295+
statusDiv.innerHTML = `<div class="error-message">Error loading voice sample: ${error.message}</div>`;
296+
}
297+
}
298+
299+
// Add voice selection change handler
300+
document.getElementById('playground-voice').addEventListener('change', function() {
301+
loadVoiceSample(this.value);
302+
});
303+
304+
// Load initial voice sample when page loads
305+
document.addEventListener('DOMContentLoaded', function() {
306+
const voiceSelect = document.getElementById('playground-voice');
307+
if (voiceSelect) {
308+
loadVoiceSample(voiceSelect.value);
309+
}
263310
});

static/styles.css

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,7 @@ code[class*="language-"] {
11081108
justify-content: center;
11091109
gap: 0.5rem;
11101110
transition: background-color 0.2s ease;
1111+
margin-top: 1rem;
11111112
}
11121113

11131114
.playground-button:hover {
@@ -1122,13 +1123,41 @@ code[class*="language-"] {
11221123
.playground-output {
11231124
display: flex;
11241125
flex-direction: column;
1125-
gap: 1rem;
1126+
gap: 2rem;
11261127
}
11271128

1128-
.playground-status {
1129+
.audio-section {
1130+
background-color: #f8fafc;
1131+
border-radius: 8px;
1132+
padding: 1.5rem;
1133+
}
1134+
1135+
.audio-section h3 {
1136+
color: #2c3e50;
1137+
font-size: 1.1rem;
1138+
margin-bottom: 1rem;
1139+
}
1140+
1141+
.audio-player {
1142+
background-color: white;
1143+
border-radius: 6px;
11291144
padding: 1rem;
1145+
min-height: 60px;
1146+
display: flex;
1147+
align-items: center;
1148+
justify-content: center;
1149+
border: 1px solid #e2e8f0;
1150+
}
1151+
1152+
.audio-player audio {
1153+
width: 100%;
1154+
}
1155+
1156+
.playground-status {
1157+
padding: 0.75rem;
11301158
border-radius: 6px;
11311159
font-size: 0.9rem;
1160+
margin-bottom: 1rem;
11321161
}
11331162

11341163
.playground-status.error {
@@ -1143,28 +1172,47 @@ code[class*="language-"] {
11431172
border: 1px solid #dcfce7;
11441173
}
11451174

1146-
.playground-audio {
1147-
background-color: #f8fafc;
1148-
border-radius: 6px;
1149-
padding: 1rem;
1150-
min-height: 100px;
1175+
.voice-select-container {
1176+
display: flex;
1177+
gap: 8px;
1178+
align-items: center;
1179+
}
1180+
1181+
.voice-select-container select {
1182+
flex: 1;
1183+
}
1184+
1185+
.sample-button {
1186+
background-color: #4a90e2;
1187+
color: white;
1188+
border: none;
1189+
border-radius: 4px;
1190+
padding: 8px 12px;
1191+
cursor: pointer;
1192+
transition: background-color 0.2s;
11511193
display: flex;
11521194
align-items: center;
11531195
justify-content: center;
11541196
}
11551197

1156-
.playground-audio audio {
1157-
width: 100%;
1158-
max-width: 400px;
1198+
.sample-button:hover {
1199+
background-color: #357abd;
1200+
}
1201+
1202+
.sample-button:active {
1203+
background-color: #2d6da3;
1204+
}
1205+
1206+
.sample-button i {
1207+
font-size: 14px;
11591208
}
11601209

11611210
@media (max-width: 768px) {
11621211
.playground-container {
11631212
grid-template-columns: 1fr;
11641213
}
1165-
}
1166-
1167-
.indicator-error {
1168-
background-color: var(--error-color);
1169-
animation: pulse 2s infinite;
1214+
1215+
.playground-output {
1216+
gap: 1.5rem;
1217+
}
11701218
}

voices/alloy_sample.mp3

146 KB
Binary file not shown.

voices/ash_sample.mp3

177 KB
Binary file not shown.

voices/ballad_sample.mp3

572 KB
Binary file not shown.

voices/coral_sample.mp3

161 KB
Binary file not shown.

0 commit comments

Comments
 (0)