Skip to content

Add NVIDIA Parakeet TDT 0.6b v3#123

Open
ollieatkinson wants to merge 10 commits intoamicalhq:mainfrom
ollieatkinson:feat/parakeet-tdt-only
Open

Add NVIDIA Parakeet TDT 0.6b v3#123
ollieatkinson wants to merge 10 commits intoamicalhq:mainfrom
ollieatkinson:feat/parakeet-tdt-only

Conversation

@ollieatkinson
Copy link
Copy Markdown

@ollieatkinson ollieatkinson commented Mar 20, 2026

  • Adds support for NVIDIA's Parakeet local transcription model.
  • Adds model install validation (feel free to remove this if you don't want it) - I added it as I accidentally broke my local install and thought it would be useful.

Fixes #45

Summary by CodeRabbit

  • New Features

    • Added local Parakeet offline transcription option (Parakeet ONNX provider).
  • Improvements

    • Offline models now support multi-file packages, per-model folders, checksums, source links, and accurate size tracking.
    • New runtime flags and smarter selection/preload/fallback between cloud and local models.
    • More robust handling when transcription services fail (fewer duplicate notifications, non-fatal preload).
  • Tests

    • Added tests for local model discovery, artifact validation, and runtime selection.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

Adds Parakeet ONNX local transcription and artifact-aware model handling: extends model schema with per-model artifacts and runtime, updates download/sync to per-model artifact validation and directories, implements Parakeet provider and feature extractor, and adapts service/recording flows to handle runtime-aware routing and transcription-service availability.

Changes

Cohort / File(s) Summary
Model schema & manifest
apps/desktop/src/constants/models.ts
Added artifacts?:[], required `runtime: "whisper-local"
DB sync & local detection
apps/desktop/src/db/models.ts
syncLocalWhisperModels now resolves per-model artifact filenames to concrete paths (root or model.id subdir), sums sizes across artifacts, records originalModel.localFiles, and upserts only on change. Removed previous directory-wide .bin enumeration.
Model service (download/validation/cleanup)
apps/desktop/src/services/model-service.ts
Downloads to modelsDirectory/<model.id>/, supports multi-artifact downloads, per-artifact checksum verification (auto-detect sha1/sha256), persists originalModel.localFiles/sourceUrl and actual sizeBytes, deletes model dir on cancel/delete, adds getValidDownloadedModels() and selection/migration logic.
Parakeet ONNX provider
apps/desktop/src/pipeline/providers/transcription/parakeet-provider.ts
New ParakeetProvider implementing buffering + VAD heuristics, artifact validation/loading, optional preprocessor, encoder/decoder-joint ONNX sessions, stepped decoding, token decoding, and lifecycle methods (preloadModel, transcribe, flush, reset, dispose).
Feature extractor & vocab
apps/desktop/src/pipeline/utils/parakeet-feature-extractor.ts
New ParakeetFeatureExtractor, ParakeetFeatures, ParakeetVocabulary, loadParakeetVocabulary, and decodeParakeetTokens; implements FFT, Mel filterbank, normalization, and vocab parsing with blank-token handling.
Transcription service integration
apps/desktop/src/services/transcription-service.ts
Instantiates/disposes ParakeetProvider, routes by model.setup/model.runtime (parakeet-onnx → Parakeet), preloads per runtime, and passes context.modelId into provider calls.
Recording & service managers
apps/desktop/src/main/managers/recording-manager.ts, apps/desktop/src/main/managers/service-manager.ts
Added getTranscriptionService() wrapper and transcriptionServiceUnavailableNotified guard; transcription preload errors reported to telemetry but non-fatal; streaming/cancel flows check service availability and surface notification once per session.
Pipeline types
apps/desktop/src/pipeline/core/pipeline-types.ts
Added modelId?: string to TranscribeContext.
API router
apps/desktop/src/trpc/routers/models.ts
getModels uses getValidDownloadedModels() and overlays display metadata from AVAILABLE_MODELS for downloaded entries.
Tests
apps/desktop/tests/services/model-validity.test.ts
New tests for Parakeet artifact sync and runtime validity: partial vs complete artifact detection, sizeBytes and originalModel.localFiles assertions, and isModelDownloaded/getValidDownloadedModels() behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Client as RecordingManager
    participant TS as TranscriptionService
    participant PP as ParakeetProvider
    participant FE as ParakeetFeatureExtractor
    participant ONNX as ONNXRuntime
    participant FS as FileSystem

    Client->>TS: processStreamingChunk(audio, context.modelId)
    TS->>PP: transcribe({audio, modelId})
    PP->>PP: buffer frames & VAD heuristics
    alt need preload / model not loaded
        PP->>FS: read artifacts (vocab, encoder.onnx, decoder.onnx, ...)
        FS-->>PP: artifact files
        PP->>ONNX: create sessions (preproc?, encoder, decoder)
        ONNX-->>PP: sessions ready
    end
    PP->>FE: extract(features)
    FE-->>PP: inputFeatures, shape
    PP->>ONNX: encoder inference
    ONNX-->>PP: encoded representations
    PP->>ONNX: stepped decoder-joint inference
    ONNX-->>PP: token ids
    PP->>PP: decodeParakeetTokens()
    PP-->>TS: transcript text
    TS->>Client: emit transcription
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feat/updated settings layout #38: Modifies apps/desktop/src/constants/models.ts and model manifest structure—closely related to artifacts/runtime schema changes.

Suggested reviewers

  • haritabh-z01

"🐰 I hopped through code and found each part,
Artifacts bundled tight within my cart.
ONNX hums, tokens leap into a line,
Local speech wakes—hop, listen, shine! 🐇"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add NVIDIA Parakeet TDT 0.6b v3' directly and specifically summarizes the main change: introduction of the Parakeet TDT 0.6b v3 model support.
Linked Issues check ✅ Passed The PR successfully implements issue #45 objectives by adding NVIDIA Parakeet as a local speech-to-text option with full model infrastructure, provider implementation, and artifact management.
Out of Scope Changes check ✅ Passed All changes are directly scoped to adding Parakeet support: model constants, database syncing, transcription provider, feature extraction utilities, service integrations, and related validation/cleanup logic.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/services/model-service.ts (1)

120-134: ⚠️ Potential issue | 🔴 Critical

Add runtime field to database schema and populate it for Parakeet models.

The database schema is missing the runtime field entirely, but transcription-service.ts line 96 depends on model?.runtime === "parakeet-onnx" to route Parakeet models to the Parakeet provider. Without this field in the schema, local Parakeet bundles cannot be persisted with their runtime discriminator and will fail provider selection.

Required changes:

  1. Add runtime?: string field to the models table schema (apps/desktop/src/db/schema.ts)
  2. Populate runtime: "parakeet-onnx" in the whisperModelsData payload at lines 120-134 for Parakeet model IDs
  3. Include runtime in the upsertModel call at lines 513-537

Also applies to: 513-537

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/services/model-service.ts` around lines 120 - 134, Add a
nullable runtime?: string field to the models table schema (in
apps/desktop/src/db/schema.ts) so model records can carry a runtime
discriminator; then when building whisperModelsData from AVAILABLE_MODELS (the
mapping that creates id/name/description/... and artifacts) include runtime:
"parakeet-onnx" for the Parakeet model IDs (only set for those models) so
Parakeet bundles persist their runtime, and finally ensure the upsertModel call
(the payload assembled in the upsertModel invocation) includes the runtime field
so the database upsert writes it.
🧹 Nitpick comments (1)
apps/desktop/src/db/models.ts (1)

236-260: Consider extracting localFiles parsing into a helper.

The nested type-narrowing for existingLocalFiles is verbose but necessary for type safety with JSON columns. Consider extracting this into a reusable helper function to reduce complexity and improve readability.

♻️ Optional: Extract helper function
function extractLocalFiles(originalModel: unknown): string[] {
  if (
    originalModel &&
    typeof originalModel === "object" &&
    !Array.isArray(originalModel) &&
    "localFiles" in originalModel &&
    Array.isArray((originalModel as { localFiles?: unknown }).localFiles)
  ) {
    return (originalModel as { localFiles: unknown[] }).localFiles.filter(
      (value): value is string => typeof value === "string"
    );
  }
  return [];
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/db/models.ts` around lines 236 - 260, Extract the verbose
type-narrowing that computes existingLocalFiles into a reusable helper (e.g.,
extractLocalFiles(originalModel: unknown): string[]) and replace the inline
block that defines existingLocalFiles with a call to that helper; the helper
should perform the same checks (non-null object, not an array, has "localFiles"
as an array) and filter entries to strings using the same type guard, so
localFilesChanged logic (which references existingLocalFiles and resolvedFiles)
remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/pipeline/providers/transcription/parakeet-provider.ts`:
- Around line 127-159: The code calls this.reset() prematurely (after
snapshotting frameBufferSpeechProbabilities and before VAD, feature extraction,
and transcribeTdt), which can permanently drop audio on any downstream error;
change the flow so buffers are not cleared until transcription succeeds: either
(a) defer calling this.reset() until after transcribeTdt(...) returns
successfully, or (b) keep a copy of the buffer/frames (e.g. const savedFrames =
[...this.frameBufferSpeechProbabilities] and const savedRaw = rawAudio) and in
the catch block restore them by reassigning the originals (so
extractSpeechFromVad, extractFeatures, and transcribeTdt can be retried). Update
references around aggregateFrames(), extractSpeechFromVad(...),
extractFeatures(...), transcribeTdt(...), and reset() to implement the chosen
approach and ensure the catch path restores or avoids clearing buffers before
successful completion.
- Around line 471-513: The code currently calls this.releaseSessions() before
the new ONNX/ORT sessions are fully created, which can leave the provider in a
broken state if later session creation (createSessionWithFallback for
resolved.nemoPreprocessorPath, resolved.encoderModelPath,
resolved.decoderJointModelPath) fails; change the flow to build all new
resources into local variables first (call resolveModelPaths, loadModelConfig,
instantiate ParakeetFeatureExtractor, loadParakeetVocabulary, then call
createSessionWithFallback for preprocessor/encoder/decoder into local consts
like newPreprocessorResult, newEncoderResult, newDecoderResult and keep local
preprocessorProviders), and only after all succeeded call this.releaseSessions()
and atomically assign this.tdtPreprocessorSession, this.tdtEncoderSession,
this.tdtDecoderJointSession, this.featureSize, this.featureExtractor,
this.vocabulary, and this.maxTokensPerStep; on any failure during creation, make
sure to dispose/close any partially created sessions from the locals before
rethrowing so there are no leaked sessions.

In `@apps/desktop/src/pipeline/utils/parakeet-feature-extractor.ts`:
- Around line 279-310: In loadParakeetVocabulary, guard against an entirely
skipped/empty vocabulary by validating tokensById and blankTokenId: after
populating tokensById check tokensById.size and throw a clear Error if zero;
compute maxId only when tokensById has entries; after constructing tokens,
verify that tokens.length > 0 and that blankTokenId (computed from
tokens.findIndex) is >= 0 and throw an Error if not. Reference
loadParakeetVocabulary, tokensById, maxId, tokens, and blankTokenId when making
these checks so the decoder never receives -1 or an empty tokens array.

In `@apps/desktop/src/services/model-service.ts`:
- Around line 446-486: Attach the fileStream "error" listener immediately after
creating fileStream (right after the fs.createWriteStream call) to avoid
unhandled errors during open/write, and move the existing fileStream.on("error",
...) registration into that spot; additionally, handle backpressure from
fileStream.write(value) inside the read loop by checking the boolean return
value and, when write returns false, pause reading by awaiting a Promise that
resolves on fileStream.once("drain"), then resume reading; keep existing
abortController checks and the final fileStream.end logic unchanged but ensure
error and drain handlers are registered before any writes occur.

---

Outside diff comments:
In `@apps/desktop/src/services/model-service.ts`:
- Around line 120-134: Add a nullable runtime?: string field to the models table
schema (in apps/desktop/src/db/schema.ts) so model records can carry a runtime
discriminator; then when building whisperModelsData from AVAILABLE_MODELS (the
mapping that creates id/name/description/... and artifacts) include runtime:
"parakeet-onnx" for the Parakeet model IDs (only set for those models) so
Parakeet bundles persist their runtime, and finally ensure the upsertModel call
(the payload assembled in the upsertModel invocation) includes the runtime field
so the database upsert writes it.

---

Nitpick comments:
In `@apps/desktop/src/db/models.ts`:
- Around line 236-260: Extract the verbose type-narrowing that computes
existingLocalFiles into a reusable helper (e.g.,
extractLocalFiles(originalModel: unknown): string[]) and replace the inline
block that defines existingLocalFiles with a call to that helper; the helper
should perform the same checks (non-null object, not an array, has "localFiles"
as an array) and filter entries to strings using the same type guard, so
localFilesChanged logic (which references existingLocalFiles and resolvedFiles)
remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1b9b935d-8684-4f6f-832e-c3c37b2c42e6

📥 Commits

Reviewing files that changed from the base of the PR and between 1944900 and 2cdbc1c.

⛔ Files ignored due to path filters (1)
  • apps/desktop/public/icons/models/nvidia.svg is excluded by !**/*.svg
📒 Files selected for processing (11)
  • apps/desktop/src/constants/models.ts
  • apps/desktop/src/db/models.ts
  • apps/desktop/src/main/managers/recording-manager.ts
  • apps/desktop/src/main/managers/service-manager.ts
  • apps/desktop/src/pipeline/core/pipeline-types.ts
  • apps/desktop/src/pipeline/providers/transcription/parakeet-provider.ts
  • apps/desktop/src/pipeline/utils/parakeet-feature-extractor.ts
  • apps/desktop/src/services/model-service.ts
  • apps/desktop/src/services/transcription-service.ts
  • apps/desktop/src/trpc/routers/models.ts
  • apps/desktop/tests/services/model-validity.test.ts

Comment thread apps/desktop/src/pipeline/utils/parakeet-feature-extractor.ts
Comment thread apps/desktop/src/services/model-service.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/services/model-service.ts (1)

936-953: ⚠️ Potential issue | 🟠 Major

getBestAvailableModelPath() still drops Parakeet when no model is selected.

Lines 941-942 will return a Parakeet localPath if it is explicitly selected, but the fallback on Lines 946-953 only searches Whisper IDs. After this PR, a Parakeet-only install can therefore make the service report local speech models as available while this helper still returns null whenever the selection is unset or cleared. Either reuse localSpeechPreference here or make this helper explicitly Whisper-only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/services/model-service.ts` around lines 936 - 953,
getBestAvailableModelPath currently only falls back to Whisper models even
though getSelectedModel/selectedModelId can return a Parakeet model; update
getBestAvailableModelPath to also consider the local speech preference before
trying the Whisper preferredOrder: after retrieving downloadedModels and
selectedModelId, if no selectedModelId check localSpeechPreference (or the same
source that stores Parakeet selection) and if that ID exists in downloadedModels
return its localPath; otherwise continue the existing preferredOrder search for
Whisper IDs. Ensure you reference getBestAvailableModelPath, downloadedModels,
selectedModelId, and localSpeechPreference (or the method that provides it) when
making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/services/model-service.ts`:
- Around line 468-473: The cleanup is happening before the file descriptor is
released; replace any listeners or synchronous cleanup that wait for "finish" or
call fileStream.close() then immediately unlink with gating on the "close" event
instead: for the abort path where abortController.signal.aborted currently calls
fileStream.close() and unlinks artifactPath, add a fileStream.once("close")
handler and perform fs.unlinkSync(artifactPath) inside that handler (and only
call fileStream.close() to trigger it); similarly change the
fileStream.once("finish") handlers at the download-completion block to
fileStream.once("close"), and apply the same pattern for the checksum cleanup
(use checksumPath) and the general error cleanup paths so unlinking happens only
after fileStream emits "close". Ensure you reference the same fileStream,
abortController, artifactPath and checksumPath variables already used in the
function.

---

Outside diff comments:
In `@apps/desktop/src/services/model-service.ts`:
- Around line 936-953: getBestAvailableModelPath currently only falls back to
Whisper models even though getSelectedModel/selectedModelId can return a
Parakeet model; update getBestAvailableModelPath to also consider the local
speech preference before trying the Whisper preferredOrder: after retrieving
downloadedModels and selectedModelId, if no selectedModelId check
localSpeechPreference (or the same source that stores Parakeet selection) and if
that ID exists in downloadedModels return its localPath; otherwise continue the
existing preferredOrder search for Whisper IDs. Ensure you reference
getBestAvailableModelPath, downloadedModels, selectedModelId, and
localSpeechPreference (or the method that provides it) when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3cdcb56c-6cb9-4b39-bf89-89cc9110b708

📥 Commits

Reviewing files that changed from the base of the PR and between 2cdbc1c and 1652647.

📒 Files selected for processing (3)
  • apps/desktop/src/pipeline/providers/transcription/parakeet-provider.ts
  • apps/desktop/src/pipeline/utils/parakeet-feature-extractor.ts
  • apps/desktop/src/services/model-service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/pipeline/providers/transcription/parakeet-provider.ts

Comment thread apps/desktop/src/services/model-service.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
apps/desktop/src/services/model-service.ts (3)

298-305: Consider adding a defensive guard for empty arrays.

The function's return type is string, but returning downloadedModelIds[0] on an empty array yields undefined. While callers currently guard against this, adding a defensive check would improve type safety.

🛡️ Optional defensive check
 private pickPreferredLocalModelId(downloadedModelIds: string[]): string {
+  if (downloadedModelIds.length === 0) {
+    throw new Error("Cannot pick from empty model list");
+  }
   for (const candidateId of this.localSpeechPreference) {
     if (downloadedModelIds.includes(candidateId)) {
       return candidateId;
     }
   }
   return downloadedModelIds[0];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/services/model-service.ts` around lines 298 - 305, The
method pickPreferredLocalModelId currently returns downloadedModelIds[0] which
can be undefined for an empty array; add a defensive guard at the top of
pickPreferredLocalModelId to handle downloadedModelIds.length === 0 (e.g.,
return an empty string '' or throw a clear error) so the function always
respects its string return type; update any call sites if you choose to throw to
handle the error accordingly.

774-789: Checksum algorithm detection heuristic is reasonable but implicit.

The 64-character length check for SHA-256 vs SHA-1 fallback works for the expected use cases. Consider adding a brief comment documenting the supported formats, as unexpected hash lengths (e.g., MD5's 32 chars) would silently use SHA-1 and fail validation.

📝 Optional: Add documentation comment
+  // Calculate file checksum (auto-detect algorithm from expected hash length)
+  // Supported: SHA-256 (64 hex chars), SHA-1 (40 hex chars, default)
   private async calculateFileChecksum(
     filePath: string,
     expectedChecksum?: string,
   ): Promise<string> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/services/model-service.ts` around lines 774 - 789, Add a
short documentation comment above the calculateFileChecksum method explaining
the checksum detection heuristic: it treats a 64-character expectedChecksum as
SHA-256 and otherwise uses SHA-1, and that other lengths (e.g., 32‑char MD5)
will fall back to SHA-1 and may therefore fail validation; reference the
function name calculateFileChecksum and the expectedChecksum parameter so future
readers understand the supported formats and behavior on unexpected hash
lengths.

957-985: Consider renaming getBestAvailableModelPath to getBestWhisperModelPath for clarity.

The function is correctly WhisperProvider-specific (called only from whisper-provider.ts:312). The exclusion of Parakeet is intentional—ParakeetProvider has its own separate model initialization logic via initializeModel(). The generic name "BestAvailableModelPath" may suggest it handles all model types, so renaming would make the Whisper-specific purpose explicit. Also, whisper-large-v1 is absent from the codebase intentionally (not in any preference lists).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/services/model-service.ts` around lines 957 - 985, Rename
the Whisper-specific helper getBestAvailableModelPath to getBestWhisperModelPath
to make its purpose explicit: change the function name in model-service.ts (the
async method currently named getBestAvailableModelPath) and update every
usage/import to the new name (notably the call in whisper-provider.ts around the
initialization logic). Ensure exported symbols/signatures and any unit tests or
type references are updated accordingly; keep the function body and its
preferredOrder list unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/desktop/src/services/model-service.ts`:
- Around line 298-305: The method pickPreferredLocalModelId currently returns
downloadedModelIds[0] which can be undefined for an empty array; add a defensive
guard at the top of pickPreferredLocalModelId to handle
downloadedModelIds.length === 0 (e.g., return an empty string '' or throw a
clear error) so the function always respects its string return type; update any
call sites if you choose to throw to handle the error accordingly.
- Around line 774-789: Add a short documentation comment above the
calculateFileChecksum method explaining the checksum detection heuristic: it
treats a 64-character expectedChecksum as SHA-256 and otherwise uses SHA-1, and
that other lengths (e.g., 32‑char MD5) will fall back to SHA-1 and may therefore
fail validation; reference the function name calculateFileChecksum and the
expectedChecksum parameter so future readers understand the supported formats
and behavior on unexpected hash lengths.
- Around line 957-985: Rename the Whisper-specific helper
getBestAvailableModelPath to getBestWhisperModelPath to make its purpose
explicit: change the function name in model-service.ts (the async method
currently named getBestAvailableModelPath) and update every usage/import to the
new name (notably the call in whisper-provider.ts around the initialization
logic). Ensure exported symbols/signatures and any unit tests or type references
are updated accordingly; keep the function body and its preferredOrder list
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3d552e57-dc35-4e5b-8cd6-3b1d37812dc9

📥 Commits

Reviewing files that changed from the base of the PR and between 1652647 and 70345d5.

📒 Files selected for processing (1)
  • apps/desktop/src/services/model-service.ts

# Conflicts:
#	apps/desktop/src/services/model-service.ts
#	apps/desktop/src/trpc/routers/models.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/desktop/src/services/model-service.ts (1)

538-544: Minor: Progress bar may jump when Content-Length updates totalBytes.

When an artifact's size is undefined but the server returns Content-Length, totalBytes increases mid-download (line 543). This can cause the progress percentage to momentarily decrease. Consider pre-fetching Content-Length for all artifacts with HEAD requests, or accepting this as acceptable behavior for edge cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/services/model-service.ts` around lines 538 - 544, The
progress bar can jump because artifactBytes (computed from
response.headers.get("content-length") or artifact.size) is applied to
progress.totalBytes mid-download when artifact.size was undefined; update the
logic in the download flow (the function handling the response parsing where
artifactBytes and progress.totalBytes are updated) to avoid changing totalBytes
after bytesReceived for that artifact > 0 — either prefetch Content-Length for
artifacts lacking size using HEAD requests before starting downloads, or only
add artifactBytes to progress.totalBytes if progress has not yet started
receiving bytes for that artifact (i.e., bytesReceived === 0); ensure you
reference and update the artifactBytes/artifact.size check and
progress.totalBytes modification accordingly.
apps/desktop/src/db/models.ts (1)

288-294: Minor: filePath fallback may not match actual primary file location.

When resolvedFiles is null (model not fully downloaded), filePath falls back to path.join(modelsDirectory, model.id, model.filename). This path is only used in the fileExists branch (line 297), but the assignment happens unconditionally. The logic is correct since fileExists guards usage, but the fallback path construction could be misleading during debugging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/db/models.ts` around lines 288 - 294, The code
unconditionally constructs a fallback path into filePath even when resolvedFiles
is null, which is misleading; update the logic in the area using
resolveRequiredLocalFiles, filePath and fileExists so the fallback path
(path.join(modelsDirectory, model.id, model.filename)) is only computed when
resolvedFiles is truthy or when you actually need it (i.e., when fileExists is
true). Concretely, make filePath derived from resolvedFiles (using
resolvedFiles.find(...)) and avoid building the join(...) fallback unless you
confirm resolvedFiles is null and you intend to check the fallback, or set
filePath to undefined/null when resolvedFiles is null and compute the fallback
later right before the branch that uses fileExists.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/managers/recording-manager.ts`:
- Around line 582-588: getTranscriptionService() already emits the
"transcription_failed" widget-notification and returns null, so throwing an
AppError here causes the catch block to emit a second notification; instead of
throwing from this location, handle the null inline: check the result of
getTranscriptionService(), emit (or rely on the existing) notification once, and
perform an early return/cleanup from the current method (the surrounding
function in recording-manager.ts that calls getTranscriptionService()) so the
catch path is not triggered. Ensure you remove the throw of AppError here and
keep any necessary cleanup logic locally rather than relying on the outer catch
to notify.

---

Nitpick comments:
In `@apps/desktop/src/db/models.ts`:
- Around line 288-294: The code unconditionally constructs a fallback path into
filePath even when resolvedFiles is null, which is misleading; update the logic
in the area using resolveRequiredLocalFiles, filePath and fileExists so the
fallback path (path.join(modelsDirectory, model.id, model.filename)) is only
computed when resolvedFiles is truthy or when you actually need it (i.e., when
fileExists is true). Concretely, make filePath derived from resolvedFiles (using
resolvedFiles.find(...)) and avoid building the join(...) fallback unless you
confirm resolvedFiles is null and you intend to check the fallback, or set
filePath to undefined/null when resolvedFiles is null and compute the fallback
later right before the branch that uses fileExists.

In `@apps/desktop/src/services/model-service.ts`:
- Around line 538-544: The progress bar can jump because artifactBytes (computed
from response.headers.get("content-length") or artifact.size) is applied to
progress.totalBytes mid-download when artifact.size was undefined; update the
logic in the download flow (the function handling the response parsing where
artifactBytes and progress.totalBytes are updated) to avoid changing totalBytes
after bytesReceived for that artifact > 0 — either prefetch Content-Length for
artifacts lacking size using HEAD requests before starting downloads, or only
add artifactBytes to progress.totalBytes if progress has not yet started
receiving bytes for that artifact (i.e., bytesReceived === 0); ensure you
reference and update the artifactBytes/artifact.size check and
progress.totalBytes modification accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da20a18c-bc19-4029-8f4c-8249633977ed

📥 Commits

Reviewing files that changed from the base of the PR and between 70345d5 and b1861d5.

📒 Files selected for processing (5)
  • apps/desktop/src/db/models.ts
  • apps/desktop/src/main/managers/recording-manager.ts
  • apps/desktop/src/services/model-service.ts
  • apps/desktop/src/services/transcription-service.ts
  • apps/desktop/src/trpc/routers/models.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/trpc/routers/models.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/services/transcription-service.ts

Comment on lines +582 to +588
const transcriptionService = this.getTranscriptionService();
if (!transcriptionService) {
throw new AppError(
"Transcription service unavailable",
ErrorCodes.WORKER_INITIALIZATION_FAILED,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Duplicate notification when transcription service is unavailable.

When the service is unavailable, getTranscriptionService() emits a widget-notification (lines 887-890) before returning null. The subsequent AppError thrown here is caught by the catch block, which emits another widget-notification at line 613. This results in the user seeing duplicate "transcription_failed" notifications.

Consider handling the unavailability inline to avoid the throw/catch path that emits a second notification:

🐛 Proposed fix
     // NORMAL - get transcription and paste
     let result = "";
     try {
       const transcriptionService = this.getTranscriptionService();
       if (!transcriptionService) {
-        throw new AppError(
-          "Transcription service unavailable",
-          ErrorCodes.WORKER_INITIALIZATION_FAILED,
-        );
+        // Notification already emitted by getTranscriptionService()
+        this.resetSessionState();
+        this.setState("idle");
+        return;
       }
 
       result = await transcriptionService.finalizeSession({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const transcriptionService = this.getTranscriptionService();
if (!transcriptionService) {
throw new AppError(
"Transcription service unavailable",
ErrorCodes.WORKER_INITIALIZATION_FAILED,
);
}
const transcriptionService = this.getTranscriptionService();
if (!transcriptionService) {
// Notification already emitted by getTranscriptionService()
this.resetSessionState();
this.setState("idle");
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/managers/recording-manager.ts` around lines 582 - 588,
getTranscriptionService() already emits the "transcription_failed"
widget-notification and returns null, so throwing an AppError here causes the
catch block to emit a second notification; instead of throwing from this
location, handle the null inline: check the result of getTranscriptionService(),
emit (or rely on the existing) notification once, and perform an early
return/cleanup from the current method (the surrounding function in
recording-manager.ts that calls getTranscriptionService()) so the catch path is
not triggered. Ensure you remove the throw of AppError here and keep any
necessary cleanup logic locally rather than relying on the outer catch to
notify.

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.

Parakeet Model as an option

1 participant