k-l-lambda commited on
Commit
ff99ee3
·
verified ·
1 Parent(s): bc3f652

Build CPU-capable Speaking MIDI audio and VoiceDesign demo

Browse files
Files changed (5) hide show
  1. README.md +11 -7
  2. app.py +13 -5
  3. deploy.py +4 -3
  4. requirements.txt +3 -3
  5. 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 GPU execution
59
 
60
- The Space defaults to `INFERENCE_DEVICE=cpu`. TTS uses float32 on CPU and SDPA attention; no FlashAttention or ZeroGPU decorator is required. A separate process loads only VoiceDesign for text jobs and releases its memory before piano fitting. The fitting worker also runs on CPU, even when a GPU is present. Model downloads are cached between requests on the running instance. CPU-basic hardware can take several minutes; the first request additionally downloads the model. Each worker has a 30-minute timeout.
61
 
62
- To run locally:
 
 
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 environment variables: `QWEN_MODEL_PATH` selects an existing local VoiceDesign snapshot; `PORT` defaults to 7860; `OMP_NUM_THREADS` defaults to 2. For GPU execution, install CUDA-compatible PyTorch/torchaudio wheels instead of the CPU wheels and set `INFERENCE_DEVICE=cuda` (or `auto` for detection). CUDA TTS uses bfloat16. The deployment targets CPU-basic and does not request GPU hardware. On 2026-10-01, Hugging Face rejected Space creation for the current account with HTTP 402: even CPU-basic Gradio hosting requires a PRO subscription. An eligible account is required to deploy; local execution works without that subscription.
 
 
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 and loads one model on demand, supports 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.
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>/hf-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.
 
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 auto')
 
 
 
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 == 'cpu':
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
- worker(command, directory/'tts.log')
 
 
 
 
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']}/hf-speaking-midi"
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
- --extra-index-url https://download.pytorch.org/whl/cpu
2
- torch==2.10.0+cpu
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