160 lines
5.0 KiB
TypeScript
160 lines
5.0 KiB
TypeScript
export type VideoPromptMention = {
|
|
id: string;
|
|
index: number;
|
|
label: string;
|
|
mediaType: "audio" | "image" | "video";
|
|
offset: number;
|
|
sourceNodeId: string;
|
|
url: string;
|
|
};
|
|
|
|
export type VideoPromptToolMark = {
|
|
id: string;
|
|
label: string;
|
|
};
|
|
|
|
export type VideoPromptToolReference = Pick<
|
|
VideoPromptMention,
|
|
"id" | "index" | "label" | "mediaType" | "offset"
|
|
>;
|
|
|
|
const STRUCTURE_TOKEN_PATTERN = /\[\[(REF|MARK):([A-Za-z0-9_-]{1,100})\]\]/g;
|
|
|
|
function normalizeTokenId(value: unknown) {
|
|
return String(value || "")
|
|
.trim()
|
|
.replace(/[^A-Za-z0-9_-]+/g, "-")
|
|
.slice(0, 100);
|
|
}
|
|
|
|
export function normalizeVideoPromptMentions(value: unknown): VideoPromptMention[] {
|
|
if (!Array.isArray(value)) return [];
|
|
|
|
return value.slice(0, 20).flatMap((item, position) => {
|
|
if (!item || typeof item !== "object") return [];
|
|
const record = item as Record<string, unknown>;
|
|
const sourceNodeId = String(record.sourceNodeId || "").trim().slice(0, 120);
|
|
if (!sourceNodeId) return [];
|
|
const index = Math.max(1, Math.min(10, Math.round(Number(record.index) || position + 1)));
|
|
const mediaType = ["audio", "image", "video"].includes(String(record.mediaType))
|
|
? (String(record.mediaType) as VideoPromptMention["mediaType"])
|
|
: "image";
|
|
const id = normalizeTokenId(record.id) || `mention-${sourceNodeId}-${position + 1}`;
|
|
|
|
return [{
|
|
id,
|
|
index,
|
|
label: String(record.label || `图片 ${index}`).trim().slice(0, 100) || `图片 ${index}`,
|
|
mediaType,
|
|
offset: Math.max(0, Math.round(Number(record.offset) || 0)),
|
|
sourceNodeId,
|
|
url: String(record.url || "").trim().slice(0, 2048),
|
|
}];
|
|
});
|
|
}
|
|
|
|
function referenceToken(id: string) {
|
|
return `[[REF:${normalizeTokenId(id)}]]`;
|
|
}
|
|
|
|
function markToken(id: string) {
|
|
return `[[MARK:${normalizeTokenId(id)}]]`;
|
|
}
|
|
|
|
export function injectVideoPromptStructure(
|
|
prompt: string,
|
|
references: VideoPromptToolReference[],
|
|
marks: VideoPromptToolMark[],
|
|
) {
|
|
const normalizedPrompt = String(prompt || "");
|
|
const insertions = references
|
|
.map((reference, order) => ({
|
|
order,
|
|
offset: Math.max(0, Math.min(normalizedPrompt.length, Math.round(reference.offset || 0))),
|
|
token: referenceToken(reference.id),
|
|
}))
|
|
.sort((left, right) => left.offset - right.offset || left.order - right.order);
|
|
let cursor = 0;
|
|
let result = "";
|
|
|
|
for (const insertion of insertions) {
|
|
result += normalizedPrompt.slice(cursor, insertion.offset);
|
|
result += insertion.token;
|
|
cursor = insertion.offset;
|
|
}
|
|
result += normalizedPrompt.slice(cursor);
|
|
|
|
const markTokens = marks
|
|
.map((mark) => markToken(mark.id))
|
|
.filter((token) => token !== "[[MARK:]]");
|
|
return markTokens.length > 0 ? `${result} ${markTokens.join(" ")}`.trim() : result;
|
|
}
|
|
|
|
export function buildVideoPromptStructureInstructions(
|
|
references: VideoPromptToolReference[],
|
|
marks: VideoPromptToolMark[],
|
|
) {
|
|
const lines = [
|
|
...references.map(
|
|
(reference) =>
|
|
`${referenceToken(reference.id)} = @${reference.index}「${reference.label}」${reference.mediaType}素材`,
|
|
),
|
|
...marks.map((mark) => `${markToken(mark.id)} = 画面元素标记「${mark.label}」`),
|
|
];
|
|
if (lines.length === 0) return "";
|
|
|
|
return [
|
|
"结构化占位符协议:下列占位符代表编辑器中的不可编辑芯片。理解其语义,并在输出中逐字保留每个占位符一次;不要翻译、改写、删除或重复占位符。",
|
|
...lines,
|
|
].join("\n");
|
|
}
|
|
|
|
export function resolveVideoPromptStructure(
|
|
generatedText: string,
|
|
references: VideoPromptToolReference[],
|
|
marks: VideoPromptToolMark[],
|
|
options: { flatten: boolean },
|
|
) {
|
|
const referenceById = new Map(references.map((item) => [normalizeTokenId(item.id), item]));
|
|
const markById = new Map(marks.map((item) => [normalizeTokenId(item.id), item]));
|
|
const mentionOffsets: Record<string, number> = {};
|
|
const preservedMarkIds: string[] = [];
|
|
let cursor = 0;
|
|
let text = "";
|
|
|
|
for (const match of String(generatedText || "").matchAll(STRUCTURE_TOKEN_PATTERN)) {
|
|
const start = match.index ?? 0;
|
|
text += String(generatedText || "").slice(cursor, start);
|
|
const kind = match[1];
|
|
const id = normalizeTokenId(match[2]);
|
|
if (kind === "REF") {
|
|
const reference = referenceById.get(id);
|
|
if (reference) {
|
|
if (options.flatten) text += reference.label;
|
|
else mentionOffsets[reference.id] = text.length;
|
|
}
|
|
} else {
|
|
const mark = markById.get(id);
|
|
if (mark) {
|
|
if (options.flatten) text += mark.label;
|
|
else preservedMarkIds.push(mark.id);
|
|
}
|
|
}
|
|
cursor = start + match[0].length;
|
|
}
|
|
text += String(generatedText || "").slice(cursor);
|
|
|
|
const leadingWhitespace = text.length - text.trimStart().length;
|
|
if (leadingWhitespace > 0) {
|
|
for (const key of Object.keys(mentionOffsets)) {
|
|
mentionOffsets[key] = Math.max(0, mentionOffsets[key] - leadingWhitespace);
|
|
}
|
|
}
|
|
|
|
return {
|
|
mentionOffsets,
|
|
preservedMarkIds,
|
|
text: text.trim(),
|
|
};
|
|
}
|