File size: 4,478 Bytes
005e075 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | import re
import tempfile
import time
from pathlib import Path
import gradio as gr
from modal_apps.modal_llm import PersonaLLM
from modal_apps.modal_tts import CharacterTTS
from src.character_registry import CHARACTER_PACKAGES, get_character
from src.stage_driver import render_character_stage
APP_CSS = """
#modal-demo-stage iframe, #modal-demo-stage { min-height: 460px; }
"""
def _character_choices() -> list[tuple[str, str]]:
return [(character["display_name"], character_id) for character_id, character in CHARACTER_PACKAGES.items()]
def _split_sentences(text: str) -> list[str]:
return [part.strip() for part in re.split(r"(?<=[。!?!?;;])\\s*", text) if part.strip()] or [text.strip()]
def _write_wav(audio: bytes, prefix: str = "vc_tts_") -> str:
handle = tempfile.NamedTemporaryFile(prefix=prefix, suffix=".wav", delete=False)
handle.write(audio)
handle.close()
return handle.name
def chat_once(message: str, history: list[dict], character_id: str, tts_enabled: bool):
if not message.strip():
yield history, None, {"status": "empty"}
return
character = get_character(character_id)
history = history + [{"role": "user", "content": message}, {"role": "assistant", "content": "Modal LLM 正在生成..."}]
yield history, None, {"status": "llm_generating"}
started = time.perf_counter()
llm_result = PersonaLLM().generate_text.remote(
user_text=message,
character=character,
max_new_tokens=120,
)
reply = llm_result["text"]
history[-1]["content"] = reply
debug = {
"status": "llm_done",
"llm_remote_s": llm_result.get("remote_s"),
"llm_output_tokens": llm_result.get("output_tokens"),
"client_elapsed_s": round(time.perf_counter() - started, 3),
}
yield history, None, debug
if not tts_enabled:
return
for index, sentence in enumerate(_split_sentences(reply), start=1):
if not sentence:
continue
debug = {**debug, "status": "tts_generating", "tts_sentence_index": index, "tts_sentence": sentence}
yield history, None, debug
tts_started = time.perf_counter()
audio = CharacterTTS().synthesize.remote(text=sentence, emotion="neutral")
audio_path = _write_wav(audio)
debug = {
**debug,
"status": "tts_chunk_done",
"tts_sentence_index": index,
"tts_remote_client_s": round(time.perf_counter() - tts_started, 3),
"audio_path": audio_path,
}
yield history, audio_path, debug
def switch_character(character_id: str):
character = get_character(character_id)
stage = {"expression": "idle", "motion": "breathe", "intensity": 0.35}
return character["summary"], render_character_stage(character, stage)
def build_demo() -> gr.Blocks:
default_id = "memory_girl"
default_character = get_character(default_id)
default_stage = {"expression": "idle", "motion": "breathe", "intensity": 0.35}
with gr.Blocks(title="Modal Virtual Character Smoke Demo") as demo:
with gr.Row():
with gr.Column(scale=1, min_width=260):
character_select = gr.Radio(_character_choices(), value=default_id, label="角色")
character_summary = gr.Markdown(default_character["summary"])
tts_enabled = gr.Checkbox(value=True, label="启用 Chatterbox TTS")
with gr.Column(scale=2, min_width=360):
stage = gr.HTML(
render_character_stage(default_character, default_stage),
elem_id="modal-demo-stage",
min_height=460,
)
with gr.Column(scale=2, min_width=360):
chatbot = gr.Chatbot(label="Modal 对话", height=380)
message = gr.Textbox(label="输入", lines=2, submit_btn=True)
audio = gr.Audio(label="分句语音", autoplay=True)
debug = gr.JSON(label="调试")
character_select.change(switch_character, inputs=[character_select], outputs=[character_summary, stage])
message.submit(
chat_once,
inputs=[message, chatbot, character_select, tts_enabled],
outputs=[chatbot, audio, debug],
).then(lambda: "", outputs=[message])
return demo
if __name__ == "__main__":
build_demo().queue().launch(css=APP_CSS, server_name="127.0.0.1", server_port=7862)
|