Use configurable video canvas prompts
This commit is contained in:
parent
7ee5dbc264
commit
79fffd88f4
@ -175,68 +175,77 @@ export function VideoCanvasSettingsClient() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{functions.map((item) => (
|
||||
<Card className={!item.enabled ? "opacity-70" : undefined} key={item.key}>
|
||||
<CardHeader className="space-y-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex flex-wrap items-center gap-2 text-base">
|
||||
<span>{item.label}</span>
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-normal text-muted-foreground">
|
||||
{functionKindLabels[item.kind]}
|
||||
</span>
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-normal text-muted-foreground">
|
||||
{item.route}
|
||||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-2">{item.description}</CardDescription>
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold tracking-tight">功能预设提示词</h2>
|
||||
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
|
||||
视频画布运行时会通过 /api/video-canvas/config 读取这里保存的提示词。比如“多机位九宫格”的分镜规则、黑色分割线要求和镜头说明,都由管理员在这里维护,前端不再写死覆盖。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{functions.map((item) => (
|
||||
<Card className={!item.enabled ? "opacity-70" : undefined} key={item.key}>
|
||||
<CardHeader className="space-y-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex flex-wrap items-center gap-2 text-base">
|
||||
<span>{item.label}</span>
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-normal text-muted-foreground">
|
||||
{functionKindLabels[item.kind]}
|
||||
</span>
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-normal text-muted-foreground">
|
||||
{item.route}
|
||||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-2">{item.description}</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor={`enabled-${item.key}`}>启用</Label>
|
||||
<Switch
|
||||
checked={item.enabled}
|
||||
id={`enabled-${item.key}`}
|
||||
onCheckedChange={(checked) => updateFunction(item.key, { enabled: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor={`enabled-${item.key}`}>启用</Label>
|
||||
<Switch
|
||||
checked={item.enabled}
|
||||
id={`enabled-${item.key}`}
|
||||
onCheckedChange={(checked) => updateFunction(item.key, { enabled: checked })}
|
||||
/>
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_120px]">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`model-${item.key}`}>模型覆盖</Label>
|
||||
<Input
|
||||
id={`model-${item.key}`}
|
||||
onChange={(event) => updateFunction(item.key, { model: event.target.value })}
|
||||
placeholder="留空使用全局默认模型"
|
||||
value={item.model || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`credit-${item.key}`}>扣点</Label>
|
||||
<Input
|
||||
id={`credit-${item.key}`}
|
||||
min={0}
|
||||
onChange={(event) =>
|
||||
updateFunction(item.key, { creditCost: Number(event.target.value || 0) })
|
||||
}
|
||||
type="number"
|
||||
value={item.creditCost ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-[1fr_120px]">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`model-${item.key}`}>模型覆盖</Label>
|
||||
<Input
|
||||
id={`model-${item.key}`}
|
||||
onChange={(event) => updateFunction(item.key, { model: event.target.value })}
|
||||
placeholder="留空使用全局默认模型"
|
||||
value={item.model || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`credit-${item.key}`}>扣点</Label>
|
||||
<Input
|
||||
id={`credit-${item.key}`}
|
||||
min={0}
|
||||
onChange={(event) =>
|
||||
updateFunction(item.key, { creditCost: Number(event.target.value || 0) })
|
||||
}
|
||||
type="number"
|
||||
value={item.creditCost ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Label htmlFor={`prompt-${item.key}`}>预设提示词 / 规则</Label>
|
||||
<Textarea
|
||||
className="mt-2 min-h-28"
|
||||
id={`prompt-${item.key}`}
|
||||
onChange={(event) => updateFunction(item.key, { prompt: event.target.value })}
|
||||
value={item.prompt}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Label htmlFor={`prompt-${item.key}`}>管理员可编辑的预设提示词 / 后端规则</Label>
|
||||
<Textarea
|
||||
className="mt-2 min-h-28"
|
||||
id={`prompt-${item.key}`}
|
||||
onChange={(event) => updateFunction(item.key, { prompt: event.target.value })}
|
||||
value={item.prompt}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -387,13 +387,10 @@ const OFFICIAL_ZOOM_DOCK_SCALE = 0.65;
|
||||
const OFFICIAL_DROPDOWN_SCALE = 0.7;
|
||||
const OFFICIAL_NINE_GRID_WIDTH = 930;
|
||||
const OFFICIAL_NINE_GRID_HEIGHT = 520;
|
||||
const MULTI_VIEW_NINE_GRID_FUNCTION_KEY = "multi-view-nine-grid";
|
||||
const PROMPT_COMPOSER_BASE_WIDTH = 895;
|
||||
const PROMPT_COMPOSER_BASE_HEIGHT = 286;
|
||||
const PRESET_COMPOSER_BASE_HEIGHT = 238;
|
||||
const LEGACY_MULTI_VIEW_NINE_GRID_PROMPT =
|
||||
"以选中图片为角色/主体参考,生成多视角 3x3 九宫格分镜图。保持主体一致,九个镜头包含远景、全身、侧身、背面、半身、近景、局部细节等视角,使用清晰黑色分割线分隔每一格,白色或干净背景。";
|
||||
const MULTI_VIEW_NINE_GRID_PROMPT =
|
||||
"以选中图片作为角色、服装、发型、场景光线的唯一参考,生成一张 3x3 多机位分镜合成图。必须是 9 个明显不同的镜头画面,不是同一张图裁切或重复人脸。九格分别包含:1 森林环境远景/主体很小,2 全身远景,3 中景正面,4 侧面半身,5 低机位或俯拍,6 面部近景,7 手部/衣袖/动作细节,8 背面或侧背,9 仰视或情绪特写。保持同一角色身份、绿色服装、自然森林光线一致,每格构图、景别、机位和主体大小必须不同。输出为单张 16:9 图片,九格之间使用清晰黑色分割线,不能出现文字、水印、编号,不能白底棚拍。";
|
||||
const NINE_GRID_CELL_VISUALS: GridCellVisual[] = [
|
||||
{ fit: "contain", objectPosition: "50% 50%", scale: 0.62 },
|
||||
{ fit: "contain", objectPosition: "58% 50%", scale: 0.74, translateX: -10 },
|
||||
@ -1016,6 +1013,7 @@ function createMediaResourceNode({
|
||||
function createNineGridCompositeNode({
|
||||
anchor,
|
||||
columns,
|
||||
gridPrompt,
|
||||
option,
|
||||
rows,
|
||||
sourceUrl,
|
||||
@ -1024,6 +1022,7 @@ function createNineGridCompositeNode({
|
||||
}: {
|
||||
anchor: CloneNode;
|
||||
columns: number;
|
||||
gridPrompt?: string;
|
||||
option?: string;
|
||||
rows: number;
|
||||
sourceUrl: string;
|
||||
@ -1035,10 +1034,11 @@ function createNineGridCompositeNode({
|
||||
cellCount === 9
|
||||
? "多机位九宫格"
|
||||
: option?.trim() || `${cellCount}宫格`;
|
||||
const gridPrompt =
|
||||
cellCount === 9 && (!option || /多|9|九/.test(option))
|
||||
? MULTI_VIEW_NINE_GRID_PROMPT
|
||||
: `基于选中图片生成 ${columns}x${rows} 宫格分镜图,使用黑色分割线分隔每一格。`;
|
||||
const resolvedGridPrompt =
|
||||
gridPrompt?.trim() ||
|
||||
(cellCount === 9 && (!option || /多|9|九/.test(option))
|
||||
? getDefaultFunctionPrompt(MULTI_VIEW_NINE_GRID_FUNCTION_KEY)
|
||||
: `基于选中图片生成 ${columns}x${rows} 宫格分镜图,使用黑色分割线分隔每一格。`);
|
||||
|
||||
return {
|
||||
id: makeId("image"),
|
||||
@ -1055,7 +1055,7 @@ function createNineGridCompositeNode({
|
||||
action: "IMAGE_GENERATE_NINE_GRID",
|
||||
generated: true,
|
||||
gridColumns: columns,
|
||||
gridPrompt,
|
||||
gridPrompt: resolvedGridPrompt,
|
||||
gridRows: rows,
|
||||
mediaType: "image",
|
||||
nineGridCells: Array.from({ length: cellCount }).map((_, index) => getGridCellVisual(index)),
|
||||
@ -1066,7 +1066,7 @@ function createNineGridCompositeNode({
|
||||
columns,
|
||||
mode: "grid",
|
||||
option: option || "",
|
||||
prompt: gridPrompt,
|
||||
prompt: resolvedGridPrompt,
|
||||
rows,
|
||||
sourceNodeId: anchor.id,
|
||||
sourceUrl,
|
||||
@ -1174,6 +1174,10 @@ function getConfiguredFunction(
|
||||
return config.functions[key] || defaultVideoCanvasFunctionConfig.functions[key];
|
||||
}
|
||||
|
||||
function getDefaultFunctionPrompt(key: string) {
|
||||
return defaultVideoCanvasFunctionConfig.functions[key]?.prompt || "";
|
||||
}
|
||||
|
||||
function getFunctionModel(
|
||||
config: VideoCanvasFunctionConfig,
|
||||
definition: VideoCanvasFunctionDefinition,
|
||||
@ -1203,16 +1207,10 @@ function composeMultiViewNineGridPrompt(
|
||||
definition: VideoCanvasFunctionDefinition,
|
||||
option?: string,
|
||||
) {
|
||||
const configuredPrompt = definition.prompt.trim();
|
||||
const configuredPrompt = definition.prompt.trim() || getDefaultFunctionPrompt(MULTI_VIEW_NINE_GRID_FUNCTION_KEY);
|
||||
const selectedOption = String(option || "").trim();
|
||||
const parts = [MULTI_VIEW_NINE_GRID_PROMPT];
|
||||
const isLegacyPrompt =
|
||||
configuredPrompt === LEGACY_MULTI_VIEW_NINE_GRID_PROMPT ||
|
||||
(configuredPrompt.includes("以选中图片为角色/主体参考") && configuredPrompt.includes("白色或干净背景"));
|
||||
const parts = [configuredPrompt];
|
||||
|
||||
if (configuredPrompt && configuredPrompt !== MULTI_VIEW_NINE_GRID_PROMPT && !isLegacyPrompt) {
|
||||
parts.push(`后台补充:${configuredPrompt}`);
|
||||
}
|
||||
if (selectedOption) {
|
||||
parts.push(`用户选择:${selectedOption}`);
|
||||
}
|
||||
@ -1544,7 +1542,7 @@ function normalizeRestoredNode(node: CloneNode, fallbackIndex: number): CloneNod
|
||||
draftValues: {
|
||||
...(node.draftValues || {}),
|
||||
action: "IMAGE_GENERATE_NINE_GRID",
|
||||
gridPrompt: getDraftString(node, "gridPrompt") || MULTI_VIEW_NINE_GRID_PROMPT,
|
||||
gridPrompt: getDraftString(node, "gridPrompt") || getDefaultFunctionPrompt(MULTI_VIEW_NINE_GRID_FUNCTION_KEY),
|
||||
nineGridComposite: true,
|
||||
outputSizeLabel: getDraftString(node, "outputSizeLabel") || "2752 × 1536",
|
||||
},
|
||||
@ -2578,10 +2576,12 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const title = labels[action];
|
||||
if (action === "grid") {
|
||||
const { columns, rows } = normalizeGridOption(option || "9宫格");
|
||||
const gridDefinition = getConfiguredFunction(videoCanvasFunctionConfig, "multi-view-nine-grid");
|
||||
const gridDefinition = getConfiguredFunction(videoCanvasFunctionConfig, MULTI_VIEW_NINE_GRID_FUNCTION_KEY);
|
||||
const prompt = composeMultiViewNineGridPrompt(gridDefinition, option);
|
||||
const gridNode = createNineGridCompositeNode({
|
||||
anchor,
|
||||
columns,
|
||||
gridPrompt: prompt,
|
||||
option,
|
||||
rows,
|
||||
sourceUrl,
|
||||
@ -2613,7 +2613,6 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
setActivePanel(null);
|
||||
|
||||
if (videoCanvasFunctionConfig.enabled && gridDefinition?.enabled) {
|
||||
const prompt = composeMultiViewNineGridPrompt(gridDefinition, option);
|
||||
const response = await imageGenerationService.generateImage({
|
||||
aspectRatio: "16:9",
|
||||
imageCount: 1,
|
||||
@ -2852,7 +2851,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const starterMode = getPresetMode(anchor);
|
||||
const functionKey =
|
||||
payload.slashScene && /九宫格|宫格|多机位/.test(payload.slashScene)
|
||||
? "multi-view-nine-grid"
|
||||
? MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
||||
: starterMode === "character-three-view"
|
||||
? "character-three-view"
|
||||
: "image-repaint";
|
||||
@ -2862,7 +2861,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
payload.prompt.trim() || payload.slashScene || "生成角色三视图",
|
||||
);
|
||||
const finalPrompt =
|
||||
functionKey === "multi-view-nine-grid"
|
||||
functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
||||
? composeMultiViewNineGridPrompt(definition, payload.prompt.trim() || payload.slashScene || "多机位九宫格")
|
||||
: prompt;
|
||||
const count = Math.max(1, Math.min(4, Number.parseInt(payload.settings.count, 10) || 1));
|
||||
@ -2885,14 +2884,14 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const generatedNode = generatedUrl
|
||||
? createMediaResourceNode({
|
||||
action: "IMAGE_RESOURCE",
|
||||
height: functionKey === "multi-view-nine-grid" ? OFFICIAL_NINE_GRID_HEIGHT : 280,
|
||||
height: functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY ? OFFICIAL_NINE_GRID_HEIGHT : 280,
|
||||
mediaType: "image",
|
||||
mediaUrl: generatedUrl,
|
||||
prompt: finalPrompt,
|
||||
title: payload.slashScene || definition.label || "图片生成",
|
||||
width: functionKey === "multi-view-nine-grid" ? OFFICIAL_NINE_GRID_WIDTH : 420,
|
||||
width: functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY ? OFFICIAL_NINE_GRID_WIDTH : 420,
|
||||
x: rightX,
|
||||
y: Math.round(anchor.y + anchor.height / 2 - (functionKey === "multi-view-nine-grid" ? OFFICIAL_NINE_GRID_HEIGHT : 280) / 2),
|
||||
y: Math.round(anchor.y + anchor.height / 2 - (functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY ? OFFICIAL_NINE_GRID_HEIGHT : 280) / 2),
|
||||
})
|
||||
: null;
|
||||
const created = generatedNode || createTargetModuleNode("image", rightX, 0, nodes.length + 1);
|
||||
@ -3118,13 +3117,16 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
if (node.inputType === "image" || /IMAGE_GENERATE/.test(action)) {
|
||||
const functionKey =
|
||||
action === "IMAGE_GENERATE_NINE_GRID"
|
||||
? "multi-view-nine-grid"
|
||||
? MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
||||
: node.draftValues?.generatorType === "ENHANCE"
|
||||
? "hd-upscale"
|
||||
: "image-repaint";
|
||||
const definition = getConfiguredFunction(videoCanvasFunctionConfig, functionKey);
|
||||
const sourceUrl = imageUrls[0] || getNodeSourceMediaUrl(node);
|
||||
const prompt = composeFunctionPrompt(definition, rawPrompt || String(params.option || node.title));
|
||||
const prompt =
|
||||
functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY
|
||||
? composeMultiViewNineGridPrompt(definition, String(params.option || node.title))
|
||||
: composeFunctionPrompt(definition, rawPrompt || String(params.option || node.title));
|
||||
let generatedUrl = "";
|
||||
|
||||
if (definition.route === "enhance" && sourceUrl) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user