API

Alt grensesnittet kan gjere, kan eit skript gjere: det lokale REST-API-et, token, TypeScript-SDK-en og døme.

På denne sida
1:45
Fortalt på engelsk, med undertekstar. Blir spelt frå YouTube når du trykkjer på avspeling.

FLOGVIT.photosuite køyrer eit lokalt REST-API, og alt grensesnittet kan gjere, finst på det. Ikkje det meste: ein test i appen feilar bygget når ei rute finst utan dokumentasjon, og ein annan feilar når framkallinga får ei innstilling API-et ikkje gjev tilgang til.

Det er heile poenget med regelen. Eit skript du skriv, går ikkje rundt appen — det brukar den same døra.

Kom i gang#

API-et lyttar på 127.0.0.1:9100 medan appen er open, og det krev eit bearer-token. Maskina di er ikkje det einaste som køyrer på maskina di.

export CULLING_API_TOKEN=$(culling --print-api-token)
curl -H "Authorization: Bearer $CULLING_API_TOKEN" \
  http://127.0.0.1:9100/api/status

Set du CULLING_API_TOKEN før du startar appen, brukar han det tokenet i staden for det i nøkkelringen — slik køyrer du han utan tilsyn, på ei maskin utan nøkkelring å låse opp.

Medan appen køyrer, finst det òg ein interaktiv nettlesar på localhost:9100/docs, og sjølve OpenAPI-dokumentet på /api-doc/openapi.json. Begge er opne utan token.

TypeScript-SDK-en#

SDK-en er éi fil, og ingenting må installerast. Lagre photosuite-sdk.mjs ved sida av skriptet ditt, og photosuite-sdk.d.mts ved sida av ho om du vil ha typane i editoren:

curl -O https://photosuite.flogvit.com/sdk/photosuite-sdk.mjs
curl -O https://photosuite.flogvit.com/sdk/photosuite-sdk.d.mts

Importer ho med stien, og køyr skriptet med Bun (bun hello.ts) eller Node 24 (node hello.ts; på Node 22 node --experimental-strip-types hello.ts). Utan noko oppgitt finn ho appen på denne maskina og tokenet sjølv, frå fila appen skriv når API-et startar. Ferdige skript å starte frå, og SDK-fila, finn du i skriptsamlinga.

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`)

SDK-en er ordna slik appen er: 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. Kvar rute appen har, er ein metode på ein av dei.

Culling frå eit 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' })
}

Batchen set flagget. client.images.reject() slår det av og på for biletet på skjermen, slik tasten gjer, så eit skript som køyrer to gonger, ville teke bort sine eigne forkastingar att.

Redigering frå eit skript#

Kvar justering finst her, og det gjer kvart verktøy.

// 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'])

Masker er strukturar, ikkje rørsler, så dei er like lette å skrive som å teikne:

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 },
  },
])

Og verktøya som les biletet:

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

Kvart av dei seier nei heller enn å gjette når biletet ikkje gjev dei noko å gå etter, og seier det i svaret — ei ramme utan ein rett kant blir late vere i staden for å bli vippa tilfeldig.

Leveranse#

Pipelinen er ei kjede av steg, der kvart tek eit handtak og gjev eit nytt tilbake.

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() er steget som tyder noko, og det folk utelèt. Utan det lastar pipelinen RAW-en på nytt og teiknar han slik han vart teken — kvar korrigering, maske og flekkfjerning fotografen gjorde, fell bort på vegen til kunden, og ingenting feilar, for eit heilt greitt bilete kjem framleis ut i den andre enden.

Følgje med på kva som skjer#

Klienten koplar seg til hendingsstraumen til appen sjølv, med same token som alle andre kall. Kvar hending, og kva ho ber, er typa i EventMap i SDK-en. Eit feil eller manglande token avsluttar waitFor med 401 med ein gong, i staden for eit tidsavbrot.

client.on('develop_changed', (e) => console.log('edited', e.index))
client.on('rating_set', (e) => console.log(e.index, 'now has', e.rating, 'stars'))

Styre grensesnittet#

Visingar, overlegg og verktøymodusar finst òg på API-et, og det er det som gjer eit skjermbiletskript mogleg — kvart bilete i denne dokumentasjonen er teke av appen sjølv, styrt gjennom desse kalla.

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')

Kva panel som er framme#

Panel dokka på same stad er faner, og berre eitt av dei er på skjermen. Å gjere eit panel synleg gjer det ikkje til det du ser på, så det er to ulike ting å lese og eitt kall for å setje:

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 legg panelet til i oppsettet om det ikkje er der, så det endar alltid med det panelet i synet. Namna er folders, filmstrip, sidebar, histogram, viewer, develop, scopes og map.

Utan vindauge#

Ei maskin som berre leverer, som livebileta på eit stemne eller eit skript som går kvar natt, treng ikkje vindauget. Start appen utan vindauge og styr han berre gjennom API-et:

/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless

På Windows er det FLOGVIT.photosuite.exe --headless i mappa photosuite er installert i. Det er heile appen utan vindauge og utan ikon i Dock: dei same innstillingane, det same biblioteket, dei same workflowane og det same API-et, og han framkallar på skjermkortet som med vindauge. API-et svarar på porten i Settings sjølv når det er slått av der, for det er den einaste vegen inn, og client.status() seier headless: true og namnet på skjermkortet.

  • Lisensen må vere aktivert i appen éin gong på maskina.
  • Gje API-tokenet i CULLING_API_TOKEN eller i ei .env-fil i innstillingsmappa til appen (~/Library/Application Support/flogvit/ på Mac), så appen ikkje blir ståande og vente på nøkkelringen utan at nokon er der og svarar.
  • --screen 3840x2160@2 set skjermen bileta blir dekoda for og skjermbileta blir tekne i. Utan han er det ein Retina-berbar sin.
  • Skjermbilete og opptak verkar. Det som treng eit vindauge, som videoavspeling, fullskjerm, å flytte vindauget og menypunkt som opnar ein filveljar, svarar 409 og seier kvifor. Rutene tek ein sti i staden.
  • Ctrl-C eller kill stoppar han, og han lagrar som ved Quit.

Ei mappe for seg sjølv#

Starta med --dir bur photosuite i éi mappe og ser ingenting utanfor ho. Innstillingar, bibliotek, album, logg og modellar ligg i .photosuite/ i mappa, og i alt han får høyre og alt han svarar, er / mappa: /shoot er shoot i mappa, og ein sti som leier ut, med .. eller ei lenkje, vert nekta. Fleire kan køyre på same maskin, med kvar si mappe, til dømes éi per arrangement eller éi per kunde.

/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless --dir /Volumes/Events/nm-2026
  • Mappa må finnast, og berre éin instans kan køyre i ho om gongen.
  • Er porten i Settings oppteken, tek han ein ledig ein, og .photosuite/culling/api.json i mappa seier kva for ein. new CullingClient({ root: '<mappe>' }) les han, og --port 9200 vel porten sjølv. Han er oppe når client.status() seier root: true.
  • API-tokenet og andre løyndomar kjem frå miljøet, frå .env i mappa eller frå .photosuite/.env, aldri frå nøkkelringen. Det appen lagrar sjølv, ligg i .photosuite/secrets.json.
  • Lisensen er mappa sin eigen om ho har ein, elles maskina sin, som han les og aldri endrar.
  • Ingen disk utanfor mappa vert lista eller skanna, og ein denoise-profil som skriv utanfor ho, vert ikkje teken inn. Med vindauge opnar filveljarane i mappa og nektar eit val utanfor ho.
  • Instansane tek tur hos DxO PureRAW, så to køyrer han aldri samstundes.
  • API-et svarar berre på denne maskina. Programma instansen startar (programmet til ein denoise-profil, dei eksterne redigeringsprogramma) vert sette i innstillingane til mappa eller i vindauget, aldri over API-et, og den eigne .photosuite/ til instansen er ikkje ein stad API-et kan nemne. Backupane til biblioteket hamnar i FLOGVIT.photosuite library backups i mappa.
  • Skal dei køyre som tenester, får kvar sin launchd-agent: sin eigen Label, og --headless --dir <mappe> i ProgramArguments.
  • For å flytte eller kopiere ein instans: stopp han, flytt mappa og start han der. Han skriv dei lagra stiane over til den nye staden når han startar. For å slette ein: stopp han og slett mappa; ingenting av han ligg nokon annan stad.

Eit skript skrive for ein slik instans kallar requireRoot() først. Køyrt mot ein app utan --dir stoppar det med ei tydeleg melding i staden for å skrive til den eigne /delivery på maskina:

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' })

Kvar rute#

API-referansen listar dei alle, laga frå det eigne OpenAPI-dokumentet til appen, med felta kvar tek og ei curl-line for kvar.

esc
    ↑↓ naviger↵ opne