补齐AGC资源生成能力并接入视频动画与音频生成

- 资源编辑命令新增create生成模式,支持无源视频、音效和背景音乐生成

- 图片聚焦态新增角色动画生成入口,提交角色动画接口并下载预览视频登记为本地媒体

- 资源页新增生成视频、生成音效、生成背景音乐入口

- 统一资源编辑面板按生成模式区分新建媒体资源与编辑现有资源

- 补充资源生成与角色动画定向测试,并更新AI游戏创作App实施计划文档
This commit is contained in:
Git Hooks Test
2026-08-23 21:14:35 +08:00
parent b1ee6d816d
commit e0319e80f0
8 changed files with 548 additions and 56 deletions
File diff suppressed because it is too large Load Diff
@@ -29,7 +29,7 @@ pub enum NodeSource {
pub enum StageStatus {
NoProblem,
NeedReview(String), // reason inside
Blocked(String), // reason inside
Blocked(String), // reason inside
}
#[cfg(test)]
@@ -14,6 +14,7 @@ export type ResourceEditSubmitInput = {
export function ResourceEditSurface({
resourceLabel,
editKind,
generationMode = 'derive',
initialAssetName,
semanticNotice,
onCancel,
@@ -21,6 +22,7 @@ export function ResourceEditSurface({
}: {
resourceLabel: string;
editKind: LocalProjectResourceEditKind;
generationMode?: 'create' | 'derive';
initialAssetName: string;
semanticNotice: string | null;
onCancel: () => void;
@@ -33,6 +35,7 @@ export function ResourceEditSurface({
const [error, setError] = useState('');
const inputLocked = attempted || submitting;
const promptMaxLength = resourceEditPromptMaxLength(editKind);
const isCreate = generationMode === 'create';
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
@@ -72,7 +75,7 @@ export function ResourceEditSurface({
</button>
<span>
<small></small>
<small>{isCreate ? '新建媒体资源' : '编辑现有资源'}</small>
<strong id="game-resource-editor-title">{resourceLabel}</strong>
</span>
</header>
@@ -96,7 +99,11 @@ export function ResourceEditSurface({
rows={7}
autoFocus
disabled={inputLocked}
placeholder="描述希望在现有资源基础上发生的变化"
placeholder={
isCreate
? '描述希望生成的视频、音效或背景音乐'
: '描述希望在现有资源基础上发生的变化'
}
onChange={(event) => setPrompt(event.currentTarget.value)}
/>
</label>
@@ -132,7 +139,9 @@ export function ResourceEditSurface({
? '正在派生…'
: editKind === 'version'
? '创建子版本'
: '生成派生资源'}
: isCreate
? '生成资源'
: '生成派生资源'}
</button>
)}
</div>
@@ -88,7 +88,10 @@ import { ResourceDependencyOverlay } from './ResourceDependencyOverlay';
import {
defaultDerivedResourceName,
type ProjectResourceEditCapability,
resolveProjectCharacterAnimationCapability,
resolveProjectResourceEditCapability,
type ResourceGenerationKind,
resourceGenerationOptions,
} from './resourceEditModel';
import {
type ResourceEditSubmitInput,
@@ -175,8 +178,9 @@ type ResourceEditorRoute = {
flowId: string;
operationId: string;
idempotencyKey: string;
resource: ProjectResource;
resource: ProjectResource | null;
capability: Extract<ProjectResourceEditCapability, { route: 'derive' }>;
generationMode: 'create' | 'derive';
};
type UiEditorRoute = {
@@ -2321,11 +2325,70 @@ export default function ProjectDevelopmentView({
idempotencyKey: crypto.randomUUID(),
resource,
capability,
generationMode: 'derive',
});
},
[advanceFocusGeneration, normalizeRasterSourceAndOpen, openAssetCanvas],
);
const openCharacterAnimation = useCallback(
(resource: ProjectResource) => {
const capability = resolveProjectCharacterAnimationCapability(resource);
if (!capability) {
return;
}
canvasOpenEpochRef.current += 1;
advanceFocusGeneration();
const flowId = crypto.randomUUID();
activeFocusFlowIdRef.current = flowId;
pendingResourceFocusRef.current = null;
setAssetCanvasRoute(null);
setAssetCanvasNotice('');
setResourceEditorRoute({
flowId,
operationId: crypto.randomUUID(),
idempotencyKey: crypto.randomUUID(),
resource,
capability,
generationMode: 'derive',
});
},
[advanceFocusGeneration],
);
const openCreateResource = useCallback(
(kind: ResourceGenerationKind) => {
canvasOpenEpochRef.current += 1;
advanceFocusGeneration();
const flowId = crypto.randomUUID();
activeFocusFlowIdRef.current = flowId;
pendingResourceFocusRef.current = null;
setAssetCanvasRoute(null);
setAssetCanvasNotice('');
const sourceMediaType = kind === 'video' ? 'video/mp4' : 'audio/mpeg';
setResourceEditorRoute({
flowId,
operationId: crypto.randomUUID(),
idempotencyKey: crypto.randomUUID(),
resource: null,
capability: {
route: 'derive',
editKind: kind,
sourceMediaType,
semanticNotice:
kind === 'background-music'
? '将生成全新的背景音乐。'
: kind === 'sound-effect'
? '将生成全新的音效。'
: '将生成全新的视频。',
generationMode: 'create',
},
generationMode: 'create',
});
},
[advanceFocusGeneration],
);
const cancelResourceEditor = useCallback(() => {
if (resourceEditorRoute) {
resourceEditorRevisionRef.current.delete(resourceEditorRoute.operationId);
@@ -2687,6 +2750,7 @@ export default function ProjectDevelopmentView({
);
}
const resource = route.resource;
const isCreate = route.generationMode === 'create';
const result = await withPlatformSessionRefresh(() =>
invoke<DeriveLocalProjectResourceResult>(
'derive_local_project_resource',
@@ -2698,16 +2762,27 @@ export default function ProjectDevelopmentView({
operationId: route.operationId,
idempotencyKey: route.idempotencyKey,
editKind: route.capability.editKind,
sourceResourceId: resource.id,
sourceAssetId: resource.manifestAssetId,
generationMode: route.generationMode,
sourceResourceId: isCreate
? `create:${route.operationId}`
: (resource?.id ?? ''),
sourceAssetId: isCreate
? null
: (resource?.manifestAssetId ?? null),
sourcePath:
resource.version || resource.subtype === 'agent-result'
isCreate ||
resource?.version ||
resource?.subtype === 'agent-result'
? null
: resource.path,
: (resource?.path ?? null),
sourceMediaType: route.capability.sourceMediaType,
sourceSubtype: resource.subtype,
producerTaskId: resource.producerTaskId,
sourceVersionId: resource.version?.versionId ?? null,
sourceSubtype: isCreate ? null : (resource?.subtype ?? null),
producerTaskId: isCreate
? null
: (resource?.producerTaskId ?? null),
sourceVersionId: isCreate
? null
: (resource?.version?.versionId ?? null),
prompt,
assetName,
},
@@ -2756,7 +2831,11 @@ export default function ProjectDevelopmentView({
setResourceEditorRoute(null);
setFocusedResourceId(null);
setMode('resources');
setAssetCanvasNotice('派生资源已保存,正在同步资源与布局…');
setAssetCanvasNotice(
isCreate
? '生成资源已保存,正在同步资源与布局…'
: '派生资源已保存,正在同步资源与布局…',
);
resourceEditorRevisionRef.current.delete(route.operationId);
},
[manifest.projectId, onManifestChange, projectPath, resourceEditorRoute],
@@ -3112,14 +3191,16 @@ export default function ProjectDevelopmentView({
: `管理未完成编辑 (${pendingResourceEdits.length})`}
</button>
) : null}
<button
type="button"
disabled
title="新增资源暂未开放,请先选择现有图片进行编辑"
>
<Sparkles size={15} aria-hidden="true" />
</button>
{resourceGenerationOptions.map((option) => (
<button
key={option.kind}
type="button"
onClick={() => openCreateResource(option.kind)}
>
<Sparkles size={15} aria-hidden="true" />
{option.label}
</button>
))}
<button
type="button"
onClick={() => void createUiDesignResource()}
@@ -3185,11 +3266,30 @@ export default function ProjectDevelopmentView({
/>
) : mode === 'resources' && resourceEditorRoute ? (
<ResourceEditSurface
resourceLabel={resourceEditorRoute.resource.label}
resourceLabel={
resourceEditorRoute.resource
? resourceEditorRoute.resource.label
: ((
{
video: '新视频',
'sound-effect': '新音效',
'background-music': '新背景音乐',
} as Record<string, string>
)[resourceEditorRoute.capability.editKind] ?? '新资源')
}
editKind={resourceEditorRoute.capability.editKind}
initialAssetName={defaultDerivedResourceName(
resourceEditorRoute.resource,
)}
generationMode={resourceEditorRoute.generationMode}
initialAssetName={
resourceEditorRoute.resource
? defaultDerivedResourceName(resourceEditorRoute.resource)
: ((
{
video: '新视频',
'sound-effect': '新音效',
'background-music': '新背景音乐',
} as Record<string, string>
)[resourceEditorRoute.capability.editKind] ?? '新资源')
}
semanticNotice={resourceEditorRoute.capability.semanticNotice}
onCancel={cancelResourceEditor}
onSubmit={submitResourceEdit}
@@ -3215,6 +3315,16 @@ export default function ProjectDevelopmentView({
</span>
</span>
<span className="game-resource-focus-actions">
{resolveProjectCharacterAnimationCapability(
focusedResource,
) ? (
<button
type="button"
onClick={() => openCharacterAnimation(focusedResource)}
>
</button>
) : null}
<button
type="button"
onClick={() => openResourceEditor(focusedResource)}
@@ -3,6 +3,7 @@ import type { ProjectResource } from './resourceProjectionModel';
export type LocalProjectResourceEditKind =
| 'image-reference'
| 'svg'
| 'character-animation'
| 'video'
| 'sound-effect'
| 'background-music'
@@ -28,8 +29,23 @@ export type ProjectResourceEditCapability =
editKind: LocalProjectResourceEditKind;
sourceMediaType: string;
semanticNotice: string | null;
generationMode?: 'create' | 'derive';
};
export type ResourceGenerationKind = Exclude<
LocalProjectResourceEditKind,
'image-reference' | 'svg' | 'text' | 'agent-result' | 'version'
>;
export const resourceGenerationOptions: Array<{
kind: ResourceGenerationKind;
label: string;
}> = [
{ kind: 'video', label: '生成视频' },
{ kind: 'sound-effect', label: '生成音效' },
{ kind: 'background-music', label: '生成背景音乐' },
];
const extensionMediaTypes: Record<string, string> = {
png: 'image/png',
jpg: 'image/jpeg',
@@ -195,6 +211,20 @@ export function resolveProjectResourceEditCapability(
};
}
export function resolveProjectCharacterAnimationCapability(
resource: ProjectResource,
): Extract<ProjectResourceEditCapability, { route: 'derive' }> | null {
const mediaType = canonicalProjectedResourceMediaType(resource);
if (!mediaType.startsWith('image/')) return null;
return {
route: 'derive',
editKind: 'character-animation',
sourceMediaType: mediaType,
semanticNotice: '将基于当前图片生成角色动作序列帧,源图片保持不变。',
generationMode: 'derive',
};
}
export function defaultDerivedResourceName(resource: ProjectResource) {
const baseName = resource.label.replace(/\.[^.]+$/u, '').trim();
return `${baseName || '资源'}-编辑版`;
@@ -210,6 +240,8 @@ export function resourceEditPromptMaxLength(
return 1_900;
case 'video':
return 4_000;
case 'character-animation':
return 4_000;
default:
return 32_000;
}
@@ -695,10 +695,12 @@ describe('project resource live canvas integration', () => {
expect(
(
screen.getByRole('button', {
name: '新增资源',
name: '生成视频',
}) as HTMLButtonElement
).disabled,
).toBe(true);
).toBe(false);
expect(screen.getByRole('button', { name: '生成音效' })).not.toBeNull();
expect(screen.getByRole('button', { name: '生成背景音乐' })).not.toBeNull();
fireEvent.click(sourceCard);
fireEvent.click(screen.getByRole('button', { name: '编辑资源' }));
expect(
@@ -2,8 +2,10 @@ import { describe, expect, it } from 'vitest';
import {
canonicalProjectedResourceMediaType,
resolveProjectCharacterAnimationCapability,
resolveProjectResourceEditCapability,
resourceEditPromptMaxLength,
resourceGenerationOptions,
} from '../src/view/project-development/resourceEditModel';
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
@@ -211,6 +213,40 @@ describe('全类型资源编辑能力模型', () => {
expect(resourceEditPromptMaxLength('background-music')).toBe(140);
expect(resourceEditPromptMaxLength('sound-effect')).toBe(1_900);
expect(resourceEditPromptMaxLength('video')).toBe(4_000);
expect(resourceEditPromptMaxLength('character-animation')).toBe(4_000);
expect(resourceEditPromptMaxLength('text')).toBe(32_000);
});
it('图片资源可进入角色动画派生入口', () => {
const capability = resolveProjectCharacterAnimationCapability(
resource({
id: 'asset:hero',
category: 'art',
path: 'assets/hero.png',
mediaType: 'image/png',
manifestAssetId: 'hero',
}),
);
expect(capability?.editKind).toBe('character-animation');
expect(capability?.generationMode).toBe('derive');
expect(
resolveProjectCharacterAnimationCapability(
resource({
id: 'asset:bgm',
category: 'audio',
path: 'audio/theme.mp3',
mediaType: 'audio/mpeg',
manifestAssetId: 'bgm',
}),
),
).toBeNull();
});
it('资源页提供新建视频、音效和背景音乐入口', () => {
expect(resourceGenerationOptions).toEqual([
{ kind: 'video', label: '生成视频' },
{ kind: 'sound-effect', label: '生成音效' },
{ kind: 'background-music', label: '生成背景音乐' },
]);
});
});
@@ -1,5 +1,13 @@
# AI 游戏创作智能体 App 实施计划
## 2026-08-23 AGC 资源生成补齐(视频 / 动画 / 音效 / 背景音乐)
- 统一资源编辑命令新增 `generationMode=create|derive``create` 用于无源生成视频、音效和背景音乐,`derive` 保持现有“基于已有资源派生”。视频新建请求不再携带 `referenceVideoSrcs`;音效 / 背景音乐新建使用同款文本生成端点,但提示词前缀改为“生成新音频”。
- 图片资源新增“生成动画”派生入口:复用 `/api/external/v1/editor/character-animations/generations`(账号模式自动映射 `/api/editor/character-animations/generations`),以源图片的稳定 objectKey、真实宽高和固定 `seedance2.0-fast / 720p / same / 32帧 / 4秒` 参数提交;完成后下载 `previewVideoPath` 预览视频并以 `character-animation` 资源类型登记为本地 MP4/WebM 媒体,避免只保存首帧。
- 资源页顶部新增“生成视频 / 生成音效 / 生成背景音乐”三个入口,图片聚焦态新增“生成动画”按钮;统一资源编辑面板按 `generationMode` 显示“新建媒体资源”或“编辑现有资源”。
- durable 账本新增 `remoteLegacyPublicPath` 字段(旧账本默认缺省),用于角色动画预览视频的换签下载与恢复;资源提交、轮询、下载、manifest 提交和恢复队列继续复用既有资源编辑事务链。
- 去背景(`/api/editor/images/background-removals`)仍缺 External v1 契约,不在本次补齐范围;本次只补视频 / 角色动画 / 音效 / 背景音乐。
## 2026-08-19 UI Editor 节点右键菜单
## 2026-08-20 UI Editor 最终预览互斥子节点