This commit is contained in:
2026-04-19 20:33:18 +08:00
parent 692643136f
commit 67c584b4df
123 changed files with 11898 additions and 4082 deletions

View File

@@ -1,7 +1,4 @@
import {
ImagePlus,
RefreshCcw,
} from 'lucide-react';
import { ImagePlus, RefreshCcw } from 'lucide-react';
import {
type ChangeEvent,
type CSSProperties,
@@ -34,6 +31,7 @@ import {
saveCharacterWorkflowCache,
} from './asset-studio/characterAssetWorkflowPersistence';
import { buildDefaultRolePromptBundle } from './asset-studio/customWorldRolePromptDefaults';
import { buildProjectPixelStyleReferenceBoard } from './asset-studio/projectPixelStyleReference';
import { CharacterAnimator } from './CharacterAnimator';
type EditableCustomWorldRole = {
@@ -92,16 +90,7 @@ const CORE_ACTIONS: CustomWorldAiActionConfig[] = [
templateId: 'attack_slash',
fps: 12,
frameCount: 8,
durationSeconds: 3,
loop: false,
},
{
animation: AnimationState.HURT,
label: '受击',
templateId: 'hurt',
fps: 10,
frameCount: 6,
durationSeconds: 3,
durationSeconds: 4,
loop: false,
},
{
@@ -329,9 +318,7 @@ function ActionButton({
<span className="flex flex-col items-start leading-tight">
<span>{label}</span>
{subLabel ? (
<span className="text-[11px] font-medium opacity-70">
{subLabel}
</span>
<span className="text-[11px] font-medium opacity-70">{subLabel}</span>
) : null}
</span>
</button>
@@ -351,7 +338,9 @@ function buildRoleCharacterBrief(
role.personality ? `角色性格:${role.personality}` : '',
role.motivation ? `角色动机:${role.motivation}` : '',
role.combatStyle ? `战斗风格:${role.combatStyle}` : '',
role.tags && role.tags.length > 0 ? `角色标签:${role.tags.join('、')}` : '',
role.tags && role.tags.length > 0
? `角色标签:${role.tags.join('、')}`
: '',
templateLabel ? `参考模板:${templateLabel}` : '',
]
.filter(Boolean)
@@ -606,6 +595,10 @@ export function CustomWorldRoleAssetStudioModal({
const [referenceImageDataUrls, setReferenceImageDataUrls] = useState<
string[]
>([]);
const [
projectStyleReferenceBoardSource,
setProjectStyleReferenceBoardSource,
] = useState('');
const [visualDrafts, setVisualDrafts] = useState<CharacterVisualDraft[]>([]);
const [selectedVisualDraftId, setSelectedVisualDraftId] = useState('');
const [visualStatus, setVisualStatus] = useState<string | null>(null);
@@ -632,9 +625,9 @@ export function CustomWorldRoleAssetStudioModal({
const selectedTemplate =
roleKind === 'playable' && workingRole.templateCharacterId
? ROLE_TEMPLATE_CHARACTERS.find(
? (ROLE_TEMPLATE_CHARACTERS.find(
(character) => character.id === workingRole.templateCharacterId,
) ?? null
) ?? null)
: null;
const characterBriefText = useMemo(
() =>
@@ -679,7 +672,7 @@ export function CustomWorldRoleAssetStudioModal({
);
const selectedActionConfig =
CORE_ACTIONS.find((item) => item.animation === selectedAnimation) ??
CORE_ACTIONS[0];
CORE_ACTIONS[0]!;
const previewCharacter = useMemo(
() =>
buildAnimationPreviewCharacter({
@@ -691,7 +684,8 @@ export function CustomWorldRoleAssetStudioModal({
const selectedAnimationConfig = previewCharacter?.animationMap?.[
selectedAnimation
] as CharacterAnimationConfig | undefined;
const selectedAnimationStatus = animationStatusByKey[selectedAnimation] ?? null;
const selectedAnimationStatus =
animationStatusByKey[selectedAnimation] ?? null;
const isSelectedAnimationGenerating =
generatingAnimationMap[selectedAnimation] === true;
const hasAnyGeneratingAnimations = Object.values(generatingAnimationMap).some(
@@ -705,9 +699,39 @@ export function CustomWorldRoleAssetStudioModal({
() => getAnimationPreviewViewportStyle(440),
[],
);
const effectiveVisualReferenceImageDataUrls = useMemo(() => {
if (!projectStyleReferenceBoardSource) {
return referenceImageDataUrls;
}
if (referenceImageDataUrls.length >= 4) {
return referenceImageDataUrls;
}
return [projectStyleReferenceBoardSource, ...referenceImageDataUrls].slice(
0,
4,
);
}, [projectStyleReferenceBoardSource, referenceImageDataUrls]);
const visualSourceMode =
referenceImageDataUrls.length > 0 ? 'image-to-image' : 'text-to-image';
useEffect(() => {
let cancelled = false;
void buildProjectPixelStyleReferenceBoard()
.then((nextBoardSource) => {
if (!cancelled) {
setProjectStyleReferenceBoardSource(nextBoardSource);
}
})
.catch(() => undefined);
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
let cancelled = false;
setWorkingRole(baseRole);
@@ -759,7 +783,9 @@ export function CustomWorldRoleAssetStudioModal({
cache.selectedVisualDraftId || cache.visualDrafts?.[0]?.id || '',
);
setSelectedAnimation(
CORE_ACTIONS.some((item) => item.animation === cache.selectedAnimation)
CORE_ACTIONS.some(
(item) => item.animation === cache.selectedAnimation,
)
? (cache.selectedAnimation as AnimationState)
: (CORE_ACTIONS[0]?.animation ?? AnimationState.IDLE),
);
@@ -774,11 +800,7 @@ export function CustomWorldRoleAssetStudioModal({
return () => {
cancelled = true;
};
}, [
baseRole,
initialPromptBundle,
roleSnapshotKey,
]);
}, [baseRole, initialPromptBundle, roleSnapshotKey]);
useEffect(() => {
if (isHydratingCache) {
@@ -913,7 +935,7 @@ export function CustomWorldRoleAssetStudioModal({
sourceMode: visualSourceMode,
promptText: visualPromptText,
characterBriefText,
referenceImageDataUrls: referenceImageDataUrls,
referenceImageDataUrls: effectiveVisualReferenceImageDataUrls,
candidateCount: 1,
imageModel: 'wan2.7-image-pro',
size: '1024*1024',
@@ -940,10 +962,6 @@ export function CustomWorldRoleAssetStudioModal({
throw new Error('请先生成角色形象,再生成动作。');
}
const isLoopAction = config.loop;
const shouldUseLastFrameReference =
!isLoopAction && config.animation !== AnimationState.DIE;
const result = await generateCharacterAnimationDraft({
characterId: workingRole.id,
strategy: 'image-to-video',
@@ -954,17 +972,16 @@ export function CustomWorldRoleAssetStudioModal({
visualSource: workingRole.imageSrc,
referenceImageDataUrls: [],
referenceVideoDataUrls: [],
lastFrameImageDataUrl: shouldUseLastFrameReference
? workingRole.imageSrc
: undefined,
lastFrameImageDataUrl: workingRole.imageSrc,
frameCount: config.frameCount,
fps: config.fps,
durationSeconds: config.durationSeconds,
loop: config.loop,
useChromaKey: true,
resolution: isLoopAction ? '720P' : '480P',
resolution: '480p',
ratio: '1:1',
imageSequenceModel: 'wan2.7-image-pro',
videoModel: isLoopAction ? 'wan2.6-i2v-flash' : 'wan2.2-kf2v-flash',
videoModel: 'doubao-seedance-2-0-fast-260128',
referenceVideoModel: 'wan2.7-r2v',
motionTransferModel: 'wan2.2-animate-move',
} satisfies CharacterAnimationGenerationPayload);
@@ -1105,7 +1122,9 @@ export function CustomWorldRoleAssetStudioModal({
onClose();
}
} catch (error) {
setSaveStatus(error instanceof Error ? error.message : '保存角色形象失败。');
setSaveStatus(
error instanceof Error ? error.message : '保存角色形象失败。',
);
} finally {
setIsSavingToRole(false);
}
@@ -1188,7 +1207,9 @@ export function CustomWorldRoleAssetStudioModal({
<ActionButton
label="清空参考图"
onClick={() => setReferenceImageDataUrls([])}
disabled={isGeneratingVisuals || isApplyingVisual || syncBusy}
disabled={
isGeneratingVisuals || isApplyingVisual || syncBusy
}
/>
</div>
</div>
@@ -1230,7 +1251,8 @@ export function CustomWorldRoleAssetStudioModal({
<div className="space-y-4">
<div className="rounded-3xl border border-white/10 bg-[linear-gradient(180deg,rgba(19,24,39,0.96),rgba(8,10,17,0.92))] p-4">
<div className="flex min-h-[28rem] items-center justify-center rounded-2xl border border-white/10 bg-black/20 p-4">
{previewCharacter && hasGeneratedAnimation(workingRole, selectedAnimation) ? (
{previewCharacter &&
hasGeneratedAnimation(workingRole, selectedAnimation) ? (
<div
className="flex items-center justify-center"
style={animationPreviewViewportStyle}
@@ -1300,8 +1322,12 @@ export function CustomWorldRoleAssetStudioModal({
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
{CORE_ACTIONS.map((item) => {
const isSelected = item.animation === selectedAnimation;
const isReady = hasGeneratedAnimation(workingRole, item.animation);
const isGenerating = generatingAnimationMap[item.animation] === true;
const isReady = hasGeneratedAnimation(
workingRole,
item.animation,
);
const isGenerating =
generatingAnimationMap[item.animation] === true;
return (
<button
key={item.animation}
@@ -1327,9 +1353,15 @@ export function CustomWorldRoleAssetStudioModal({
</div>
</div>
<StatusBadge
tone={isGenerating ? 'amber' : isReady ? 'green' : 'zinc'}
tone={
isGenerating ? 'amber' : isReady ? 'green' : 'zinc'
}
>
{isGenerating ? '生成中' : isReady ? '已生成' : '待生成'}
{isGenerating
? '生成中'
: isReady
? '已生成'
: '待生成'}
</StatusBadge>
</div>
</button>