a1fed2cd71
Multi-stage image built from the `web` extra only, so it carries no Qt: the whole web path (webapp, web_session, metrics_json, and the shared analysis modules) imports no Qt, pyqtgraph or matplotlib canvas. The build stage has the compiler scipy/numba need; the runtime layer installs the built wheel, so the same image works from a checkout or a release tarball. Compose declares no audio volume on purpose. The app keeps no audio, so there is nothing to persist: `read_only` plus a /tmp tmpfs means request spools are memory-backed and never reach a disk. Publishing is loopback-only because the tool has no authentication, and BLAS thread pools are pinned because analysis is CPU-bound. Fix three packaging bugs the build surfaced, each of which broke the image: - `pip install --extra web` is not valid for a source tree; uv wants `.[web]` and failed with "Requesting extras requires a pylock.toml". - The wheel shipped no static assets at all. `package-data` keys match *packages*, and the app modules are top-level `py-modules`, so the `"*"` key matched nothing - verified by inspecting the built wheel: 14 .py files, zero HTML/JS/CSS. An installed `ujm-web` would have 404'd every asset. `static/` is now declared a package (hence its packaging-shim `__init__.py`) so the assets install next to the modules, which is where `webapp.py` looks for them. - The Dockerfile's curated COPY list omitted modules that `py-modules` requires, failing the build with "file <module>.py not found". It copies the checkout, filtered by .dockerignore. Move PyQt5/pyqtgraph into a `desktop` extra so the web install is genuinely Qt-free. Verified: `find` for Qt in a web-only venv now returns nothing, and the desktop path still resolves and imports. Add scripts/make_fixtures.py so the smoke suite has no network or ffmpeg dependency (the tagged MP3 is optional and its assertions skip when absent), and make the suite tolerate that. Verified against the *installed package* running the container's own command line, from a directory with no source tree: 61/61 passing. Also correct README claims that had gone stale: BPM (removed), the matplotlib canvas (replaced by pyqtgraph), and font_control_widget.py (deleted).
85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the audio fixtures `webapi_smoke.py` expects.
|
|
|
|
Synthesised rather than downloaded so the smoke test has no network dependency
|
|
and no external tools — `soundfile` and `numpy` come in with `librosa`, so this
|
|
runs in the container image as-is (no ffmpeg needed).
|
|
|
|
sine440.wav 12 s, 440 Hz, stereo, -12 dBFS — a known-tone reference
|
|
pinknoise.wav 8 s, pink noise, stereo — broadband, exercises the STFT
|
|
tagged.mp3 a tagged file — only if ffmpeg is available
|
|
|
|
Usage: python3 scripts/make_fixtures.py [output_dir] (default /tmp/ujm-test)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import soundfile as sf
|
|
|
|
SR = 44100
|
|
|
|
|
|
def sine(path: Path, seconds: float = 12.0, hz: float = 440.0,
|
|
dbfs: float = -12.0) -> None:
|
|
"""A tone at a known level. Peak is exactly 10**(dbfs/20) before encoding."""
|
|
t = np.arange(int(seconds * SR)) / SR
|
|
mono = (10 ** (dbfs / 20)) * np.sin(2 * np.pi * hz * t)
|
|
sf.write(path, np.stack([mono, mono], axis=1), SR, subtype="PCM_16")
|
|
|
|
|
|
def pink(path: Path, seconds: float = 8.0, peak: float = 0.25) -> None:
|
|
"""Pink noise via 1/sqrt(f) shaping, normalised to a known peak."""
|
|
rng = np.random.default_rng(7)
|
|
n = int(seconds * SR)
|
|
spectrum = np.fft.rfft(rng.standard_normal(n))
|
|
freqs = np.fft.rfftfreq(n, 1 / SR)
|
|
freqs[0] = freqs[1] # avoid dividing by zero at DC
|
|
shaped = np.fft.irfft(spectrum / np.sqrt(freqs), n)
|
|
mono = peak * shaped / np.max(np.abs(shaped))
|
|
sf.write(path, np.stack([mono, mono], axis=1), SR, subtype="PCM_16")
|
|
|
|
|
|
def tagged_mp3(path: Path) -> bool:
|
|
"""An MP3 with ID3 tags, so the tag-reading path is exercised.
|
|
|
|
Genuinely optional: MP3 encoding is the one thing numpy cannot do, so this
|
|
needs ffmpeg and is skipped when it is absent. The suite's tag assertions
|
|
adapt — see `webapi_smoke.py`.
|
|
"""
|
|
if shutil.which("ffmpeg") is None:
|
|
return False
|
|
subprocess.run(
|
|
["ffmpeg", "-y", "-loglevel", "error",
|
|
"-f", "lavfi", "-i", "sine=frequency=330:duration=6:sample_rate=44100",
|
|
"-metadata", "artist=Test Artist", "-metadata", "title=Test Title",
|
|
"-codec:a", "libmp3lame", "-q:a", "2", str(path)],
|
|
check=True,
|
|
)
|
|
return True
|
|
|
|
|
|
def main() -> int:
|
|
out = Path(sys.argv[1] if len(sys.argv) > 1 else "/tmp/ujm-test")
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
|
|
sine(out / "sine440.wav")
|
|
pink(out / "pinknoise.wav")
|
|
mp3 = tagged_mp3(out / "テスト.mp3")
|
|
|
|
for f in sorted(out.iterdir()):
|
|
print(f" {f.name} {f.stat().st_size / 1024:.0f} KiB")
|
|
if not mp3:
|
|
print("\nnote: no ffmpeg, so no tagged MP3 fixture was written "
|
|
"(tag assertions will be skipped)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|