penguinfish1688 commited on
Commit
e98ae76
·
verified ·
1 Parent(s): 4123b95

Include local Gradio app and standalone installation guide

Browse files
Files changed (3) hide show
  1. README.md +16 -1
  2. app.py +132 -0
  3. requirements-demo.txt +3 -0
README.md CHANGED
@@ -23,7 +23,7 @@ model-index:
23
  XML dialogue and two selectable voice templates. The output is a normalized
24
  24 kHz stereo WAV: **speaker A on the left, speaker B on the right**.
25
 
26
- [Try the interactive demo](https://huggingface.co/spaces/penguinfish1688/duplexdataengine-demo)
27
 
28
  This repository contains a standalone Stage III inference release: merged
29
  model weights, the deterministic XML interleaver, the audio codec, voice
@@ -72,6 +72,21 @@ the shorter finished lane is zero-padded. CPU execution is available through
72
  `--device cpu`, but is much slower. Hardware and timing verification are in
73
  `verification.json` once the public download check is complete.
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  ## How to write the XML
76
 
77
  Use `<A>` and `<B>` for the two speakers. Exactly **one turn has no `start`**:
 
23
  XML dialogue and two selectable voice templates. The output is a normalized
24
  24 kHz stereo WAV: **speaker A on the left, speaker B on the right**.
25
 
26
+ The interactive app is included as `app.py`; see **Run the interactive demo** below.
27
 
28
  This repository contains a standalone Stage III inference release: merged
29
  model weights, the deterministic XML interleaver, the audio codec, voice
 
72
  `--device cpu`, but is much slower. Hardware and timing verification are in
73
  `verification.json` once the public download check is complete.
74
 
75
+ ## Run the interactive demo
76
+
77
+ The downloadable Gradio app provides editable XML, independent voice selectors,
78
+ reference previews, generation controls, and the two featured examples:
79
+
80
+ ```bash
81
+ pip install -r requirements-demo.txt
82
+ python app.py
83
+ ```
84
+
85
+ Open the local URL printed by Gradio. A GPU is recommended. Set `DUDE_DEVICE=cpu`
86
+ to use CPU inference, which is much slower. The same app can be deployed to a
87
+ Hugging Face Gradio Space with appropriate hosting access. Hosting availability
88
+ does not affect public downloads or local inference.
89
+
90
  ## How to write the XML
91
 
92
  Use `<A>` and `<B>` for the two speakers. Exactly **one turn has no `start`**:
app.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public DuDE demo; model/code/assets are independent of the research repo."""
2
+ import spaces # Enable ZeroGPU before importing torch.
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import tempfile
7
+
8
+ import gradio as gr
9
+ from huggingface_hub import snapshot_download
10
+
11
+ from dude_tts import DuDE
12
+
13
+ MODEL_ID = 'penguinfish1688/duplexdataengine'
14
+ LOCAL = Path(os.environ.get('DUDE_MODEL_DIR', Path(__file__).resolve().parent))
15
+ ROOT = LOCAL if (LOCAL/'release.json').is_file() else Path(
16
+ snapshot_download(MODEL_ID, revision=os.environ.get('DUDE_MODEL_REVISION')))
17
+ EXAMPLES = json.loads((ROOT/'examples/examples.json').read_text())
18
+ PRESETS = json.loads((ROOT/'voices/presets.json').read_text())
19
+ CHOICES = [(value['label'], key) for key, value in PRESETS.items()]
20
+ MODEL = DuDE.from_pretrained(ROOT, device=os.environ.get('DUDE_DEVICE', 'cuda'))
21
+
22
+
23
+ @spaces.GPU(duration=120)
24
+ def synthesize(xml, voice_a, voice_b, seed, max_seconds):
25
+ try:
26
+ result = MODEL.generate(xml, voice_a, voice_b, seed=int(seed), max_seconds=float(max_seconds))
27
+ except ValueError as error:
28
+ raise gr.Error(str(error)) from error
29
+ output = Path(tempfile.mkdtemp(prefix='dude-output-'))/'dialogue.wav'
30
+ result.save(output)
31
+ status = 'Both speakers emitted EOS.' if all(result.eos) else (
32
+ 'The safety limit was reached before both speakers finished. Increase it or shorten the dialogue.')
33
+ return str(output), str(output), status
34
+
35
+
36
+ def preset_preview(voice):
37
+ return str(ROOT/PRESETS[voice]['preview'])
38
+
39
+
40
+ def load_example(index):
41
+ example = EXAMPLES[index]
42
+ return ((ROOT/example['xml']).read_text(), example['voice_a'], example['voice_b'],
43
+ example['seed'], example['max_seconds'])
44
+
45
+
46
+ XML_GUIDE = '''
47
+ ### Write a two-speaker dialogue
48
+
49
+ Use `<A>` and `<B>` for speakers. Exactly one turn has **no `start`**: the root.
50
+ Every later turn references an inline anchor with `start="e1"`. Define each
51
+ anchor once as `<e1/>`, `<e2/>`, etc. The `<duplex>` wrapper is optional.
52
+
53
+ **Turn-taking:** put the anchor at the preceding turn's end.
54
+ ```xml
55
+ <duplex>
56
+ <A>Hello! How was your day?<e1/></A>
57
+ <B start="e1">Pretty good. I finished the project.<e2/></B>
58
+ <A start="e2">That's great! <laugh/></A>
59
+ </duplex>
60
+ ```
61
+
62
+ **Overlap:** put the anchor inside a turn so the other speaker enters there.
63
+ ```xml
64
+ <duplex>
65
+ <A>I walked to the park, <e1/>and then stopped for coffee.<e2/></A>
66
+ <B start="e1">Mm-hmm.</B>
67
+ <B start="e2">That sounds nice.</B>
68
+ </duplex>
69
+ ```
70
+
71
+ Anchors indicate text dependencies, not timestamps. Exact timing is learned and
72
+ can vary. Same-speaker collisions are serialized in XML order. Available events:
73
+ `<laugh/>`, `<cough/>`, `<breath/>`, `<sigh/>`, `<cry/>`, `<sneeze/>`, `<gasp/>`.
74
+ Write spoken backchannels as ordinary text. Escape literal `&` as `&amp;`.
75
+ Do not add `type`, timestamp, or duration attributes. Each anchor must be unique
76
+ and all `start` references must exist. Cycles and multiple roots are rejected.
77
+ '''
78
+
79
+
80
+ with gr.Blocks(title='DuDE — Duplex Speech', delete_cache=(3600, 3600)) as demo:
81
+ gr.Markdown('''# Two voices. One conversation.
82
+ **DuDE by Cheng-Kuang Chang** turns an XML dialogue into synchronized stereo speech.
83
+ Choose one voice for each speaker. **A plays on the left; B on the right.**
84
+ [Download the model and inference code](https://huggingface.co/penguinfish1688/duplexdataengine)
85
+ ''')
86
+ with gr.Accordion('How to write the XML', open=True):
87
+ gr.Markdown(XML_GUIDE)
88
+ initial = load_example(0)
89
+ xml = gr.Code(value=initial[0], language='html', label='Dialogue XML', lines=15)
90
+ with gr.Row():
91
+ voice_a = gr.Dropdown(CHOICES, value=initial[1], label='Speaker A — left')
92
+ voice_b = gr.Dropdown(CHOICES, value=initial[2], label='Speaker B — right')
93
+ with gr.Row():
94
+ preview_a = gr.Audio(value=preset_preview(initial[1]), label='Voice A reference', interactive=False)
95
+ preview_b = gr.Audio(value=preset_preview(initial[2]), label='Voice B reference', interactive=False)
96
+ voice_a.change(preset_preview, voice_a, preview_a, queue=False)
97
+ voice_b.change(preset_preview, voice_b, preview_b, queue=False)
98
+ with gr.Accordion('Generation settings', open=False):
99
+ seed = gr.Number(value=initial[3], precision=0, label='Random seed')
100
+ maximum = gr.Slider(10, 180, value=initial[4], step=1, label='Audio safety limit (seconds; not target duration)')
101
+ generate = gr.Button('Generate stereo speech', variant='primary')
102
+ output = gr.Audio(type='filepath', label='Generated conversation', interactive=False)
103
+ download = gr.File(label='Download stereo WAV')
104
+ status = gr.Textbox(label='Generation status', interactive=False)
105
+ generate.click(synthesize, [xml, voice_a, voice_b, seed, maximum], [output, download, status],
106
+ api_name='generate', concurrency_limit=1)
107
+ gr.Markdown('## Listen to the featured examples\nThese are the original checkpoint-8565 outputs.')
108
+ for index, example in enumerate(EXAMPLES):
109
+ gr.Markdown(f'### {example["id"]}-student')
110
+ gr.Audio(value=str(ROOT/example['audio']), label='Normalized stereo example', interactive=False)
111
+ with gr.Accordion('Show complete input XML', open=False):
112
+ gr.Code(value=(ROOT/example['xml']).read_text(), language='html', interactive=False)
113
+ gr.Button('Use this example').click(lambda i=index: load_example(i), outputs=[xml, voice_a, voice_b, seed, maximum], queue=False)
114
+ gr.Markdown('''### Notes and citation
115
+ English is the evaluated language. Words, pauses, vocal events, and overlaps can be imperfect.
116
+ Ten held-out conversations all emitted EOS on both lanes; automated WER was 15.70%,
117
+ but the timing check did not pass. See the model card for the full evaluation.
118
+ Model and reference assets are for noncommercial use under CC BY-NC 4.0.
119
+ References: Qwen3-TTS and Meta Seamless Interaction.
120
+
121
+ ```bibtex
122
+ @misc{chang2026dude,
123
+ author = {Cheng-Kuang Chang},
124
+ title = {DuDE: XML-conditioned Duplex Text-to-Speech},
125
+ year = {2026},
126
+ url = {https://huggingface.co/penguinfish1688/duplexdataengine}
127
+ }
128
+ ```
129
+ ''')
130
+
131
+ if __name__ == '__main__':
132
+ demo.queue(max_size=20).launch()
requirements-demo.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ -r requirements.txt
2
+ gradio==6.17.3
3
+ spaces