API
Todo lo que hace la interfaz lo puede hacer un script: la API REST local, el token, el SDK de TypeScript y ejemplos.
1:45FLOGVIT.photosuite ejecuta una API REST local, y todo lo que puede hacer la interfaz está en ella. No la mayor parte: todo. Una prueba de la aplicación hace fallar la compilación cuando existe una ruta sin documentación, y otra cuando el revelado gana un ajuste que la API no expone.
Ese es el sentido de la regla. Un script que escribes no rodea la aplicación — usa la misma puerta.
Primeros pasos#
La API escucha en 127.0.0.1:9100 mientras la aplicación está abierta, y necesita un token bearer. Tu máquina no es lo único que se ejecuta en tu máquina.
export CULLING_API_TOKEN=$(culling --print-api-token)
curl -H "Authorization: Bearer $CULLING_API_TOKEN" \
http://127.0.0.1:9100/api/status
Define CULLING_API_TOKEN antes de iniciar la aplicación y usará ese token en lugar del del llavero — así se ejecuta sin supervisión, en una máquina sin llavero que desbloquear.
Con la aplicación en marcha hay también un navegador interactivo en localhost:9100/docs, y el documento OpenAPI en bruto en /api-doc/openapi.json. Ambos están abiertos sin token.
El SDK de TypeScript#
El SDK es un solo archivo y no hay nada que instalar. Guarda photosuite-sdk.mjs junto a tu script, y photosuite-sdk.d.mts a su lado si quieres los tipos en tu editor:
curl -O https://photosuite.flogvit.com/sdk/photosuite-sdk.mjs
curl -O https://photosuite.flogvit.com/sdk/photosuite-sdk.d.mts
Impórtalo por su ruta y ejecuta el script con Bun (bun hello.ts) o Node 24 (node hello.ts; en Node 22, node --experimental-strip-types hello.ts). Sin indicarle nada, encuentra por sí mismo la aplicación en este equipo y su token, en el archivo que la aplicación escribe al arrancar su API. Scripts listos para empezar, y el archivo del SDK, están en la colección 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`)
El SDK está organizado como la aplicación: 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. Cada ruta de la aplicación es un método de uno de ellos.
Seleccionar desde 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' })
}
El lote pone la bandera. client.images.reject() la activa o desactiva en la foto de la pantalla, como la tecla, así que un script ejecutado dos veces quitaría sus propios rechazos.
Editar desde un script#
Está cada ajuste, y también cada herramienta.
// 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'])
Las máscaras son estructuras, no gestos, así que son tan fáciles de escribir como de dibujar:
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 },
},
])
Y las herramientas que leen la foto:
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
Cada una se niega en lugar de adivinar cuando la foto no le da nada en que apoyarse, y lo dice en la respuesta — un encuadre sin un borde recto se deja como está en lugar de inclinarse al azar.
Entrega#
El pipeline es una cadena de pasos, cada uno consume un identificador y devuelve uno nuevo.
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() es el paso que importa y el que la gente omite. Sin él, el pipeline vuelve a cargar el RAW y lo renderiza tal como se tomó — cada corrección, máscara y retoque de manchas del fotógrafo se pierde camino del cliente, y nada falla, porque al otro lado sigue saliendo una foto perfectamente correcta.
Seguir lo que pasa#
El cliente se conecta solo al flujo de eventos de la aplicación, con el mismo token que cualquier otra llamada. Cada evento, y lo que lleva, está tipado en el EventMap del SDK. Un token erróneo o ausente termina waitFor con un 401 al momento en lugar de con un tiempo de espera.
client.on('develop_changed', (e) => console.log('edited', e.index))
client.on('rating_set', (e) => console.log(e.index, 'now has', e.rating, 'stars'))
Manejar la interfaz#
Las vistas, las capas y los modos de herramienta también están en la API, y eso es lo que hace posible un script de capturas — cada imagen de esta documentación la toma la propia aplicación, manejada con estas llamadas.
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')
Qué panel está delante#
Los paneles acoplados en el mismo sitio son pestañas, y solo uno está en pantalla. Hacer visible un panel no lo convierte en el que estás mirando, así que hay dos datos distintos que leer y una llamada para fijarlo:
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 añade el panel a la distribución si no está, así que siempre termina con ese panel a la vista. Los nombres son folders, filmstrip, sidebar, histogram, viewer, develop, scopes y map.
Sin ventana#
Un equipo que solo entrega, como las fotos en directo de una competición o un script que se ejecuta cada noche, no necesita la ventana. Inicia la app sin ventana y manéjala solo a través de la API:
/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless
En Windows es FLOGVIT.photosuite.exe --headless en la carpeta donde está instalado photosuite. Es la app entera sin ventana y sin icono en el Dock: los mismos ajustes, la misma biblioteca, los mismos workflows y la misma API, y revela en la tarjeta gráfica igual que con ventana. La API responde en el puerto de Settings aunque esté desactivada ahí, porque es la única forma de entrar, y client.status() indica headless: true y el nombre de la tarjeta gráfica.
- La licencia tiene que haberse activado una vez en la app en ese equipo.
- Pasa el token de la API en
CULLING_API_TOKENo en un archivo.envde la carpeta de ajustes de la app (~/Library/Application Support/flogvit/en Mac), para que la app no se quede esperando al llavero sin nadie que le responda. --screen 3840x2160@2fija la pantalla para la que se decodifican las fotos y en la que se toman las capturas. Sin ello, la de un portátil Retina.- Las capturas y las grabaciones funcionan. Lo que necesita una ventana, como la reproducción de vídeo, la pantalla completa, mover la ventana y los elementos de menú que abren un selector de archivos, responde 409 y dice por qué. Las rutas aceptan una ruta de archivo en su lugar.
- Ctrl-C o
killla detiene, y guarda como con Quit.
Una carpeta propia#
Iniciada con --dir, photosuite vive en una sola carpeta y no ve nada fuera de ella. Sus ajustes, biblioteca, álbumes, registro y modelos se guardan en el .photosuite/ de la carpeta, y en todo lo que se le dice y todo lo que responde, / es la carpeta: /shoot es el shoot de la carpeta, y una ruta que sale de ella, con .. o con un enlace, se rechaza. Pueden funcionar varias en el mismo equipo, cada una con su carpeta, por ejemplo una por evento o una por cliente.
/Applications/FLOGVIT.photosuite.app/Contents/MacOS/culling --headless --dir /Volumes/Events/nm-2026
- La carpeta tiene que existir, y solo una instancia puede funcionar en ella a la vez.
- Si el puerto de Settings está ocupado, toma uno libre, y
.photosuite/culling/api.jsonen la carpeta dice cuál.new CullingClient({ root: '<carpeta>' })lo lee, y--port 9200elige el puerto usted mismo. Está lista cuandoclient.status()diceroot: true. - El token de la API y los demás secretos vienen del entorno, del
.envde la carpeta o de.photosuite/.env, nunca del llavero. Lo que la app guarda por su cuenta va a.photosuite/secrets.json. - La licencia es la de la carpeta si tiene una; si no, la del equipo, que lee y nunca cambia.
- Ningún disco fuera de la carpeta se lista ni se escanea, y un perfil de reducción de ruido que escribe fuera no se recoge. Con ventana, los selectores de archivos se abren en la carpeta y rechazan una elección fuera de ella.
- Las instancias se turnan con DxO PureRAW, así que dos nunca lo ejecutan a la vez.
- La API solo responde en este equipo. Los programas que la instancia inicia (el de un perfil de reducción de ruido, los editores externos) se configuran en los ajustes propios de la carpeta o en la ventana, nunca por la API, y el
.photosuite/de la instancia no es un sitio que la API pueda nombrar. Las copias de seguridad de la biblioteca van aFLOGVIT.photosuite library backupsdentro de la carpeta. - Para ejecutarlas como servicios, dé a cada una su propio agente de launchd: su propio
Label, y--headless --dir <carpeta>enProgramArguments. - Para mover o copiar una instancia: deténgala, mueva la carpeta e iníciela allí. Al arrancar reescribe sus rutas guardadas al nuevo lugar. Para eliminar una: deténgala y elimine la carpeta; nada de ella se guarda en otro sitio.
Un script escrito para una instancia así llama primero a requireRoot(). Ejecutado contra una app sin --dir, se detiene con un mensaje claro en lugar de escribir en el /delivery del propio equipo:
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' })
Todas las rutas#
La referencia de la API las lista todas, generada a partir del documento OpenAPI de la propia aplicación, con los campos que acepta cada una y una línea curl para cada una.