screengraft 0.39.0 → 0.40.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.39.0",
3
+ "version": "0.40.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
@@ -797,12 +797,31 @@ def detect_edges(gray: np.ndarray, click=None, trace=None):
797
797
  # Sweep the Canny thresholds off the image's own median rather than fixed
798
798
  # numbers, then a few sigmas around it — one exposure doesn't suit both a
799
799
  # bright render and a dim photo.
800
+ #
801
+ # ... and ALSO off Otsu's threshold, because the median anchor has a hole
802
+ # the median cannot see: a dark photograph. A black phone on a black
803
+ # backdrop has a median of 0..4, so every sweep point lands at hi <= 7 and
804
+ # Canny fires on every pixel of noise -- the edge map is a solid sheet and
805
+ # no closed quad survives it. Both photographs in the labelled corpus on
806
+ # which nothing near the screen was EVER proposed (11 Sep 2026) are exactly
807
+ # this, and edge returned zero candidates on them. Otsu splits the two
808
+ # modes that are actually there -- 97 and 127 on those two -- and the same
809
+ # sweep then proposes the screen at 9% and 12% unrefined, which is where
810
+ # every other photograph's nearest candidate sits. On the other seven the
811
+ # nearest candidate is unchanged. The saturation channel already anchors
812
+ # on Otsu for the same reason (one percentile fails when the thing sought
813
+ # is smaller than the percentile).
800
814
  med = float(np.median(blur))
801
- for sigma in (0.20, 0.33, 0.50, 0.66):
802
- lo = int(max(0, (1.0 - sigma) * med))
803
- hi = int(min(255, (1.0 + sigma) * med))
804
- if hi <= lo:
815
+ otsu, _ = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
816
+ sweep = [((1.0 - s) * med, (1.0 + s) * med) for s in (0.20, 0.33, 0.50, 0.66)]
817
+ sweep += [(otsu * f / 2.0, otsu * f) for f in (0.5, 1.0, 1.5)]
818
+ seen = set()
819
+ for lo_f, hi_f in sweep:
820
+ lo = int(max(0, lo_f))
821
+ hi = int(min(255, hi_f))
822
+ if hi <= lo or (lo, hi) in seen:
805
823
  continue
824
+ seen.add((lo, hi))
806
825
  edges = cv2.Canny(blur, lo, hi, L2gradient=True)
807
826
  # Close small gaps so a bezel outline broken by a notch or a glare
808
827
  # spot still forms one closed contour.
@@ -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.39):** 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.40):** 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