Repository navigation
Parakeet: GPU speech engine as Flow's default (v0.8.0) - #16
Merged
Merged
Conversation
Item 1 of the production-readiness list, and the first of the five that argues with something already written down. flow/version.py said nothing calls the check but the flag, as architecture rather than habit: three tests and the README defend it, on the argument that a privacy claim is only worth anything if it is an enumeration rather than a description of the common case. So this does not flip that. profile.check_update is off in every profile that has never had it set, absent reads as off, and the distinction is drawn at did-a-person-ask rather than at did-code-do-it. Flow Home grows a switch beside the button it already had. Once per launch, on a daemon thread - TIMEOUT_SEC is 3s and 3s in front of the pill is a launch that looks hung. No timer; am I current has a shelf life measured in releases. The call-site test is what I would point at. It counted files whose text contains check_update, and failed the moment profile.py declared a bool with that name - a field that reaches no network. A test counting mentions instead of calls is a test that has to be renamed to stay true. It now walks the AST for real Call nodes, which is strictly sharper: getattr(mod, 'check' + '_update')() walked straight past the grep. It also never looked inside flow/home/, so Home's own Check-for-updates button has been a caller since Home shipped, unobserved by the assertion written to catch exactly that. Recursive now too. The startup line reads from the setting instead of asserting a fixed reassurance, for diag.py's reason: a promise printed as a constant goes stale the moment the behaviour changes. Suite 3299 -> 3304, OK.
Item 3. The per-accent WER table and the P2 false-reject bound were two hand-typed blocks in docs/roadmap.md that nothing could re-derive, so they rot silently every time accent_bench.py was re-run. accent_report.py reads .bench/accent/results-*.json and prints them; --markdown emits the table for pasting back in. It also prints what is interesting rather than what is flattering: base.en 0/300 false-rejects on the conversational slice, 3/280 = 1.1% on the short-clip slice, which is OVER the P2 bound of 1%. That number was already in the repo. Publishing it beside the passing one is the point - P2 is a claim about what a user can recover, and a bound you only ever print when you clear it is not a bound. The arithmetic is pinned in tests/test_bench.py because a report that scores wrongly is worse than no report. Two zero-denominator cases are asserted specifically: a group with no reference words and a run with no clips must both print n/a, never 0.000, because 0.000 in a WER table is a claim of perfection and a zero-clip run is a claim the product passed a test nobody ran. Suite 3304 -> 3312, OK.
Item 4. Hands-free is the one thing Flow does that Wispr Flow does not, and it was a footnote in the guide nobody reaches. Step 5 of the first run now names it - the step where this person has already held the chord once, so the sentence is one variation on something their hands know rather than a new thing to learn. A feature page would have been a sixth skippable step, which is the exact thing nobody reads. Both halves or it misleads: tap the same chord and Flow keeps listening, hold it a beat longer and it dictates as always. Describing only the tap is how somebody learns press-to-press and silently loses the mode they used every day. Lite is told to tap the pill because it has no chord to name, and the send word is read off the session rather than hardcoded - send_word is a setting with a tested preset list behind it, and onboarding that says send to somebody who changed it teaches the wrong word on their first evening. The fallback is SEND_WORD, which is boom, not send.
Item 5. docs/guide.md claimed ~1.4 s cold start (0.40 s import + 0.98 s model load), typed from a run nobody in the repo could repeat - and the two parts did not add up to the total they were a breakdown of. flow/coldstart.py plus scripts/cold_start.py replace it with a harness: fresh interpreter, four stages, median of N, and a check that the stages add up to the total they report. That last check is the one worth having. The first run printed ready=5.38 s against stages summing to 2.69 s. The obvious reading was a missing stage, so I added one for the CUDA probe; it came back at 0.23 s and explained none of it. The real cause was one line of arithmetic that took origin() after the load and then added the load duration again. Both facts are in the module docstring because the wrong inference is the useful one: a total that does not equal the sum of its parts is a bug until proven otherwise, and adding a stage to hide the difference is how a measurement ends up explaining itself instead of being right. time._start_time does not exist on Windows - it is built only on the POSIX paths - so the first version reported n/a for two of three columns on the one platform Flow ships on, and carried on. GetProcessTimes via ctypes now, with the 64-bit signatures declared: a HANDLE defaulting to a signed 32-bit ctypes int is sign-extended negative and every call fails silently. from_filetime refuses 0 and any future stamp, because 0 minus the 1601 epoch is a date in 1601 and about 1.3e11 seconds of nonsense reads like a plausible measurement. Both halves asserted, including the one that refuses too much. Measured warm on this machine: 2.97 s to first word, 0.21 import + 0.22 CUDA probe + 2.55 model load, CUDA. The guide now says that, and says the post-reboot figure is NOT YET MEASURED rather than guessing one - a warm number understates a cold boot by seconds, which is why the script labels its own output 'NOT the number a reboot gives'. Suite 3312 -> 3328, OK.
…w redundant Item 2, step 3 of docs/one-surface.md. The compact Refine panel carries an Edit chip between Copy and Send; pressing it opens a tk.Text in its own Toplevel over the panel, with Escape, Ctrl+Enter, and Cancel/Done buttons because a shortcut nobody can find is not a way out. The session seam was already there - begin_edit / commit_edit / cancel_edit do the work and the P4 learning - so the port added a chip and a box, not a feature. Its own window is forced, not chosen: this surface is layered and GDI+-composited, so it shows the bitmap Windows was handed and a tk.Text inside it would not be on screen at all. The shipped surface hit the same wall with the same answer (decisions.md, 'the shipped surface cannot be composited'). Ask gets no editor - its panel holds a conversation, not one prompt, and an editor there would be a way to correct somebody else's turn. Type gets none because paste-on-release is the point of Type. _can_edit reads _spec(), which is keyed on _panel_mode rather than session.mode: a mode switch closes the band and an answer arriving after one still draws as the Ask it is, so asking the live mode would put an Edit chip over a conversation. Both asserted. This is the port, not the retirement. ui.py is ~7000 lines and the majority of the suite still exercises it directly - test_pill.py alone has 48 references - so deleting it in the same commit would have buried a real refactor under a wall of red. Steps 4 and 5 (notes-loop strip lines, Refine's say-more) and the deletion are still open, and docs/one-surface.md now says so with the same reasoning. Suite 3328 -> 3339, OK.
Steps 4 and 5 of docs/one-surface.md, and with them the compact surface has everything the shipped one has. Step 4: the notes loop already worked - keep note and wrap up are session commands - but on a strip with no panel behind it, a note filed into a file you cannot see was an act with no feedback at all. Session now emits a 'kept' event; the strip says 'kept', or 'kept <file>' when there was a workspace. The leaf, not the path: this strip is 400px and a Windows path is a sentence. ui.py ignores the event on purpose - it already says the whole sentence and has a card stack to say it in, and two notices about one act would sit there together. Step 5: the foot has always said 'hold the mic to say more' and it did not. send() cleared the draft on the way in, so the second hold refined its own words with no memory of the first. Session now arms following_up when it delivers a refine as a result - which is what puts the previous result into the CLI's context - and the panel keeps that result on screen while the next one runs, appending the new words to the heard block rather than replacing them. Wiping it would leave you holding the mic to add a sentence to something you can no longer see. The surface reads the session's flag rather than keeping a copy, because that same flag decides whether context reaches the CLI: a private copy could say 'extending' while the CLI was asked cold. A failed refine is not extended - its CLI line stands in the same slot a result does, and there is nothing there worth adding to. Both halves are tested against each other for that reason. Suite 3339 -> 3350, OK.
Reported as 'sometimes it catches, sometimes it does not'. The window was press-to-press, which charges the user for their own press duration: two 200 ms taps with a 150 ms pause between them measure 550 ms and never pair, inside a 500 ms window. So whether the gesture worked depended on how fast they could press two modifiers - and there is no pause length that fixes it, because the press durations are inside the number. Release-to-press reads 150 ms, which is what the fingers control. Found by reading Fireflies' shipped source, not by reasoning. I had been relying on a note in hold.py claiming its numbers matched ours; they do - DOUBLE_TAP_WINDOW_MS 500, MIN_DOUBLE_TAP_GAP_MS 20, CHORD_TAP_MAX_MS 400, all identical. The constants were never the difference. Its dictation-focus.js stores lastTapUpAt on the release and compares the next press to it, and that measurement point is the whole fix. I should not have repeated 'it matches' without opening the file. Two guards keep this from becoming 'anything pairs'. A key bounce 10 ms after the release is still refused by MIN_DOUBLE_TAP_GAP_MS, or widening the window would turn bounce into hands-free listening. And a pause one millisecond past the window still refuses, so a window that only ever accepts is as broken as one that only ever refuses. Both boundaries asserted from both sides. Suite 3350 -> 3355, OK.
The double tap was not a race and never was. A trace off the owner's own machine said so: of 74 deliberate presses the median hold was 626 ms and only 28% came in under TAP_MAX_MS (400). A two-modifier tap on that keyboard is a ~600 ms press, so most presses were classified as sentences, and a double tap is two of those inside a 500 ms window -- which no hand produces twice in a row. The previous commit widened the pairing window and could not have fixed it; it was tuning the wrong number. One press has no window to miss. Holding past TAP_MAX_MS is still a sentence, so the two gestures stay distinct by duration rather than by count: a rule a hand can satisfy where two presses inside half a second is one it cannot. The machine now registers on_latch instead of on_double_tap and LATCH maps to toggle_action. DOUBLE_TAP stays mapped, so anything registering the other callback still works rather than falling through to no word at all. Words a person reads -- the banner, the pill's foot, the help row, the guide -- now say "tap" instead of "double-tap". The help row keeps "release to send", which is deliberate and nearly lost: it is the half that stops people hunting for a second shortcut to find.
The filter was working the whole time. Whisper emits "Thank you for watching." into near-silence -- it is in the training data as a YouTube outro -- and clean.py drops it: all 19 standalone occurrences in this machine's history are kind=set_aside with reason="empty", none pasted, and the last seven that did paste predate commit 0bd6a31. What put the sentence back in front of the user was the reporting of that rejection. _pump_drops put Drop.describe() on the drop event, and describe() quotes the text and its evidence: dropped 'Thank you for watching.' (empty, ns=0.85 lp=-1.10, final) The compact surface *speaks* its events. So a hallucination on silence came back out of the speaker as a sentence nobody in the room had said, with the bare numbers read as gibberish on the way. The flaky tap gesture was the trigger -- a burst of near-empty captures produced a burst of these -- which is why it recurred across a session. Drop.announce() is the surface form: no quoted words, no bare numbers, one clause a person can act on. It reports that a segment was refused and why, never what it said, except a word count. describe() is untouched and the text still goes to History, which is the only place reading it back does any good -- a test asserts both. test_integration asserted the quoted form end to end, which is the defect stated as a requirement; it now asserts the event does not carry the words. P2 is unaffected: a rejection is still never silent.
Reported as "the GPU is not set by default". It was, and always was:
resolve_device("auto") returned cuda on this machine, cuda_reason()
returned "", and the profile already held decode_device: "cuda". Decoding
was on large-v3 in CUDA the whole time, finals at a median 956 ms.
What was wrong was the report of it. ModelManager.snapshot read the
device off the live transcriber and fell back to a literal "cpu" when
there was not one, and read the asked-for device off the same object with
an "auto" fallback. session.asr is None for the whole warm-up -- which is
exactly when somebody opens Settings to check whether the GPU is being
used. So for as long as the model took to load, a GPU machine was told
"cpu" and the "Run speech on" dropdown showed "Automatic" instead of the
stored choice.
Both now come from the profile when there is no transcriber yet, and the
device is resolved through the same resolve_device the startup line uses,
so the page and the console cannot disagree about one machine. A loaded
transcriber still wins; the profile is only the fallback for the window
before it exists.
The default is deliberately unchanged. Forcing "always GPU" would have
been redundant and would have taken away the ability to pin the CPU.
Five tests, checked against the old code: three fail with 'cpu' != 'cuda'
and 'auto' == 'auto'.
`test_hold.py` and `test_pill.py` carry UTF-8 that was read as cp1252 and written
back, so an em dash arrived as three characters. Both files, 61 sites, all in
comments and docstrings.
The two files are damaged differently, which is why this is two substitutions and
not one pass:
test_pill.py single level - `c3 a2 e2 82 ac e2 80 9d` ("â€"")
test_hold.py double level - the mojibake string was itself re-encoded, so the
eight characters `ââ'¬â€\x9d` are one em dash damaged twice
Byte substitution rather than `text.encode('cp1252').decode('utf-8')`, because the
double-level form contains U+009D, which cp1252 cannot encode at all, and a
fallback that guesses the undefined half is how one mojibake becomes another.
Only these two sequences are touched. The repo legitimately contains a section sign
and a middle dot, both correctly encoded, and a sweep over "suspicious" codepoints
would have rewritten both.
Measured rather than asserted: every changed line was classified against the parsed
tree and none is neither a comment nor inside a docstring, so behaviour cannot have
moved. Every file still decodes as UTF-8 and no C1 control survives. 3374 tests, all
pass but the known lifecycle teardown race.
The cause was a shell redirect reading these as cp1252. `subprocess` did the same
thing to `git diff` while writing the checker for this, which is why the checker
passes encoding="utf-8".
`--engine parakeet` runs NVIDIA Parakeet TDT 0.6B v3 (int8) through sherpa-onnx, behind a `[parakeet]` extra, so a default install still fetches three packages (R16). `auto` never chooses it. Measured on this machine (Zen 2 CPU, 8 threads) on the 300-clip EdAcc slice, errors per 100 words, Parakeet against small.en: indian 18.9/21.9 japanese 26.3/23.4 russian 9.9/15.1 spanish 14.6/16.6 us-control 19.9/22.1 all 17.3/19.5 large-v3 on the GTX 1070 scores 16.8. Real-time factor 0.070 on the CPU (small.en manages about 0.54 there), load 2.2 s. Japanese-accented English is the one group where it loses, and the docstring says so. It is not `blind`. On 8 silence and noise clips small.en invented text on all 8; Parakeet returned nothing on 7. The eighth, "It is." on fan noise, had a mean token log-prob of -1.10, against real speech at p1 -0.59. That becomes `clean.TOKEN_LOGPROB_MIN = -0.8`, which is read only when `no_speech_prob` is None, so Whisper's rules are unchanged. It rests on one invention and is marked provisional. The model is fetched once from GitHub releases, not huggingface.co, into ~/.flow/models. Every archive member is checked before extraction, and the fetch is atomic. That makes parakeet.py the second module allowed a URL opener; test_version and architecture.md both name it. The moondream parakeet-redux (Photon) route was tried first and dropped: the Windows wheel has no AVX2 kernel and its GPU kernels need sm80+. 3433 tests; the 2 failures are the existing test_lifecycle grandchild failures, reproduced on a clean HEAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner is buying a Mac and has decided the native body gets built. That supersedes the 2026-08-02 rule that the port waits on Lite's clipboard-hop number; the number is still worth recording as a baseline. docs/mac-plan.md lays out what exists today, layer by layer, and orders the work so that measurements come before the code that depends on them: a first-day checklist on the Mac, the engine choice (no CUDA there, so Parakeet is the leading default), a CGEventTap hotkey helper in the flow_stt pattern, the pill's focus question, `say` for spoken replies, and a signed, notarised .app so permission prompts name Flow. Nothing in it has run on a Mac. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Speech recognition card gains an engine control, Whisper or Parakeet. Choosing Parakeet collapses the two tier dropdowns into one line saying why: one model does both, it runs on the CPU, it is weaker on Japanese-accented English, and it takes no hotwords. Parakeet gets its own catalog row with size, errors and download/cancel/delete. Its speed is marked "on CPU" with a footnote, because the column is otherwise measured on the GTX 1070. It is not tagged as inventing words in silence; the spike showed it does not. Switching is live. Session.set_engine loads the new transcriber on its own thread, then swaps it in on the session thread through DecodeWorker.swap, which refuses under the worker's lock if anything is queued or decoding. Only then is the old one unloaded. It is refused, with a note, while talking, while capturing, while a reply plays, or mid-switch. Transcribers are cached by engine, so switching back keeps the tier choice and baseline. The choice is profile.engine, saved only once a switch succeeds. An explicit --engine still wins for a launch. A saved "parakeet" whose runtime or model is missing starts on Whisper and says why; it never puts a 465 MB download in front of the pill. The Models page offers the download instead, and switches when it lands. fetch() gains a cooperative cancel that leaves no partial model. With the add-on missing, as in the packaged .exe, the option is disabled with the install command as its reason. 3493 tests; the 2 failures are the known test_lifecycle grandchild pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GPU spike found Parakeet cannot run on the GTX 1070 through any ONNX route, but it measured the full-precision export on the CPU, and that is the most accurate model Flow has measured. scripts/parakeet_bench.py, the 300 EdAcc clips, 8 CPU threads, errors per 100 words: group fp32 int8 small.en indian 15.5 18.9 21.9 japanese 20.3 23.9 23.4 russian 9.4 10.4 15.1 spanish 13.0 13.9 16.6 us-control 17.2 19.9 22.1 all 14.7 16.9 19.5 (large-v3 on the GPU: 16.8) Real-time factor about 0.07 for both. fp32 invented nothing on the 8 silence and noise clips. The accuracy and log-prob figures repeated exactly across two runs; only the speed moved. So the runtime changes from sherpa-onnx (93 MB, int8 only) to onnx-asr (~7 MB; onnxruntime was already locked through faster-whisper), and the Models page offers Accurate (fp32, 2.4 GB) and Light (int8, 640 MB). The choice lives in profile.parakeet_model and switches live through the same safe swap as the engine. "Weaker on Japanese" now applies only to Light. The files come from huggingface.co (istupakov/parakeet-tdt-0.6b-v3-onnx), pinned to commit 8f23f0c0, with every file's size and published SHA-256 checked. They land as plain files under ~/.flow/models, because ORT refuses external weights behind HF-cache symlinks. This build no longer avoids huggingface.co; the egress note and test_version say so. The old sherpa folder is never deleted for you; the page offers to remove it. TOKEN_LOGPROB_MIN stays at -0.8 for both builds, because the scales agree: real speech p1 is -0.51 for fp32 and -0.57 for int8, and it catches the measured inventions "Ha ha" (-0.90) and "It is." (-1.10). Only three inventions exist to set it by, and the comment says so. The Models page now highlights the best errors figure among the models that do not invent words in silence, which is fp32's 14.7, in place of a hardcoded large-v3. large-v3's note now says it is the most accurate *Whisper* model. 3531 tests; the 2 failures are the known test_lifecycle grandchild pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From using it live: a switch made while Flow was busy was refused with a note on the pill, so the only sign of progress was retrying until it took. And the Engine and Version controls sat above a table of every model from both engines, which did not say what it was showing. A switch asked for while Flow is busy is now queued, not refused. Busy means a reply playing, speech being gated or held, or a decode queued or running. Session._pump_engine runs at the end of pump_results, the call both surfaces make every frame, and applies the queued switch on the first idle frame; no timer, no polling. A newer choice replaces it, and choosing the engine already running cancels it. Hard refusals (no add-on, no model, unknown engine) still refuse. speech.switch carries the state to the page: waiting, downloading (bytes and percent), loading (with the measured load time), done, or failed with the reason. It is derived on the server, so it survives a reload. A strip under the Engine control shows it, with Cancel. The sidebar footer still reads the engine actually running, so it never claims the new one early. The Engine control now filters the table, headed "Whisper models" or "Parakeet versions". The separate Version control is gone; each Parakeet row carries "Use this" or "Download & use". The footnote shows only the sentences that apply to the rows shown. docs/flow.gif leaves the sdist. This change took the sdist 3,811 B over its 2 MB guard. The guard exists to catch a 14 MB .bench or tool cache slipping in, which it still would. The 688 KB README tour is shown by GitHub and read by nothing an install runs. 3561 tests; the 2 failures are the known test_lifecycle grandchild pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every ONNX route failed on the GTX 1070 (sm61): sherpa's CUDA wheel has no kernels for it, onnxruntime-gpu 1.30 needs CUDA 13, which drops Pascal, and 1.22's cuDNN fails on its convolutions and LSTM. parakeet.cpp (ggml, MIT) runs it. scripts/parakeet_bench.py gpu, 300 EdAcc clips: backend errors/100 RTF p50 / p95 silence inventions CUDA 14.5 0.020 107 / 229 ms 0 of 8 Vulkan 14.5 0.036 211 / 386 ms 0 of 8 That is the fp32 build's accuracy from 8-bit weights, at 50x real time: about 3x faster than Parakeet on the CPU and about 8x faster than Whisper large-v3 on the same card. It runs as a helper process, parakeet-server.exe, so a crash is an exit code and not a dead interpreter. Flow passes --host 127.0.0.1 explicitly, and netstat showed it listening on loopback only. A Job Object with KILL_ON_JOB_CLOSE takes it down with Flow, even on a hard kill. Flow now downloads and executes a binary, so it trusts nothing it has not pinned. The v0.5.0 release zips are pinned by SHA-256 (matching GitHub's published digests); only parakeet-server.exe and LICENSE are extracted, matched by basename. The extracted exe is pinned too and re-hashed before every launch (0.05 s Vulkan, 0.17 s CUDA). The q8_0 GGUF is pinned to a mudler/parakeet-cpp-gguf commit and its published SHA-256. CUDA is used when an NVIDIA GPU and cudart, cublas and cublasLt are present. Those come from NVIDIA's pip wheels in the [cuda] extra, which gains nvidia-cuda-runtime-cu12, never from a third-party zip. Otherwise Vulkan, a 36 MB helper with no CUDA at all. Windows only for now; mac-plan notes the Metal build. The gate uses the helper's per-word confidence, through clean.WORD_CONF_MIN = 0.45 and the reason "unconfident-words". Real speech: p1 0.52, p50 0.89. There were no inventions to calibrate it against; the comment says it is exp(TOKEN_LOGPROB_MIN) carried over. The Models page gains a third Parakeet row. Its speed is on the same GTX 1070 basis as Whisper's, and it says "via CUDA" or "via Vulkan". The card at the top says the GPU is in use when it is. A credits line covers NVIDIA (CC-BY-4.0), onnx-asr and parakeet.cpp (MIT); so do the README and architecture.md. 3656 tests; the 2 failures are the known test_lifecycle grandchild pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trace's identity named Whisper's model revisions and the agent CLIs, but nothing about Parakeet, so a decode on any of its three builds could not be traced to the weights or the binary that produced it. Each Parakeet transcriber now says what it is: the build, the model at its pinned commit, and the runtime. That is onnx-asr and onnxruntime for the CPU builds, and the parakeet.cpp release, backend and the pinned SHA-256 of the helper executable for the GPU build. record_identity appends it at startup. Because the engine can now change live, the switch also writes the new engine's identity at the moment it takes over, so the trace says where the decodes changed hands. Values are tokens, because diag refuses anything else; the first tests found that out, with "<refused>" in place of every value. So: "parakeet-gpu", the bare 40-character commit (each model name has one repo), "v0.5.0-cuda", and the executable hash's first 16 hex digits. A test holds every build's values to diag's own _TOKEN pattern. 3661 tests; the 2 failures are the known test_lifecycle grandchild pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`auto` never selected Parakeet; that rule predates every measurement of it. On 300 clips of accented English, Parakeet on the GPU makes 14.5 errors per 100 words against 16.8 for large-v3, at a real-time factor of 0.020 against 0.190. It invented nothing on 8 silence and noise clips, where small.en invented on all 8 and was caught only by no_speech_prob. On the owner's own log, finals went from a 790 ms median to about 70 ms. On the owner's own voice (4 clips, 64 words) it made 4 errors to large-v3's 7. Precedence is now: an explicit --engine; then the engine saved on the Models page; then auto, which is Parakeet GPU when this PC has a GPU backend and the build is installed. With a backend but no build, Flow starts on Whisper and downloads the build in the background through Home's own download-then-switch, so the status strip shows it and it takes over when it lands. profile.engine now defaults to "" rather than "whisper", so "never chose" and "chose Whisper" are different; a saved "whisper" is honoured. Whisper model flags count as asking for Whisper. The catalog keeps large-v3 and small (the GPU pair, and the multilingual ones) and small.en and base.en (the CPU pair for the default install). It drops large-v2 (worse and slower than large-v3), medium.en, and the three that invent words in silence. Nothing is deleted; the Whisper tab offers to remove the dropped models still on disk, by name and size, after a confirm. A dropped model named by the profile or a flag still works and shows as "no longer listed". Whisper keeps what Parakeet cannot do, and the page says so: Hindi and Hinglish heard and written as English (asr.TASK = "translate", on the multilingual models), languages beyond Parakeet's 25, the hotword retry of a mis-heard command, and a PC with no GPU backend and no add-on. decisions.md records the reversal, the evidence, the cut with each reason, and what reopens it. 3708 tests; the 2 failures are the known test_lifecycle grandchild pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two Parakeet builds on this PC were "Access is denied" to the owner's own Flow, so they showed as not downloaded and could not be loaded or deleted. Each had a protected ACL - SYSTEM, Administrators, OWNER RIGHTS, nothing inherited - and was owned by whichever identity downloaded it. The cause is tempfile.mkdtemp. On Windows, Python 3.12.4+ gives its directory exactly that owner-only ACL, and fetch() built each model in a mkdtemp scratch directory and renamed it into ~/.flow/models, which kept the ACL. _scratch now makes a plain uuid-named directory, which inherits the parent's ACL like every other folder Flow makes. Measured before the fix: a mkdtemp directory's icacls shows no inherited "(I)" entry, and a plain mkdir beside it does. The new Windows test checks that "(I)" is present. The two folders already broken need an administrator to take ownership or remove; Flow cannot fix a folder it cannot open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two gaps left by making Parakeet GPU the default. A fresh PC with a GPU backend started the Parakeet GPU download in the background, while the first run's model step still offered Whisper's pair, so it fetched large-v3's 2.9 GB too. main() now hands Home the same _gpu_auto answer that chose the engine (Home.gpu_auto), so the page does not re-derive the precedence. On that path the step is one Parakeet GPU line, showing the job the background fetch already started; it never starts a second (a test patches start). "Short on space or time?" still offers the CPU Whisper pair. With no backend the payload is exactly the old one, and a test pins that. A cancelled background fetch used to start again on every launch, because nobody had chosen an engine. Cancelling a download that was waiting to switch engines now saves profile.engine = "whisper" unless a choice is already saved: the same value the Models page's Whisper button writes, for the engine the user is then running. The Models page still offers Download & use. A failure saves nothing and retries once next launch, with its reason in the strip; _background_gpu_fetch has one call site, and polling the pages starts no download. 3729 tests; the 2 failures are the known test_lifecycle grandchild pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parakeet as a third speech engine and Flow's default on a PC with a GPU, the Whisper catalog cut to four, and live engine switching from Flow Home. Per packaging/PUBLISHING.md step 5: pyproject, the lock, and the four manifests move together, and both hash fields go back to FILL-ME-SHA256 until v0.8.0's zip exists and step 1 fills them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #16's CI failed in three ways, all in tests; no product code changes. Windows (3.12 and 3.14): the folder-permission test asserted an "(I)" inherited entry, but a GitHub runner's temp directory hands down none, so even a plain folder fails it. It now compares Flow's scratch folder with a sibling made by plain os.mkdir. That is what the fix promises - no worse than the default - and it still fails if mkdtemp comes back (its owner-only ACL differs from its sibling's here). macOS: the GPU tests' fake parakeet-server is an HTTPServer, whose server_bind does a reverse-DNS getfqdn that stalls past the 20 s start timeout on GitHub's macOS runners. The fake now skips the lookup, as the real server does. TestOnePressIsTheGesture imports flow.hotkey, which is Win32 at import, so it is now Windows-only. The cold-start sum test now skips where coldstart.origin() is None (macOS exposes no start time), as its sibling test already did. The two test_lifecycle grandchild failures pass on CI; they are specific to this machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
samartomar
added a commit
that referenced
this pull request
Oct 5, 2026
`macos-latest` resolves to the image macos-26-arm64 today (macOS 26.6.2, Tahoe, Apple silicon: the PR #16 and #17 runs). It moves to the next macOS on GitHub's schedule, with no change in this repository to show it. The Mac leg exists to say Flow works on the Mac the owner is buying, which runs 26, so the suite and the Swift helper build now name it. test_packaging's platform check named the runner string, so it follows. Co-Authored-By: Claude Opus 5.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.
Summary
v0.8.0. Parakeet becomes Flow's speech engine on a PC with a GPU; Whisper stays for Hindi, for the CPU-only default install, and for the hotword retry.
Catalog: keeps
large-v3+small(GPU pair; multilingual, so Hindi/Hinglish → English viaTASK = "translate") andsmall.en+base.en(CPU pair). Dropslarge-v2,medium.en,distil-large-v3.5,distil-large-v3,large-v3-turbofrom the list; nothing is deleted, and a flag or profile naming one still works.Also in this branch, from before this work: the update check, the compact surface's hand editor, the one-press gesture, the device-truth and hallucination-speech fixes (11 commits from
9690330to94e85cc).Evidence
300 EdAcc clips of accented English,
scripts/accent_bench.pyscorer, GTX 1070 (scripts/parakeet_bench.py, results in.bench/accent/results-parakeet-*.json):no_speech_prob)After: Parakeet GPU finals ~70 ms.
uv run python -m unittest discover -s tests→ 3729 tests; the only failures are the two pre-existingtest_lifecyclegrandchild tests, which fail identically on94e85cc.Merge Danger
Door: two-way
The new default is a precedence rule plus a saved profile value;
--engine whisperor one click on the Models page goes back, and reverting the branch restoresauto → whisper. Dropped Whisper models are de-listed, never deleted.Blast Radius: engine
parakeet-server.exe, MIT). It is pinned to the v0.5.0 release by SHA-256, re-hashed before every launch, and bound to 127.0.0.1.WORD_CONF_MIN = 0.45) has never seen an invention to calibrate against.docs/mac-plan.md).[cuda]extra gainsnvidia-cuda-runtime-cu12; the[parakeet]extra isonnx-asr[cpu].🤖 Generated with Claude Code