feat: deliver in chosen format (not forced JPEG); wire output_ext through jobs/Telegram/MCP
- delivery.ts: upload the stored result in its chosen format (output_ext, global or per-recipe) instead of always converting non-alpha to JPEG. - jobs POST froze the snapshot without output_ext -> per-conversion/per-recipe format choice was silently dropped. Now carried through. - telegram.ts: recipe snapshots carry output_ext + delivery_target_id (custom formats already flow via output_format). - mcp/klarbild-mcp.mjs: process_images gains tasks, output_ext, orientation, crop_mode, contour_mm, picdrop_gallery; job_status shows finished_at + errors. - Docs: llms.txt, mcp/README, changelog updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
+6
-30
@@ -45,35 +45,11 @@ async function sidecarForKey(key: string): Promise<boolean> {
|
||||
return !!t?.metadata_sidecar;
|
||||
}
|
||||
|
||||
/** Picdrop ist ein Foto-Proofing-Tool und erwartet handliche JPGs, keine 30-MB-PNGs.
|
||||
* Ergebnisse ohne Transparenz werden darum als JPEG (q92) ausgeliefert; freigestellte
|
||||
* Motive (Sticker mit Alpha) bleiben PNG. Rückgabe: umgewandelter Puffer + passender Name. */
|
||||
async function deliverableBuffer(
|
||||
buf: Buffer, filename: string, hasAlpha: boolean,
|
||||
): Promise<{ buf: Buffer; name: string }> {
|
||||
if (hasAlpha) return { buf, name: filename };
|
||||
// Schon JPEG (globales/Rezept-Format = jpg)? Dann unverändert lassen.
|
||||
if (buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return { buf, name: filename };
|
||||
try {
|
||||
const sharp = (await import('sharp')).default;
|
||||
const meta = await sharp(buf, { failOn: 'none' }).metadata();
|
||||
const dpi = meta.density && meta.density > 0 ? meta.density : 300;
|
||||
const jpg = await sharp(buf, { failOn: 'none' })
|
||||
.flatten({ background: '#ffffff' })
|
||||
.withMetadata({ density: dpi })
|
||||
.jpeg({ quality: 92, mozjpeg: true, chromaSubsampling: '4:4:4' })
|
||||
.toBuffer();
|
||||
const name = filename.replace(/\.(png|webp|tiff?)$/i, '.jpg');
|
||||
return { buf: jpg, name: /\.jpe?g$/i.test(name) ? name : name + '.jpg' };
|
||||
} catch (e) {
|
||||
console.error('[delivery] JPEG-Wandlung fehlgeschlagen, sende Original', e);
|
||||
return { buf, name: filename };
|
||||
}
|
||||
}
|
||||
|
||||
/** Liefert ein fertiges Item an die passende Picdrop-Galerie aus. */
|
||||
/** Liefert ein fertiges Item an die passende Picdrop-Galerie aus.
|
||||
* Ausgeliefert wird das gespeicherte Ergebnis **im gewählten Format** (PNG/JPG laut
|
||||
* globaler bzw. Rezept-Einstellung) — keine erzwungene Umwandlung mehr. */
|
||||
export async function deliverItem(itemId: string): Promise<{ ok: boolean; message: string }> {
|
||||
const it = await one<any>('SELECT id, result_path, filename, folder_id, job_id, has_alpha FROM items WHERE id=$1', [itemId]);
|
||||
const it = await one<any>('SELECT id, result_path, filename, folder_id, job_id FROM items WHERE id=$1', [itemId]);
|
||||
if (!it?.result_path) return { ok: false, message: 'Kein Ergebnis vorhanden.' };
|
||||
const target = await targetFor(it);
|
||||
if (!target) { await query(`UPDATE items SET delivery_status='failed' WHERE id=$1`, [itemId]); return { ok: false, message: 'Kein Auslieferungsziel konfiguriert.' }; }
|
||||
@@ -82,8 +58,8 @@ export async function deliverItem(itemId: string): Promise<{ ok: boolean; messag
|
||||
await query(`UPDATE items SET delivery_status='pending' WHERE id=$1`, [itemId]);
|
||||
try {
|
||||
const gallery = await galleryFor(it);
|
||||
const raw = await getObject(it.result_path);
|
||||
const { buf, name } = await deliverableBuffer(raw, it.filename || `${it.id}.png`, !!it.has_alpha);
|
||||
const buf = await getObject(it.result_path);
|
||||
const name = it.filename || `${it.id}.png`;
|
||||
await uploadBuffer(cfg, gallery, name, buf);
|
||||
// Optional: Metadaten als begleitende .md-Datei — pro Quelle steuerbar.
|
||||
if (await sidecarForKey(key)) {
|
||||
|
||||
+5
-2
@@ -165,10 +165,10 @@ async function chatRecipeDefaults(chatId: number): Promise<any> {
|
||||
if (r) return {
|
||||
output_format: r.output_format, orientation: r.orientation, crop_mode: r.crop_mode || 'crop',
|
||||
dpi: r.dpi || 300, model_key: r.model_key, delivery: r.delivery || 'library',
|
||||
picdrop_gallery: r.picdrop_gallery,
|
||||
picdrop_gallery: r.picdrop_gallery, output_ext: r.output_ext, delivery_target_id: r.delivery_target_id,
|
||||
};
|
||||
}
|
||||
return { output_format: 'keep', crop_mode: 'crop', dpi: 300, delivery: 'library' };
|
||||
return { output_format: 'keep', crop_mode: 'crop', dpi: 300, delivery: 'library', output_ext: null };
|
||||
}
|
||||
|
||||
async function dispatchDraft(chatId: number, draftId: string, recipeId: string, b: Bot) {
|
||||
@@ -187,6 +187,7 @@ async function dispatchDraft(chatId: number, draftId: string, recipeId: string,
|
||||
crop_mode: recipe.crop_mode || 'crop', dpi: recipe.dpi || 300, contour_mm: recipe.contour_mm,
|
||||
model_key: recipe.model_key, custom_instruction: recipe.custom_instruction,
|
||||
delivery: recipe.delivery || 'library', picdrop_gallery: recipe.picdrop_gallery,
|
||||
output_ext: recipe.output_ext, delivery_target_id: recipe.delivery_target_id,
|
||||
};
|
||||
const job = await one<{ id: string }>(
|
||||
`INSERT INTO jobs (created_by, origin, mode, recipe_snapshot, status, total, telegram_chat_id)
|
||||
@@ -217,6 +218,7 @@ async function dispatchCompose(chatId: number, draftId: string, description: str
|
||||
tasks: wantsFormat ? ['format'] : [], output_format: d.output_format, orientation: d.orientation,
|
||||
crop_mode: d.crop_mode || 'crop', dpi: d.dpi || 300, model_key: d.model_key,
|
||||
prompt_text: description, delivery: d.delivery || 'library', picdrop_gallery: d.picdrop_gallery,
|
||||
output_ext: d.output_ext, delivery_target_id: d.delivery_target_id,
|
||||
};
|
||||
const job = await one<{ id: string }>(
|
||||
`INSERT INTO jobs (created_by, origin, mode, recipe_snapshot, status, total, telegram_chat_id)
|
||||
@@ -240,6 +242,7 @@ async function dispatchGenerate(chatId: number, description: string, b: Bot) {
|
||||
tasks: wantsFormat ? ['format'] : [], output_format: d.output_format, orientation: d.orientation,
|
||||
crop_mode: d.crop_mode || 'crop', dpi: d.dpi || 300, model_key: d.model_key,
|
||||
prompt_text: description, delivery: d.delivery || 'library', picdrop_gallery: d.picdrop_gallery,
|
||||
output_ext: d.output_ext, delivery_target_id: d.delivery_target_id,
|
||||
};
|
||||
const job = await one<{ id: string }>(
|
||||
`INSERT INTO jobs (created_by, origin, mode, recipe_snapshot, status, total, telegram_chat_id)
|
||||
|
||||
Reference in New Issue
Block a user