API

Allt gränssnittet kan göra kan ett skript göra: det lokala REST-API:t, token, TypeScript-SDK:t och exempel.

På den här sidan
1:45
Berättad på engelska, med undertexter. Spelas från YouTube när du trycker på uppspelning.

FLOGVIT.photosuite kör ett lokalt REST-API, och allt gränssnittet kan göra finns på det. Inte det mesta: ett test i appen får bygget att misslyckas när en route finns utan dokumentation, och ett annat när framkallningen får en inställning som API:t inte ger åtkomst till.

Det är hela poängen med regeln. Ett skript du skriver går inte runt appen — det använder samma dörr.

Kom igång#

API:t lyssnar på 127.0.0.1:9100 medan appen är öppen, och det kräver en bearer-token. Din dator är inte det enda som körs på din dator.

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

Sätter du CULLING_API_TOKEN innan du startar appen använder den den token i stället för den i nyckelringen — så kör du den obevakad, på en dator utan nyckelring att låsa upp.

Medan appen körs finns det också en interaktiv webbläsare på localhost:9100/docs, och själva OpenAPI-dokumentet på /api-doc/openapi.json. Båda är öppna utan token.

TypeScript-SDK:t#

SDK:n är en enda fil, och inget behöver installeras. Spara photosuite-sdk.mjs bredvid ditt skript, och photosuite-sdk.d.mts bredvid den om du vill ha typerna i editorn:

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

Importera den med sökvägen och kör skriptet med Bun (bun hello.ts) eller Node 24 (node hello.ts; på Node 22 node --experimental-strip-types hello.ts). Utan några uppgifter hittar den appen på den här datorn och dess token själv, i filen appen skriver när API:t startar. Färdiga skript att utgå från, och SDK-filen, finns 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:t är ordnat som appen är: 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. Varje route appen har är en metod på någon av dem.

Gallra från ett 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 sätter flaggan. client.images.reject() slår av och på den för bilden på skärmen, som tangenten gör, så ett skript som körs två gånger skulle ta bort sina egna förkastningar igen.

Redigera från ett skript#

Varje justering finns här, och det gör varje verktyg också.

// 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 är strukturer, inte gester, så de är lika lätta att skriva som att rita:

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

Och verktygen som läser bilden:

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

Vart och ett av dem säger nej hellre än att gissa när bilden inte ger dem något att gå på, och säger det i svaret — en bild utan en rak kant lämnas i fred i stället för att lutas på måfå.

Leverans#

Pipelinen är en kedja av steg, där varje steg tar ett handtag och ger tillbaka ett nytt.

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() är steget som betyder något, och det folk utelämnar. Utan det läser pipelinen in RAW-filen igen och ritar den som den togs — varje korrigering, mask och fläckborttagning fotografen gjorde faller bort på vägen till kunden, och inget misslyckas, för en fullt duglig bild kommer fortfarande ut i andra änden.

Följa vad som händer#

Klienten ansluter till appens händelseström av sig själv, med samma token som alla andra anrop. Varje händelse, och vad den bär, är typad i SDK:ts EventMap. En felaktig eller saknad token avslutar waitFor med 401 direkt i stället för en timeout.

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

Styra gränssnittet#

Vyer, överlägg och verktygslägen finns också på API:t, och det är vad som gör ett skärmbildsskript möjligt — varje bild i den här dokumentationen är tagen av appen själv, styrd genom de här anropen.

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

Vilken panel som ligger överst#

Paneler dockade på samma ställe är flikar, och bara en av dem syns på skärmen. Att göra en panel synlig gör den inte till den du tittar på, så det finns två olika saker att läsa och ett anrop för att ställa in:

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 lägger till panelen i layouten om den inte finns där, så det slutar alltid med att den panelen syns. Namnen är folders, filmstrip, sidebar, histogram, viewer, develop, scopes och map.

Utan fönster#

En dator som bara levererar, som livebilderna på en tävling eller ett skript som körs varje natt, behöver inte fönstret. Starta appen utan fönster och styr den bara genom API:t:

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

På Windows är det FLOGVIT.photosuite.exe --headless i mappen photosuite är installerat i. Det är hela appen utan fönster och utan ikon i Dock: samma inställningar, samma bibliotek, samma workflows och samma API, och den framkallar på grafikkortet som med fönster. API:t svarar på porten i Settings även när det är avstängt där, eftersom det är den enda vägen in, och client.status() säger headless: true och namnet på grafikkortet.

  • Licensen måste ha aktiverats i appen en gång på datorn.
  • Ange API-token i CULLING_API_TOKEN eller i en .env-fil i appens inställningsmapp (~/Library/Application Support/flogvit/ på Mac), så att appen inte blir stående och väntar på nyckelringen utan att någon finns där och svarar.
  • --screen 3840x2160@2 anger skärmen bilderna avkodas för och skärmbilderna tas i. Utan den är det en Retina-bärbars.
  • Skärmbilder och inspelningar fungerar. Det som behöver ett fönster, som videouppspelning, helskärm, att flytta fönstret och menyval som öppnar en filväljare, svarar 409 och säger varför. Rutterna tar en sökväg i stället.
  • Ctrl-C eller kill stoppar den, och den sparar som vid Quit.

En egen mapp#

Startad med --dir bor photosuite i en mapp och ser ingenting utanför den. Inställningar, bibliotek, album, logg och modeller ligger i mappens .photosuite/, och i allt den får höra och allt den svarar är / mappen: /shoot är mappens shoot, och en sökväg som leder ut, med .. eller en länk, nekas. Flera kan köras på samma dator, med var sin mapp, till exempel en per evenemang eller en per kund.

/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless --dir /Volumes/Events/nm-2026
  • Mappen måste finnas, och bara en instans kan köras i den åt gången.
  • Är porten i Settings upptagen tar den en ledig, och .photosuite/culling/api.json i mappen säger vilken. new CullingClient({ root: '<mapp>' }) läser den, och --port 9200 väljer porten själv. Den är uppe när client.status() säger root: true.
  • API-token och andra hemligheter kommer från miljön, från .env i mappen eller från .photosuite/.env, aldrig från nyckelringen. Det appen sparar själv ligger i .photosuite/secrets.json.
  • Licensen är mappens egen om den har en, annars datorns, som den läser och aldrig ändrar.
  • Ingen disk utanför mappen listas eller skannas, och en denoise-profil som skriver utanför den tas inte in. Med fönster öppnar filväljarna i mappen och nekar ett val utanför den.
  • Instanserna turas om hos DxO PureRAW, så två kör den aldrig samtidigt.
  • API:et svarar bara på den här datorn. Programmen instansen startar (en denoise-profils program, de externa redigeringsprogrammen) ställs in i mappens egna inställningar eller i fönstret, aldrig via API:et, och instansens egen .photosuite/ är inte en plats API:et kan ange. Bibliotekets säkerhetskopior hamnar i FLOGVIT.photosuite library backups i mappen.
  • Ska de köras som tjänster får var och en sin egen launchd-agent: sin egen Label, och --headless --dir <mapp> i ProgramArguments.
  • För att flytta eller kopiera en instans: stoppa den, flytta mappen och starta den där. Den skriver över sina sparade sökvägar till den nya platsen när den startar. För att ta bort en: stoppa den och ta bort mappen; ingenting av den finns kvar någon annanstans.

Ett skript skrivet för en sådan instans anropar requireRoot() först. Kört mot en app utan --dir stannar det med ett tydligt besked i stället för att skriva till datorns 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' })

Varje route#

API-referensen listar dem alla, genererad från appens eget OpenAPI-dokument, med fälten var och en tar och en curl-rad för varje.

esc
    ↑↓ navigera↵ öppna