API
Tout ce que fait l’interface, un script peut le faire : l’API REST locale, le jeton, le SDK TypeScript et des exemples.
1:45FLOGVIT.photosuite fait tourner une API REST locale, et tout ce que l’interface sait faire s’y trouve. Pas l’essentiel : tout. Un test de l’application fait échouer la compilation quand une route existe sans documentation, et un autre quand le développement gagne un réglage que l’API n’expose pas.
C’est tout l’intérêt de la règle. Un script que vous écrivez ne contourne pas l’application — il passe par la même porte.
Démarrer#
L’API écoute sur 127.0.0.1:9100 tant que l’application est ouverte, et elle demande un jeton bearer. Votre machine n’est pas la seule chose qui tourne sur votre machine.
export CULLING_API_TOKEN=$(culling --print-api-token)
curl -H "Authorization: Bearer $CULLING_API_TOKEN" \
http://127.0.0.1:9100/api/status
Définissez CULLING_API_TOKEN avant de lancer l’application et elle utilise ce jeton plutôt que celui du trousseau — c’est ainsi qu’on la fait tourner sans surveillance, sur une machine sans trousseau à déverrouiller.
Pendant que l’application tourne, il y a aussi un navigateur interactif sur localhost:9100/docs, et le document OpenAPI brut sur /api-doc/openapi.json. Les deux sont ouverts sans jeton.
Le SDK TypeScript#
Le SDK tient en un seul fichier, rien à installer. Enregistrez photosuite-sdk.mjs à côté de votre script, et photosuite-sdk.d.mts à côté si vous voulez les types dans votre éditeur :
curl -O https://photosuite.flogvit.com/sdk/photosuite-sdk.mjs
curl -O https://photosuite.flogvit.com/sdk/photosuite-sdk.d.mts
Importez-le par son chemin et lancez le script avec Bun (bun hello.ts) ou Node 24 (node hello.ts ; sous Node 22, node --experimental-strip-types hello.ts). Sans rien lui indiquer, il trouve tout seul l’application sur cette machine et son jeton, dans le fichier que l’application écrit au démarrage de son API. Des scripts prêts à l’emploi pour commencer, et le fichier du SDK, sont dans la collection de scripts.
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`)
Le SDK est organisé comme l’application : 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. Chaque route de l’application est une méthode de l’un d’eux.
Trier depuis un script#
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' })
}
Le lot pose le drapeau. client.images.reject() l’active ou le désactive sur l’image à l’écran, comme la touche, si bien qu’un script lancé deux fois retirerait ses propres rejets.
Retoucher depuis un script#
Chaque réglage est là, et chaque outil aussi.
// 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'])
Les masques sont des structures plutôt que des gestes, donc ils sont aussi faciles à écrire qu’à dessiner :
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 },
},
])
Et les outils qui lisent l’image :
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
Chacun refuse plutôt que de deviner quand l’image ne lui donne rien sur quoi s’appuyer, et le dit dans la réponse — un cadre sans bord droit est laissé tel quel au lieu d’être incliné au hasard.
Livraison#
Le pipeline est une chaîne d’étapes, chacune consommant une poignée et en rendant une nouvelle.
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() est l’étape qui compte, et celle qu’on oublie. Sans elle, le pipeline recharge le RAW et le rend tel que pris — chaque correction, masque et retouche de taches du photographe disparaît en route vers le client, et rien n’échoue, car une image tout à fait correcte sort quand même à l’autre bout.
Suivre ce qui se passe#
Le client se connecte tout seul au flux d’événements de l’application, avec le même jeton que pour tout autre appel. Chaque événement, et ce qu’il porte, est typé dans l’EventMap du SDK. Un jeton faux ou absent termine waitFor aussitôt par une 401 plutôt que par un délai dépassé.
client.on('develop_changed', (e) => console.log('edited', e.index))
client.on('rating_set', (e) => console.log(e.index, 'now has', e.rating, 'stars'))
Piloter l’interface#
Les vues, les surimpressions et les modes d’outil sont aussi dans l’API, ce qui rend possible un script de captures d’écran — chaque image de cette documentation est prise par l’application elle-même, pilotée par ces appels.
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')
Quel panneau est devant#
Les panneaux ancrés au même endroit sont des onglets, et un seul est à l’écran. Rendre un panneau visible n’en fait pas celui que vous regardez ; il y a donc deux faits distincts à lire et un appel pour régler :
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 ajoute le panneau à la disposition s’il n’y est pas, et finit donc toujours avec ce panneau à l’écran. Les noms sont folders, filmstrip, sidebar, histogram, viewer, develop, scopes et map.
Sans fenêtre#
Une machine qui ne fait que livrer, comme les photos en direct d’une compétition ou un script lancé chaque nuit, n’a pas besoin de la fenêtre. Lancez l’app sans fenêtre et pilotez-la uniquement par l’API :
/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless
Sous Windows, c’est FLOGVIT.photosuite.exe --headless dans le dossier où photosuite est installé. C’est l’app entière, sans fenêtre et sans icône dans le Dock : les mêmes réglages, la même bibliothèque, les mêmes workflows et la même API, avec le développement sur la carte graphique comme avec une fenêtre. L’API répond sur le port indiqué dans Settings même quand elle y est désactivée, puisque c’est le seul accès, et client.status() indique headless: true et le nom de la carte graphique.
- La licence doit avoir été activée une fois dans l’app sur cette machine.
- Donnez le jeton de l’API dans
CULLING_API_TOKENou dans un fichier.envdu dossier de réglages de l’app (~/Library/Application Support/flogvit/sur Mac), pour que l’app n’attende pas le trousseau sans personne pour lui répondre. --screen 3840x2160@2fixe l’écran pour lequel les photos sont décodées et dans lequel les captures sont prises. Sans cela, celui d’un portable Retina.- Les captures d’écran et les enregistrements fonctionnent. Ce qui a besoin d’une fenêtre, comme la lecture vidéo, le plein écran, le déplacement de la fenêtre et les éléments de menu qui ouvrent un sélecteur de fichiers, répond 409 et dit pourquoi. Les routes prennent un chemin à la place.
- Ctrl-C ou
killl’arrête, et elle enregistre comme avec Quit.
Un dossier à elle#
Lancée avec --dir, photosuite vit dans un seul dossier et ne voit rien en dehors. Ses réglages, sa bibliothèque, ses albums, son journal et ses modèles sont gardés dans le .photosuite/ du dossier, et dans tout ce qu’on lui dit et tout ce qu’elle répond, / est le dossier : /shoot est le shoot du dossier, et un chemin qui en sort, par .. ou par un lien, est refusé. Plusieurs peuvent tourner sur la même machine, chacune dans son dossier, par exemple une par événement ou une par client.
/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless --dir /Volumes/Events/nm-2026
- Le dossier doit exister, et une seule instance peut y tourner à la fois.
- Quand le port de Settings est pris, elle en prend un libre, et
.photosuite/culling/api.jsondans le dossier dit lequel.new CullingClient({ root: '<dossier>' })le lit, et--port 9200choisit le port vous-même. Elle est prête quandclient.status()ditroot: true. - Le jeton de l’API et les autres secrets viennent de l’environnement, du
.envdu dossier ou de.photosuite/.env, jamais du trousseau. Ce que l’app enregistre elle-même est gardé dans.photosuite/secrets.json. - La licence est celle du dossier s’il en a une, sinon celle de la machine, qu’elle lit sans jamais la modifier.
- Aucun disque hors du dossier n’est listé ni analysé, et un profil de débruitage qui écrit en dehors n’est pas repris. Avec une fenêtre, les sélecteurs de fichiers s’ouvrent dans le dossier et refusent un choix en dehors.
- Les instances passent chacune leur tour chez DxO PureRAW, si bien que deux ne le lancent jamais en même temps.
- L’API ne répond que sur cette machine. Les programmes que l’instance lance (celui d’un profil de débruitage, les éditeurs externes) se règlent dans les réglages du dossier ou dans la fenêtre, jamais par l’API, et le
.photosuite/de l’instance n’est pas un endroit que l’API peut nommer. Les sauvegardes de la bibliothèque vont dansFLOGVIT.photosuite library backupsdans le dossier. - Pour les faire tourner comme services, donnez à chacune son agent launchd : son propre
Label, et--headless --dir <dossier>dansProgramArguments. - Pour déplacer ou copier une instance : arrêtez-la, déplacez le dossier et lancez-la là. Elle réécrit ses chemins enregistrés vers le nouvel emplacement au démarrage. Pour en supprimer une : arrêtez-la et supprimez le dossier ; rien d’elle n’est gardé ailleurs.
Un script écrit pour une telle instance appelle d’abord requireRoot(). Lancé contre une app sans --dir, il s’arrête avec un message clair au lieu d’écrire dans le /delivery de la machine :
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' })
Toutes les routes#
La référence de l’API les liste toutes, générée à partir du document OpenAPI de l’application, avec les champs que chacune prend et une ligne curl pour chacune.