Skip to content

WorkWingman — Study Visuals (Technical)

Optional, original AI-generated illustrations for a study concept — the illustrated-explanation spirit of books like Grokking Algorithms (a diagram that makes an idea click at a glance), never their artwork — plus an optional sharpen/enhance step and an optional image-to-video "animate" step. Feeds prep-studio flashcards, the Claude Project study workspace, and the NotebookLM export as optional visual enrichment.

Friendlier version: ../plain/study-visuals.md

This feature generates ONLY original illustrations from a text prompt it builds itself. It never reproduces, requests, or approximates any copyrighted book's actual images or figures.

The inspiration for "visual study aids" is the pedagogy of illustrated-explanation books — plain, labeled diagrams that make a hard concept click — not their artwork. Concretely:

  • ConceptVisualPromptBuilder (src/WorkWingman.Infrastructure/Visuals/ConceptVisualPromptBuilder.cs) describes the concept generically (what to depict, generic style words like "clean flat educational diagram," "labeled," "high contrast") plus the study context. It never names a book, author, or illustrator, never says "in the style of <work>," and never asks to "recreate figure N."
  • Every prompt ends with an explicit instruction to the image model: "Depict the concept itself only — do not include any book covers, page layouts, logos, or any other copyrighted artwork; this must be a wholly original image."
  • ConceptVisualPromptBuilderTests asserts, as a regression guard, that no generated prompt ever contains a list of known copyrighted-work/illustrator names (deliberately including Grokking Algorithms and its author, to prove the inspiration never becomes a literal reference) or phrases like "in the style of" / "recreate figure."

If a future change to the prompt builder is tempted to reference a specific book, illustrator, or "style of X," stop — that is exactly the line this feature is built not to cross.

Provider tiers

Every provider below is paid, BYO-key, and optional — the user brings their own API key from their own account with that provider, config-gated the standard way (absent key → that source is simply unavailable; the rest of Study keeps working). WorkWingman never calls any of these APIs without a user-provided key, and never pays for or shares access to a key on the user's behalf.

Tier Provider What it does Cost
Generate (mid) Google "Nano Banana" (Gemini 2.5 Flash Image) Text-to-image, original illustration ~$0.02–$0.04/image
Generate (mid) OpenAI (gpt-image-1.5) Text-to-image, original illustration ~$0.01–$0.25/image
Enhance (pricier, optional add-on) Magnific (image-upscaler API) Sharpens/upscales an already-generated illustration — not a generator credit-subscription, ~$0.05/standard upscale-equivalent
Video (built) Magnific (image-to-video / Kling API) Animates an already-generated illustration into a short original clip credit-subscription (Premium 20K / Premium+ 45K / Pro 300K credits per month); video costs more credits per job than an upscale
Video (documented only, not built) Revid.ai Paid AI video generation from a script $39–$199/month subscription, credit-metered

Research verdict: the two image-GENERATION APIs

Both are paid, BYO-key services — the user brings their own API key from their own account with that provider. WorkWingman never calls either API without a user-provided key, and never pays for or shares access to a key on the user's behalf.

Google "Nano Banana" = Gemini 2.5 Flash Image

Verified against ai.google.dev, 2026:

Endpoint POST https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
Auth Header x-goog-api-key: {key}
Model id gemini-2.5-flash-image (this is the model nicknamed "Nano Banana"; Google has since introduced newer "Nano Banana 2" generations — the model id is a single constant, GeminiImageSource.DefaultModel, so bumping it later is a one-line change)
Request { "contents": [{ "parts": [{ "text": prompt }] }] }
Response candidates[0].content.parts[] — the image part carries inlineData.data (base64) + inlineData.mimeType; a text part may also be present and is ignored
Cost (verified 2026) ~$0.039/image standard tier (1290 output tokens × $30/1M output-token rate), ~$0.0195/image via the batch API — paid, BYO key

OpenAI images = the gpt-image-* family

Verified against developers.openai.com, 2026:

Endpoint POST https://api.openai.com/v1/images/generations
Auth Header Authorization: Bearer {key}
Model id gpt-image-1.5 (OpenAiImageSource.DefaultModel)
Request { "model", "prompt", "n", "size", "response_format": "b64_json" }
Response { "data": [ { "b64_json": "..." } ] }
Cost (verified 2026) roughly $0.01–$0.25/image depending on quality tier and resolution — paid, BYO key

Model-lifecycle note (important, checked live during this build): OpenAI's gpt-image-1 is scheduled for deprecation (Oct 23, 2026), and DALL·E 2/3 were already removed from the API in May 2026. gpt-image-2 is the current flagship. This integration deliberately targets gpt-image-1.5 — a current, non-deprecated model — rather than the gpt-image-1 name mentioned in the original brief, precisely because that name is being retired. The request/response contract (endpoint, JSON shape) is stable across the whole gpt-image-* family, so bumping the model id later (e.g. to gpt-image-2) is a one-line change in OpenAiImageSource.DefaultModel.

Research verdict: Magnific (image enhance/upscale — built)

Magnific (now unified under Freepik's platform, as of an April 2026 domain redirect) is best known for high-resolution upscaling/enhancement, not from-scratch text-to-image generation — its API surface (verified via the machine-readable docs.magnific.com/llms.txt) includes both generation endpoints (Mystic, Flux, Seedream models) and dedicated image-editing endpoints (upscaler-creative, upscaler-precision, relight, style-transfer). Since this feature already has two from-scratch generators, Magnific is wired as the sharpen/enhance step on top of an already-generated illustration — matching what it's actually known for and avoiding a third redundant generator.

Endpoint POST https://api.magnific.com/v1/ai/image-upscaler (create), GET https://api.magnific.com/v1/ai/image-upscaler/{task_id} (poll status)
Auth Header x-magnific-api-key: {key}
Request { "image": base64, "scale_factor": "2x" } (input is the already-generated illustration's bytes, base64-encoded)
Response Asynchronous task model: { "data": { "task_id", "status": "IN_PROGRESS\|COMPLETED\|FAILED", "generated": [url,...] } } — the create call returns immediately with a task_id; the adapter polls the status endpoint until COMPLETED/FAILED or a bounded poll budget is exhausted, then downloads the first generated image URL
Cost (verified 2026) Credit-subscription model: Premium (20K credits/mo), Premium+ (45K credits/mo), Pro (300K credits/mo, 3 keys); roughly $0.05 per standard upscale in credit terms

Pixabay was evaluated and rejected for this feature. Pixabay's public API (pixabay.com/api/docs) is a stock-media search endpoint (find existing royalty-free photos and videos), not an AI image generator — Pixabay's AI image generator is a website-only feature with no documented public/programmatic API. Since this feature's premise is original AI illustration (not stock-photo search), and there was no verifiable generation API to integrate, Pixabay was not added in any form.

Research verdict: Magnific video (image-to-video — BUILT)

Magnific DOES offer a video API. Its llms.txt (docs.magnific.com/llms.txt) lists a large set of async, task-based video endpoints — image-to-video (Kling v2.1/2.5/2.6-pro, MiniMax Hailuo, WAN i2v, RunWay Gen4, Seedance, PixVerse, OmniHuman) and text-to-video (WAN t2v, LTX). They share the exact same auth (x-magnific-api-key) and async response shape ({ data: { task_id, status, generated: [url,...] } }) as the image-upscaler endpoint that was verified in full. So this feature wires Magnific video as the video tier: it animates an already-generated illustration into a short original concept clip (image-to-video, kling-v2-1-pro — a bumpable constant).

Endpoint POST https://api.magnific.com/v1/ai/{model} (create), GET .../{model}/{task_id} (poll)
Auth Header x-magnific-api-key: {key} (a separate BYO key row, magnific-video)
Request { "image": base64, "prompt": text } (the illustration's bytes + its concept prompt)
Response Async: { data: { task_id, status, generated: [videoUrl,...] } } — poll to COMPLETED/FAILED, then download the mp4
Cost Credit-subscription (Premium/Premium+/Pro tiers); a video job costs more credits than an image upscale

Verification caveat (documented honestly): the exact per-model optional request fields (duration, aspect ratio) for the specific video endpoints were not individually retrievable from the public docs at build time (the llms.txt index lists the endpoint paths; a concrete video request-body example wasn't in the fetched llms-full.txt). Because the auth, the async task response, and the poll pattern are verified-identical to the upscaler, MagnificVideoSource sends the conservative, task-API-consistent { image, prompt } body and the model id is a single bumpable constant — the same forward-compatible approach used for the image model ids. If a specific model's full schema is later confirmed, add the optional fields there.

Copyright: the input is an ORIGINAL illustration this app generated from a generic concept prompt — animating it yields an original clip, never a copy of copyrighted footage. StudyVideo carries the same Concept/Prompt transparency fields as StudyVisual, and video generation is gated + path-free + local-storage exactly like the image tiers.

Research verdict: Revid.ai (AI video — documented only, not built)

Revid.ai is a paid AI video-generation product (script/link/recording → short-form video, avatars, voiceovers). Verified via revid.ai/docs. It is a different shape from the Magnific video path built above — text/script-to-video with voice/captions/avatars rather than animating an existing illustration — and is the priciest tier (a monthly subscription, not simple per-call pricing). It is documented here but deliberately not built; a future contributor could add it behind a revid-video provider id following the same async-create-and-poll shape.

Base URL https://www.revid.ai/api/public/v3
Auth Header key: {your-api-key}
Endpoint POST /render{ "workflow": "script-to-video", "source": { "text": "..." }, "webhookUrl": "..." }
Response Asynchronous: returns a pid, poll GET /status?pid=... or receive a webhook callback
Cost (verified 2026) Subscription-gated API access: Growth $39/month (2,000 credits/mo) or Ultra $199/month (12,000 credits/mo); ~10 credits + generation charges per render

This is documented as an optional future stretch and deliberately not built in this feature: it is the priciest tier (monthly subscription, not simple per-call pricing), video adds meaningfully more surface area (script generation, voice, captions, rendering time), and the brief's scope for this pass is illustrations. A future contributor adding Revid.ai should follow the same shape as MagnificSource/IStudyVisualEnhancer — async create + poll, config-gated on a revid-video provider id — but this branch does not implement it.

The generator seam

public interface IStudyVisualGenerator
{
    StudyVisualSource Source { get; }
    Task<bool> IsAvailableAsync(CancellationToken ct = default);
    Task<StudyVisualResult> GenerateAsync(ConceptVisualRequest request, CancellationToken ct = default);
}

(src/WorkWingman.Core/Interfaces/IStudyVisualGenerator.cs)

Two adapters implement it:

Both follow the same shape: resolve the key via IApiKeyProvider, build the prompt via ConceptVisualPromptBuilder, POST through Flurl, decode the base64 image, save it locally via StudyVisualStore, and return a StudyVisualResult. Any failure — no key, HTTP error, malformed/empty response — degrades to StudyVisualResult.Unavailable(message); neither adapter ever throws out of GenerateAsync.

No retries on the paid POST. The generation call uses ResiliencePipelines.NoRetry (timeout only), not the retrying Default pipeline every other REST client uses — because a generation request is non-idempotent and billed. If it timed out or hit a transient 5xx after the provider had already accepted the job, a retry could create several paid images for one click. A failure surfaces once and degrades gracefully. (Magnific's safe idempotent status-poll and image-download GETs keep the retrying Default pipeline; only its billed create POST is NoRetry.)

StudyVisualService (src/WorkWingman.Infrastructure/Visuals/StudyVisualService.cs) orchestrates both sources: GetAvailableSourcesAsync reports which ones currently have a usable key (drives the frontend gate), GenerateAsync/RegenerateAsync/DeleteAsync persist the resulting gallery in a single local JSON index (study-visuals) via the existing LocalJsonStore, scoped per study item id.

The enhancer seam (Magnific)

Sharpening is a distinct operation from generating — it takes an existing StudyVisual and produces a new, sharper one — so it gets its own small interface rather than overloading IStudyVisualGenerator:

public interface IStudyVisualEnhancer
{
    Task<bool> IsAvailableAsync(CancellationToken ct = default);
    Task<StudyVisualResult> EnhanceAsync(StudyVisual original, CancellationToken ct = default);
}

(src/WorkWingman.Core/Interfaces/IStudyVisualEnhancer.cs)

MagnificSource (src/WorkWingman.Infrastructure/Visuals/MagnificSource.cs) is the only implementation today. Enhancing never replaces or deletes the original — the enhanced result is a new StudyVisual with Source = MagnificEnhanced and EnhancedFromVisualId pointing back at the original, so both stay in the gallery for the user to compare. StudyVisualService.EnhanceAsync(visualId) looks up the stored original, tries each registered (and available) enhancer in order until one succeeds, and persists only a successful result — same "never persist a failure" rule as generation.

The video seam (Magnific image-to-video)

The video tier is a third small interface — output is a video file, not an image, and it's the priciest, most optional aid:

public interface IStudyVideoGenerator
{
    StudyVideoSource Source { get; }
    Task<bool> IsAvailableAsync(CancellationToken ct = default);
    Task<StudyVideoResult> GenerateAsync(StudyVisual original, CancellationToken ct = default);
}

(src/WorkWingman.Core/Interfaces/IStudyVideoGenerator.cs)

MagnificVideoSource (src/WorkWingman.Infrastructure/Visuals/MagnificVideoSource.cs) animates a stored illustration into a short clip via Magnific's image-to-video (Kling) endpoint, following the same async create-then-poll-then-download shape as the upscaler (the billed create POST uses the NoRetry pipeline; the poll + download use Default). The result is a StudyVideo (source MagnificVideo, FromVisualId set) persisted in a separate study-videos JSON index with its own concurrency gate; the image gallery is untouched. StudyVisualService.GenerateVideoAsync / GetVideosAsync / DeleteVideoAsync mirror the image methods.

Config-gating: never call a paid API without a user key

IApiKeyProvider (src/WorkWingman.Core/Interfaces/IApiKeyProvider.cs) is the contract config-gated adapters depend on: null means "not configured," and a missing key never throws (the vault-first, config-fallback routing below is the concrete implementation's behavior, not part of the interface). This feature was built standalone in its own worktree behind a tiny config-only stub; that stub was deleted when feature-apikey-onboarding merged, and the four visual sources now depend on the real vault-first implementation, ApiKeyProvider — vault entry first, then an IConfigurationLookup fallback over appsettings + WorkWingman:* config/env keys, and never the config fallback in a sandbox environment (a fresh-user world must not inherit a developer's real keys). The four provider ids live in ApiKeyProviderRegistry, each with its own config key:

Provider id Registry ConfigKey Free tier
gemini-image WorkWingman:GeminiImageApiKey Yes (rate-limited)
openai-image WorkWingman:OpenAiImageApiKey No
magnific-image WorkWingman:MagnificImageApiKey No
magnific-video WorkWingman:MagnificVideoApiKey No

(A fifth, revid-video, could be added the same way if Revid.ai is ever built — see the Revid.ai research verdict above.) The image and video Magnific keys are separate rows deliberately: the same Magnific account key can be pasted into both, but keeping them distinct lets a user enable image sharpening without opting into the pricier video tier.

Absent key → no-op, not a free call. Both GenerateAsync implementations check the key before building a request. No key configured for a source means that source is simply excluded from GetAvailableSourcesAsync, and a direct GenerateAsync call against an unconfigured source returns StudyVisualResult.Unavailable("Add a Gemini/OpenAI API key in Integrations to enable …") — never a network call. StudyVisualGeneratorTests asserts the HttpTest call log is empty in this case for both adapters.

Local storage

StudyVisualStore (src/WorkWingman.Infrastructure/Visuals/StudyVisualStore.cs) writes generated image bytes under the existing app data directory (%USERPROFILE%\Wingman\data\study-visuals\, alongside every other local-first JSON store in this app) and returns the local file path. StudyVisualService persists a StudyVisual record — id, concept, the exact prompt sent, source, local path, the study item it's attached to, and a timestamp — in a single JSON index (study-visuals.json), following the same LocalJsonStore pattern as every other collection in this codebase.

public class StudyVisual
{
    public string Id { get; set; }
    public string Concept { get; set; }
    public string Prompt { get; set; }       // exact prompt sent, kept for transparency/regenerate
    public StudyVisualSource Source { get; set; }
    public string LocalImagePath { get; set; }
    public string? StudyItemId { get; set; }
    public DateTimeOffset AsOf { get; set; }
}

Privacy: what actually leaves the machine

Generation (Gemini/OpenAI): only the concept prompt text goes to the user's own chosen image API — nothing else about the user, ever. The request carries the generated prompt string (a generic description of the concept; the app sends an empty context, so it's just the typed concept) plus the user's own API key. No resume data, no job description, no personal profile fields, no other app data. The image comes back and is written to local disk.

Enhance / Animate (Magnific): these are honestly different — to sharpen or animate an illustration, the already-generated illustration's image bytes (plus its prompt) are uploaded to the user's own Magnific account to process. That is the generated study image, not the user's personal data — but it is an upload, so the UI copy is explicit about it (a separate line appears whenever a Magnific key is configured) rather than claiming "nothing leaves the machine." Still the user's own key, own account, own cost; still no resume/JD/profile data.

In all cases the result (image or video) is written to local disk and is never uploaded anywhere beyond the single API call to the user's own chosen provider.

Integration wiring (feeds flashcards / Claude Project / NotebookLM)

This feature defines small, stable input contracts other in-flight branches integrate against:

  • Study feature (this branch, wired today): Study frontend component reads GET /api/study-visuals/available-sources to gate the "Visual study aids" section and GET /api/study-visuals/{studyItemId} for the per-job gallery, keyed by jobId as the studyItemId.
  • prep-studio flashcards (parallel branch, feature-prep-studio): a flashcard is a natural studyItemId — pass the flashcard id as ConceptVisualRequest.StudyItemId and the flashcard's front-text as Concept to attach an illustration to that specific card. Documented here as the wiring contract; the prep-studio branch owns rendering the attached visual on its own card UI.
  • Claude Project / NotebookLM export (parallel branches, feature-notebooklm): when building a study pack for export, include each StudyVisual.LocalImagePath + Concept alongside the existing study resources so the grounded workspace can reference the illustration file; the Prompt field is kept specifically so an export can show "this is what was asked for" next to the image for full transparency.

None of the above requires this branch to depend on the others' code — the contract is just "pass a studyItemId string that means something to you" and "read StudyVisual.LocalImagePath off disk," which is why this branch compiles and tests standalone.

API

Route Method Purpose
/api/study-visuals/available-sources GET Which sources (GeminiNanoBanana, OpenAiImages) currently have a usable key
/api/study-visuals/enhancement-available GET Whether a Magnific key is configured (drives the "Sharpen" button)
/api/study-visuals/{studyItemId} GET The gallery for one study item, newest first
/api/study-visuals/{visualId}/image GET Streams the visual's image bytes (never a raw file:// path — see below)
/api/study-visuals/generate POST ConceptVisualRequest body → StudyVisualResult
/api/study-visuals/{visualId}/regenerate POST Re-run the same concept/source, adds alongside (does not replace) the prior visual
/api/study-visuals/{visualId}/enhance POST Sharpen/upscale via Magnific, adds alongside (does not replace) the original
/api/study-visuals/{visualId} DELETE Remove the visual and its local image file
/api/study-visuals/video-available GET Whether a Magnific video key is configured (drives the "Animate" button)
/api/study-visuals/videos/{studyItemId} GET The generated-video gallery for one study item, newest first (path-free)
/api/study-visuals/video/{videoId}/file GET Streams the video's mp4 bytes (never a raw file:// path)
/api/study-visuals/{visualId}/video POST Animate a visual into a short clip via Magnific → StudyVideoResult
/api/study-visuals/video/{videoId} DELETE Remove the video and its local file

Image serving, not raw file paths. The frontend renders each visual via /api/study-visuals/{visualId}/image rather than binding a file:/// URL directly — browsers (and the packaged Electron renderer) don't load arbitrary file:// URLs from a web page, so a raw local path would silently fail to render. StudyVisualsController.GetImage also defends in depth: it resolves the stored path to an absolute path and refuses to serve anything that doesn't resolve under StudyVisualStore.VisualsDirectory, even though every path stored today was written by StudyVisualStore.SaveAsync itself.

The absolute local path never crosses the API. The internal StudyVisual model carries LocalImagePath, but every API response projects to a path-free view (StudyVisualView / StudyVisualResultView) that omits it entirely — the gallery GET, and the generate/regenerate/ enhance results. Because the loopback API allows any origin, this keeps a page that can reach the API from learning absolute local filesystem paths; the renderer only ever needs the image endpoint URL (built from the visual's id), never a path.

Token gating. Every endpoint that spends the user's paid quota or mutates the gallery (generate, regenerate, enhance, delete) is gated on the per-launch ApiToken — the same protection the dependency installer uses — so a hostile page in the same browser cannot drive a cross-origin POST against the loopback API and burn the user's Gemini/OpenAI/Magnific credits. GET (list/detect/image) stays open. The Electron preload forwards the token as the X-WorkWingman-Token header (see the frontend apiTokenInterceptor); the Angular dev origin http://localhost:4200 is also trusted for development.

Frontend

Study (frontend/src/app/features/study/study.ts) adds a "Visual study aids (optional)" card. visualsEnabled (a computed signal) is true only when getAvailableVisualSources() returns at least one source; the disabled state shows "Connect a Gemini or OpenAI key in Integrations to enable illustrations for study concepts" instead of the generate control. Only the generate/regenerate controls are gated on a configured key — an existing gallery of previously-generated visuals and videos stays visible (and deletable, a local op) even if the key is later removed. Copy is explicit that this is optional, uses the user's own subscription, the result is an original AI illustration that "may contain errors — verify facts," and (per the privacy section above) only the concept text leaves the machine.

Each gallery tile gets a "Sharpen" button (shown only when enhancementAvailable is true and the tile isn't already an enhanced result) and an "Animate" button (shown only when videoAvailable is true, on non-enhanced tiles) — clicking Sharpen prepends the sharpened copy to the image gallery; clicking Animate prepends the clip to a separate video gallery rendered below with an HTML5 <video controls> player. Enhanced tiles show only Delete — Regenerate, Sharpen, and Animate are hidden on them, because regenerate has no generator for the MagnificEnhanced source (you regenerate/sharpen/animate the original, not the sharpened copy). Regenerate, Sharpen, and Animate each guard against a double-click (disabled + in-flight signal) so one accidental repeat click never starts a second paid job.

Generation sends only the concept text the user typedcontext is deliberately left empty rather than including the gap analysis or any JD/resume-derived prep, so the "nothing but the concept text you type ever leaves your machine" copy stays literally true.

Tests

  • ConceptVisualPromptBuilderTests.cs — determinism (same input → byte-identical output, including across 25 randomized Bogus-generated concepts), the copyright-clean regression guard (asserts the prompt never contains a list of known copyrighted-work/illustrator names or "in the style of"/"recreate figure" phrasing), exact full-prompt-text assertions, and edge cases (empty concept throws, null request throws, blank context omits the context sentence, whitespace collapsing).
  • StudyVisualGeneratorTests.csGeminiImageSourceTests + OpenAiImageSourceTests against Flurl HttpTest fixtures returning a tiny fake 1×1 PNG: config-gate no-op (asserts the HttpTest call log is empty when unkeyed), exact endpoint/verb/header assertions, graceful degradation on an empty/malformed response, on malformed (non-base64) image data (regression test — this previously let FormatException escape uncaught), and on HTTP error status codes (with exact user-facing messages) — both suites drive the adapters through a FakeApiKeyProvider stand-in for the real vault-first key provider.
  • MagnificSourceTests.cs — the async create-then-poll flow against Flurl HttpTest: config-gate no-op, the full POST-then-GET-until-COMPLETED sequence with a real download of the final image, FAILED status, an exhausted poll budget (proves the loop is bounded and never hangs), a COMPLETED status with no generated images, a missing task_id, and an HTTP error — all degrade to Unavailable, never throw. Every test sets PollDelay = TimeSpan.Zero so the suite stays fast.
  • MagnificVideoSourceTests.cs — the image-to-video create-then-poll flow: config-gate no-op, missing-original guard, the full POST-then-poll-until-COMPLETED-then-download-mp4 sequence, FAILED status, an exhausted poll budget (bounded, never hangs), a missing task_id, and an HTTP error — all degrade to Unavailable, never throw. PollDelay = TimeSpan.Zero keeps it fast.
  • StudyVisualsControllerTests.cs — the image- and video-serving endpoints (real bytes + correct content-type, 404 for an unknown id, a missing-on-disk file, or a stored path that resolves outside the visuals directory — including a sibling directory that shares the prefix — the defense-in-depth guard); the path-free gallery views (neither StudyVisualView nor StudyVideoView has a local-path property); and the app-token gate — generate/regenerate/enhance/delete + generate-video/delete-video return 401 without the token and proceed with it.
  • StudyVisualServiceTests.cs — orchestration: only available sources/enhancers/video-generators are reported, generate/regenerate/delete/enhance/animate round-trip through a real (temp-directory) LocalJsonStore + StudyVisualStore, per-study-item gallery scoping, newest-first ordering, that a failure is never persisted, that enhancing/animating adds alongside the original (video goes to a separate study-videos index, image gallery untouched), that a failing enhancer falls back to the next, and that concurrent generations all persist (the index gate prevents lost updates).
  • study.spec.ts — the visual study aids describe block: gate on/off for generation, enhancement, and video; gallery + video-gallery load; generate/regenerate/delete/enhance/animate state updates; the paid-call double-click guards; that generation is skipped client-side when no source is available or the concept is blank; and that the image/video URL helpers never produce a file:// path.
  • api-token.interceptor.spec.ts — the token is attached to install + study-visuals generate/regenerate/enhance/delete, but never to a study-visuals GET or unrelated calls, and never when no token is present (dev server).
  • Stryker.NET mutation score for **/Visuals/*.cs: 92.75% (178/192 mutants killed) using a scoped config, stryker-visuals-scoped.json, well above the 80% "high" threshold. The full mutate list in stryker-config.json also includes **/Visuals/*.cs for the regular scheduled full run.