Metadata-Version: 2.4
Name: abstractcamera
Version: 0.2.0
Summary: Camera control abstractions (tethered PTP bodies, built-in webcams, DWARF telescopes, simulators) for the Abstract ecosystem
Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
License-Expression: MIT
Project-URL: Homepage, https://github.com/lpalbou/abstractcamera
Project-URL: Repository, https://github.com/lpalbou/abstractcamera
Keywords: camera,photography,gphoto2,webcam,ptp,tethering,abstractcore,abstractframework,ai-tools,telescope,astrophotography,computer-vision
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics :: Capture
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: opencv-python>=4.8
Requires-Dist: pyobjc-framework-AVFoundation>=10.0; sys_platform == "darwin"
Requires-Dist: pyobjc-framework-Quartz>=10.0; sys_platform == "darwin"
Requires-Dist: pyobjc-framework-libdispatch>=10.0; sys_platform == "darwin"
Provides-Extra: gphoto2
Requires-Dist: gphoto2>=2.5.10; extra == "gphoto2"
Provides-Extra: clips
Requires-Dist: av>=12.0; extra == "clips"
Provides-Extra: raw
Requires-Dist: rawpy>=0.19; extra == "raw"
Provides-Extra: dwarf
Requires-Dist: websocket-client>=1.7; extra == "dwarf"
Provides-Extra: test
Requires-Dist: pytest>=7.0.0; extra == "test"
Provides-Extra: docs
Requires-Dist: mkdocs>=1.5; extra == "docs"
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
Provides-Extra: all
Requires-Dist: gphoto2>=2.5.10; extra == "all"
Requires-Dist: av>=12.0; extra == "all"
Requires-Dist: rawpy>=0.19; extra == "all"
Requires-Dist: websocket-client>=1.7; extra == "all"
Provides-Extra: dev
Requires-Dist: gphoto2>=2.5.10; extra == "dev"
Requires-Dist: av>=12.0; extra == "dev"
Requires-Dist: rawpy>=0.19; extra == "dev"
Requires-Dist: websocket-client>=1.7; extra == "dev"
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: ruff>=0.5.7; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=4.0; extra == "dev"
Requires-Dist: mkdocs>=1.5; extra == "dev"
Requires-Dist: mkdocs-material>=9.0; extra == "dev"
Dynamic: license-file

# AbstractCamera

Camera control abstractions for Python and the Abstract* ecosystem: one
thread-safe orchestrator (`CameraManager`) drives any camera family behind a
session protocol — tethered PTP bodies over libgphoto2, the machine's own
cameras (macOS AVFoundation), and a scriptable simulator for camera-less
development and tests. A `CameraHub` pilots several cameras at once (one
worker per camera, hardware-validated with four simultaneously), with
per-device capture folders under `~/Pictures/<device>/`.

## What you get

- A high-level orchestrator: [`CameraManager`](src/abstractcamera/camera_manager.py)
  — live view (latest-JPEG + measured fps), honest config dials backed by a
  write-verification ledger (every write is confirmed or explicitly declared
  reverted), single/burst/movie capture, focus actions, an absolute-deadline
  intervalometer with JSONL manifests, live-view detection
  (lightning/meteor/motion) with auto-fire, a rolling pre-capture buffer, and
  automatic capture downloads.
- Family adapters (hardware-validated): [`adapters/`](src/abstractcamera/adapters/)
  - **Nikon Z** (validated on a Z6 II): exposure-blocking triggers, lazy
    config settling, live-view-gated writes, movie prohibit pre-checks,
    wedge recovery.
  - **Sony Alpha** (validated on an A7R IV): async write settling with
    verify-retry, `[-2]` busy backoff, `prioritymode` gating, press-and-hold
    bursts, silent-AF-refusal detection, fetch-on-announce downloads
    (sdram slot eviction), honest unconfirmable-movie receipts.
  - **Webcam** (validated on a MacBook Pro camera): honest capability
    surface — one real dial (resolution), stills that are video frames and
    say so, genuinely confirmable in-process MP4 recording, detection and
    intervalometer riding the frame stream.
  - **DWARF smart telescope** (network/Wi-Fi, DwarfLab API v2): RTSP live
    view, device-table exposure/gain dials, album-backed captures that
    download over Wi-Fi, and the MOUNT as family actions — GOTO (RA/Dec or
    solar-system), joystick slews, calibration, astro autofocus.
  - **Generic PTP** fallback for unknown tethered bodies.
- Multi-camera piloting: [`hub.py`](src/abstractcamera/hub.py) — a
  `CameraHub` runs one manager/worker per connected camera (concurrent live
  views, sequences and recordings), tracks an ACTIVE selection for
  single-panel hosts, and derives stable device identities (model slug +
  serial disambiguation) that name the per-device capture folders
  `<root>/<device>/[<sequence>/]` with an on-device/local save policy.
- Transport drivers + discovery: [`drivers/`](src/abstractcamera/drivers/),
  [`discovery.py`](src/abstractcamera/discovery.py) — non-invasive
  `list_cameras()` across transports (no device opened, no LED, no
  permission prompt at list time). Webcam identity is the AVFoundation
  uniqueID and capture opens THAT device natively (ADR 0009: names cannot
  point at the wrong camera); Continuity iPhones labeled, never auto-picked.
- A simulator that is a drop-in gphoto2 module:
  [`sim/gphoto2.py`](src/abstractcamera/sim/gphoto2.py) with Nikon Z6 II and
  Sony A7R IV personalities reproducing hardware-measured quirks
  (`ABSTRACTCAMERA_FAKE=1`).
- Device media downloads, one abstraction across devices (ADR 0011):
  `abstractcamera download` copies ALL media a device holds into
  `~/Pictures/<device>/` — USB-mounted cards (auto-detected by album
  signature) or the DWARF album over Wi-Fi (`--host`); `--delete` frees
  the device after size-verified copies (protected device state like the
  DWARF's dark library always stays). Per-device `MediaStore` adapters
  ride one sync engine that owns all safety rules; PTP-card stores
  (Sony/Nikon) are the named next adapters.
- A CLI for manual checks: `abstractcamera list` / `abstractcamera preview`
  / `abstractcamera download` (self-contained local operations). Detection
  runs in-process and its events are readable through the capability's
  event API (`camera_get_events` / `/v1/camera/events`); waking a durable
  workflow on motion is a consumer built at a framework entry (gateway
  run / flow), not a camera-shipped daemon.
- **AbstractCore integration (ADR 0012)**: installing abstractcamera beside
  [AbstractCore](https://github.com/lpalbou/abstractcore) auto-registers the
  `camera` capability (entry point `abstractcore.capabilities_plugins`) —
  open/close cameras, capture photos and bounded video clips, arm
  motion/lightning/meteor detection with auto-capture — plus an explicit AI
  tool set: `from abstractcamera.integrations.abstractcore_tools import
  camera_tools` then `llm.generate("Take a photo if something moves",
  tools=camera_tools())`. Eleven `camera_*` tools (including
  `camera_preview_photo` — look without firing the shutter) ship with a
  classification map (`captures_environment` is the privacy fact approval
  layers key on).

## Install

```bash
pip install abstractcamera                 # webcam + simulator (numpy + OpenCV)
pip install "abstractcamera[gphoto2]"      # + tethered PTP bodies (libgphoto2)
pip install "abstractcamera[clips]"        # + MP4 clip/movie encoding (PyAV)
pip install "abstractcamera[raw]"          # + RAW capture thumbnails (rawpy)
pip install "abstractcamera[dwarf]"        # + DWARF smart telescopes (Wi-Fi)
pip install "abstractcamera[all]"          # everything above
```

## Quickstart

```python
from abstractcamera import CameraManager, list_cameras

for camera in list_cameras():
    print(camera["id"], camera["name"])

manager = CameraManager()
status = manager.connect()            # default: first PTP body, else webcam
manager.set_config_value("iso", "800")   # confirmed/reverted via the ledger
manager.request_trigger()                # capture -> auto-download + catch log
jpeg, seq = manager.get_latest_frame()   # live view
manager.start_interval_sequence(interval_s=5.0, count=10)
manager.disconnect()
```

Camera-less development: set `ABSTRACTCAMERA_FAKE=1` and every transport is
replaced by the simulator (scriptable via `abstractcamera.sim.gphoto2.configure`).

## How it fits together

```mermaid
flowchart LR
  Host[Host app / CLI] --> CM[CameraManager]
  CM --> W[worker thread: one owner of all camera I/O]
  W --> AD[family adapter: Nikon Z / Sony Alpha / webcam / generic]
  AD --> S[CameraSession: gphoto2.Camera / WebcamSession / simulator]
  D[discovery + drivers] --> CM
```

The manager owns everything family-independent (scheduling windows, the
write ledger, downloads, detection, the watchdog); the adapter owns every
family quirk; the session speaks the pinned wire protocol
([`wire.py`](src/abstractcamera/wire.py), values pinned to libgphoto2).
Architecture decisions live in [docs/adr/](docs/adr/).

## Honesty principles (ADR 0004)

Capabilities never pretend: a webcam exposes no ISO dial instead of a fake
one; Sony movie recording is labeled unconfirmable over USB; every config
write is confirmed against the body or explicitly reported reverted; webcam
names are `reported` from the same device object the session captures from
(ADR 0009 — positional guessing is gone).

## Documentation

- [docs/getting-started.md](docs/getting-started.md)
- [docs/architecture.md](docs/architecture.md)
- [docs/api.md](docs/api.md)
- [docs/faq.md](docs/faq.md) · [docs/troubleshooting.md](docs/troubleshooting.md)
- [docs/adr/](docs/adr/) — architecture decision records
- AI-readable: [llms.txt](llms.txt), [llms-full.txt](llms-full.txt)

## AbstractFramework ecosystem

AbstractCamera is part of the **AbstractFramework** ecosystem
(<https://github.com/lpalbou/AbstractFramework>), alongside AbstractCore,
AbstractVision, AbstractVoice, and friends. It was extracted from the
BlackPixel desktop editor's hardware-validated tethering stack (2026-07-12)
through a 3-agent adversarial design review.

## License

MIT — see [LICENSE](LICENSE).
