Spaces:
Sleeping
Sleeping
Build CPU-capable Speaking MIDI audio and VoiceDesign demo
Browse files- README.md +11 -7
- app.py +13 -5
- deploy.py +4 -3
- requirements.txt +3 -3
- zero_tts.py +25 -0
README.md
CHANGED
|
@@ -55,25 +55,29 @@ All four spectrogram panels use the same color scale. Metrics compare the input-
|
|
| 55 |
|
| 56 |
The fitting core is vendored from [Speaking MIDI](https://github.com/k-l-lambda/speaking-midi/tree/932f8eb), with a portable SoundFont path and an optional quick-refinement flag. See that project for full experiments, derivations, and reproducible demo provenance.
|
| 57 |
|
| 58 |
-
## CPU and
|
| 59 |
|
| 60 |
-
The
|
| 61 |
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
|
| 64 |
```bash
|
| 65 |
# Python 3.10; install libfluidsynth3, libsndfile1 and sox using your OS package manager.
|
| 66 |
python -m venv .venv
|
| 67 |
. .venv/bin/activate
|
| 68 |
pip install -r requirements.txt gradio==6.17.3
|
| 69 |
-
python app.py
|
| 70 |
```
|
| 71 |
|
| 72 |
-
Optional
|
|
|
|
|
|
|
| 73 |
|
| 74 |
## Relationship to the Qwen Space
|
| 75 |
|
| 76 |
-
The [official Qwen3-TTS Space](https://huggingface.co/spaces/Qwen/Qwen3-TTS/blob/6ae5dfa/app.py) was inspected as the backend reference. This app follows its `Qwen3TTSModel.from_pretrained(..., attn_implementation='sdpa')` and `generate_voice_design(text, language, instruct)` flow. Our backend is written separately
|
| 77 |
|
| 78 |
## Assets and attribution
|
| 79 |
|
|
@@ -81,4 +85,4 @@ The bundled `assets/gm.sf3` is MuseScore General v0.2, with SHA-256 `5b85b6c2c61
|
|
| 81 |
|
| 82 |
## Deployment
|
| 83 |
|
| 84 |
-
Authenticate using `hf auth login` or set `HF_TOKEN`, then run `python deploy.py`. The script creates `<authenticated-user>/
|
|
|
|
| 55 |
|
| 56 |
The fitting core is vendored from [Speaking MIDI](https://github.com/k-l-lambda/speaking-midi/tree/932f8eb), with a portable SoundFont path and an optional quick-refinement flag. See that project for full experiments, derivations, and reproducible demo provenance.
|
| 57 |
|
| 58 |
+
## CPU and ZeroGPU execution
|
| 59 |
|
| 60 |
+
The hosted [Speaking MIDI Space](https://huggingface.co/spaces/k-l-lambda/speaking-midi) uses `INFERENCE_DEVICE=zerogpu`. VoiceDesign loads at startup with CUDA placement and bfloat16, following the ZeroGPU model-loading contract. A `@spaces.GPU(duration=120)` function requests a shared GPU only for speech generation. MIDI fitting runs in a separate CPU worker and does not consume GPU quota. ZeroGPU requests can queue or fail when the visitor's GPU quota is exhausted; the precomputed speech examples can still be fitted without generating another voice.
|
| 61 |
|
| 62 |
+
The requirements use `torch==2.10.0` and matching torchaudio. Do not add `+cpu` or the CPU-only wheel index to the hosted requirements: ZeroGPU rejects that version specification and needs GPU-capable PyTorch.
|
| 63 |
+
|
| 64 |
+
Local execution defaults to `INFERENCE_DEVICE=cpu`. TTS uses float32 with SDPA in a separate process, releasing model memory before MIDI fitting. No GPU is needed for this path.
|
| 65 |
|
| 66 |
```bash
|
| 67 |
# Python 3.10; install libfluidsynth3, libsndfile1 and sox using your OS package manager.
|
| 68 |
python -m venv .venv
|
| 69 |
. .venv/bin/activate
|
| 70 |
pip install -r requirements.txt gradio==6.17.3
|
| 71 |
+
INFERENCE_DEVICE=cpu python app.py
|
| 72 |
```
|
| 73 |
|
| 74 |
+
Optional variables: `QWEN_MODEL_PATH` selects a local VoiceDesign snapshot; `PORT` defaults to 7860; `OMP_NUM_THREADS` defaults to 2. `INFERENCE_DEVICE=cuda` enables a regular local GPU, and `auto` detects CUDA availability. Keep `zerogpu` for a configured ZeroGPU runtime. For a smaller CPU-only local install, install PyTorch and torchaudio from the CPU wheel index separately; those wheels must not be used on ZeroGPU.
|
| 75 |
+
|
| 76 |
+
Each CPU worker has a 30-minute timeout. Hosted GPU time is limited to 120 seconds per speech call. The Space uses shared ZeroGPU hardware, without dedicated paid hardware. The local directory remains `hf-speaking-midi`.
|
| 77 |
|
| 78 |
## Relationship to the Qwen Space
|
| 79 |
|
| 80 |
+
The [official Qwen3-TTS Space](https://huggingface.co/spaces/Qwen/Qwen3-TTS/blob/6ae5dfa/app.py) was inspected as the backend reference. This app follows its `Qwen3TTSModel.from_pretrained(..., attn_implementation='sdpa')` and `generate_voice_design(text, language, instruct)` flow. Our backend is written separately, supports both startup-loaded ZeroGPU inference and on-demand float32 CPU inference, and limits generation to 512 new tokens. We deliberately use VoiceDesign 1.7B because the 0.6B CustomVoice model does not honor natural-language voice instructions.
|
| 81 |
|
| 82 |
## Assets and attribution
|
| 83 |
|
|
|
|
| 85 |
|
| 86 |
## Deployment
|
| 87 |
|
| 88 |
+
Authenticate using `hf auth login` or set `HF_TOKEN`, then run `python deploy.py`. The script selects ZeroGPU hardware and creates `<authenticated-user>/speaking-midi` and commits the application and assets to that Space. It never upgrades an account or requests paid hardware. See [validation results](VALIDATION.md) for what has actually been tested.
|
app.py
CHANGED
|
@@ -18,8 +18,11 @@ ROOT = Path(__file__).resolve().parent
|
|
| 18 |
VOICE = '一位温柔自信的年轻女性,声音清晰,语气亲切,表达轻快而富有感染力。'
|
| 19 |
MODEL = 'Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign'
|
| 20 |
DEVICE = os.getenv('INFERENCE_DEVICE', 'cpu')
|
| 21 |
-
if DEVICE not in {'cpu', 'cuda', 'auto'}:
|
| 22 |
-
raise ValueError('INFERENCE_DEVICE must be cpu, cuda or
|
|
|
|
|
|
|
|
|
|
| 23 |
RUNS = ROOT / 'outputs' / 'runs'
|
| 24 |
RUNS.mkdir(parents=True, exist_ok=True)
|
| 25 |
|
|
@@ -27,8 +30,9 @@ RUNS.mkdir(parents=True, exist_ok=True)
|
|
| 27 |
def worker(command, log, timeout=1800):
|
| 28 |
"""Bound each job and release all model memory when the child exits."""
|
| 29 |
env = os.environ.copy()
|
| 30 |
-
if DEVICE
|
| 31 |
env['CUDA_VISIBLE_DEVICES'] = ''
|
|
|
|
| 32 |
with log.open('w') as stream:
|
| 33 |
process = subprocess.Popen([sys.executable, *command], cwd=ROOT, env=env,
|
| 34 |
stdout=stream, stderr=subprocess.STDOUT)
|
|
@@ -99,7 +103,7 @@ def run(mode, audio, text, language, description, seconds, budget, refine, progr
|
|
| 99 |
if mode == 'Text → speech → MIDI':
|
| 100 |
if not text.strip() or len(text) > 300 or not description.strip() or len(description) > 600:
|
| 101 |
raise gr.Error('Enter 1–300 text characters and a voice description of 1–600 characters.')
|
| 102 |
-
yield ('1/4 · Generating speech. CPU VoiceDesign can take several minutes on first use.', *empty)
|
| 103 |
progress(.05, desc='Loading VoiceDesign and generating speech')
|
| 104 |
prompts = directory/'prompts.json'
|
| 105 |
prompts.write_text(json.dumps([dict(name='speech', text=text.strip(), language=language, instruct=description.strip())]))
|
|
@@ -107,7 +111,11 @@ def run(mode, audio, text, language, description, seconds, budget, refine, progr
|
|
| 107 |
'--samples', str(prompts), '--out', str(directory)]
|
| 108 |
if os.getenv('QWEN_MODEL_PATH'):
|
| 109 |
command += ['--model-path', os.environ['QWEN_MODEL_PATH']]
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
audio = str(directory/'speech.wav')
|
| 112 |
duration = prepare_audio(audio, directory/'input.wav', seconds)
|
| 113 |
yield (f'2/4 · Analyzing the first {duration:.2f} s and building piano templates.',
|
|
|
|
| 18 |
VOICE = '一位温柔自信的年轻女性,声音清晰,语气亲切,表达轻快而富有感染力。'
|
| 19 |
MODEL = 'Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign'
|
| 20 |
DEVICE = os.getenv('INFERENCE_DEVICE', 'cpu')
|
| 21 |
+
if DEVICE not in {'cpu', 'cuda', 'auto', 'zerogpu'}:
|
| 22 |
+
raise ValueError('INFERENCE_DEVICE must be cpu, cuda, auto or zerogpu')
|
| 23 |
+
if DEVICE == 'zerogpu':
|
| 24 |
+
from zero_tts import generate_speech as generate_zero_speech
|
| 25 |
+
|
| 26 |
RUNS = ROOT / 'outputs' / 'runs'
|
| 27 |
RUNS.mkdir(parents=True, exist_ok=True)
|
| 28 |
|
|
|
|
| 30 |
def worker(command, log, timeout=1800):
|
| 31 |
"""Bound each job and release all model memory when the child exits."""
|
| 32 |
env = os.environ.copy()
|
| 33 |
+
if DEVICE in {'cpu', 'zerogpu'}:
|
| 34 |
env['CUDA_VISIBLE_DEVICES'] = ''
|
| 35 |
+
env['SPACES_ZERO_GPU'] = '0'
|
| 36 |
with log.open('w') as stream:
|
| 37 |
process = subprocess.Popen([sys.executable, *command], cwd=ROOT, env=env,
|
| 38 |
stdout=stream, stderr=subprocess.STDOUT)
|
|
|
|
| 103 |
if mode == 'Text → speech → MIDI':
|
| 104 |
if not text.strip() or len(text) > 300 or not description.strip() or len(description) > 600:
|
| 105 |
raise gr.Error('Enter 1–300 text characters and a voice description of 1–600 characters.')
|
| 106 |
+
yield ('1/4 · Generating speech. '+('Waiting for a shared GPU.' if DEVICE == 'zerogpu' else 'CPU VoiceDesign can take several minutes on first use.'), *empty)
|
| 107 |
progress(.05, desc='Loading VoiceDesign and generating speech')
|
| 108 |
prompts = directory/'prompts.json'
|
| 109 |
prompts.write_text(json.dumps([dict(name='speech', text=text.strip(), language=language, instruct=description.strip())]))
|
|
|
|
| 111 |
'--samples', str(prompts), '--out', str(directory)]
|
| 112 |
if os.getenv('QWEN_MODEL_PATH'):
|
| 113 |
command += ['--model-path', os.environ['QWEN_MODEL_PATH']]
|
| 114 |
+
if DEVICE == 'zerogpu':
|
| 115 |
+
wav, rate = generate_zero_speech(text.strip(), language, description.strip())
|
| 116 |
+
sf.write(directory/'speech.wav', wav, rate, subtype='FLOAT')
|
| 117 |
+
else:
|
| 118 |
+
worker(command, directory/'tts.log')
|
| 119 |
audio = str(directory/'speech.wav')
|
| 120 |
duration = prepare_audio(audio, directory/'input.wav', seconds)
|
| 121 |
yield (f'2/4 · Analyzing the first {duration:.2f} s and building piano templates.',
|
deploy.py
CHANGED
|
@@ -4,10 +4,11 @@ from huggingface_hub import HfApi
|
|
| 4 |
|
| 5 |
if __name__ == '__main__':
|
| 6 |
api = HfApi()
|
| 7 |
-
repo = f"{api.whoami()['name']}/
|
| 8 |
-
api.create_repo(repo, repo_type='space', space_sdk='gradio', exist_ok=True)
|
|
|
|
| 9 |
commit = api.upload_folder(repo_id=repo, repo_type='space', folder_path=Path(__file__).parent,
|
| 10 |
-
allow_patterns=['app.py', 'deploy.py', 'README.md', 'VALIDATION.md', 'requirements.txt', 'packages.txt',
|
| 11 |
'.gitignore', 'speaking_midi/*.py', 'scripts/*.py', 'tests/*.py',
|
| 12 |
'assets/*', 'examples/**'],
|
| 13 |
commit_message='Build CPU-capable Speaking MIDI audio and VoiceDesign demo')
|
|
|
|
| 4 |
|
| 5 |
if __name__ == '__main__':
|
| 6 |
api = HfApi()
|
| 7 |
+
repo = f"{api.whoami()['name']}/speaking-midi"
|
| 8 |
+
api.create_repo(repo, repo_type='space', space_sdk='gradio', space_hardware='zero-a10g', exist_ok=True)
|
| 9 |
+
api.add_space_variable(repo, 'INFERENCE_DEVICE', 'zerogpu')
|
| 10 |
commit = api.upload_folder(repo_id=repo, repo_type='space', folder_path=Path(__file__).parent,
|
| 11 |
+
allow_patterns=['app.py', 'zero_tts.py', 'deploy.py', 'README.md', 'VALIDATION.md', 'requirements.txt', 'packages.txt',
|
| 12 |
'.gitignore', 'speaking_midi/*.py', 'scripts/*.py', 'tests/*.py',
|
| 13 |
'assets/*', 'examples/**'],
|
| 14 |
commit_message='Build CPU-capable Speaking MIDI audio and VoiceDesign demo')
|
requirements.txt
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
torchaudio==2.10.0+cpu
|
| 4 |
qwen-tts==0.1.1
|
| 5 |
transformers==4.57.3
|
| 6 |
accelerate==1.12.0
|
|
@@ -10,3 +9,4 @@ soundfile==0.13.1
|
|
| 10 |
pyfluidsynth==1.4.0
|
| 11 |
mido==1.3.3
|
| 12 |
matplotlib==3.10.8
|
|
|
|
|
|
| 1 |
+
torch==2.10.0
|
| 2 |
+
torchaudio==2.10.0
|
|
|
|
| 3 |
qwen-tts==0.1.1
|
| 4 |
transformers==4.57.3
|
| 5 |
accelerate==1.12.0
|
|
|
|
| 9 |
pyfluidsynth==1.4.0
|
| 10 |
mido==1.3.3
|
| 11 |
matplotlib==3.10.8
|
| 12 |
+
spaces
|
zero_tts.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ZeroGPU VoiceDesign entry point; CPU execution remains in the CLI worker."""
|
| 2 |
+
import spaces # Initialize ZeroGPU before importing torch or loading the model.
|
| 3 |
+
import os
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from huggingface_hub import snapshot_download
|
| 7 |
+
from qwen_tts import Qwen3TTSModel
|
| 8 |
+
|
| 9 |
+
MODEL_ID = 'Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign'
|
| 10 |
+
REVISION = '5ecdb67327fd37bb2e042aab12ff7391903235d3'
|
| 11 |
+
path = os.getenv('QWEN_MODEL_PATH') or snapshot_download(MODEL_ID, revision=REVISION)
|
| 12 |
+
# ZeroGPU records startup CUDA placements and restores them when a GPU is allocated.
|
| 13 |
+
model = Qwen3TTSModel.from_pretrained(
|
| 14 |
+
path, device_map='cuda', dtype=torch.bfloat16, attn_implementation='sdpa')
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@spaces.GPU(duration=120)
|
| 18 |
+
def generate_speech(text, language, description):
|
| 19 |
+
torch.manual_seed(42)
|
| 20 |
+
np.random.seed(42)
|
| 21 |
+
with torch.inference_mode():
|
| 22 |
+
wavs, rate = model.generate_voice_design(
|
| 23 |
+
text=text, language=language, instruct=description,
|
| 24 |
+
non_streaming_mode=True, max_new_tokens=512)
|
| 25 |
+
return np.asarray(wavs[0], dtype=np.float32), rate
|