packaging/demo.spec
Rama main · réplica de lectura del repositorio original.
"""
PyInstaller spec for the STT demo.
Unlike watchdog.spec — which builds a bootstrapper that clones the app and provisions
a venv on the user's machine — this bundles the whole application, so the artifact
runs with nothing installed: no Python, no pip, no models, no ML libraries. It
replays a recorded service through the real UI (see stt/demo_playback.py) and answers
the model/TTS/translation routes from recorded fixtures (stt/demo_api.py).
The ML excludes below are the load-bearing part. They are what keeps the build at
~25MB and what proves the demo genuinely cannot reach torch: if a code path ever
started importing one at module scope, this build would fail rather than quietly ship
a 3GB bundle.
Build (from the repo root):
STT_DEMO_DB=/path/to/session.db pyinstaller packaging/demo.spec
Or via build.py:
python packaging/build.py --demo --session /path/to/session.db
"""
import os
import sys
block_cipher = None
IS_WINDOWS = sys.platform == "win32"
IS_MACOS = sys.platform == "darwin"
ROOT = os.path.abspath(os.path.join(SPECPATH, "..")) # noqa: F821 (PyInstaller global)
_icon_file = os.path.join(ROOT, "icon.icns" if IS_MACOS else "icon.ico")
_icon = _icon_file if os.path.exists(_icon_file) else None
try:
with open(os.path.join(ROOT, "VERSION")) as _vf:
_bundle_version = _vf.read().strip() or "0.0.0"
except OSError:
_bundle_version = "0.0.0"
# The recording to ship. Deliberately not defaulted: a session database is
# congregation speech until someone has decided otherwise, so the build refuses to
# guess which one — and therefore cannot produce an artifact from a recording nobody
# chose. See stt/demo_scrub.py and the sessions/ drop-in folder for the alternatives.
_demo_db = os.environ.get("STT_DEMO_DB", "").strip()
if not _demo_db:
raise SystemExit(
"demo.spec: set STT_DEMO_DB to the session database to bundle.\n"
" Generate a synthetic one: python scripts/make_demo_session.py --synthetic -o build/demo.db\n"
" Or scrub a real service: python scripts/make_demo_session.py --from-session <db> ..."
)
if not os.path.isfile(_demo_db):
raise SystemExit(f"demo.spec: STT_DEMO_DB does not exist: {_demo_db}")
_datas = [
(os.path.join(ROOT, "templates"), "templates"),
(os.path.join(ROOT, "static"), "static"),
(os.path.join(ROOT, "VERSION"), "."),
(_demo_db, "demo"),
]
# Only the tracked templates — never a live config, which carries the build
# machine's settings and possibly its credentials.
_config_dir = os.path.join(ROOT, "config")
for _name in sorted(os.listdir(_config_dir)):
if _name.endswith(".default.json"):
_datas.append((os.path.join(_config_dir, _name), "config"))
if os.path.exists(_icon_file):
_datas.append((_icon_file, "."))
a = Analysis(
[os.path.join(ROOT, "speech_to_text.py")],
pathex=[ROOT],
binaries=[],
datas=_datas,
hiddenimports=[
# engineio picks its async driver by dynamic import, which PyInstaller's
# static analysis cannot see. Without this the server starts and then fails
# to accept a single WebSocket — the classic flask-socketio freeze bug.
"engineio.async_drivers.threading",
"simple_websocket",
"wsproto",
"bidict",
"jinja2.ext",
"werkzeug",
# Imported by the monolith through a name PyInstaller cannot follow.
"stt.demo_mode",
"stt.demo_playback",
"stt.demo_api",
"stt.demo_fixtures",
"stt.demo_redact",
"stt.demo_synth",
"stt.demo_window",
# The two things a demo reports. requests is what the live-map ping uses;
# sentry_sdk loads its integrations dynamically, so PyInstaller's static
# analysis needs them named (same list as watchdog.spec).
"requests",
"certifi",
"sentry_sdk",
"sentry_sdk.integrations.flask",
"sentry_sdk.integrations.stdlib",
"sentry_sdk.integrations.excepthook",
"sentry_sdk.integrations.dedupe",
"sentry_sdk.integrations.atexit",
"sentry_sdk.integrations.modules",
"sentry_sdk.integrations.logging",
"sentry_sdk.integrations.threading",
"sentry_sdk.integrations.argv",
# The control window (stt/demo_window.py). The demo is windowed on Windows
# and macOS, so without it there is no console to Ctrl-C and no way at all
# to quit the thing short of the task manager.
"tkinter",
"_tkinter",
],
hookspath=[],
hooksconfig={},
runtime_hooks=[os.path.join(SPECPATH, "demo_rthook.py")], # noqa: F821
excludes=[
"matplotlib",
"pytest",
"IPython",
"jupyter",
"notebook",
# The whole point of the demo build. Every one of these is reached only from
# the transcription worker or the TTS backends, neither of which a demo runs.
"torch",
"torchaudio",
"transformers",
"faster_whisper",
"whisper",
"openai_whisper",
"huggingface_hub",
"ctranslate2",
"panns_inference",
"torchlibrosa",
"silero_vad",
"speech_recognition",
"edge_tts",
"piper",
"piper_tts",
"supertonic",
"onnxruntime",
"llama_cpp",
"numpy",
"scipy",
"librosa",
"pandas",
"soundfile",
"pydub",
"datasets",
"accelerate",
# tkinter is deliberately NOT excluded — see the hiddenimports above.
"PIL",
],
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="STT-Demo",
debug=False,
strip=False,
upx=True,
# A double-clicked demo must not open a terminal window; on Linux it is run from
# one anyway, and the console is where its log goes.
console=not (IS_WINDOWS or IS_MACOS),
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=_icon,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name="STT-Demo",
)
if IS_MACOS:
app = BUNDLE(
coll,
name="STT Demo.app",
icon=_icon,
# Its own identity, so installing the demo can never replace a real STT.
bundle_identifier="com.stt.demo",
info_plist={
"CFBundleName": "STT Demo",
"CFBundleDisplayName": "STT Demo",
"CFBundleExecutable": "STT-Demo",
"CFBundleShortVersionString": _bundle_version,
"CFBundleVersion": _bundle_version,
"NSHighResolutionCapable": True,
# No NSMicrophoneUsageDescription on purpose: the demo never opens an
# audio device, and asking for the microphone would be a lie.
"LSUIElement": False,
},
)