| import argparse |
| import json |
| import os |
| import uuid |
| from pathlib import Path |
|
|
| |
| os.environ["TORCHDYNAMO_DISABLE"] = "1" |
| os.environ["TORCHINDUCTOR_DISABLE"] = "1" |
|
|
| import torch._dynamo as dynamo |
|
|
| dynamo.config.suppress_errors = True |
|
|
| import gradio as gr |
| import numpy as np |
| import soundfile as sf |
| import spaces |
| import torch |
|
|
| from voxtream.config import SpeechGeneratorConfig |
| from voxtream.generator import SpeechGenerator |
| from voxtream.utils.app import ( |
| CUSTOM_CSS, |
| AppConfig, |
| GenerationControl, |
| SharedGenerationState, |
| SpeakingRateState, |
| VisualizationState, |
| build_low_latency_audio_head, |
| clear_outputs, |
| empty_rate_plot, |
| float32_to_int16, |
| load_app_config, |
| render_audio_stream, |
| render_text_progress, |
| ) |
| from voxtream.utils.generator import DTYPE_MAP, existing_file, text_generator |
| from voxtream.utils.generator.helpers import autocast_ctx |
| from voxtream.utils.generator.text import build_text_progress_metadata |
|
|
|
|
| def generation_button_updates(running: bool, paused: bool = False): |
| if not running: |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
| if paused: |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True, interactive=True), |
| gr.update(visible=True, interactive=True), |
| ) |
| return ( |
| gr.update(visible=True, interactive=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| ) |
|
|
|
|
| def ensure_generator_on_cuda(speech_generator: SpeechGenerator) -> None: |
| if not torch.cuda.is_available(): |
| return |
| if next(speech_generator.model.parameters()).device.type != "cpu": |
| return |
|
|
| dtype = DTYPE_MAP["cuda"] |
| speech_generator.model.to("cuda", dtype=dtype) |
| speech_generator.mimi.to("cuda", dtype=dtype) |
| speech_generator.ctx.mimi_prompt.to("cuda", dtype=dtype) |
| speech_generator.ctx.spk_enc.to("cuda", dtype=dtype) |
| speech_generator.ctx.device = "cuda" |
| speech_generator.ctx.dtype = dtype |
| speech_generator._autocast_ctx = autocast_ctx(device="cuda", dtype=dtype) |
|
|
| if speech_generator._mimi_streaming_started: |
| speech_generator._mimi_stream_ctx.__exit__(None, None, None) |
| speech_generator._mimi_stream_ctx = None |
| speech_generator._mimi_streaming_started = False |
|
|
|
|
| def demo_app( |
| config: SpeechGeneratorConfig, |
| app_config: AppConfig, |
| demo_examples, |
| synthesize_fn, |
| speaking_rate_state: SpeakingRateState, |
| generation_control: GenerationControl, |
| shared_generation_state: SharedGenerationState, |
| ): |
| with gr.Blocks( |
| css=CUSTOM_CSS, |
| head=build_low_latency_audio_head(app_config), |
| title="VoXtream2", |
| ) as demo: |
| gr.Markdown("# VoXtream2 TTS demo") |
| gr.Markdown( |
| "⚠️ The initial latency can be high due to deployment on ZeroGPU. " |
| "For faster inference, please try local deployment." |
| ) |
|
|
| with gr.Row(equal_height=True, elem_id="cols"): |
| with gr.Column(scale=1, elem_id="left-col"): |
| prompt_audio = gr.Audio( |
| sources=["microphone", "upload"], |
| type="filepath", |
| label=( |
| "Prompt audio (3-10 sec of target voice. " |
| f"Max {config.max_prompt_sec} sec)" |
| ), |
| ) |
| with gr.Accordion("Advanced options", open=False): |
| enable_speaking_rate = gr.Checkbox( |
| label="Use speaking rate control", value=True |
| ) |
| prompt_enhancement = gr.Checkbox( |
| label="Prompt enhancement", value=False |
| ) |
| prompt_enhancement_msg = gr.Markdown( |
| "⚠️ First 3-5 runs may have higher latency due to model " |
| "loading and warmup.", |
| visible=False, |
| ) |
| voice_activity_detection = gr.Checkbox( |
| label="Voice activity detection", value=False |
| ) |
| streaming_input = gr.Checkbox(label="Streaming input", value=False) |
|
|
| with gr.Column(scale=1, elem_id="right-col"): |
| target_text = gr.Textbox( |
| lines=4, |
| max_length=config.max_phone_tokens, |
| label=( |
| "Target text (Required, " |
| f"max {config.max_phone_tokens} chars)" |
| ), |
| placeholder="What you want the model to say", |
| ) |
| output_audio = gr.Audio( |
| label="Synthesized audio", |
| interactive=False, |
| streaming=False, |
| autoplay=False, |
| show_download_button=True, |
| show_share_button=False, |
| visible=False, |
| ) |
| stream_audio = gr.HTML( |
| render_audio_stream(app_config), elem_id="audio-stream-container" |
| ) |
|
|
| with gr.Row(): |
| clear_btn = gr.Button("Clear", elem_id="clear", variant="secondary") |
| submit_btn = gr.Button( |
| "Submit", elem_id="submit", variant="primary", interactive=False |
| ) |
| pause_btn = gr.Button( |
| "Pause", elem_id="pause", variant="secondary", visible=False |
| ) |
| resume_btn = gr.Button( |
| "Resume", elem_id="resume", variant="primary", visible=False |
| ) |
| stop_btn = gr.Button("Stop", elem_id="stop", variant="stop", visible=False) |
|
|
| validation_msg = gr.Markdown("", visible=False) |
| speaking_rate_control = gr.Slider( |
| minimum=app_config.speaking_rate_min, |
| maximum=app_config.speaking_rate_max, |
| step=app_config.speaking_rate_step, |
| value=app_config.speaking_rate_default, |
| label="Speaking rate (SPS). Change the speed of speech synthesis in real-time. ", |
| ) |
| rate_plot = gr.HTML(empty_rate_plot(app_config), elem_id="rate-plot-container") |
| text_progress = gr.HTML( |
| render_text_progress(app_config, None), elem_id="text-progress-container" |
| ) |
| generation_session = gr.State("") |
|
|
| def validate_inputs(audio, ttext): |
| if not audio: |
| return gr.update( |
| visible=True, value="⚠️ Please provide a prompt audio." |
| ), gr.update(interactive=False) |
| if not ttext or not ttext.strip(): |
| return gr.update( |
| visible=True, value="⚠️ Please provide target text." |
| ), gr.update(interactive=False) |
| return gr.update(visible=False, value=""), gr.update(interactive=True) |
|
|
| enable_speaking_rate.change( |
| fn=lambda enabled: gr.update(interactive=enabled), |
| inputs=enable_speaking_rate, |
| outputs=speaking_rate_control, |
| ) |
| prompt_enhancement.change( |
| fn=lambda enabled: gr.update(visible=enabled), |
| inputs=prompt_enhancement, |
| outputs=prompt_enhancement_msg, |
| ) |
|
|
| def update_speaking_rate(value, session_id): |
| speaking_rate_state.update(value) |
| shared_generation_state.update_speaking_rate(session_id, value) |
|
|
| speaking_rate_control.input( |
| fn=update_speaking_rate, |
| inputs=[speaking_rate_control, generation_session], |
| queue=False, |
| show_progress="hidden", |
| ) |
| speaking_rate_control.release( |
| fn=update_speaking_rate, |
| inputs=[speaking_rate_control, generation_session], |
| queue=False, |
| show_progress="hidden", |
| ) |
|
|
| for inp in [prompt_audio, target_text]: |
| inp.change( |
| fn=validate_inputs, |
| inputs=[prompt_audio, target_text], |
| outputs=[validation_msg, submit_btn], |
| ) |
|
|
| def prepare_generation(speaking_rate, enable_rate): |
| session_id = shared_generation_state.create(speaking_rate) |
| generation_control.start() |
| speaking_rate_state.start(speaking_rate) |
| return ( |
| gr.update(value=None, visible=False), |
| gr.update(interactive=False), |
| empty_rate_plot(app_config, show_target=enable_rate), |
| render_text_progress(app_config, None), |
| render_audio_stream(app_config, session_id=session_id), |
| *generation_button_updates(running=True), |
| session_id, |
| ) |
|
|
| submit_btn.click( |
| fn=prepare_generation, |
| inputs=[speaking_rate_control, enable_speaking_rate], |
| outputs=[ |
| output_audio, |
| enable_speaking_rate, |
| rate_plot, |
| text_progress, |
| stream_audio, |
| pause_btn, |
| resume_btn, |
| stop_btn, |
| generation_session, |
| ], |
| show_progress="hidden", |
| ).then( |
| fn=synthesize_fn, |
| inputs=[ |
| prompt_audio, |
| target_text, |
| prompt_enhancement, |
| voice_activity_detection, |
| streaming_input, |
| speaking_rate_control, |
| enable_speaking_rate, |
| generation_session, |
| ], |
| outputs=[ |
| output_audio, |
| enable_speaking_rate, |
| rate_plot, |
| text_progress, |
| stream_audio, |
| pause_btn, |
| resume_btn, |
| stop_btn, |
| generation_session, |
| ], |
| ) |
|
|
| def pause_generation(session_id): |
| generation_control.pause() |
| shared_generation_state.pause(session_id) |
| return generation_button_updates(running=True, paused=True) |
|
|
| def resume_generation(session_id): |
| generation_control.resume() |
| shared_generation_state.resume(session_id) |
| return generation_button_updates(running=True) |
|
|
| def stop_generation(session_id): |
| generation_control.stop() |
| speaking_rate_state.stop() |
| shared_generation_state.stop(session_id) |
| return generation_button_updates(running=False) |
|
|
| pause_btn.click( |
| fn=pause_generation, |
| inputs=generation_session, |
| outputs=[pause_btn, resume_btn, stop_btn], |
| js=( |
| "() => { if (window.voxtreamLowLatencyAudio) { " |
| "window.voxtreamLowLatencyAudio.pause(); } return []; }" |
| ), |
| queue=False, |
| ) |
| resume_btn.click( |
| fn=resume_generation, |
| inputs=generation_session, |
| outputs=[pause_btn, resume_btn, stop_btn], |
| js=( |
| "() => { if (window.voxtreamLowLatencyAudio) { " |
| "window.voxtreamLowLatencyAudio.resume(); } return []; }" |
| ), |
| queue=False, |
| ) |
| stop_btn.click( |
| fn=stop_generation, |
| inputs=generation_session, |
| outputs=[pause_btn, resume_btn, stop_btn], |
| js=( |
| "() => { if (window.voxtreamLowLatencyAudio) { " |
| "window.voxtreamLowLatencyAudio.stop(); } return []; }" |
| ), |
| queue=False, |
| ) |
|
|
| def clear_generation(session_id): |
| generation_control.stop() |
| speaking_rate_state.stop() |
| shared_generation_state.stop(session_id) |
| return ( |
| gr.update(value=None), |
| gr.update(value=""), |
| gr.update(value=None, visible=False), |
| gr.update(visible=False, value=""), |
| gr.update(interactive=False), |
| gr.update(interactive=True), |
| empty_rate_plot(app_config), |
| render_text_progress(app_config, None), |
| render_audio_stream(app_config, session_id=uuid.uuid4().hex), |
| *generation_button_updates(running=False), |
| "", |
| ) |
|
|
| clear_btn.click( |
| fn=clear_generation, |
| inputs=generation_session, |
| outputs=[ |
| prompt_audio, |
| target_text, |
| output_audio, |
| validation_msg, |
| submit_btn, |
| enable_speaking_rate, |
| rate_plot, |
| text_progress, |
| stream_audio, |
| pause_btn, |
| resume_btn, |
| stop_btn, |
| generation_session, |
| ], |
| ) |
|
|
| gr.Markdown("### Examples") |
| ex = gr.Examples( |
| examples=demo_examples, |
| inputs=[ |
| prompt_audio, |
| target_text, |
| prompt_enhancement, |
| voice_activity_detection, |
| streaming_input, |
| speaking_rate_control, |
| enable_speaking_rate, |
| ], |
| outputs=[ |
| output_audio, |
| enable_speaking_rate, |
| rate_plot, |
| text_progress, |
| stream_audio, |
| pause_btn, |
| resume_btn, |
| stop_btn, |
| generation_session, |
| ], |
| fn=synthesize_fn, |
| cache_examples=False, |
| ) |
|
|
| ex.dataset.click( |
| fn=lambda: (*clear_outputs(app_config), ""), |
| inputs=[], |
| outputs=[ |
| output_audio, |
| rate_plot, |
| text_progress, |
| stream_audio, |
| generation_session, |
| ], |
| queue=False, |
| ).then( |
| fn=validate_inputs, |
| inputs=[prompt_audio, target_text], |
| outputs=[validation_msg, submit_btn], |
| queue=False, |
| ) |
|
|
| demo.queue(default_concurrency_limit=1).launch() |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "-c", |
| "--config", |
| type=existing_file, |
| help="Path to the config file", |
| default="configs/generator.json", |
| ) |
| parser.add_argument( |
| "--app-config", |
| type=existing_file, |
| help="Path to the app config file", |
| default="configs/app.json", |
| ) |
| parser.add_argument( |
| "--spk-rate-config", |
| type=existing_file, |
| help="Path to the speaking rate config file", |
| default="configs/speaking_rate.json", |
| ) |
| parser.add_argument( |
| "--examples-config", |
| type=existing_file, |
| help="Path to the examples config file", |
| default="assets/examples.json", |
| ) |
| args = parser.parse_args() |
|
|
| with open(args.config) as f: |
| config = SpeechGeneratorConfig(**json.load(f)) |
| config.hf_token = os.environ.get("TOKEN") |
|
|
| |
| torch.hub.load( |
| config.spk_enc_repo, |
| config.spk_enc_model, |
| model_name=config.spk_enc_model_name, |
| train_type=config.spk_enc_train_type, |
| dataset=config.spk_enc_dataset, |
| trust_repo=True, |
| verbose=False, |
| ) |
|
|
| with open(args.spk_rate_config) as f: |
| spk_rate_config = json.load(f) |
|
|
| app_config = load_app_config(args.app_config) |
|
|
| with open(args.examples_config) as f: |
| examples_config = json.load(f) |
| demo_examples = examples_config.get("examples", []) |
|
|
| speech_generator = SpeechGenerator(config, spk_rate_config) |
| speaking_rate_state = SpeakingRateState(app_config.speaking_rate_default) |
| generation_control = GenerationControl() |
| shared_generation_state = SharedGenerationState() |
| chunk_size = int(config.mimi_sr * app_config.min_chunk_sec) |
|
|
| @spaces.GPU |
| def synthesize_fn( |
| prompt_audio_path, |
| target_text, |
| prompt_enhancement, |
| voice_activity_detection, |
| streaming_input, |
| speaking_rate_control, |
| enable_speaking_rate=True, |
| generation_session_id="", |
| ): |
| control_session_id = generation_session_id or shared_generation_state.create( |
| speaking_rate_control |
| ) |
| stream_session_id = control_session_id or uuid.uuid4().hex |
| stream_seq = 0 |
|
|
| if not prompt_audio_path or not target_text: |
| speaking_rate_state.stop() |
| generation_control.finish() |
| shared_generation_state.finish(control_session_id) |
| yield ( |
| gr.update(value=None, visible=False), |
| gr.update(interactive=True), |
| empty_rate_plot(app_config, show_target=enable_speaking_rate), |
| render_text_progress(app_config, None), |
| render_audio_stream(app_config, session_id=stream_session_id), |
| *generation_button_updates(running=False), |
| control_session_id, |
| ) |
| return |
|
|
| if shared_generation_state.is_stopped(control_session_id): |
| speaking_rate_state.stop() |
| generation_control.finish() |
| shared_generation_state.finish(control_session_id) |
| yield ( |
| gr.update(value=None, visible=False), |
| gr.update(interactive=True), |
| empty_rate_plot(app_config, show_target=enable_speaking_rate), |
| render_text_progress(app_config, None), |
| render_audio_stream( |
| app_config, |
| session_id=stream_session_id, |
| active=False, |
| final=True, |
| ), |
| *generation_button_updates(running=False), |
| control_session_id, |
| ) |
| return |
|
|
| ensure_generator_on_cuda(speech_generator) |
| speaking_rate_state.ensure_started(speaking_rate_control) |
| speaking_rate_gen = ( |
| shared_generation_state.speaking_rate_values( |
| control_session_id, speaking_rate_control |
| ) |
| if enable_speaking_rate |
| else None |
| ) |
| text_metadata = build_text_progress_metadata( |
| target_text, |
| config=config, |
| phone_to_token=speech_generator.ctx.phone_to_token, |
| phonemizer=speech_generator.ctx.phonemizer, |
| max_phone_tokens=config.max_phone_tokens, |
| ) |
| rate_window_sec = ( |
| config.spk_rate_window_sec |
| if config.spk_rate_window_sec and config.spk_rate_window_sec > 0 |
| else app_config.plot_window_sec |
| ) |
| frame_sec = config.mimi_frame_ms / 1000.0 |
| text_progress_delay_sec = ( |
| app_config.audio_stream_start_delay_sec |
| + config.audio_delay_frames * frame_sec |
| ) |
| visualization = VisualizationState( |
| text_metadata=text_metadata, |
| app_config=app_config, |
| rate_window_sec=rate_window_sec, |
| frame_sec=frame_sec, |
| text_progress_delay_sec=text_progress_delay_sec, |
| show_target=enable_speaking_rate, |
| ) |
|
|
| stream = speech_generator.generate_stream( |
| prompt_audio_path=Path(prompt_audio_path), |
| text=text_generator(target_text) if streaming_input else target_text, |
| speaking_rate=speaking_rate_gen, |
| enhance_prompt=prompt_enhancement, |
| apply_vad=voice_activity_detection, |
| return_progress=True, |
| min_streaming_rtf=app_config.min_streaming_rtf, |
| ) |
|
|
| buffer = [] |
| buffer_len = 0 |
| total_buffer = [] |
| stopped = False |
|
|
| stream_iter = iter(stream) |
| while True: |
| if not shared_generation_state.wait_if_paused(control_session_id): |
| stopped = True |
| break |
| try: |
| frame, _, progress = next(stream_iter) |
| except StopIteration: |
| break |
| if shared_generation_state.is_stopped(control_session_id): |
| stopped = True |
| break |
|
|
| buffer.append(frame) |
| total_buffer.append(frame) |
| buffer_len += frame.shape[0] |
| plot_update, text_update = visualization.update(progress) |
|
|
| if buffer_len >= chunk_size: |
| if shared_generation_state.is_stopped(control_session_id): |
| stopped = True |
| break |
| audio = np.concatenate(buffer) |
| stream_seq += 1 |
| yield ( |
| gr.update(), |
| gr.update(), |
| plot_update, |
| text_update, |
| render_audio_stream( |
| app_config, |
| session_id=stream_session_id, |
| seq=stream_seq, |
| sample_rate=config.mimi_sr, |
| audio=float32_to_int16(audio), |
| active=True, |
| ), |
| *generation_button_updates( |
| running=True, |
| paused=shared_generation_state.is_paused(control_session_id), |
| ), |
| control_session_id, |
| ) |
|
|
| buffer = [] |
| buffer_len = 0 |
|
|
| stopped = stopped or shared_generation_state.is_stopped(control_session_id) |
| if stopped and hasattr(stream, "close"): |
| stream.close() |
| final_text = visualization.final_text() |
|
|
| if buffer_len > 0 and not stopped: |
| final = np.concatenate(buffer) |
| nfade = min(int(config.mimi_sr * app_config.fade_out_sec), final.shape[0]) |
| if nfade > 0: |
| fade = np.linspace(1.0, 0.0, nfade, dtype=np.float32) |
| final[-nfade:] *= fade |
| stream_seq += 1 |
| yield ( |
| gr.update(), |
| gr.update(), |
| visualization.latest_plot, |
| visualization.latest_text, |
| render_audio_stream( |
| app_config, |
| session_id=stream_session_id, |
| seq=stream_seq, |
| sample_rate=config.mimi_sr, |
| audio=float32_to_int16(final), |
| active=True, |
| ), |
| *generation_button_updates( |
| running=True, |
| paused=shared_generation_state.is_paused(control_session_id), |
| ), |
| control_session_id, |
| ) |
|
|
| if len(total_buffer) > 0: |
| full_audio = np.concatenate(total_buffer) |
| nfade = min( |
| int(config.mimi_sr * app_config.fade_out_sec), full_audio.shape[0] |
| ) |
| if nfade > 0: |
| fade = np.linspace(1.0, 0.0, nfade, dtype=np.float32) |
| full_audio[-nfade:] *= fade |
|
|
| file_path = f"/tmp/voxtream_{uuid.uuid4().hex}.wav" |
| sf.write(file_path, float32_to_int16(full_audio), config.mimi_sr) |
|
|
| speaking_rate_state.stop() |
| generation_control.finish() |
| shared_generation_state.finish(control_session_id) |
| yield ( |
| gr.update(value=file_path, visible=True), |
| gr.update(interactive=True), |
| visualization.latest_plot, |
| final_text, |
| render_audio_stream( |
| app_config, |
| session_id=stream_session_id, |
| seq=stream_seq + 1, |
| sample_rate=config.mimi_sr, |
| active=False, |
| final=True, |
| ), |
| *generation_button_updates(running=False), |
| control_session_id, |
| ) |
| else: |
| speaking_rate_state.stop() |
| generation_control.finish() |
| shared_generation_state.finish(control_session_id) |
| yield ( |
| gr.update(value=None, visible=False), |
| gr.update(interactive=True), |
| visualization.latest_plot, |
| final_text, |
| render_audio_stream( |
| app_config, |
| session_id=stream_session_id, |
| seq=stream_seq + 1, |
| sample_rate=config.mimi_sr, |
| active=False, |
| final=True, |
| ), |
| *generation_button_updates(running=False), |
| control_session_id, |
| ) |
|
|
| demo_app( |
| config, |
| app_config, |
| demo_examples, |
| synthesize_fn, |
| speaking_rate_state, |
| generation_control, |
| shared_generation_state, |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|