ai-chatbot/lib/agents/psd-template-repair.ts
2026-06-01 17:53:31 +08:00

377 lines
11 KiB
TypeScript

import "server-only";
import type { AgentPsdFont } from "@/lib/agents/psd-font-config";
import {
doesPsdFontMatchDetectedNames,
findBestMatchingPsdFont,
} from "@/lib/agents/psd-font-matching";
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
import {
AGENT_PSD_TEMPLATE_ASSET_VERSION,
hasAgentPsdTemplateRenderSegments,
isAgentPsdImageLayerSlot,
isAgentPsdLayerSuperLocked,
isAgentPsdTemplateLayerUserEditable,
isAgentPsdTextLayerSlot,
type AgentPsdTemplate,
} from "@/lib/agents/psd-template-config";
import {
isPsdAdjustmentLikeImageLayer,
pickPrimaryPsdImageLayerId,
} from "@/lib/agents/psd-template-image-slot-selection";
import {
parsePsdTemplateFileFromSource,
preparePsdTemplateRenderAssets,
uploadPreparedPsdTemplateRenderSegments,
} from "@/lib/agents/psd-template-parser";
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
import { createStorageUploadSession } from "@/lib/storage/service";
function hasMissingLayerAssets(template: AgentPsdTemplate) {
const hasRenderBase = hasDedicatedRenderBase(template);
return template.layers.some(
(layer) => {
if (
isAgentPsdTemplateLayerUserEditable(layer) &&
!String(layer.previewUrl || "").trim()
) {
return true;
}
if (hasRenderBase) {
return (
isAgentPsdImageLayerSlot(layer) &&
isAgentPsdTemplateLayerUserEditable(layer) &&
!String(layer.sourceUrl || "").trim()
);
}
return !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(renderBaseUrl) && renderBaseUrl !== previewUrl;
}
function hasStaticTemplateLayers(template: AgentPsdTemplate) {
return template.layers.some((layer) => !isAgentPsdTemplateLayerUserEditable(layer));
}
function repairEditableImageLayerSelection(template: AgentPsdTemplate) {
const imageLayers = template.layers.filter(isAgentPsdImageLayerSlot);
const unlockedImageLayers = imageLayers.filter(
(layer) => !isAgentPsdLayerSuperLocked(layer)
);
if (unlockedImageLayers.length === 0) {
return template.layers;
}
const editableImageLayers = unlockedImageLayers.filter(
isAgentPsdTemplateLayerUserEditable
);
const defaultImageLayer = unlockedImageLayers.find(
(layer) =>
isAgentPsdTemplateLayerUserEditable(layer) && layer.isDefaultResultSource
);
const shouldAutoRepairSelection =
editableImageLayers.length === 0 ||
!defaultImageLayer ||
(editableImageLayers.length === 1 && isPsdAdjustmentLikeImageLayer(editableImageLayers[0]));
if (!shouldAutoRepairSelection) {
return template.layers;
}
const nextPrimaryLayerId = pickPrimaryPsdImageLayerId({
imageLayers: unlockedImageLayers,
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 function repairAgentPsdTemplateFontBindings(
template: AgentPsdTemplate,
fonts: AgentPsdFont[]
) {
if (fonts.length === 0) {
return template;
}
const fontMap = new Map(fonts.map((font) => [font.id, font]));
let didChange = false;
const nextLayers = template.layers.map((layer) => {
if (!isAgentPsdTextLayerSlot(layer) || layer.styleSource === "custom") {
return layer;
}
const detectedFontNames = [
...((layer.detectedFontNames || []).filter(Boolean)),
...(layer.detectedFontName ? [layer.detectedFontName] : []),
];
const preferredDetectedFontName =
detectedFontNames.find((entry) => String(entry || "").trim()) || "";
const currentFont = layer.fontId ? fontMap.get(layer.fontId) : undefined;
const currentFontMatches = currentFont
? doesPsdFontMatchDetectedNames({
detectedFontNames,
font: currentFont,
})
: false;
const matchedFont = findBestMatchingPsdFont({
detectedFontNames,
fonts,
});
if (
matchedFont?.confidence === "high" &&
(!currentFontMatches || !layer.fontId)
) {
didChange = true;
return {
...layer,
fontId: matchedFont.font.id,
fontFamily: matchedFont.font.family,
fontLabel: matchedFont.font.name,
fontFileUrl: matchedFont.font.fileUrl,
fontContentType: matchedFont.font.contentType,
};
}
if (
!matchedFont &&
!currentFontMatches &&
(
layer.fontId ||
layer.fontFileUrl ||
layer.fontContentType ||
(preferredDetectedFontName &&
(
layer.fontFamily !== preferredDetectedFontName ||
layer.fontLabel !== preferredDetectedFontName
))
)
) {
didChange = true;
return {
...layer,
fontId: undefined,
fontFamily: preferredDetectedFontName || undefined,
fontLabel: preferredDetectedFontName || undefined,
fontFileUrl: undefined,
fontContentType: undefined,
};
}
if (currentFont && (
layer.fontFamily !== currentFont.family ||
layer.fontLabel !== currentFont.name ||
layer.fontFileUrl !== currentFont.fileUrl ||
layer.fontContentType !== currentFont.contentType
)) {
didChange = true;
return {
...layer,
fontFamily: currentFont.family,
fontLabel: currentFont.name,
fontFileUrl: currentFont.fileUrl,
fontContentType: currentFont.contentType,
};
}
return layer;
});
if (!didChange) {
return template;
}
return {
...template,
layers: nextLayers,
updatedAt: new Date().toISOString(),
} satisfies AgentPsdTemplate;
}
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 needsRenderSegmentRepair =
hasStaticTemplateLayers(template) && !hasAgentPsdTemplateRenderSegments(template);
const fonts = await readAgentPsdFonts().catch(() => []);
const needsFontDetection = needsTextMetadataRepair(template);
let nextLayers = repairEditableImageLayerSelection(template);
if (needsFontDetection) {
try {
const localFilePath = resolveAgentPsdLocalCachePath({
templateId: template.id,
fileName: template.fileName,
});
const parsedTemplate = await parsePsdTemplateFileFromSource({
fileUrl: template.fileUrl,
filePath: localFilePath,
});
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,
textColor:
layer.styleSource !== "custom" &&
(!String(layer.textColor || "").trim() || layer.textColor === "#111111") &&
parsedLayer.detectedTextColor
? parsedLayer.detectedTextColor
: layer.textColor,
};
});
} catch (error) {
console.warn("[PSD Template] failed to repair parsed text metadata:", {
templateId: template.id,
error,
});
}
}
const fontRepairedTemplate = repairAgentPsdTemplateFontBindings(
{
...template,
layers: nextLayers,
},
fonts
);
nextLayers = fontRepairedTemplate.layers;
const hasFontRepair = nextLayers.some(
(layer, index) => JSON.stringify(layer) !== JSON.stringify(template.layers[index])
);
if (
!needsRepair &&
!hasFontRepair &&
!needsRenderBaseRepair &&
!needsRenderSegmentRepair
) {
return template;
}
try {
const uploadAsset = await createStorageUploadSession();
const preparedAssets = await preparePsdTemplateRenderAssets({
templateId: template.id,
fileUrl: template.fileUrl,
filePath: resolveAgentPsdLocalCachePath({
templateId: template.id,
fileName: template.fileName,
}),
layers: nextLayers,
forceRefreshLayerAssets: needsAssetVersionRefresh,
uploadAsset,
});
const uploadedRenderBase =
needsRenderBaseRepair
? await uploadAsset(
`agent-psd-base-${template.id}-${Date.now()}.png`,
toArrayBuffer(preparedAssets.renderBaseBuffer)
)
: null;
const uploadedRenderSegments =
needsRenderSegmentRepair || needsAssetVersionRefresh
? await uploadPreparedPsdTemplateRenderSegments({
templateId: template.id,
segments: preparedAssets.renderSegments,
uploadAsset,
})
: null;
return {
...template,
renderBaseUrl: uploadedRenderBase?.url || template.renderBaseUrl,
renderBasePath: uploadedRenderBase?.pathname || template.renderBasePath,
renderSegments: uploadedRenderSegments || template.renderSegments,
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;
}
}