合并最新主分支
This commit is contained in:
@@ -1638,6 +1638,8 @@ pub(crate) fn register_local_asset_entry(
|
||||
kind: kind.to_string(),
|
||||
media_type: media_type.to_string(),
|
||||
local_path: normalized_path.clone(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source,
|
||||
});
|
||||
Ok((id, "asset.register"))
|
||||
|
||||
@@ -2312,6 +2312,8 @@ mod ui_editor_font_tests {
|
||||
kind: "font".to_string(),
|
||||
media_type: "font/ttf".to_string(),
|
||||
local_path: relative_path.to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -2762,6 +2762,8 @@ fn commit_asset_canvas_at_internal(
|
||||
kind: asset_kind,
|
||||
media_type: staged.media_type.clone(),
|
||||
local_path: final_relative_path.clone(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source,
|
||||
};
|
||||
manifest.assets.push(asset.clone());
|
||||
|
||||
@@ -2958,6 +2958,8 @@ mod tests {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
@@ -4108,6 +4110,8 @@ mod tests {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -408,6 +408,8 @@ fn refine_preserves_source_and_records_non_destructive_lineage() {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
@@ -488,6 +490,8 @@ fn discovers_unique_active_refine_draft_and_rejects_ambiguous_candidates() {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
@@ -979,6 +983,8 @@ fn rejects_symlinked_or_hardlinked_media_at_source_staging_and_final_paths() {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -183,6 +183,8 @@ fn successful_first_playable_registration_creates_one_initial_version_with_asset
|
||||
kind: "character".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/player.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -633,6 +633,8 @@ mod tests {
|
||||
kind: "test".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: format!("assets/{id}.png"),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: None,
|
||||
|
||||
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>
|
||||
|
||||
@@ -89,7 +89,10 @@ import { ResourceDependencyOverlay } from './ResourceDependencyOverlay';
|
||||
import {
|
||||
defaultDerivedResourceName,
|
||||
type ProjectResourceEditCapability,
|
||||
resolveProjectCharacterAnimationCapability,
|
||||
resolveProjectResourceEditCapability,
|
||||
type ResourceGenerationKind,
|
||||
resourceGenerationOptions,
|
||||
} from './resourceEditModel';
|
||||
import {
|
||||
type ResourceEditSubmitInput,
|
||||
@@ -176,8 +179,9 @@ type ResourceEditorRoute = {
|
||||
flowId: string;
|
||||
operationId: string;
|
||||
idempotencyKey: string;
|
||||
resource: ProjectResource;
|
||||
resource: ProjectResource | null;
|
||||
capability: Extract<ProjectResourceEditCapability, { route: 'derive' }>;
|
||||
generationMode: 'create' | 'derive';
|
||||
};
|
||||
|
||||
type UiEditorRoute = {
|
||||
@@ -2324,11 +2328,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);
|
||||
@@ -2690,6 +2753,7 @@ export default function ProjectDevelopmentView({
|
||||
);
|
||||
}
|
||||
const resource = route.resource;
|
||||
const isCreate = route.generationMode === 'create';
|
||||
const result = await withPlatformSessionRefresh(() =>
|
||||
invoke<DeriveLocalProjectResourceResult>(
|
||||
'derive_local_project_resource',
|
||||
@@ -2701,16 +2765,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,
|
||||
},
|
||||
@@ -2759,7 +2834,11 @@ export default function ProjectDevelopmentView({
|
||||
setResourceEditorRoute(null);
|
||||
setFocusedResourceId(null);
|
||||
setMode('resources');
|
||||
setAssetCanvasNotice('派生资源已保存,正在同步资源与布局…');
|
||||
setAssetCanvasNotice(
|
||||
isCreate
|
||||
? '生成资源已保存,正在同步资源与布局…'
|
||||
: '派生资源已保存,正在同步资源与布局…',
|
||||
);
|
||||
resourceEditorRevisionRef.current.delete(route.operationId);
|
||||
},
|
||||
[manifest.projectId, onManifestChange, projectPath, resourceEditorRoute],
|
||||
@@ -3116,14 +3195,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()}
|
||||
@@ -3197,11 +3278,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}
|
||||
@@ -3227,6 +3327,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;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,14 @@ export type ProjectResource = {
|
||||
dependencyDepth: number;
|
||||
content?: string;
|
||||
version?: ProjectVersionResourceSummary;
|
||||
imageSequenceFrames?: Array<{
|
||||
imageSrc: string;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
}> | null;
|
||||
imageSequenceDurationMs?: number | null;
|
||||
};
|
||||
|
||||
const documentExtension =
|
||||
@@ -200,6 +208,8 @@ export function projectResourcesFromReadModels(
|
||||
referenceResourceIds: asset.source.referenceResourceIds ?? [],
|
||||
dependencies: [],
|
||||
dependencyDepth: 0,
|
||||
imageSequenceFrames: asset.imageSequenceFrames ?? null,
|
||||
imageSequenceDurationMs: asset.imageSequenceDurationMs ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,16 @@
|
||||
# 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 媒体,避免只保存首帧。
|
||||
- 本地 manifest 资产模型新增 `imageSequenceFrames` 与 `imageSequenceDurationMs`:角色动画完成时把远端 `frames` 与 `durationSeconds` 随账本持久化,恢复或重新提交后仍写回 manifest,前端资源投影同步暴露。
|
||||
- 资源页顶部新增“生成视频 / 生成音效 / 生成背景音乐”三个入口,图片聚焦态新增“生成动画”按钮;统一资源编辑面板按 `generationMode` 显示“新建媒体资源”或“编辑现有资源”。
|
||||
- 新建视频 / 音效 / 背景音乐时,先复用 `prepare_external_canvas_generation_context` 创建或匹配同名画板项目与素材库目录,并在生成请求中携带 `projectId / assetFolderId / canvasCompletion`,结果同时进入平台画布 / 素材库与本地 manifest。
|
||||
- durable 账本新增 `remoteLegacyPublicPath` 字段(旧账本默认缺省),用于角色动画预览视频的换签下载与恢复;资源提交、轮询、下载、manifest 提交和恢复队列继续复用既有资源编辑事务链。
|
||||
- 资源编辑请求指纹纳入 `generationMode`,同一 `operationId` 换生成模式重试时不再按旧账本模式恢复;旧账本继续通过 legacy 指纹兼容。
|
||||
- 去背景(`/api/editor/images/background-removals`)仍缺 External v1 契约,不在本次补齐范围;本次只补视频 / 角色动画 / 音效 / 背景音乐。
|
||||
|
||||
## 2026-08-19 UI Editor 节点右键菜单
|
||||
|
||||
## 2026-08-20 UI Editor 最终预览互斥子节点
|
||||
|
||||
@@ -466,6 +466,14 @@ export interface GameCreationAppAssetManifestEntry {
|
||||
mediaType: string;
|
||||
localPath: string;
|
||||
source: GameCreationAppAssetSource;
|
||||
imageSequenceFrames?: Array<{
|
||||
imageSrc: string;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
}> | null;
|
||||
imageSequenceDurationMs?: number | null;
|
||||
}
|
||||
|
||||
export const GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION =
|
||||
|
||||
@@ -478,6 +478,18 @@ pub struct GameCreationAppAssetSource {
|
||||
pub reference_resource_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameCreationAppImageSequenceFrame {
|
||||
pub image_src: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub object_key: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub asset_object_id: Option<String>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameCreationAppAssetManifestEntry {
|
||||
@@ -486,6 +498,10 @@ pub struct GameCreationAppAssetManifestEntry {
|
||||
pub media_type: String,
|
||||
pub local_path: String,
|
||||
pub source: GameCreationAppAssetSource,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub image_sequence_frames: Option<Vec<GameCreationAppImageSequenceFrame>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub image_sequence_duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
pub const GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION: &str = "game-creator-resource-layout.v1";
|
||||
@@ -1777,6 +1793,8 @@ mod tests {
|
||||
kind: "character".to_string(),
|
||||
media_type: "image".to_string(),
|
||||
local_path: "assets/images/player.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: Some("canvas-project-1".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user