Repository navigation
Conversation
Able to get around 50% performance gain using IOBindings and just generally reducing allocations. BEFORE Average: 553ms Min: 486ms Max: 775ms Median: 529ms AFTER Average: 358ms Min: 303ms Max: 455ms Median: 349ms
…only taking into account first LLM token Now the LLM tracking stops when the first applicable sentences to start TTS (In audio mode) has been generated.
…ch to CPU execution provider
…TS -> AUDIO timings now.
…borator delegation - Extract event processing loops to ConversationSession.EventProcessing.cs - Extract state action methods to ConversationSession.StateActions.cs - Rewrite core to slim fields/ctor/lifecycle only - Wire up TurnMetricsTracker and TurnPipelineCoordinator collaborators - Update ConfigureStateMachine to use _pipeline.CurrentTurnId - Fix bug: ProcessOutputEventsAsync now completes _outputChannelWriter (was _inputChannelWriter)
…/Synthesis directory Extract 5 focused collaborators from PhonemizerG2P (840 → 68 lines): - TextPreprocessor: markdown-link parsing, feature extraction, subtokenization - TokenAligner: bidirectional alignment and feature application - TokenRestructurer: token conversion, folding, retokenization, merging - PhonemeResolver: phoneme lookup, subtoken resolution, stress processing - RetokenizedItem: type-safe discriminated union replacing List<object> Reorganize flat Synthesis/ directory (52 files) into 4 subdirectories: - Engine/: TTS orchestration and caching - Audio/: Kokoro synthesis, voice data, audio filters - TextProcessing/: normalization and sentence segmentation - Phonemization/: G2P pipeline, lexicon, POS tagger, fallback Add test project with 44 characterization tests to verify behavior preservation.
…nversationSession files
…ory layout Bundle CUDA/cuDNN runtime DLLs with the application so users no longer need to manually install CUDA Toolkit and copy cuDNN files. Native DLLs are organized into a native/ subfolder via a post-publish MSBuild target, with SetDllDirectory used at startup to ensure Windows finds them. - Add CUDA/cuDNN DLL bundling via Resources/native/cuda/ (gitignored) - Enable single-file self-contained publish for Release config - Add post-publish target to move loose native DLLs into native/ - Use kernel32 SetDllDirectory to add native/ to DLL search path - Place custom native libs (Live2D, Rust, etc.) directly into native/
- Add StartupValidator that runs before DI container build, checking GPU, CUDA, required models, espeak-ng, LLM config, Live2D, and prompt files with actionable error messages via Serilog - Initialize Serilog and suppress ONNX Runtime warnings before any sessions are created (move OrtEnv log level to Program.cs) - Change KokoroAudioSynthesizer ONNX session log level from WARNING to ERROR to suppress noisy Memcpy node warnings - Add SelfContained and RuntimeIdentifier to Release publish config - Apply CSharpier formatting to touched files
- StartupValidator now hits /models endpoint to verify reachability - Checks configured model exists and lists available models on mismatch - Validates both Text and Vision endpoints with API key auth - Console waits for keypress on validation failure so errors are readable - Fix VisionEndpoint passed as model name in AddLLM (copy-paste bug) - Guard against empty VisionEndpoint in DI registration
* feat: add SentenceId property to AudioSegment for streaming chunk grouping * feat: stamp SentenceId on AudioSegments in TtsOrchestrator * fix: propagate SentenceId through AudioFilterPipeline * test: add sentence-scoped lip-sync tracking tests * test: add failing TDD tests for sentence-scoped lip-sync tracking * fix: sentence-scoped phoneme tracking for streaming TTS lip-sync * fix: restore _isStarted guards and make testable via internal * style: format with CSharpier * test: add integration tests for Qwen3 streaming CTC + phoneme enrichment + lip-sync pipeline * feat: cliff detection in ConvertCtcToTokens to truncate unspoken words * refactor: remove proportional estimate, use CTC-only timing path * fix: only replace lip-sync phonemes when CTC covers more time * test: add time-coverage guard tests for lip-sync * style: format with CSharpier * chore: update NuGet packages * refactor: replace ModelType enum with type-safe nested ModelId system * refactor: introduce TTS engine abstraction layer * refactor: migrate Kokoro to ISentenceSynthesizer * feat: add Qwen3-TTS sentence synthesizer and GGUF engine * refactor: enhance RVC with RMVPE pitch estimation and audio preprocessing * refactor: extract IForcedAligner interface and enhance subtitle system * feat: update UI for new Qwen3 TTS + updated imGUI * refactor: wire up multi-engine DI, text processing, and audio filters * feat: update app config and DLL loading * test: clean up Qwen3 tts test * chore: delete stale refactor planning docs * test: add subtitle tests * feat: add top-P (nucleus sampling) to Qwen3 sampler The TopP config was plumbed through options and UI but never applied during sampling. Add ApplyTopP after top-K filtering. Guarded by TopP < 1.0 so default behavior (TopP=1.0) is unchanged. * refactor: internalize Kokoro audio synthesizer and clarify type names Delete IAudioSynthesizer interface (single implementation, Kokoro-specific). KokoroAudioSynthesizer is now internal and owned by KokoroSentenceSynthesizer rather than registered in DI, matching how Qwen3 owns its engine internally. Rename AudioData → KokoroSynthesisResult and VoiceData → KokoroVoiceEmbedding to eliminate generic naming for Kokoro-specific types. * fix: extract KokoroTokenConverter — fix buffer corruption and off-by-one EOS bug * fix: extract AudioSilenceTrimmer to Audio namespace — use MathF.Abs in hot loop * fix: correct ArrayPool lifetime, dispose OrtValues, thread-safe session locking - Move ArrayPool.Return after ONNX inference completes so the token buffer isn't recycled while OrtValue still references it - Add using declarations for inputIdsOrtValue and styleOrtValue to prevent resource leaks on both happy and exception paths - Replace lock(_session) with a dedicated _sessionLock object to avoid locking on a mutable nullable reference - Add _disposed guard with ObjectDisposedException checks and idempotent DisposeAsync - Replace _session! null-forgiving with explicit ObjectDisposedException * perf: cache speed shape, pin backing array, readonly phonemeMap * chore: add .worktrees to gitignore * feat: extract ArgMax as SpanMathExtensions utility Add SpanMathExtensions.ArgMax extension method on ReadOnlySpan<float>, consolidating a pattern duplicated 5+ times across the codebase. Includes full xUnit test coverage for edge cases (single element, duplicates, negatives, NegativeInfinity). * refactor: add Reset() to AudioFilterPipeline, narrow IList to List * feat: add LogSoftmaxInPlace to SpanMathExtensions Adds a numerically stable log-softmax implementation (max-subtraction trick) to the shared SpanMathExtensions utility, with five covering tests. Extracts the pattern duplicated in CtcForcedAligner.cs and Qwen3TtsGgufEngine.cs. * feat: extract StringDistanceExtensions utility Adds Levenshtein distance (O(m) space, stackalloc for short strings), normalized similarity, and KeepOnlyLetters as shared utilities extracted from CtcForcedAligner, with full unit test coverage. * feat: add TextExtensions.TrimPunctuation * feat: add AlignmentResult.Empty to avoid needless pool rental * feat: add IncrementalSentenceAccumulator with cheap punctuation pre-check * feat: add TtsEventEmitter with lifecycle event protocol * refactor: replace private duplicates with shared SpanMathExtensions and TextExtensions * feat: add SentenceProcessor with reusable state and GeneratedRegex * refactor: fix CtcForcedAligner allocations, use shared extensions, modernize C# * fix: synchronize subtitle Words list access to prevent concurrent modification crash UpdateSegment on the background command-loop thread mutated SubtitleLine.Words while the render thread iterated it in Render/PositionLines, causing InvalidOperationException. Expose RunLocked on SubtitleTimeline and use it to guard both the UpdateSegment call and the render iteration. Move PositionLines inside the existing lock in GetVisibleLinesAndPosition. * refactor: rewrite TtsOrchestrator as thin coordinator * refactor: update DI, remove dead ITextProcessor/TextProcessor/ProcessedText * fix: correct ArrayPool lifetime safety in CtcForcedAligner * refactor: add TrimPunctuationSpan overload, DRY TrimPunctuation, replace SentenceProcessor duplicate * style: apply CSharpier formatting * feat: route llama.cpp logs through Serilog, suppress below error * fix: make SentenceProcessor and TtsOrchestrator constructor public for DI resolution internal constructor prevented Microsoft.Extensions.DependencyInjection from resolving TtsOrchestrator — DI requires public constructors. * chore: update package references for Microsoft.Extensions.Options and Microsoft.SemanticKernel to latest versions * refactor: update SentenceProcessorTests for PhonemeResult Token[] constructor * refactor: remove ProvidesPhonemes capability flag, phonemizer is now always core * refactor: add PhonemeResult parameter to ISynthesisSession.SynthesizeAsync * refactor: move phonemizer to SentenceProcessor core, remove from Kokoro * test: update SentenceProcessor tests for new pipeline (always phonemize, no capabilities) * refactor: Qwen3 uses phonemizer tokens with ArraySegment slicing, drops SplitIntoWords/BuildSliceTokens * refactor: EmotionProcessor uses character-offset resolution, removes marker system * refactor: change PhonemeResult.Tokens from IReadOnlyList<Token> to Token[] for zero-alloc slicing * fix: map CTC word indices to phonemizer token ranges for correct Qwen3 lip-sync timing
* feat: add LipSyncTimeline, ILipSyncProcessor, and provider data structures
* feat: add BVLS blendshape solver for Audio2Face
* feat: add Audio2Face ONNX inference wrapper and model IDs
* feat: extract VBridger phoneme logic into VBridgerLipSyncProcessor
* feat: integrate ILipSyncProcessor into SentenceProcessor pipeline
* feat: add LipSyncAnimationService as unified Live2D consumer
* feat: add Audio2Face streaming lip sync processor with sync compensation
- Audio2FaceLipSyncProcessor: sentence-level audio buffer with sliding
window inference, silence/carryover pre-fill for GRU warmup, deferred
full-audio re-runs to preserve temporal continuity across right-padded
windows, frame timestamp offset (1.55s) for ~200ms sync lag
- LipSyncAnimationService: drives Live2D params from pre-computed
timeline with chunk-relative playback, smooth-to-neutral on idle
- ARKitToLive2DMapper, ResponseCurves, ParamSmoother: VBridger
AdvancedARKit_V3.0 mapping with per-parameter EMA smoothing
- BlendshapeData: NPZ loader for skin basis, frontal mask, model data
- AnimatorSkinConfig: per-identity vertex composition parameters
- Audio2FaceInference: noise seed 0 (matching Python reference)
- LipSyncTimeline: binary-search frame interpolation
- ILipSyncProcessor: BeginSentence + Process + Reset interface
- SentenceProcessor: enriches TTS chunks with lip sync timelines
* feat: extract IBlendshapeSolver with PGD and BVLS implementations
- IBlendshapeSolver: strategy interface for solver hot-swap via config
- PgdBlendshapeSolver: renamed from BlendshapeSolver, eigenvalue-based
step size (power iteration) replacing Gershgorin bound
- BvlsBlendshapeSolver: active-set bounded variable least squares
matching scipy lsq_linear(method="bvls"), Cholesky sub-system solve,
~3x more accurate blendshape weights than PGD
- SolverType config ("BVLS" default, "PGD" for lower latency)
* test: add Audio2Face solver, mapper, smoother, and response curve tests
- BlendshapeSolverTests: PGD + BVLS identity recovery, clamping,
zero-delta, and temporal smoothing tests
- ARKitToLive2DMapperTests, ParamSmootherTests, ResponseCurvesTests,
BlendshapeDataTests
* fix: ReadFloat32Into writes into destination span instead of discarded copy
* fix: dispose SessionOptions after InferenceSession creation, null cached noise on dispose
* perf: pre-allocate inference tensors and output buffers, eliminate ~8 MB per window
* perf: pre-allocate frame processing buffers, reuse static zero-padding
* refactor: extract SolverMath, pre-allocate all solver working buffers
* refactor: merge identity switches, downgrade lip-sync null log to debug
* fix: always register both lip-sync processors for hot-reload support
* fix: add thread safety to LipSyncAnimationService, const strings, remove dead handler
* refactor: use MathF.Clamp consistently, extract shared Hermite helper
* fix: replace silence-influenced lipsync frames during deferred re-run
Right-padded inference windows commit frames where the model sees
zeros (silence) instead of future audio, causing periodic dips toward
neutral mouth pose. The deferred re-run previously only advanced
internal state—now it also overwrites those frames in-place with
correct ones computed from the actual audio, eliminating lip jumps
at chunk boundaries.
* perf: enable CUDA execution provider for CTC forced aligner
* perf: eliminate redundant ConcatAudio allocations in streaming timing loop
Replace List<float[]> + ConcatAudio (full-buffer copy every alignment
interval) with a flat List<float> and CollectionsMarshal.AsSpan for
zero-allocation access. Removes 4-5 full-buffer copies per sentence.
Delete the now-dead ConcatAudio helper method.
* perf: migrate Audio2Face to IoBinding API with Span-based extraction
* fix: ignore stale pipeline events in Idle, ProcessingInput, and WaitingForLlm states
After barge-in interruption, the previous turn's TTS/LLM pipeline can
still have events queued in the output channel since cancellation is
cooperative. Stale triggers like TtsStreamChunkReceived arriving in
states that don't handle them caused an unhandled trigger error that
killed the session. Added ignore directives matching the pattern
already established by the Interrupted state.
* merge: integrate refactor/dry-cleanup into feature/audio2face-lipsync
Merge DRY utilities (OnnxSessionFactory, PooledArray, AnimationServiceBase,
WavFileHandler, AudioValidation) and reorganized Utils/ structure from
refactor/dry-cleanup. Update Audio2Face code to use new shared utilities.
Resolve conflicts in CtcForcedAligner (CUDA+factory) and NpyReader (zero-copy).
* fix: skip lip-sync chunks with shorter time coverage than existing data
Prevents partial CTC results from replacing higher-quality alignment
data already loaded for the same sentence.
…t-reload, health probes (#21) * feat(ui): add LayoutContext space-tracking stack * feat(ui): add WindowScope, RowScope, and Ui entry points * feat(ui): add SplitScope with draggable divider * feat(ui): rewrite control panel to use declarative layout system ControlPanelComponent.Render uses Ui.Window/Row/HSplit scopes. StatusBar, Navigation, ControlBar are now pure content renderers. * feat(ui): migrate Dashboard to layout system, add child flags to RowScope Dashboard uses Ui.Row for vertical section tiling — eliminates magic 120px height reservation. Transcript fills remaining space automatically. RowScope now accepts ImGuiChildFlags for bordered/scrollable regions. * fix(ui): fix Dashboard layout gaps and health card spacing Push zero ItemSpacing around Row tiling so the LayoutContext height math is exact — no hidden 8px gaps between children. Use explicit 8px cardGap constant for health cards instead of reading ItemSpacing (which is zero in the Row context). * fix(ui): RowScope owns zero-spacing, add EqualCols, simplify Dashboard RowScope now pushes zero ItemSpacing on the parent before BeginChild and pops after EndChild — rows always tile correctly regardless of parent context. No consumer needs to manage ItemSpacing manually. Added Ui.EqualCols(count, height, gap, childFlags) for evenly-spaced horizontal columns. Dashboard uses it for health cards — no manual width math or SameLine gap calculations. Dashboard.Render is now pure layout declarations: 3 Rows + 1 EqualCols. * fix(ui): add Ui.Rows for pre-resolved sizing, fix two-phase style push Ui.Rows(params Sz[]) pre-resolves all row sizes using Sz.Resolve so Fill rows correctly account for sibling Fixed rows. Previously Ui.Row(Sz.Fill()) greedily consumed all remaining height, leaving 0 for subsequent Fixed rows — causing overflow and scroll. RowScope and SplitChildScope now use two-phase style push: container style (padding, bg) before BeginChild, content style (item spacing) after BeginChild. Default style inherits parent's spacing instead of zeroing it. Style.IsDefault property added to detect zero-initialized structs. * fix(ui): prevent WindowPadding leaking between layout scopes Every scope now explicitly sets WP before BeginChild (zero for default style, style value for explicit) — never relying on inheritance. After BeginChild, WP is reset to zero so nested children start clean. This fixes: mini scrollbars on health cards (parent WP(16,16) leaked into 48px-tall cards leaving only 16px content), conversation overlap, and session section being pushed offscreen. Dashboard restored to use Ui.Rows + Ui.EqualCols with the fixed scopes. * fix(ui): move Conversation label above border, fix stats overflow Transcript: section header renders above the bordered area. Messages are in an inner bordered child that fills the remaining row space. Stats: increased StatsSectionHeight from 70 to 100 to fit the section header (~36px) + table (~40px) without triggering a scrollbar. * feat(ui): add Ui.FillChild scope for remaining-space children FillChildScope creates a child window filling GetContentRegionAvail() with explicit padding and WP isolation. Useful after a header to fill the rest of a row with bordered/scrollable content. Dashboard transcript uses Ui.FillChild instead of manual push/pop. * feat(ui): update color palette to 4-level surface hierarchy and increase spacing * feat(ui): increase structural sizes — sidebar, bars, divider, column gaps * feat(ui): restyle section headers, nav items, and dashboard cards * fix(ui): rename all old Theme constant references to new surface hierarchy * feat(ui): darken status bar background and increase height * feat(ui): add optional glow halo to StatusDot helper * feat(ui): restyle status bar with pulsing glow, accent border, and typography hierarchy * refactor(ui): remove legacy GUI system, add grid layout, and consolidate into ControlPanel Delete 26 unused files from UI/GUI/ (old config editors, theme manager, notification system, state management). Move ImGuiController into ControlPanel. Add GridScope for auto-height column grids and padding support to EqualColsScope. Refactor Avatar, Streaming, and RouletteWheelPanel to use grid layout for dimension inputs. * feat(ui): restyle status bar and fix layout padding system - Status bar: Surface2 background, 46px height, pulsing glow dot, accent bottom border, explicit TextPrimary/TextTertiary typography - Fix layout padding: WindowPadding not applied by ImGui to child windows in Hexa.NET binding; use Indent/Dummy for left/top padding - SplitChildScope: dual-child mode (right panel, true 4-side padding) vs indent mode (sidebar, allows draw-list access to window edge) - RowGroupScope: add gap parameter for spacing between rows - FillChildScope: manual padding via Indent/Dummy - Dashboard: 12px row gaps, conversation label spacing - Navigation: accent bar drawn at sidebar outer edge * feat(ui): wire up animation system — toggle switches, panel transitions Propagate deltaTime through the entire ControlPanel render pipeline so animation helpers (AnimatedFloat, OneShotAnimation, Easing) actually receive frame timing. Replace all eight ImGui.Checkbox calls with the animated ToggleSwitch helper and add a fade+slide-up transition when switching between navigation panels. * fix(ui): enforce minimum window size via GLFW and fix ImGui resize desync Use glfwSetWindowSizeLimits to prevent the OS from allowing the window below the configured minimum (800x600 default), eliminating visual artifacts during drag. Remove ImGuiController's cached window size in favour of reading _view.Size each frame so ImGui always sees the post-clamp dimensions. Add defensive clamping to SplitScope so the sidebar/content split stays valid at boundary sizes. * feat(ui): add SineOscillator and EaseOutBack to animation system * feat(audio): add IAudioAmplitudeProvider for SRP-compliant amplitude exposure Extracts audio amplitude computation from the UI domain into the audio domain layer. UI reads a single float from IAudioAmplitudeProvider.CurrentAmplitude instead of handling raw PCM samples directly. * feat(ui): add PersonaStateProvider to bridge conversation state for UI effects * feat(ui): add ParticleSystem for ambient floating particle effects * feat(ui): add AmbientRenderer with gradient glows, particles, and gradient dividers * feat(ui): add breathing gradient, pulsing border, and hover warmth to sidebar nav * feat(ui): add springy toggle, slider linger, button press feedback - ToggleSwitch: EaseOutBack overshoot + ripple ring while settling - SliderGlow: lingers ~300ms after interaction via exponential decay (dt param, backward-compatible default) - PrimaryButtonWithFeedback: non-linear press darkening + warm glow pulse on click - LerpColor: promoted from private to public * feat(ui): add staggered transitions, ambient wiring, save pop animation * feat(ui): add audio-reactive border, warmth overlay, and dot flare to status bar * feat(ui): add sound event hooks and wire all companion space services in DI Registers AudioAmplitudeProvider, PersonaStateProvider, AmbientRenderer, and IUiSoundEmitter (NoOpUiSoundEmitter) in the DI container so the ControlPanelComponent can resolve all dependencies at runtime. * fix(ui): polish nav hover, fix ambient visibility, redesign save toast, bump status warmth * fix(ui): center save toast in status bar and soften ambient rendering * feat(ui): expose outer drawlist on SplitChildScope; render ambient edge-to-edge Capture outer child draw list, pos and size in SplitChildScope before the inner padded child opens, exposing OuterDrawList/OuterPos/OuterSize properties. ControlPanelComponent now passes these to AmbientRenderer so glows fill the full content area instead of respecting inner padding. * refactor(ui): use pre-rendered gradient texture for ambient glows with smooth state transitions Replace ~40 AddCircleFilled rings per glow (visible banding) with a single 256x256 RGBA8 radial gradient texture uploaded once at Initialize. Each frame draws two textured quads via ImGui.AddImage, letting GPU bilinear filtering produce smooth falloff. AnimatedColor (exponential decay on Vector4) added to Animation.cs for seamless persona-state color transitions. RadialGradientTexture owns its GL handle and disposes cleanly through ControlPanelComponent.Dispose. * feat(ui): companion space iteration — presence orb, voice envelope, dithered glows Major polish pass on the companion-space ambient layer: - HAL-style PresenceOrb: replaces the basic StatusDot in the status bar. Three layers (core/inner/outer) react to amplitude with staggered smoothing so energy ripples outward instead of scaling in lockstep. Spawns randomized expanding rings on syllable peaks (variable cooldown, spawn delay, duration, radius, thickness) for organic sound-wave look. Soft-knee compression curve so quiet sounds get amplified more, peaks taper toward (but rarely hit) the ceiling. - AmplitudeEnvelope: pre-computes per-window RMS across a chunk's audio data; AudioAmplitudeProvider now subscribes to PlaybackProgress and samples the envelope, so the orb sees real per-syllable amplitude instead of a single per-sentence average. - BayerDither + ImGuiShaderSources: extracts inline ImGui shader strings into reusable composable sources; injects 4×4 Bayer ordered dither into the fragment shader (~±1/255 amplitude) to suppress visible banding in low-alpha gradients. - AmbientRenderer rewritten on top of a shared RadialGradientTexture: drifting corner glows drawn via AddImage with animated tint, edge-to- edge in the content area (SplitChildScope now exposes outer drawlist/ bounds). Particle simulation moved into RenderBackground where panel bounds are known. - Save indicator → toast card: rounded bg + accent border + dot, slides in from right, vertically centered in the status bar; rapid successive saves pulse the existing toast instead of restarting. - Smaller polish: nav hover → subtle rect tint (was a giant circle), status bar warmth alpha bumped, springy toggle ripples, slider glow linger, smooth state-to-state color transitions on glows, finer ImGui circle tessellation for small dots. * feat(ui): voice panel redesign — mode-first UX, audition pipeline, layout system extensions Redesign the Voice panel around an honest Clear vs Expressive mode selector that surfaces the real trade-offs between Kokoro (phoneme-accurate, light GPU) and Qwen3 (context-aware emotion, heavy GPU). Voice audition lets users hear any voice via a dedicated PortAudio stream without disturbing the live conversation pipeline. Layout system: - Ui.Card (auto-height via AutoResizeY, optional fixed height/width) - Ui.Chip (pill toggle, interactive or display-only) - Ui.CollapsibleSection (animated expand/collapse, inline hint text) - Ui.LabeledSlider (perceptual labels, float + int overloads) - Ui.IconButton, Ui.PreviewButton (draw-list shapes, no font dependency) - Ui.WrapGrid (auto-column tile grid) - UniformHeightTracker (cross-frame max-height enforcement) - SettingLabel + SettingEndRow (consistent row spacing + vertical alignment) Voice panel sections: - VoiceModeSelector — two trade-off cards (Clear / Expressive) - VoiceCard — selected voice summary with preview + gender/cloned chips - VoiceGallery — horizontal scroll strip with filter chips + uniform tiles - CloneLayerSection — RVC with mode-sensitive hint + animated collapse - AdvancedSection — all engine knobs including new Qwen3 sampling params Audition pipeline: - IVoiceAuditionService with per-request ID tracking + StopAsync - OneShotAudioPlayer (callback-based PortAudio, independent stream) - ISentenceSynthesizer.CreateSession(voiceName) for instant preview - RvcOverride for per-call voice/pitch override Infrastructure fixes: - RVCFilter thread-safety: ReaderWriterLockSlim around model swap (P4) - RVCFilter async-void crash: try/catch in OnOptionsChanged - ITtsEngineProvider.Get(engineId) for per-engine lookup - PhonemeResult.Empty static field - EqualColsScope uses GetContentRegionAvail (scrollbar-aware) - Dashboard health cards migrated to Ui.Card * feat(dashboard): add SessionStatsCollector with MeterListener-backed stats accumulation * refactor(dashboard): extract SystemHealthSection * feat(dashboard): add SessionStatsSection wired to SessionStatsCollector * refactor(dashboard): extract TranscriptSection * refactor(dashboard): replace monolithic Dashboard with thin orchestrator Extract the 220-line Dashboard into a slim orchestrator that delegates to SystemHealthSection, TranscriptSection, and SessionStatsSection. * refactor(dashboard): wire new Dashboard sections and collector into DI Register SessionStatsCollector, SystemHealthSection, TranscriptSection, and SessionStatsSection in the DI container. Update imports in ServiceCollectionExtensions and ControlPanelComponent to resolve the Dashboard class via a using alias (avoids class/namespace ambiguity). Includes incidental CSharpier formatting fixes. * feat(metrics): add GetTurnElapsedMs() to TurnMetricsTracker * fix(metrics): use end-to-end turn elapsed for first_audio_latency histogram * fix(ui): status bar showing Listening after session ends Expose ConversationState via IConversationSession.CurrentState so the StatusBar and PersonaStateProvider check the actual state machine state instead of inferring from PendingTurn alone. Sessions in Ended, Error, or Initial states now correctly display as No Session. Also refactor Dashboard to use three-row layout (health/transcript/stats) and simplify TranscriptSection to fill available space. * refactor(ui): redesign Personality panel with section-based architecture Replace monolithic Personality.cs with thin orchestrator + three focused sections (PromptSource, CurrentVibe, Topics). Add explicit UseCustomPrompt bool to ConversationContextOptions for clear file/custom prompt switching. Topics now use chip-based input with add/remove instead of comma-separated text. * feat(ui): show hand cursor on hover for all clickable elements Add HandCursorOnHover() helper to ImGuiHelpers and apply it across all interactive widgets: buttons, combos, toggles, chips, preview buttons, collapsible headers, radio buttons, selectable nav items, voice tiles, mode selector cards, and removable topic chips. * fix(dashboard): exclude first turn warmup from avg latency calculation * feat(ui): add TitleBarBg color and TitleBar style for custom title bar * feat(ui): add Win32WindowHelper for borderless window chrome * feat(ui): make window borderless and expose Win32WindowHelper Switch WindowBorder from Resizable to Hidden for custom chrome support. Create Win32WindowHelper in OnLoad (after HWND is available) and dispose it in OnClose. Expose it via public Win32Helper property for TitleBar. * refactor(di): register WindowManager as DI singleton Extract WindowManager construction out of AvatarApp into the DI container via AddUI, so downstream components like TitleBar can take it as a constructor dependency. AvatarApp now receives it injected. * feat(ui): add TitleBar layout component with window controls * feat(ui): wire TitleBar into control panel layout and DI * style: format custom title bar files with CSharpier * fix(ui): fix title bar HWND, draw list icons, rounding, and centering * fix(ui): enable edge resize, double-click maximize, and right-click system menu * fix(ui): handle double-click maximize and enforce min window size during resize * fix(ui): explicitly set max track size to allow window resize beyond min size * fix(ui): call original WndProc before overriding MINMAXINFO constraints * fix(ui): use unsafe pointer for MINMAXINFO to avoid marshaling issues * fix(ui): write MINMAXINFO via raw pointer offsets to fix resize constraints * fix(ui): use WM_SIZING for min size enforcement instead of MINMAXINFO * refactor(ui): remove unsafe from Win32WindowHelper, use Marshal.Read/WriteInt32 * feat(ui): add rounded window corners and animated state-reactive frame glow * fix(ui): clamp the dragged edge during resize to prevent window moving at min size * style(ui): reduce window frame glow outer thickness to 2px * feat(ui): add floating per-pixel-alpha overlay for avatar and subtitles * feat(utils): add FloatRingBuffer circular buffer primitive * feat(audio): add SamplesAvailable event to IMicrophone for live amplitude taps * feat(audio): add MicrophoneAmplitudeProvider with smoothed RMS + history ring * feat(asr): expose Silero VAD per-batch probability via IVadDetector.ProbabilityObserved * feat(asr): add VadProbabilityProvider with latest-value + history ring * feat(di): register IMicrophoneAmplitudeProvider and IVadProbabilityProvider singletons * perf(audio): reuse AmplitudeEnvelope buffer and sample event-provided elapsed time - AudioAmplitudeProvider.OnPlaybackProgress reads e.CurrentPlaybackTime instead of computing UtcNow - _chunkStartUtc (correctness + ~100 ns/tick x 240/sec saved) - AmplitudeEnvelope converts its static factory into an instance with RebuildFrom that reuses the internal buffer via Array.Resize - drops one heap allocation per TTS chunk to ~1 for the process lifetime - Existing tests pass unchanged (From() retained as a convenience wrapper) * perf(audio): direct multi-cast invoke for high-frequency PlaybackProgress Eliminates per-raise Delegate[] allocation from GetInvocationList() at the ~240 Hz playback-progress tick rate. Chunk started/ended keep per-handler isolation. * feat(ui): scaffold Listening panel folder with stub sections + widget * refactor(ui): remove old Listening panel monolith Resolves namespace collision between the old Panels.Listening class and the new Panels.Listening.* namespace introduced by the scaffold in the preceding commit. Clearing DI refs + constructor param here; the new ListeningPanel + sections will be wired in a follow-up. * feat(ui): MicrophoneDeviceSection — device picker + Test Microphone round-trip * feat(ui): add LiveMeterWidget with amplitude bar + VAD probability sparkline * feat(ui): SpeechDetectionSection with live meter + three VAD sliders * feat(ui): RecognitionSection with quality chips + custom vocabulary editor * feat(ui): InterruptionSection two-axis composer + pure Compose/Decompose helpers * feat(ui): wire ListeningPanel + sections into DI and ControlPanelComponent * fix(build): resolve clean-checkout build for the Listening rework Three fixes that should have landed with the Listening panel redesign but were masked by concurrent local refactoring on the branch: - MicrophoneInputPortAudioSource (dead code, still implements IMicrophone) now implements the SamplesAvailable event added to IMicrophone in 3ee3b6d. Without this, PersonaEngine.Lib fails to build on a clean checkout. - Remove accidental "using PersonaEngine.Lib.UI.ControlPanel.Visuals" and "using PersonaEngine.Lib.UI.Host" added to ServiceCollectionExtensions.cs and ControlPanelComponent.cs during DI wiring. Those namespaces do not exist in the committed tree (they're part of unrelated WIP overlay refactoring). The types resolve via the existing "PersonaEngine.Lib.UI" and "PersonaEngine.Lib.UI.ControlPanel" usings at HEAD. * feat(conversation): add IConversationInputGate with hold-count scope composition * feat(conversation): expose Pause/Resume on session + gate input loop * feat(conversation): add PauseAll/ResumeAll on orchestrator * feat(ui): rewrite overlay on D3D11 + DirectComposition, reorganize UI folders The floating overlay now renders via a D3D11 swap chain composed through DirectComposition instead of the old OpenGL + glReadPixels + UpdateLayeredWindow path. The legacy chain spent 3-5 ms/frame on CPU readback and GDI blitting; the new path is pure GPU with sub-millisecond overlay-thread cost. A DXGI frame-latency waitable + 60 FPS stopwatch cap keeps GPU usage bounded so high-refresh displays don't pin the card at 240 Hz. Overlay rendering stack: * Raw Win32 HWND with WS_EX_NOREDIRECTIONBITMAP (required at CreateWindowEx time) hosts a composition-mode swap chain with premultiplied alpha. * SpoutD3D11Source opens the Spout sender's NT handle directly via ID3D11Device.OpenSharedResource - no GL interop, no SpoutDX P/Invoke. The shared texture is cached by handle; reopened only when the sender restarts or resizes. * Four focused SDF/rect pipelines compose the hover chrome: - ButtonPipeline: rounded-rect button (fill + accent border + dark halo) - IconPipeline: rect-batched drag glyph (cardinal-axis four-way arrow) - OutlinePipeline: rounded-rect window outline with interior dark halo - ArrowPipeline: SDF diagonal arrow (sdSegment + 2x sdTriangleIsosceles, rotated so axis-aligned arithmetic produces clean antialiased 45° geometry that pixel-art rectangles can't match) ChromeRenderer coordinates them and owns shared blend + rasterizer state. * ShaderCompiler and FullscreenQuad helpers deduplicate the formerly copy-pasted HLSL compile path and clip-space triangle-strip vertex buffer. Interaction: * Drag and resize buttons cluster in the top-right. Resize pivots on the bottom-left corner (NE<->SW diagonal) with matching IDC_SIZENESW cursor. * SetCapture/ReleaseCapture keeps the gesture live when the cursor leaves the overlay bounds; WM_CAPTURECHANGED dispatches a synthetic LeftButtonUp so state never stays stuck if another app steals capture. * Aspect ratio is always locked; LockAspect and ChromeFadeSeconds knobs removed from OverlayConfiguration and the control panel. * OverlayHost now closes the running overlay window (via thread-safe PostMessage WM_CLOSE) when Enabled flips off in config, instead of only setting the stop flag. UI folder reorganization: * UI/Native/Win32.cs - shared P/Invoke + POINT/RECT/MSG/WNDCLASSEX, consumed by both the overlay and host subsystems. Replaces duplicated DllImport declarations previously scattered across four files. * UI/Host/ - Win32WindowHelper and WindowManager moved out of UI/ root. * UI/ControlPanel/Visuals/ - PresenceOrb, ParticleSystem, AmbientRenderer, and WindowFrameGlow moved out of ControlPanel/ root. * UI/Overlay/Native/ and UI/Overlay/Rendering/ - internal overlay split. Misc cleanup: * Removed unreachable cleanup after early return in RouletteWheel.Dispose. * Fixed four pre-existing PersonaStateProvider test failures; the session CurrentState mock was returning the enum default (Initial), which the provider treats as not-running. * appsettings.json excluded from git (holds API keys and local model paths). All 329 tests pass. * feat(ui): add IActivatablePanel lifecycle and dispatch on nav transitions * feat(ui): Listening panel closes input gate while active for calibration * feat(ui): wire ControlBar pause button to orchestrator PauseAll/ResumeAll * feat(di): register IConversationInputGate singleton * perf(audio): sub-window RMS sampling for smooth mic amplitude meter NAudio's 100 ms default buffer gave only 10 Hz amplitude history updates — visibly staccato on 60+ FPS displays. Splitting each buffer into 16 sub-windows lifts the effective rate to ~160 Hz. History capacity raised to 128 entries (~0.8 s visible window) to compensate for faster advance. * feat(ui): redesign LiveMeterWidget — single-trace, draggable threshold, smooth scroll Replaces the dual-row meter with a single VAD-probability trace, above-threshold shading, a state banner ('Silent' / 'Picking up sound' / 'Listening to you'), a compact mic-alive amplitude pip, and a draggable sensitivity threshold line. Adds sub-slot scroll interpolation to eliminate the staccato feel that sub-window sampling alone could not fix — the trace scrolls smoothly between pushes at pixel resolution, using an EMA-estimated push interval so it adapts whether the source updates at 31 Hz (VAD) or 160 Hz (amplitude). * fix(ui): eliminate scroll jitter in LiveMeterWidget Three causes rolled into one fix: 1. EMA interval estimate oscillated on frame-quantized observations. The VAD pushes at 32 ms but the UI ticks at ~16 ms, so observedInterval alternated between 16/32/48 ms each push, causing the estimate — and thus scroll speed — to visibly wobble. Replaced with a fixed constant derived from Silero's known rate (512 samples / 16 kHz = 32 ms). 2. Offset was overwritten each frame with elapsed/interval, instantly discarding the "decrement by slotsAdvanced on push" bookkeeping. Result was a sawtooth reset at every push instead of a continuous phase advance. Advance offset continuously; only decrement on head change; let the safety clamp catch long pauses. 3. New data points popped into existence at the right edge at each push. Shifting the whole trace right by one step width makes fresh samples slide in from off-screen right over their first interval of life, then scroll past plotMax.X and exit naturally on the left. Added a PushClipRect/PopClipRect around the plot region so the off-screen portion is cleanly clipped. * feat(ui): overhaul Listening panel — smooth meter, UX polish LiveMeterWidget: rewrite from ring-buffer to time-based rendering. Sample VAD probability once per frame with wall-clock timestamps for smooth scrolling. Apply asymmetric envelope (5 ms attack / 250 ms release) so speech onset is sharp while decay flows like a VU meter. MicrophoneDeviceSection: replace ambiguous recording countdown with "Speak now" label + progress bar. Draw play-back triangle via draw list for consistent sizing. SpeechDetectionSection: add "Reset defaults" button (enabled only when values differ from defaults). Update ASR defaults to match appsettings.json (MinSpeech 150 ms, MinSilence 450 ms). RecognitionSection: add "Add" button to vocabulary input, matching the Topics section pattern. InterruptionSection: shorten "Allow while avatar is speaking" to "During responses" to prevent toggle overlap. Reorder so the toggle sits above the slider. ImGuiHelpers: add SubtleButton with enabled/disabled state. FillChildScope/Ui: add windowFlags parameter; ListeningPanel uses AlwaysVerticalScrollbar for stable layout width. * refactor(tts): extract LlamaTtsContext from LlamaTtsInference Per-session inference context owning its own LLamaContext pair and native batch. Preparation for concurrency fix. * refactor(tts): add LlamaTtsModel with bounded context pool Shared model weights + ConcurrentBag pool (max 2) with SemaphoreSlim gate. Enables concurrent TTS generation without context races. * refactor(tts): update Qwen3TtsGgufEngine to use LlamaTtsModel context pool Replace direct LlamaTtsInference usage with LlamaTtsModel/LlamaTtsContext rent/return pattern. A context is rented before TalkerPrefill, threaded through GenerateCodeFrames, and returned in the producer task's finally block. On error the context is marked corrupted so the pool disposes it instead of reusing stale CUDA state. * refactor(tts): remove LlamaTtsInference, replaced by LlamaTtsModel + LlamaTtsContext * fix(tts): prevent context leak if TalkerPrefill throws before producer task Wrap TalkerPrefill in try/catch that returns the rented context as corrupted on failure. Without this, a prefill exception would permanently consume a pool slot since the producer task's finally never runs. * refactor(config): convert Live2D/LipSync options to records * feat(ui): add AvatarPanel thin orchestrator * feat(ui): add ModelSection with scanned character picker * fix(ui): handle ModelSection stale (not found) entry + enforce ordinal compare * feat(ui): add LipSyncSection with Style/Quality chips and GPU toggle * refactor(ui): wire AvatarPanel into DI and remove old Avatar * style(ui): csharpier format Avatar panel redesign * refactor(overlay): move resize handle to bottom-right corner The handle now sits in the grow-direction corner, so the cursor stays on it as the window expands toward the SE. Resize gesture pins the top-left (natural anchor) instead of the bottom-left; cursor swaps to IDC_SIZENWSE and the SDF arrow rotates to the NW-SE diagonal. * feat(ui): refine Avatar panel UX - Replace ResolutionPicker with stream-friendly ResolutionChips (orientation row + 4 size chips: 720p/1080p/1440p/4K and portrait /square siblings). Nearest-by-pixel-count when flipping orientation. - Remove Models Folder input from UI (ModelPath stays in config with its default, same treatment as Audio2Face.Identity). - Apply consistent SettingEndRow spacing to Style/Quality/UseGpu rows, drop the ad-hoc ImGui.Spacing() — matches the Listening panel. - Reorder LipSync rows: Style -> Quality -> Use GPU. Quality is the user-facing fidelity knob; Use GPU is the performance lever. - Fix Chip to strip "##id" suffix from displayed text, matching ImGui native widget convention. Fixes visible ids in the new chip labels. * feat(ui): swap LipSync Quality chip order — Fast before Accurate Match the Style row convention (Simple before Realistic) — cheaper/ faster option on the left, higher-fidelity option on the right. Tooltip text reordered to match. * refactor(config): convert SubtitleOptions to record * feat(ui): promote hex color helpers to Theme * feat(ui): extract ScannedNamePicker + ImGuiHelpers.ScannedCombo * feat(ui): add ImGuiHelpers.DrawOutlinedText for preview rendering * refactor(ui): ModelSection uses ScannedNamePicker helper * feat(subtitles): hot-reload config edits without restart * feat(ui): SubtitlesPanel orchestrator and 4 standard sections * feat(ui): PreviewSection with ImDrawList-simulated outline * refactor(ui): wire SubtitlesPanel into DI and remove old Subtitles * feat(ui): add OffscreenBuffer FBO primitive for reusable off-screen rendering * fix(ui): address OffscreenBuffer review issues (split read/draw FBO restore, disposed guard, Span overload) * feat(ui): add SubtitlePreviewRenderer for WYSIWYG TTF-based preview baking * fix(ui): align SubtitlePreviewRenderer color parsing with live renderer and guard Dispose * refactor(ui): rewrite Subtitles PreviewSection as WYSIWYG FBO consumer * feat(ui): wire Subtitle preview FBO renderer through ControlPanel lifecycle * chore(ui): remove unused DrawOutlinedText helper (dead after preview rewrite) * refactor(ui): render subtitle preview at fixed 55pt Configured FontSize doesn't translate meaningfully to the preview because the live OBS output is scaled and resized independently. Decouple the preview font from _opts.FontSize and bake at a constant 55pt, with a fixed canvas height. Caption continues to show the configured size so the user can still confirm their pick. * feat(ui): add Reset defaults button to Subtitle Placement section Mirrors the SpeechDetection header-action pattern: a right-aligned "Reset defaults" button that only enables when MaxVisibleLines, BottomMargin, SideMargin, InterSegmentSpacing, or AnimationDuration deviate from SubtitleOptions defaults. Resets only those five fields via a record `with` expression, leaving font, colors, stroke, and canvas dimensions untouched. * chore(config): align in-code defaults with current appsettings.json Bring the compiled-in defaults back in step with the shipping appsettings.json so fresh installs render identically to the reference config: - SubtitleOptions: FontSize 72→125, Color→#FFf8f6f7, Highlight→#FFc4251e, BottomMargin 50→250, SideMargin 100→30, InterSegmentSpacing 16→10, MaxVisibleLines 6→2, canvas 1920x1080→1080x1920, Font→DynaPuff.ttf. - RouletteWheelOptions: Font→DynaPuff.ttf. - AvatarApp: load Montserrat.ttf and Seguiemj.ttf (current Resources/Fonts filenames, previously referenced by stale names). * feat(overlay): add OverlayStatus enum for live lifecycle state Introduces Off/Starting/Active/Stopping/Failed — the real-world thread state, distinct from OverlayConfiguration.Enabled (the user's desired state). Lets the UI honestly reflect work in flight without lying about intent. * feat(overlay): add Stateless-based OverlayStateMachine with full transition coverage Wraps Stateless.StateMachine<OverlayStatus, OverlayTrigger> with callback- driven entry actions so the transition topology is testable without Win32, D3D11, or OverlayHost. Matches the existing pattern used by ConversationSession. Transition table covers Off/Starting/Active/Stopping/Failed with the reverse-intent-mid-Stopping reconcile: TurnOn during Stopping is ignored but DesiredEnabled flips; Off.OnEntry then re-fires TurnOn when the old thread exits. 14 unit tests cover basic transitions, reverse-intent sequences, external close, Failed/retry, and idempotent trigger handling. * feat(overlay): drive lifecycle directly from SetEnabled, decouple from config watcher Adds an instant-feeling public API to OverlayHost that replaces the old file-watcher round-trip path: - New public surface: SetEnabled(bool), DesiredEnabled, Status, LastError, StatusChanged event. UI binds to DesiredEnabled so the checkbox flips within one frame; Status reflects the real thread state via the Stateless machine. - SetEnabled fires a trigger immediately (no file write in the live path) and schedules persistence in a debounced background task (200 ms), so rapid toggles don't thrash appsettings.json. - Machine entry actions handle thread lifecycle: Starting.OnEntry spawns the overlay thread; Stopping.OnEntry posts WM_CLOSE; Off.OnEntry reconciles reverse-intent by re-firing TurnOn if desired flipped back on mid-Stopping. - RunOverlayThread fires Ready right before overlay.Run() (Starting → Active), Failed in the catch, and ThreadExited in the finally (with external-close detection flipping DesiredEnabled=false for UI honesty). - ApplyConfig's Enabled branch becomes idempotent — external edits to appsettings.json still sync into the machine, but the live UI path no longer rides on the file watcher. * feat(overlay): add ResetPosition on OverlayHost and live MoveTo on OverlayWindow * feat(ui): add reusable StatusPill for overlay status indicators * refactor(ui): reorder nav sections and add Overlay entry * feat(ui): add OverlayPanel with live status pill and reset-position * feat(ui): wire OverlayPanel into DI and ControlPanel dispatch * refactor(ui): remove Floating Overlay from Streaming panel (moved to OverlayPanel) * feat(ui): add Overlay quick-toggle card to Dashboard System Health row * feat(overlay): add ResetSize on OverlayHost and live ResizeTo on OverlayWindow Mirrors the existing ResetPosition path. OverlayNativeWindow gains a WM_APP+2 cross-thread resize message handled via SetWindowPos with SWP_NOMOVE. OverlayWindow exposes ResizeTo(w, h). OverlayHost.ResetSize pulls Width/Height defaults from a fresh OverlayConfiguration record so there are no magic numbers — persists and live-resizes under the same _lifecycleLock as ResetPosition. Position is preserved. * feat(ui): add Reset size button to OverlayPanel alongside Reset position Inline button pair on the geometry row, matching the existing Reset position affordance. Calls OverlayHost.ResetSize which writes the OverlayConfiguration record defaults (360x640) and live-resizes the overlay window if it is currently running. * feat(ui): upgrade StatusPill with broadcast-style live ring and one-shot transitions Adds a second motion language on top of the existing transitional dot-alpha pulse: an Active-state ring halo that expands 1x->3x with fading opacity on a 1.6s cycle — the convention used by streaming apps for 'live now' indicators. Three one-shot transitions are exposed via PillTransition so callers can drive stateful choreography: - Ignite (400ms outward boom) — entering Active - Extinguish (200ms inward collapse) — leaving Active - Alert (300ms white->red flash) — entering Failed StatusPillStyle bundles the broadcast knobs (background tint, live pulse, transitions) with a .Broadcast preset for the Dashboard badge. The original Render(status, elapsed, lastError) overload is preserved so the Overlay panel's existing call site is unchanged. TransitionFor(prev, curr) and DurationOf(...) helpers keep the choreography logic in one place. * feat(ui): promote overlay toggle to Dashboard presence strip, quiet overlay logs - New PresenceStripSection: full-width, always-visible click-anywhere strip with status-tinted plate, pulse ring on Live, and action verb (Show/Hide/Retry/Cancel). Replaces the old health-row card. - SystemHealthSection reduced to a pure 4-card health grid (no overlay dep). - OverlayPanel: dropped the status pill; just the enable checkbox plus geometry readout and Reset position/size. - Unify OverlayInteractionController's PositionCommitted + SizeCommitted into a single BoundsCommitted event. Two back-to-back writes for the same config type were being clobbered by ConfigWriter's debounce on resize gestures, which left the OverlayPanel showing stale Width x Height. - Downgrade overlay init / first-frame / thread-start / FPS chatter from Information to Debug. Logs are now quiet at the default level. * fix(ui): release GL resources on WindowManager.Close instead of Dispose Silk.NET's GL wrapper resolves entrypoints lazily via glfwGetProcAddress, which throws GlfwException(NoContext) once the window's context is gone. The DI container disposes singletons after WindowManager.Run returns — i.e. after the context is already torn down — so SubtitlePreviewRenderer now hooks WindowManager.Close to free GL resources while the context is still current, with Dispose as a best-effort fallback. The lower-level GL wrappers (OffscreenBuffer, BufferObject, VertexArrayObject) drop their try/catch+CheckError dance — glDelete* is spec'd to no-op on unknown/zero handles and throwing from Dispose would abort the rest of the DI disposal chain. * refactor(config): move Vision enable flag into LlmOptions VisionConfig.Enabled removed; LlmOptions.VisionEnabled takes its place so everything about the vision LLM lives in one panel. VisualQAService now reads the new flag. * refactor(vision): drop unused _visionConfig field in VisualQAService VisualQAService only ever read the vision-enabled flag; now that the flag lives on LlmOptions, VisionConfig is not read here at all. Dropping the now-dead field. * feat(ui): StatusPill accepts optional overrideLabel Keeps the colour / pulse / ring coming from OverlayStatus, lets callers supply domain-specific text. Existing callers pass null and get the old label. Used by the upcoming LLM Connection panel for 'Ready', 'Model not found', 'Auth failed', etc. * docs(ui): clarify StatusPill overrideLabel semantics + theory tests Adds the <param> tag to the convenience overload, documents that empty strings are a legitimate dot-only label on ResolveLabel, and collapses the three label-resolution tests into one [Theory] to match the rest of the UI test suite. * feat(health): add ISubsystemHealthProbe + NavRequestBus scaffolding * docs(health,nav): document SubsystemStatus params + probe bootstrap contract Clarifies the Label/Detail split so probe authors don't each invent their own convention. ISubsystemHealthProbe.Current is now called out as the bootstrap surface after subscribing to StatusChanged. NavRequestBus gains a one-line summary noting the single-thread assumption. * feat(llm): live LLM connection probe with /models reachability * refactor(llm): harden LlmConnectionProbe + document public surface Replaces the no-op FireAndForget helper with a ContinueWith(OnlyOnFaulted) log observer so background re-probe exceptions surface instead of vanishing. Caller-cancellation mid-probe now resets the channel to Unknown so the Probing state is never leaked. Adds a lock around _text / _vision to prevent torn reads of the ~32-byte LlmProbeResult struct from the UI thread. Documents all four public surfaces with XML summaries. * feat(llm): hot-reloadable Kernel gated by conversation-idle LlmKernelProvider owns the Kernel and rebuilds it when LlmOptions change. KernelReloadCoordinator fires SafeToReload on transitions into ConversationState.Idle so in-flight turns finish against the kernel they started with. Swap is Volatile.Write — no lock on the hot read path. Adds minimal StateChanged event to IConversationSession (raised from the Stateless OnTransitioned callback) and an aggregating StateChanged on IConversationOrchestrator that forwards every session's transitions. * fix(llm): harden kernel hot-reload against threading races KernelReloadCoordinator: guard state with lock; fire SafeToReload edges once per transition into Idle. LlmKernelProvider: serialise ApplyPending behind a CompareExchange gate and drain pending in a loop so edits arriving mid-build still coalesce into a single rebuild. ConversationSession: dispatch StateChanged subscribers via Task.Run so the FSM callback thread is immune to subscriber-thrown triggers or exceptions (previous sync invocation would deadlock Stateless if a subscriber fired another trigger). * fix(conversation): serialise StateChanged dispatch in FSM order Task.Run dispatch decoupled subscribers from the FSM callback thread but dropped ordering, which could misfire edge-triggered listeners (e.g. KernelReloadCoordinator firing SafeToReload mid-turn after a rapid Idle→Listening→Idle churn). Chain subscriber invocations onto a single continuation tail so they run sequentially in emission order while still off the FSM thread. * refactor(llm): chat engines consume ILlmKernelProvider IChatEngine / IVisualChatEngine implementations now capture _kernelProvider.Current.GetRequiredService<IChatCompletionService> at the top of each streaming call instead of holding a DI-injected Kernel singleton. AddLLM registers the provider + coordinator + HttpClient + ILlmConnectionProbe and drops the Kernel singleton. * docs(llm): clarify why kernel capture sits outside the retry loop * feat(health): LlmHealthProbe adapts ILlmConnectionProbe for Dashboard * fix(health): harden LlmHealthProbe against concurrent OnChanged Inner LlmConnectionProbe has independent per-channel gates, so text and vision StatusChanged can fire on different threads simultaneously. Guard _current with a lock, make Dispose idempotent, and invoke StatusChanged outside the lock to avoid subscriber re-entrancy. Add missing test coverage for Probing transitions, vision ModelMissing / InvalidUrl, and the defensive text-Disabled fall-through. * feat(health): MicrophoneHealthProbe observes device presence Adds minimal CurrentDeviceName / AvailableDevices read-only surface on IMicrophone so the probe can adapt to SubsystemStatus without the probe doing any device enumeration itself. MicrophoneHealthProbe polls on a 1-second timer plus OnChange, using the same lock-guarded current-status pattern as LlmHealthProbe. * fix(health): reconcile MicrophoneHealthProbe Compute with spec (missing device -> Degraded) * feat(health): TtsHealthProbe observes ITtsEngine readiness * feat(health): SpoutHealthProbe reports configured vs active senders * feat(ui): dashboard health cards driven by ISubsystemHealthProbe Replaces the hard-coded OK array with one card per registered probe. Cards are clickable - clicking navigates to the probe's TargetPanel via INavRequestBus. * fix(ui): claim health card hit region before drawing text GetItemRectMin/Max returned stale rect inside the Card child scope, mis-routing clicks and tooltips. Claim the hit region with InvisibleButton at the captured origin first, matching the PresenceStripSection pattern. * feat(ui): TextLlmSection with live probe + preset chips + scanned model combo * feat(ui): VisionLlmSection with ToggleSwitch header and collapsed off-state * feat(ui): LlmConnectionPanel orchestrator + DI wiring Replaces the flat legacy LlmConnection panel with a thin orchestrator over TextLlmSection + VisionLlmSection. Registers the new sections and panel in DI; drops the legacy class. * refactor(ui): ScreenAwareness links to LLM panel for vision toggle The enable-vision flag now lives solely on LlmOptions.VisionEnabled, set from the LLM Connection panel. Screen Awareness shows a muted hint + a button that jumps to that panel via INavRequestBus. Subscribes to IOptionsMonitor.OnChange so the flip is reflected immediately; class becomes IDisposable to release the subscription. * refactor(startup): drop LLM validation from startup Moved live into the LLM Connection panel via ILlmConnectionProbe. The other startup checks (GPU/CUDA/models/espeak/Live2D/prompt) stay — those are environmental and still the right place to hard-fail. * fix(di): break LLM kernel provider resolution cycle with Lazy<T> Singleton resolution deadlocked silently on startup because ILlmKernelProvider sat in a cycle it couldn't see at validation time: LlmKernelProvider -> IKernelReloadCoordinator -> IConversationOrchestrator -> IConversationSessionFactory -> IChatEngine -> ILlmKernelProvider. The factory-lambda registration hid the inner edges from MS DI's cycle detector, so instead of throwing it blocked the resolver thread and the UI never came up. Break the cycle by having the chat engines take Lazy<ILlmKernelProvider> and defer the .Value access to first use (after the graph is built). * fix(ui): guard zero-size InvisibleButton in health cards ImGui asserts on zero-size InvisibleButton. SystemHealthSection.RenderCard called GetContentRegionAvail() inside the card scope, which returns (0, 0) on the first frame before column layout has settled, tripping imgui_widgets.cpp:834 and crashing the app immediately after DI resolved. Skip the hit region and tooltip when the available size is zero; the card still draws and picks up interactivity on the next frame. * feat(ui): add IUiThreadDispatcher for marshalling off-thread callbacks * fix(ui): restore UiThreadDispatcher MaxPerFrame=64 and clarify docs * feat(conversation): add Cancelled state and cancel/retry triggers * fix(conversation): ignore input + duplicate cancel triggers in Cancelled state * feat(conversation): allow Error -> Idle via RetryRequested trigger * feat(conversation): add CancelAsync and RetryAsync on IConversationSession * feat(conversation): add CancelActiveTurnsAsync and RetryErroredSessionsAsync orchestrator APIs * feat(conversation): add IMicMuteController over IConversationInputGate Add Muted variant to SubsystemHealth enum, define IMicMuteController interface, implement MicMuteController wrapping IConversationInputGate scope for idempotent mute toggling, and add 4 unit tests covering scope acquisition, disposal, event firing, and thread safety. * feat(health): MicrophoneHealthProbe reports Muted when user has muted input Register IMicMuteController as a singleton in AddConversationPipeline. Inject IMicMuteController into MicrophoneHealthProbe; short-circuit status to SubsystemHealth.Muted on mute toggle via OnMuteChanged, and unsubscribe in Dispose. Update existing probe tests to pass the new parameter; add one Muted scenario test. * chore(health): remove unused SpoutHealthProbe and related references Deletes SpoutHealthProbe.cs and its test file, removes the TODO comment from ServiceCollectionExtensions.cs, and updates the SystemHealthSection doc comment (Mic → LLM → TTS). Layout is dynamically driven by the ISubsystemHealthProbe enumerable so no column-count adjustment was needed. * feat(ui): add Dashboard ControlsSection (cancel, retry, mute) * feat(ui): integrate ControlsSection into Dashboard as 5-row layout * fix(ui): propagate Dispose from ControlPanelComponent to owned panels * fix(ui): marshal LLM section probe/options callbacks via UI dispatcher * feat(ui): add SubsystemStatusChip shared widget Introduces SubsystemStatusChip, a static widget that centralises the SubsystemHealth → OverlayStatus mapping and delegates rendering to StatusPill with broadcast-style visuals. Extends OverlayStatus with three new values (Degraded, Muted, Unknown) and adds corresponding StatusPill.StatusVisuals cases. Dashboard health cards and LLM section headers (Tasks 14–15) will call this widget instead of duplicating the mapping inline. * refactor(ui): route health cards through SubsystemStatusChip * refactor(ui): route LLM section pills through SubsystemStatusChip * feat(ui): add EndpointPickerRow shared widget * refactor(ui): consume EndpointPickerRow from LLM sections * feat(ui): add ScannedModelPicker shared widget * refactor(ui): consume ScannedModelPicker from LLM sections * fix(ui): keep vision toggle visible and interactive when section is disabled Always render the header status chip, synthesising a Disabled/Off status when VisionEnabled is false so the toggle has a visual anchor. Previously the chip was hidden in the off state and the toggle sat alone at the far right edge of the card, easy to miss. The probe does not run while the channel is disabled, so relying on _probe.VisionStatus for the off state would show an ambiguous '?' — synthesising the status explicitly is the source of truth here. * fix(session): keep Error state durable so Retry works HandleErrorAsync used to auto-fire StopRequested, collapsing Error to Ended in the same tick — so the Retry card could never observe the Error state and remained non-interactive. Error is a durable recovery state the user exits via RetryRequested (configured in ConfigureStateMachine); removing the auto-fire restores that contract. * fix(startup): flush pre-abort llama logs + unhandled exception sink llama.cpp/ggml call abort() shortly after emitting an error, so the managed Console buffer was being discarded and the diagnostic line never reached the log. Flush Console.Out/Error synchronously on the Error path so the last words survive the abort. Also move CreateLogger ahead of the NativeLibraryConfig callback so the Serilog sink exists before llama.cpp can emit anything, and install AppDomain.UnhandledException + ProcessExit sinks that Log.Fatal the exception and CloseAndFlush so crashes always hit disk. * fix(health): keep LLM card Healthy when vision is unprobed Text channel is load-bearing — if text is Reachable but vision is Unknown (channel disabled in config, or still in flight on the first frame) the card rendered as a grey 'Ready' which reads as 'something is broken.' Treat (Healthy, Unknown) as overall Healthy; vision's own panel still surfaces its real status. Added two InlineData rows covering the regression plus a comment explaining the 'Ready but grey' failure mode. * feat(ui-layout): add Sz.Auto row mode with measured-height caching Layouts can now mix three row modes in one group: Fixed (explicit px), Auto (open an AutoResizeY child and let content dictate the height), and Fill (claim whatever remains). RowGroupScope caches each auto row's measured height per group id so Fill rows can reserve accurate space across frames without needing callers to hard-code section heights. * refactor(ui-dashboard): size sections to content, cards signal affordance Dashboard drops the hard-coded HealthSectionHeight / ControlsSectionHeight / StatsSectionHeight constants and lets every section except the chat transcript use Sz.Auto; the transcript claims remaining space via Fill. Health strip and transcript share a nested gap-0 row group so they still read as one conversation surface. Control cards now use semantic color: disabled falls back to flat Surface2 neutral, enabled carries an accent-tinted fill + border (thicker on hover/active) so clickability is obvious at a glance rather than a 2% alpha delta. Guards zero-size frames defensively the same way the health cards do. * refactor(ui-llm): unify preset picker on BeginCombo, fix panel cutoffs EndpointPickerRow now uses the same BeginCombo + Selectable widget pattern as ScannedModelPicker (one idiom for both dropdowns). The 'Presets' label is kept as the combo's preview string — there is no canonical 'selected preset' to show since users may edit the URL freely. Presets are shared via EndpointPickerRow.DefaultPresets so Text and Vision sections stop duplicating the OpenAI / LM Studio / Ollama list. Also fixes a set of right-edge cutoffs: InputText now reserves explicit width for every trailing button/combo (presets combo, Hide/Show key button) instead of relying on SetNextItemWidth(-1), and the Vision header captures GetContentRegionAvail AFTER SameLine so its toggle + chip layout math matches the working TextLlm pattern. Toggle is pulled up by FramePadding.Y so its self-centered track visually aligns with the chip's midline, without pushing the chip around. ScannedModelPicker uses '##model' (hidden id) to stop ImGui painting a visible label to the right of the combo past the card's edge, and drops the 'advanced custom model name' free-text fallback — the dropdown is the only path. * perf(rvc): own destination buffer, single-pass chunk loop, cache audition models ProcessChunk now writes into a caller-owned Memory<float> destination and returns the produced sample count, eliminating the per-segment new float[] allocation. ProcessInChunks computes the total upper bound in one pass, allocates a single backing array, and writes successive chunks at offsets, removing the List<Memory<float>>, the LINQ Sum, the CombineChunks helper, and the redundant Array.Copy. The voice-audition path used to construct a fresh RmvpeOnnx + OnnxRVC pair on every call (multi-second ONNX session init). Added a bounded LRU cache (capacity 2) keyed by voice name + hop size, drained on dispose. Closed a Dispose race in OnOptionsChanged: the new predictor/model pair is disposed if _disposed flips to true between background load and write-lock acquisition, instead of leaking the freshly-built sessions. Marked _currentOptions volatile so MinimumSampleCount's lock-free read sees the published reference rather than a cached register value. * fix(qwen3): observe cancellationToken in GenerateCodeFrames The decode loop now accepts a CancellationToken and checks it at the top of every step. Previously a cancelled turn would keep generating until maxSteps or natural EOS, wasting GPU time and delaying context release. Reset() in the finally still clears the talker KV cache, so cooperative cancellation is safe. * perf(audio): pool mic scratch, publish amplitude with Volatile, drain on dispose MicrophoneInputNAudioSource: replaced the single _samplesScratch field (grown on the capture thread via Array.Resize, racing against any UI reader of the prior buffer) with an ArrayPool<float> rental per DataAvailable callback. The IMicrophone.AudioSamplesHandler contract already says the span is only valid for the call, so no lifetime change. MicrophoneAmplitudeProvider: _smoothed is written from the capture callback thread and read from the UI thread; the read/write are now Volatile so the UI sees a recently-published value rather than a cached register. Added a volatile _disposed flag and an early-out in OnSamplesAvailable, since NAudio may invoke subscribers for an in-flight buffer after unsubscribe. * fix(state-machine): ignore Cancel/Retry triggers in no-op states Adds .Ignore(CancelRequested) and .Ignore(RetryRequested) to every state where firing the trigger is a benign no-op: Initial, Initializing, Idle, Listening (substate of Idle), Interrupted, Paused, Cancelled (both), plus RetryRequested on ActiveTurn (covers all turn substates). Also extends Cancelled with .Ignore(PauseRequested). With the FSM now the single source of truth, the TOCTOU pre-checks in ConversationSession.CancelAsync and RetryAsync (IsInState(...) → FireAsync) are removed. Previously a turn completing between the two calls would fire Cancel/Retry into Idle, trip OnUnhandledTriggerAsync, and push the session into Error. * fix(session): keep Cancelled state durable if teardown throws HandleCancelledAsync now wraps the pipeline teardown in try/finally and fires CancelComplete unconditionally. If the teardown throws, the exception is logged but the FSM still lands back in Idle. Rationale: Cancel is user-initiated. Leaking a pipeline fault into Error would punish the user for an internal problem on a path they explicitly asked to abort. Getting stuck in Cancelled (the prior behaviour) was worse — no trigger out of Cancelled except CancelComplete/StopRequested/Error, and the user has no UI affordance to force them. * fix(conversation): subscribe to session.StateChanged before RunAsync The orchestrator previously called session.RunAsync(...) and started HandleSessionCompletionAsync before wiring up StateChanged, so the initial Initializing → Idle transition could fire before the subscription existed on fast-init paths (the UI then missed the first state edge). Subscription now happens immediately after session creation, before RunAsync is invoked. All failure paths (TryAdd collision, OperationCanceled, generic exception) unsubscribe before disposing, keeping the subscribe/unsubscribe pair symmetric with HandleSessionCompletionAsync. * fix(conversation): invoke MicMuteController.MutedChanged under the lock Two near-simultaneous SetMuted calls could serialise the scope change but deliver the corresponding events in reverse order, because the event was raised after the lock released. The invocation now happens inside the lock. Subscribers therefore run on the caller's thread while the lock is held — acceptable here because (a) mute toggles are rare, (b) current subscribers are cheap (MicrophoneHealthProbe.Refresh uses its own lock and doesn't re-enter the mute controller; ControlsSection marshals via IUiThreadDispatcher). * fix(llm): close hot-reload races, dedup probe events, pool HTTP via factory - LlmKernelProvider.ApplyPending now re-checks IKernelReloadCoordinator.IsSafeToReloadNow at the top of each drain iteration AND immediately before publishing the rebuilt Kernel. SafeToReload fires outside the coordinator's lock, so state can flip Idle->Busy in the gap; the second-gate check prevents a kernel swap landing mid-turn. Pending options are restored when the post-Build gate closes so the next idle transition retries from the same baseline. - LlmKernelProvider now disposes the previous Kernel.Services after a 30s grace period (covers the 60s Polly LLM timeout with margin). Kernel itself is not IDisposable, but Kernel.Services is the DI ServiceProvider that owns OpenAIChatCompletionService -> OpenAIClient -> HttpClient. Each replaced kernel schedules its own disposal independently — no shared slot to coordinate. - LlmConnectionProbe.OnOptionsChanged holds _syncRoot across the snapshot read+swap so concurrent change-callbacks can no longer both observe the same prev and skip a re-probe. - Switch from a singleton HttpClient (DNS pinned forever) to IHttpClientFactory.CreateClient("llm-probe"). DI registers AddHttpClient with the same 5s timeout; SocketsHttpHandler rotation is now the factory default (2 min). Constructor signature changes from HttpClient to IHttpClientFactory; test stub updated. - LlmConnectionProbe.Store() dedupes by (Status, AvailableModels) so re-probes that yield the same surface no longer fire StatusChanged. ILlmConnectionProbe XML doc updated to match. - On OperationCanceledException mid-probe, no longer overwrite the stored result with Unknown — leaves the prior status visible instead of flapping the UI grey to "Not tested" before the next probe lands. * fix(health): dedup on surface, drain mic timer before unsubscribe - LlmHealthProbe.OnChanged now compares (Health, Label) only via SameSurface, ignoring Detail. Detail is built fresh per probe and can carry probe-local information; gating dedup on it would silently break and storm StatusChanged. SubsystemStatus.Equals (record-struct full equality) is left intact for callers that want it. - MicrophoneHealthProbe.Dispose now stops the timer with Change(Infinite), blocks on Timer.Dispose(WaitHandle) until any in-flight callback returns, THEN unsubscribes from MutedChanged and disposes the options subscription. A callback already running Refresh() would otherwise read a stale muteController if unsubscribe came first. * fix(ui): guard SystemHealthSection.Dispose against double-dispose Add _disposed flag matching the pattern in ControlsSection so the section can be torn down multiple times without unsubscribing from probe.StatusChanged twice. Required so the next change — disposing the section from Dashboard.Dispose AND from the panel-registration teardown — is safe by construction. * refactor(ui): make ControlPanel registration disposal-aware Extend PanelRegistration with an optional IDisposable so RegisterPanel can declare both lifecycle-aware and disposable panels in a single call. ControlPanelComponent.Dispose now iterates the registration dictionary and disposes any non-null Disposable, replacing the previous hard-coded field forwarding to Dashboard / LlmConnectionPanel / ScreenAwareness. This makes future panels disposal-safe by construction: opting in is one named argument on RegisterPanel rather than a field + Dispose() update in three places. All currently-disposable panels (Dashboard, LlmConnectionPanel, ScreenAwareness, Personality, Listening, Voice, Avatar, Subtitles) are re-registered through the new overload. Listening continues to declare its IActivatablePanel role too — disposal and activation are orthogonal axes. * fix(ui): propagate Dispose to AvatarPanel children ModelSection and LipSyncSection both register IOptionsMonitor.OnChange callbacks (Live2DOptions and LipSyncOptions respectively); when the control panel was torn down those subscriptions stayed live, so option changes kept invoking handlers on a logically-gone panel. AvatarPanel now implements IDisposable with a _disposed guard and forwards Dispose() to both child sections. The control panel's disposal-aware registration calls it on shutdown. * fix(ui): propagate Dispose to SubtitlesPanel children All five subtitle sections are IDisposable: TextStyleSection, ColorsSection, PlacementSection, and CanvasSection register IOptionsMonitor.OnChange callbacks, and PreviewSection additionally owns a SubtitlePreviewRenderer with a GL framebuffer object. When the control panel was torn down none of these were released — handlers kept firing and the FBO leaked. SubtitlesPanel now implements IDisposable with a _disposed guard and forwards Dispose() to all five sections. The control panel's disposal-aware registration calls it on shutdown. * fix(ui): propagate Dispose to ListeningPanel children All four listening sections (MicrophoneDeviceSection, SpeechDetectionSection, RecognitionSection, InterruptionSection) hold IOptionsMonitor.OnChange subscriptions; when the control panel was torn down those handlers stayed live. ListeningPanel now implements IDisposable in addition to its existing IActivatablePanel role, with a _disposed guard, and forwards Dispose() to all four child sections. The activation-lifecycle gate scope in OnActivated/OnDeactivated stays as-is — it covers a different axis (panel-active vs panel-alive). * fix(ui): dispose SystemHealthSection from Dashboard teardown Dashboard.Dispose only tore down ControlsSection, so SystemHealthSection kept its probe.StatusChanged subscriptions live after shutdown. Dispose SystemHealthSection alongside ControlsSection and guard Dispose with a _disposed flag for idempotence. The remaining three sections (PresenceStripSection, TranscriptSection, SessionStatsSection) are not IDisposable — audited; nothing to release. * fix(ui): propagate Dispose to PersonalityPanel and VoicePanel children Same leak class as the Avatar/Subtitles/Listening panels: every section under PersonalityPanel (PromptSourceSection, CurrentVibeSection, TopicsSection) and the disposable sections under VoicePanel (VoiceModeSelector, VoiceGallery, CloneLayerSection, AdvancedSection) register IOptionsMonitor.OnChange callbacks that stayed live after the control panel was torn down. Both orchestrators now implement IDisposable with _disposed guards and forward Dispose() to each disposable child. VoiceCard is intentionally not disposed — it reads option values via CurrentValue only and never subscribes to OnChange. * style: csharpier format Wave-1 changes * perf(ui-layout): remove per-frame allocations in row/grid scopes RowGroupScope previously allocated a Sz[] snapshot and float[] auto-heights array on every frame, and the shared AutoHeightCache was an unbounded ConcurrentDictionary with no eviction. Per-row BeginChild ids were built via interpolation using the cursor-Y/height hash, which is per-row-per-frame string work. - Hold ReadOnlySpan<Sz> directly in the ref struct (caller's stack). - Reuse the cached auto-heights array in place via a single lookup; writes land in the cache slot so there is no copy-back on Dispose. - Replace ConcurrentDictionary with a Dictionary + lock and bound it at 256 entries via drop-oldest insertion-order eviction (LinkedList<string> + node map for O(1) removal). The layout system only runs on the UI thread so the lock is trivial; the bound exists to catch misuse (varying ids) rather than as a design point. - Pre-compute RowIds[0..15] and use the row index for stable BeginChild ids; fall back to i…
…ease (#22) * chore: remove dead files and clean up stale comments - Delete fully commented-out IStreamingAudioPlayer.cs (unused interfaces) - Delete fully commented-out AdapterManagerService.cs (P2 dead code) - EmotionAnimationService: drop stale TODO and speculative self-musing comments - IdleBlinkingAnimationService: replace dead-code block with single factual note, remove "redundant checks, but safe" self-deprecation * chore(bootstrapper): scaffold PersonaEngine.Lib.Bootstrapper project * test(bootstrapper): scaffold test project with smoke test * feat(assets): add manifest enums + AssetId/FeatureId record structs * feat(bootstrapper): AssetSource discriminated union + JSON converter * feat(bootstrapper): AssetEntry + AssetCapability + InstallManifest records * feat(bootstrapper): InstalledAssetRecord + InstallStateLock records * feat(bootstrapper): ManifestLoader with embedded resource + schema-version guard * feat(bootstrapper): atomic InstallStateLockStore with corruption recovery * feat(bootstrapper): Sha256VerifyingStream + IntegrityException * feat(bootstrapper): IAssetSource abstraction + AssetDownload record * feat(bootstrapper): ZipExtractor with selective extraction + zip-slip guard * feat(bootstrapper): NvidiaRedistManifest parser with size-as-string-or-number support * feat(bootstrapper): NvidiaRedistClient with per-instance manifest cache * feat(bootstrapper): HuggingFaceClient with HF_ENDPOINT override + tag-only revisions * feat(bootstrapper): AssetDownloader with Range-resume + atomic rename + verify * feat(bootstrapper): named AssetDownload HttpClient with Polly retry policy * feat(bootstrapper): AssetAction + AssetPlanItem + AssetPlan types * feat(bootstrapper): AssetPlanner with profile/exclusion/mode rules * feat(assets): IAssetCatalog interface + FeatureIds constants + UserAsset * feat(assets): debounced UserContentWatcher * feat(assets): AssetCatalog with FrozenDictionary feature cache + user-content enumeration * test(assets): build-time manifest validation + FeatureIds coverage * feat(bootstrap): mode/options/result carriers * feat(bootstrap): IBootstrapUserInterface abstraction * feat(bootstrap): profile picker copy as static catalog * feat(bootstrap): BootstrapRunner orchestrating manifest→plan→download→catalog * fix(bootstrap): align Phase 7 with spec verbatim - BootstrapMode: replace doc comments with spec-exact text - BootstrapResult/BootstrapOptions: add per-property doc comments per spec - IBootstrapUserInterface: replace redesigned interface with spec contract (PickProfileAsync+choices list, ShowPlanSummary, RunWithProgressAsync, ShowResult); move ProfileChoice record here, replacing ProfileChoiceEntry - ProfileChoiceCatalog: use ProfileChoice record, spec-verbatim copy for all three profiles (titles/sizes/taglines/bullets exactly as spec) - BootstrapRunner: remove extra resourceRoot ctor param; use spec's RunWithProgressAsync/ShowPlanSummary/ShowResult UI calls; extract ResolveProfileAsync and BuildUpdatedLock helpers per spec - BootstrapRunnerTests: replace with 4 spec scenarios (no_lock_runs_picker, complete_lock_skips, offline_failure, preselected_overrides_picker) - ProfileChoiceCatalogTests: rename to spec-exact method names, assert choice.Profile (not .Tier), include SizeLabel.Contain("GB") * fix(bootstrap): add missing test assertions per spec * fix(bootstrap): filter plan.Items by action when writing lock and dispatching downloads - BootstrapRunner.RunAsync now pre-filters plan to only Download/Redownload items before calling RunWithProgressAsync, so Skip/Remove/Reverify items are never dispatched to the downloader (Critical #1). - BuildUpdatedLock now uses a switch on AssetAction: writes new records for Download/Redownload/Reverify, removes entries for Remove, and leaves Skip entries untouched — fixing the silent re-write-on-Remove and skip-bump bugs (Critical #2). - Offline error message now filters to non-Skip items only (Important #3). - Added deliberate-fallback comment in ResolveProfileAsync for Verify/Repair with no existing lock (Important #4). - Documented _catalog field (kept for DI shape) and ResourceRootOverride (consumed by Phase 9 host wiring) (Important #5). - Restored class-level XML doc on BootstrapRunner (Important #6). - Added doc comment to IBootstrapUserInterface.RunWithProgressAsync clarifying pre-filtered items contract (part of Critical #1 fix). - Strengthened AutoIfMissing_with_no_lock test with lock-content assertions (Test #8). - Added BuildUpdatedLock_removes_out_of_profile_asset_and_writes_downloaded_asset regression test (Test #7). All 49 tests pass. * feat(bootstrap): CommandLineArgs parser for installer flags * feat(bootstrap): Spectre.Console picker + progress UI * feat(bootstrap): non-interactive UI implementation * fix(bootstrap): rewrite Phase 8 to match spec * fix(bootstrap): propagate cancellation and assert pass-through ordering * feat(app): run bootstrapper before DI, register IAssetCatalog as singleton * feat(di): register IAssetCatalog in AddApp via AssetCatalogFactory * feat(di): gate RVC/Vision/Audio2Face on IAssetCatalog.IsFeatureEnabled Refactor AddConversation to probe IAssetCatalog before registering optional subsystems whose ONNX models / runtime DLLs are owned by the bootstrapper: RVC voice cloning (FeatureIds.VoiceCloning), Audio2FaceLipSyncProcessor (FeatureIds.Audio2Face), and the visual chat engine + screen capture (FeatureIds.LlmVision). Splits AddChatEngineSystem so the always-required text engine stays unconditional while IVisualChatEngine, IVisualQAService, and WindowCaptureService are extracted into a new AddVisualChatEngine extension that only registers when the vision tier was installed. Removes the unconditional Audio2FaceLipSyncProcessor registration from AddTTSSystem; LipSyncProcessorProvider remains so the empty-list case still resolves cleanly. Uses a temporary BuildServiceProvider() probe disposed before the real graph is built so its singletons do not leak. * feat(ui): voice/avatar pickers source from IAssetCatalog with live refresh ModelSection (Live2D) and CloneLayerSection (RVC) now enumerate available characters/voices via IAssetCatalog.GetUserAssets instead of poking the disk directly or asking IRVCVoiceProvider. Both sections subscribe to the catalog's Changed event so dropping a new model into live2d/ or a new RVC model into rvc/voices/ refreshes the picker without restarting the UI — the catalog's debounced FileSystemWatcher already covers that signalling. ModelSection no longer scans Live2DOptions.ModelPath; the catalog owns the canonical user-content roots derived from the install manifest. Kokoro voice gallery is unchanged: it is driven by the embedded VoiceMetadataCatalog (static metadata), not disk enumeration. * refactor(startup): drop model-existence probes (now owned by IAssetCatalog) CheckModels and CheckLive2D duplicated work the bootstrapper already does before this validator runs: it provisions every required asset and the runtime IAssetCatalog reports authoritative state via GetAssetState / IsFeatureEnabled. Removes both probes plus the now-unused ModelsDir field and updates the type doc comment to call out the new division of labour. Retains GPU/CUDA/espeak-ng/prompt checks: those validate environment prerequisites the bootstrapper does not (and cannot) install. * fix(bootstrap): eliminate temp provider probe and marshal catalog events to UI thread * chore: stage local-only asset source tree * tools: hash-manifest.ps1 to refresh manifest SHA256 from assets-source/ * tools: upload-hf-assets.ps1 for tagged HuggingFace publish * feat(manifest): populate install-manifest.json with all required assets * refactor: unify runtime asset layout under <BaseDir>/Resources/<subsystem> ModelUtils, TtsConfiguration.ModelDirectory, and Live2DOptions.ModelPath now resolve under AppContext.BaseDirectory/Resources/<lowercase-subsystem> to match the canonical layout the installer writes via install-manifest.json. Lexicon now reads from kokoro/lexicons/ so its files live under the kokoro subsystem rather than at the ModelDirectory root. ModelType enum descriptions carry their subsystem prefix (silero-vad/, whisper/, profanity/) so each consumer resolves to the same canonical path. This fixes the divergence where bootstrapper-installed assets landed at lowercase paths but runtime consumers still read from legacy mixed-case Resources/Models and Resources/Live2D/Avatars locations. * refactor: align install-manifest.json with runtime asset layout Rewrite installPaths to drop the models/ and runtime/ prefixes so they resolve under <BaseDir>/Resources/<subsystem>/ matching the unified runtime layout. Add missing asset entries the runtime requires for a clean install: - silero-vad (gates Asr.WhisperTiny + Asr.WhisperTurbo) - profanity-bundle (gates Tts.ProfanityBeep) - opennlp-bundle (gates Tts.Kokoro + Tts.Qwen3) - kokoro-lexicons (gates Tts.Kokoro) - rvc-rmvpe (gates Tts.Rvc) - mdx-bundle (gates Music.SourceSeparation) - mel-band-roformer (gates Music.SourceSeparation) Add corresponding FeatureIds.MusicSourceSeparation and Live2DAvatar so the FeatureIdsCoverageTests assertion still passes. Fix whisper-tiny filename to ggml-tiny.en.bin (the actual file the runtime expects). * chore: polish asset helper scripts and document assets-source layout hash-manifest.ps1 - Validate manifest + assets-source paths up front - Refuse to write the manifest while any HuggingFace asset is missing under assets-source/ (lists each missing entry by id) - Skip NVIDIA assets silently - Add -WhatIf preview support - Print summary: number of assets hashed, total size, files updated upload-hf-assets.ps1 - Validate huggingface-cli presence with a clear remediation message - Echo repo + revision + source path before uploading - After tagging, print the resulting tree URL (https://huggingface.co/<repo>/tree/<revision>) for verification assets-source/README.md - Document why the directory is gitignored - Add a full layout tree mapping local paths to manifest entry ids - Walk through the contributor workflow: drop -> hash -> commit -> upload - Note the no-overwriting-tags rule for reproducible installs * fix(live2d): resolve ModelPath against BaseDirectory + align default to aria C1: Live2DManager.Initialize() now joins relative ModelPath against AppContext.BaseDirectory before handing it to LAppLive2DManager, matching the BaseDirectory-relative resolution that TtsConfiguration.ModelDirectory and StartupValidator.CheckPrompt already use. Without this fix the path resolved against Environment.CurrentDirectory, which differs from the install root when launched outside the bin folder. C2: Live2DOptions.ModelName default changes from 'angel' to 'aria' to match the live2d-aria asset bundle the installer drops at Resources/live2d/aria. The shipped appsettings.json already used 'aria'; this aligns the code default so a clean install boots without the loader failing on a missing 'angel' folder. I4: FeatureIds.Live2DAvatar and FeatureIds.MusicSourceSeparation are manifest-only sentinels (no conditional DI). Documented intent so future readers don't wire them or treat them as dead code. * fix(build): align dev-tree Resources layout with installer canonical paths C3: csproj copy-to-output globs now mirror the per-subsystem layout the bootstrapper writes to <BaseDir>/Resources/ (silero-vad, whisper, kokoro, qwen3-tts, wav2vec2, opennlp, rvc, audio2face, mdx, mel_band_roformer, profanity, live2d). Previously the globs targeted Resources/Models/** and Resources/Live2D/Avatars/**, which copied to bin/.../Resources/Models/... and bin/.../Resources/Live2D/Avatars/... — paths the runtime hadn't read from since commit a3bb102 (where ModelUtils, TtsConfiguration, Lexicon all moved to lowercase per-subsystem roots). Result: F5/dotnet run loaded the previous build's cached files and broke after any Resources change. The dev tree under src/.../Resources/ is gitignored so this commit only touches the csproj globs; the matching folder rename (Resources/Models/<sub> → Resources/<sub>, Resources/Live2D/Avatars/<m> → Resources/live2d/<m>, plus loose files lifted into silero-vad/, whisper/, profanity/, kokoro/lexicons/) has to be done in the local working copy. Rebuild verified bin/Debug/net9.0/Resources/ now contains the same subsystem dirs the install-manifest.json declares. I3 (folded in to keep the test suite green after the layout change): CtcConfidenceAnalysisTests no longer hard-codes a developer-machine path. It now derives the model base directory from AppContext.BaseDirectory (overridable via TTS_MODEL_BASE_DIR) and skips cleanly when the optional Qwen3/CTC models aren't present, instead of throwing in FileModelProvider's directory-existence check. * fix(bootstrap): preserve in-zip directory structure on extract C4: ZipExtractor was calling Path.GetFileName(entry.FullName) and writing every entry flat into the target directory. That broke bundles whose runtime consumers read from subdirectories — qwen3-tts (embeddings/, speakers/), wav2vec2 (onnx/model.onnx). Extracted assets landed at the bundle root and FileModelProvider.GetModelPath() pointed at non-existent nested paths. Now the extractor preserves entry.FullName under the target directory, recursively creates intermediate directories, and keeps both path-traversal guards (raw '..' segment check on the unresolved name + post-resolve StartsWith check on the absolute destination) so the zip-slip mitigation is unchanged. Updated Extract_only_writes_requested_entries to assert the bin/ prefix is preserved (was previously asserting flat filenames). Added Extract_preserves_nested_directory_structure as the regression test for the qwen3-tts / wav2vec2 bundle shape. * fix(bootstrap): anchor archive extraction at caller-resolved install path (I1) Source clients used to receive the raw AssetEntry.InstallPath (a relative path) and forward it directly to ZipExtractor, which resolves against Environment.CurrentDirectory. That meant archives could land anywhere depending on where the bootstrapper was launched from instead of under the configured resource root. Pass the absolute, resource-root-anchored install path down through IAssetSource.ResolveAsync. PlanItemAssetDownloader now computes the destination once and hands it to both the source client (for extraction targets) and the inner AssetDownloader (for non-archive moves), making the resource root the single source of truth for install location. Tests updated to pass the new argument; added a coverage test that proves HuggingFaceClient extracts to the resolved absolute path even when the entry's InstallPath would otherwise resolve against CWD. * docs(installer): deprecate manual install guide and fix hash-manifest WhatIf counter (I2+I5) INSTALLATION.md: prepend a deprecation callout pointing users at the in-app bootstrapper (which handles model download, SHA-256 verification, and native-runtime install on first launch). The manual flow is kept for reference / air-gapped installs only. hash-manifest.ps1: $updated was being incremented unconditionally, so in -WhatIf preview runs it counted assets that were not actually mutated. Move the increment inside ShouldProcess and track preview counts in a separate $wouldUpdate variable so the summary line accurately reflects what was (or would be) written. * tools: build-release.ps1 with bootstrap-aware Resources stripping Strips only the subdirectories that the bootstrapper installs at first run (whisper, kokoro, rvc, etc.), preserving baked-in shaders, fonts, prompts, and native libraries. Hard ceiling at 1.8 GB. * ci: release workflow that publishes, strips bootstrap dirs, zips, asserts size * docs: rewrite install section around bootstrapper + flags * refactor: stop bundling CUDA DLLs; pre-load bootstrapped copies at startup Removes the ~1.7 GB Content Include for CUDA/cuDNN DLLs from PersonaEngine.Lib.csproj. The bootstrapper already installs the same libraries via NvidiaRedist manifest entries on first run. Adds an explicit pre-load loop in Program.cs that walks the bootstrapped Resources/cuda/{cudart,cublas,cufft} and Resources/cudnn directories and LoadLibraryEx's each DLL in cudart-first order so ONNX Runtime GPU and Whisper.net's CUDA backend resolve their imports against the pre-loaded copies. This works around the single-directory limit of SetDllDirectory. Reduces the release zip from ~2.1 GB to ~705 MB. * fix: register AssetPlanner with ResourceRoot factory; implement Remove action AssetPlanner ctor takes (string installRoot) but was registered without a factory, causing DI to fail at startup with "Unable to resolve service for type 'System.String'". Centralize the resource-root path as a single source of truth used by AssetPlanner, PlanItemAssetDownloader, and the new BootstrapRunner factory. Implement BootstrapRunner.ApplyRemovals to delete on-disk files and directories for plan items with AssetAction.Remove, with absolute-path and path-traversal guards rooted at the resource root. Best-effort per-asset error handling so a single failure does not abort bootstrap. Add ServiceCollectionExtensionsTests that builds the full DI graph with ValidateOnBuild=true and resolves BootstrapRunner end-to-end, preventing regression of the original DI defect. * ci: gate releases against placeholder all-zero SHA256 hashes Tighten ManifestValidationTests.Every_asset_has_sha256_or_is_NativeRuntime to also reject the all-zero placeholder hashes left in install-manifest.json before scripts/hash-manifest.ps1 has been run. Suppress with the PERSONA_ALLOW_PLACEHOLDER_HASHES=1 env var for local pre-release dev so the build stays green until the real hashes are populated. Add an explicit "Verify manifest hashes are populated" step to the release workflow that fails fast on tag push if any placeholder hashes remain. This ensures a v* tag cannot ship a manifest where every download fails SHA verification. * fix: bootstrapper hardening — lock dir creation, error logging, extract cleanup InstallStateLockStore.Write now ensures the lockfile parent directory exists before opening the temp file. NoOpBootstrapUserInterface now logs per-asset download failures with the full exception, so non-interactive callers (CI, scripts) can diagnose "One or more assets failed to download" without having to enable interactive mode. ZipExtractor wraps per-entry writes in a try/catch that deletes the partially-written destination file on cancellation or I/O failure before rethrowing, preventing orphaned partials from accumulating across repeated Ctrl-C runs. * chore: replace dingbat glyphs with ASCII; drop unused PassThrough arg SpectreBootstrapUserInterface now uses [OK]/[FAIL] instead of the Unicode check/cross dingbats that violated the project's no-emoji-in-code rule. CommandLineArgs no longer parses or exposes PassThrough — the property was never read anywhere downstream. Updated the corresponding test to assert the silent-ignore behavior that replaces it. * docs: note that pre-installer Resources/ trees are ignored on upgrade Adds a brief upgrade callout under the install section so users coming from a pre-bootstrapper build know their existing Resources/Models and Resources/Live2D/Avatars contents will be ignored and re-downloaded into the new layout. * feat(installer): profile-aware DI gating and CUDA 13 preload Register ASR/TTS services based on installed feature set — BuildWithIt gets Whisper Turbo + Qwen3, lower tiers skip the gated model factories so DI build doesn't crash on missing .bin files. Preload cudart-v13 / cublas-v13 alongside the existing CUDA 12 stack for Whisper.net's ggml-cuda-whisper, which links against CUDA 13. Route bootstrap-time Serilog through the DI logging pipeline so operators see per-asset download failures. - Add EspeakResolver to locate bundled espeak-ng + phontab/dict - Add FeatureProfileMap + LockedAffordances widget for gated UI panels - Bump install-manifest with cudart-v13 / cublas-v13 entries - Drop bundled libsodium/opus DLLs (now delivered via asset download) * feat(bootstrap): GPU preflight, UI polish, and asset pipeline hardening Run nvidia-smi + nvcuda.dll probe before the profile picker so users on unsupported hardware (driver < 580.65 or compute < Pascal 6.0) get a warning instead of downloading ~9 GB of CUDA redistributables. Abort in non-interactive mode; interactive users can override with C/Y/Enter or bypass via --skip-gpu-check. Replace the standard figlet banner with a curvy script-style PersonaEngine title and pink sparkle garland. - Rework profile picker with keyboard tab-nav + per-profile detail panel - Harden asset pipeline: safer zip extraction, NVIDIA redist manifest parsing, per-item download retries, PlanItemAssetDownloader - Add 28 tests covering GPU preflight decision matrix + UI rendering (SpectreBootstrapUserInterfacePickerTests, GpuWarningTests, NvidiaGpuPreflightCheckTests) * chore(release): bootstrap-aware build scripts and CI workflow Publish now strips the pre-downloaded Resources/ tree before zipping so the installer archive only ships binaries + manifest; the bootstrapper fetches assets on first run. Replace the PowerShell hash-manifest with a Python port that runs on the CI Linux runners. - scripts/verify-imports.py: static PE import audit for self-contained - scripts/verify-self-contained.ps1: runtime self-contained assertion - scripts/stage-hf-assets.ps1: pre-upload staging for HF buckets - .github/workflows/release.yml: wire the new scripts into the pipeline * refactor(bootstrap): address code-review findings - Drop redundant StartupValidator.CheckGpu; bootstrap preflight already validates driver + compute capability before this runs. - BootstrapResult.Ok / Fail factories replace 7 duplicated new{} blocks in BootstrapRunner. - AssetPlan caches IsEmpty + RequiredBytes at construction instead of re-walking the item list on every read. - AssetCatalog publishes State + Feature caches atomically via Volatile.Write over a single Snapshot record; GetAssetState now hits the cache instead of re-stat'ing the filesystem on every call. - AssetCatalogChangedEventArgs dropped — the payload was always empty and no subscriber used it; Changed is now plain EventHandler. - AssetPlanner: log-and-keep orphan lock entries instead of silently dropping them, honor UserExclusions uniformly so a toggled exclusion produces a Remove against an already-locked asset, and split profile/exclusion/action selection into focused helpers. - AssetDownloader: detect 200-on-Range (server ignored the header) and restart from scratch before appending wrong-offset bytes; File.Move uses overwrite:true for atomic rename. - UserContentWatcher: dispose superseded CancellationTokenSources so long-running processes don't leak one CTS per filesystem event. - SpectreBootstrapUserInterface: number-key picker shortcut via a single range check (TryNumberKeyToIndex) so adding a 4th profile doesn't require another switch arm. - FeatureProfileMap: VisionCapture mapped to TryItOut (Rust screen-capture lib ships in every release — no install gate); added FeatureProfileMapDriftTests that caught the misleading BuildWithIt. - BootstrapRunner takes TimeProvider for testable lock timestamps. * refactor: finish deferred cleanup — split UI partials, extract GpuFloor, primary ctors - SpectreBootstrapUserInterface split into 5 partial files (Theme, Picker, GpuWarning, Progress + main) with a new IsInteractiveTerminal() helper that consolidates the duplicate redirected-stdin/interactive-capability check. - Extract GpuFloor with MinDriver / MinCompute / CompareVersion / TryParseVersion so the UI and tests can read the floors without pulling in the probe services. NvidiaGpuPreflightCheck keeps re-exports of MinDriver/MinCompute for callers that already referenced them. - Lock gate in UserContentWatcher uses System.Threading.Lock (.NET 9) for a cheaper hot-path and compiler-enforced scoped usage. - Primary constructors for AssetDownloader, AssetPlanner, NvidiaSmiRunner, NvidiaRedistClient, HuggingFaceClient, PlanItemAssetDownloader, InstallStateLockStore where the ctor was plain parameter-to-field wiring. - ArgumentNullException.ThrowIfNull on the IBootstrapUserInterface public surface (picker, GPU warning, progress, result). Not addressed, with rationale: - JsonSerializerContext source-gen for ManifestLoader: the custom AssetSourceJsonConverter doubles the `type` field as both polymorphic discriminator and the record's Type property, which STJ's [JsonPolymorphic] does not support. Migrating would require a wire-format change that could invalidate existing install-state.lock.json files. The app is not trimmed or AOT-published, so the reflection cost is negligible (~5 ms at startup). - Shared PartialFileWriter: AssetDownloader (HTTP stream with resume + SHA256 verify + optional PostProcess) and ZipExtractor (per-entry copy loop) differ enough that a shared abstraction would be either leaky or trivial. All 571 tests (113 bootstrapper + 458 lib) pass. CSharpier clean. * refactor(assets): migrate manifest JSON to source-gen polymorphism Replaces the handwritten AssetSourceJsonConverter with System.Text.Json source-gen polymorphism. The base AssetSource record now carries [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] with one [JsonDerivedType] per concrete source, and a new ManifestJsonContext (JsonSerializerContext) drives all manifest + install-lock (de)serialization. ManifestLoader.JsonOptions is replaced by ManifestLoader.JsonContext; the runtime Type record property is marked [JsonIgnore] so STJ writes the discriminator exactly once. Wire format is unchanged: {"type":"NvidiaRedist", ...}. Benefits: trim/AOT safe, no reflection-based serialization at startup, one source of truth for the JSON options used by ManifestLoader and InstallStateLockStore. Test coverage preserved (three roundtrip/negative cases) and retargeted at the source-gen context. * test(config): poll for ConfigWriter flush instead of fixed delay AutoDiscovery_FindsAllNestedOptionsTypes was flaky on CI: six sequential Write() calls each reset the 50ms debounce Timer, then the test slept DebounceMs * 4 = 200ms. Under ThreadPool contention from parallel tests the Timer callback landed outside that window, leaving LastSaveTime null and failing the Assert.NotNull. Observed in release.yml run 24679117765 where the test wall-clock took 6s — clear sign of starved ThreadPool. Replace the fixed sleep with a 5s polling loop on LastSaveTime. Fast path still completes in ~50ms; CI-slow path doesn't false-fail. * fix(config): race in ConfigWriter.Dispose crashes process on shutdown Timer.Dispose() without a WaitHandle does not block for in-flight callbacks. If a callback was inside the try { FlushToDisk(); } block at the moment Dispose ran, the race was: 1. Callback acquired _flushLock.Wait(0), ran FlushToDisk, drained _pendingSections, but hadn't reached finally { _flushLock.Release() } yet. 2. Dispose saw _pendingSections.IsEmpty, took the else branch, called _flushLock.Dispose(). 3. Callback's Release() fired on a disposed SemaphoreSlim, throwing ObjectDisposedException on a ThreadPool thread — an unhandled exception that crashed the host. Surfaced in release.yml run 24679371862 where Write_SetsLastSaveTime aborted the test runner mid-suite. Fix: switch to the Timer.Dispose(WaitHandle) overload so Dispose blocks until every pending callback has actually returned, then it's safe to Wait / Release / Dispose _flushLock unconditionally. Collapses the two Dispose branches into one. * test(assets): widen FileSystemWatcher first-event timeout to 10s Fires_Changed_when_file_added gave the watcher only 2s to deliver the first Created event. Locally that fires in <100ms, but Windows CI runners under parallel-test load delivered it outside 2s (observed: 15s test duration in release.yml run 24679665779). 10s keeps the assertion meaningful — still catches a watcher that is truly not wired up — while absorbing CI-side FSW latency. * ci(release): seed appsettings.json from committed template PR #21 (d92367f) removed appsettings.json from git and gitignored it for secrets hygiene, but PersonaEngine.App.csproj still declares it as an AdditionalFiles entry with CopyToOutputDirectory=Always, so every 'dotnet publish' in CI has been failing with MSB3030 since that merge. Ship a sanitized appsettings.template.json (empty API keys, everything else schema-complete) and copy it to appsettings.json before the publish step. This also means the released zip now contains a usable schema next to the exe — end users fill in their own keys instead of hitting a missing-config crash on first run. * docs: rewrite INSTALLATION.md around the in-app installer, split out CONFIGURATION.md INSTALLATION.md was written for the pre-installer world — manual CUDA/cuDNN tarball copy, .NET Runtime install, espeak-ng PATH setup, Whisper download, appsettings.json hand-editing. All of that is now handled by the bootstrapper (models + NVIDIA redists, SHA-256 verified) and the self-contained publish (bundled espeak-ng, no runtime install). The old doc was actively misleading. New INSTALLATION.md covers what users actually need: - requirements (GPU + Windows + disk + network, that's it) - quick start and profile picker walk-through (Try / Stream / Build with sizes) - "turning on the better models after Build-with-it" — Build downloads the assets but defaults stay on the light stack; users have to flip Voice to Expressive (Qwen3), Listening→Recognition to Accurate (Whisper Turbo), and Avatar→Lip-Sync to Realistic (Audio2Face). This wasn't documented anywhere and is the #1 confusion vector for Build users. - CLI flags (all seven: --profile, --reinstall, --repair, --verify, --offline, --non-interactive, --skip-gpu-check) - LLM + personality configuration via their dedicated panels - viewing the avatar: built-in overlay is the primary path now (PR #21 added the always-on-top desktop overlay, no OBS required); Spout/OBS kept as the streaming path - building from source (uses the new appsettings.template.json seed) - upgrading from pre-installer builds - troubleshooting scoped to bootstrapper failure modes only (hash mismatch, network, disk, GPU gate, lock file, verify+repair loop) Air-gapped install is dropped — if someone truly needs it they can read the manifest and use --offline with a pre-seeded tree, but writing it up as a supported path was overpromising. CONFIGURATION.md is new and owns the field-by-field appsettings.json reference (previously buried mid-INSTALLATION). Documents every field under Config.*, including the sections that landed in PR #21 and earlier: Tts.ActiveEngine (kokoro/qwen3), Tts.AuditionSample, Tts.Qwen3 sampling knobs, LipSync (VBridger/Audio2Face) + Audio2FaceOptions, Overlay geometry, ConversationContext.UseCustomPrompt, updated Asr.VadMinSpeechDuration=150. README.md: fix three references to PersonaEngine.App.exe — the csproj sets <AssemblyName>PersonaEngine</AssemblyName>, so the actual exe is PersonaEngine.exe. Users following today's README hit "file not found". Also replace the obsolete "Manual / air-gapped install" pointer with a "Deeper docs" section pointing at the new INSTALLATION.md and CONFIGURATION.md, and update the features-list appsettings.json link. * docs: restructure README around the new UI, add panel screenshots Rework the README as a proper landing page for the post-installer / post-UI-rework state: hero + At-a-glance strip, collapsible TOC, profile comparison table with a "flip the switches in the UI" callout, feature cards, and a screenshot grid covering Dashboard, Voice, Listening, Avatar, and Overlay. Drops stale OBS-first framing in favour of the built-in overlay. Tones down the heavier emoji usage while keeping the project's voice. Also picks up a trivial quote/comment reflow in release.yml left behind by a post-commit formatter on the previous commit.
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
Merges
devintomainfor the v3.0.0 release.Rollup of:
Plus direct-to-dev commits: self-contained publish, OpenAI-compat endpoint validation,
ConversationSession/PhonemizerG2Pdecomposition, CSharpier v1.0.0 formatting, test expansion.Full changelog goes on the GitHub release after the tag workflow lands.
Test plan
dotnet test src/PersonaEngine/PersonaEngine.sln --configuration Releasepasses on devv3.0.0on main after merge → release workflow produces zip