Conversation
`ailoud setup` refused the sudo step, which is what it is supposed to do: with no terminal to answer a password prompt on it reports the exact command rather than hanging. A runner has no terminal, so the apt install is the workflow's job and only the rest -- the whisper release and the model files, neither needing sudo -- is left to setup. Also makes the back-merge workflow say so when there is no develop branch, instead of dying on `fatal: Not a valid object name origin/develop`. That is how its first run failed, and the copy it came from has the same weakness.
…ck-merge PR Two separate failures, both in what I added rather than in the product. The provisioned e2e job had every doctor check green and every transcribe spec exiting 3. The sandbox writes its own config naming only the MODEL and leaves `binary` at its default, so whisper-cli has to be on PATH -- while `setup` installs it under the data directory and records the absolute path in the user's config. Nothing was wrong with provisioning; the two configs simply disagreed about how the binary is found. The job now reads the installed paths back out of that config and adds their directories to PATH, read rather than spelled so bumping a pinned release cannot silently break it. The back-merge workflow could not open its PR: "GitHub Actions is not permitted to create or approve pull requests", a repository setting that is off by default. Enabled with default_workflow_permissions still `read`, so the token gains nothing beyond what this needs. Also adds the author (contact@lorem.dev) to all four manifests, the dev-tag skill and its spec, the branch and tag rules in AGENTS.md, and a publish-time check that every tarball carries the LICENSE and no source or tests -- pnpm copies the repository LICENSE into each workspace tarball, which is why no package holds its own copy, and an invariant worth relying on is worth checking. The `.agents/` skills were also missed by the rename and still said "laud".
The provisioned end-to-end job runs only on push, so requiring it on a branch would leave every pull request waiting for a check that never arrives.
Every transcribe spec on CI failed with "Exceeded timeout of 5000 ms" -- Jest's default, not the 600 s the config asks for. Jest takes the per-test timeout from the GLOBAL config. Splitting jest.config into `projects` moved `testTimeout` into each project, where it appears in `configs[].testTimeout` while `globalConfig.testTimeout` stays undefined, so every test fell back to 5 s. Confirmed both ways with `jest --showConfig`: projects 600000, global undefined; with the value also at the root, global reads 600000. Locally this was invisible. The transcribe specs were already failing fast for a missing model -- mine are still under the pre-rename path -- so nothing ran long enough to hit a 5 s limit. On CI, where setup had just downloaded them, whisper takes tens of seconds and every spec timed out. The bug was mine and the runner was the only place it could be seen. Separately, the build's own config files were in eslint's ignore list, so every editor reported "File ignored because of a matching ignore pattern" on opening one, and a mistake in them was caught by nothing -- including the mistake above. They are linted now, with Node globals and the recommended rules rather than the type-aware config the packages use. Verified by planting an unused variable in jest.config.cjs and watching eslint catch it.
Cutting 1.0.0 after 1.0.0-dev.1, -dev.2 and -rc.1 left four changelog sections describing one release. `fold-prereleases.mjs` merges them into one, keeping the subsection grouping and dropping duplicates -- including a duplicate whose copies were wrapped differently, which is the common case across two dev tags. Only the same version's pre-releases fold, so an entry from an abandoned line cannot reappear under a release it was never part of. `check-changelog.mjs` is a required step on every tag, before the gate and before anything is built: a version number can never be reused and the unpublish window is 72 hours, so every reason to refuse is worth finding while refusing is free. It checks the section exists and has entries, is inside the hard limit, that nothing is stranded under Development, and that a final tag folded its pre-releases. Problems are collected and reported together, because somebody fixing a changelog wants the list rather than one round trip each. The limits were copied into three scripts, so they now live once in scripts/lib/changelog.mjs alongside the parsing all three need. A limit that differs between the script that warns and the script that refuses is worse than no limit: one of them is wrong and nobody knows which. The soft limit is a warning rather than an error -- console.warn locally, a GitHub annotation under Actions. It read as a failure before. The scripts have tests now: 20 over the shared library, and 22 driving all three end to end. Two of them WRITE, so the tests copy scripts/ into a throwaway directory beside a fixture CHANGES.md, and each asserts it is under the temp directory before running anything. One test reads the repository's own changelog afterwards and fails if it changed -- if a script ever resolves the wrong root, that is how it will be caught. They run in ci.yml, which triggers on branches and pull requests but not on tags, so a broken script is caught before a release depends on it. Also lints scripts/ and the build's config files, which were both in eslint's ignore list: every editor reported "File ignored because of a matching ignore pattern" on opening one, and `node --check` was the only gate on scripts/ -- which sees syntax, not an unused variable. Found while writing the tests: execFileSync discards stderr on success, so the soft-limit test could never have seen the warning it was asserting. spawnSync returns both streams.
Cutting a final tag leaves its `-dev.N` snapshots behind: still installable, still holding the `dev` dist-tag, still tagged. `scripts/retire-prereleases.mjs` clears them in one step, printing the plan and changing nothing without --yes. It deprecates rather than unpublishes -- npm allows unpublishing for 72 hours, the version number can never be reused after, and anyone who pinned it has their install broken. A deprecated version keeps working and says why. It deletes a tag only when the tag's commit is reachable from origin/main. The provenance of a published package names both the commit and the tag: losing the name costs convenience, but deleting a tag that holds the only reference to its commit lets the commit be collected, which costs the attestation its subject. The decision of which tags those are is a pure function in the shared lib, so it is tested without a repository; the CLI is tested against a throwaway one with a tag on each side of the line. Splitting the script tests one module per script also turned the single end-of-file "the real changelog is untouched" test into an afterEach that compares its bytes after every test, and catches a stray RELEASE_NOTES.md too -- verified by planting both.
…pm download Two failures on the runner that no local run could produce. The scripts read $GITHUB_REF_NAME as a tag fallback and print warnings as ::warning:: annotations on stdout when $GITHUB_ACTIONS is set. Both leaked into the tests through the inherited environment, so `check-changelog` with no argument found the tag `main` instead of failing, and the warning the retire test looked for on stderr had gone to stdout. The harness now scrubs every GITHUB_ variable, and tests that want that behaviour pass it explicitly -- which also gets both warning channels covered for the first time. Verified by running the suite with the runner's variables set: two failures before, none after. Separately, `corepack enable` only writes shims, leaving the pinned pnpm to be downloaded by whoever invokes it first -- setup-node's cache probe, where the download crashed on an undici assertion inside Node 24.20.0 and nothing could retry it. The setup is now one composite action, shared by all four jobs, that downloads pnpm in a step of our own and retries it three times.
… skipping Chasing the resolver's "multiple projects" warning turned up something worse behind it: `boundaries/dependencies` never saw a cross-package import written as a package name. `@ailoud/providers` resolves through node_modules to `packages/providers/dist/index.js`, which matched no element pattern, so the rule classified the target as unknown and reported nothing -- while the same import written as `../../providers/src/index.js` was caught. Listing dist under the same type as src makes both forms the same violation. Verified by planting each one in packages/core: the package-name form was accepted before this change and is an error after it, the path form and the node:fs restriction still bite, and a clean tree lints silently. The resolver now reads the root tsconfig, which includes every package's sources, instead of a glob over the per-package ones -- one project rather than four, which is what it was asking for.
The v1.0.0-dev.1 release failed at "Run the gate", not at anything about releasing: the gate includes packages/providers/src/audio/ffmpeg.test.ts, which spawns the real binary, and this job never installed it. CI installs it in both jobs that run tests for exactly this reason, and the publish job runs the same gate.
Every `pnpm test` printed "error: unknown option '--bogus'" twice, a full usage block, and one ExperimentalWarning per worker -- so a real error had to be picked out of noise the tests produce on purpose. Commander writes usage errors and no-argument help to stderr, which is right for a CLI. The four tests that provoke it now silence writeErr only; writeOut stays as buildProgram set it, because that is how help reaches context.write and one of the tests asserts on it. The SQLite notice is the flag the installed binary already carries in its shebang: node:sqlite is experimental and this project knowingly depends on it. Set through NODE_OPTIONS rather than the pool's execArgv -- that route works, but switching the pool to forks took doctor.test.ts from 3 seconds to a 225-second timeout.
`npm publish dist-npm/ailoud-core-1.0.0-dev.1.tgz` never looked at the file: npm parsed the slash as the `owner/repo` GitHub shorthand and ran `git ls-remote ssh://git@github.com/dist-npm/ailoud-core-1.0.0-dev.1.tgz.git`, which failed on a missing public key -- an error about ssh keys in a step that has no business talking to git. An absolute path cannot be read that way. The `ls` that found the file is gone too: it existed to expand a name that was never a glob, and it turned a missing tarball into a confusing npm error instead of saying which one was missing.
`ailoud audio import` takes video because a meeting recording usually is one, and domain/mime.ts maps four containers -- but nothing exercised any of them. The audio path was tested against a tone generated at run time; video needs a real container, so scripts/make-fixtures.mjs now wraps en-short.wav in each of the four and the fixtures are committed, through LFS like the audio. They are deliberately dull: 32x32 of black at 5 fps, which keeps each file under 14 kB while still being a real, decodable video stream. Each container gets the codec pair it carries in the wild, and one test per container asserts what actually matters -- that whatever went in, the 16 kHz mono WAV whisper.cpp needs comes out. The video length comes from the audio rather than from `-shortest`, which produced an 18-second mp4 from a 2.5-second source.
…to a branch Re-running the publish workflow on v1.0.0-dev.1 failed with `no "## Version main" section`. The changelog check was the one step that passed no tag and let the script fall back to $GITHUB_REF_NAME -- which is the tag on a tag push but the BRANCH on a workflow_dispatch, so a manual re-run of a good tag checked the wrong version and could never pass. The tag is now resolved once into a job-level TAG and used by all three steps that need it, so there is no second answer to which tag is being released.
Publishing failed with `404 PUT https://registry.npmjs.org/@ailoud%2fcore` even after the organization existed, and the log never mentioned trusted publishing -- because it never happened. setup-node's registry-url writes an .npmrc holding `_authToken=${NODE_AUTH_TOKEN}`; this workflow has no token secret by design, so npm found a credential, sent an empty one, and had no reason to reach for OIDC. The registry answered as it would to any stranger. Without that .npmrc npm sees it has nothing, and does the exchange the `id-token: write` permission is there for. The default registry is registry.npmjs.org regardless, so nothing else changes -- and the `Unknown user config "always-auth"` warning was setup-node's file too.
…t can work npm answers ENEEDAUTH for @ailoud/core however complete the OIDC setup is -- npm 11.19.0, id-token: write, no stale .npmrc -- because a trusted publisher is attached to a package on npmjs.com and there is no page to attach it to until the package exists. The first version of each of the three has to go out on a credential; nothing about the workflow was wrong. So NPM_TOKEN is used when the secret is present and ignored when it is not, which makes removing the secret the whole of the switch to trusted publishing. Provenance is attached either way. The token is a bootstrap, not a fixture: once all three package pages have the publisher attached, deleting the secret leaves the arrangement with nothing to expire, which was the point of using OIDC in the first place.
Four places said only a final tag moves `latest`, without qualification. The first release proved otherwise: npm set `latest` to 1.0.0-dev.1 on all three packages, because it does that on a package's first publish whatever `--tag` says, and `latest` can be moved but never removed. So `npm install ailoud` returns the snapshot until 1.0.0 exists. Nothing in the workflow can prevent it, which is exactly why it belongs in the documentation rather than in a check.
All three npm pages read "This package does not have a README" after 1.0.0-dev.1 -- the first thing anyone arriving from a search saw. npm shows the README from inside the tarball, and no package directory had one. The two libraries get their own, short and specific: what the package is, how it relates to the other two, and that its interfaces are not stable and there is no reason to depend on it directly. The CLI's README is the repository's, so rather than keep a second copy in git that would drift, its prepack script copies the root file in at pack time -- verified identical in the packed tarball -- and the copy is gitignored. The packing guard now fails a release whose tarball has no README, beside the existing licence and no-source checks: the CLI's copy is a script that could stop working quietly, which is exactly the kind of thing that guard is for.
`ailoud --version` on the published 1.0.0-dev.1 answered `0.0.0`, and the MCP server told every client the same, because buildProgram passed commander a hardcoded string. Found by installing the release from the registry rather than by reading the code -- nothing in the repository disagreed with itself. The version now comes from the package's own manifest, resolved relative to the module so `dist/version.js` finds it one level up both here and in an installed package. The manifest is the one copy a release already updates and it ships inside the tarball, so it cannot go stale. The test asserts agreement with the manifest rather than a literal, so it needs no edit per release -- an edit per release is what would rot it into agreeing with whatever is there. Checked both ways: it fails against the old hardcoded value, and the packed tarballs installed into a scratch project report 1.0.0-dev.2.
The token exists to introduce each package to the registry, because a trusted publisher is attached to a package that already exists and there is no page to attach it to before the first publish. That bootstrap is over the moment the three packages exist -- but a token that keeps working is a token nobody gets round to removing, and the whole point of OIDC was having nothing stored. So the rule is enforced rather than remembered: a pre-release published on the secret logs a warning, and a final release refuses before anything is published and names the two steps that clear it. Publishing with no secret goes through OIDC exactly as before. Simulated all three paths -- pre-release with the token, final with the token, final without -- because the first version of this check read $version above the line that assigns it, which under `set -u` would have failed every release rather than only the ones it means to.
1.0.0-dev.2 cannot be republished -- npm's policy is that a version number is never reused, even after an unpublish -- so verifying that a release needs no stored credential takes a new number. Saying so in the changelog beats leaving someone to wonder what changed between two snapshots that are the same code.
…red token Deprecating the snapshots a release supersedes was a manual step because trusted publishing is defined for publishing: `npm deprecate` in the same job has nothing to authenticate with, and I was not willing to guess otherwise halfway through a release. Reading npm's own lib/utils/oidc.js settles it. Publishing gets its credential by exchanging the CI identity for a per-package token -- a GitHub id token with audience `npm:registry.npmjs.org`, posted to `/-/npm/v1/oidc/token/exchange/package/<name>` -- and the exchange is an ordinary request anything can make. So the script makes it, and retiring a release needs no more stored credential than publishing one. The token never reaches a command line or a log: it goes into a temporary 0600 npmrc that is removed in a finally, which the tests check both ways. `retire.yml` carries this, called by publish.yml after a final release and dispatchable alone -- without `confirm` it exchanges a token, uses it for nothing, and reports whether it worked, so the credential path can be checked without waiting for a release to find out.
A dispatchable retirement was two mistakes at once. Retiring snapshots means nothing unless something supersedes them, and the standalone run could not authenticate anyway: npm binds a trusted publisher to a workflow file, so a run entered through retire.yml is a different identity from one entered through publish.yml and the exchange is refused with `OIDC token exchange error - package not found`. The dry run proved that before a release depended on it. So retire.yml is workflow_call only, reached by publish.yml for a final tag, and its `confirm` input is gone -- it could only ever have been true, and a knob with one reachable value describes a choice that is not there. The script keeps its plan-first default for the laptop, where nothing has been decided. It runs after the publish rather than before. Deprecating the snapshots first would, if the publish then failed, leave every -dev.N pointing at a release that does not exist while `dev` is the only thing installable.
The release rules had grown by accretion: credentials were explained in two places, the retirement rules were spread over four paragraphs written as each came up, and a sentence about the changelog check sat at the end of a section about tags. Anyone reading it would have had to assemble the rules themselves. Now "Branches and Tags" covers branches, tags and the changelog fold, and a "Publishing" section covers the rest: the OIDC exchange with the two calls verbatim, the three consequences that constrain anyone changing it, the bootstrap exception with the token as a table, retirement, and the npm facts none of it can work around -- a version number used up forever, `latest` set on first publish and never removable, and trusted publishing covering `npm publish` and nothing else. Every claim states what it costs to get wrong, because that is what makes a rule followed rather than looked up: why the entry workflow cannot be retire.yml, why deprecating happens after the publish and not before, why the token goes to a file and not a command line.
A new `check-dependencies` skill: advisories first, funding second, updates last -- because the first two decide what an update is for. `pnpm audit` and `pnpm audit --prod` are read separately, since a high-severity advisory in a test runner cannot reach a user and one in `commander` is on their machine. The 14-day rule is a script rather than advice, because advice is what gets skipped at the moment it matters. `scripts/check-dependency-age.mjs` refuses any pinned direct dependency published less than 14 days ago: a compromised release is found by other people and that takes days, and there is no urgency in a patch that has been out two weeks that was not there on day one. The rule yields to a critical advisory -- two weeks with a known exploit is worse than a version nobody has audited yet -- through `scripts/dependency-age-exceptions.json`, where an exemption carries its advisory ID. That makes it a decision in the repository rather than an argument someone remembers to pass, and the check reports entries that have aged out so the file does not accumulate permanent holes. It runs second in `pre-release-check`, before the tests: an update it recommends changes what everything below it is testing.
Dependabot with `cooldown: default-days: 14`. Without it Dependabot opens a pull request the moment a version appears -- exactly the window check-dependency-age exists to refuse -- and the check would then fail on Dependabot's own branch, leaving the two arguing on every update. Security updates ignore cooldown, which is the behaviour we want and the same exception the age check records for a human: a known advisory beats an unaudited release. `versioning-strategy: increase` because exact pins are the convention here; `widen` would turn a pin into a range and hand the choice of version to whatever resolved last, which no age check can judge. The dev toolchain arrives as one grouped pull request since it cannot reach a user, while anything that ships gets its own. CodeQL is committed rather than enabled through the repository's default setup, for the reason every other check here is a file: what runs, when, and over what belongs in a diff. Weekly as well as per-push, because most findings arrive when the queries improve, not when the code changes.
…acktrack
CodeQL's first run found ten things; seven were real and this is them.
Two places decided whether an endpoint is a hosted API by substring:
`baseUrl.startsWith('https://api.openai.com')` and
`/api\.(openai|anthropic)\.com/.test(baseUrl)`. Both answer yes for
`https://api.openai.com.example.net/v1`, where the part of a hostname that
decides where the request goes is the end of it, and yes again for
`https://example.net/?upstream=api.openai.com`. They now share `isHostedLlm`,
which parses the URL and compares the hostname exactly.
Four `replace(/\/+$/, '')` calls stripped trailing slashes with a pattern that
backtracks; on a value that is mostly slashes that is a denial of service, and
the value comes from configuration. `withoutTrailingSlashes` is a loop, which
is what the operation always was. Two of the four CodeQL did not flag -- same
defect, below its threshold.
`escapePackageName` used `replace('/', '%2f')`, which substitutes only the
first match. A package name holds at most one slash, so it was right by
accident rather than by what it said; `replaceAll` says it. The age check had
its own copy of the same line and now imports the one function.
The stale-lock takeover in setupLock had a real window: two runs can find the
same stale lock, both remove it, and the loser's `open(path, 'wx')` failed with
a raw EEXIST about a path the user has never heard of. It now refuses the way
every other contended case does. Not covered by a test -- reproducing it needs
two interleaved processes -- so it is one branch converting one error code.
Dependabot's first run proposed TypeScript 7.0.2, and lint failed outright: "typescript-eslint does not support TS 7.0. Please see ... to run typescript-eslint using the TS 6 API." The bump is blocked by a peer, not by anything here, and left alone it would return every Monday with the same failure. Ignored for majors only, with the tracking issue named, so patches and minors keep arriving. Drop the entry when typescript-eslint supports TS >= 7.1.
The multilingual copy of the denoise notice/warning block ran on every --multilingual transcription but had no test, unlike its single-pass twin; add the same four cases against the multilingual path.
ResourceBudget.diarizerThreads is renamed cappedThreads: the VAD segmenter has the same measured optimum as the diarizer (both slower past base - 2, for different mechanisms) but was still handed the full thread ceiling. createSegmenter now takes the capped share, and doctor's cpu line credits both engines. Also corrects whisperVad.ts's GPU comment and its test: the binary does have a GPU flag, -ug/--use-gpu, spelled opt-in rather than whisper-cli's opt-out -ng/--no-gpu, which is why grepping for the opt-out spelling found nothing and looked like proof of absence. Measured that passing it aborts the process (SIGABRT), so it stays unpassed for a true reason instead of a false one.
denoiseMessage said "no measurable noise floor" whenever the SNR was null, which is also what --denoise on/off report -- both skip the measurement entirely, so their profile is always two nulls. It now names the mode: "not measured, denoising was requested" for on, "not measured, denoising is off" for off, and keeps the true "no measurable noise floor" wording for auto's own null result. Also widens commands.test.ts's --detach validation test to actually exercise --denoise, matching what its name already claimed, and imports WavPrepared from its port module in fakes.ts instead of through the package barrel, matching the rest of that file's imports.
cappedThreads used to be min(threads, base - 2), which only ever bound by coincidence on the one 8-performance-core machine both engines were measured on. base becomes plain logical whenever performance is unknown, which is every non-darwin platform by design, so the cap stopped binding above about sixteen cores and handed both the capped engines thread counts far outside the region either was ever measured in -- a regression against the 4 threads both defaulted to before this feature existed. Replace it with an absolute ceiling, CAPPED_MAX_THREADS = 6, the only optimum ever measured for either engine. Sweep a range of plausible machine shapes across the full percent range so a future change to the formula that stops binding cannot pass review again.
testContext's fake context tracked every createStt/createSegmenter/ createDiarizer/createSummarizer call in one shared budgets array, so commands.test.ts and summarize.test.ts could only assert that some factory somewhere received a budget. createStt always does, so deleting the budget argument at createSegmenter (transcribe.ts:334) or createSummarizer (summarizeRun.ts:113) left every one of those assertions green. Split the shared array into one per factory and add a test for each of the two previously unarmed call sites. Verified by temporarily deleting the budget argument at all four production call sites in turn: each now fails exactly one test (createStt: "accepts a --max-cpu inside the range...", "uses the configured default share...", and "--no-gpu forwards gpu: false..."; createSegmenter: "forwards the resulting budget to createSegmenter under --multilingual"; createDiarizer: "forwards the resulting budget to createDiarizer under --diarize"; createSummarizer: both tests in the "--max-cpu, --no-gpu" describe block in summarize.test.ts).
--no-gpu on summarize provably did nothing: budget.gpu is never read by any summariser, since llama's -ngl was deliberately dropped for want of a measurement and the three hosted providers have no GPU to disable. Remove the flag, its forwarding in summarizeChildArgs, and its row from the cli.md and recordings.md option tables; correct recordings.md's row and the CHANGES.md entry, which both implied it applied to summarize too. --max-cpu stays: it reaches llama's -t. Also correct three comments made false by this branch's own work, following the pattern commit 7dafab5 set for their siblings in the same file: whisperCpp.ts:36 still claimed the argument list below it was unverified, contradicting line 85 fifty lines down. doctor.ts's two "no such binary is available in this environment" claims (sherpa and whisper-cli) are also now false in this environment -- both binaries are present and confirmed to exit 0 on --help -- so both are rewritten to say so, the same way whisperCpp.ts's were.
Measured across three corpora -- a 24 kbit/s Russian conference recording, FLEURS ru_ru and LibriSpeech test-clean -- plus a pink-noise sweep from 10 dB down to 0 dB, with every close comparison resolved by a bootstrap over clips rather than by reading a table. On Russian, where the models differ most, it makes about a third of `small`'s errors: 7.5% -> 2.1% on read speech, 32.0% -> 23.6% on conversation, 12.6% -> 3.5% at 10 dB. On clean English narration nothing from `small` up can be told apart, so that corpus did not decide it. The quantisation is what makes it affordable. Against the f16 build of the same model no corpus could separate the two, while this file is a third of the size -- and without a GPU it decodes at 0.449 times real time against `small`'s 0.451, because 5-bit weights halve the memory traffic and memory bandwidth is what limits CPU decoding. So the better model is free on a CPU-only machine and costs 1.7x the decode time on a GPU. An existing installation is untouched: a healthy configured model is preferred over this constant, so nobody's `small` is silently replaced. The e2e specs stopped naming the model file and now ask the models directory what `setup` left there, since hard-coding `ggml-small.bin` made them depend on which entry happened to be the default.
The chain was benchmarked on 2026-09-08 across six corpora -- the supplied 24 kbit/s Russian conference recording, AMI single-distant-mic, spontaneous Russian, FLEURS ru and de and fr, LibriSpeech test-clean -- eight whisper models, and noise conditions from clean down to 0 dB, with every close comparison resolved by a bootstrap over clips. About 26 paired comparisons: not one shows denoising improving a transcript, and five show it making one significantly worse. The one condition where `auto` fires on real audio is a far-field meeting mic, which measures 4 to 15 dB. There it moved the word error rate by 0.00 points for the default model and hurt `large-v3` by 4.6 points. So the mode was not dormant -- it switched itself on for exactly the recordings this tool exists for, and bought nothing. Worth knowing beyond the rates: on a small model the chain can drop whole passages and still return a fluent, correctly punctuated sentence. A transcript missing a third of what was said, with nothing in the output saying so, is worse than a wrong word. `on` and `auto` both still work, and `toWav16kMono` already returns before measuring when the mode is off, so the default path no longer runs the astats pass either.
Both are dominated by something smaller, measured across six corpora and eight models on 2026-09-08. `medium` is bigger, slower AND less accurate than large-v3-turbo everywhere it was tried. The f16 large-v3-turbo is 2.8x the default's download and, on a machine without a GPU, 1.9x its decode time, for an accuracy difference no comparison could separate from zero. `large-v3` takes their place at the top of the list: about 2 points better than the default on hard audio, 2 points worse on far-field meeting audio, and 2.9 GB. Retired, not deleted, because both shipped in 1.0.0 and deleting them breaks two things for existing installations. `ailoud setup --model medium` would start failing on somebody's script, for a model that works and is merely a poor choice. Worse, `configuredModelName` maps an installed file back to a catalogue name, and a name it cannot find falls through to DEFAULT_MODEL_NAME -- so `setup --force` on a machine running `medium` would have silently replaced a healthy model the user picked on purpose. That exact silent switch was found and fixed here once already. So RETIRED_MODELS keeps them resolvable while TRANSCRIPTION_MODELS, which is what the picker and the "choose one of" message read, offers only the five worth recommending. findModel and the new findModelFile search both lists; catalogue.test.ts pins the difference, and narrowing either search back to the offered list fails four of its cases. Also corrects the "up to 1.6 GB" asides: the largest offered model is now 3.1 GB.
Benchmark corpora, downloaded model files and measurement output live there. None of it is an input to the build and some of it runs to gigabytes.
… change The download figures were given in binary units while the tool prints decimal ones, so `setup` said "574 MB" and "3.1 GB" on screen while the changelog and docs said "547 MB" and "2.9 GB" for the same two files. Both `--help` strings still named `small` as the fallback default, and so did the getting-started page a new user reads first. `setup --help` now interpolates DEFAULT_MODEL_NAME and the Windows steps take their file names from the catalogue, because a name typed into a string is how all three of these went stale in the first place. The changelog's Development section goes from 18 bullets to 11, merging entries that describe one feature: the three shell-completions entries, the detach and progress entries, the resource-cap and doctor-reporting entries, the model default and the catalogue retirement, and the two MCP tool changes. The `Fixed` entry stays: the marker-block feature shipped in 1.1.0 and the fix came after it, so a released version did have the bug. eslint now ignores `tmp/`. A throwaway script left in that git-ignored scratch directory failed `pnpm lint` for the whole repository.
The guard's own comment says the bar is not spelling, and that baking today's misspellings in would turn it into a snapshot of a model version. The list had become exactly that: it encoded `small`'s output, and the new default writes "тайм-аут" (the dictionary form) and "деплай" (one vowel off), neither of which is a loanword replaced by an unrelated word. Hyphens now come out of the transcript before matching, and "деплой" matches as the stem "депл". Verified both ways: adding a word the fixture never says still fails the guard, and the reworked guard passes with `small` as well, so it is no longer tied to one model.
Diarization can only number speakers. Which `speaker_00` is Ann is knowable by exactly one party -- a person -- and the moment to ask is when a transcript has just arrived and somebody is looking at it. Until now nothing prompted for it, so recordings kept their labels and every later summary attributed decisions to a number. `job_status` on a finished transcribe job now reports `unnamedSpeakers` and one line of what to do about it. Data-driven, not standing advice: it appears only for labels no name covers, so a recording transcribed without `--diarize` gets nothing, and neither does one whose speakers are named. The MCP instructions and the agent rules block say the same thing in a sentence each; the rules block stays at 29 lines against its ceiling of 30 because the point of it is brevity. The labels travel in the job's own result, recorded where the segments are already in memory. Reading them back would mean loading every segment of an hour-long transcript on a call whose whole purpose is being cheap enough to poll -- a test asserts no lookup happens for a recording with no labels, since asserting the outcome alone cannot see that cost creeping back. Four mutations of the decision -- dropping the named-label filter, the state check, the kind check and the empty-label skip -- each fail at least one case.
An edit spliced the file at the words `## Development` and hit the occurrence inside the rules comment rather than the heading, taking the end of that sentence, the comment's closing marker and the heading itself with it. `bump-version` refuses a file with no heading to promote, which is how it surfaced.
CI caught this on the release commit. `waitForTerminal` allowed a job one minute to reach a terminal state, which was enough while the default model was `small` and stopped being enough with `large-v3-turbo-q5_0`: on a four-core CPU-only runner, jest runs suites in parallel, so several real whisper processes share those cores and a 574 MB model is loaded before any of them decodes. The jest timeout is ten minutes, so five leaves room to fail as a timeout rather than as a killed worker. Giving up now names the percentage and stage the job reached. "Expected done, received running" cannot distinguish a job progressing slowly from one that is wedged, and that distinction is the whole question when this fails again.
Two things the provisioned e2e run on the release commit taught, neither of which a laptop could have. The suite went from 4m48s to 19 minutes. Per suite: resources 67s -> 374s, pipeline 83s -> 496s, jobs 58s -> 175s. The transcribing specs test the pipeline, not model quality, so they now take their model from AILOUD_E2E_MODEL, which the workflow points at `small`. That the shipped default downloads and installs is a different question, answered by the provisioning step that runs `setup` exactly as a user does -- and it passed. The docs claimed that without a GPU the new default "is free". That was measured on one Apple Silicon laptop, where it holds: 0.449 against 0.451 seconds per second of audio. On four x86 cores it is about five times slower, because there the extra compute of 32 layers against 12 dominates the memory traffic that quantisation saves. The claim is now a range with both ends named and the advice to measure, and `--model small` is offered as the way back.
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.
Automated back-merge of
mainintodevelop.Latest commit on main:
6c7ad2c- fix: drop --no-gpu from summarize; correct stale env commentsTriggered by push to
mainin workflowBack-merge main -> developrun34197608801.Merge this PR (or enable auto-merge on it) to keep
developin sync with the latest release commit onmain. Resolve any conflicts manually before merging.