feat(canvas): 项目画风 - 扩展 18 套预设并联动角色三视图
This commit is contained in:
@@ -5,6 +5,7 @@ import { Box, Check, ChevronDown, FileText, Image as ImageIcon, Link2, Music2, P
|
||||
|
||||
import { WorkspaceState } from "@/components/layout/workspace-state";
|
||||
import { WorkspaceSignalIcon } from "@/components/ui/aceternity/workspace-signal-icon";
|
||||
import { canvasStylePresets } from "@/components/canvas/canvas-style-picker-modal";
|
||||
import { resourceFileUrl, resourceIdFromStorageKey } from "@/services/api/resources";
|
||||
import {
|
||||
bindProjectCharacterVoice,
|
||||
@@ -99,7 +100,8 @@ export default function ProjectAssetsView({ detail, refreshProject }: ProjectDet
|
||||
const model = effectiveConfig.imageModel || effectiveConfig.model;
|
||||
const config = { ...effectiveConfig, model };
|
||||
if (!isAiConfigReady(config, model)) throw new Error("请先在设置中配置可用的图片模型");
|
||||
await generateCharacterTurnaround({ projectId: detail.project.id, assetId: asset.id, versionId: asset.character.versionId, name: asset.title, definition: asset.character.definition, config });
|
||||
const projectStyle = canvasStylePresets.find((preset) => preset.id === detail.project.stylePresetId);
|
||||
await generateCharacterTurnaround({ projectId: detail.project.id, assetId: asset.id, versionId: asset.character.versionId, name: asset.title, definition: asset.character.definition, projectStyle, config });
|
||||
return getProjectCharacter(detail.project.id, asset.id);
|
||||
},
|
||||
onSuccess: (result) => { syncPersonalCharacterProjection(result.asset); done("三视图已生成并绑定到新角色版本"); },
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { runBackendCanvasGenerationTask } from "@/lib/canvas/canvas-project-generation";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
export async function generateCharacterTurnaround(input: { projectId: string; assetId: string; versionId: string; name: string; definition: Record<string, unknown>; config: AiConfig }) {
|
||||
const prompt = characterTurnaroundPrompt(input.name, input.definition);
|
||||
type ProjectStylePrompt = { id: string; title: string; prompt: string };
|
||||
|
||||
export async function generateCharacterTurnaround(input: { projectId: string; assetId: string; versionId: string; name: string; definition: Record<string, unknown>; projectStyle?: ProjectStylePrompt; config: AiConfig }) {
|
||||
const prompt = characterTurnaroundPrompt(input.name, input.definition, input.projectStyle);
|
||||
await runBackendCanvasGenerationTask({
|
||||
projectId: input.projectId,
|
||||
nodeId: `character-turnaround:${input.assetId}`,
|
||||
mode: "image",
|
||||
prompt,
|
||||
config: { ...input.config, model: input.config.imageModel || input.config.model, count: "1" },
|
||||
metadata: { operation: "character_turnaround", characterAssetId: input.assetId, resolvedCharacterVersions: [{ assetId: input.assetId, versionId: input.versionId }] },
|
||||
metadata: { operation: "character_turnaround", characterAssetId: input.assetId, stylePresetId: input.projectStyle?.id, resolvedCharacterVersions: [{ assetId: input.assetId, versionId: input.versionId }] },
|
||||
});
|
||||
}
|
||||
|
||||
export function characterTurnaroundPrompt(name: string, definition: Record<string, unknown>) {
|
||||
export function characterTurnaroundPrompt(name: string, definition: Record<string, unknown>, projectStyle?: ProjectStylePrompt) {
|
||||
const visual = [definition.role, definition.appearance, definition.physique, definition.clothing, definition.personality, definition.props, definition.consistencyPrompt, definition.multiViewPrompt]
|
||||
.map((value) => String(value || "").trim())
|
||||
.filter(Boolean)
|
||||
@@ -23,7 +25,23 @@ export function characterTurnaroundPrompt(name: string, definition: Record<strin
|
||||
`为角色“${name}”制作专业人物三视图设定表。`,
|
||||
"画面严格分成三个等宽竖向区域,从左到右依次为正面全身、右侧面全身、背面全身。",
|
||||
"三个视角必须是同一个人、同一服装、同一发型、同一体型和同一比例,站立中性姿势,完整显示头顶到脚底。",
|
||||
"纯净浅灰背景,均匀棚拍光线,不添加文字、边框、道具说明、表情变化或额外人物。",
|
||||
"使用纯净中性浅色背景和均匀设定稿光线;背景只负责分离轮廓,不得改变项目画风的绘画或渲染媒介。",
|
||||
"不添加文字、边框、道具说明、表情变化或额外人物。",
|
||||
characterStyleConstraint(projectStyle),
|
||||
`角色设定:${visual}`,
|
||||
].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
function characterStyleConstraint(projectStyle?: ProjectStylePrompt) {
|
||||
if (!projectStyle) return "";
|
||||
// 三视图只继承角色资产相关规范,避免把建筑、运镜等项目规则错误塞进静态设定表。
|
||||
const characterSections = new Set(["项目定位", "项目色彩系统", "角色设计系统", "服饰与材质系统", "资产一致性", "全局禁用"]);
|
||||
const rules = projectStyle.prompt.split("\n").filter((line) => {
|
||||
const title = line.match(/^【([^】]+)】/)?.[1];
|
||||
return title ? characterSections.has(title) : false;
|
||||
});
|
||||
return [
|
||||
`项目画风:${projectStyle.title}。角色造型、配色、服装材质与最终渲染媒介必须遵循以下规范:`,
|
||||
...rules,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { App, Button, Input, Modal, Select, Tooltip } from "antd";
|
||||
import { Archive, Check, Save, ShieldAlert } from "lucide-react";
|
||||
import { App, Button, Input, Modal, Select } from "antd";
|
||||
import { Archive, Check, Eye, Save, ShieldAlert } from "lucide-react";
|
||||
|
||||
import { canvasStylePresets } from "@/components/canvas/canvas-style-picker-modal";
|
||||
import { CanvasStyleDetailModal, canvasStylePresets, type CanvasStylePreset } from "@/components/canvas/canvas-style-picker-modal";
|
||||
import { updateProject } from "@/services/api/projects";
|
||||
|
||||
import type { ProjectDetailViewProps } from "./shared";
|
||||
@@ -16,6 +16,7 @@ export default function ProjectSettingsView({ detail, refreshProject }: ProjectD
|
||||
const [aspectRatio, setAspectRatio] = useState(project.aspectRatio);
|
||||
const [sourceType, setSourceType] = useState(project.sourceType);
|
||||
const [stylePresetId, setStylePresetId] = useState(project.stylePresetId || "");
|
||||
const [styleDetail, setStyleDetail] = useState<CanvasStylePreset | null>(null);
|
||||
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||
useEffect(() => { setName(project.name); setDescription(project.description || ""); setAspectRatio(project.aspectRatio); setSourceType(project.sourceType); setStylePresetId(project.stylePresetId || ""); }, [project]);
|
||||
const dirty = useMemo(() => name.trim() !== project.name || description !== (project.description || "") || aspectRatio !== project.aspectRatio || sourceType !== project.sourceType || stylePresetId !== (project.stylePresetId || ""), [aspectRatio, description, name, project, sourceType, stylePresetId]);
|
||||
@@ -41,7 +42,7 @@ export default function ProjectSettingsView({ detail, refreshProject }: ProjectD
|
||||
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{canvasStylePresets.map((preset) => {
|
||||
const active = preset.id === stylePresetId;
|
||||
return <Tooltip key={preset.id} title={preset.description}><button type="button" onClick={() => setStylePresetId(preset.id)} className={`group grid min-w-0 grid-cols-[72px_minmax(0,1fr)_24px] items-center gap-2 overflow-hidden rounded-lg border p-1.5 text-left transition-colors ${active ? "border-[var(--workspace-accent)] bg-[var(--workspace-accent-soft)]" : "border-border/80 hover:border-foreground/25"}`}><img src={preset.imageUrl} alt="" loading="lazy" className="h-11 w-[72px] rounded object-cover" /><span className="min-w-0"><span className="block truncate text-xs font-medium">{preset.title}</span><span className="mt-0.5 block truncate text-[10px] text-foreground/42">{preset.description}</span></span><span className={`grid size-5 place-items-center rounded-full ${active ? "bg-[var(--workspace-accent)] text-white" : "border border-border text-transparent"}`}><Check className="size-3" /></span></button></Tooltip>;
|
||||
return <div key={preset.id} className={`group flex min-w-0 items-center overflow-hidden rounded-lg border transition-colors ${active ? "border-[var(--workspace-accent)] bg-[var(--workspace-accent-soft)]" : "border-border/80 hover:border-foreground/25"}`}><button type="button" onClick={() => setStylePresetId(preset.id)} className="grid min-w-0 flex-1 grid-cols-[72px_minmax(0,1fr)_24px] items-center gap-2 p-1.5 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"><img src={preset.imageUrl} width="72" height="44" alt={`${preset.title}画风示意`} loading="lazy" className="h-11 w-[72px] rounded object-cover" /><span className="min-w-0"><span className="block truncate text-xs font-medium">{preset.title}</span><span className="mt-0.5 block truncate text-[10px] text-foreground/42">{preset.description}</span></span><span className={`grid size-5 place-items-center rounded-full ${active ? "bg-[var(--workspace-accent)] text-white" : "border border-border text-transparent"}`}><Check className="size-3" /></span></button><button type="button" className="mr-1 grid size-8 shrink-0 place-items-center rounded-md text-foreground/42 hover:bg-foreground/[.06] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" onClick={() => setStyleDetail(preset)} aria-label={`查看${preset.title}详情`} title="查看画风详情"><Eye className="size-3.5" /></button></div>;
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
@@ -54,6 +55,7 @@ export default function ProjectSettingsView({ detail, refreshProject }: ProjectD
|
||||
</section>
|
||||
|
||||
<Modal title={project.status === "archived" ? "恢复项目" : "归档项目"} open={archiveOpen} okText={project.status === "archived" ? "确认恢复" : "确认归档"} cancelText="取消" okButtonProps={{ danger: project.status !== "archived", loading: archiveMutation.isPending }} onCancel={() => setArchiveOpen(false)} onOk={() => archiveMutation.mutate()} width={440} styles={{ body: { paddingTop: 12 } }}><p className="m-0 text-sm leading-6 text-foreground/65">{project.status === "archived" ? "恢复后项目会重新进入可编辑状态。" : "归档不会删除章节、画布或资产,画布文档仍可在创作画布中打开。"}</p></Modal>
|
||||
<CanvasStyleDetailModal open={Boolean(styleDetail)} preset={styleDetail} selected={styleDetail?.id === stylePresetId} onClose={() => setStyleDetail(null)} onSelect={(preset) => { setStylePresetId(preset.id); setStyleDetail(null); }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user