Compare commits

...

1 Commits

Author SHA1 Message Date
Rockachopa
0763a20d74 feat(glitch-detector): add --live browser vision integration for live world scanning
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 34s
Smoke Test / smoke (pull_request) Failing after 26s
Validate Config / YAML Lint (pull_request) Failing after 22s
Validate Config / JSON Validate (pull_request) Successful in 22s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 1m5s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 34s
Validate Config / Cron Syntax Check (pull_request) Successful in 9s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 13s
Validate Config / Playbook Schema Validation (pull_request) Successful in 21s
Architecture Lint / Lint Repository (pull_request) Failing after 30s
PR Checklist / pr-checklist (pull_request) Successful in 8m28s
- Add --live flag to use Playwright-based browser capture
- Implement _browser_capture() with Playwright for live scanning
- Support camera angle rotation (yaw/pitch) for multi-angle sweep
- Fall back to placeholder when --live not set and Playwright unavailable
- Hard-fail fast when --live is set but Playwright not installed

Closes #541
2026-04-29 07:52:27 -04:00

View File

@@ -37,6 +37,12 @@ from glitch_patterns import (
)
try:
from playwright.sync_api import sync_playwright
_PLAYWRIGHT_AVAILABLE = True
except ImportError:
_PLAYWRIGHT_AVAILABLE = False
@dataclass
class DetectedGlitch:
"""A single detected glitch with metadata."""
@@ -97,7 +103,7 @@ def generate_scan_angles(num_angles: int) -> list[dict]:
]
def capture_screenshots(url: str, angles: list[dict], output_dir: Path) -> list[Path]:
def capture_screenshots(url: str, angles: list[dict], output_dir: Path, live: bool = False) -> list[Path]:
"""Capture screenshots of a 3D web world from multiple angles.
Uses browser_vision tool when available; falls back to placeholder generation
@@ -111,7 +117,7 @@ def capture_screenshots(url: str, angles: list[dict], output_dir: Path) -> list[
# Attempt browser-based capture via browser_vision
try:
result = _browser_capture(url, angle, filename)
result = _browser_capture(url, angle, filename, live=live)
if result:
screenshots.append(filename)
continue
@@ -125,26 +131,83 @@ def capture_screenshots(url: str, angles: list[dict], output_dir: Path) -> list[
return screenshots
def _browser_capture(url: str, angle: dict, output_path: Path) -> bool:
def _browser_capture(url: str, angle: dict, output_path: Path, live: bool = False) -> bool:
"""Capture a screenshot via browser automation.
This is a stub that delegates to the browser_vision tool when run
in an environment that provides it. In CI or offline mode, returns False.
Uses Playwright to navigate to the URL and capture a screenshot.
In live mode, this is required; otherwise, falls back to placeholder.
Args:
url: Target URL to scan
angle: Camera angle dict with yaw/pitch/label
output_path: Where to save the screenshot
live: If True, hard-fail when browser unavailable
Returns:
True on success, False otherwise
"""
# Check if browser_vision is available via environment
bv_script = os.environ.get("BROWSER_VISION_SCRIPT")
if bv_script and Path(bv_script).exists():
import subprocess
cmd = [
sys.executable, bv_script,
"--url", url,
"--screenshot", str(output_path),
"--rotate-yaw", str(angle["yaw"]),
"--rotate-pitch", str(angle["pitch"]),
]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return proc.returncode == 0 and output_path.exists()
return False
# Import check
if not _PLAYWRIGHT_AVAILABLE:
if live:
raise ImportError(
"browser automation requires playwright. Install with: pip install playwright && playwright install chromium"
)
return False
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1280, "height": 720})
page.goto(url, wait_until="networkidle", timeout=30000)
page.wait_for_timeout(2000)
# Rotate camera if using custom angles (yaw/pitch in degrees)
yaw = angle.get("yaw", 0)
pitch = angle.get("pitch", 0)
if yaw != 0 or pitch != 0:
yaw_rad = yaw * (3.14159 / 180)
pitch_rad = pitch * (3.14159 / 180)
js = f"""
(function() {{
// Try common Three.js camera locations
let cam = null;
if (window.renderer && window.renderer.camera) cam = window.renderer.camera;
else if (window.scene && window.scene.camera) cam = window.scene.camera;
else if (window.camera) cam = window.camera;
// Check for Three.js r125+ camera in renderer.info
const canvases = document.querySelectorAll('canvas');
for (const c of canvases) {{
if (c.__three__ && c.__three__.camera) {{ cam = c.__three__.camera; break; }}
}}
if (cam) {{
cam.rotation.set({pitch_rad}, {yaw_rad}, 0, 'YXZ');
if (cam.updateMatrixWorld) cam.updateMatrixWorld();
}}
}})();
"""
page.evaluate(js)
page.wait_for_timeout(500)
page.screenshot(path=str(output_path))
browser.close()
return output_path.exists()
except Exception as e:
if live:
raise
# Silent fail for automatic fallback
return False
def _generate_placeholder_screenshot(path: Path, angle: dict):
"""Generate a minimal 1x1 PNG as a placeholder for testing."""
png_data = (
b"\x89PNG\r\n\x1a\n"
b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89"
b"\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01"
b"\r\n\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
)
path.write_bytes(png_data)
def _generate_placeholder_screenshot(path: Path, angle: dict):
@@ -524,6 +587,11 @@ Examples:
help="Minimum severity to include in report",
)
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
parser.add_argument(
"--live",
action="store_true",
help="Use live browser automation (requires playwright); falls back to placeholders if unavailable",
)
parser.add_argument(
"--threejs",
action="store_true",
@@ -555,7 +623,7 @@ Examples:
# Capture screenshots
screenshots_dir = Path(f"/tmp/matrix_glitch_{scan_id}")
screenshots = capture_screenshots(args.url, angles, screenshots_dir)
screenshots = capture_screenshots(args.url, angles, screenshots_dir, live=args.live)
print(f"[*] Captured {len(screenshots)} screenshots")
# Filter patterns by severity and type