API

Alt grensesnittet kan gjøre, kan et skript gjøre: det lokale REST-API-et, token, TypeScript-SDK-en og eksempler.

På denne siden
1:45
Fortalt på engelsk, med undertekster. Spilles fra YouTube når du trykker på avspilling.

FLOGVIT.photosuite kjører et lokalt REST-API, og alt grensesnittet kan gjøre, finnes på det. Ikke det meste: en test i appen feiler bygget når en rute finnes uten dokumentasjon, og en annen feiler når fremkallingen får en innstilling API-et ikke gir tilgang til.

Det er hele poenget med regelen. Et skript du skriver, går ikke rundt appen — det bruker den samme døra.

Kom i gang#

API-et lytter på 127.0.0.1:9100 mens appen er åpen, og det krever et bearer-token. Maskinen din er ikke det eneste som kjører på maskinen din.

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

Setter du CULLING_API_TOKEN før du starter appen, bruker den det tokenet i stedet for det i nøkkelringen — slik kjører du den ubetjent, på en maskin uten nøkkelring å låse opp.

Mens appen kjører, finnes det også en interaktiv nettleser på localhost:9100/docs, og selve OpenAPI-dokumentet på /api-doc/openapi.json. Begge er åpne uten token.

TypeScript-SDK-en#

SDK-en er én fil, og ingenting må installeres. Lagre photosuite-sdk.mjs ved siden av skriptet ditt, og photosuite-sdk.d.mts ved siden av den hvis du vil ha typene i editoren:

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

Importer den med stien, og kjør skriptet med Bun (bun hello.ts) eller Node 24 (node hello.ts; på Node 22 node --experimental-strip-types hello.ts). Uten noe oppgitt finner den appen på denne maskinen og tokenet selv, fra fila appen skriver når API-et starter. Ferdige skript å starte fra, og SDK-fila, finner du i skriptsamlingen.

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 ordnet 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. Hver rute appen har, er en metode på en av dem.

Culling fra et 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 setter flagget. client.images.reject() slår det av og på for bildet på skjermen, slik tasten gjør, så et skript som kjører to ganger, ville tatt bort sine egne forkastninger igjen.

Redigering fra et skript#

Hver justering finnes her, og det gjør hvert 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 strukturer, ikke bevegelser, så de er like lette å skrive som å tegne:

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øyene som leser bildet:

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

Hvert av dem sier nei heller enn å gjette når bildet ikke gir dem noe å gå etter, og sier det i svaret — en ramme uten en rett kant blir latt være i stedet for å vippes tilfeldig.

Leveranse#

Pipelinen er en kjede av steg, der hvert tar et håndtak og gir et 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 betyr noe, og det folk utelater. Uten det laster pipelinen RAW-en på nytt og tegner den slik den ble tatt — hver korrigering, maske og flekkfjerning fotografen gjorde, faller bort på veien til kunden, og ingenting feiler, for et helt greit bilde kommer fortsatt ut i den andre enden.

Følge med på hva som skjer#

Klienten kobler seg til appens hendelsesstrøm selv, med samme token som alle andre kall. Hver hendelse, og hva den bærer, er typet i SDK-ens EventMap. Et feil eller manglende token avslutter waitFor med 401 med én gang, i stedet for et tidsavbrudd.

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#

Visninger, overlegg og verktøymoduser finnes også på API-et, og det er det som gjør et skjermbildeskript mulig — hvert bilde i denne dokumentasjonen er tatt av appen selv, styrt gjennom disse kallene.

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

Hvilket panel som er foran#

Paneler dokket på samme sted er faner, og bare ett av dem er på skjermen. Å gjøre et panel synlig gjør det ikke til det du ser på, så det er to ulike ting å lese og ett kall for å sette:

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 legger panelet til i oppsettet hvis det ikke er der, så det ender alltid med det panelet i synet. Navnene er folders, filmstrip, sidebar, histogram, viewer, develop, scopes og map.

Uten vindu#

En maskin som bare leverer, som livebildene på et stevne eller et skript som går hver natt, trenger ikke vinduet. Start appen uten vindu og styr den bare 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 hele appen uten vindu og uten ikon i Dock: de samme innstillingene, det samme biblioteket, de samme workflowene og det samme API-et, og den fremkaller på skjermkortet som med vindu. API-et svarer på porten i Settings selv når det er slått av der, for det er den eneste veien inn, og client.status() sier headless: true og navnet på skjermkortet.

  • Lisensen må være aktivert i appen én gang på maskinen.
  • Gi API-tokenet i CULLING_API_TOKEN eller i en .env-fil i appens innstillingsmappe (~/Library/Application Support/flogvit/ på Mac), så appen ikke blir stående og vente på nøkkelringen uten at noen er der og svarer.
  • --screen 3840x2160@2 setter skjermen bildene dekodes for og skjermbildene tas i. Uten den er det en Retina-bærbar sin.
  • Skjermbilder og opptak virker. Det som trenger et vindu, som videoavspilling, fullskjerm, å flytte vinduet og menypunkter som åpner en filvelger, svarer 409 og sier hvorfor. Rutene tar en sti i stedet.
  • Ctrl-C eller kill stopper den, og den lagrer som ved Quit.

En mappe for seg selv#

Startet med --dir bor photosuite i én mappe og ser ingenting utenfor den. Innstillinger, bibliotek, album, logg og modeller ligger i mappas .photosuite/, og i alt den får høre og alt den svarer, er / mappa: /shoot er mappas shoot, og en sti som leder ut, med .. eller en lenke, nektes. Flere kan kjøre på samme maskin, med hver sin mappe, for eksempel én per arrangement eller én per kunde.

/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless --dir /Volumes/Events/nm-2026
  • Mappa må finnes, og bare én instans kan kjøre i den om gangen.
  • Er porten i Settings opptatt, tar den en ledig en, og .photosuite/culling/api.json i mappa sier hvilken. new CullingClient({ root: '<mappe>' }) leser den, og --port 9200 velger porten selv. Den er oppe når client.status() sier root: true.
  • API-tokenet og andre hemmeligheter kommer fra miljøet, fra .env i mappa eller fra .photosuite/.env, aldri fra nøkkelringen. Det appen lagrer selv, ligger i .photosuite/secrets.json.
  • Lisensen er mappas egen hvis den har en, ellers maskinens, som den leser og aldri endrer.
  • Ingen disk utenfor mappa listes eller skannes, og en denoise-profil som skriver utenfor den, blir ikke tatt inn. Med vindu åpner filvelgerne i mappa og nekter et valg utenfor den.
  • Instansene tar tur hos DxO PureRAW, så to kjører den aldri samtidig.
  • API-et svarer bare på denne maskinen. Programmene instansen starter (en denoise-profils program, de eksterne redigeringsprogrammene) settes i mappas egne innstillinger eller i vinduet, aldri over API-et, og instansens egen .photosuite/ er ikke et sted API-et kan nevne. Bibliotekets backuper havner i FLOGVIT.photosuite library backups i mappa.
  • Skal de kjøre som tjenester, får hver sin launchd-agent: sin egen Label, og --headless --dir <mappe> i ProgramArguments.
  • For å flytte eller kopiere en instans: stopp den, flytt mappa og start den der. Den skriver de lagrede stiene over til det nye stedet når den starter. For å slette en: stopp den og slett mappa; ingenting av den ligger noe annet sted.

Et skript skrevet for en slik instans kaller requireRoot() først. Kjørt mot en app uten --dir stopper det med en tydelig beskjed i stedet for å skrive til maskinens egen /delivery:

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

Hver rute#

API-referansen lister dem alle, laget fra appens eget OpenAPI-dokument, med feltene hver tar og en curl-linje for hver.

esc
    ↑↓ naviger↵ åpne