Skip to content

feat: Implement concurrency guards for GGUF engines, optimize context windows, and add spoken-word system prompts to prevent self-triggering VAD feedback. - #11

Merged
kevinliddel merged 4 commits into
mainfrom
refacto/local-llm
Apr 30, 2026
Merged

kevinliddel merged 4 commits into
mainfrom
refacto/local-llm

Conversation

@kevinliddel

@kevinliddel kevinliddel commented Apr 30, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Bug Fixes

    • Prevented overlapping local model generations to avoid crashes and dropped requests
    • Improved recording/voice-end handling to avoid spurious transcription attempts
  • New Features

    • Per-character editable local system prompts with save/reset persistence
    • UI to edit local prompts when using on-device models; model switch restarts sessions
    • Enhanced automatic voice selection and punctuation-aware speech tuning
  • Performance

    • Increased local model context lengths for better response coherence
  • Documentation

    • Added “Offline AI Voices” guidance and updated TTS flow docs; clarified persona templates

… windows, and add spoken-word system prompts to prevent self-triggering VAD feedback.
@coderabbitai

coderabbitai Bot commented Apr 30, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@kevinliddel has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 12 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e5b4ecb6-3d57-40eb-95e3-8f6b8d96685d

📥 Commits

Reviewing files that changed from the base of the PR and between 4ef7ec1 and 6860768.

📒 Files selected for processing (1)
  • docs/npu.md
📝 Walkthrough

Walkthrough

Local GGUF engines now serialize generation with an atomic lock to prevent concurrent runs. System prompts moved to a prompt store with character-specific TTS voice selection. Llama/Qwen default context lengths were increased. TTS playback readiness is now gated on actual audio buffer consumption.

Changes

Cohort / File(s) Summary
Persona Content
NeuraLink/AI/CharacterPersona.swift
Removed example "Key Phrases" lines from Ekaterina and Dedicatus persona instructions.
Generation Guards (GGUF Engines)
NeuraLink/AI/GGUF/GGUFLlamaEngine+Generate.swift, NeuraLink/AI/GGUF/GGUFQwenEngine+Generate.swift
Added generationLock and _isGenerating checks to drop concurrent generation requests, notify delegate with empty result, and guarantee flag reset via defer.
GGUF Engine State & Config
NeuraLink/AI/GGUF/GGUFLlamaEngine.swift, NeuraLink/AI/GGUF/GGUFQwenEngine.swift
Introduced generationLock and _isGenerating internal flags; increased model contextLength defaults for larger KV-cache (Llama: 256→2048, Qwen: 1024→2048).
LlamaBridge Defaults
NeuraLink/AI/GGUF/LlamaBridge.swift
Changed LlamaBridge.init default contextLength from 256 to 2048 and updated inline documentation.
Voice Recording & Delegation
NeuraLink/AI/LocalLLMManager+Delegates.swift
Guarded voice-start to only begin when .ready; atomically capture/clear recording state on voice end and skip transcription when no real audio was recorded.
TTS Helpers & Voice Selection
NeuraLink/AI/LocalLLMManager+TTS.swift
New extension: localLLMSystemPrompt(for:) and bestAvailableVoice(for:) — enumerates AVSpeechSynthesisVoice, logs once, selects voice by character name with fallbacks.
LocalLLMManager TTS Integration
NeuraLink/AI/LocalLLMManager.swift
Switched prompt source to localLLMSystemPrompt(for:); adjusted llama prompt/token handling and maxTokens; added TTS sanitization, punctuation-driven rate/pitch, pendingTTSBuffers + ttsGenerationDone to gate .ready, restart() method, and voicesLogged flag.
Local Prompt Persistence
NeuraLink/AI/LocalLLMPromptStore.swift
Added @Observable singleton LocalLLMPromptStore to persist per-character local system prompts with defaults and save/reset APIs.
Settings/UI
NeuraLink/UI/AI/AISettingsView.swift, NeuraLink/UI/AI/PersonaSettingsView.swift
Model picker now restarts LocalLLMManager on selection when local LLM enabled; Persona settings add local system-prompt editing bound to LocalLLMPromptStore with save/reset branching.
Docs & README
README.md, docs/npu.md, docs/npu_migration.md
Renamed section text to "Tool Calling", added "Offline AI Voices" doc and TTS notes; updated TTS flow references and removed "Risk & Rollback" section in migration guide.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant GGUFEngine
    participant Lock as generationLock
    participant TTS as TTS System
    participant AudioBuffer
    participant Delegate
    participant VAD

    Client->>GGUFEngine: Request generation
    GGUFEngine->>Lock: Atomically check/set _isGenerating
    alt already generating
        GGUFEngine->>Delegate: localLLM(didFinishGeneration: "")
        GGUFEngine-->>Client: Return early (dropped)
    else proceed
        GGUFEngine->>GGUFEngine: Generate tokens
        GGUFEngine->>TTS: Send text for TTS
        TTS->>TTS: Select voice, sanitize text
        TTS->>AudioBuffer: Schedule PCM (increment pendingTTSBuffers)
        AudioBuffer->>AudioBuffer: On play complete decrement pendingTTSBuffers
        alt pendingTTSBuffers == 0 && generation finished
            GGUFEngine->>VAD: Set status = .ready
        end
        GGUFEngine->>Lock: Reset _isGenerating
    end
Loading
sequenceDiagram
    participant User
    participant VoiceHandler
    participant RecordingLock
    participant Transcriber
    participant VAD

    User->>VoiceHandler: Voice input starts
    VoiceHandler->>VoiceHandler: Check state.status == .ready
    alt ready
        VoiceHandler->>RecordingLock: set isRecordingVoice = true
        User-->>VoiceHandler: Audio captured
    else not ready
        VoiceHandler-->>User: Ignore input
    end

    User->>VoiceHandler: Voice input ends
    VoiceHandler->>RecordingLock: capture wasTrulyRecording, clear buffer, stop recording
    alt wasTrulyRecording
        VoiceHandler->>Transcriber: Send audio for transcription
        Transcriber-->>VAD: Transcript result
    else no audio
        VoiceHandler-->>VAD: Skip transcription
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Custom voice for local LLMs #12 — Matches changes introducing bestAvailableVoice(for:) and offline voice documentation; likely addressed by the TTS/voice selection additions.

Possibly related PRs

Poem

🐰 I nibble locks to keep runs neat,

Tokens hum in orderly beat.
Voices chosen, buffers drained,
Prompts persisted, tweaks retained.
Hooray — no race, the flow’s complete! 🎧✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% 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 accurately summarizes the three main objectives: concurrency guards for GGUF engines, context window optimization, and spoken-word system prompts for VAD prevention.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refacto/local-llm

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
Review rate limit: 0/1 reviews remaining, refill in 48 minutes and 12 seconds.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@NeuraLink/AI/GGUF/GGUFQwenEngine.swift`:
- Around line 43-46: The current LlamaBridge initialization uses contextLength:
2048 which can cause OOM on 4GB devices; change the default to a lower safe
value (e.g., 512 or 1024) in the LlamaBridge(...) call (reference: LlamaBridge,
contextLength) and add a short comment documenting the tradeoff and device RAM
requirement, or add a runtime check that inspects available memory and
caps/adjusts contextLength before calling LlamaBridge(modelPath: url.path,
contextLength: ...).

In `@NeuraLink/AI/LocalLLMManager`+TTS.swift:
- Around line 32-43: The switch case for "sonya" in LocalLLMManager+TTS.swift
returns a prompt that names the persona "Dedicatus" which likely is a copy-paste
identity mismatch; either change the prompt string inside the case "sonya" from
"You are Dedicatus" to "You are Sonya" (or the correct persona name) or, if this
is intentional, add a clarifying comment above the case explaining that Sonya
intentionally uses the alternate persona "Dedicatus" so reviewers and future
maintainers understand the mismatch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a36e7724-2fa9-45df-9278-3f74dbd1451f

📥 Commits

Reviewing files that changed from the base of the PR and between 1cf9dbd and e5b5a40.

📒 Files selected for processing (9)
  • NeuraLink/AI/CharacterPersona.swift
  • NeuraLink/AI/GGUF/GGUFLlamaEngine+Generate.swift
  • NeuraLink/AI/GGUF/GGUFLlamaEngine.swift
  • NeuraLink/AI/GGUF/GGUFQwenEngine+Generate.swift
  • NeuraLink/AI/GGUF/GGUFQwenEngine.swift
  • NeuraLink/AI/GGUF/LlamaBridge.swift
  • NeuraLink/AI/LocalLLMManager+Delegates.swift
  • NeuraLink/AI/LocalLLMManager+TTS.swift
  • NeuraLink/AI/LocalLLMManager.swift
💤 Files with no reviewable changes (1)
  • NeuraLink/AI/CharacterPersona.swift

Comment thread NeuraLink/AI/GGUF/GGUFQwenEngine.swift
Comment thread NeuraLink/AI/LocalLLMManager+TTS.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
NeuraLink/AI/LocalLLMManager.swift (1)

460-465: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the full-response log.

Printing every assistant response verbatim leaks conversation content into device logs. If you need this for debugging, gate it behind #if DEBUG or redact it.

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

In `@NeuraLink/AI/LocalLLMManager.swift` around lines 460 - 465, In
localLLM(didFinishGeneration fullText: String) remove or stop the unconditional
print of the full assistant response to avoid leaking conversation content;
either delete the print("[LocalLLM] Full response: \(fullText)") call or wrap it
behind a conditional compilation flag like `#if` DEBUG (or redact the text) so
fullText is only logged in debug builds or when explicitly allowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/npu.md`:
- Line 71: Update the broken relative markdown link that points to the TTS
extension: replace the current target "./NeuraLink/AI/LocalLLMManager+TTS.swift"
with a one-level-up relative path "../NeuraLink/AI/LocalLLMManager+TTS.swift" so
the reference to LocalLLMManager+TTS.swift resolves correctly from the docs
folder.

In `@NeuraLink/AI/LocalLLMManager.swift`:
- Around line 252-269: Introduce a session token to invalidate in-flight TTS
callbacks: add a property like ttsSessionToken (UUID) and assign a fresh UUID
inside restart() (alongside resetting pendingTTSBuffers/ttsGenerationDone); when
starting any TTS generation capture the current ttsSessionToken and have all
async paths (the .dataConsumed callbacks, the async increment path, and
didFinishGeneration) check that the captured token still equals the manager's
ttsSessionToken before mutating pendingTTSBuffers, ttsGenerationDone, or other
session state; additionally ensure those mutations run on the same serialized
execution context (MainActor or a dedicated serial queue) to avoid races.

In `@NeuraLink/UI/AI/PersonaSettingsView.swift`:
- Around line 108-130: The display name edits are not persisted in local LLM
mode; update the Save Changes and Reset to Default branches so local mode also
persists or restores the persona name (e.g., call a new or existing
LocalLLMPromptStore methods like saveName(for: modelID, name: persona.name) and
effectiveName(for: modelID) when isLocalLLMMode) OR make the name field
read-only when isLocalLLMMode by disabling the TextField bound to persona.name;
modify the Save Changes block (currently calling
LocalLLMPromptStore.shared.savePrompt(...)) and the Reset to Default block
(currently calling LocalLLMPromptStore.shared.resetPrompt(...) and reading
effectivePrompt) to handle persona.name consistently.

---

Outside diff comments:
In `@NeuraLink/AI/LocalLLMManager.swift`:
- Around line 460-465: In localLLM(didFinishGeneration fullText: String) remove
or stop the unconditional print of the full assistant response to avoid leaking
conversation content; either delete the print("[LocalLLM] Full response:
\(fullText)") call or wrap it behind a conditional compilation flag like `#if`
DEBUG (or redact the text) so fullText is only logged in debug builds or when
explicitly allowed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dffb1b00-eef2-451e-a0d4-dca441faab32

📥 Commits

Reviewing files that changed from the base of the PR and between e5b5a40 and 4ef7ec1.

📒 Files selected for processing (8)
  • NeuraLink/AI/LocalLLMManager+TTS.swift
  • NeuraLink/AI/LocalLLMManager.swift
  • NeuraLink/AI/LocalLLMPromptStore.swift
  • NeuraLink/UI/AI/AISettingsView.swift
  • NeuraLink/UI/AI/PersonaSettingsView.swift
  • README.md
  • docs/npu.md
  • docs/npu_migration.md
💤 Files with no reviewable changes (1)
  • docs/npu_migration.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • NeuraLink/AI/LocalLLMManager+TTS.swift

Comment thread docs/npu.md Outdated
Comment thread NeuraLink/AI/LocalLLMManager.swift
Comment thread NeuraLink/UI/AI/PersonaSettingsView.swift
@kevinliddel
kevinliddel merged commit 69fbaf5 into main Apr 30, 2026
1 check passed
@kevinliddel
kevinliddel deleted the refacto/local-llm branch April 30, 2026 19:38
kevinliddel added a commit that referenced this pull request May 27, 2026
feat: Implement concurrency guards for GGUF engines, optimize context windows, and add spoken-word system prompts to prevent self-triggering VAD feedback.
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.

1 participant