fix(lint): resolve #280 Lint failures (route formatting + landing import sort)

#280 left 10 section-prefixed custom-route files mis-indented and one
unsorted import block in the new landing section-index page. Fixed via
biome formatter (api) and manual import sort (landing). No config change
(biome.json is hook-protected); no suppression. pnpm lint + typecheck
now exit 0.
This commit is contained in:
SnapOtter
2026-06-21 02:58:46 +08:00
parent 71fefc05b0
commit 6e1b9cd3cc
11 changed files with 1016 additions and 986 deletions
+80 -77
View File
@@ -90,86 +90,89 @@ registerAiJobHandler("auto-subtitles", async (input, data, ctx) => {
});
export function registerAutoSubtitles(app: FastifyInstance) {
app.post("/api/v1/tools/video/auto-subtitles", async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "auto-subtitles";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let filename = "video";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
});
}
if (!inputKey) {
return reply.status(400).send({ error: "No video file provided" });
}
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply.status(400).send({
error: "Invalid settings",
details: formatZodErrors(result.error.issues),
app.post(
"/api/v1/tools/video/auto-subtitles",
async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "auto-subtitles";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
const progressJobId = clientJobId || jobId;
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let filename = "video";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
});
}
return reply.status(202).send({ jobId: progressJobId, async: true });
});
if (!inputKey) {
return reply.status(400).send({ error: "No video file provided" });
}
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply.status(400).send({
error: "Invalid settings",
details: formatZodErrors(result.error.issues),
});
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
const progressJobId = clientJobId || jobId;
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
return reply.status(202).send({ jobId: progressJobId, async: true });
},
);
}
+158 -155
View File
@@ -79,189 +79,192 @@ function buildOverlaySvg(
* Read barcodes (all 1D + 2D types) from uploaded images using zxing-wasm.
*/
export function registerBarcodeRead(app: FastifyInstance) {
app.post("/api/v1/tools/image/barcode-read", async (request: FastifyRequest, reply: FastifyReply) => {
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
app.post(
"/api/v1/tools/image/barcode-read",
async (request: FastifyRequest, reply: FastifyReply) => {
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
// --- Parse multipart ---
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const chunks: Buffer[] = [];
for await (const chunk of part.file) {
chunks.push(chunk);
// --- Parse multipart ---
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const chunks: Buffer[] = [];
for await (const chunk of part.file) {
chunks.push(chunk);
}
fileBuffer = Buffer.concat(chunks);
filename = sanitizeFilename(part.filename ?? "image");
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
}
fileBuffer = Buffer.concat(chunks);
filename = sanitizeFilename(part.filename ?? "image");
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: err instanceof Error ? err.message : String(err),
});
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: err instanceof Error ? err.message : String(err),
});
}
if (!fileBuffer || fileBuffer.length === 0) {
return reply.status(400).send({ error: "No image file provided" });
}
// --- Validate ---
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({
error: `Invalid image: ${validation.reason}`,
});
}
// Parse and validate settings
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
if (!fileBuffer || fileBuffer.length === 0) {
return reply.status(400).send({ error: "No image file provided" });
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
try {
const tryHarder = settings.tryHarder;
// --- Validate ---
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({
error: `Invalid image: ${validation.reason}`,
});
}
if (validation.format === "heif") {
try {
fileBuffer = await decodeHeic(fileBuffer);
} catch (err) {
return reply.status(422).send({
error: "Failed to decode HEIC file. Ensure libheif-examples is installed.",
details: err instanceof Error ? err.message : String(err),
});
// Parse and validate settings
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
if (needsCliDecode(validation.format)) {
try {
const fileExt = filename.split(".").pop()?.toLowerCase();
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format, fileExt);
} catch {
try {
const tryHarder = settings.tryHarder;
if (validation.format === "heif") {
try {
await sharp(fileBuffer).metadata();
fileBuffer = await decodeHeic(fileBuffer);
} catch (err) {
return reply.status(422).send({
error: `Failed to decode ${validation.format.toUpperCase()} file`,
error: "Failed to decode HEIC file. Ensure libheif-examples is installed.",
details: err instanceof Error ? err.message : String(err),
});
}
}
}
if (validation.format === "svg") {
try {
fileBuffer = decompressSvgz(fileBuffer);
fileBuffer = sanitizeSvg(fileBuffer);
} catch (err) {
return reply.status(400).send({
error: err instanceof Error ? err.message : "Invalid SVG",
if (needsCliDecode(validation.format)) {
try {
const fileExt = filename.split(".").pop()?.toLowerCase();
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format, fileExt);
} catch {
try {
await sharp(fileBuffer).metadata();
} catch (err) {
return reply.status(422).send({
error: `Failed to decode ${validation.format.toUpperCase()} file`,
details: err instanceof Error ? err.message : String(err),
});
}
}
}
if (validation.format === "svg") {
try {
fileBuffer = decompressSvgz(fileBuffer);
fileBuffer = sanitizeSvg(fileBuffer);
} catch (err) {
return reply.status(400).send({
error: err instanceof Error ? err.message : "Invalid SVG",
});
}
}
fileBuffer = await autoOrient(fileBuffer);
// Convert to raw RGBA pixel data
const image = sharp(fileBuffer);
const metadata = await image.metadata();
const width = metadata.width ?? 0;
const height = metadata.height ?? 0;
if (width === 0 || height === 0) {
return reply.status(422).send({
error: "Could not determine image dimensions",
});
}
}
fileBuffer = await autoOrient(fileBuffer);
// Convert to raw RGBA pixel data
const image = sharp(fileBuffer);
const metadata = await image.metadata();
const width = metadata.width ?? 0;
const height = metadata.height ?? 0;
const rawData = await image.ensureAlpha().raw().toBuffer();
if (width === 0 || height === 0) {
return reply.status(422).send({
error: "Could not determine image dimensions",
// --- Detect barcodes via zxing-wasm ---
const imageData = {
data: new Uint8ClampedArray(rawData.buffer, rawData.byteOffset, rawData.length),
width,
height,
} as ImageData;
const results = await readBarcodes(imageData, {
tryHarder,
maxNumberOfSymbols: 255,
});
}
const rawData = await image.ensureAlpha().raw().toBuffer();
const validResults = results.filter((r) => r.isValid);
// --- Detect barcodes via zxing-wasm ---
const imageData = {
data: new Uint8ClampedArray(rawData.buffer, rawData.byteOffset, rawData.length),
width,
height,
} as ImageData;
const results = await readBarcodes(imageData, {
tryHarder,
maxNumberOfSymbols: 255,
});
const validResults = results.filter((r) => r.isValid);
// Map to the response shape
const barcodes = validResults.map((r) => ({
type: r.format,
text: r.text,
position: {
topLeft: { x: r.position.topLeft.x, y: r.position.topLeft.y },
topRight: { x: r.position.topRight.x, y: r.position.topRight.y },
bottomLeft: {
x: r.position.bottomLeft.x,
y: r.position.bottomLeft.y,
// Map to the response shape
const barcodes = validResults.map((r) => ({
type: r.format,
text: r.text,
position: {
topLeft: { x: r.position.topLeft.x, y: r.position.topLeft.y },
topRight: { x: r.position.topRight.x, y: r.position.topRight.y },
bottomLeft: {
x: r.position.bottomLeft.x,
y: r.position.bottomLeft.y,
},
bottomRight: {
x: r.position.bottomRight.x,
y: r.position.bottomRight.y,
},
},
bottomRight: {
x: r.position.bottomRight.x,
y: r.position.bottomRight.y,
},
},
}));
}));
// No barcodes found - return early
if (barcodes.length === 0) {
return reply.send({
filename,
barcodes: [],
annotatedUrl: null,
previewUrl: null,
});
}
// --- Generate annotated image ---
const jobId = randomUUID();
// Save original input
await putObject(`uploads/${jobId}/${filename}`, fileBuffer);
// Build SVG overlay with bounding boxes
const overlaySvg = buildOverlaySvg(width, height, barcodes);
const stem = filename.replace(/\.[^.]+$/, "");
const outputFilename = `annotated-${stem}.png`;
const annotatedBuffer = await sharp(fileBuffer)
.composite([{ input: Buffer.from(overlaySvg), top: 0, left: 0 }])
.png()
.toBuffer();
await putObject(`outputs/${jobId}/${outputFilename}`, annotatedBuffer);
const downloadUrl = `/api/v1/download/${jobId}/${encodeURIComponent(outputFilename)}`;
// No barcodes found - return early
if (barcodes.length === 0) {
return reply.send({
filename,
barcodes: [],
annotatedUrl: null,
previewUrl: null,
barcodes,
annotatedUrl: downloadUrl,
previewUrl: downloadUrl,
});
} catch (err) {
request.log.error({ err, toolId: "barcode-read" }, "Barcode read failed");
return reply.status(422).send({
error: "Barcode reading failed",
details: err instanceof Error ? err.message : "Unknown error",
});
}
// --- Generate annotated image ---
const jobId = randomUUID();
// Save original input
await putObject(`uploads/${jobId}/${filename}`, fileBuffer);
// Build SVG overlay with bounding boxes
const overlaySvg = buildOverlaySvg(width, height, barcodes);
const stem = filename.replace(/\.[^.]+$/, "");
const outputFilename = `annotated-${stem}.png`;
const annotatedBuffer = await sharp(fileBuffer)
.composite([{ input: Buffer.from(overlaySvg), top: 0, left: 0 }])
.png()
.toBuffer();
await putObject(`outputs/${jobId}/${outputFilename}`, annotatedBuffer);
const downloadUrl = `/api/v1/download/${jobId}/${encodeURIComponent(outputFilename)}`;
return reply.send({
filename,
barcodes,
annotatedUrl: downloadUrl,
previewUrl: downloadUrl,
});
} catch (err) {
request.log.error({ err, toolId: "barcode-read" }, "Barcode read failed");
return reply.status(422).send({
error: "Barcode reading failed",
details: err instanceof Error ? err.message : "Unknown error",
});
}
});
},
);
}
+109 -106
View File
@@ -65,119 +65,122 @@ registerAiJobHandler("blur-faces", async (input, data, ctx) => {
/** Face detection and blurring route. */
export function registerBlurFaces(app: FastifyInstance) {
app.post("/api/v1/tools/image/blur-faces", async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "blur-faces";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
app.post(
"/api/v1/tools/image/blur-faces",
async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "blur-faces";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
});
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
fileBuffer = await getObjectBuffer(inputKey);
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
try {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
}
if (needsCliDecode(validation.format)) {
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
}
fileBuffer = await autoOrient(fileBuffer);
} catch (err) {
request.log.error({ err, toolId: "blur-faces" }, "Input decoding failed");
return reply.status(422).send({
error: "Face blur failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
});
}
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== inputKey) {
await putObject(decodedKey, fileBuffer);
inputKey = decodedKey;
} else {
await putObject(inputKey, fileBuffer);
}
const progressJobId = clientJobId || jobId;
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
fileBuffer = await getObjectBuffer(inputKey);
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
try {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
}
if (needsCliDecode(validation.format)) {
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
}
fileBuffer = await autoOrient(fileBuffer);
} catch (err) {
request.log.error({ err, toolId: "blur-faces" }, "Input decoding failed");
return reply.status(422).send({
error: "Face blur failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
});
}
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== inputKey) {
await putObject(decodedKey, fileBuffer);
inputKey = decodedKey;
} else {
await putObject(inputKey, fileBuffer);
}
const progressJobId = clientJobId || jobId;
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
return reply.status(202).send({ jobId: progressJobId, async: true });
});
return reply.status(202).send({ jobId: progressJobId, async: true });
},
);
// Register in the pipeline/batch registry
registerToolProcessFn({
+91 -88
View File
@@ -109,107 +109,110 @@ export function registerEditMetadata(app: FastifyInstance) {
);
// Edit endpoint - writes metadata in-place using ExifTool (no pixel re-encoding)
app.post("/api/v1/tools/image/edit-metadata", async (request: FastifyRequest, reply: FastifyReply) => {
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
app.post(
"/api/v1/tools/image/edit-metadata",
async (request: FastifyRequest, reply: FastifyReply) => {
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const chunks: Buffer[] = [];
for await (const chunk of part.file) {
chunks.push(chunk);
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const chunks: Buffer[] = [];
for await (const chunk of part.file) {
chunks.push(chunk);
}
fileBuffer = Buffer.concat(chunks);
filename = sanitizeFilename(part.filename ?? "image");
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
}
fileBuffer = Buffer.concat(chunks);
filename = sanitizeFilename(part.filename ?? "image");
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
}
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: err instanceof Error ? err.message : String(err),
});
}
if (!fileBuffer || fileBuffer.length === 0) {
return reply.status(400).send({ error: "No image file provided" });
}
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
// Parse and validate settings
let settings: Settings;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
} catch (err) {
return reply.status(400).send({
error: "Invalid settings",
details: formatZodErrors(result.error.issues),
error: "Failed to parse multipart request",
details: err instanceof Error ? err.message : String(err),
});
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
try {
// Build ExifTool arguments from settings
const tags = buildTagArgs(settings as EditMetadataSettings);
// If no changes requested, return the original buffer
let outputBuffer: Buffer;
if (tags.length === 0) {
outputBuffer = fileBuffer;
} else {
outputBuffer = await writeMetadata(fileBuffer, filename, tags);
if (!fileBuffer || fileBuffer.length === 0) {
return reply.status(400).send({ error: "No image file provided" });
}
// Determine content type from validated format
const contentType = MIME_BY_FORMAT[validation.format] ?? "image/jpeg";
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
// Save output to object storage
const jobId = randomUUID();
await putObject(`outputs/${jobId}/${filename}`, outputBuffer);
// Generate preview for non-browser-previewable formats (HEIF, TIFF)
let previewUrl: string | undefined;
if (!BROWSER_PREVIEWABLE.has(contentType)) {
try {
let previewInput = outputBuffer;
if (contentType === "image/heif" || contentType === "image/heic") {
previewInput = await decodeHeic(outputBuffer);
}
const previewBuffer = await sharp(previewInput).webp({ quality: 80 }).toBuffer();
await putObject(`outputs/${jobId}/preview.webp`, previewBuffer);
previewUrl = `/api/v1/download/${jobId}/preview.webp`;
} catch {
// Non-fatal - frontend shows fallback
// Parse and validate settings
let settings: Settings;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply.status(400).send({
error: "Invalid settings",
details: formatZodErrors(result.error.issues),
});
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
return reply.send({
jobId,
downloadUrl: `/api/v1/download/${jobId}/${encodeURIComponent(filename)}`,
previewUrl,
originalSize: fileBuffer.length,
processedSize: outputBuffer.length,
});
} catch (err) {
request.log.error({ err, toolId: "edit-metadata" }, "Metadata edit failed");
return reply.status(422).send({
error: "Metadata edit failed",
details: err instanceof Error ? err.message : "Unknown error",
});
}
});
try {
// Build ExifTool arguments from settings
const tags = buildTagArgs(settings as EditMetadataSettings);
// If no changes requested, return the original buffer
let outputBuffer: Buffer;
if (tags.length === 0) {
outputBuffer = fileBuffer;
} else {
outputBuffer = await writeMetadata(fileBuffer, filename, tags);
}
// Determine content type from validated format
const contentType = MIME_BY_FORMAT[validation.format] ?? "image/jpeg";
// Save output to object storage
const jobId = randomUUID();
await putObject(`outputs/${jobId}/${filename}`, outputBuffer);
// Generate preview for non-browser-previewable formats (HEIF, TIFF)
let previewUrl: string | undefined;
if (!BROWSER_PREVIEWABLE.has(contentType)) {
try {
let previewInput = outputBuffer;
if (contentType === "image/heif" || contentType === "image/heic") {
previewInput = await decodeHeic(outputBuffer);
}
const previewBuffer = await sharp(previewInput).webp({ quality: 80 }).toBuffer();
await putObject(`outputs/${jobId}/preview.webp`, previewBuffer);
previewUrl = `/api/v1/download/${jobId}/preview.webp`;
} catch {
// Non-fatal - frontend shows fallback
}
}
return reply.send({
jobId,
downloadUrl: `/api/v1/download/${jobId}/${encodeURIComponent(filename)}`,
previewUrl,
originalSize: fileBuffer.length,
processedSize: outputBuffer.length,
});
} catch (err) {
request.log.error({ err, toolId: "edit-metadata" }, "Metadata edit failed");
return reply.status(422).send({
error: "Metadata edit failed",
details: err instanceof Error ? err.message : "Unknown error",
});
}
},
);
// Register in pipeline/batch registry
registerToolProcessFn({
+109 -106
View File
@@ -58,119 +58,122 @@ registerAiJobHandler("enhance-faces", async (input, data, ctx) => {
/** Face enhancement route using GFPGAN/CodeFormer. */
export function registerEnhanceFaces(app: FastifyInstance) {
app.post("/api/v1/tools/image/enhance-faces", async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "enhance-faces";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
app.post(
"/api/v1/tools/image/enhance-faces",
async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "enhance-faces";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
});
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
fileBuffer = await getObjectBuffer(inputKey);
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
try {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
}
if (needsCliDecode(validation.format)) {
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
}
fileBuffer = await autoOrient(fileBuffer);
} catch (err) {
request.log.error({ err, toolId: "enhance-faces" }, "Input decoding failed");
return reply.status(422).send({
error: "Face enhancement failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
});
}
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== inputKey) {
await putObject(decodedKey, fileBuffer);
inputKey = decodedKey;
} else {
await putObject(inputKey, fileBuffer);
}
const progressJobId = clientJobId || jobId;
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
fileBuffer = await getObjectBuffer(inputKey);
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
try {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
}
if (needsCliDecode(validation.format)) {
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
}
fileBuffer = await autoOrient(fileBuffer);
} catch (err) {
request.log.error({ err, toolId: "enhance-faces" }, "Input decoding failed");
return reply.status(422).send({
error: "Face enhancement failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
});
}
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== inputKey) {
await putObject(decodedKey, fileBuffer);
inputKey = decodedKey;
} else {
await putObject(inputKey, fileBuffer);
}
const progressJobId = clientJobId || jobId;
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
return reply.status(202).send({ jobId: progressJobId, async: true });
});
return reply.status(202).send({ jobId: progressJobId, async: true });
},
);
// Register in the pipeline/batch registry
registerToolProcessFn({
+134 -131
View File
@@ -33,147 +33,150 @@ const settingsSchema = z.object({
* via getObjectBuffer(data.inputRefs[1]) inside the handler.
*/
export function registerEraseObject(app: FastifyInstance) {
app.post("/api/v1/tools/image/erase-object", async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "erase-object";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
app.post(
"/api/v1/tools/image/erase-object",
async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "erase-object";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let imageBuffer: Buffer | null = null;
let maskBuffer: Buffer | null = null;
let filename = "image";
let clientJobId: string | null = null;
let fileId: string | null = null;
let format = "png";
let quality = 95;
let imageKey: string | null = null;
let maskKey: string | null = null;
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let imageBuffer: Buffer | null = null;
let maskBuffer: Buffer | null = null;
let filename = "image";
let clientJobId: string | null = null;
let fileId: string | null = null;
let format = "png";
let quality = 95;
let imageKey: string | null = null;
let maskKey: string | null = null;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
if (part.fieldname === "mask") {
const upload = await receiveUpload(part, jobId);
maskKey = upload.key;
} else {
const upload = await receiveUpload(part, jobId);
imageKey = upload.key;
filename = upload.filename;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
if (part.fieldname === "mask") {
const upload = await receiveUpload(part, jobId);
maskKey = upload.key;
} else {
const upload = await receiveUpload(part, jobId);
imageKey = upload.key;
filename = upload.filename;
}
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "format") {
format = (part.value as string) || "png";
} else if (part.fieldname === "quality") {
quality = Number(part.value) || 95;
}
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
} else if (part.fieldname === "format") {
format = (part.value as string) || "png";
} else if (part.fieldname === "quality") {
quality = Number(part.value) || 95;
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
});
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
});
}
if (!imageKey) {
return reply.status(400).send({ error: "No image file provided" });
}
if (!maskKey) {
return reply.status(400).send({
error: "No mask image provided. Upload a mask as a second file with fieldname 'mask'",
});
}
imageBuffer = await getObjectBuffer(imageKey);
maskBuffer = await getObjectBuffer(maskKey);
const imageValidation = await validateImageBuffer(imageBuffer, filename);
if (!imageValidation.valid) {
return reply.status(400).send({ error: `Invalid image: ${imageValidation.reason}` });
}
const maskValidation = await validateImageBuffer(maskBuffer, "mask.png");
if (!maskValidation.valid) {
return reply.status(400).send({ error: `Invalid mask: ${maskValidation.reason}` });
}
// Validate format and quality via Zod
const settingsResult = settingsSchema.safeParse({ format, quality });
if (!settingsResult.success) {
return reply.status(400).send({
error: "Invalid settings",
details: settingsResult.error.issues
.map((i) => (i.path.length > 0 ? `${i.path.join(".")}: ${i.message}` : i.message))
.join("; "),
});
}
format = settingsResult.data.format;
quality = settingsResult.data.quality;
if (format === "auto") {
const detected = await resolveOutputFormat(imageBuffer, filename);
format = detected.format === "jpeg" ? "jpg" : detected.format;
quality = detected.quality;
}
try {
if (imageValidation.format === "heif") {
imageBuffer = await decodeHeic(imageBuffer);
if (!imageKey) {
return reply.status(400).send({ error: "No image file provided" });
}
if (needsCliDecode(imageValidation.format)) {
imageBuffer = await decodeToSharpCompat(imageBuffer, imageValidation.format);
if (!maskKey) {
return reply.status(400).send({
error: "No mask image provided. Upload a mask as a second file with fieldname 'mask'",
});
}
imageBuffer = await autoOrient(imageBuffer);
} catch (err) {
request.log.error({ err, toolId: "erase-object" }, "Input decoding failed");
return reply.status(422).send({
error: "Object erasing failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
imageBuffer = await getObjectBuffer(imageKey);
maskBuffer = await getObjectBuffer(maskKey);
const imageValidation = await validateImageBuffer(imageBuffer, filename);
if (!imageValidation.valid) {
return reply.status(400).send({ error: `Invalid image: ${imageValidation.reason}` });
}
const maskValidation = await validateImageBuffer(maskBuffer, "mask.png");
if (!maskValidation.valid) {
return reply.status(400).send({ error: `Invalid mask: ${maskValidation.reason}` });
}
// Validate format and quality via Zod
const settingsResult = settingsSchema.safeParse({ format, quality });
if (!settingsResult.success) {
return reply.status(400).send({
error: "Invalid settings",
details: settingsResult.error.issues
.map((i) => (i.path.length > 0 ? `${i.path.join(".")}: ${i.message}` : i.message))
.join("; "),
});
}
format = settingsResult.data.format;
quality = settingsResult.data.quality;
if (format === "auto") {
const detected = await resolveOutputFormat(imageBuffer, filename);
format = detected.format === "jpeg" ? "jpg" : detected.format;
quality = detected.quality;
}
try {
if (imageValidation.format === "heif") {
imageBuffer = await decodeHeic(imageBuffer);
}
if (needsCliDecode(imageValidation.format)) {
imageBuffer = await decodeToSharpCompat(imageBuffer, imageValidation.format);
}
imageBuffer = await autoOrient(imageBuffer);
} catch (err) {
request.log.error({ err, toolId: "erase-object" }, "Input decoding failed");
return reply.status(422).send({
error: "Object erasing failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
});
}
// Write decoded image for the worker
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== imageKey) {
await putObject(decodedKey, imageBuffer);
imageKey = decodedKey;
} else {
await putObject(imageKey, imageBuffer);
}
const progressJobId = clientJobId || jobId;
// Enqueue with both image and mask as inputRefs; the worker handler
// reads them via getObjectBuffer.
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [imageKey, maskKey],
filename,
settings: { format, quality },
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
}
// Write decoded image for the worker
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== imageKey) {
await putObject(decodedKey, imageBuffer);
imageKey = decodedKey;
} else {
await putObject(imageKey, imageBuffer);
}
const progressJobId = clientJobId || jobId;
// Enqueue with both image and mask as inputRefs; the worker handler
// reads them via getObjectBuffer.
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [imageKey, maskKey],
filename,
settings: { format, quality },
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
return reply.status(202).send({ jobId: progressJobId, async: true });
});
return reply.status(202).send({ jobId: progressJobId, async: true });
},
);
}
// ── AI job handler (separate import for the worker) ───────────────
+36 -33
View File
@@ -98,46 +98,49 @@ const settingsSchema = z.object({
export function registerGifTools(app: FastifyInstance) {
// ── Metadata endpoint ───────────────────────────────────────────
app.post("/api/v1/tools/image/gif-tools/info", async (request: FastifyRequest, reply: FastifyReply) => {
let fileBuffer: Buffer | null = null;
app.post(
"/api/v1/tools/image/gif-tools/info",
async (request: FastifyRequest, reply: FastifyReply) => {
let fileBuffer: Buffer | null = null;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const chunks: Buffer[] = [];
for await (const chunk of part.file) {
chunks.push(chunk);
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const chunks: Buffer[] = [];
for await (const chunk of part.file) {
chunks.push(chunk);
}
fileBuffer = Buffer.concat(chunks);
}
fileBuffer = Buffer.concat(chunks);
}
} catch {
return reply.status(400).send({ error: "Failed to parse request" });
}
} catch {
return reply.status(400).send({ error: "Failed to parse request" });
}
if (!fileBuffer || fileBuffer.length === 0) {
return reply.status(400).send({ error: "No file provided" });
}
if (!fileBuffer || fileBuffer.length === 0) {
return reply.status(400).send({ error: "No file provided" });
}
try {
const meta = await sharp(fileBuffer).metadata();
const pages = meta.pages ?? 1;
const delay = meta.delay ?? Array(pages).fill(100);
try {
const meta = await sharp(fileBuffer).metadata();
const pages = meta.pages ?? 1;
const delay = meta.delay ?? Array(pages).fill(100);
return reply.send({
width: meta.width ?? 0,
height: meta.pageHeight ?? meta.height ?? 0,
pages,
delay,
loop: meta.loop ?? 0,
fileSize: fileBuffer.length,
duration: delay.reduce((sum: number, d: number) => sum + d, 0),
});
} catch {
return reply.status(422).send({ error: "Could not read image metadata" });
}
});
return reply.send({
width: meta.width ?? 0,
height: meta.pageHeight ?? meta.height ?? 0,
pages,
delay,
loop: meta.loop ?? 0,
fileSize: fileBuffer.length,
duration: delay.reduce((sum: number, d: number) => sum + d, 0),
});
} catch {
return reply.status(422).send({ error: "Could not read image metadata" });
}
},
);
// ── Processing endpoint ─────────────────────────────────────────
createToolRoute(app, {
+109 -106
View File
@@ -69,119 +69,122 @@ registerAiJobHandler("noise-removal", async (input, data, ctx) => {
* Uses the Python sidecar for multi-tier denoising.
*/
export function registerNoiseRemoval(app: FastifyInstance) {
app.post("/api/v1/tools/image/noise-removal", async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "noise-removal";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
app.post(
"/api/v1/tools/image/noise-removal",
async (request: FastifyRequest, reply: FastifyReply) => {
const toolId = "noise-removal";
if (!isToolInstalled(toolId)) {
const bundle = getBundleForTool(toolId);
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: TOOL_BUNDLE_MAP[toolId],
featureName: bundle?.name ?? toolId,
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
});
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
fileBuffer = await getObjectBuffer(inputKey);
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
let parsed: z.infer<typeof settingsSchema>;
try {
const raw = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(raw);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
}
parsed = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
try {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
}
if (needsCliDecode(validation.format)) {
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
}
fileBuffer = await autoOrient(fileBuffer);
} catch (err) {
request.log.error({ err, toolId: "noise-removal" }, "Input decoding failed");
return reply.status(422).send({
error: "Noise removal failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
});
}
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== inputKey) {
await putObject(decodedKey, fileBuffer);
inputKey = decodedKey;
} else {
await putObject(inputKey, fileBuffer);
}
const progressJobId = clientJobId || jobId;
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings: parsed,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
fileBuffer = await getObjectBuffer(inputKey);
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
let parsed: z.infer<typeof settingsSchema>;
try {
const raw = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(raw);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
}
parsed = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
try {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
}
if (needsCliDecode(validation.format)) {
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
}
fileBuffer = await autoOrient(fileBuffer);
} catch (err) {
request.log.error({ err, toolId: "noise-removal" }, "Input decoding failed");
return reply.status(422).send({
error: "Noise removal failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
});
}
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== inputKey) {
await putObject(decodedKey, fileBuffer);
inputKey = decodedKey;
} else {
await putObject(inputKey, fileBuffer);
}
const progressJobId = clientJobId || jobId;
await enqueueToolJob({
jobId,
toolId,
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings: parsed,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
return reply.status(202).send({ jobId: progressJobId, async: true });
});
return reply.status(202).send({ jobId: progressJobId, async: true });
},
);
// Register in the pipeline/batch registry
registerToolProcessFn({
+79 -76
View File
@@ -30,83 +30,86 @@ const settingsSchema = z.object({
* images from text input, not from uploaded files.
*/
export function registerQrGenerate(app: FastifyInstance) {
app.post("/api/v1/tools/image/qr-generate", async (request: FastifyRequest, reply: FastifyReply) => {
let body: unknown;
try {
body = request.body;
} catch {
return reply.status(400).send({ error: "Invalid request body" });
}
const result = settingsSchema.safeParse(body);
if (!result.success) {
return reply.status(400).send({
error: "Invalid settings",
details: formatZodErrors(result.error.issues),
});
}
const settings = result.data;
try {
// When a logo is present, force max error correction so the QR
// remains scannable despite the logo occluding the center.
const ecLevel = settings.logoDataUri ? "H" : settings.errorCorrection;
let buffer = await QRCode.toBuffer(settings.text, {
width: settings.size,
errorCorrectionLevel: ecLevel,
color: {
dark: settings.foreground,
light: settings.background,
},
type: "png",
margin: 2,
});
if (settings.logoDataUri) {
// Decode the data-URI base64 payload into a buffer
const base64Part = settings.logoDataUri.split(",")[1];
let logoBuffer: Buffer;
try {
logoBuffer = Buffer.from(base64Part, "base64");
// Validate that sharp can decode it
await sharp(logoBuffer).metadata();
} catch {
return reply.status(400).send({ error: "Invalid logo image" });
}
// Resize logo to 22% of QR size and composite centered
const logoSize = Math.round(settings.size * 0.22);
const resizedLogo = await sharp(logoBuffer)
.resize(logoSize, logoSize, {
fit: "contain",
background: { r: 255, g: 255, b: 255, alpha: 0 },
})
.png()
.toBuffer();
buffer = await sharp(buffer)
.composite([{ input: resizedLogo, gravity: "centre" }])
.png()
.toBuffer();
app.post(
"/api/v1/tools/image/qr-generate",
async (request: FastifyRequest, reply: FastifyReply) => {
let body: unknown;
try {
body = request.body;
} catch {
return reply.status(400).send({ error: "Invalid request body" });
}
const jobId = randomUUID();
const filename = "qrcode.png";
await putObject(`outputs/${jobId}/${filename}`, buffer);
const result = settingsSchema.safeParse(body);
if (!result.success) {
return reply.status(400).send({
error: "Invalid settings",
details: formatZodErrors(result.error.issues),
});
}
return reply.send({
jobId,
downloadUrl: `/api/v1/download/${jobId}/${filename}`,
originalSize: 0,
processedSize: buffer.length,
});
} catch (err) {
return reply.status(422).send({
error: "QR code generation failed",
details: err instanceof Error ? err.message : "Unknown error",
});
}
});
const settings = result.data;
try {
// When a logo is present, force max error correction so the QR
// remains scannable despite the logo occluding the center.
const ecLevel = settings.logoDataUri ? "H" : settings.errorCorrection;
let buffer = await QRCode.toBuffer(settings.text, {
width: settings.size,
errorCorrectionLevel: ecLevel,
color: {
dark: settings.foreground,
light: settings.background,
},
type: "png",
margin: 2,
});
if (settings.logoDataUri) {
// Decode the data-URI base64 payload into a buffer
const base64Part = settings.logoDataUri.split(",")[1];
let logoBuffer: Buffer;
try {
logoBuffer = Buffer.from(base64Part, "base64");
// Validate that sharp can decode it
await sharp(logoBuffer).metadata();
} catch {
return reply.status(400).send({ error: "Invalid logo image" });
}
// Resize logo to 22% of QR size and composite centered
const logoSize = Math.round(settings.size * 0.22);
const resizedLogo = await sharp(logoBuffer)
.resize(logoSize, logoSize, {
fit: "contain",
background: { r: 255, g: 255, b: 255, alpha: 0 },
})
.png()
.toBuffer();
buffer = await sharp(buffer)
.composite([{ input: resizedLogo, gravity: "centre" }])
.png()
.toBuffer();
}
const jobId = randomUUID();
const filename = "qrcode.png";
await putObject(`outputs/${jobId}/${filename}`, buffer);
return reply.send({
jobId,
downloadUrl: `/api/v1/download/${jobId}/${filename}`,
originalSize: 0,
processedSize: buffer.length,
});
} catch (err) {
return reply.status(422).send({
error: "QR code generation failed",
details: err instanceof Error ? err.message : "Unknown error",
});
}
},
);
}
+109 -106
View File
@@ -83,125 +83,128 @@ registerAiJobHandler("restore-photo", async (input, data, ctx) => {
* optional colorization.
*/
export function registerRestorePhoto(app: FastifyInstance) {
app.post("/api/v1/tools/image/restore-photo", async (request: FastifyRequest, reply: FastifyReply) => {
if (!isToolInstalled("restore-photo")) {
const bundle = getBundleForTool("restore-photo");
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: "photo-restoration",
featureName: bundle?.name ?? "Photo Restoration",
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
app.post(
"/api/v1/tools/image/restore-photo",
async (request: FastifyRequest, reply: FastifyReply) => {
if (!isToolInstalled("restore-photo")) {
const bundle = getBundleForTool("restore-photo");
return reply.status(501).send({
error: "Feature not installed",
code: "FEATURE_NOT_INSTALLED",
feature: "photo-restoration",
featureName: bundle?.name ?? "Photo Restoration",
estimatedSize: bundle?.estimatedSize ?? "unknown",
});
}
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
const userId = getAuthUser(request)?.id ?? null;
const jobId = randomUUID();
let fileBuffer: Buffer | null = null;
let filename = "image";
let settingsRaw: string | null = null;
let clientJobId: string | null = null;
let fileId: string | null = null;
let inputKey: string | null = null;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
try {
const parts = request.parts();
for await (const part of parts) {
if (part.type === "file") {
const upload = await receiveUpload(part, jobId);
inputKey = upload.key;
filename = upload.filename;
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
const raw = part.value as string;
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
clientJobId = raw;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
} else if (part.fieldname === "fileId") {
fileId = part.value as string;
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
});
}
} catch (err) {
return reply.status(400).send({
error: "Failed to parse multipart request",
details: stripInternalPaths(err instanceof Error ? err.message : String(err)),
});
}
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
fileBuffer = await getObjectBuffer(inputKey);
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
if (!inputKey) {
return reply.status(400).send({ error: "No image file provided" });
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
try {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
fileBuffer = await getObjectBuffer(inputKey);
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
if (needsCliDecode(validation.format)) {
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
}
fileBuffer = await autoOrient(fileBuffer);
if (validation.format === "avif") {
try {
fileBuffer = await sharp(fileBuffer).png().toBuffer();
} catch {
fileBuffer = await decodeAnyFormat(fileBuffer, "avif");
let settings: z.infer<typeof settingsSchema>;
try {
const parsed = settingsRaw ? JSON.parse(settingsRaw) : {};
const result = settingsSchema.safeParse(parsed);
if (!result.success) {
return reply
.status(400)
.send({ error: "Invalid settings", details: formatZodErrors(result.error.issues) });
}
settings = result.data;
} catch {
return reply.status(400).send({ error: "Settings must be valid JSON" });
}
} catch (err) {
request.log.error({ err, toolId: "restore-photo" }, "Input decoding failed");
return reply.status(422).send({
error: "Photo restoration failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
try {
if (validation.format === "heif") {
fileBuffer = await decodeHeic(fileBuffer);
}
if (needsCliDecode(validation.format)) {
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
}
fileBuffer = await autoOrient(fileBuffer);
if (validation.format === "avif") {
try {
fileBuffer = await sharp(fileBuffer).png().toBuffer();
} catch {
fileBuffer = await decodeAnyFormat(fileBuffer, "avif");
}
}
} catch (err) {
request.log.error({ err, toolId: "restore-photo" }, "Input decoding failed");
return reply.status(422).send({
error: "Photo restoration failed",
details: stripInternalPaths(err instanceof Error ? err.message : "Unknown error"),
});
}
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== inputKey) {
await putObject(decodedKey, fileBuffer);
inputKey = decodedKey;
} else {
await putObject(inputKey, fileBuffer);
}
const progressJobId = clientJobId || jobId;
await enqueueToolJob({
jobId,
toolId: "restore-photo",
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
}
const decodedKey = `uploads/${jobId}/${filename}`;
if (decodedKey !== inputKey) {
await putObject(decodedKey, fileBuffer);
inputKey = decodedKey;
} else {
await putObject(inputKey, fileBuffer);
}
const progressJobId = clientJobId || jobId;
await enqueueToolJob({
jobId,
toolId: "restore-photo",
userId,
pool: "ai",
inputRefs: [inputKey],
filename,
settings,
clientJobId: clientJobId ?? undefined,
fileId: fileId ?? undefined,
kind: "ai-tool",
});
return reply.status(202).send({ jobId: progressJobId, async: true });
});
return reply.status(202).send({ jobId: progressJobId, async: true });
},
);
// Register in the pipeline/batch registry
registerToolProcessFn({
@@ -1,8 +1,8 @@
---
import { SECTIONS, TOOLS, toolSection } from "@snapotter/shared";
import Base from "@/layouts/Base.astro";
import Navbar from "@/components/Navbar.astro";
import Footer from "@/components/Footer.astro";
import Navbar from "@/components/Navbar.astro";
import Base from "@/layouts/Base.astro";
export function getStaticPaths() {
return SECTIONS.map((s) => ({