ai-chatbot/lib/agents/psd-template-repair.ts
Your Name bcb0006a9e
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: stop psd replacements inheriting source masks
2026-04-03 11:36:07 +08:00

220 lines
6.5 KiB
TypeScript

import "server-only";
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import {
AGENT_PSD_TEMPLATE_ASSET_VERSION,
isAgentPsdImageLayerSlot,
isAgentPsdTextLayerSlot,
type AgentPsdTemplate,
} from "@/lib/agents/psd-template-config";
import {
isPsdAdjustmentLikeImageLayer,
pickPrimaryPsdImageLayerId,
} from "@/lib/agents/psd-template-image-slot-selection";
import {
parsePsdTemplateFileFromUrl,
preparePsdTemplateRenderAssets,
} from "@/lib/agents/psd-template-parser";
import { uploadFile } from "@/lib/storage/service";
function hasMissingLayerAssets(template: AgentPsdTemplate) {
return template.layers.some(
(layer) =>
!String(layer.previewUrl || "").trim() ||
!String(layer.sourceUrl || "").trim()
);
}
function needsTextMetadataRepair(template: AgentPsdTemplate) {
return template.layers.some(
(layer) =>
isAgentPsdTextLayerSlot(layer) &&
(!String(layer.detectedFontName || "").trim() ||
!Array.isArray(layer.detectedFontNames) ||
layer.detectedFontNames.length === 0)
);
}
function hasDedicatedRenderBase(template: AgentPsdTemplate) {
const renderBaseUrl = String(template.renderBaseUrl || "").trim();
const previewUrl = String(template.previewUrl || "").trim();
return Boolean(String(template.renderBasePath || "").trim()) || (
Boolean(renderBaseUrl) &&
renderBaseUrl !== previewUrl
);
}
function repairEditableImageLayerSelection(template: AgentPsdTemplate) {
const imageLayers = template.layers.filter(isAgentPsdImageLayerSlot);
if (imageLayers.length === 0) {
return template.layers;
}
const editableImageLayers = imageLayers.filter((layer) => layer.isEditable);
const defaultImageLayer = imageLayers.find((layer) => layer.isEditable && layer.isDefaultResultSource);
const shouldAutoRepairSelection =
editableImageLayers.length === 0 ||
!defaultImageLayer ||
(editableImageLayers.length === 1 && isPsdAdjustmentLikeImageLayer(editableImageLayers[0]));
if (!shouldAutoRepairSelection) {
return template.layers;
}
const nextPrimaryLayerId = pickPrimaryPsdImageLayerId({
imageLayers,
canvasWidth: template.width,
canvasHeight: template.height,
});
if (!nextPrimaryLayerId) {
return template.layers;
}
const currentPrimaryLayerId = defaultImageLayer?.id || editableImageLayers[0]?.id || "";
if (currentPrimaryLayerId === nextPrimaryLayerId) {
return template.layers;
}
return template.layers.map((layer) => {
if (!isAgentPsdImageLayerSlot(layer)) {
return layer;
}
if (layer.id === nextPrimaryLayerId) {
return {
...layer,
isEditable: true,
isDefaultResultSource: true,
allowUserUploadOverride: true,
};
}
if (layer.id === currentPrimaryLayerId) {
return {
...layer,
isEditable: false,
isDefaultResultSource: false,
allowUserUploadOverride: false,
};
}
return layer;
});
}
function toArrayBuffer(buffer: Buffer) {
return buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
) as ArrayBuffer;
}
export async function repairAgentPsdTemplateAssetsIfNeeded(
template: AgentPsdTemplate
) {
if (template.parseStatus !== "completed") {
return template;
}
const needsAssetVersionRefresh =
Number(template.assetVersion || 0) !== AGENT_PSD_TEMPLATE_ASSET_VERSION;
const needsRepair = needsAssetVersionRefresh || hasMissingLayerAssets(template);
const needsRenderBaseRepair = !hasDedicatedRenderBase(template);
const fonts = await readAgentPsdFonts().catch(() => []);
const needsFontDetection = needsTextMetadataRepair(template);
let nextLayers = repairEditableImageLayerSelection(template);
if (needsFontDetection) {
try {
const parsedTemplate = await parsePsdTemplateFileFromUrl(template.fileUrl);
const parsedTextLayerMap = new Map(
parsedTemplate.layers
.filter((layer) => layer.slotType === "text")
.map((layer) => [layer.layerId, layer])
);
nextLayers = nextLayers.map((layer) => {
if (!isAgentPsdTextLayerSlot(layer)) return layer;
const parsedLayer = parsedTextLayerMap.get(layer.layerId);
if (!parsedLayer) return layer;
return {
...layer,
detectedFontName: parsedLayer.detectedFontName || layer.detectedFontName,
detectedFontNames: parsedLayer.detectedFontNames || layer.detectedFontNames,
};
});
} catch (error) {
console.warn("[PSD Template] failed to repair parsed text metadata:", {
templateId: template.id,
error,
});
}
}
nextLayers = nextLayers.map((layer) => {
if (!isAgentPsdTextLayerSlot(layer) || layer.fontId) {
return layer;
}
const matchedFont = findBestMatchingPsdFont({
detectedFontNames: [
...((layer.detectedFontNames || []).filter(Boolean)),
...(layer.detectedFontName ? [layer.detectedFontName] : []),
],
fonts,
});
if (!matchedFont) {
return layer;
}
return {
...layer,
fontId: matchedFont.font.id,
fontFamily: matchedFont.font.family,
fontLabel: matchedFont.font.name,
fontFileUrl: matchedFont.font.fileUrl,
fontContentType: matchedFont.font.contentType,
};
});
const hasFontRepair = nextLayers.some(
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
);
if (!needsRepair && !hasFontRepair && !needsRenderBaseRepair) {
return template;
}
try {
const preparedAssets = await preparePsdTemplateRenderAssets({
templateId: template.id,
fileUrl: template.fileUrl,
layers: nextLayers,
forceRefreshLayerAssets: needsAssetVersionRefresh,
});
const uploadedRenderBase =
needsRenderBaseRepair
? await uploadFile(
`agent-psd-base-${template.id}-${Date.now()}.png`,
toArrayBuffer(preparedAssets.renderBaseBuffer)
)
: null;
return {
...template,
renderBaseUrl: uploadedRenderBase?.url || template.renderBaseUrl,
renderBasePath: uploadedRenderBase?.pathname || template.renderBasePath,
layers: preparedAssets.layers,
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
updatedAt: new Date().toISOString(),
} satisfies AgentPsdTemplate;
} catch (error) {
console.warn("[PSD Template] failed to repair template assets:", {
templateId: template.id,
error,
});
return template;
}
}