Tool Deployment Template — raw code → published + supported
Tool Deployment Template — raw code → published + supported
A reusable, tool-agnostic playbook for taking ANY internal/proprietary desktop tool from a working source repo to a publicly downloadable, supported product. Derived from the Echo pilot (Plans 1–3).
Placeholders: <Tool> = display name (accents OK) · <tool> = ASCII identifier · <company-site> = static company site repo.
Worked example — Echo.
<Tool>=Echo,<tool>=Echo,<company-site>=jct-www(jacksoncreektech.ca). Local Windows dictation tool: Ctrl+Space → faster-whisper (local) → auto-paste. Source repo private:C:\Projects\superwhisper-clone.
Stage 1 — Brand pass
- Separate two name spaces. Pick a DISPLAY name (user-visible, accents/spaces allowed) and a distinct FILESYSTEM/registry IDENTIFIER (ASCII, no spaces, safe for paths/keys/exe names).
Echo: display
Echo; identifierEcho. Old brand was displayChuchoteur/ idChuchoteur, plus the working titleSuperWhisper Clone. - Rename display strings (window titles, tray text, about box, site copy) →
<Tool>. - Rename filesystem/registry identifiers (exe, folders, AppData dir, autostart registry key, launchers) →
<tool>. - Add a one-time data migration so existing users keep their data. Move
%APPDATA%\<OldId>→%APPDATA%\<tool>on first run; preserve everything (models, history, config). Cover it with a TDD test.Echo:
migrate.pymoves%APPDATA%\Chuchoteur → %APPDATA%\Echo, preserving downloaded models + history. Test-first. - Rename launchers to
<Tool>.bat/<Tool>.vbs. - Grep gate — confirm no old brand survives in user-visible code/strings, except deliberate internal sentinels (e.g. the migration source path).
# expect ZERO hits outside migrate.py / changelog rg -i "chuchoteur|superwhisper" --glob '!migrate.py'
Stage 2 — Package
PyInstaller one-folder spec. Use one-folder (not one-file) for faster startup and easier native-extension bundling. Keep a checked-in
<tool>.spec.collect_allfor native-extension deps. Whisper/ML stacks ship compiled binaries + data that PyInstaller misses by default. Addcollect_allfor each.from PyInstaller.utils.hooks import collect_all datas, binaries, hiddenimports = [], [], [] for pkg in ("ctranslate2", "av", "onnxruntime"): d, b, h = collect_all(pkg) datas += d; binaries += b; hiddenimports += hGotcha — interpreter version must match. The Python that runs PyInstaller must be the same version you built/tested against; mismatched minor versions produce broken or missing-DLL builds.
Echo built with Python 3.13 (app targets 3.13/3.14). Output:
dist\Echo\Echo.exe(~27 MB exe, ~374 MB folder).GATE — single-instance + visible hotkey failure (background-app tools). Any tool that grabs a global hotkey MUST: (a) guard against a second instance (named mutex / lock file) — two copies silently fight over the hotkey; (b) surface a registration failure to the user (tray notification), because a windowed
.exehas no stdout, so aprint()is invisible. Without both, a stale/duplicate instance makes a fresh build look dead.Echo: discovered 2026-06-01 when a source instance from 2 days prior held
Ctrl+Spaceand the newEcho.exefailedRegisterHotKeysilently. Tracked as JCT-17 (single-instance) + JCT-18 (surface failure).GATE — user data/config in a writable location, never next to the code. Use
Path(__file__).parentfor the BUNDLED default only; the live config + user data must live under%APPDATA%\<Tool>\(seeded from the bundled default on first run). In a packaged build__file__is inside read-onlyProgram Files, so writing config there fails (EPERM) and Settings silently don't persist.Echo: discovered 2026-06-01 —
config.pywrote toProgram Files, changing the model needed elevation. Fixed to%APPDATA%\Echo\config.yaml. Tracked as JCT-19.GATE — hardware-aware model default + honest latency expectation (local-inference tools). Detect the accelerator (CUDA vs CPU) at first run and set the default model accordingly — but know the hard limit: good multilingual ASR is encoder-bound and there is NO model/param that is both fast on CPU and accurate at bilingual. On CPU, a medium-class model is the quality floor and runs at RTF ~0.6–0.7x (a 30s clip ≈ 20s); smaller models trade away quality, and
large-v3-turbois actually SLOWER than medium on CPU (bigger encoder). beam-size / word-timestamps tuning saves only ~10%. The real speedup is a CUDA GPU (RTF ~0.05–0.1x). So: default medium for multilingual, auto-bump to large only on CUDA, and where the tool needs to be fast, run it on an NVIDIA box — don't expect CPU tuning to save it. Pair with a streaming partial-text UI so the unavoidable CPU wait shows visible progress.Echo: AMD iGPU (no CUDA, unusable by ctranslate2) +
medium= ~20s on long clips;smallfast but failed bilingual; turbo slower than medium; params negligible. Measured on the AMD 780M. Tracked as JCT-20.GATE — installer creates Start Menu (+ Desktop) shortcut. A bare
.exeinProgram Fileswith no shortcut leaves users unable to launch it. Ship an installer or aninstall-shortcut.ps1.Echo: discovered 2026-06-01 — no shortcut after install. Tracked as JCT-21.
Ship unsigned now, leave a signing hook for later. Add the signing step (commented) in
<tool>.spec/BUILD.mdso it can be enabled without rework.Code-signing options (CAD), when ready:
Option Cost (CAD) SmartScreen reputation Azure Trusted Signing ~$13–14/mo (cheapest) Builds over time EV code-signing cert ~$400–700/yr Instant (no warm-up) SECURITY GATE — scan bundled config/data for secrets before producing any public artifact. Open every bundled
config.yaml/.env/data file and confirm no API keys, tokens, or credentials are present.Echo: verified bundled
config.yamlhad an EMPTY api key. No public artifact ships until this passes.
Stage 3 — Distribute
- Size decision rule. If the artifact is > 25 MB, do NOT use Cloudflare Pages (25 MB/file limit). Use GitHub Releases.
Echo zip = 152 MB → GitHub Releases.
- Dedicated PUBLIC releases repo; source stays private. Create
<owner>/<tool>-releases(public) for binaries only. The source repo remains private.Echo: public
bockbr/echo-releases; sourcesuperwhisper-clonestays private. - Cut the release & wire the download button to the asset URL.
gh release create v0.1.0 --repo <owner>/<tool>-releases <Tool>-windows-x64.zip # asset URL pattern: # https://github.com/<owner>/<tool>-releases/releases/download/v0.1.0/<Tool>-windows-x64.zipEcho asset:
https://github.com/bockbr/echo-releases/releases/download/v0.1.0/Echo-windows-x64.zip - Extend the company static site with
/tools/<tool>/. Add a/tools/index + a product page: pitch, CSS-only how-it-works capsules, download CTA → release URL, install steps, changelog, feedback link.Echo: added
/tools/index +/tools/echo/tojct-www(jacksoncreektech.ca).
Stage 4 — Feedback
- Create issues with a per-tool LABEL (not a component). A label needs no admin/project setup and gives identical filtering.
project = <KEY> AND labels = <tool>Echo: 8 backlog items → JSM project JCT as JCT-7..JCT-14, label
echo. Filter:project = JCT AND labels = echo. - Tokenless feedback link. Point the site feedback button at the JSM customer portal URL. No API token/secret in the static site.
- Migrate any existing backlog file into Jira and make the file point to Jira (single source of truth = the tracker).
Stage 5 — Document & close
- Create the production-line folder
production-lines/<tool>/with:pipeline.md,deployment-template.md(this),how-it-works.md,internal-runbook.md. - Write a how-it-works doc describing runtime flow + key modules.
- Close the tracking order (request/work-order) once the page is live and feedback loop is verified.
What to decide per tool
| Decision | Field to fill | Echo value |
|---|---|---|
| Display name | <Tool> (accents OK) |
Echo |
| Identifier | <tool> (ASCII, paths/exe) |
Echo |
| Releases repo | <owner>/<tool>-releases (public) |
bockbr/echo-releases |
| Jira label | <tool> (in project <KEY>) |
echo (JCT) |
| Signing | unsigned / Azure Trusted Signing / EV | unsigned (hook ready) |