API
Alles, was die Oberfläche kann, kann ein Skript: die lokale REST-API, das Token, das TypeScript-SDK und Beispiele.
1:45FLOGVIT.photosuite betreibt eine lokale REST-API, und alles, was die Oberfläche kann, ist darauf. Nicht das meiste: alles. Ein Test der App lässt den Build scheitern, wenn eine Route ohne Dokumentation existiert, und ein anderer, wenn die Entwicklung eine Einstellung bekommt, die die API nicht anbietet.
Das ist der Sinn der Regel. Ein Skript, das du schreibst, umgeht die App nicht — es benutzt dieselbe Tür.
Erste Schritte#
Die API lauscht auf 127.0.0.1:9100, solange die App offen ist, und sie braucht ein Bearer-Token. Dein Rechner ist nicht das Einzige, was auf deinem Rechner läuft.
export CULLING_API_TOKEN=$(culling --print-api-token)
curl -H "Authorization: Bearer $CULLING_API_TOKEN" \
http://127.0.0.1:9100/api/status
Setze CULLING_API_TOKEN, bevor du die App startest, und sie nimmt dieses Token statt dem aus dem Schlüsselbund — so lässt du sie unbeaufsichtigt laufen, auf einem Rechner ohne Schlüsselbund zum Entsperren.
Läuft die App, gibt es auch einen interaktiven Browser unter localhost:9100/docs und das rohe OpenAPI-Dokument unter /api-doc/openapi.json. Beide sind ohne Token offen.
Das TypeScript-SDK#
Das SDK ist eine einzige Datei, es muss nichts installiert werden. Speichern Sie photosuite-sdk.mjs neben Ihrem Skript und photosuite-sdk.d.mts daneben, wenn Sie die Typen im Editor haben möchten:
curl -O https://photosuite.flogvit.com/sdk/photosuite-sdk.mjs
curl -O https://photosuite.flogvit.com/sdk/photosuite-sdk.d.mts
Importieren Sie es über seinen Pfad und starten Sie das Skript mit Bun (bun hello.ts) oder Node 24 (node hello.ts; unter Node 22 node --experimental-strip-types hello.ts). Ohne Angaben findet es die App auf diesem Rechner und ihr Token selbst, aus der Datei, die die App beim Start ihrer API schreibt. Fertige Skripte als Ausgangspunkt und die SDK-Datei finden Sie in der Skriptsammlung.
import { CullingClient } from './photosuite-sdk.mjs'
const client = new CullingClient() // reads CULLING_API_TOKEN
await client.folder.open('/Users/me/Pictures/2026-06-06 Coots')
const status = await client.status()
console.log(`${status.total_images} frames`)
Das SDK ist so gegliedert wie die App: client.navigate, client.images, client.selection, client.burst, client.develop, client.export, client.metadata, client.iptc, client.folder, client.catalog, client.library, client.eventphotos, client.upload, client.connections, client.workflow, client.tether, client.setups, client.ui, client.pipeline. Jede Route der App ist eine Methode auf einem davon.
Sichten aus einem Skript#
await client.filter.set({ min_rating: 3 })
// `sharpness` is null until the background pass has scored the frame
const soft = (await client.images.list())
.filter((image) => image.sharpness !== null && image.sharpness < 400)
.map((image) => image.index)
if (soft.length > 0) {
await client.batch.apply({ indices: soft, action: 'set_flag', value: 'reject' })
}
Der Stapel setzt die Markierung. client.images.reject() schaltet sie beim Bild auf dem Bildschirm um, wie die Taste, sodass ein zweimal laufendes Skript seine eigenen Ablehnungen wieder entfernen würde.
Bearbeiten aus einem Skript#
Jede Einstellung ist hier, und jedes Werkzeug auch.
// The same edit across a whole shoot
await client.develop.set({ exposure_ev: 0.4, shadows: 25, vibrance: 15 })
await client.develop.copy()
await client.selection.all()
await client.develop.paste()
// …or just the white balance, leaving each frame's own exposure alone
await client.develop.paste(['color'])
Masken sind Strukturen statt Gesten, also genauso leicht zu schreiben wie zu zeichnen:
await client.develop.setMasks([
{
name: 'Sky',
components: [
{ geometry: { kind: 'linear', x0: 0.5, y0: 0, x1: 0.5, y1: 0.35 } },
{ geometry: { kind: 'luminance', low: 0.45, high: 1, feather: 0.25 },
mode: 'intersect' },
],
values: { exposure_ev: -0.6, highlights: -35 },
},
])
Und die Werkzeuge, die das Bild lesen:
await client.images.autoLevel() // finds the angle from the frame's edges
await client.develop.addSkyMask()
await client.images.constrainCrop() // crop clear of a perspective correction
Jedes davon lehnt ab, statt zu raten, wenn das Bild ihm nichts gibt, woran es sich halten kann, und sagt das in der Antwort — ein Bild ohne gerade Kante bleibt, wie es ist, statt zufällig gekippt zu werden.
Lieferung#
Die Pipeline ist eine Kette von Schritten, von denen jeder ein Handle nimmt und ein neues zurückgibt.
const session = await client.pipeline.open('/Volumes/Card/DSC00042.ARW')
await session.handle
.develop() // the picture, with the edits stored beside the file
.autoCrop('16:9')
.resize(2048)
.export(null, '/tmp/out') // null: a local folder rather than a connection
await client.pipeline.close(session.session)
develop() ist der Schritt, auf den es ankommt, und der, den Leute weglassen. Ohne ihn lädt die Pipeline das RAW neu und rendert es wie aufgenommen — jede Korrektur, Maske und Fleckentfernung des Fotografen geht auf dem Weg zum Kunden verloren, und nichts schlägt fehl, denn am anderen Ende kommt trotzdem ein völlig brauchbares Bild heraus.
Verfolgen, was passiert#
Der Client verbindet sich von selbst mit dem Ereignisstrom der App, mit demselben Token wie jeder andere Aufruf. Jedes Ereignis und was es trägt, ist im EventMap des SDK typisiert. Ein falsches oder fehlendes Token beendet waitFor sofort mit 401 statt mit einer Zeitüberschreitung.
client.on('develop_changed', (e) => console.log('edited', e.index))
client.on('rating_set', (e) => console.log(e.index, 'now has', e.rating, 'stars'))
Die Oberfläche steuern#
Ansichten, Überlagerungen und Werkzeugmodi sind auch auf der API, und das macht ein Screenshot-Skript möglich — jedes Bild in dieser Dokumentation hat die App selbst aufgenommen, über diese Aufrufe gesteuert.
await client.view.setMode('develop')
await client.view.decodeSource('native-raw')
await client.develop.setTools({ before_after: true })
await client.ui.screenshot('main', '/tmp/shot.png')
Welches Panel vorne ist#
Am selben Ort angedockte Panels sind Tabs, und nur eines ist auf dem Bildschirm. Ein Panel sichtbar zu machen, macht es nicht zu dem, das du ansiehst, also gibt es zwei verschiedene Dinge zu lesen und einen Aufruf zum Setzen:
for (const panel of await client.ui.panels()) {
console.log(`${panel.title}: ${panel.visible ? 'shown' : 'hidden'}${panel.active ? ', in front' : ''}`);
}
await client.ui.focusPanel('develop');
focusPanel fügt das Panel dem Layout hinzu, wenn es nicht da ist, und endet daher immer mit diesem Panel im Blick. Die Namen sind folders, filmstrip, sidebar, histogram, viewer, develop, scopes und map.
Ohne Fenster#
Ein Rechner, der nur liefert, etwa die Live-Bilder eines Wettkampfs oder ein Skript, das jede Nacht läuft, braucht das Fenster nicht. Starte die App ohne Fenster und steuere sie nur über die API:
/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless
Unter Windows ist es FLOGVIT.photosuite.exe --headless im Ordner, in dem photosuite installiert ist. Es ist die ganze App ohne Fenster und ohne Symbol im Dock: dieselben Einstellungen, dieselbe Bibliothek, dieselben Workflows und dieselbe API, und sie entwickelt auf der Grafikkarte wie mit Fenster. Die API antwortet auf dem Port aus Settings, auch wenn sie dort ausgeschaltet ist, denn sie ist der einzige Weg hinein, und client.status() meldet headless: true und den Namen der Grafikkarte.
- Die Lizenz muss einmal in der App auf diesem Rechner aktiviert worden sein.
- Gib das API-Token in
CULLING_API_TOKENoder in einer.env-Datei im Einstellungsordner der App an (~/Library/Application Support/flogvit/auf dem Mac), damit die App nicht auf den Schlüsselbund wartet, ohne dass jemand antwortet. --screen 3840x2160@2legt den Bildschirm fest, für den die Bilder dekodiert und in dem die Bildschirmfotos aufgenommen werden. Ohne die Angabe der eines Retina-Laptops.- Bildschirmfotos und Aufnahmen funktionieren. Was ein Fenster braucht, etwa Videowiedergabe, Vollbild, das Verschieben des Fensters und Menüpunkte, die eine Dateiauswahl öffnen, antwortet mit 409 und sagt warum. Die Routen nehmen stattdessen einen Pfad.
- Strg-C oder
killbeendet sie, und sie speichert wie bei Quit.
Ein eigener Ordner#
Mit --dir gestartet, lebt photosuite in einem einzigen Ordner und sieht nichts außerhalb davon. Einstellungen, Bibliothek, Alben, Protokoll und Modelle liegen im .photosuite/ des Ordners, und in allem, was ihr gesagt wird, und allem, was sie antwortet, ist / der Ordner: /shoot ist der shoot des Ordners, und ein Pfad, der hinausführt, mit .. oder einem Link, wird abgelehnt. Mehrere können auf demselben Rechner laufen, jede mit ihrem Ordner, etwa eine pro Veranstaltung oder eine pro Kunde.
/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless --dir /Volumes/Events/nm-2026
- Der Ordner muss existieren, und nur eine Instanz kann gleichzeitig darin laufen.
- Ist der Port aus Settings belegt, nimmt sie einen freien, und
.photosuite/culling/api.jsonim Ordner sagt, welchen.new CullingClient({ root: '<Ordner>' })liest ihn, und--port 9200wählt den Port selbst. Sie läuft, wennclient.status()root: truesagt. - Das API-Token und andere Geheimnisse kommen aus der Umgebung, aus
.envim Ordner oder aus.photosuite/.env, nie aus dem Schlüsselbund. Was die App selbst speichert, liegt in.photosuite/secrets.json. - Die Lizenz ist die des Ordners, wenn er eine hat, sonst die des Rechners, die sie liest und nie ändert.
- Kein Laufwerk außerhalb des Ordners wird aufgelistet oder durchsucht, und ein Entrauschungsprofil, das außerhalb schreibt, wird nicht übernommen. Mit Fenster öffnen die Dateiauswahlen im Ordner und lehnen eine Wahl außerhalb ab.
- Die Instanzen wechseln sich bei DxO PureRAW ab, so dass nie zwei es gleichzeitig ausführen.
- Die API antwortet nur auf diesem Rechner. Die Programme, die die Instanz startet (das eines Entrauschungsprofils, die externen Editoren), werden in den eigenen Einstellungen des Ordners oder im Fenster festgelegt, nie über die API, und das eigene
.photosuite/der Instanz ist kein Ort, den die API nennen kann. Die Sicherungen der Bibliothek landen inFLOGVIT.photosuite library backupsim Ordner. - Sollen sie als Dienste laufen, bekommt jede ihren eigenen launchd-Agenten: ein eigenes
Labelund--headless --dir <Ordner>inProgramArguments. - Um eine Instanz zu verschieben oder zu kopieren: anhalten, den Ordner verschieben und dort starten. Beim Start schreibt sie ihre gespeicherten Pfade auf den neuen Ort um. Um eine zu löschen: anhalten und den Ordner löschen; nichts von ihr bleibt anderswo.
Ein Skript für eine solche Instanz ruft zuerst requireRoot() auf. Gegen eine App ohne --dir ausgeführt, hält es mit einer klaren Meldung an, statt in das eigene /delivery des Rechners zu schreiben:
import { CullingClient } from './photosuite-sdk.mjs'
// The instance started with --dir /Volumes/Events/nm-2026
const client = new CullingClient({ root: '/Volumes/Events/nm-2026' })
await client.requireRoot() // throws against an app without --dir
await client.folder.open('/shoot') // /Volumes/Events/nm-2026/shoot
await client.export.quickJpeg({ output_dir: '/delivery' })
Jede Route#
Die API-Referenz listet sie alle, erzeugt aus dem eigenen OpenAPI-Dokument der App, mit den Feldern, die jede nimmt, und einer curl-Zeile für jede.