Files
voicebox/backend/voicebox-server.spec
T
c8cb12f1bc fix(build): repair frozen-binary imports for kokoro, chatterbox-multilingual, scipy, transformers (#438)
* fix(build): bundle kokoro source files for transformers runtime introspection

transformers opens .py source files at runtime to check attention/MoE
implementation via regex (e.g. _can_set_attn_implementation). PyInstaller's
--hidden-import only bundles .pyc bytecode, so kokoro/modules.py was missing
from the bundle causing a FileNotFoundError on Kokoro model load.

Switch from individual --hidden-import entries to --collect-all kokoro in both
build_binary.py and voicebox-server.spec. The kokoro package is 172K so no
meaningful bundle size impact.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(build): use SPECPATH for runtime hook instead of hardcoded absolute path

The linter expanded runtime_hooks=[] to an absolute /Users/... path which
would break CI and other dev machines. Use os.path.join(SPECPATH, ...) to
mirror the relative approach in build_binary.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(build): runtime hook to work around PyInstaller + Python 3.12 import breakages

Four distinct bundling-specific crashes blocked Kokoro and Qwen CustomVoice
from loading in the frozen binary:

1. torch._dynamo import triggered via class-body decorators
   (@torch._dynamo.allow_in_graph on PreTrainedModel,
   @torch.compiler.disable in flex_attention) pulls in torch._numpy._ufuncs
   which crashes on module load with NameError: name 'name' is not defined.

2. AlbertModel (Kokoro) triggers @auto_docstring -> modeling_auto ->
   GenerationMixin -> candidate_generator -> sklearn -> scipy, which hits
   the same class of bug in scipy.stats._distn_infrastructure (NameError:
   name 'obj' is not defined).

3. AutoModel (Qwen) pulls the same sklearn -> scipy chain directly.

4. librosa (required by most TTS engines) -> scipy.signal -> scipy.stats
   hits the _distn_infrastructure crash regardless of the transformers
   stubs above.

The root cause of (1) and (4) is that PyInstaller's frozen importer runs
module-level `for X in [<list-comp using dir()>]:` loops with an empty
iterable, leaving the loop variable unbound. Trailing `del obj` / unrelated
references then crash.

Fix: a single runtime hook (pyi_rth_torch_compiler_disable.py) installs:

- sys.modules stubs for torch._dynamo and torch._dynamo.config, plus a
  meta-path finder for torch._dynamo.* submodules — voicebox never uses
  torch.compile/dynamo for inference, so a permissive no-op stub (callable
  as decorator, falsey as predicate, context-manager-safe for
  TransformGetItemToIndex) is drop-in safe.
- meta-path finder stubs for transformers.utils.auto_docstring and
  transformers.generation.candidate_generator — both import-chain
  short-circuits; docstrings and speculative decoding aren't used for TTS.
- meta-path finder for scipy.stats._distn_infrastructure that reads the
  real .py source via the wrapped loader's get_source(), replaces the
  bundling-broken `del obj` with `globals().pop('obj', None)`, and
  compile+exec's the patched source. This keeps the real scipy module
  intact so librosa and everything downstream works normally.

Supporting changes:

- backend/pyi_hooks/hook-scipy.stats._distn_infrastructure.py sets
  module_collection_mode = "pyz+py" so the .py source is actually in the
  bundle for the runtime patcher to read.
- build_binary.py and voicebox-server.spec register the runtime hook and
  the new hooks dir.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(build): force transformers torch<2.6 mask path and bundle spacy_pkuseg

- patch transformers.masking_utils to set _is_torch_greater_or_equal_than_2_6
  = False, forcing sdpa_mask_older_torch and avoiding the vmap .item() crash
  that breaks Qwen CustomVoice generation (our torch._dynamo stub can't
  reproduce TransformGetItemToIndex's graph transform).
- add PyInstaller hook to bundle transformers.masking_utils .py source so the
  runtime finder can source-patch it.
- --collect-all spacy_pkuseg so Chatterbox Multilingual can load its Chinese
  segmenter (dicts/default.pkl + native .so extensions).
- add per-finder install diagnostics + _HOOK_VERSION marker to make future
  bundle-only regressions easier to triage.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(build): pass PyInstaller hook paths relative so .spec is portable

Absolute paths ended up in the auto-regenerated voicebox-server.spec
because build_binary.py prefixed every --runtime-hook and
--additional-hooks-dir with str(backend_dir / ...). That broke builds
on any machine whose checkout wasn't at /Users/jamie/... and anyone
invoking pyinstaller voicebox-server.spec directly.

os.chdir(backend_dir) already runs before PyInstaller (same reason
server.py works as a bare filename), so the backend_dir prefix is
unnecessary. Drop it so the generated spec references pyi_hooks/,
pyi_rth_numpy_compat.py, pyi_rth_torch_compiler_disable.py as repo-
relative paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-18 03:12:13 -07:00

90 lines
4.8 KiB
RPMSpec

# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_submodules
from PyInstaller.utils.hooks import collect_all
from PyInstaller.utils.hooks import copy_metadata
datas = []
binaries = []
hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.services.profiles', 'backend.services.history', 'backend.services.tts', 'backend.services.transcribe', 'backend.utils.platform_detect', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.backends.qwen_custom_voice_backend', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.services.cuda', 'backend.services.effects', 'backend.utils.effects', 'backend.services.versions', 'pedalboard', 'chatterbox', 'chatterbox.tts_turbo', 'chatterbox.mtl_tts', 'backend.backends.chatterbox_backend', 'backend.backends.chatterbox_turbo_backend', 'backend.backends.luxtts_backend', 'zipvoice', 'zipvoice.luxvoice', 'torch', 'transformers', 'fastapi', 'uvicorn', 'sqlalchemy', 'soundfile', 'qwen_tts', 'qwen_tts.inference', 'qwen_tts.inference.qwen3_tts_model', 'qwen_tts.inference.qwen3_tts_tokenizer', 'qwen_tts.core', 'qwen_tts.cli', 'requests', 'pkg_resources.extern', 'backend.backends.hume_backend', 'tada', 'tada.modules', 'tada.modules.tada', 'tada.modules.encoder', 'tada.modules.decoder', 'tada.modules.aligner', 'tada.modules.acoustic_spkr_verf', 'tada.nn', 'tada.nn.vibevoice', 'tada.utils', 'tada.utils.gray_code', 'tada.utils.text', 'backend.utils.dac_shim', 'torchaudio', 'backend.backends.kokoro_backend', 'en_core_web_sm', 'loguru', 'backend.backends.mlx_backend', 'mlx', 'mlx.core', 'mlx.nn', 'mlx_audio', 'mlx_audio.tts', 'mlx_audio.stt']
datas += copy_metadata('qwen-tts')
datas += copy_metadata('requests')
datas += copy_metadata('transformers')
datas += copy_metadata('huggingface-hub')
datas += copy_metadata('tokenizers')
datas += copy_metadata('safetensors')
datas += copy_metadata('tqdm')
datas += copy_metadata('en_core_web_sm')
hiddenimports += collect_submodules('jaraco')
hiddenimports += collect_submodules('tada')
hiddenimports += collect_submodules('mlx')
hiddenimports += collect_submodules('mlx_audio')
tmp_ret = collect_all('spacy_pkuseg')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('zipvoice')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('linacodec')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('lazy_loader')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('librosa')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('qwen_tts')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('inflect')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('perth')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('piper_phonemize')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('kokoro')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('misaki')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('language_tags')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('espeakng_loader')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('en_core_web_sm')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('mlx')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('mlx_audio')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['server.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=['pyi_hooks'],
hooksconfig={},
runtime_hooks=['pyi_rth_numpy_compat.py', 'pyi_rth_torch_compiler_disable.py'],
excludes=['nvidia', 'nvidia.cublas', 'nvidia.cuda_cupti', 'nvidia.cuda_nvrtc', 'nvidia.cuda_runtime', 'nvidia.cudnn', 'nvidia.cufft', 'nvidia.curand', 'nvidia.cusolver', 'nvidia.cusparse', 'nvidia.nccl', 'nvidia.nvjitlink', 'nvidia.nvtx'],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='voicebox-server',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)