合并最新 master 到 feat/five_min_design,并修复 master 的 Windows 构建中断
master 侧 5 个提交(8e78be766..9f5c84ee7)。仅一处冲突: src/view/home/index.tsx 的 react import——我们这侧对该文件只有 prettier 格式化 改动,master 新增的 useRef 在合并后正文里用了 2 次,取 master 那行。 修复 master 带来的 Windows 构建中断(E0658): project/manifest.rs 的 #[cfg(windows)] 分支用了 std 未稳定 API `MetadataExt::number_of_links`(rust-lang#63010),而 rust-toolchain.toml 锁在 stable 1.96.0。该文件与 origin/master 逐字节相同,即 master 自身在 Windows 上就 构建不过——Linux CI 上 #[cfg(windows)] 整块不参与编译,所以 CI 全绿。 改为本仓库既有写法:自声明 ByHandleFileInformation 调 GetFileInformationByHandle (另见 runner/endpoint.rs、tool_plan_handoff/storage_windows.rs 等六处)。保留原 错误文案与 fail-closed 语义(取不到句柄信息与确实是硬链接同等拒绝),并按 endpoint.rs 先例一并拒绝 directory / reparse point。 该修复目前只在本分支,须回流 master,否则下次合并会再撞一次。踩坑记录见 pitfalls.md 2026-08-15 条。 验证: - cargo check --offline --all-targets 通过(修复前 E0658,修复后 Finished) - cargo fmt --check 通过 - 定向 Rust 测试 project::manifest / godot / static_delegate / collaboration::static_deliveries 65 passed / 0 failed - agc:typecheck 通过;check:encoding 通过(5376 files);git diff --check 干净 - 前端 vitest apps/ai-game-creator-shell/tests:738 passed / 1 failed,唯一失败是 已知的 Windows symlink EPERM(agentSwarmTestEntry),非本次回归 两条既有环境失败,已核实与本次合并无关: - command_exec::tests 两条报「找不到受信任的 rg 可执行文件」,该文件相对合并基线 逐字节相同 - npm run check:native-shells 在合并前的 master worktree 上失败得一模一样 (spawnSync npm.cmd EINVAL,脚本 spawn npm.cmd 未带 shell: true) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -371,6 +371,11 @@ describe('ImageCanvasContextMenusView', () => {
|
||||
mediaType: 'audio' as const,
|
||||
assetKind: 'background-music' as const,
|
||||
},
|
||||
{
|
||||
title: '视频',
|
||||
mediaType: 'video' as const,
|
||||
assetKind: 'video' as const,
|
||||
},
|
||||
])('hides quick edit from both menus for $title', (overrides) => {
|
||||
const layer = createLayer(overrides);
|
||||
renderContextMenus({
|
||||
|
||||
@@ -1795,7 +1795,6 @@ export function ImageCanvasGenerationComposerView({
|
||||
{quickEditPanel && quickEditSourceLayer && quickEditPanelStyle ? (
|
||||
<ImageCanvasQuickEditPanelView
|
||||
panel={quickEditPanel}
|
||||
sourceLayer={quickEditSourceLayer}
|
||||
style={quickEditPanelStyle}
|
||||
referenceButtonRef={generationReferenceButtonRef}
|
||||
isReferenceMenuOpen={isGenerationReferenceMenuOpen}
|
||||
|
||||
@@ -964,9 +964,16 @@ describe('ImageCanvasGenerationModel', () => {
|
||||
isQuickEditSupportedLayer({
|
||||
...buildSourceLayer(),
|
||||
mediaType: 'video',
|
||||
assetKind: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isQuickEditSupportedLayer({
|
||||
...buildSourceLayer(),
|
||||
mediaType: 'image',
|
||||
assetKind: 'video',
|
||||
}),
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('adds reference image semantics and snapshots for spec generation references', () => {
|
||||
|
||||
@@ -712,7 +712,6 @@ export function buildQuickEditModelOptions(currentModel: string) {
|
||||
const QUICK_EDIT_SUPPORTED_MEDIA_TYPES = new Set<CanvasLayer['mediaType']>([
|
||||
undefined,
|
||||
'image',
|
||||
'video',
|
||||
]);
|
||||
|
||||
const QUICK_EDIT_SUPPORTED_ASSET_KINDS = new Set<CanvasLayer['assetKind']>([
|
||||
@@ -725,7 +724,6 @@ const QUICK_EDIT_SUPPORTED_ASSET_KINDS = new Set<CanvasLayer['assetKind']>([
|
||||
'publication-material',
|
||||
'ui-design',
|
||||
'scene',
|
||||
'video',
|
||||
]);
|
||||
|
||||
export function isQuickEditSupportedLayer(layer: CanvasLayer) {
|
||||
|
||||
@@ -424,6 +424,30 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects persisted video quick-edit dialogs during submission planning', () => {
|
||||
const sourceLayer = createLayer({
|
||||
title: '历史视频',
|
||||
src: 'https://assets.example.test/history.mp4',
|
||||
mediaType: 'video',
|
||||
assetKind: 'video',
|
||||
originalWidth: 854,
|
||||
originalHeight: 480,
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
mode: 'quick-edit',
|
||||
prompt: '把当前画面改成雨天',
|
||||
status: 'idle',
|
||||
sourceLayerId: sourceLayer.id,
|
||||
},
|
||||
layers: [sourceLayer],
|
||||
nextGeneratedIndex: 4,
|
||||
}),
|
||||
).toThrow('当前素材类型不支持快速编辑');
|
||||
});
|
||||
|
||||
it('does not send image style from an incomplete quick-edit snapshot', () => {
|
||||
const plan = buildImageGenerationSubmissionPlan({
|
||||
dialog: {
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
inferEditorImageAspectRatio,
|
||||
inferEditorImageSizeLabel,
|
||||
isQuickEditSupportedLayer,
|
||||
normalizeEditorImageModel,
|
||||
resolveCharacterAnimationSourceImageSrc,
|
||||
resolveEditorImageGenerationPixelSize,
|
||||
@@ -577,6 +578,9 @@ export function buildImageGenerationSubmissionPlan({
|
||||
if (!sourceLayer) {
|
||||
throw new Error('未找到要改造的原图');
|
||||
}
|
||||
if (!isQuickEditSupportedLayer(sourceLayer)) {
|
||||
throw new Error('当前素材类型不支持快速编辑');
|
||||
}
|
||||
const basePrompt = dialog.prompt.trim() || '快速编辑图片';
|
||||
const normalizedQuickEditPrompt = basePrompt;
|
||||
const requestedImageModel = normalizeEditorImageModel(
|
||||
|
||||
@@ -6,30 +6,12 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
|
||||
import type {
|
||||
CanvasLayer,
|
||||
CharacterReferenceImage,
|
||||
QuickEditPanelState,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasQuickEditPanelView } from './ImageCanvasQuickEditPanelView';
|
||||
|
||||
function createLayer(): CanvasLayer {
|
||||
return {
|
||||
id: 'layer-a',
|
||||
resourceId: 'resource-a',
|
||||
title: '源图',
|
||||
src: 'data:image/png;base64,c291cmNl',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 320,
|
||||
height: 180,
|
||||
originalWidth: 320,
|
||||
originalHeight: 180,
|
||||
zIndex: 1,
|
||||
sourceType: 'uploaded',
|
||||
};
|
||||
}
|
||||
|
||||
function QuickEditPanelHarness({
|
||||
initialPanel,
|
||||
onSubmit = vi.fn(),
|
||||
@@ -50,7 +32,6 @@ function QuickEditPanelHarness({
|
||||
<div>
|
||||
<ImageCanvasQuickEditPanelView
|
||||
panel={panel}
|
||||
sourceLayer={createLayer()}
|
||||
style={{ left: 10, top: 20 }}
|
||||
referenceButtonRef={referenceButtonRef}
|
||||
isReferenceMenuOpen={isReferenceMenuOpen}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
|
||||
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
|
||||
import type {
|
||||
CanvasLayer,
|
||||
GenerateDialogState,
|
||||
QuickEditPanelState,
|
||||
UploadTarget,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
|
||||
export type ImageCanvasQuickEditPanelViewProps = {
|
||||
panel: QuickEditPanelState;
|
||||
sourceLayer: CanvasLayer;
|
||||
style: CSSProperties;
|
||||
referenceButtonRef?: RefObject<HTMLButtonElement | null>;
|
||||
isReferenceMenuOpen?: boolean;
|
||||
@@ -92,7 +90,6 @@ function syncPanelFromDialogUpdate(
|
||||
|
||||
export function ImageCanvasQuickEditPanelView({
|
||||
panel,
|
||||
sourceLayer,
|
||||
style,
|
||||
referenceButtonRef,
|
||||
isReferenceMenuOpen = false,
|
||||
@@ -106,7 +103,6 @@ export function ImageCanvasQuickEditPanelView({
|
||||
hasPendingImageReferenceUploads = false,
|
||||
onSubmit,
|
||||
}: ImageCanvasQuickEditPanelViewProps) {
|
||||
const isImageQuickEdit = sourceLayer.mediaType !== 'video';
|
||||
const referenceCount = panel.quickEditReferences?.length ?? 0;
|
||||
const canAddReferences =
|
||||
referenceCount <
|
||||
@@ -148,7 +144,7 @@ export function ImageCanvasQuickEditPanelView({
|
||||
hasPendingImageReferenceUploads={hasPendingImageReferenceUploads}
|
||||
onSubmit={onSubmit}
|
||||
dialogLabel="快速编辑图片"
|
||||
includeDimensions={isImageQuickEdit}
|
||||
includeDimensions={true}
|
||||
includeModel={true}
|
||||
includeReferences={false}
|
||||
promptLabel="快速编辑提示词"
|
||||
|
||||
@@ -444,7 +444,7 @@ describe('ImageCanvasSelectedLayerToolbarView', () => {
|
||||
within(videoToolbar)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['快速编辑', '改造', '下载按钮']);
|
||||
).toEqual(['改造', '下载按钮']);
|
||||
|
||||
cleanup();
|
||||
renderSelectedToolbar({
|
||||
|
||||
@@ -337,14 +337,12 @@ function SubmissionWorkflowHarness({
|
||||
quickEditPanel,
|
||||
quickEditSourceLayer,
|
||||
quickEditSelectionState: initialQuickEditSelectionState,
|
||||
canvasGenerationDialogs: dialogs.canvasGenerationDialogs,
|
||||
setQuickEditPanel,
|
||||
characterAnimationPanel,
|
||||
characterAnimationDialog: activeCanvasDialog,
|
||||
characterAnimationSourceLayer,
|
||||
setCharacterAnimationPanel,
|
||||
setGenerateDialog: dialogs.setGenerateDialog,
|
||||
openCanvasGenerationDialog: dialogs.openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById: dialogs.hasCanvasGenerationDialogById,
|
||||
getCanvasGenerationDialogById: dialogs.getCanvasGenerationDialogById,
|
||||
@@ -1496,47 +1494,15 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(refreshWalletBalance).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('submits video quick edits through the video API with the source video', async () => {
|
||||
const applyProjectSnapshot = vi.fn();
|
||||
const upsertGeneratedAsset = vi.fn();
|
||||
generateEditorVideoMock.mockResolvedValueOnce(
|
||||
createVideoGenerated({
|
||||
prompt: '把当前图改成雨天',
|
||||
project: {
|
||||
projectId: 'editor-project-1',
|
||||
title: '项目',
|
||||
coverImageSrc: null,
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
layers: [],
|
||||
resources: [],
|
||||
createdAt: '2026-06-27T00:00:00.000Z',
|
||||
updatedAt: '2026-06-27T00:00:00.000Z',
|
||||
},
|
||||
asset: {
|
||||
assetId: 'asset-video-quick-edit',
|
||||
folderId: 'user-1:asset-folder:project',
|
||||
label: '源视频 快速编辑',
|
||||
imageSrc: 'https://assets.example.test/generated-video.mp4',
|
||||
thumbnailSrc: 'https://assets.example.test/generated-video-cover.png',
|
||||
width: 854,
|
||||
height: 480,
|
||||
sourceType: 'generated',
|
||||
assetKind: 'video',
|
||||
},
|
||||
}),
|
||||
);
|
||||
it('rejects direct video quick-edit submission without calling generation APIs', () => {
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
projectId="editor-project-1"
|
||||
applyProjectSnapshot={applyProjectSnapshot}
|
||||
upsertGeneratedAsset={upsertGeneratedAsset}
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
title: '源视频',
|
||||
src: 'data:video/mp4;base64,c291cmNlLXZpZGVv',
|
||||
src: 'https://assets.example.test/source-video.mp4',
|
||||
mediaType: 'video',
|
||||
assetKind: 'video',
|
||||
model: 'gpt-image-2',
|
||||
originalWidth: 854,
|
||||
originalHeight: 480,
|
||||
width: 427,
|
||||
@@ -1549,66 +1515,58 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
size: '854x480',
|
||||
model: 'gpt-image-2',
|
||||
status: 'idle',
|
||||
quickEditReferences: [
|
||||
{
|
||||
id: 'ref-style',
|
||||
label: '风格参考',
|
||||
src: 'https://assets.example.test/style.png',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorVideoMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '把当前图改成雨天',
|
||||
model: 'seedance2.0-fast',
|
||||
aspectRatio: '16:9',
|
||||
durationSeconds: 4,
|
||||
resolution: '480p',
|
||||
mode: 'std',
|
||||
sound: 'off',
|
||||
referenceVideoSrcs: [
|
||||
'generated-character-drafts/editor/generation-references/reference.png',
|
||||
],
|
||||
generationInputs: expect.objectContaining({
|
||||
version: 2,
|
||||
action: 'video.generate',
|
||||
references: [
|
||||
{
|
||||
id: 'videoReference',
|
||||
title: '参考视频 1',
|
||||
label: '源视频',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-source',
|
||||
},
|
||||
],
|
||||
}),
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '源视频 快速编辑',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(generateEditorVideoMock.mock.calls[0]?.[0]).not.toHaveProperty(
|
||||
'referenceImageSrcs',
|
||||
);
|
||||
expect(editEditorImageMock).not.toHaveBeenCalled();
|
||||
expect(generateEditorImageMock).not.toHaveBeenCalled();
|
||||
expect(generateEditorCharacterAnimationMock).not.toHaveBeenCalled();
|
||||
expect(generateEditorVideoMock).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('quick-edit').textContent).toContain(
|
||||
'failed:把当前图改成雨天:当前素材类型不支持快速编辑',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a hydrated video quick-edit dialog before calling generation APIs', async () => {
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
title: '历史视频',
|
||||
src: 'https://assets.example.test/history.mp4',
|
||||
mediaType: 'video',
|
||||
assetKind: 'video',
|
||||
originalWidth: 854,
|
||||
originalHeight: 480,
|
||||
width: 427,
|
||||
height: 240,
|
||||
}),
|
||||
]}
|
||||
initialDialog={{
|
||||
id: 'generation-dialog-hydrated-video-quick-edit',
|
||||
mode: 'quick-edit',
|
||||
prompt: '把当前画面改成雨天',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
sourceLayerId: 'layer-source',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(applyProjectSnapshot).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ projectId: 'editor-project-1' }),
|
||||
);
|
||||
expect(upsertGeneratedAsset).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
assetId: 'asset-video-quick-edit',
|
||||
thumbnailSrc: 'https://assets.example.test/generated-video-cover.png',
|
||||
}),
|
||||
expect(screen.getByTestId('tracked-dialog').textContent).toContain(
|
||||
'quick-edit:把当前画面改成雨天:failed:open:当前素材类型不支持快速编辑',
|
||||
);
|
||||
});
|
||||
expect(editEditorImageMock).not.toHaveBeenCalled();
|
||||
expect(generateEditorImageMock).not.toHaveBeenCalled();
|
||||
expect(generateEditorVideoMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uploads inline video references and keeps image objectKey and asset URLs', async () => {
|
||||
|
||||
@@ -50,7 +50,6 @@ import type {
|
||||
QuickEditPanelState,
|
||||
SidebarPanel,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { createQuickEditGenerationDialogDraft } from './ImageCanvasGenerationDialogModel';
|
||||
import {
|
||||
applyImageEditResultToSourceLayer,
|
||||
createAudioResultLayer,
|
||||
@@ -63,12 +62,9 @@ import {
|
||||
buildCharacterAnimationGenerationInputs,
|
||||
buildQuickEditGenerationInputs,
|
||||
buildUiDesignAssetExtractionGenerationInputs,
|
||||
buildVideoGenerationInputs,
|
||||
createCanvasLayerReference,
|
||||
DEFAULT_EDITOR_BGFILTER_SEG_MODEL,
|
||||
DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
|
||||
DEFAULT_IMAGE_MODEL,
|
||||
DEFAULT_VIDEO_MODEL,
|
||||
inferEditorImageAspectRatio,
|
||||
inferEditorImageSizeLabel,
|
||||
isCanvasGenerationDialog,
|
||||
@@ -77,7 +73,6 @@ import {
|
||||
resolveEditorImageGenerationPixelSize,
|
||||
resolveImageGenerationErrorMessage,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { chooseGenerationPlacement } from './ImageCanvasGenerationPlacementModel';
|
||||
import {
|
||||
buildCharacterAnimationSubmissionPlan,
|
||||
buildIconSpritesheetGenerationSubmissionPlan,
|
||||
@@ -338,7 +333,6 @@ type GenerationSubmissionWorkflowOptions = {
|
||||
quickEditPanel: QuickEditPanelState | null;
|
||||
quickEditSourceLayer: CanvasLayer | null;
|
||||
quickEditSelectionState: UiAssetExtractionState | null;
|
||||
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
||||
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
|
||||
characterAnimationPanel: CharacterAnimationPanelState | null;
|
||||
characterAnimationDialog: CanvasGenerationDialogState | null;
|
||||
@@ -347,9 +341,6 @@ type GenerationSubmissionWorkflowOptions = {
|
||||
SetStateAction<CharacterAnimationPanelState | null>
|
||||
>;
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
openCanvasGenerationDialog: (
|
||||
dialog: Omit<CanvasGenerationDialogState, 'id'>,
|
||||
) => string;
|
||||
updateCanvasGenerationDialogById: (
|
||||
dialogId: string,
|
||||
updater: CanvasGenerationDialogUpdater,
|
||||
@@ -650,10 +641,6 @@ function buildRightSideCanvasCompletionPlaceholder(
|
||||
};
|
||||
}
|
||||
|
||||
function isVideoQuickEditSource(layer: CanvasLayer) {
|
||||
return layer.mediaType === 'video' || layer.assetKind === 'video';
|
||||
}
|
||||
|
||||
export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
layers,
|
||||
canvasSize,
|
||||
@@ -662,14 +649,12 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
quickEditPanel,
|
||||
quickEditSourceLayer,
|
||||
quickEditSelectionState,
|
||||
canvasGenerationDialogs,
|
||||
setQuickEditPanel,
|
||||
characterAnimationPanel,
|
||||
characterAnimationDialog,
|
||||
characterAnimationSourceLayer,
|
||||
setCharacterAnimationPanel,
|
||||
setGenerateDialog,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
getCanvasGenerationDialogById,
|
||||
@@ -1451,63 +1436,22 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
const normalizedQuickEditModel = normalizeEditorImageModel(
|
||||
quickEditPanel.model,
|
||||
);
|
||||
const quickEditOutputSize = isVideoQuickEditSource(quickEditSourceLayer)
|
||||
? {
|
||||
width: quickEditSourceLayer.originalWidth,
|
||||
height: quickEditSourceLayer.originalHeight,
|
||||
}
|
||||
: resolveEditorImageGenerationPixelSize({
|
||||
model: normalizedQuickEditModel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
});
|
||||
const quickEditOutputSize = resolveEditorImageGenerationPixelSize({
|
||||
model: normalizedQuickEditModel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
});
|
||||
const quickEditSize = `${quickEditOutputSize.width}x${quickEditOutputSize.height}`;
|
||||
const normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片';
|
||||
let quickEditDialogId: string | undefined;
|
||||
const shouldUseCanvasPlaceholder =
|
||||
isVideoQuickEditSource(quickEditSourceLayer);
|
||||
if (shouldUseCanvasPlaceholder) {
|
||||
const quickEditDraft = createQuickEditGenerationDialogDraft({
|
||||
sourceLayer: quickEditSourceLayer,
|
||||
prompt: normalizedPrompt,
|
||||
status: 'generating',
|
||||
references: quickEditReferences,
|
||||
model: quickEditPanel.model,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
frame: {
|
||||
width: quickEditOutputSize.width,
|
||||
height: quickEditOutputSize.height,
|
||||
},
|
||||
assetLabel: quickEditResultTitle,
|
||||
});
|
||||
const quickEditPlacement = quickEditDraft.placeholder
|
||||
? chooseGenerationPlacement({
|
||||
canvasSize,
|
||||
viewport,
|
||||
frame: quickEditDraft.placeholder,
|
||||
layers,
|
||||
generationDialogs: canvasGenerationDialogs,
|
||||
})
|
||||
: quickEditDraft.placeholder;
|
||||
quickEditDialogId = openCanvasGenerationDialog({
|
||||
...quickEditDraft,
|
||||
// 中文注释:视频快速编辑仍落到独立视频结果占位,图片快速编辑直接替换源图。
|
||||
placeholder: quickEditPlacement,
|
||||
});
|
||||
setQuickEditPanel(null);
|
||||
selectSingleLayer(null);
|
||||
} else {
|
||||
setQuickEditPanel({
|
||||
...quickEditPanel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
size: quickEditSize,
|
||||
prompt: normalizedPrompt,
|
||||
status: 'generating',
|
||||
errorMessage: undefined,
|
||||
});
|
||||
}
|
||||
setQuickEditPanel({
|
||||
...quickEditPanel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
size: quickEditSize,
|
||||
prompt: normalizedPrompt,
|
||||
status: 'generating',
|
||||
errorMessage: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const extraReferenceImageSrcs = await Promise.all(
|
||||
@@ -1515,105 +1459,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
resolveEditorGenerationMediaReference(reference, 'image', projectId),
|
||||
),
|
||||
);
|
||||
const quickEditFrame = quickEditDialogId
|
||||
? getGeneratingDialogPlaceholder({
|
||||
id: quickEditDialogId,
|
||||
mode: 'quick-edit',
|
||||
prompt: normalizedPrompt,
|
||||
status: 'generating',
|
||||
})
|
||||
: undefined;
|
||||
const quickEditCanvasCompletionPlaceholder =
|
||||
quickEditFrame ??
|
||||
buildRightSideCanvasCompletionPlaceholder(quickEditSourceLayer, {
|
||||
width: quickEditOutputSize.width,
|
||||
height: quickEditOutputSize.height,
|
||||
originalWidth: quickEditOutputSize.width,
|
||||
originalHeight: quickEditOutputSize.height,
|
||||
});
|
||||
if (isVideoQuickEditSource(quickEditSourceLayer)) {
|
||||
const videoModel = DEFAULT_VIDEO_MODEL;
|
||||
const videoResolution = '480p' as const;
|
||||
const videoDurationSeconds = 4 as const;
|
||||
const sourceVideoSrc = await resolveEditorGenerationMediaReference(
|
||||
quickEditSourceLayer,
|
||||
'video',
|
||||
projectId,
|
||||
{ allowRegisteredIds: false },
|
||||
);
|
||||
const generationInputs = buildVideoGenerationInputs(
|
||||
normalizedPrompt,
|
||||
[createCanvasLayerReference(quickEditSourceLayer)],
|
||||
{
|
||||
model: videoModel,
|
||||
aspectRatio: '16:9',
|
||||
resolution: videoResolution,
|
||||
durationSeconds: videoDurationSeconds,
|
||||
sound: 'off',
|
||||
},
|
||||
);
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
generateEditorVideo({
|
||||
prompt: normalizedPrompt,
|
||||
model: videoModel,
|
||||
aspectRatio: '16:9',
|
||||
durationSeconds: videoDurationSeconds,
|
||||
resolution: videoResolution,
|
||||
mode: 'std',
|
||||
sound: 'off',
|
||||
...(extraReferenceImageSrcs.length
|
||||
? { referenceImageSrcs: extraReferenceImageSrcs }
|
||||
: {}),
|
||||
referenceVideoSrcs: [sourceVideoSrc],
|
||||
projectId,
|
||||
generationInputs,
|
||||
sourceResourceId: quickEditSourceLayer.resourceId,
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
assetFolderId,
|
||||
assetLabel: quickEditResultTitle,
|
||||
...(projectId && quickEditCanvasCompletionPlaceholder
|
||||
? {
|
||||
canvasCompletion: {
|
||||
dialogId: quickEditDialogId,
|
||||
title: quickEditResultTitle,
|
||||
placeholder: quickEditCanvasCompletionPlaceholder,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
if (
|
||||
await applyQueuedEditorGenerationProject(
|
||||
generated,
|
||||
projectId,
|
||||
applyProjectSnapshot,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (generated.project && applyProjectSnapshot) {
|
||||
applyProjectSnapshot(generated.project);
|
||||
if (generated.asset) {
|
||||
upsertGeneratedAsset?.(generated.asset);
|
||||
}
|
||||
return;
|
||||
}
|
||||
addVideoResultLayer(
|
||||
generated,
|
||||
quickEditResultTitle,
|
||||
generationInputs,
|
||||
quickEditFrame,
|
||||
quickEditDialogId,
|
||||
{
|
||||
sourceLayer: quickEditSourceLayer,
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
const quickEditSelectionMarks =
|
||||
quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id
|
||||
? quickEditSelectionState.marks
|
||||
@@ -1700,22 +1545,10 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
generated,
|
||||
quickEditSourceLayer,
|
||||
generationInputs,
|
||||
quickEditDialogId,
|
||||
undefined,
|
||||
quickEditResultTitle,
|
||||
);
|
||||
} catch (error) {
|
||||
if (quickEditDialogId) {
|
||||
updateCanvasGenerationDialogById(
|
||||
quickEditDialogId,
|
||||
(currentDialog) => ({
|
||||
...currentDialog,
|
||||
prompt: normalizedPrompt,
|
||||
status: 'failed',
|
||||
composerOpen: false,
|
||||
errorMessage: resolveImageGenerationErrorMessage(error),
|
||||
}),
|
||||
);
|
||||
}
|
||||
setQuickEditPanel({
|
||||
...quickEditPanel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
@@ -1727,26 +1560,16 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
});
|
||||
}
|
||||
}, [
|
||||
addVideoResultLayer,
|
||||
applyQuickEditResultToSourceLayer,
|
||||
applyProjectSnapshot,
|
||||
canvasGenerationDialogs,
|
||||
canvasSize,
|
||||
getGeneratingDialogPlaceholder,
|
||||
layers,
|
||||
openCanvasGenerationDialog,
|
||||
projectId,
|
||||
assetFolderId,
|
||||
upsertGeneratedAsset,
|
||||
quickEditPanel,
|
||||
quickEditSourceLayer,
|
||||
quickEditSelectionState,
|
||||
selectSingleLayer,
|
||||
setActiveSidebarPanel,
|
||||
setActiveTool,
|
||||
setQuickEditPanel,
|
||||
updateCanvasGenerationDialogById,
|
||||
viewport,
|
||||
onGenerationWarning,
|
||||
onQueuedGenerationTask,
|
||||
onWalletBalanceMayHaveChanged,
|
||||
|
||||
@@ -1558,6 +1558,28 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not open quick edit programmatically for video layers', () => {
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
title: '源视频',
|
||||
src: '/generated/video.mp4',
|
||||
mediaType: 'video',
|
||||
assetKind: 'video',
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
|
||||
|
||||
expect(screen.getByTestId('tool').textContent).toBe('select');
|
||||
expect(screen.getByTestId('selected').textContent).toBe('-');
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
||||
expect(screen.getByTestId('quick-edit-selection').textContent).toBe('-');
|
||||
});
|
||||
|
||||
it('calculates character animation button price from the model pricing config', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
|
||||
@@ -3871,14 +3871,12 @@ export function useImageCanvasGenerationWorkflow({
|
||||
quickEditPanel,
|
||||
quickEditSourceLayer,
|
||||
quickEditSelectionState,
|
||||
canvasGenerationDialogs,
|
||||
setQuickEditPanel,
|
||||
characterAnimationPanel: effectiveCharacterAnimationPanel,
|
||||
characterAnimationDialog,
|
||||
characterAnimationSourceLayer,
|
||||
setCharacterAnimationPanel,
|
||||
setGenerateDialog,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
getCanvasGenerationDialogById,
|
||||
|
||||
Reference in New Issue
Block a user