Spaces:
Running on Zero
Running on Zero
| """Gradio Space: forced alignment with Qwen3-ForcedAligner-0.6B. | |
| Serves both: | |
| 1. A usable web UI for interactive alignment. | |
| 2. A programmatic API endpoint (`/align`), auto-exposed by Gradio, that | |
| other services can call via `gradio_client.Client` β see | |
| `examples/client_example.py`. | |
| """ | |
| from __future__ import annotations | |
| # `spaces` must be imported before `torch` (imported transitively via | |
| # `aligner`) so it can patch CUDA initialization for ZeroGPU Spaces. On | |
| # non-ZeroGPU hardware (or local dev), `spaces.GPU` is a harmless no-op. | |
| import spaces | |
| import gradio as gr | |
| from aligner import MAX_AUDIO_SECONDS, SUPPORTED_LANGUAGES, align | |
| DATAFRAME_HEADERS = ["#", "Text", "Start (s)", "End (s)"] | |
| EXAMPLE_AUDIO_URL = ( | |
| "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_zh.wav" | |
| ) | |
| EXAMPLE_TEXT = "ηθ³εΊη°δΊ€ζε δΉεζ»ηζ ε΅γ" | |
| def run_alignment(audio, text: str, language: str): | |
| """Event handler for the UI button, and the function exposed as the API. | |
| Returns (dataframe_rows, raw_json) so the UI shows both a readable | |
| table and the full structured payload; API callers get the same tuple. | |
| `@spaces.GPU` is required on ZeroGPU Spaces: it's how the ZeroGPU | |
| scheduler detects which functions need a GPU attached and allocates one | |
| for the duration of the call. Model loading happens lazily inside | |
| `aligner.get_model()`, on first call to `align()` below, so it also | |
| runs inside this GPU-attached window (required β CUDA calls made | |
| outside a `@spaces.GPU` call fail on ZeroGPU hardware). | |
| """ | |
| try: | |
| spans = align(audio=audio, text=text, language=language) | |
| except Exception as exc: # surface a clean error instead of a traceback | |
| raise gr.Error(str(exc)) from exc | |
| rows = [[s.index, s.text, round(s.start_time, 3), round(s.end_time, 3)] for s in spans] | |
| raw = [ | |
| {"index": s.index, "text": s.text, "start_time": s.start_time, "end_time": s.end_time} | |
| for s in spans | |
| ] | |
| return rows, raw | |
| with gr.Blocks(title="Qwen3 Forced Aligner") as demo: | |
| gr.Markdown( | |
| """ | |
| # Qwen3 Forced Aligner | |
| Align a text transcript to its audio and get per-unit start/end timestamps, | |
| powered by [Qwen/Qwen3-ForcedAligner-0.6B](https://huggingface.co/Qwen/Qwen3-ForcedAligner-0.6B). | |
| Upload or record audio, paste the matching transcript, pick the language, and align. | |
| This UI also doubles as an API β see the "Use via API" link at the bottom of the page. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| audio_input = gr.Audio( | |
| sources=["upload", "microphone"], | |
| type="filepath", | |
| label="Audio", | |
| ) | |
| text_input = gr.Textbox( | |
| label="Text to align", | |
| placeholder="Enter the transcript that matches the audio...", | |
| lines=4, | |
| ) | |
| language_input = gr.Dropdown( | |
| choices=SUPPORTED_LANGUAGES, | |
| value="English", | |
| label="Language", | |
| ) | |
| gr.Markdown( | |
| f"_Max audio length: ~{MAX_AUDIO_SECONDS // 60} minutes per request._" | |
| ) | |
| align_button = gr.Button("Align", variant="primary") | |
| with gr.Column(): | |
| output_table = gr.Dataframe( | |
| headers=DATAFRAME_HEADERS, | |
| datatype=["number", "str", "number", "number"], | |
| label="Aligned spans", | |
| wrap=True, | |
| ) | |
| with gr.Accordion("Raw JSON output", open=False): | |
| output_json = gr.JSON(label="Raw result") | |
| gr.Examples( | |
| examples=[[EXAMPLE_AUDIO_URL, EXAMPLE_TEXT, "Chinese"]], | |
| inputs=[audio_input, text_input, language_input], | |
| ) | |
| align_button.click( | |
| fn=run_alignment, | |
| inputs=[audio_input, text_input, language_input], | |
| outputs=[output_table, output_json], | |
| api_name="align", | |
| concurrency_limit=1, # one model instance on one GPU | |
| ) | |
| demo.queue() | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |