1
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user