Reduce post-synthesis UI flicker by removing 1s status timer.
Refresh status every 60s only, shorten synth log, update log before audio, and isolate repaint regions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ Gradio Web 中控:音色锁定 → Whisper 识别 → Gemma4 润色 → ChatTT
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
@@ -127,16 +128,27 @@ def ui_check_ollama() -> str:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 模块 4:ChatTTS 音频合成
|
# 模块 4:ChatTTS 音频合成
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def ui_synthesize(polished_text: str, voice_label: str) -> tuple[str | None, str]:
|
def _short_synth_log(msg: str, ok: bool) -> str:
|
||||||
"""【TTS 合成】生成最终 wav 配音文件。"""
|
"""合成日志简短显示,避免长路径触发大面积重绘闪屏。"""
|
||||||
|
if not ok:
|
||||||
|
return f"❌ {msg}"
|
||||||
|
|
||||||
|
chars = re.search(r"朗读\s*(\d+)\s*字", msg)
|
||||||
|
segs = re.search(r"共\s*(\d+)\s*段", msg)
|
||||||
|
if chars:
|
||||||
|
seg_note = f",{segs.group(1)} 段拼接" if segs else ""
|
||||||
|
return f"✅ 配音完成({chars.group(1)} 字{seg_note})。请用下方播放器试听、下载。"
|
||||||
|
return "✅ 配音完成。请用下方播放器试听、下载。"
|
||||||
|
|
||||||
|
|
||||||
|
def ui_synthesize(polished_text: str, voice_label: str) -> tuple[str, str | None]:
|
||||||
|
"""【TTS 合成】生成最终 wav 配音文件。先更新日志再更新播放器,减轻闪屏。"""
|
||||||
if not polished_text or not polished_text.strip():
|
if not polished_text or not polished_text.strip():
|
||||||
return None, "请先完成 Gemma4 润色。"
|
return "请先完成 Gemma4 润色。", None
|
||||||
|
|
||||||
voice_id = label_to_voice_id(voice_label)
|
voice_id = label_to_voice_id(voice_label)
|
||||||
ok, msg, wav_path = generate_voice(polished_text, voice_id=voice_id)
|
ok, msg, wav_path = generate_voice(polished_text, voice_id=voice_id)
|
||||||
if ok and wav_path:
|
return _short_synth_log(msg, ok), wav_path if ok else None
|
||||||
return wav_path, f"✅ {msg}"
|
|
||||||
return None, f"❌ {msg}"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -863,10 +875,25 @@ gradio-app,
|
|||||||
.status-err { border-left-color: #ef4444 !important; background: #2a1515 !important; }
|
.status-err { border-left-color: #ef4444 !important; background: #2a1515 !important; }
|
||||||
.status-err .status-body { color: #fecaca !important; }
|
.status-err .status-body { color: #fecaca !important; }
|
||||||
|
|
||||||
|
/* 状态栏与播放器隔离重绘,减轻合成完成后的闪屏 */
|
||||||
|
.status-row {
|
||||||
|
contain: layout style paint;
|
||||||
|
min-height: 88px;
|
||||||
|
}
|
||||||
.gradio-container .audio-container,
|
.gradio-container .audio-container,
|
||||||
.gradio-container .upload-container {
|
.gradio-container .upload-container {
|
||||||
border: 2px dashed #4b5563 !important;
|
border: 2px dashed #4b5563 !important;
|
||||||
background: #1a2332 !important;
|
background: #1a2332 !important;
|
||||||
|
contain: layout style paint;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
.gradio-container .audio-container audio,
|
||||||
|
.gradio-container .audio-container canvas,
|
||||||
|
.gradio-container .waveform-container {
|
||||||
|
background: #1a2332 !important;
|
||||||
|
}
|
||||||
|
.gradio-container .pipeline-step-card textarea {
|
||||||
|
contain: layout style;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer { visibility: hidden; }
|
footer { visibility: hidden; }
|
||||||
@@ -918,11 +945,8 @@ def ui_check_ollama_html(force: bool = False) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def ui_initial_load() -> tuple[str, str]:
|
def ui_initial_load() -> tuple[str, str]:
|
||||||
"""首屏立即返回,不发起网络请求,避免平板白屏等待。"""
|
"""首屏加载:检测 Ollama + 音色(仅一次,不用高频 Timer 避免闪屏)。"""
|
||||||
return (
|
return ui_refresh_status_html(force=False)
|
||||||
_status_html("Ollama 节点", "后台检测中,请稍候…", "warn"),
|
|
||||||
ui_speaker_status_html(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def ui_refresh_status_html(force: bool = False) -> tuple[str, str]:
|
def ui_refresh_status_html(force: bool = False) -> tuple[str, str]:
|
||||||
@@ -998,19 +1022,13 @@ def build_app() -> gr.Blocks:
|
|||||||
outputs=[ollama_status, speaker_status],
|
outputs=[ollama_status, speaker_status],
|
||||||
)
|
)
|
||||||
|
|
||||||
# 首屏秒开:仅本地检测音色,Ollama 延后到 Timer
|
|
||||||
demo.load(
|
demo.load(
|
||||||
fn=ui_initial_load,
|
fn=ui_initial_load,
|
||||||
outputs=[ollama_status, speaker_status],
|
outputs=[ollama_status, speaker_status],
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1 秒后后台检测 Ollama;之后每 30s 刷新(30s 内走缓存)
|
# 仅低频刷新状态(去掉 1s Timer,避免合成后整页闪屏)
|
||||||
status_timer = gr.Timer(value=1, active=True)
|
status_timer_slow = gr.Timer(value=60, active=True)
|
||||||
status_timer.tick(
|
|
||||||
fn=lambda: ui_refresh_status_html(force=False),
|
|
||||||
outputs=[ollama_status, speaker_status],
|
|
||||||
)
|
|
||||||
status_timer_slow = gr.Timer(value=30, active=True)
|
|
||||||
status_timer_slow.tick(
|
status_timer_slow.tick(
|
||||||
fn=lambda: ui_refresh_status_html(force=True),
|
fn=lambda: ui_refresh_status_html(force=True),
|
||||||
outputs=[ollama_status, speaker_status],
|
outputs=[ollama_status, speaker_status],
|
||||||
@@ -1100,7 +1118,8 @@ def build_app() -> gr.Blocks:
|
|||||||
synth_btn.click(
|
synth_btn.click(
|
||||||
ui_synthesize,
|
ui_synthesize,
|
||||||
[polished_text, tts_voice],
|
[polished_text, tts_voice],
|
||||||
[output_audio, synth_log],
|
[synth_log, output_audio],
|
||||||
|
show_progress="hidden",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- Tab 3: 一键生产 ----
|
# ---- Tab 3: 一键生产 ----
|
||||||
|
|||||||
Reference in New Issue
Block a user