Skip to content

feat(dictation): Parakeet idle unload/warm-up, tap-or-hold mode, custom dictionary - #279

Merged
sambitcreate merged 8 commits into
mainfrom
feature/dictation-parakeet-modes
Sep 30, 2026
Merged

sambitcreate merged 8 commits into
mainfrom
feature/dictation-parakeet-modes

Conversation

@sambitcreate

Copy link
Copy Markdown
Owner

Summary

This PR is the first slice of the Handy P1 dictation work. It adds three things:

  • Parakeet idle unload and warm-up: the on-device model is unloaded after a configurable idle period, and preloaded when you press the hotkey or start the mic.
  • Activation modes: the dictation shortcut can be hold, toggle, or the new hybrid "Tap or hold".
  • Custom dictionary: your own words and replacements are applied to every transcript, for example "aiden" → "Aiden".

VAD, history and mute are out of scope and deferred.

Source: the user's request "Parakeet idle unload / warm-up, then dictation activation modes + custom dictionary (Handy P1); VAD / history / mute later."

Plan: docs/plans/dictation-parakeet-modes-plan.md

Changes by surface

Main process

parakeet-idle-unload.ts (new)

  • A lease-counted idle timer. The countdown starts only after the last in-flight status, warm or transcribe call ends.
  • A read of the idle period that races a new lease cannot arm the timer.
  • Changing the setting re-arms the countdown.
  • Unloading kills the utility process, which is the only reliable way to free sherpa-onnx native memory. An in-process fallback releases its recognizers instead.

Parakeet protocol, worker, process core and engine

  • New warm request, serialized through the existing transcription lane.
  • The protocol version is unchanged because the parent and the worker ship in the same bundle.

dictation-coordinator.ts

  • New hybrid mode. The key watch starts at press time. A release less than 300 ms after key-down counts as a tap and latches recording on. A longer hold works as push-to-talk.
  • Press and release timestamps are taken when the call happens, so a slow pill cannot misclassify a hold.
  • If the watch fails, recording latches as toggle and shows the hint "Press the shortcut again to stop".
  • Warm-up runs once per press from idle and is best effort.
  • The dictionary is applied after optional LLM cleanup. If it fails, the original transcript is kept.

dictation.ts

  • Wires in the mode resolution, warm-up (only when the voice provider is local) and the dictionary.

settings:set

  • Validates dictationActivationMode, which also sets dictationHoldToTalk, so the Linux portal hold binding keeps working.
  • Validates dictationDictionary and localVoiceIdleUnloadMinutes, and re-arms the idle timer when the period changes.

localVoice:warm IPC

  • Fire-and-forget. Errors are swallowed.

runtimeSettingsFrom

  • Drops invalid hand-edited shapes of the three new keys.

Renderer

Settings → Voice

  • "Shortcut behavior" gets a third radio card, "Tap or hold". It uses the same unbordered well/selection cards as the existing two.
  • On-Device Engine gets a new "Free memory when idle" select: Never, or 2, 5, 10 (default), 15, 30 or 60 minutes.
  • New "Custom Dictionary" group:
    • "Heard as" and "Replace with" inputs;
    • a list of rules with named remove buttons;
    • an inline error announced with role="alert".

Composer mic

  • Warms the local model when capture starts.
  • Applies the dictionary to its transcript.

Onboarding

  • The voice tile copy now mentions tap/hold and custom words.

Docs

  • New plan doc and a row in docs/plans/README.md.
  • New memory note: .memory/dictation-parakeet-modes.md.
  • Two entries in .papercuts/troubleshooting.md.

Tests run

  • npm run test:voice: 139 passed, 0 failed. New files are registered in this script:
    • parakeet-idle-unload.test.ts
    • dictation-dictionary.test.ts
    • dictation-preferences.test.ts
    • dictation-dictionary-settings.test.tsx
  • The script also runs 9 new hybrid, warm-up and dictionary tests in dictation-coordinator.test.ts.
  • main/services/config-store-core.test.ts and main/services/dictation-coordinator.test.ts: 128 passed. This includes a new test for normalizing hand-edited preferences.
  • npm run test:onboarding: 64 passed.
  • npm run test:settings-design: 60 passed.
  • renderer/components/composer.test.tsx and dictation-hold-settings.test.ts: 25 passed.
  • renderer/components/environment-subagents-contract.test.ts: 28 passed. I replaced its source-grep assertions on dictation-shortcut-settings.tsx with behavioral checks of resolveDictationActivationMode.
  • npm run type-check, scoped ESLint and git diff --check all pass.

Follow-ups

  • VAD trimming, dictation history, and muting other audio while recording.
  • Remote (iOS/Android) transcription that uses the Mac's Parakeet does not apply the dictionary yet. Native on-device dictation parity is still to come. The server contract is unchanged, so this PR does not touch the mobile clients.
  • Acceptance on real hardware is still needed: macOS hold/tap timing, the Linux portal in hybrid mode, and memory actually reclaimed after an idle unload.

🤖 Generated with Claude Code

…custom dictionary

- Unload the on-device Parakeet worker after a configurable idle period
  (default 10 minutes, or never) and preload it on hotkey press or mic start.
- Add a hybrid "Tap or hold" shortcut mode alongside hold and toggle.
- Apply a user dictionary of words and replacements to every transcript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Hybrid activation and locale-sensitive dictionary matching fail for supported inputs. Please correct these user-visible behavior paths before merging.

Reviewed changes I reviewed PR #279's Parakeet model lifecycle and warm-up, activation-mode state machine and settings, custom dictionary behavior, and composer/onboarding changes.

  • Parakeet lifecycle and warm-up. Idle leases cover status, warm-up, and transcription; a new worker request preloads the recognizer and the configurable idle policy terminates the utility process.
  • Activation modes. Settings persist toggle, hold, and hybrid behavior, including the Linux portal release path and hybrid tap-versus-hold timing.
  • Dictionary and UI. Shared validation and replacement logic apply to global dictation and composer microphone transcripts; Voice settings and onboarding copy expose the feature.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Luna | 𝕏

Comment thread main/services/dictation-coordinator.ts
Comment thread renderer/shared/dictation-dictionary.ts Outdated
Comment thread main/services/parakeet.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes I reviewed the two new commits since the previous Pullfrog review (6599de34, b43faa61) plus their interaction with the rest of the PR, and re-ran the focused dictation suites locally.

  • Hybrid fallback for unmappable shortcuts. beginHoldWatch() now latches toggle with a pill hint when the shortcut has no key-code mapping or no watch can start, and OS key repeats while held are ignored; regression test covers the Command+F13 class of shortcut.
  • Leased recognizer release. releaseRecognizer runs inside the transcription lane under an idle-unloader lease, so an expiring idle timer can no longer dispose the worker mid-request and fail model deletion.
  • Locale-independent dictionary matching. matchKey uses toLowerCase() to agree with the /iu matcher; the Turkish IĞDIR regression test covers it.
  • CLI speech worker warm handling. The bundled worker now answers warm requests explicitly instead of falling through to the PCM16 branch.
  • Docs and CI registry. Fix summary recorded in .memory, and the four new test files were added to scripts/ci-test-registry.json lanes.

All three open threads from the previous review were verified as addressed against the new code and resolved. Several dictation settings toggles wired to composer/consume paths remain inaccessible in this validated clone on Linux (native helpers are macOS build products, which is a pre-existing constraint noted in the plan's pending real-hardware acceptance).

Pullfrog  | View workflow run | Using GLM Flash | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes A new commit (8c2f386e) landed while the previous incremental review was being finalized, so I re-ran the incremental flow against the new head.

  • Dictionary replacement selection reworked. The pattern now keeps each rule's original Unicode spelling inside one capture group per rule (sorted longest-first), and the replace callback picks the rule owning the first defined capture. This removes the earlier locale-map round-trip entirely: dotted Turkish İ, long ſ, and Greek final ς matches now select the correct replacement via the regex engine's /iu case folding, covered by the new regression test.
  • I verified the capture-selection edge behavior directly with additional probes: rules sharing a prefix never leak each other's captures, identical-spelling duplicates keep the first entry, and same-key-different-case duplicates behave as before.

Focused dictionary suite re-run at this head: 9 passed.

Pullfrog  | View workflow run | Using GLM Flash | 𝕏

@pullfrog

pullfrog Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Pullfrog stalled

The agent stopped emitting events for 125s and was killed by the activity-timeout watchdog. The model produced no output at all before the stall — the request was sent but nothing came back. This is usually transient; re-running often succeeds.

Recent agent stderr
timestamp=2026-09-27T18:00:07.500Z level=ERROR run=ed42445f message="stream error" providerID=opencode-go modelID=glm-5.3-flash session.id=ses_f1bf9d81dffevLSJm68G5SyQBa small=false agent=build mode=primary error.error="AI_APICallError: Go usage limit exceeded"

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using GLM Flash | 𝕏

@very-hermes-bot

very-hermes-bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Hermes Review Bot

Confidence: 5

Engine: agy/gemini-3.8-flash-high
Review mode: full
Head: fcca8abaab1333f7c290a6401cfe1c3e041b3dfd
Generated: 2026-09-30T22:55:30+00:00
Reviews: 1

Summary

This change introduces the first slice of Handy P1 dictation capabilities: lease-tracked idle unloading and on-demand model warm-up for the on-device Parakeet engine, a hybrid "Tap or hold" activation mode alongside hold and toggle, and a custom dictionary applied to final transcripts across both global dictation and composer recordings. Model memory is reclaimed by terminating the utility process after a configurable quiet period (defaulting to 10 minutes), while warm-up preloads recognizers in the serialized transcription lane on hotkey press or microphone start. The hybrid activation mode captures timestamps synchronously at call time rather than dequeue time, latching capture on if released within 300 ms and operating as push-to-talk for longer holds. Dictionary parsing and replacement enforce Unicode word boundaries, preserve exact replacement casing without re-scanning, and de-duplicate spoken terms case-insensitively. Maintainers should double-check physical macOS hardware behavior when using global shortcuts during cold window launches to verify that the 300 ms tap threshold feels comfortable under default OS key repeat rates.

Confidence Score: 5/5

5/5 — All changed control flows, async concurrency races, protocol extensions, dictionary regex mechanics, and platform capabilities were traced end-to-end through callers, callees, and tests.

📁 Important Files Changed
  • main/services/parakeet-idle-unload.ts: Implements lease-counted idle timer management with revision tracking to safely unload worker processes after quiet periods without racing active work.
  • main/services/parakeet.ts: Integrates model leases across status checks, warm-up, and transcription lanes; disposes the utility process on idle; and manages in-process recognizer fallbacks.
  • main/services/dictation-coordinator.ts: Implements hybrid tap/hold activation mode, stamps keypress timestamps at event time, begins key watches from press in hybrid mode, triggers best-effort warm-up, and applies the custom dictionary after transcript cleanup.
  • renderer/shared/dictation-dictionary.ts: Shared pure module handling dictionary parsing, 200-entry limits, term cleaning, and single-pass case-insensitive regex replacement using Unicode word boundaries.
  • renderer/shared/dictation-preferences.ts: Centralizes preference normalization, idle unload duration mapping, and activation mode resolution across host capabilities.
  • renderer/components/settings/dictation-dictionary-settings.tsx: Settings UI for viewing, adding, and removing dictionary entries with accessible form validation and semantic styling.
  • renderer/components/settings/local-voice-settings.tsx: Adds the "Free memory when idle" configuration select to On-Device Engine settings.
  • renderer/components/settings/dictation-shortcut-settings.tsx: Adds the "Tap or hold" radio option to shortcut settings while keeping Linux portal hold bindings synchronized.
  • renderer/components/composer.tsx: Wires model warm-up on recording start and applies dictionary replacements to composer transcripts.

Findings

No findings.

📊 Sequence Diagram
sequenceDiagram
  autonumber
  actor User
  participant Coord as DictationCoordinator
  participant Lane as TranscriptionLane
  participant Unload as ParakeetIdleUnloader
  participant Worker as Parakeet Worker

  User->>Coord: press() [shortcut down]
  Note over Coord: Stamp pressedAt = now()<br/>Start release watch (hybrid)
  Coord->>Lane: warmLocalVoice()
  Lane->>Unload: begin() [lease++]
  Lane->>Worker: postMessage({ kind: "warm", modelId })
  Worker-->>Lane: result (model loaded in RAM)
  Lane->>Unload: end() [lease-- -> arm(idleDelay)]

  alt Quick tap (< 300 ms)
    User->>Coord: release() [shortcut up]
    Note over Coord: releasedAt - pressedAt < 300ms<br/>Latch recording = true (toggle behavior)
    User->>Coord: press() [shortcut down again]
    Note over Coord: Stop recording -> transcribing
  else Long hold (>= 300 ms)
    User->>Coord: release() [shortcut up]
    Note over Coord: Grace window expires -> transcribing
  end

  Coord->>Lane: transcribePcmBase64()
  Lane->>Unload: begin() [cancels idle timer, lease++]
  Lane->>Worker: postMessage({ kind: "transcribe" })
  Worker-->>Lane: text
  Lane->>Unload: end() [lease-- -> arm(idleDelay)]
  Coord->>Coord: applyDictationDictionary(text)
  Coord-->>User: paste(correctedTranscript)

  Note over Unload: Idle timer expires after quiet period
  Unload->>Worker: dispose() [kill utilityProcess]
Loading

Machine-Readable Findings

[]

Last reviewed commit: fcca8abaab13
Reviews (1) · Comment /hermes review to trigger a new review · /hermes review full for full re-review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes I reviewed the two commits since the previous Pullfrog review (7abc4a9, fec82e8) and checked them against the full PR context.

  • Aligned dictionary row keys. Switched the settings list to locale-independent lowercase keys, matching dictionary deduplication, and added a Turkish-casing regression for distinct I and dotless-ı entries.
  • Removed branch-specific troubleshooting notes. Dropped the two papercut entries introduced by this branch.

Pullfrog  | View workflow run | Using GPT Luna | 𝕏

# Conflicts:
#	docs/plans/README.md
#	scripts/ci-test-registry.json
# Conflicts:
#	docs/plans/README.md
#	package.json

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes I reviewed the fcca8ab merge from current main and traced its dictation delivery and composer changes against the full PR diff.

  • Secure Input delivery integration. The coordinator preserves paste-result reasons and messages, allowing the pill to explain manual paste and keep Secure Input warnings visible longer.
  • Composer queue integration. The submission gate now follows composerInputLocked, matching the editable composer behavior when messages can queue during compaction.
  • Plan and test-script conflict resolutions. The merge retains current-main entries and tests alongside the dictation plan row and focused suites.

Validation passed: npm run test:voice (152 passed, 3 skipped), composer tests (21 passed), and git diff --check.

Pullfrog  | View workflow run | Using GPT Luna | 𝕏

@sambitcreate
sambitcreate merged commit d2197df into main Sep 30, 2026
24 checks passed
@sambitcreate
sambitcreate deleted the feature/dictation-parakeet-modes branch September 30, 2026 23:21
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