screengraft 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screengraft",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "description": "Put a UI screenshot or screen recording onto a photographed device screen with the perspective exactly right \u2014 a homography you confirm by hand, not a generative guess.",
5
5
  "keywords": [
6
6
  "mockup",
package/scripts/detect.py CHANGED
@@ -635,7 +635,30 @@ def _finalize(candidates, img_area: float, refine: bool = True, img_shape=None,
635
635
  # project has never had the number. Off unless asked for, and it must not
636
636
  # change the answer: it observes the same lists the walk below uses.
637
637
  walked = {}
638
- for cand in sorted(candidates, key=lambda c: c[0], reverse=True):
638
+ # Walk order: by score -- EXCEPT when the best-scoring candidate has no
639
+ # shape evidence at all (tier 0: sharp, unmeasurable, or inconsistent
640
+ # corners) while a tier-2 candidate exists in the same list. A tier-0
641
+ # winner cannot be corroborated and ends in an abstention or a refusal, so
642
+ # nothing is lost by looking past it; a tier-2 candidate has four corners
643
+ # agreeing on a radius, which on the labelled corpus has meant "on the
644
+ # screen" for every channel-accepted result. Deliberately NOT tier-first
645
+ # ordering of the whole list: that was tried on 11 Sep 2026 and broke three
646
+ # photographs, because a tier-2 WRONG candidate sits deeper in the list on
647
+ # each. This only moves when score alone would have handed the answer to a
648
+ # quad with nothing to say for itself.
649
+ def _tier_of(cand):
650
+ _score, quad, contour, _tag = cand
651
+ q = order_quad(refine_corners(contour, quad, rail_band)[0]) if refine else quad
652
+ return shape_tier({"corner_radius": measure_corner_radius(contour, q)})
653
+
654
+ order = sorted(candidates, key=lambda c: c[0], reverse=True)
655
+ if order and _tier_of(order[0]) == 0:
656
+ # Identity, not equality: candidates hold numpy arrays.
657
+ strong = [c for c in order if _tier_of(c) == 2]
658
+ if strong:
659
+ strong_ids = {id(c) for c in strong}
660
+ order = strong + [c for c in order if id(c) not in strong_ids]
661
+ for cand in order:
639
662
  # NOT necessarily `cand`: pick_innermost steps inward from it while a
640
663
  # comparably screen-like quad nests inside, so the quad that gets
641
664
  # validated -- and returned -- can belong to a different candidate.
@@ -927,6 +950,18 @@ def has_rounded_corners(result) -> bool:
927
950
  if float(cr["photo_px"]) <= MIN_ROUNDING_PX:
928
951
  return False
929
952
  per = [float(v) for v in cr["per_corner_px"]]
953
+ # ALL FOUR corners, not most of them. A per-corner estimate of exactly 0.0
954
+ # is the estimator saying "no arc here", and a rounded rectangle does not
955
+ # have corners with no arc. The spread test alone let [0, 0, 56, 56]
956
+ # through -- a quad rounded at two corners and square at the other two,
957
+ # a shape no screen has -- because its spread lands at exactly 2.00 and the
958
+ # comparison is <=. That quad shipped as a CONFIDENT answer 124% off
959
+ # (iPhone-8, 12 Sep 2026): the only confidently wrong result the labelled
960
+ # bench has ever produced. Categorical rather than a threshold: on the 14
961
+ # labelled photographs every on-screen quad this accepts has all four
962
+ # corners measured (smallest 1.6px); the wrong one had two at 0.0.
963
+ if min(per) <= 0.0:
964
+ return False
930
965
  r = float(cr["photo_px"])
931
966
  spread = (max(per) - min(per)) / max(r, 1e-6)
932
967
  return spread <= MAX_RADIUS_SPREAD
@@ -5,7 +5,7 @@ description: Injects a UI screenshot OR a screen recording onto a photographed d
5
5
 
6
6
  # Inject a screenshot onto a photographed device
7
7
 
8
- **What ships (v0.42):** a local browser UI (`scripts/ui.py`) that walks the designer through the whole job — pick the photo and the screen source, which may be an image **or a video** (recent Desktop/Downloads images, drag-drop, browse, path, or a **Figma frame link**), auto-detect the screen as a starting position — and when detection cannot tell which region is a screen, **Point at screen**: one click inside it and the detector uses that point — or, if this photograph has been fitted before, **the fit it was saved with comes back** as the starting position instead of a detection, recognised by the photo's own pixels so a rename or a drag-drop still match — and every save also writes a **portable `.fit.json` beside the mockup** that can be dropped back onto the page later, which is how a fit survives a re-export, another machine, or someone else's hands — then **match the four edges** (drag an edge's middle to slide it, near an end to pivot; corners still draggable) with canvas navigation that follows the usual conventions — **hold ⌘ and scroll to zoom to the pointer, hold space and drag to pan** — and a rectified strip loupe. The fit and the composite sit **side by side and always have** — the result pane re-renders as you drag, which is how a corner gets judged, so it is the layout rather than a mode you can switch off. Then an on-by-default realism pass that colour-matches the source to the photo's light, **Save** (or **Render**, for a video) into the project folder (`--out-dir`), and a **Send to Claude** button that reaches you through the plugin's own MCP server. The UI is a hand port of the project's Figma design file — dark only.
8
+ **What ships (v0.43):** a local browser UI (`scripts/ui.py`) that walks the designer through the whole job — pick the photo and the screen source, which may be an image **or a video** (recent Desktop/Downloads images, drag-drop, browse, path, or a **Figma frame link**), auto-detect the screen as a starting position — and when detection cannot tell which region is a screen, **Point at screen**: one click inside it and the detector uses that point — or, if this photograph has been fitted before, **the fit it was saved with comes back** as the starting position instead of a detection, recognised by the photo's own pixels so a rename or a drag-drop still match — and every save also writes a **portable `.fit.json` beside the mockup** that can be dropped back onto the page later, which is how a fit survives a re-export, another machine, or someone else's hands — then **match the four edges** (drag an edge's middle to slide it, near an end to pivot; corners still draggable) with canvas navigation that follows the usual conventions — **hold ⌘ and scroll to zoom to the pointer, hold space and drag to pan** — and a rectified strip loupe. The fit and the composite sit **side by side and always have** — the result pane re-renders as you drag, which is how a corner gets judged, so it is the layout rather than a mode you can switch off. Then an on-by-default realism pass that colour-matches the source to the photo's light, **Save** (or **Render**, for a video) into the project folder (`--out-dir`), and a **Send to Claude** button that reaches you through the plugin's own MCP server. The UI is a hand port of the project's Figma design file — dark only.
9
9
 
10
10
  The geometry is exact (`warp.py`); the detection is advisory (`detect.py`) and the human corrects it. **When a detection is wrong and you want to know why**, ask for the candidate list: `POST /api/detect {"trace": true}` writes `<session>/candidates.json`, or run `python3 scripts/detect.py --photo P --out-corners /tmp/c.json --trace /tmp/t.json` (add `--click X,Y`). Every candidate quad is in there with its score and whether it was accepted, rejected, never reached, or filtered out by the click — which is what separates "the screen was never proposed" from "it was proposed and something else won".
11
11