Skip to content

Give the app an install command that no OS gate stands in front of - #32

Merged
sim186 merged 5 commits into
mainfrom
claude/app-signing-distribution-smrny8
Aug 8, 2026
Merged

Give the app an install command that no OS gate stands in front of#32
sim186 merged 5 commits into
mainfrom
claude/app-signing-distribution-smrny8

Conversation

@sim186

@sim186 sim186 commented Aug 7, 2026

Copy link
Copy Markdown
Owner

The release zips are unsigned PyInstaller bundles, so Gatekeeper and SmartScreen both stop them and the README has to teach a four-step dance around System Settings. Signing is a separate paid track, and Tauri would not have removed the need for it — under the sidecar design the PyInstaller binary ends up inside the bundle and needs notarizing either way.

A wheel sidesteps the whole category: pip and uv install into an environment the user already trusts, so nothing is a downloaded application and no gate applies. uv tool install amicoscript becomes the recommended route, identical on all three platforms, and it fits because the heavy dependencies were already deferred — transcription goes through CTranslate2 and never imports torch.

How the wheel is built

The repo is not laid out as a Python package: backend/ is a directory of flat modules that run.py puts on sys.path. Rather than restructure it, hatchling force-include mappings reproduce the repo's shape inside the package, so run.py's BASE_DIR and main.py's FRONTEND_DIR resolve correctly with no packaging-specific branches. amicoscript/cli.py is the only new runtime code, and run.py grows a main() shared by the console script, python run.py, and PyInstaller.

That approach has one bad failure mode: a broken mapping yields a wheel that builds, installs and starts — serving nothing. scripts/check_wheel.py asserts the payload is present, including every vendored asset index.html references, and the workflow additionally installs the wheel and checks it serves. Both run on dry runs, so a tag is never the first execution of this path.

Publishing uses PyPI trusted publishing, so there is no token to store. It is gated behind the GitHub release because a PyPI version is spent on first upload and cannot be reused, and the tag is checked against VERSION for the same reason. release now also needs wheel, so a failed wheel cannot leave a version published on GitHub but absent from PyPI.

Verified

The wheel job passed on every dry run of this branch — build, twine check, check_wheel.py, and installing the wheel into a clean venv and confirming it serves the frontend. Locally: 874 tests pass (3 pre-existing test_tui_deferred_imports failures, unrelated), and a wheel rebuilt from the sdist is also complete.

What this PR deliberately does not change

backend/requirements-diarization.txt and requirements-diarization-cu121.txt are byte-identical to main.

A separate, pre-existing problem blocks tagging: pyannote.audio 4.0 requires torch>=2.8, the cu121 index has nothing newer than the 2.6 line, and generate_runtime_manifest.py fails with ResolutionImpossible on Linux and Windows. main cannot cut a release today either — this PR neither causes nor fixes that.

Several attempts to fix it here were reverted because they did not work, and one made things worse. docs/pypi-release.md records the blocker, everything ruled out, and where the fix probably is. Merging this lands the wheel plumbing; a tag still will not publish until the diarization resolve is fixed, because release needs build.

claude added 5 commits August 7, 2026 20:41
The release zips are unsigned PyInstaller bundles, so Gatekeeper and
SmartScreen both stop them and the README has to teach a four-step dance
around System Settings. Signing is a separate paid track, and Tauri would not
have removed the need for it — under the sidecar design the PyInstaller binary
ends up inside the bundle and needs notarizing either way.

A wheel sidesteps the whole category: pip and uv install into an environment
the user already trusts, so nothing is a downloaded application and no gate
applies. `uv tool install amicoscript` is now the recommended route, identical
on all three platforms, and it fits because the heavy dependencies were already
deferred — transcription goes through CTranslate2 and never imports torch.

The repo is not laid out as a Python package: backend/ is a directory of flat
modules that run.py puts on sys.path. Rather than restructure it, hatchling
force-include mappings reproduce the repo's shape inside the package, so
run.py's BASE_DIR and main.py's FRONTEND_DIR resolve correctly with no
packaging-specific branches. cli.py is the only new runtime code, and run.py
grows a main() that the console script, `python run.py` and PyInstaller share.

That approach has one bad failure mode: a broken mapping yields a wheel that
builds, installs and starts, serving nothing. scripts/check_wheel.py asserts
the payload is present — including every vendored asset index.html references —
and the workflow additionally installs the wheel and checks it serves. Both run
on dry runs, so a tag is never the first execution of this path.

Publishing uses PyPI trusted publishing, so there is no token to store. It is
gated behind the GitHub release because a PyPI version is spent on first upload
and cannot be reused, and the tag is checked against VERSION for the same
reason. release now also needs wheel, so a failed wheel cannot leave a version
published on GitHub but absent from PyPI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JidoPdgd5BDroVFcEdF52
pyannote.audio 4.0 landed after v1.16.0 and requires torch>=2.8. The cu121
runtime pins torch<2.7 because the cu121 index has nothing newer and never
will, so with the range left open pip resolves pyannote to 4.x and then fails:
ResolutionImpossible. generate_runtime_manifest.py runs before PyInstaller, so
this took out the Linux and Windows builds entirely — macOS survived only
because it resolves no CUDA variant.

The CPU runtime is the less obvious half. It has no torch ceiling, so it kept
resolving green while silently moving to pyannote 4 — a major the backend was
not written against, and a different major from the one GPU machines would get.
A green build was hiding that, which is why the cap goes in both files rather
than only in the one that failed.

Capping at <4 restores what v1.16.0 shipped and resolves to pyannote 3.4.0 with
torch 2.6.0. The wheel's diarization extra gets the same cap, so a pip install
does not land somewhere else again.

Moving to pyannote 4 is a real upgrade — torch 2.8+, a CUDA index newer than
cu121, and backend/core/diarization.py updated for the 4.x API. Deliberate
work, not a range left open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JidoPdgd5BDroVFcEdF52
Capping pyannote at <4 in the previous commit fixed the cu121 resolve and broke
the CPU one. With torch left open, the resolver pairs pyannote 3.4 with the
newest torch on the CPU index and finds no consistent solution for the tree
pyannote 3 pulls in — lightning, speechbrain, torchmetrics all move with torch.
Bounded to <2.7 it lands on torch 2.6.0 and torchaudio 2.6.0, a matched pair,
the same generation the cu121 flavour resolves to.

Unbounded it also drifted to torch 2.13 with torchaudio 2.11 — a mismatched
pair pip is willing to install. Two runtimes that agree on the pyannote major
but not the torch generation were never the parity this was after.

The two ceilings move together now. Raising one alone puts the flavours back
out of step, which is the failure this pair of commits exists to close.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JidoPdgd5BDroVFcEdF52
Capping the CPU runtime alongside the CUDA one broke its resolve, twice, in a
way that does not reproduce outside CI: the same requirement set against the
same bundled pins resolves cleanly to pyannote 3.4.0 with torch 2.6.0 on PyPI,
with and without a torch ceiling. The difference is the CPU index itself, which
is not reachable from where this was tested, so the cap goes back off the file
CI has actually observed green and stays on the one whose failure it explains.

That leaves the two runtimes on different pyannote majors — CPU on 4.x, CUDA on
3.x — which is the state main is already in rather than a regression this adds.
The cu121 file carries the gap as a comment so the next person does not
rediscover it by capping the CPU file again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JidoPdgd5BDroVFcEdF52
Three dry runs went into capping pyannote so a tag could publish, and none of
them worked. Capping the cu121 file does not fix its resolve; capping the CPU
file as well breaks one that was green. Neither reproduces outside CI — with
the same pip generation and the exact bundled pins the build resolves against,
the capped set resolves cleanly on PyPI. The variable left untested is the
PyTorch index as primary index, which needs a machine that can reach it.

So both files go back to exactly what main has, and this branch carries only
the wheel and PyPI work, which has passed every run. The wheel's diarization
extra drops its cap too, so it mirrors the CPU runtime rather than encoding a
decision that is still open.

docs/pypi-release.md records the blocker, everything ruled out, and where the
fix probably is — forward to pyannote 4 on a newer CUDA index, not backward.
A tag still cannot publish until that is done; release needs build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JidoPdgd5BDroVFcEdF52
@sim186
sim186 merged commit 55ea9fa into main Aug 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants