ported from the standalone officer-atelier service: providers (adapter-shaped, openai the only adapter today), the generation queue, collections, and the database layer — kept on its own Postgres database rather than folded into officer_db, since both collections and provider_settings had rows on the machine this was extracted from. what changed rather than just moved: - images are served from an authenticated GET /files/:filename instead of an unauthenticated PUBLIC_URL-prefixed static route - the OpenAI-key bootstrap fallback reads the secret store (getSecret) before .env, per-adapter - provider_settings (the one-row-per-adapter shape providers replaced) did not come along — it has no callers left anywhere in the server code, only a migration script that also did not move ping/echo (smoke-test routes with no caller) did not move either; the sidecar's own /_health covers liveness. not yet wired into the platform: sidecar/index.ts exists but nothing mounts it, and web/ has not been written, so this is not yet a discoverable plugin.
53 lines
2.1 KiB
TypeScript
53 lines
2.1 KiB
TypeScript
import sharp from 'sharp';
|
|
|
|
export type ImageFacts = {
|
|
width: number | null;
|
|
height: number | null;
|
|
hasAlpha: boolean | null;
|
|
opaque: boolean | null;
|
|
};
|
|
|
|
const UNKNOWN: ImageFacts = { width: null, height: null, hasAlpha: null, opaque: null };
|
|
|
|
/**
|
|
* Measure the file that was actually written.
|
|
*
|
|
* Read from disk rather than echoed from the request, because the request is what was asked for and
|
|
* this is what arrived — and those are precisely the two things that come apart. The caller is an
|
|
* agent that cannot look at the image, so anything it is not told, it cannot know.
|
|
*
|
|
* `hasAlpha` and `opaque` answer different questions and the gap between them is the point:
|
|
*
|
|
* hasAlpha: false no alpha channel at all
|
|
* hasAlpha: true, opaque: false transparency present and used
|
|
* hasAlpha: true, opaque: true an alpha channel the model never used
|
|
*
|
|
* The third is the failure worth catching. `background: "transparent"` is accepted, the job succeeds,
|
|
* and every pixel comes back fully opaque — which for an app icon is the difference between usable and
|
|
* a black square, and is invisible from both ends without this field.
|
|
*
|
|
* Never throws. A generation that produced real bytes must not be recorded as failed because the
|
|
* measurement of it did; nulls say "not known" and the image is still there.
|
|
*/
|
|
export async function inspectImage(path: string): Promise<ImageFacts> {
|
|
try {
|
|
const image = sharp(path);
|
|
const metadata = await image.metadata();
|
|
|
|
// stats() decodes the pixels — the only way to answer "is anything actually transparent", since
|
|
// the header can only say whether a channel exists. Skipped when there is no alpha channel at
|
|
// all, where the answer is already known and the decode would be waste.
|
|
const opaque = metadata.hasAlpha ? (await image.stats()).isOpaque : true;
|
|
|
|
return {
|
|
width: metadata.width ?? null,
|
|
height: metadata.height ?? null,
|
|
hasAlpha: metadata.hasAlpha ?? null,
|
|
opaque,
|
|
};
|
|
} catch (error) {
|
|
console.error(`[inspect] could not read ${path}:`, error instanceof Error ? error.message : error);
|
|
return UNKNOWN;
|
|
}
|
|
}
|