Fix packaging, release versioning and the untracked test suite - #2
Merged
Merged
Conversation
The application code was sound, but nothing that left the machine worked:
the repo shipped without tests, the Windows build shipped without Pillow,
and every release archive was named v1.0.0 regardless of the tag.
Packaging
* Add Pillow to requirements.txt. ui/icons.py imports it at module level,
so a fresh clone could never start the app.
* Stop excluding PIL from the PyInstaller build. It was in UNNEEDED_PACKAGES
and additionally deleted by prune_unneeded(), so every released binary
died with "No module named 'PIL'".
* Add verify_bundle(): the build now fails if a module the app imports at
startup is missing from the bundle, instead of finding out on a user's
desktop.
* ui/app.py falls back to plain pyaudio when pyaudiowpatch is absent, the
same way capture.py already did. The Linux and macOS artifacts could not
start without this.
* Split requirements by platform: PyAudioWPatch on Windows, PyAudio elsewhere.
Release versioning
* build_release.py resolves the version from an argument, RELEASE_VERSION,
the git tag, or a dev fallback - the hardcoded v1.0.0 is gone.
* The script is now cross-platform and picks the archive name and format per
OS, so all three CI jobs call it instead of duplicating the naming rules.
* A version job resolves the tag once and every job and release note reads
that single value.
* CI runs the test suite before building, and publishing is gated on a tag.
Test suite
* Remove tests/ and run_tests.py from .gitignore - the entire suite was
excluded from the repository despite being documented in the README.
* Tests patched pipeline.OUT_DIR, which _process never reads: the output
path comes from Settings.get_output_dir(). Every finalizer test wrote into
the real output/ folder. They now steer it via settings.output_dir.
* Add a regression test for FileFinalizer.run_async without base_name.
Correctness
* FileFinalizer.run_async called paths.safe_output_name() while pipeline.py
never imported paths - NameError on the default-name path.
* Drop a duplicated write of the transcript file.
* ElevenLabsBackend.cancel() closes the live response instead of only
setting a flag nothing read; a cancelled upload no longer sits on its
900 s timeout.
* Reset seg_end when flushing an ElevenLabs segment, so a following segment
cannot inherit the previous end time.
* loader.py no longer swallows its own "no sample data" error into the
FFmpeg fallback, which reported a misleading "FFmpeg not found".
* Slider honours state="disabled"; _enabled was assigned but never
initialised or checked.
* app.py no longer binds two different buttons to self.save_btn.
* Redact the revoked example API key in legacy/settings.v1.json.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PyInstaller builds for the host architecture unless target_arch=universal2 is set, so the macos-latest runner (arm64) produced an arm64-only binary that was published as "macos-universal". Intel users downloaded a build that could not run on their machine. The slug now follows platform.machine(): macos-arm64 or macos-x64. README and release notes say Apple Silicon and point Intel users at running from source or building locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI
* New ci.yml runs the suite on every push to main and every pull request,
on Windows and Linux. Until now the tests only ran on a release tag, so
nothing guarded day-to-day changes.
* The Linux job adds an explicit import check of ui.app. No test imports
that module unless a display is available, which is exactly why the
unconditional pyaudiowpatch import and the missing Pillow dependency
reached two published release assets unnoticed.
* Linux runs the suite under xvfb so the GUI smoke test executes instead of
skipping itself.
Device reconfiguration race
* AudioEngine.configure() now holds a dedicated lock for the whole
reconfiguration. _gen_lock was released between stop_streams() and opening
the new streams, so two rapid device changes could interleave as
A.stop / B.stop / A.assign / B.assign and leak A's streams. Verified: the
unlocked path produces enter-A, enter-B, leave-A, leave-B.
* Added AudioEngine.is_configuring so callers can tell "reconfiguring right
now" apart from "no working device".
* start_recording() waits for an in-flight reconfiguration instead of
failing. configure() runs off the GUI thread and briefly holds no active
track; pressing Start in that window reported "Neither audio source is
active" straight after a perfectly valid device change. It now polls via
root.after so the interface stays responsive, disables Start immediately
against double-triggering, and after a 6 s deadline falls through to the
engine's real error rather than hiding it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Caught by the new Linux CI job on its first run: os.path.basename() follows host rules, so on Linux and macOS a backslash is an ordinary character. The Windows-style input '..\..\windows\system32\evil' sanitised to 'evil' on Windows and to '_' on Linux - the same settings.json produced a different output file name depending on where it ran. Both separators are now stripped everywhere. The result was safe on either platform; it just was not the same one. Also add a property test stating the actual guarantee - the result contains no separator, no '..', is non-empty, and stays inside the directory it is joined onto - rather than only checking a table of Windows-shaped strings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every action in both workflows still targeted the Node 20 runtime, which the runner now force-migrates to Node 24 with a deprecation annotation on each run. checkout v4 -> v7 setup-python v5 -> v7 upload-artifact v4 -> v7 download-artifact v4 -> v8 action-gh-release v2 -> v3 The two artifact actions land on different majors on purpose: their version lines diverged after v4 (download-artifact took an extra major for an artifact-ids path fix we do not use). Only v3-and-below artifacts are cross-incompatible; upstream's own examples pair the two lines freely. Checked every breaking change against how these workflows actually call the actions - the only one that touches us is download-artifact v8 turning a digest mismatch from a warning into an error, which is what a release pipeline wants anyway. setup-python v7 drops the pip-install input, unused here; all other majors are runtime moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The application code was in good shape — the problems were all at the boundary between the repo and everything that leaves it. As shipped, neither a fresh clone nor any release artifact worked for anyone else.
Packaging
The released Windows binary could not start.
ui/icons.pyimports Pillow at module level, butPILwas listed inUNNEEDED_PACKAGES(--exclude-module PIL) and deleted again byprune_unneeded(). Verified against the previously builtdist/: zero PIL files. Every download failed withNo module named 'PIL'.PILfrom the exclude list and addedPillowtorequirements.txt— a fresh clone could not start the app either.verify_bundle(): the build now fails if a module the app imports at startup is missing from the bundle, rather than surfacing on a user's desktop.ui/app.pynow falls back to plainpyaudiowhenpyaudiowpatchis absent, matching whatcapture.pyalready did. The Linux and macOS artifacts could not start without this, since CI installs plain PyAudio there.requirements.txtsplit by platform: PyAudioWPatch on Windows, PyAudio elsewhere.Verified on the real artifact — the built
.exestarts and has Pillow's native extensions loaded at runtime:Release versioning
Version was hardcoded as
v1.0.0inbuild_release.pyand again in the Linux and macOS CI steps, while the release notes linked${{ github.ref_name }}filenames. Taggingv1.1.0produced assets namedv1.0.0and download links that 404.resolve_version()resolves once, in order: CLI argument →RELEASE_VERSION→ git tag →v0.0.0-dev.build_release.pyis now cross-platform and derives the archive name and format per OS, so all three CI jobs call the same script instead of duplicating naming rules.versionjob resolves the tag once; every job and the release notes read that single value.github.ref_type == 'tag'.macOS assets were labelled
macos-universal, but PyInstaller builds host-architecture-only unlesstarget_arch=universal2is set — the arm64 runner produced an arm64-only binary advertised to Intel users. The slug now followsplatform.machine():macos-arm64ormacos-x64.AudioTranscriber-v1.4.2-macos-arm64.zipAudioTranscriber-v1.4.2-macos-x64.zipAudioTranscriber-v1.4.2-windows-x64.zipAudioTranscriber-v1.4.2-linux-x64.tar.gzTest suite
.gitignorecontainedtests/andrun_tests.py, so the entire suite was excluded from the repository despite the README documentingpython run_tests.pyand "128+ unit tests".git ls-filesconfirmed not a single test file was tracked. Both entries removed and the suite committed.While fixing the unused
OUT_DIRimport, 13 tests failed and exposed a second issue: they patchedpipeline.OUT_DIR, which_processnever reads — the output path comes fromSettings.get_output_dir(). The patch was always a no-op and every finalizer test wrote into the realoutput/directory. Tests now steer it viasettings.output_dir.Correctness
FileFinalizer.run_asynccalledpaths.safe_output_name()whilepipeline.pynever importedpaths→NameErroron the default-name path. Latent because the GUI and every test passedbase_nameexplicitly; a regression test now covers it.ElevenLabsBackend.cancel()only set a flag nothing read; it now closes the live response, so a cancelled upload no longer sits on its 900 s timeout.seg_endis reset when flushing an ElevenLabs segment, so a following segment cannot inherit the previous end time.loader.pyno longer swallows its own "no sample data" error into the FFmpeg fallback, which reported a misleading "FFmpeg not found".Sliderhonoursstate="disabled"—_enabledwas assigned but never initialised or checked.app.pyno longer binds two different buttons toself.save_btn.legacy/settings.v1.json.Verification
Not verified: the Linux and macOS builds cannot be exercised from Windows — the import fallback and CI changes are correct by inspection, but the first tag push will be the real test.
🤖 Generated with Claude Code