Files
NianAIGC/lib/seedream/creation.ts
2026-08-25 14:06:04 +08:00

136 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export type SeedreamCreationMode = "basic" | "interactive" | "layers";
export type SeedreamPoint = {
x: number;
y: number;
};
export type SeedreamBox = {
left: number;
top: number;
right: number;
bottom: number;
};
export type SeedreamAnnotation = {
id: string;
index: number;
kind: "point" | "box" | "stroke";
sourceKey?: string;
point?: SeedreamPoint;
box?: SeedreamBox;
points?: SeedreamPoint[];
};
export type SeedreamAnnotationReference = {
token: string;
label: string;
caption: "画布标注";
};
export function seedreamAnnotationToken(index: number, kind: SeedreamAnnotation["kind"] = "box") {
return kind === "stroke" ? `@标记${index}` : `@标注${index}`;
}
export function seedreamAnnotationReferences(annotations: SeedreamAnnotation[], query = ""): SeedreamAnnotationReference[] {
const normalizedQuery = query.replace(/^@+/, "").trim().toLowerCase();
return [...annotations]
.sort((a, b) => a.index - b.index)
.map((annotation) => {
const token = seedreamAnnotationToken(annotation.index, annotation.kind);
const label = annotation.kind === "box"
? `框选区域 ${annotation.index}`
: annotation.kind === "point"
? `点选位置 ${annotation.index}`
: `蓝色标记 ${annotation.index}`;
return { token, label, caption: "画布标注" as const };
})
.filter((reference) => !normalizedQuery || [reference.token, reference.token.slice(1), reference.label, reference.caption]
.some((value) => value.toLowerCase().includes(normalizedQuery)));
}
export function seedreamAnnotationTag(annotation: SeedreamAnnotation, imageIndex?: number) {
const prefix = imageIndex && imageIndex > 0 ? `图${imageIndex}` : "";
if (annotation.kind === "point" && annotation.point) {
return `${prefix}<point>${coordinate(annotation.point.x)} ${coordinate(annotation.point.y)}</point>`;
}
if (annotation.kind === "box" && annotation.box) {
return `${prefix}<bbox>${coordinate(annotation.box.left)} ${coordinate(annotation.box.top)} ${coordinate(annotation.box.right)} ${coordinate(annotation.box.bottom)}</bbox>`;
}
if (annotation.kind === "stroke" && annotation.points?.length) {
return `${prefix || "图片"}中的蓝色标记区域`;
}
return "";
}
export function missingSeedreamAnnotationTokens(prompt: string, annotations: SeedreamAnnotation[]) {
const available = new Set(annotations.map((annotation) => seedreamAnnotationToken(annotation.index, annotation.kind)));
const missing = new Set<string>();
for (const match of prompt.matchAll(/@(标注|标记)(\d+)/g)) {
const token = `@${match[1]}${match[2]}`;
if (!available.has(token)) missing.add(token);
}
return [...missing].sort((a, b) => annotationTokenIndex(a) - annotationTokenIndex(b));
}
export function buildSeedreamInteractivePrompt(prompt: string, annotations: SeedreamAnnotation[], sourceKeys: string[] = []) {
const annotationByIndex = new Map(annotations.map((annotation) => [annotation.index, annotation]));
const imageIndexBySource = new Map(sourceKeys.map((key, index) => [key, index + 1]));
const referenced = new Set<number>();
const transformed = normalizeSeedreamImageTokens(prompt).replace(/@(标注|标记)(\d+)/g, (token, kind: string, rawIndex: string) => {
const index = Number(rawIndex);
const annotation = annotationByIndex.get(index);
if (!annotation || seedreamAnnotationToken(index, annotation.kind) !== token) return token;
referenced.add(index);
return seedreamAnnotationTag(annotation, imageIndexFor(annotation, imageIndexBySource));
}).trim();
const unreferenced = annotations
.filter((annotation) => !referenced.has(annotation.index))
.map((annotation) => `${annotation.kind === "stroke" ? "标记" : "标注"}${annotation.index} ${seedreamAnnotationTag(annotation, imageIndexFor(annotation, imageIndexBySource))}`)
.filter((value) => !value.endsWith(" "));
if (!unreferenced.length) return transformed;
return `${transformed}\n编辑位置:${unreferenced.join(";")}。`.trim();
}
export function buildSeedreamLayerPrompt(prompt: string, annotations: SeedreamAnnotation[]) {
const annotationByIndex = new Map(annotations.map((annotation) => [annotation.index, annotation]));
const referenced = new Set<number>();
const transformed = normalizeSeedreamImageTokens(prompt, true).replace(/@(标注|标记)(\d+)/g, (token, _kind: string, rawIndex: string) => {
const index = Number(rawIndex);
const annotation = annotationByIndex.get(index);
if (!annotation || seedreamAnnotationToken(index, annotation.kind) !== token) return token;
referenced.add(index);
return seedreamAnnotationTag(annotation);
}).trim();
const unreferenced = annotations
.filter((annotation) => !referenced.has(annotation.index))
.map((annotation) => seedreamAnnotationTag(annotation))
.filter(Boolean);
if (!unreferenced.length) return transformed;
const instruction = `需拆分区域:${unreferenced.join(";")}。`;
return transformed ? `${transformed}\n${instruction}` : instruction;
}
export function normalizeSeedreamImageTokens(prompt: string, forceFirstImage = false) {
return prompt.replace(/@(图片|图)(\d+)/g, (_token, _kind: string, rawIndex: string) => `图${forceFirstImage ? 1 : Number(rawIndex)}`);
}
export function seedreamCreationModeLabel(mode: SeedreamCreationMode) {
if (mode === "interactive") return "交互编辑";
if (mode === "layers") return "图层拆分";
return "基础生图";
}
function coordinate(value: number) {
return Math.max(0, Math.min(1000, Math.round(value)));
}
function imageIndexFor(annotation: SeedreamAnnotation, imageIndexBySource: Map<string, number>) {
if (annotation.sourceKey) return imageIndexBySource.get(annotation.sourceKey) || 1;
return 1;
}
function annotationTokenIndex(token: string) {
return Number(token.match(/\d+$/)?.[0] || 0);
}