Photonic is a local-first, non-destructive RAW photo editor built with React, WebGL2, FastAPI and LibRaw. It combines plain-language controls, reusable looks, local masks, culling and full-resolution export in a browser interface served from your own machine. Original photos are never modified; Photonic keeps its edit state in separate XMP sidecars.
The complete manual editing workflow is local and needs no account or subscription. The optional chat assistant is the one runtime exception: when used, it sends the current preview, request, edit parameters and notes through your authenticated Claude Code CLI. See Chat and privacy.
./setup.sh # once
./photonic ~/Pictures/YourPhotos # every time after
RAW editors are powerful, but the path from importing a shoot to a consistent, finished set often spans several disconnected tools. Photonic explores a smaller workflow: fast GPU feedback, edits that remain inspectable as slider values, reusable looks that adapt exposure per frame, and an assistant that proposes reversible changes instead of hiding the edit behind a generated result.
- Real-time editing. The WebGL2 pipeline redraws the full preview on every slider change; the target M2 development machine measures about 2 ms per render.
- Looks, previewed on your photo. The thumbnails at the top right are your picture under each look, next to your camera's own JPEG for reference.
- Local adjustments. Brighten a face without flattening the room behind it. Trace an outline around anything and adjust only that.
- Optional chat. Describe what you want — "warm the lamps only, don't let them blow out" — and it proposes visible, undoable slider and mask changes.
- Crop that uses the sensor. On a 40MP frame you can take a third of the width and still export 4.6MP of real detail. Nothing is upscaled.
- Rate as you go, then export the keepers in one unattended run, filed into folders by star rating.
| Operating system | macOS is the tested target. The Bash launcher and Python/Node stack should also run on Linux or WSL2, but those paths are not yet verified. |
| Python 3.11+ | python.org/downloads |
| Node.js 20+ | nodejs.org — used once, to build the interface |
| A browser | Chrome, Safari, Firefox or Edge, updated in the last couple of years (it needs WebGL2) |
| Claude Code CLI | Optional — only the chat box needs it. See Chat and privacy. |
Open Terminal and run these four lines. Copy them one at a time.
git clone https://github.com/ariyannp07/Photonic.git
cd Photonic
./setup.sh
./photonic ~/Pictures/YourPhotossetup.sh checks the installed versions, creates a project-local Python virtual
environment, installs the backend and frontend packages, and builds the
interface. It does not install project dependencies system-wide. Expect a few
minutes the first time.
On macOS, the browser opens on its own. On other systems, open the localhost URL
printed by the launcher. Next time, ./photonic with no folder reopens whatever
you had last.
The launcher treats its port as dedicated and stops an existing listener before
starting. It defaults to 8765; choose a free alternative if that port belongs
to another process:
PHOTONIC_PORT=8876 ./photonic ~/Pictures/YourPhotosIf setup fails, it tells you what to do. The one that catches people out is
rawpy, which needs LibRaw:
brew install libraw # macOS
sudo apt install libraw-dev # Ubuntu/DebianThen run ./setup.sh again.
At runtime, the chat box is the only feature that talks to a remote service. It is entirely optional: every slider, mask, crop, rating and export works without it.
It works by shelling out to the Claude Code CLI that you install and log into yourself. Photonic has no API key of its own. A request includes the preview-sized rendered image, your instruction, the current edit parameters and the photo/mask notes; the response is schema-validated and clamped before it is applied. Do not put sensitive information in notes if you plan to use chat.
If your Claude plan includes Claude Code, sign in with your Anthropic account. Usage remains subject to the limits and terms of that plan.
npm install -g @anthropic-ai/claude-code
claude auth loginThat opens a browser and signs you in to your Anthropic account. Start Photonic and the chat box works.
If you would rather bill against the API — or you have no subscription — set the environment variable and the CLI uses it automatically:
export ANTHROPIC_API_KEY=sk-ant-...Treat the key as a secret: do not commit it or paste it into screenshots. Use your normal secret-management workflow if you want it to persist.
Ask the CLI directly:
claude auth statusIt prints JSON. Two fields answer the question:
| Field | Subscription | API key |
|---|---|---|
authMethod |
claude.ai |
apiKey |
subscriptionType |
pro or max |
absent |
An API key wins if one is set. If ANTHROPIC_API_KEY exists in your
environment, the CLI can bill API usage even when you are also logged in with a
subscription. Check without printing the key itself:
[[ -n "${ANTHROPIC_API_KEY:-}" ]] && echo "API key set" || echo "API key not set"
unset ANTHROPIC_API_KEY # use subscription auth for this shellPhotonic defaults to the Claude Code opus model alias. Requests run in an
empty temporary directory with project settings, project MCP servers and
session persistence disabled. Cost and latency depend on the model and the
authentication method configured in your CLI.
To use a cheaper model, set:
export PHOTONIC_VISION_MODEL=sonnetIf the CLI is missing or not logged in, the chat box says so and everything else carries on working.
Pick a look, then tune it. The tiles at the top right are your photo under
each look. Click one to load it, then adjust. Save current… turns whatever
you have into a new look you can reuse.
Sliders are grouped Light / Colour / Presence, with the tone curve, colour mixer, grading and effects folded away underneath. Double-click any slider to reset it.
Masks do what global sliders cannot. Add a Radial, Graduated, Outline or Brightness mask, place it on the photo, and it gets its own set of adjustments. An Outline is traced — click around the thing you want and the mask follows its actual shape instead of catching the wall behind it. "Limit by brightness" restricts a mask to a tonal range, so brighten her, not the lamp behind her is one drag plus one slider.
Rotate and straighten are separate repairs. ] and [ turn the photo a
quarter at a time — a pure transpose, not one pixel lost. For a crooked horizon,
press C and drag either of the two rails that appear on the picture: one along
the bottom, one up the right side. Both move the same angle, so you use
whichever reference the photograph gives you — a sea horizon or the edge of a
building. The rule is that the edge you grab follows your finger.
Crop uses the sensor, not a resize. Press C, drag the rectangle, pick an
aspect if you want one. The panel shows the true output resolution as you drag,
because that is the whole argument for cropping a big frame. Every output pixel
comes from a sensor pixel.
Chat. Describe what you want. It sees the photo as it currently looks and the masks already on it, so it can work mask by mask: create one, refine one you have ("cooler still"), or remove one. One Undo takes back the whole suggestion. Underneath is a Notes box for that photo, kept between sessions and sent with every request.
Copy a look across the shoot. Copy look → applies the current photo's look
to every photo the filter is showing. Tick thumbnails first to be selective.
Each photo gets the look with its exposure rebalanced to its own histogram, so
the same +0.4 EV that flattered one frame doesn't blow out the next.
Rate as you go. 1–5 for stars, P to pick, X to reject, 0 to clear.
The filter bar narrows the filmstrip to Picked / Rated / Edited / Rejected, and
the arrow keys then step through only what is showing. Nothing is ever deleted;
reject is only a marker.
Export writes full-resolution JPEGs at quality 0.95 and never overwrites an
existing file. When a paired camera JPEG is present, Photonic copies its usable
EXIF, drops its stale thumbnail and normalizes the orientation tag. Choose a
scope — this photo, your ticks, picks, ★★★+, or everything — name a folder, and
leave it. Tick
Separate by rating and the run is filed as it goes into 5 stars,
4 stars, … Unrated, best first, so the folder you care about is finished and
uploadable while the rest is still rendering.
Rating a shoot and exporting it are the same pass of work, but the export is
the slow half. Tick Export as I go and every photo you mark with P is
rendered in the background, filed by rating, while you carry on editing. One at
a time, and only once you have stopped touching the sliders for a moment, so
the work you are doing always wins over the work happening for you.
The check is a commit point: your adjustments are written to disk before the
mark is recorded, so what gets exported is the version on screen. Revise a
photo after marking it and its file is replaced — never joined by
DSCF0001-1.jpg.
For a big shoot, run the exporter in a second window on its own port so the rendering is nowhere near the window you are rating in. Both point at the same folder; the exporter notices your marks within a few seconds.
# window 1 — editing
./photonic ~/Pictures/Trip/Originals
# window 2 — exporting, filed beside Originals rather than inside it
.venv/bin/python -m photonic \
--folder ~/Pictures/Trip/Originals \
--export-root ~/Pictures/Trip \
--auto-export --port 8766--export-root is what lets finished JPEGs sit in Trip/Exported/5 stars
next to Trip/Originals, instead of nested inside the folder of RAWs. The
destination typed in the browser is still confined to that root; what changed
is that whoever launches the server chooses where the root is.
\ (hold) |
See the original |
1–5 / 0 |
Rate / clear rating |
P / X |
Pick / reject |
C |
Crop, rotate and straighten |
] / [ |
Rotate right / left (90°, lossless) |
← → |
Previous / next photo |
⌘/Ctrl+Z / ⇧⌘/Ctrl+Shift+Z |
Undo / redo |
| Double-click a slider | Reset it |
| Cmd/Ctrl-click a thumbnail | Add to selection |
Photonic never modifies your RAW or your camera JPEG. Edits are saved
automatically as XMP sidecars under .photonic/:
YourPhotos/
DSCF0452.RAF ← untouched
DSCF0452.JPG ← untouched
DSCF0452.RAF.xmp ← darktable's sidecar, untouched
.photonic/edits/ ← Photonic's edits and ratings live here
.photonic/proxies/ ← decode cache, ~30 MB/photo, safe to delete
Exported/ ← your finished JPEGs
Deleting .photonic/ removes Photonic's edits, ratings, flags, notes, user
recipes and caches. It does not remove originals or already exported JPEGs.
Ratings use the standard xmp:Rating field inside Photonic's sidecars. An
XMP-aware tool can recognize the field if it reads those files, but many tools
only auto-discover sidecars next to the original and will not scan
.photonic/edits/. Render settings are Photonic's own JSON — deliberately
not darktable's format, because darktable stores module settings as
version-specific binary blobs. The two tools coexist without touching each
other's work, but cannot read each other's edits.
Development measurements on an Apple M2 with a 40 MP Fujifilm RAF; they are reference figures, not cross-platform guarantees:
| Open a photo, first time ever | ~2.4 s |
| Open any cached photo | ~40 ms |
| Move a slider | ~2 ms (GPU, every frame) |
| Chat suggestion | ~12 s |
| Export one photo | ~21 s |
A cold open is dominated by the RAW decode, so Photonic caches its proxy on disk. On opening a folder, a two-worker background queue decodes the remaining frames and prioritizes the visible filmstrip. Cached proxies survive restarts.
The cache costs roughly 30 MB per photo. It lives in .photonic/proxies/ and is
safe to delete at any time.
(cd backend && ../.venv/bin/python -m pytest ../tests) # 226 tests
(cd frontend && npm test) # 170 testsFor frontend development, keep the API and Vite server in separate terminals:
# Terminal 1
cd backend
../.venv/bin/python -m photonic --folder ~/Pictures/YourPhotos
# Terminal 2
cd frontend
npm run devOpen http://127.0.0.1:5173. Vite proxies /api to the backend on port 8765.
npm run build runs the TypeScript check and creates the production bundle.
One set of GPU shaders renders both the preview and the export. Four fragment- shader passes — point operations, two blur passes, then a finish pass for sharpening, grain and vignette — run over a linear scene-referred float16 copy of the photo.
The preview works on a 2400px proxy decoded once and cached. Export re-renders the same shader over the full-resolution image in 1024px tiles with a 128px halo, so neighbourhood effects match at the seams, then crops out of the finished full-size frame.
More detail in docs/DESIGN.md.
The catalog scans one folder (non-recursively) and recognizes RAF, CR2, CR3, NEF, ARW, DNG, ORF, RW2, JPG and JPEG files. Real-camera testing documented in this repository is on Fujifilm X-T50 X-Trans files; support for any listed RAW extension still depends on the LibRaw build and the specific camera.
The launcher binds the API to 127.0.0.1 by default. It has no authentication
and is designed for a single user on a trusted machine; do not expose it to a
LAN or the public internet. Export paths are restricted to at most two directory
segments inside the selected photo folder.
- Brush masks are wired through the shader and the data model but have no painting UI yet. Radial, Graduated and Outline masks work fully.
- Crop edge handles. Corners and whole-rectangle dragging work; there are no midpoint edge handles.
- Zooming past 100% magnifies the 2400px proxy, so fine detail goes soft on screen. The exported file is unaffected.
- Camera coverage is currently limited. The automated suite uses synthetic JPEG fixtures and mocks for most decode boundaries; real-camera verification documented in the project history is on Fujifilm X-T50 RAF files.
MIT — see LICENSE.