feat(bridge): live phone-camera mode — desktop (#49) - #58
Open
yb85 wants to merge 5 commits into
Open
Conversation
Slice 1 of the phone-as-camera feature. Adds BridgeLiveServer — a persistent token-gated HTTPS session (stdlib ThreadingHTTPServer + ssl, no new deps) over the same ephemeral-cert/fingerprint-pin trust triad as the one-shot receiver: the phone streams low-res preview frames (POST /v1/frame), long-polls for a desktop shutter (GET /v1/command), and returns a full-res still (POST /v1/still). - ReceiverInfo.qr_uri(mode="live") → aglaia://v1?…&m=live; old push app hitting /import on a live server gets a graceful 404 hint. - FakePhone test client drives the whole protocol over pinned TLS; 11 tests (409 conflict, still round-trip, poll timeout, liveness, bye). - docs/bridge.md: authoritative bridge-live/1 spec + failure matrix + live-device checklist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 2. Extracts WebcamThread's rotate/mirror/flip into _apply_transform (shared by the real + fake loops), then adds BridgeCameraThread: a WebcamThread subclass that emits phone preview frames via change_pixmap_signal and whose get_frame() fetches a remote full-res still (BridgeLiveServer.request_still). Because it duck-types the webcam thread, MainWindow.capture()/voice/zoom work unchanged. Zoom is a desktop-side digital crop (center-crop 1/zoom, resize back up: LINEAR for preview, CUBIC on the still output path) so effective_dpi = base × zoom stays correct with no wire command. get_frame has a ~1s freshness cache and degrades to None + session_lost/still_failed signals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 4. Adds a 6th sidebar tab that pairs a phone (QR) then hosts a live CaptureTab backed by BridgeCameraThread: - BridgeTab (QR ↔ live QStackedWidget; right-click QR → Copy pairing URI) + BridgeLiveController (marshals server callbacks to Qt signals). - MainWindow: arm/disarm on tab entry (tab_changed); _make_live_capture_tab (bridge=True) hides camera/format/freehand + relabels Deactivate → "End bridge session"; session start builds the live tab and repoints _capture_tab so update_image/DPI route to it; teardown restores it and re-arms a fresh QR. Mutual exclusion with the local webcam. capture() tags bridge stills source_ref="bridge#<device>". closeEvent disarms (queues bye). - CaptureTab: camera/format rows wrapped in hideable containers. - tools/fake_bridge_phone.py: device-free E2E driver (streams an image as preview, answers shutters with the full-size still). Verified end to end. - 6 gui tests (tab swap, picker hiding, mutual exclusion, start/teardown). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Slice 7. The DPI dialog's 60 ms live tick now reads get_preview_frame() when the source exposes it (a bridge), so framing/quad-overlay/steadiness run on the low-res stream and never trigger a remote still. Only a measurement — capture-refine, manual trace, or ruler — pulls one full-res still via _grab_measure_frame(), and the card DPI is always computed on those sensor pixels. The live ≈dpi readout is lifted by the still/preview long-edge ratio (_measure_scale) to approximate real DPI; scale-invariant, so the steadiness gate is unaffected. A local webcam is unchanged (get_frame is already full-res, scale 1.0). 5 tests: live ticks pull 0 stills, capture-refine/ruler pull exactly 1, measure-scale math, webcam fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gui.md gains a Bridge-tab section; architecture.md lists the bridge-live producer; README + the CLAUDE.md pointer table link docs/bridge.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Desktop half of the live phone-camera feature (#49) — the "Alternative" path (no Continuity Camera). The iOS half is yb85/aglaia-bridge#.
What's here
bridge_live.py—BridgeLiveServer: a persistent token-gated HTTPS session over the same stdlibThreadingHTTPServer+ ephemeral-cert/fingerprint-pin stack as the PRD: aglaia-bridge v1 — iOS capture + QR-bootstrapped TLS-pinned handoff #47 receiver (no new deps). Phone streams preview frames (POST /v1/frame), long-polls the shutter (GET /v1/command), returns a full-res still (POST /v1/still). Protocolbridge-live/1.BridgeCameraThread— aWebcamThreadlook-alike, soMainWindow.capture(), voice, zoom, and DPI calibration work unchanged; stills taggedsource_ref="bridge#<device>". Zoom is a desktop-side digital crop (effective_dpi = base × zoomintact).BridgeTab+BridgeLiveController— 6th sidebar tab: QR pairing ↔ live capture swap, arm/disarm on tab entry, mutual exclusion with the local webcam.tools/fake_bridge_phone.py— device-free E2E driver (right-click the QR → "Copy pairing URI").docs/bridge.md— authoritativebridge-live/1spec, trust model, failure matrix, live-device checklist; gui/architecture/README updated.Trust model
QR = trusted side channel, LAN = hostile → ephemeral TLS + fingerprint pin (server auth) + single-use bearer token. Fresh cert + token each pairing.
Tests
29 new tests (server/protocol 11, camera thread 7, tab/wiring 6, DPI dialog 5). Full suite 427 passed / 10 skipped. Device-free E2E verified end to end (fake phone → 3840×2880 still through the real server).
Remaining
Live end-to-end on a real device pair (checklist in
docs/bridge.md) — the only unverified path.Closes #49
🤖 Generated with Claude Code