feat(radio): replace robot TTS with Piper Neural voices & add instant voice preview
- Integrate local Piper ONNX neural TTS engine (Dmitri, Irina, Denis, Ruslan) - Add GET /api/radio/preview-voice endpoint with cached pre-rendered samples - Add '▶ Прослушать голос' button in Web UI for 1-click preview before generating - Add automated filter to strip LLM refusals and fall back to clean radio phrasing
This commit is contained in:
1 parent
e5afeb06cc
commit
4ed8e0c71a
7 files changed
+183
-24
No files matched your search
+32
-1
@@ -501,6 +501,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
// --- AI Radio Host Controller (Phase 3) ---
|
||||
const radioGenerateBtn = document.getElementById("radioGenerateBtn");
|
||||
const radioVoiceSelect = document.getElementById("radioVoiceSelect");
|
||||
const radioVoicePreviewBtn = document.getElementById("radioVoicePreviewBtn");
|
||||
const radioStatusLabel = document.getElementById("radioStatusLabel");
|
||||
const radioClipsCountLabel = document.getElementById("radioClipsCountLabel");
|
||||
const radioSegmentsContainer = document.getElementById("radioSegmentsContainer");
|
||||
@@ -508,10 +509,29 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
fetchRadioStatus();
|
||||
|
||||
// Voice sample preview playback
|
||||
if (radioVoicePreviewBtn) {
|
||||
radioVoicePreviewBtn.addEventListener("click", () => {
|
||||
const selectedVoice = radioVoiceSelect ? radioVoiceSelect.value : "ru_RU-dmitri-medium";
|
||||
const audioUrl = `api/radio/preview-voice?voice=${encodeURIComponent(selectedVoice)}`;
|
||||
const selectedOption = (radioVoiceSelect && radioVoiceSelect.options[radioVoiceSelect.selectedIndex])
|
||||
? radioVoiceSelect.options[radioVoiceSelect.selectedIndex].text
|
||||
: "Голос ведущего";
|
||||
playPreviewTrack(
|
||||
audioUrl,
|
||||
`Сэмпл: ${selectedOption}`,
|
||||
"AI Радио (Предпрослушивание)",
|
||||
"",
|
||||
"",
|
||||
audioUrl
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (radioGenerateBtn) {
|
||||
radioGenerateBtn.addEventListener("click", async () => {
|
||||
radioGenerateBtn.disabled = true;
|
||||
const voice = radioVoiceSelect ? radioVoiceSelect.value : "ru-RU-DmitryNeural";
|
||||
const voice = radioVoiceSelect ? radioVoiceSelect.value : "ru_RU-dmitri-medium";
|
||||
if (radioStatusLabel) {
|
||||
radioStatusLabel.textContent = "🎙️ Ведущий пишет реплики и сводит эфир...";
|
||||
radioStatusLabel.className = "badge badge-llm";
|
||||
@@ -560,6 +580,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (radioClipsCountLabel) {
|
||||
radioClipsCountLabel.textContent = `${data.speech_clips_count || 0} записей`;
|
||||
}
|
||||
|
||||
// Populate voice selector dynamically
|
||||
if (radioVoiceSelect && data.available_voices && data.available_voices.length > 0) {
|
||||
const currentVal = radioVoiceSelect.value || data.default_voice;
|
||||
radioVoiceSelect.innerHTML = data.available_voices.map(v => `
|
||||
<option value="${escapeHtml(v.id)}" ${v.id === currentVal ? "selected" : ""}>
|
||||
🎙️ ${escapeHtml(v.name)}
|
||||
</option>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
if (radioStatusLabel) {
|
||||
if (data.playlist_ready && data.speech_clips_count > 0) {
|
||||
radioStatusLabel.textContent = "🟢 Эфир в ротации";
|
||||
|
||||
Reference in new issue
Block a user