Files
ttstd-infinite-canvas/canvas-agent/src/portrait-clearance/contracts.ts
T

273 lines
13 KiB
TypeScript

import { z } from "zod";
export const PORTRAIT_CLEARANCE_SCHEMA_VERSION = 1 as const;
export const PORTRAIT_CLEARANCE_MODULE_ID = "portrait-clearance" as const;
export const portraitRiskLevelSchema = z.enum(["high", "medium", "low_to_medium", "low", "unable_to_determine"]);
export type PortraitRiskLevel = z.infer<typeof portraitRiskLevelSchema>;
export const portraitClearanceModeSchema = z.enum(["direct-compare", "network-search"]);
export type PortraitClearanceMode = z.infer<typeof portraitClearanceModeSchema>;
export const portraitClearanceAnalysisModeSchema = z.enum(["local-plus-vision", "local-only"]);
export type PortraitClearanceAnalysisMode = z.infer<typeof portraitClearanceAnalysisModeSchema>;
export const portraitClearanceDedupModeSchema = z.enum(["phash", "arcface"]);
export type PortraitClearanceDedupMode = z.infer<typeof portraitClearanceDedupModeSchema>;
export const portraitClearanceInputRoleSchema = z.enum(["query", "reference", "candidate"]);
export type PortraitClearanceInputRole = z.infer<typeof portraitClearanceInputRoleSchema>;
export const portraitClearanceTaskStatusSchema = z.enum(["queued", "running", "waiting_model", "partial", "completed", "failed", "cancelled"]);
export type PortraitClearanceTaskStatus = z.infer<typeof portraitClearanceTaskStatusSchema>;
export const portraitClearanceTaskStageSchema = z.enum([
"validating-inputs",
"checking-model-resources",
"preparing-query",
"searching",
"downloading-candidates",
"deduplicating",
"local-comparing",
"waiting-for-model",
"model-comparing",
"building-report",
"done",
]);
export type PortraitClearanceTaskStage = z.infer<typeof portraitClearanceTaskStageSchema>;
export const portraitClearanceSettingsSchema = z.object({
maxCandidates: z.number().int().min(1).max(60),
searchScrolls: z.number().int().min(0).max(20),
dedupMode: portraitClearanceDedupModeSchema,
modelConcurrency: z.number().int().min(1).max(10),
showBrowserForDebug: z.boolean(),
}).strict();
export type PortraitClearanceSettings = z.infer<typeof portraitClearanceSettingsSchema>;
export const portraitClearanceInputSchema = z.object({
nodeId: z.string().min(1).max(160),
role: portraitClearanceInputRoleSchema,
fileName: z.string().min(1).max(240),
mimeType: z.enum(["image/jpeg", "image/png", "image/webp"]),
dataUrl: z.string().regex(/^data:image\/(?:jpeg|png|webp);base64,[A-Za-z0-9+/=_-]+$/),
}).strict();
export type PortraitClearanceInput = z.infer<typeof portraitClearanceInputSchema>;
export const createPortraitClearanceTaskRequestSchema = z.object({
schemaVersion: z.literal(PORTRAIT_CLEARANCE_SCHEMA_VERSION),
clientOperationId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{7,159}$/),
ownerScopeHash: z.string().regex(/^[a-f0-9]{64}$/),
projectId: z.string().min(1).max(160),
nodeId: z.string().min(1).max(160),
mode: portraitClearanceModeSchema,
analysisMode: portraitClearanceAnalysisModeSchema,
modelRef: z.string().min(1).max(240).optional(),
inputs: z.array(portraitClearanceInputSchema).min(1).max(61),
settings: portraitClearanceSettingsSchema,
}).strict();
export type CreatePortraitClearanceTaskRequest = z.infer<typeof createPortraitClearanceTaskRequestSchema>;
export const portraitImageQualitySchema = z.object({
width: z.number().int().positive(),
height: z.number().int().positive(),
sharpness: z.number().finite().nonnegative(),
brightness: z.number().finite().min(0).max(255),
contrast: z.number().finite().nonnegative(),
grade: z.enum(["good", "usable", "poor"]),
}).strict();
export type PortraitImageQuality = z.infer<typeof portraitImageQualitySchema>;
export const portraitFaceSchema = z.object({
bbox: z.tuple([z.number().finite(), z.number().finite(), z.number().finite(), z.number().finite()]),
detScore: z.number().finite().min(0).max(1),
areaRatio: z.number().finite().min(0).max(1),
}).strict();
export const portraitLocalPrecheckSchema = z.object({
qualityA: portraitImageQualitySchema,
qualityB: portraitImageQualitySchema,
facesA: z.number().int().nonnegative(),
facesB: z.number().int().nonnegative(),
selectedFaceA: portraitFaceSchema.optional(),
selectedFaceB: portraitFaceSchema.optional(),
faceSimilarity: z.number().finite().min(-1).max(1).optional(),
ssim: z.number().finite().min(-1).max(1),
colorHistogramCorrelation: z.number().finite().min(-1).max(1),
canExtractEmbedding: z.boolean(),
reliabilityIssues: z.array(z.string().min(1).max(500)).max(32),
}).strict();
export type PortraitLocalPrecheck = z.infer<typeof portraitLocalPrecheckSchema>;
export const portraitFeatureKeySchema = z.enum(["face_shape", "facial_layout", "eyes_brows", "nose_mouth", "hair_hairline", "distinctive_features"]);
export type PortraitFeatureKey = z.infer<typeof portraitFeatureKeySchema>;
const portraitFeatureComparisonSchema = z.object({
similarity: z.enum(["high", "medium", "low", "none"]),
note: z.string().max(1_000),
}).strict();
export const portraitVisionComparisonSchema = z.object({
imageAType: z.enum(["realistic", "stylized"]),
imageBType: z.enum(["realistic", "stylized"]),
analysisPath: z.enum(["A", "B"]),
status: z.enum(["success", "unable_to_determine"]),
riskLevel: portraitRiskLevelSchema,
overallSimilarity: z.number().finite().min(0).max(1),
featureComparison: z.object({
face_shape: portraitFeatureComparisonSchema,
facial_layout: portraitFeatureComparisonSchema,
eyes_brows: portraitFeatureComparisonSchema,
nose_mouth: portraitFeatureComparisonSchema,
hair_hairline: portraitFeatureComparisonSchema,
distinctive_features: portraitFeatureComparisonSchema,
}).strict(),
basis: z.array(z.string().min(1).max(1_000)).max(16),
limitations: z.array(z.string().min(1).max(1_000)).max(16),
modificationSuggestions: z.array(z.string().min(1).max(1_000)).max(16),
insightfaceFusionNote: z.string().max(1_000),
manualReviewRecommended: z.boolean(),
}).strict();
export type PortraitVisionComparison = z.infer<typeof portraitVisionComparisonSchema>;
export const portraitPairResultSchema = z.object({
id: z.string().min(1).max(160),
queryImageId: z.string().min(1).max(160),
comparisonImageId: z.string().min(1).max(160),
candidateId: z.string().min(1).max(160).optional(),
source: z.enum(["connected-reference", "connected-candidate", "baidu"]),
status: z.enum(["success", "partial", "failed"]),
riskLevel: portraitRiskLevelSchema,
overallSimilarity: z.number().finite().min(0).max(1).optional(),
analysisPath: z.enum(["A", "B", "unable"]),
localPrecheck: portraitLocalPrecheckSchema,
visionComparison: portraitVisionComparisonSchema.optional(),
basis: z.array(z.string().min(1).max(1_000)).max(32),
limitations: z.array(z.string().min(1).max(1_000)).max(32),
error: z.object({ code: z.string().min(1).max(120), message: z.string().min(1).max(1_000), retryable: z.boolean() }).strict().optional(),
}).strict();
export type PortraitPairResult = z.infer<typeof portraitPairResultSchema>;
export const portraitCandidateSchema = z.object({
id: z.string().min(1).max(160),
originalRank: z.number().int().nonnegative(),
title: z.string().max(500),
imageArtifactId: z.string().min(1).max(160),
imageUrl: z.string().url().max(4_000).optional(),
sourcePageUrl: z.string().url().max(4_000).optional(),
sourceDomain: z.string().max(255).optional(),
source: z.enum(["connected", "baidu"]),
byteSize: z.number().int().nonnegative(),
width: z.number().int().positive().optional(),
height: z.number().int().positive().optional(),
dedupGroupId: z.string().min(1).max(160).optional(),
resultId: z.string().min(1).max(160).optional(),
}).strict();
export type PortraitCandidate = z.infer<typeof portraitCandidateSchema>;
export const portraitClearanceResultSchema = z.object({
schemaVersion: z.literal(PORTRAIT_CLEARANCE_SCHEMA_VERSION),
taskId: z.string().min(1).max(160),
mode: portraitClearanceModeSchema,
queryImageId: z.string().min(1).max(160),
highestRisk: portraitRiskLevelSchema,
riskCounts: z.record(portraitRiskLevelSchema, z.number().int().nonnegative()),
candidateCount: z.number().int().nonnegative(),
comparedCount: z.number().int().nonnegative(),
candidates: z.array(portraitCandidateSchema).max(60),
pairs: z.array(portraitPairResultSchema).max(60),
limitations: z.array(z.string().min(1).max(1_000)).max(32),
createdAt: z.string().datetime(),
completedAt: z.string().datetime().optional(),
}).strict();
export type PortraitClearanceResult = z.infer<typeof portraitClearanceResultSchema>;
export const portraitModelJobStatusSchema = z.enum(["pending", "leased", "completed", "failed"]);
export type PortraitModelJobStatus = z.infer<typeof portraitModelJobStatusSchema>;
export const portraitModelJobSchema = z.object({
jobId: z.string().regex(/^portrait-job-[a-f0-9]{16,80}$/),
taskId: z.string().min(1).max(160),
pairId: z.string().min(1).max(160),
queryImageId: z.string().min(1).max(160),
comparisonImageId: z.string().min(1).max(160),
status: portraitModelJobStatusSchema,
attempt: z.number().int().nonnegative(),
leaseToken: z.string().regex(/^[A-Za-z0-9_-]{16,160}$/).optional(),
leaseExpiresAt: z.string().datetime().optional(),
errorCode: z.string().min(1).max(120).optional(),
errorMessage: z.string().min(1).max(1_000).optional(),
}).strict();
export type PortraitModelJob = z.infer<typeof portraitModelJobSchema>;
export const portraitModelJobCompleteRequestSchema = z.object({
attempt: z.number().int().nonnegative(),
leaseToken: z.string().regex(/^[A-Za-z0-9_-]{16,160}$/),
visionComparison: portraitVisionComparisonSchema,
}).strict();
export type PortraitModelJobCompleteRequest = z.infer<typeof portraitModelJobCompleteRequestSchema>;
export const portraitModelJobFailRequestSchema = z.object({
attempt: z.number().int().nonnegative(),
leaseToken: z.string().regex(/^[A-Za-z0-9_-]{16,160}$/),
errorCode: z.string().min(1).max(120),
errorMessage: z.string().min(1).max(1_000),
retryable: z.boolean(),
}).strict();
export type PortraitModelJobFailRequest = z.infer<typeof portraitModelJobFailRequestSchema>;
export const portraitTaskSummarySchema = z.object({
taskId: z.string().min(1).max(160),
clientOperationId: z.string().min(1).max(160),
ownerScopeHash: z.string().regex(/^[a-f0-9]{64}$/),
projectId: z.string().min(1).max(160),
nodeId: z.string().min(1).max(160),
mode: portraitClearanceModeSchema,
analysisMode: portraitClearanceAnalysisModeSchema,
modelRef: z.string().min(1).max(240).optional(),
status: portraitClearanceTaskStatusSchema,
stage: portraitClearanceTaskStageSchema,
progress: z.number().finite().min(0).max(1),
processedCandidates: z.number().int().nonnegative(),
totalCandidates: z.number().int().nonnegative().optional(),
errorCode: z.string().min(1).max(120).optional(),
errorMessage: z.string().min(1).max(1_000).optional(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
completedAt: z.string().datetime().optional(),
detailsAvailable: z.boolean(),
}).strict();
export type PortraitTaskSummary = z.infer<typeof portraitTaskSummarySchema>;
export const PORTRAIT_ERROR_MESSAGES: Record<string, string> = {
portrait_plugin_disabled: "请先在插件中心启用肖像排查",
portrait_runtime_unavailable: "肖像排查本机引擎不可用",
portrait_module_unavailable: "当前 Canvas Agent 不包含肖像排查模块",
portrait_model_missing: "请先安装并校验本地肖像模型",
portrait_input_missing: "请连接所需的图片节点",
portrait_input_invalid: "输入图片损坏、过大或格式不受支持",
portrait_multiple_faces: "图片包含多张人脸,请先裁剪到单一目标后重试",
portrait_search_captcha: "搜索服务需要验证码,请使用可见浏览器重试或连接手动候选",
portrait_candidate_download_blocked: "候选图片下载被安全策略阻止",
portrait_task_cancelled: "任务已停止,已完成阶段仍可查看",
portrait_model_job_not_found: "视觉模型作业不存在",
portrait_model_job_conflict: "视觉模型作业已失效,请重新打开任务",
portrait_artifact_not_found: "排查报告不存在",
};
export function riskFromFaceSimilarity(similarity: number | undefined): PortraitRiskLevel {
if (similarity === undefined || !Number.isFinite(similarity)) return "unable_to_determine";
if (similarity >= 0.65) return "high";
if (similarity >= 0.5) return "medium";
if (similarity >= 0.35) return "low_to_medium";
return "low";
}
export function riskScore(level: PortraitRiskLevel) {
return ({ high: 1, medium: 0.66, low_to_medium: 0.5, low: 0.2, unable_to_determine: 0 } as const)[level];
}
export function highestRisk(levels: readonly PortraitRiskLevel[]): PortraitRiskLevel {
return levels.reduce<PortraitRiskLevel>((highest, current) => riskScore(current) > riskScore(highest) ? current : highest, "unable_to_determine");
}