feat: add admin accounts and image templates

This commit is contained in:
inman
2026-07-03 11:25:25 +08:00
parent d98e58adfa
commit c3ea9f0eb1
61 changed files with 7016 additions and 199 deletions

View File

@@ -1,4 +1,5 @@
import { getTemplateById, type VideoTemplate } from "@/lib/content/video-templates";
import { extractMaterialPlaceholders } from "@/lib/prompt/material-placeholders";
export type StoryboardScene = {
id: string;
@@ -112,12 +113,8 @@ export function assemblePrompt(input: PromptAssemblyInput): PromptAssemblyResult
export function extractMaterialRequirements(prompt: string) {
const requirements = { image: 0, video: 0, audio: 0 };
for (const match of prompt.matchAll(/@(参考视频|图片|图|视频|音频)(\d*)/g)) {
const kind = match[1];
const index = match[2] ? Number(match[2]) : 1;
if (kind === "图片" || kind === "图") requirements.image = Math.max(requirements.image, index);
if (kind === "视频" || kind === "参考视频") requirements.video = Math.max(requirements.video, index);
if (kind === "音频") requirements.audio = Math.max(requirements.audio, index);
for (const placeholder of extractMaterialPlaceholders(prompt)) {
requirements[placeholder.type] = Math.max(requirements[placeholder.type], placeholder.index);
}
return requirements;
}

View File

@@ -0,0 +1,63 @@
import { extractMaterialPlaceholders } from "@/lib/prompt/material-placeholders";
export type MaterialDraftKind = "image" | "video" | "audio";
const tokenBoundaryBeforePattern = /[\s(【\[]$/;
const tokenBoundaryAfterPattern = /^[\s)】\]]/;
export function detectMaterialDraftStart(previous: string, next: string, cursor: number): { text: string; index: number } | null {
const index = cursor - 1;
if (index < 0 || next[index] !== "@") return null;
if (next.length !== previous.length + 1) return null;
if (`${next.slice(0, index)}${next.slice(cursor)}` !== previous) return null;
return { text: previous, index };
}
export function normalizeMaterialDraftToken(input: string, prompt: string, defaultKind: MaterialDraftKind = "image"): string | null {
const query = input.replace(/^@+/, "").trim().replace(/\s+/g, "");
if (!query) return nextMaterialDraftToken(defaultKind, prompt);
const explicit = query.match(/^(参考视频|图片|图|视频|音频)(\d+)$/);
if (explicit) {
const kind = kindFromText(explicit[1]);
const index = Number(explicit[2]);
if (Number.isInteger(index) && index > 0) return labelForDraftKind(kind, index);
return null;
}
const kindOnly = query.match(/^(参考视频|图片|图|视频|音频)$/);
if (kindOnly) return nextMaterialDraftToken(kindFromText(kindOnly[1]), prompt);
return null;
}
export function nextMaterialDraftToken(kind: MaterialDraftKind, prompt: string): string {
const placeholders = extractMaterialPlaceholders(prompt).filter((placeholder) => placeholder.type === kind);
const nextIndex = placeholders.reduce((max, placeholder) => Math.max(max, placeholder.index), 0) + 1;
return labelForDraftKind(kind, nextIndex);
}
export function insertMaterialDraftToken(text: string, index: number, token: string): { text: string; cursor: number } {
const safeIndex = Math.max(0, Math.min(index, text.length));
const before = text.slice(0, safeIndex);
const after = text.slice(safeIndex);
const needsLeadingSpace = before.length > 0 && !tokenBoundaryBeforePattern.test(before);
const needsTrailingSpace = after.length === 0 || !tokenBoundaryAfterPattern.test(after);
const insertion = `${needsLeadingSpace ? " " : ""}${token}${needsTrailingSpace ? " " : ""}`;
return {
text: `${before}${insertion}${after}`,
cursor: before.length + insertion.length
};
}
export function labelForDraftKind(kind: MaterialDraftKind, index: number): string {
if (kind === "video") return `@视频${index}`;
if (kind === "audio") return `@音频${index}`;
return `@图片${index}`;
}
function kindFromText(value: string): MaterialDraftKind {
if (value === "视频" || value === "参考视频") return "video";
if (value === "音频") return "audio";
return "image";
}

View File

@@ -0,0 +1,35 @@
export type MaterialPlaceholder = {
token: string;
type: "image" | "video" | "audio";
index: number;
};
const materialTokenPattern = /@(参考视频|图片|图|视频|音频)(\d+)/g;
export function extractMaterialPlaceholders(prompt: string): MaterialPlaceholder[] {
const placeholders: MaterialPlaceholder[] = [];
const seen = new Set<string>();
for (const match of prompt.matchAll(materialTokenPattern)) {
const kind = match[1];
const index = Number(match[2]);
if (!Number.isInteger(index) || index < 1) continue;
const type = typeForKind(kind);
const token = labelForPlaceholder(type, index);
if (seen.has(token)) continue;
seen.add(token);
placeholders.push({ token, type, index });
}
return placeholders;
}
function typeForKind(kind: string): MaterialPlaceholder["type"] {
if (kind === "视频" || kind === "参考视频") return "video";
if (kind === "音频") return "audio";
return "image";
}
function labelForPlaceholder(type: MaterialPlaceholder["type"], index: number) {
if (type === "video") return `@视频${index}`;
if (type === "audio") return `@音频${index}`;
return `@图片${index}`;
}