698 Commits
Author SHA1 Message Date
Victor Kuznetsov 4241a01659 Release 0.45.1 v0.45.1 2026-10-08 16:42:57 -07:00
Victor Kuznetsov c37378b347 Merge pull request #117 from dichiara19/fix/sdxl-zimage-global-offload
Stream the sdxl-zimage global stack on cards below 12 GiB
2026-10-08 16:34:43 -07:00
Victor Kuznetsov 8cb99b73b2 Fix SDXL offload placement on the current CUDA device 2026-10-08 16:17:11 -07:00
Giuseppe G. Di Chiara c45e6db3e4 Stream the sdxl-zimage global stack on cards below 12 GiB
The SDXL global stack (fp16 UNet, Canny ControlNet, both text encoders, VAE)
is ~8.8 GiB and was always moved to CUDA with .to(device), so it could not
fit an 8 GiB card and --cpu-offload never reached it. Under the Windows
display driver, WSL2 included, the overflow spilled into shared system
memory and the four-step pass ran for over ten minutes instead of failing.

Residency now follows the same rule as the Qwen stack: an explicit request
wins, otherwise the card's total VRAM decides against
RESIDENT_SDXL_GLOBAL_MIN_VRAM_GIB. Below it the Lightning LoRA is fused on
the CPU and the stack is streamed with enable_sequential_cpu_offload, the
fastest placement that completed on every run of an 8 GiB card with 16 GiB
of system RAM. sdxl-zimage joins GLOBAL_OFFLOAD_PROFILES, so the
--cpu-offload warning now fires for chroma-zimage only.
2026-10-08 23:27:55 +02:00
Victor Kuznetsov 37fb080c74 Merge pull request #116 from wiltodelta/dependabot/uv/minor-and-patch-931ef6a6bd 2026-10-08 10:30:09 -07:00
dependabot[bot] 954298aa14 Bump the minor-and-patch group with 3 updates
Bumps the minor-and-patch group with 3 updates: [python-dotenv](https://github.com/theskumar/python-dotenv), [openai](https://github.com/openai/openai-python) and [ruff](https://github.com/astral-sh/ruff).


Updates `python-dotenv` from 1.2.3 to 1.2.4
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v1.2.3...v1.2.4)

Updates `openai` from 3.22.1 to 3.24.0
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v3.22.1...v3.24.0)

Updates `ruff` from 0.16.9 to 0.16.10
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.9...0.16.10)

---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-version: 1.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: openai
  dependency-version: 3.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: ruff
  dependency-version: 0.16.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-07 08:48:45 +00:00
Victor KuznetsovandClaude Opus 5.5 10c7775990 Hide maintainer skills from public skill discovery
The skills CLI discovers every SKILL.md under .claude/skills, so
provider-oracles and signal-discovery were offered by `npx skills add`
and listed on skills.sh beside the published skill. Mark both
metadata.internal, the CLI's documented hide flag, and fail the suite
on any project skill that lacks it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:04:18 -07:00
Victor KuznetsovandClaude Opus 5.5 3f6447b23d Record how to run the gate from an agent session
The full gate outlasts the agent's 10-minute foreground shell cap, so it must
run detached with its exit code written to a file. A stray run is stopped by
its own PID, because a pattern kill also ends other sessions' suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:25:14 -07:00
Victor KuznetsovandClaude Opus 5.5 cc63bfd7bf Release 0.45.0
Bump the package to 0.45.0 and the agent skill to 1.0.11. The skill now names
the dola and workbuddy image mark keys, which 0.44.0 rejects, so the probe's
MIN_CLI_VERSION rises to 0.45.0 and both keys carry a "since CLI" note that
the floor test reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.45.0
2026-10-05 17:45:18 -07:00
Victor Kuznetsov 898f8be789 Add AI disclosure layouts, tighten detection and provenance warnings 2026-10-04 11:03:37 -07:00
Victor KuznetsovandClaude Opus 5.5 72752feb33 Scan uv.lock in CI and add a security policy
Add a CI job running the maintain.sh uv-secure scan, pinned to the gate by
a parity test, and a SECURITY.md with private reporting and dependency
remediation. Refresh the security-gate block record for accelerate 1.15.0.

Closes #114

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:33:50 -07:00
Victor KuznetsovandClaude Opus 5.5 70adddf960 Record why Qwen-Image-2.1 is not a regeneration profile
Open weights remove the hosted-only objection to the 2.x line, but the
Diffusers pipeline has no strength or latent noise-mask input and the Qwen
Research License is non-commercial (issue #111).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:33:49 -07:00
Victor Kuznetsov 01a2665181 Reconcile research follow-up status 2026-10-01 22:10:00 -07:00
Victor Kuznetsov 0c3e0fa415 Release 0.44.0 v0.44.0 2026-10-01 14:24:27 -07:00
Victor Kuznetsov b3b868153f Record Google Photos checks and continue across provider accounts 2026-10-01 13:42:21 -07:00
Victor Kuznetsov 304bd071c0 Unify TC260 producers and strengthen provenance regression coverage
Share producer identities across mark routing, attribution, and pinned signers.
Preserve nested Honor TC260 fields and report unsupported RSA signatures.
Document the Google Photos checks and public sample search.

Use an SDK-signed synthetic carrier for negative EKU checks after the original
Microsoft certificates expired. Skip boundary regex scans when no C2PA
soft-binding identifier is present.
2026-10-01 11:30:32 -07:00
Victor Kuznetsov 2e26df1f21 Restore C2PA certificate validation and refresh compatible dependencies 2026-09-30 16:58:43 -07:00
Victor KuznetsovandClaude Opus 5.5 d800ec9455 Verify TC260 SecurityData label signatures against a pinned MiniMax key
MiniMax signs its TC260 label with SM3withSM2 per TC260-PG-202511A. A
verification-only SM3/SM2 (no dependency provides the SM2 curve) checks the
appendix A.1 label message; a signature under the pinned MiniMax key is a
high-confidence aigc_signature signal, a failing one is a caveat, and every
result is the aigc_signature metadata marker. Pinned by the GB/T 32905 SM3
vectors, OpenSSL's SM2 verify vectors and real MiniMax files. Over 411 local
files the only verdict-record change is the marker on nine MiniMax videos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:36:08 -07:00
Victor KuznetsovandClaude Opus 5.5 ef7b972ed1 Sync the C2PA soft-binding snapshot to d36d1c1
The upstream list widened com.museblossom.contentsdefence.1 from image to
audio, image and video. Verdicts over 411 local files are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:36:08 -07:00
Victor KuznetsovandClaude Opus 5.5 b96ede6f44 Record the verified MiniMax TC260 SecurityData signatures
MiniMax writes a TC260-PG-202511A SecurityData object with SM3withSM2
signatures. The label signature verifies on all four local MiniMax videos
against the guide's appendix A.1 message, and the Binding signature over the
compact Bindings entry; the SM3 content hash does not match a naive selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:16:42 -07:00
Victor KuznetsovandClaude Opus 5.5 ca59c032a1 Release 0.43.0
Bump the package to 0.43.0 and the agent skill to 1.0.10. The skill now names
the wan image and vidu video mark keys, which 0.42.0 rejects, so the probe's
MIN_CLI_VERSION rises to 0.43.0 and both keys carry a "since CLI" note that
the floor test reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.43.0
2026-09-29 21:39:08 -07:00
Victor Kuznetsov c5e138357e Record Z-Image-Turbo global-stage rejection 2026-09-29 20:55:59 -07:00
Victor Kuznetsov 21661bea35 Reproduce re-watermarking and verify SynthID coexistence 2026-09-29 13:50:52 -07:00
Victor Kuznetsov 5cb4f2f808 Add Perth and PixelSeal benchmark oracles 2026-09-28 11:14:28 -07:00
Victor Kuznetsov 33f748b32a Record VACE and ROSE video inpainting results 2026-09-28 10:54:19 -07:00
Victor KuznetsovandClaude Opus 5.5 742a0dccab Align the remaining klein and color-restoration wording with the results
module-internals still called FLUX.2 klein pending; the sweep still counted
three inconclusive Gemini rows where only one was an explicit tool verdict,
and the removal page stated a Google chroma leak the data only warns of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:24:59 -07:00
Victor KuznetsovandClaude Opus 5.5 b5fc3ef9a6 Add face and text fidelity to the FLUX.2 klein comparison
fidelity_metrics.py at each engine's clean strengths: klein keeps Latin and
Cyrillic sign lettering better on two Google carriers, but loses every face
comparison and the verified CJK text, which the 512-pixel LPIPS had hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:59:06 -07:00
Victor KuznetsovandClaude Opus 5.5 6eb1a94799 Close the FLUX.2 klein Google measurement
The fourth Google carrier clears at 0.25, so the klein operating point with
the usual cross-source margin is 0.40, where the limiting carrier is at
LPIPS 0.270 against qwen-zimage's 0.183. klein is not a replacement global
stage for either vendor. Also record that Gemini's image-check allowance is
per account and the new "checks on similar images" quota wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:02:33 -07:00
Victor KuznetsovandClaude Opus 5.5 32791ee832 Record the FLUX.2 klein Google brackets from four Gemini accounts
Three of four Google carriers clear with klein at or below 0.20, where it
matches qwen-zimage's PSNR at 0.35, beats its LPIPS, and keeps the objects
qwen-zimage redraws. The fourth is detected at 0.10 and inconclusive at 0.20,
so there is no Google operating point yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:49:57 -07:00
Victor KuznetsovandClaude Opus 5.5 a103b4ec15 Record the FLUX.2 klein global-stage measurement
Stock Flux2KleinInpaintPipeline feeds the clean source back as reference
tokens, so it copies the source and OpenAI SynthID survives strength 0.30.
Without those tokens klein clears OpenAI between 0.05 and 0.20, at lower
fidelity than qwen-zimage at its floor. Google is unmeasured: the Gemini
Verify AI tool answered with a tool error or quota limit, which also casts
doubt on two of the earlier color-restoration "inconclusive" rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:17:26 -07:00
Victor KuznetsovandClaude Opus 5.5 7341e1a648 Record the CIELAB color-restoration measurement after regeneration
Transplanting the source's a,b channels onto qwen-zimage outputs at the vendor
floors cut CIEDE2000 on all seven tracked originals, kept every OpenAI API
verdict not_detected, but left three of four Gemini checks inconclusive and
painted old object colors onto redrawn ones. Not shipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:49:22 -07:00
Victor Kuznetsov dfbaa018c6 Merge pull request #112 from wiltodelta/docs/library-research-landscape
Research sweep, provider captures, and provenance attribution fixes
2026-09-27 11:39:15 -07:00
Victor KuznetsovandClaude Opus 5.5 e310a22bd8 Compare capture paths in POSIX form on every platform
The manifest stores forward-slash paths, and str() of a Windows path uses
backslashes, so the index test failed on both Windows CI jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:11:54 -07:00
Victor KuznetsovandClaude Opus 5.5 f855c4700f Merge origin/main into docs/library-research-landscape
Main's claim_generator_platform and c2pa_vendor_platform now use the
branch's whole-word registry match, which replaces
claim_generator_word_platform. The branch's provenance veto covers every
video mark and made main's Veo-over-Sora override unreachable, so the
override is removed. Main's credential rule carries into the veto: an
invalid credential vetoes nothing. Provenance that confirms the mark's
own vendor also vetoes nothing. Over 78 local videos with a C2PA AI
claim, the only verdict changes against main are Kling/Hailuo false
positives on Veo and FLUX clips that the veto now removes. C2PA verdicts
over 1,379 files are unchanged against the pre-merge branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:52:29 -07:00
Victor KuznetsovandClaude Opus 5.5 5a0f771ed7 Close the remaining raw-byte identity scans and unify two rules
- A decoded manifest no longer takes its generator from a raw
  claim_generator CBOR value or a soft binding from raw bytes. Device tokens
  (NIKON, Pixel Camera, sony.sig) stay a byte scan, since the decoded strings
  do not carry them, but read the manifest store: an EXIF Make beside an
  unrelated AI manifest is not a verified capture. The source-kind scan keeps
  the whole buffer because it also reads IPTC digitalSourceType in XMP.
- The byte fallback's c2pa.ai-disclosure kind now uses the structured rule:
  human_validated beside prompt_guided is a generation, not an enhancement.
- Wan and Yuanbao share BottomRightAnchoredEngine instead of two copies of
  the corner gate. The Yuanbao anchor test shifted the mark outside the locate
  window, so it passed with or without the gate; it now shifts it inside.

Verdicts over 1,379 C2PA files, 468 fixture and capture files, and Wan and
Yuanbao detections over 285 images are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 22:19:30 -07:00
Victor KuznetsovandClaude Opus 5.5 4931b93097 Add a Higgsfield DoP capture
Higgsfield's own DoP model ("Higgsfield Lite" in the web app, job model
image2video/lite) is not exposed by the MCP connector, so the capture was made
in the web app on a JPEG frame after a first job on a PNG failed and was
refunded. Like Genjutsu, the output carries no provenance: no C2PA, no TC260
label and no visible mark, only a Lavf re-mux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:17:27 -07:00
Victor KuznetsovandClaude Opus 5.5 509ce633a3 Judge decoded C2PA manifests on their decoded strings alone
Extend the whole-word rule to every identity lookup. Once the reader has
decoded a manifest, no identity is re-derived from its raw bytes: not the
signer, not a signer-platform token, not the AI-tool fallback for the
generator, and not the SynthID vendor inference, in identify and in
metadata.synthid_source alike. The byte scans read the manifest store only
when the reader could not open it.

Claim-generator tokens match as whole words through one shared
claim_generator_word_platform (suno no longer matches Sunoco), for images
and video alike.

The verdict now reads SynthID from the decoded C2PA info when the metadata
dict lacks it; three synthetic Google Photos tests had relied on the byte
fallback for that.

Verdicts over 1,379 C2PA files and 468 fixture and capture files are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:19:13 -07:00
Victor KuznetsovandClaude Opus 5.5 b02eb12cb2 Match registry tokens as whole words in decoded manifest strings
Registry tokens were raw substrings, so the Canva token matched the "Nova
Canvas" agent of an Amazon Bedrock image. Raw manifest bytes cannot take a
word-boundary rule: across the 1,379 C2PA files in the local corpus, 374 of
37,325 store matches had a letter or digit beside them, nearly all CBOR text
headers and DER tags. Decoded strings have no such framing.

- Match signer, claim-generator, agent and video-marker strings with
  registry_word_matches.
- When the reader decoded the manifest, never re-derive the signer from raw
  bytes: an unregistered signer stays unknown. Bytes decide only for
  manifests the reader cannot open.
- Add fal's signer "Features & Labels"; its attribution had relied on the byte
  fallback finding the "fal-ai" generator prefix.

Verdicts over 468 images and 365 videos are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:38:35 -07:00
Victor KuznetsovandClaude Opus 5.5 fb92c7dbc8 Attribute Amazon Bedrock C2PA and scope the signer fallback scan
Nova Canvas images published in aws-samples were reported as Canva: with no
Amazon row, the byte fallback matched the Canva token inside the "Nova
Canvas" software agent. Add an Amazon Web Services row ahead of Canva, which
also names Nova Reel video.

When the reader cannot open a manifest, scan for signers in the C2PA store
the container locates (JPEG APP11, PNG caBX) instead of the whole metadata
region. A vivo X300 camera capture was labeled "C2PA signer: Adobe" from its
XMP toolkit string. The SynthID provenance scan reads the same store.

Adds the Nova Canvas and Nova Reel samples as MIT-0 fixtures and records the
public-sample search for the other conformance-list signers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 16:21:13 -07:00
Victor KuznetsovandClaude Opus 5.5 7ba07869d2 Add paid-service captures, aggregator attribution, and provenance vetoes
Collect provider samples from Higgsfield, Runway and Dreamina plus Google
Photos, Gemini, Grok and YouTube re-encodes into data/captures/2026-09 with
a hash-checked manifest, and use them to fix attribution and false positives:

- Attribute aggregator outputs: Higgsfield job metadata, Dreamina's
  re-signed manifests, Runway's own C2PA signer, TC260 video producers.
- Veto any visible video mark when a valid C2PA claim names another vendor
  (a Gemini Omni clip read as Sora on steam), and gate Kling and Hailuo
  matches on edge, anchor and white-label evidence.
- Veto the Gemini sparkle when trusted provenance names another vendor.
- Report SynthID as present for Google Photos AI edits: Google's checker
  found it on all four measured edits, though manifests record no action.
- Add Vidu video and Wan image marks, TC260 audio placements, and record
  the AIGC key in metadata records.
- Document the research sweep, the Dreamina badge decision, and verified
  Google statements on SynthID in Photos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:32:53 -07:00
Victor Kuznetsov d25cada76b Export calibrated image source providers 2026-09-25 14:38:04 -07:00
Victor Kuznetsov b1f15818d7 Add pinned OpenAI and Google image source classifier 2026-09-25 14:23:01 -07:00
Victor Kuznetsov 317124ee85 Keep model prereleases out of package distribution 2026-09-25 13:54:06 -07:00
Victor Kuznetsov 0d468b6022 Add verified publication path for image source model image-source-freeze-2026-09-25-v1 2026-09-25 13:25:20 -07:00
Victor KuznetsovandClaude Opus 5.5 1dc70df289 Upgrade to OpenCV 5 with frozen Hershey templates and tighten video marks
- Lock OpenCV 5 and cap NumPy below 2.4 through uv constraint-dependencies so
  PaddleX 3.7 keeps resolving; Dependabot ignores mirror the cap.
- OpenCV 5 draws Hershey putText glyphs differently, so the Dola and Kling
  video templates ship as committed OpenCV 4 renders with a recipe script, and
  the OpenArt alpha recipe now refuses any OpenCV but 4.x.
- Break Sora/Veo cross-matches in favor of Veo under valid Google AI-video
  provenance, and require the full Sora mascot shape for mascot-only matches.
- Let the Doubao engine detect marks tinted by saturated backgrounds.
- Share C2PA claim-generator, credential-level and vendor-platform helpers,
  JPEG suffixes and the DCT basis instead of per-module copies.
- Log model download failures, document the optional GITHUB_TOKEN, mark the
  photo and source classifier model cards archived, and refresh the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:19:00 -07:00
Victor Kuznetsov 6f629a4ea6 Release 0.42.0 v0.42.0 2026-09-22 09:25:21 -07:00
Victor KuznetsovandClaude Sonnet 5 e3bbe88155 Authenticate C2PA registry-sync GitHub API calls to fix CI rate-limiting
scripts/sync_c2pa_soft_bindings.py --check hits api.github.com to resolve
the latest upstream commit SHA, unauthenticated. That endpoint's 60/hour
rate limit is per source IP, which GitHub Actions runners exhaust fast
since many workflows share the runner IP pool -- this was failing the
lint job with "403: rate limit exceeded" on every push regardless of
whether the change touched anything related.

Send an Authorization: Bearer header built from GITHUB_TOKEN/GH_TOKEN
when set, scoped to api.github.com only (raw.githubusercontent.com is a
separate, much higher-limit CDN and doesn't need it). Wire the workflow's
own GITHUB_TOKEN secret into that step's env so CI actually has one to
send, raising the limit to 5000/hour.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 08:11:48 -07:00
Victor Kuznetsov be8f24c7cb Raise OpenAI Qwen strength floor 2026-09-21 23:02:49 -07:00
Victor KuznetsovandClaude Sonnet 5 9990f3e6ca Fix missing OpenArt gallery fixture and alternate-size entry
The OpenArt visible-mark engine (commit 9d29d2c) was registered without
a data/fixtures/visible/openart/example.png gallery canary and without
an _ALTERNATE_SIZE entry in test_visible_fill_quality.py, breaking
TestGallery::test_every_registered_mark_has_an_example,
TestGallery::test_engine_detects_its_own_example[openart], and
test_cv2_mask_quality_smoke[alternate-openart] on every CI platform.
Generate the fixture via render_visible_examples.py (self-detects at
conf=0.968) and add the missing size entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:37:21 -07:00
Victor KuznetsovandClaude Sonnet 5 83be3a8926 Add generic bare "AI生成" text-mark detector (brand-less TC260 fallback)
Every existing text-mark engine (Doubao, Jimeng, Qwen, Baidu, Kling,
Yuanbao) is tuned to one vendor's exact wordmark silhouette, so none of
them match a bare "AI生成" strip with no brand name in front of it.
Confirmed on raiw-app production: a free `visible` run left a bottom-right
"AI生成" watermark completely unremoved because no detector fired.

Research found the wording is not tied to one dominant generator: vivo's
own support documentation confirms its Gallery stamps a bare "AI生成"
label after AI消除/AI扩图/大片模式 editing, and independent reporting
documents the same watermark on Xiaomi Gallery's AI-edit output. Since
the label is a GB 45438-2025/TC260 compliance stamp rather than one
vendor's brand mark, this adds one generic fallback (`generic_ai_label`)
instead of another per-OEM tuned config.

The detection silhouette is font-rendered (no logo element to capture)
via the shared scripts/render_vendor_silhouettes.py pipeline already
used for Qwen/Baidu/Kling/Yuanbao's own text-only marks. Calibration is
synthetic-only (no real captured generator screenshot was available);
the module docstring and DETECT_NCC_THRESHOLD comment carry that caveat
explicitly and this should be recalibrated once real captures exist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:01:10 -07:00