TASK 51 slice 5: webcam Background section in the Layers properties — Remove/Blur PillToggles (radio-exclusive) + Strength slider; strength changes never blank the webcam (new variant inherits the same-effect frame + bitmap). TASK 51 built (Unit: bg-effects UI)

This commit is contained in:
2026-10-09 10:31:11 -07:00
parent de22983b36
commit 352f117cb2
11 changed files with 169 additions and 49 deletions
+31
View File
@@ -284,6 +284,37 @@
<TextBlock Text="{Binding SelectedElement.BorderWidth}" Foreground="#a0a0b0" FontSize="11" <TextBlock Text="{Binding SelectedElement.BorderWidth}" Foreground="#a0a0b0" FontSize="11"
VerticalAlignment="Center" Margin="8,0,0,0" Width="20"/> VerticalAlignment="Center" Margin="8,0,0,0" Width="20"/>
</DockPanel> </DockPanel>
<!-- TASK 51: background effects — two pills acting as radio buttons
(one on turns the other off; both off = no effect). -->
<TextBlock Text="Background" Foreground="#a0a0b0" FontSize="11" FontWeight="SemiBold" Margin="0,6,0,6"/>
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="Remove" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="84"/>
<ToggleButton x:Name="RemoveBackgroundPill" Style="{StaticResource PillToggle}"
IsChecked="{Binding SelectedElement.RemoveBackground, Mode=TwoWay}"
AutomationProperties.Name="Remove background"
ToolTip="Remove the background behind you — the layers below show through"
HorizontalAlignment="Left"/>
</DockPanel>
<DockPanel Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="Blur" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="84"/>
<ToggleButton x:Name="BlurBackgroundPill" Style="{StaticResource PillToggle}"
IsChecked="{Binding SelectedElement.BlurBackground, Mode=TwoWay}"
AutomationProperties.Name="Blur background"
ToolTip="Blur the background behind you"
HorizontalAlignment="Left"/>
</DockPanel>
<DockPanel Margin="0,0,0,6"
Visibility="{Binding SelectedElement.HasBackgroundEffect, Converter={StaticResource BoolToVis}}">
<TextBlock DockPanel.Dock="Left" Text="Strength" Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Width="84"/>
<TextBlock DockPanel.Dock="Right" Text="{Binding SelectedElement.BackgroundStrength, StringFormat={}{0:P0}}"
Foreground="#a0a0b0" FontSize="11" VerticalAlignment="Center" Margin="8,0,0,0" Width="34"/>
<Slider x:Name="BackgroundStrengthSlider" Minimum="0" Maximum="1"
Value="{Binding SelectedElement.BackgroundStrength, Mode=TwoWay}"
TickFrequency="0.05" IsSnapToTickEnabled="True" VerticalAlignment="Center"
AutomationProperties.Name="Background effect strength"
ToolTip="Blur: how strong the blur is · Remove: how soft the edge is"/>
</DockPanel>
</StackPanel> </StackPanel>
<!-- Countdown config --> <!-- Countdown config -->
+1 -1
View File
@@ -12,7 +12,7 @@ See [`schema.md`](../schema.md) for the memory-map conventions.
| `TopBar.xaml(.cs)` | Row 0 session surface (2026-09-22 redesign): brand + gear menu (left), one segmented REC|ON-AIR mode switch + world actions (Record: Start Recording · Stream: Go Live + Test) + reality line while running (dot/word/elapsed) + End (center), account zone — Sign In or avatar w/ Change Account/Logout context menu (right). `RefreshAvatar()`, `GearButton_Click` | | `TopBar.xaml(.cs)` | Row 0 session surface (2026-09-22 redesign): brand + gear menu (left), one segmented REC|ON-AIR mode switch + world actions (Record: Start Recording · Stream: Go Live + Test) + reality line while running (dot/word/elapsed) + End (center), account zone — Sign In or avatar w/ Change Account/Logout context menu (right). `RefreshAvatar()`, `GearButton_Click` |
| `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials) | | `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials) |
| `SceneThumbnailStrip.xaml(.cs)` | Row 0 scene-picker strip (`ItemsControl` over `Scenes`), staged-red / live-green border highlight | | `SceneThumbnailStrip.xaml(.cs)` | Row 0 scene-picker strip (`ItemsControl` over `Scenes`), staged-red / live-green border highlight |
| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept | | `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on) |
| `OverlayHost.xaml(.cs)` | Root-grid overlay dialogs: Settings / Report Bug / Feature Request / About (+ licenses + license-key entry). `BackToAbout_Click` lives here | | `OverlayHost.xaml(.cs)` | Root-grid overlay dialogs: Settings / Report Bug / Feature Request / About (+ licenses + license-key entry). `BackToAbout_Click` lives here |
| `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above | | `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above |
| `LeftPanel.xaml.cs` | Code-behind for the panel above | | `LeftPanel.xaml.cs` | Code-behind for the panel above |
+33 -36
View File
@@ -1,50 +1,47 @@
# HANDOFF — 2026-10-09 (TASK 51 slice 1 shipped: background-effects engine) # HANDOFF — 2026-10-09 (TASK 51 webcam background Remove + Blur — BUILT)
## Branch ## Branch
- main (no feature branch). Pushed through `f8dd107`; the TASK 51 slice-1 commit sits on top, - main (no feature branch). Pushed through `f8dd107`; everything after is local, **not pushed**
**not pushed** (push at the user's call). (push at the creator's call).
## Dirty files (NOT mine — do NOT commit or co-edit) ## Dirty files (NOT mine — do NOT commit)
- The in-flight "pinned chat" work unit: `Models/YouTube.cs`, `ViewModels/MainViewModel.Chat.cs`, - The in-flight "pinned chat" work unit: `Models/YouTube.cs`, `ViewModels/MainViewModel.Chat.cs`,
`Controls/LeftPanel.xaml`, untracked `Helpers/RelayCommandGeneric.cs`. untracked `Helpers/RelayCommandGeneric.cs`, and **its hunks in `Controls/LeftPanel.xaml`**
(lines ~599–610, the chat area). TASK 51 slice 5 edited the SAME file in a different region (the
webcam properties, ~line 287); only the TASK 51 hunk was staged (`git apply --cached` of that hunk
alone) — the pinned-chat hunks are still unstaged and untouched.
- The creator's untracked logo art in `Assets/` (`llamacasty text.*`, `llamacasty-text-nobg*.png`, - The creator's untracked logo art in `Assets/` (`llamacasty text.*`, `llamacasty-text-nobg*.png`,
`thin logo wordmark.png`). `thin logo wordmark.png`).
## What just happened — TASK 51 slice 1 (engine) ## What just happened — TASK 51, all five slices
- **Model:** Google's official MediaPipe Selfie Segmenter (landscape, 144×256, Apache-2.0), 1. **Engine** — Google's MediaPipe Selfie Segmenter (landscape, Apache-2.0) converted to ONNX by
converted to ONNX by our own `tools/selfie-seg/convert_tflite_to_onnx.py` and **verified against `tools/selfie-seg/` and verified vs MediaPipe's own runtime; embedded resource; onnxruntime CPU.
MediaPipe's own runtime** (`verify_against_mediapipe.py`, max |diff| 5e-5). Embedded in the exe 2. **Blur** — background at ¼ res, 3 box passes, mask-blended; Remove gets edge feather.
(`Assets/Models/selfie_segmenter_landscape.onnx`, 430 KB, in git). 3. **Config** — `WebcamSceneConfig.BackgroundEffect` + `BackgroundStrength`, persisted (additive cols).
- **Code:** `Services/BackgroundEffects/` — `IPersonSegmenter` seam, `OnnxPersonSegmenter` 4. **Hookup** — `CameraManager` renders per-scene effect **variants** (one segmentation per raw frame
(onnxruntime 1.31.0 CPU, never throws), `BackgroundEffectProcessor` (None passthrough, Remove = on a latest-frame-drop worker; raw path untouched); VM `SyncWebcamEffects()` declares/adopts them.
alpha mask, Blur = passthrough until slice 2). `Models/BackgroundEffect.cs` enum. Synthetic 720p Debug: segment 1.6 ms, Remove 3.3 ms, Blur 17.5 ms.
- **Not wired:** nothing calls it yet — no config, UI, or camera hookup. Zero risk to the live path. 5. **UI** — Background section in the webcam's Layers properties: Remove / Blur `PillToggle`s,
- **Docs:** ai.md (stale "background removal PERMANENTLY OUT" bullet replaced by the reversal + a radio-exclusive, + Strength slider (visible while an effect is on).
"Webcam background effects" section + a model-provenance licensing guard), THIRD-PARTY-NOTICES Also this session: 64-bit requirement (`PlatformTarget=x64`), GPL-3.0-or-later licensing, README
items 11 (ONNX Runtime, MIT) + 12 (MediaPipe model, Apache-2.0 with §4b change note), rewrite, TASK 55 (disk hygiene) + TASK 56 (secret scan before going public) queued.
TASK 51 file + TASKS.md row, Services/Models indexes, MyMistakes conversion recipe.
## Tests ## Tests
- Full suite **408 passed / 0 failed**, 0 build warnings. New: `BackgroundEffectEngineTests` (Remove - Full suite **412 passed / 0 failed**, 0 build warnings. New: `BackgroundEffectEngineTests` (Remove,
integration test with a fake segmenter; bundled-model contract test vs a MediaPipe reference mask, Blur, bundled-model contract), `CameraManagerTests` (variant reaches compositor + preview, raw
fixtures in `ytLive.Tests/Fixtures/`). stays raw), `LayoutStorePersistenceTests` (per-scene roundtrip + legacy migration),
`WebcamBackgroundPillTests` (radio semantics + XAML wiring).
- Known environment flakes (not regressions): `Mix_HonorsProviderGains…` (fails while system audio
plays), `RealMouseDrag…` (needs an idle desktop).
## Known issues / follow-ups ## Known issues / follow-ups
- **64-bit only (2026-10-09):** `PlatformTarget=x64` in both csproj files (verified: ytLive.dll PE - **Creator's real-camera check (TASK 51):** toggle Remove/Blur on a webcam, watch edges/lighting,
machine 0x8664, 408/408 green). TASK 48 item 2 carries the x64-only MSIX + `-r win-x64` publish. and read `startup.log` → "background effects avg … ms/frame".
- `onnxruntime.dll` is ~17 MB; an RID-less build copies every RID's native folder — TASK 48's MSIX - `onnxruntime.dll` ~17 MB; TASK 48's MSIX publish must pin `-r win-x64` (x64-only already enforced).
publish must pin win-x64. - Still stale, not rewritten: ai.md "Windows packaging" (Store IAP / branding-flash paid delta) and
- **License: GPL-3.0-or-later** (2026-10-09: `LICENSE`, `ADDITIONAL-TERMS.md` with §7 b/c/e, icon + "Monetization" sections; MARCOM.md asset table (`llama-logo.png` doesn't exist).
logo all rights reserved, AI-generated backgrounds/alert video = GPL placeholders; both files ship - TASK 56: secret-scan the full history BEFORE the Gitea repo (`gramps/LlamaCasty`) goes public.
beside the exe). Source offer = `https://llgit.llamachile.tube/gramps/LlamaCasty` (Gitea; the
creator toggles it public), in the notices header + README. **Before it goes public: scan git
history for secrets.** README rewritten (creator's voice, generic comparison, real build steps).
Still stale and NOT yet rewritten: ai.md "Windows packaging" (Store IAP / branding-flash paid
delta) and "Monetization" sections, MARCOM.md asset table (`llama-logo.png` doesn't exist).
- Carried: "Use default background" toggle still not working (from 2026-10-07). - Carried: "Use default background" toggle still not working (from 2026-10-07).
## Next step ## Next step
- TASK 51 slice 2: **Blur** in `BackgroundEffectProcessor` (separable box/gaussian, strength), one - Creator runs the app: webcam → Layers properties → Background → Remove / Blur + Strength.
integration test. Then slice 3 config + persistence (`BackgroundEffect` + `Strength`), slice 4
camera-path hookup + perf check on the creator's box, slice 5 UI: radio-exclusive Remove/Blur pill
switches + strength slider in the webcam's Layers properties.
+4
View File
@@ -26,9 +26,13 @@ public class WebcamSceneConfig : SceneElement
if (!Set(ref _backgroundEffect, value)) return; if (!Set(ref _backgroundEffect, value)) return;
Raise(nameof(RemoveBackground)); Raise(nameof(RemoveBackground));
Raise(nameof(BlurBackground)); Raise(nameof(BlurBackground));
Raise(nameof(HasBackgroundEffect));
} }
} }
/// <summary>True while Remove or Blur is on — shows the strength slider.</summary>
public bool HasBackgroundEffect => BackgroundEffect != BackgroundEffect.None;
private double _backgroundStrength = 0.5; private double _backgroundStrength = 0.5;
/// <summary>TASK 51: 0..1 — Blur amount, or Remove's edge softness (persisted).</summary> /// <summary>TASK 51: 0..1 — Blur amount, or Remove's edge softness (persisted).</summary>
public double BackgroundStrength public double BackgroundStrength
+1 -1
View File
@@ -12,7 +12,7 @@ Plain data types. No logic beyond what a property can carry. See
| `ZoomSceneElement.cs` | **Transient zoom box (TASK 52, creator-corrected 2026-10-08):** carries the box rect AND the anchored `CaptureX`/`CaptureY` (the fixed box/2 capture window set on `ActivateZoom`; moving the box never touches it). `IsZoomActive` flips `Kind` Static→Dynamic (leaves the baked base while active); never persisted (LayoutStore save skips non-Source/non-WebcamSceneConfig rows), never shown in the layers list or the element template (`IsZoomLayer` gates both). The compositor SKIPS the active element in every layer loop and draws its 2x content LAST (layer n+1) via `ZoomMagnifier`. | | `ZoomSceneElement.cs` | **Transient zoom box (TASK 52, creator-corrected 2026-10-08):** carries the box rect AND the anchored `CaptureX`/`CaptureY` (the fixed box/2 capture window set on `ActivateZoom`; moving the box never touches it). `IsZoomActive` flips `Kind` Static→Dynamic (leaves the baked base while active); never persisted (LayoutStore save skips non-Source/non-WebcamSceneConfig rows), never shown in the layers list or the element template (`IsZoomLayer` gates both). The compositor SKIPS the active element in every layer loop and draws its 2x content LAST (layer n+1) via `ZoomMagnifier`. |
| `Webcam.cs` | Singleton webcam identity: `Id`, `DeviceId`, `Name` (one row app-wide) | | `Webcam.cs` | Singleton webcam identity: `Id`, `DeviceId`, `Name` (one row app-wide) |
| `BackgroundEffect.cs` | **TASK 51:** per-webcam background effect enum `None` / `Remove` / `Blur`, carried by `WebcamSceneConfig` | | `BackgroundEffect.cs` | **TASK 51:** per-webcam background effect enum `None` / `Remove` / `Blur`, carried by `WebcamSceneConfig` |
| `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam`; **TASK 51:** `BackgroundEffect` + `BackgroundStrength` (0..1, default 0.5) + the radio-pill surface `RemoveBackground`/`BlurBackground` (one on turns the other off; both off = None) | | `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam`; **TASK 51:** `BackgroundEffect` + `BackgroundStrength` (0..1, default 0.5) + the radio-pill surface `RemoveBackground`/`BlurBackground` (one on turns the other off; both off = None) + `HasBackgroundEffect` (shows the strength slider) |
| `QualityOption.cs` | Resolution tier record `(Display, Fps, Bitrate, Width, Height)`; `Label` formats as `1080p60 (8Mbps)` — drives the bottom-bar dropdown and the output-rect math | | `QualityOption.cs` | Resolution tier record `(Display, Fps, Bitrate, Width, Height)`; `Label` formats as `1080p60 (8Mbps)` — drives the bottom-bar dropdown and the output-rect math |
| `StreamConfig.cs` | `StreamConfig` (note: `TargetBitrate`/`Resolution` defaults are stale — not yet wired to the dropdown), `StreamHealth` (bitrate/FPS/dropped/duration), `StreamStatus` enum | | `StreamConfig.cs` | `StreamConfig` (note: `TargetBitrate`/`Resolution` defaults are stale — not yet wired to the dropdown), `StreamHealth` (bitrate/FPS/dropped/duration), `StreamStatus` enum |
| `MicStatus.cs` | Footer mic indicator: `NotConnected` / `Connected` / `Problem` — the state behind the MIC button's status dot (red = no device/capture off, green = capturing, yellow = a requested mic failed: in use/unplugged) | | `MicStatus.cs` | Footer mic indicator: `NotConnected` / `Connected` / `Problem` — the state behind the MIC button's status dot (red = no device/capture off, green = capturing, yellow = a requested mic failed: in use/unplugged) |
+14 -4
View File
@@ -380,14 +380,24 @@ public sealed class CameraManager : IDisposable
}, DispatcherPriority.Render); }, DispatcherPriority.Render);
} }
// Caller holds _gate. // Caller holds _gate. A new key inherits the last frame + bitmap of a dropped variant with
// the SAME effect (a strength change), so neither the stream nor the preview blanks for a
// frame while the creator drags the strength slider.
private static void SyncVariants(CameraSession session, HashSet<WebcamEffectKey> keys) private static void SyncVariants(CameraSession session, HashSet<WebcamEffectKey> keys)
{ {
foreach (var stale in session.Effects.Keys.Where(k => !keys.Contains(k)).ToList()) var dropped = session.Effects.Where(kv => !keys.Contains(kv.Key)).ToList();
foreach (var (stale, _) in dropped)
session.Effects.Remove(stale); session.Effects.Remove(stale);
foreach (var key in keys) foreach (var key in keys)
if (!session.Effects.ContainsKey(key)) {
session.Effects[key] = new EffectVariant(); if (session.Effects.ContainsKey(key)) continue;
var heir = dropped.FirstOrDefault(kv => kv.Key.Effect == key.Effect).Value;
session.Effects[key] = new EffectVariant
{
LatestFrame = heir?.LatestFrame,
PreviewBitmap = heir?.PreviewBitmap,
};
}
} }
private void ScheduleEffects(CameraSession session) private void ScheduleEffects(CameraSession session)
+1 -1
View File
@@ -66,7 +66,7 @@
| 48 | Distribution & packaging: **MSIX + Store IAP** | 🔶 In progress — **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28** | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) | | 48 | Distribution & packaging: **MSIX + Store IAP** | 🔶 In progress — **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28** | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) |
| 49 | Chat profanity filter (local, opt-in, non-persistent, user word list) | ☐ Queued (2026-09-27) | [`TASKS/task-49-chat-profanity-filter.md`](TASKS/task-49-chat-profanity-filter.md) | | 49 | Chat profanity filter (local, opt-in, non-persistent, user word list) | ☐ Queued (2026-09-27) | [`TASKS/task-49-chat-profanity-filter.md`](TASKS/task-49-chat-profanity-filter.md) |
| 50 | One-button "Publish this video" (resumable upload of the local recording) | ☐ Queued (2026-09-28) | [`TASKS/task-50-publish-recording.md`](TASKS/task-50-publish-recording.md) | | 50 | One-button "Publish this video" (resumable upload of the local recording) | ☐ Queued (2026-09-28) | [`TASKS/task-50-publish-recording.md`](TASKS/task-50-publish-recording.md) |
| 51 | **Webcam background effects: Remove + Blur** | 🔶 **v1 requirement — in progress**: slice 1 engine shipped 2026-10-09 (bundled MediaPipe Selfie Segmenter → ONNX, verified vs Google's runtime; Remove); next: Blur → config → camera hookup → UI | [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md) | | 51 | **Webcam background effects: Remove + Blur** | ✅ **Built 2026-10-09** (v1 requirement) — bundled MediaPipe Selfie Segmenter → ONNX (verified vs Google's runtime), Remove + Blur with strength, per-scene config persisted, one processing point in CameraManager, radio-pill UI in the webcam's Layers properties. Pending: creator's real-camera check | [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md) |
| 52 | **Zoom box (anchored 2× magnifier)** | ✅ Done — shipped 2026-10-08 (3 creator corrections during the build: loupe → anchored capture over the full composite, drawn last → never shows the dim shading) | [`TASKS/task-52-zoom-box.md`](TASKS/task-52-zoom-box.md) | | 52 | **Zoom box (anchored 2× magnifier)** | ✅ Done — shipped 2026-10-08 (3 creator corrections during the build: loupe → anchored capture over the full composite, drawn last → never shows the dim shading) | [`TASKS/task-52-zoom-box.md`](TASKS/task-52-zoom-box.md) |
| 53 | **Output-format verification: Shorts/TikTok geometry (9:16)** | ☐ Queued (2026-10-08) — output formats never tested end-to-end; pin + prove the vertical geometry | [`TASKS/task-53-output-formats.md`](TASKS/task-53-output-formats.md) | | 53 | **Output-format verification: Shorts/TikTok geometry (9:16)** | ☐ Queued (2026-10-08) — output formats never tested end-to-end; pin + prove the vertical geometry | [`TASKS/task-53-output-formats.md`](TASKS/task-53-output-formats.md) |
| 54 | **Marcom round: free-forever positioning + merch storefront** | 🔶 In progress — llamacasty.com copy live 2026-10-08 (pricing stripped); merch POD + app About links queued | [`TASKS/task-54-marcom-free-merch.md`](TASKS/task-54-marcom-free-merch.md) | | 54 | **Marcom round: free-forever positioning + merch storefront** | 🔶 In progress — llamacasty.com copy live 2026-10-08 (pricing stripped); merch POD + app About links queued | [`TASKS/task-54-marcom-free-merch.md`](TASKS/task-54-marcom-free-merch.md) |
+5 -4
View File
@@ -4,8 +4,9 @@
> **Booked 2026-10-08 by creator ruling** — *"we're doing the bg removal and blur for v1 - book it, Danno"*. > **Booked 2026-10-08 by creator ruling** — *"we're doing the bg removal and blur for v1 - book it, Danno"*.
> This **reverses** the 2026-10-05 "background removal: out of product" exclusion (TASK 3 item 19). > This **reverses** the 2026-10-05 "background removal: out of product" exclusion (TASK 3 item 19).
**Status:** 🔶 In progress — **slice 1 (engine: bundled model + runner + Remove) shipped 2026-10-09**. **Status:** ✅ **Built 2026-10-09 — all five slices shipped** (engine, Blur, config + persistence,
Next: slice 2 (Blur), then config + persistence, then the camera-path hookup + UI. camera hookup, UI). Remaining: the creator's manual check on a real camera (quality, lighting,
`startup.log` perf line).
**Goal:** per-webcam **Background Remove** and **Background Blur** on the live webcam layer, **Goal:** per-webcam **Background Remove** and **Background Blur** on the live webcam layer,
selected per element in its properties panel, composited into stream + recording like any selected per element in its properties panel, composited into stream + recording like any
@@ -72,7 +73,7 @@ delivery shape changed:
- ✅ `WebcamSceneConfig` gains `BackgroundEffect` (`None | Remove | Blur`) + `BackgroundStrength` (slice 3). - ✅ `WebcamSceneConfig` gains `BackgroundEffect` (`None | Remove | Blur`) + `BackgroundStrength` (slice 3).
- ✅ Persist via the existing `WebcamSceneConfig` table path in `LayoutStore.Save.cs`/ - ✅ Persist via the existing `WebcamSceneConfig` table path in `LayoutStore.Save.cs`/
`Load.cs` (new columns + migration). Follow `LayoutStore.Migrations.cs` conventions. `Load.cs` (new columns + migration). Follow `LayoutStore.Migrations.cs` conventions.
- ☐ **Properties UI (creator spec 2026-10-09):** in the webcam's properties in the **Layers - ✅ **Properties UI (creator spec 2026-10-09, slice 5):** in the webcam's properties in the **Layers
panel**, two **pill switches** — "Remove background" and "Blur background" — using the existing panel**, two **pill switches** — "Remove background" and "Blur background" — using the existing
`PillToggle` style (`Themes/Controls.xaml`, already used in `Controls/LeftPanel.xaml`). They act `PillToggle` style (`Themes/Controls.xaml`, already used in `Controls/LeftPanel.xaml`). They act
as **radio buttons**: turning one on turns the other off; at most one is active; both off = as **radio buttons**: turning one on turns the other off; at most one is active; both off =
@@ -104,7 +105,7 @@ delivery shape changed:
- ✅ Separate work unit after the zoom build (zoom shipped 2026-10-08). - ✅ Separate work unit after the zoom build (zoom shipped 2026-10-08).
- Slice order: 1 engine ✅ → 2 Blur ✅ → 3 config + persistence ✅ → 4 camera-path hookup ✅ (one - Slice order: 1 engine ✅ → 2 Blur ✅ → 3 config + persistence ✅ → 4 camera-path hookup ✅ (one
processing point, both consumers) + perf check → 5 properties-panel UI. processing point, both consumers) + perf check → 5 properties-panel UI ✅.
## Related (not duplicated here) ## Related (not duplicated here)
+9 -2
View File
@@ -244,11 +244,18 @@ public partial class MainViewModel
config.PropertyChanged += OnWebcamConfigEffectChanged; config.PropertyChanged += OnWebcamConfigEffectChanged;
var key = EffectKeyFor(config); var key = EffectKeyFor(config);
var bitmap = _cameraManager.GetPreviewBitmap(_webcam.DeviceId, key); var bitmap = _cameraManager.GetPreviewBitmap(_webcam.DeviceId, key);
if (bitmap != null || key.Effect != BackgroundEffect.None) // A strength change within the same effect keeps showing the previous variant's
config.VideoImageSource = bitmap; // last frame until the new one renders (no blank flicker while dragging the slider);
// switching INTO an effect shows nothing first, so Remove never flashes the room.
var sameEffect = _shownEffect.TryGetValue(config, out var shown) && shown.Effect == key.Effect;
_shownEffect[config] = key;
if (bitmap != null) config.VideoImageSource = bitmap;
else if (key.Effect != BackgroundEffect.None && !sameEffect) config.VideoImageSource = null;
} }
} }
private readonly Dictionary<WebcamSceneConfig, WebcamEffectKey> _shownEffect = new();
private void OnWebcamConfigEffectChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) private void OnWebcamConfigEffectChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{ {
if (e.PropertyName is not (nameof(WebcamSceneConfig.BackgroundEffect) or nameof(WebcamSceneConfig.BackgroundStrength))) if (e.PropertyName is not (nameof(WebcamSceneConfig.BackgroundEffect) or nameof(WebcamSceneConfig.BackgroundStrength)))
+7
View File
@@ -663,6 +663,13 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr
per pixel) — the first naive per-pixel 2-D bilinear cost 46/80 ms. `CameraManager` logs the real per pixel) — the first naive per-pixel 2-D bilinear cost 46/80 ms. `CameraManager` logs the real
average once per session after 120 processed frames (`startup.log`: "background effects avg …"). average once per session after 120 processed frames (`startup.log`: "background effects avg …").
`MainViewModel.PersonSegmenterOverride` is the hermetic-test seam. `MainViewModel.PersonSegmenterOverride` is the hermetic-test seam.
- **UI (slice 5, creator spec 2026-10-09):** the webcam's properties in the Layers panel
(`Controls/LeftPanel.xaml`) gain a **Background** section — two `PillToggle` switches, Remove and
Blur, bound to `RemoveBackground`/`BlurBackground` (radio-exclusive: one on turns the other off,
turning the active one off = None) and a Strength slider (0..1, 5% snaps, visible only while an
effect is on; Blur = amount, Remove = edge softness). Dragging the slider never blanks the webcam:
a new strength variant inherits the dropped same-effect variant's last frame + bitmap
(`CameraManager.SyncVariants`), and the VM keeps the shown bitmap until the new one renders.
- **Tests:** `BackgroundEffectEngineTests` — the Remove integration test (fake segmenter), and a - **Tests:** `BackgroundEffectEngineTests` — the Remove integration test (fake segmenter), and a
model contract test that runs the BUNDLED model through the C# preprocessing on a fixture and model contract test that runs the BUNDLED model through the C# preprocessing on a fixture and
matches the MediaPipe-produced reference mask (≥99.5% agreement; fixture = MediaPipe's public test matches the MediaPipe-produced reference mask (≥99.5% agreement; fixture = MediaPipe's public test
+63
View File
@@ -0,0 +1,63 @@
using System.Linq;
using System.Xml.Linq;
using Xunit;
using ytLive.Models;
namespace ytLive.Tests;
/// <summary>TASK 51 slice 5 — the creator's UI spec (2026-10-09): Remove and Blur are two pill
/// switches in the webcam's Layers properties that act as radio buttons (one on turns the other
/// off; both off = no effect), plus a strength slider.</summary>
public class WebcamBackgroundPillTests
{
[Fact]
public void Remove_and_Blur_pills_are_radio_exclusive_and_wired_in_the_webcam_properties()
{
// Behaviour: the bools the pills bind to.
var config = new WebcamSceneConfig();
var raised = new System.Collections.Generic.List<string?>();
config.PropertyChanged += (_, e) => raised.Add(e.PropertyName);
config.RemoveBackground = true;
Assert.Equal(BackgroundEffect.Remove, config.BackgroundEffect);
Assert.True(config.HasBackgroundEffect);
raised.Clear();
config.BlurBackground = true; // turning Blur on turns Remove off
Assert.Equal(BackgroundEffect.Blur, config.BackgroundEffect);
Assert.False(config.RemoveBackground);
Assert.Contains(nameof(WebcamSceneConfig.RemoveBackground), raised); // Remove pill un-checks
Assert.Contains(nameof(WebcamSceneConfig.BlurBackground), raised);
config.RemoveBackground = false; // turning the INACTIVE pill off changes nothing
Assert.Equal(BackgroundEffect.Blur, config.BackgroundEffect);
config.BlurBackground = false; // turning the active pill off = None
Assert.Equal(BackgroundEffect.None, config.BackgroundEffect);
Assert.False(config.HasBackgroundEffect);
// Wiring: the two PillToggle switches + the strength slider live in the webcam's
// properties section of the Layers panel, bound to those properties.
XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml";
var doc = XDocument.Load(SourceAssert.RepoPath("Controls", "LeftPanel.xaml"));
var webcamPanel = doc.Descendants()
.Single(e => e.Name.LocalName == "StackPanel"
&& ((string?)e.Attribute("Visibility") ?? "").Contains("SelectedElement.IsWebcam"));
XElement Named(string name) => webcamPanel.Descendants().Single(e => (string?)e.Attribute(x + "Name") == name);
foreach (var (pill, property) in new[] { ("RemoveBackgroundPill", "RemoveBackground"), ("BlurBackgroundPill", "BlurBackground") })
{
var toggle = Named(pill);
Assert.Equal("ToggleButton", toggle.Name.LocalName);
Assert.Contains("PillToggle", (string?)toggle.Attribute("Style"));
Assert.Contains($"SelectedElement.{property}", (string?)toggle.Attribute("IsChecked"));
Assert.Contains("TwoWay", (string?)toggle.Attribute("IsChecked"));
}
var slider = Named("BackgroundStrengthSlider");
Assert.Contains("SelectedElement.BackgroundStrength", (string?)slider.Attribute("Value"));
Assert.Equal("0", (string?)slider.Attribute("Minimum"));
Assert.Equal("1", (string?)slider.Attribute("Maximum"));
}
}