Switch to VectorEngine gpt-image-2 and edits
Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
This commit is contained in:
@@ -49,6 +49,9 @@ export type MiniGameDraftGenerationPhase =
|
||||
| 'match3d-upload-images'
|
||||
| 'match3d-generate-views'
|
||||
| 'match3d-background-image'
|
||||
| 'match3d-level-scene'
|
||||
| 'match3d-derived-assets'
|
||||
| 'match3d-parse-spritesheet'
|
||||
| 'match3d-write-draft'
|
||||
| 'match3d-ready'
|
||||
| 'baby-object-draft'
|
||||
@@ -59,6 +62,9 @@ export type MiniGameDraftGenerationPhase =
|
||||
| 'jump-hop-tile-atlas'
|
||||
| 'jump-hop-slice-tiles'
|
||||
| 'jump-hop-write-draft'
|
||||
| 'puzzle-cover-image'
|
||||
| 'puzzle-level-scene'
|
||||
| 'puzzle-ui-assets'
|
||||
| 'puzzle-images'
|
||||
| 'puzzle-ui-background'
|
||||
| 'puzzle-select-image'
|
||||
@@ -73,6 +79,9 @@ export type MiniGameDraftGenerationState = {
|
||||
completedAssetCount: number;
|
||||
totalAssetCount: number;
|
||||
error: string | null;
|
||||
metadata?: {
|
||||
puzzleAiRedraw?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type MiniGameStepDefinition = {
|
||||
@@ -82,65 +91,134 @@ type MiniGameStepDefinition = {
|
||||
weight: number;
|
||||
};
|
||||
|
||||
type TimedMiniGameStepDefinition = Omit<MiniGameStepDefinition, 'weight'> & {
|
||||
durationMs: number;
|
||||
};
|
||||
|
||||
type MiniGameAnchorSource = {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const PUZZLE_STEPS = [
|
||||
{
|
||||
id: 'compile',
|
||||
label: '编译首关草稿',
|
||||
detail: '读取画面描述,建立可编辑草稿与首关结构。',
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-level-name',
|
||||
label: '生成关卡名称',
|
||||
detail: '根据画面描述和图像语义整理首关题目。',
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-images',
|
||||
label: '并行生成素材',
|
||||
detail: '同时生成首关画面与 9:16 纯背景。',
|
||||
weight: 74,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-ui-background',
|
||||
label: '校验背景资源',
|
||||
detail: '确认首关图和 UI 背景都已写入资产库。',
|
||||
weight: 0,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-select-image',
|
||||
label: '写入正式草稿',
|
||||
detail: '写入首图、UI背景和首关数据。',
|
||||
weight: 8,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
const PUZZLE_IMAGE_GENERATION_EXPECTED_MS = 90_000;
|
||||
const PUZZLE_COMPILE_EXPECTED_MS = 8_000;
|
||||
const PUZZLE_LEVEL_NAME_EXPECTED_MS = 10_000;
|
||||
const PUZZLE_WRITE_DRAFT_EXPECTED_MS = 10_000;
|
||||
|
||||
function shouldSkipPuzzleCoverGeneration(state: MiniGameDraftGenerationState) {
|
||||
return state.metadata?.puzzleAiRedraw === false;
|
||||
}
|
||||
|
||||
function buildWeightedPuzzleSteps(
|
||||
steps: ReadonlyArray<TimedMiniGameStepDefinition>,
|
||||
) {
|
||||
const totalDuration = steps.reduce((sum, step) => sum + step.durationMs, 0);
|
||||
let usedWeight = 0;
|
||||
return steps.map((step, index) => {
|
||||
const weight =
|
||||
index === steps.length - 1
|
||||
? Math.max(1, 100 - usedWeight)
|
||||
: Math.max(1, Math.round((step.durationMs / totalDuration) * 100));
|
||||
usedWeight += weight;
|
||||
return {
|
||||
id: step.id,
|
||||
label: step.label,
|
||||
detail: step.detail,
|
||||
weight,
|
||||
} satisfies MiniGameStepDefinition;
|
||||
});
|
||||
}
|
||||
|
||||
function buildPuzzleTimedSteps(state: MiniGameDraftGenerationState) {
|
||||
const steps: TimedMiniGameStepDefinition[] = [
|
||||
{
|
||||
id: 'compile',
|
||||
label: '编译首关草稿',
|
||||
detail: '建立可恢复草稿,整理首关描述与关卡结构,约 8 秒。',
|
||||
durationMs: PUZZLE_COMPILE_EXPECTED_MS,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-level-name',
|
||||
label: '生成关卡名称',
|
||||
detail: '根据描述生成关卡名、作品描述和标签,约 10 秒。',
|
||||
durationMs: PUZZLE_LEVEL_NAME_EXPECTED_MS,
|
||||
},
|
||||
];
|
||||
|
||||
if (!shouldSkipPuzzleCoverGeneration(state)) {
|
||||
steps.push({
|
||||
id: 'puzzle-cover-image',
|
||||
label: '生成拼图首图',
|
||||
detail: '调用 gpt-image-2 生成 1:1 拼图首图,预计 90 秒。',
|
||||
durationMs: PUZZLE_IMAGE_GENERATION_EXPECTED_MS,
|
||||
});
|
||||
}
|
||||
|
||||
steps.push(
|
||||
{
|
||||
id: 'puzzle-level-scene',
|
||||
label: '生成关卡画面',
|
||||
detail: shouldSkipPuzzleCoverGeneration(state)
|
||||
? '直接使用上传图作为参考,调用 gpt-image-2 生成 9:16 完整关卡画面,预计 90 秒。'
|
||||
: '使用拼图首图作为参考,调用 gpt-image-2 生成 9:16 完整关卡画面,预计 90 秒。',
|
||||
durationMs: PUZZLE_IMAGE_GENERATION_EXPECTED_MS,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-ui-assets',
|
||||
label: '生成UI与背景',
|
||||
detail:
|
||||
'用关卡画面作参考,并发生成 UI spritesheet 与 9:16 纯背景;两次 gpt-image-2 并发,预计 90 秒。',
|
||||
durationMs: PUZZLE_IMAGE_GENERATION_EXPECTED_MS,
|
||||
},
|
||||
{
|
||||
id: 'puzzle-select-image',
|
||||
label: '写入正式草稿',
|
||||
detail: '校验资产并写入正式首关、作品摘要和草稿投影,约 10 秒。',
|
||||
durationMs: PUZZLE_WRITE_DRAFT_EXPECTED_MS,
|
||||
},
|
||||
);
|
||||
return steps;
|
||||
}
|
||||
|
||||
function buildPuzzleSteps(state: MiniGameDraftGenerationState) {
|
||||
return buildWeightedPuzzleSteps(buildPuzzleTimedSteps(state));
|
||||
}
|
||||
|
||||
function resolvePuzzleEstimatedWaitMs(state: MiniGameDraftGenerationState) {
|
||||
return buildPuzzleTimedSteps(state).reduce(
|
||||
(sum, step) => sum + step.durationMs,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
const PUZZLE_ESTIMATED_WAIT_MS = 5 * 60_000;
|
||||
const PUZZLE_NON_READY_MAX_PROGRESS = 98;
|
||||
const BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS = 6 * 60_000;
|
||||
const PUZZLE_PHASE_TIMELINE: Array<{
|
||||
function buildPuzzlePhaseTimeline(state: MiniGameDraftGenerationState): Array<{
|
||||
phase: Extract<
|
||||
MiniGameDraftGenerationPhase,
|
||||
| 'compile'
|
||||
| 'puzzle-level-name'
|
||||
| 'puzzle-images'
|
||||
| 'puzzle-ui-background'
|
||||
| 'puzzle-cover-image'
|
||||
| 'puzzle-level-scene'
|
||||
| 'puzzle-ui-assets'
|
||||
| 'puzzle-select-image'
|
||||
>;
|
||||
durationMs: number;
|
||||
}> = [
|
||||
{ phase: 'compile', durationMs: 12_000 },
|
||||
{ phase: 'puzzle-level-name', durationMs: 8_000 },
|
||||
{ phase: 'puzzle-images', durationMs: 260_000 },
|
||||
{ phase: 'puzzle-ui-background', durationMs: 10_000 },
|
||||
{ phase: 'puzzle-select-image', durationMs: 10_000 },
|
||||
];
|
||||
}> {
|
||||
return buildPuzzleTimedSteps(state).map((step) => ({
|
||||
phase: step.id as Extract<
|
||||
MiniGameDraftGenerationPhase,
|
||||
| 'compile'
|
||||
| 'puzzle-level-name'
|
||||
| 'puzzle-cover-image'
|
||||
| 'puzzle-level-scene'
|
||||
| 'puzzle-ui-assets'
|
||||
| 'puzzle-select-image'
|
||||
>,
|
||||
durationMs: step.durationMs,
|
||||
}));
|
||||
}
|
||||
|
||||
const BIG_FISH_STEPS = [
|
||||
{
|
||||
@@ -198,65 +276,69 @@ const MATCH3D_STEPS = [
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
id: 'match3d-background-prompt',
|
||||
label: '生成背景提示词',
|
||||
detail: '整理纯背景图与容器 UI 图提示词。',
|
||||
weight: 6,
|
||||
id: 'match3d-level-scene',
|
||||
label: '生成关卡整图',
|
||||
detail: '调用 gpt-image-2 生成 9:16 完整抓大鹅关卡画面。',
|
||||
weight: 28,
|
||||
},
|
||||
{
|
||||
id: 'match3d-material-sheet',
|
||||
label: '分批生成素材图',
|
||||
detail: '按 1K 参数分批生成 5x5 多视角素材图。',
|
||||
weight: 24,
|
||||
id: 'match3d-derived-assets',
|
||||
label: '生成三张派生图',
|
||||
detail: '以关卡整图为参考,并发生成 UI、背景和 10x10 物品 Sprite。',
|
||||
weight: 34,
|
||||
},
|
||||
{
|
||||
id: 'match3d-slice-images',
|
||||
label: '切割独立图片',
|
||||
detail: '把素材图切成每个物品的五个视角。',
|
||||
weight: 12,
|
||||
},
|
||||
{
|
||||
id: 'match3d-upload-images',
|
||||
label: '上传图片资产',
|
||||
detail: '上传每个物品的 2D 五视角素材。',
|
||||
weight: 14,
|
||||
},
|
||||
{
|
||||
id: 'match3d-generate-views',
|
||||
label: '校验素材结构',
|
||||
detail: '确认物品顺序和五视角图片。',
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
id: 'match3d-background-image',
|
||||
label: '生成UI背景',
|
||||
detail: '生成无 UI 元素纯背景,并生成题材容器 UI 图。',
|
||||
weight: 16,
|
||||
id: 'match3d-parse-spritesheet',
|
||||
label: '解析物品Sprite',
|
||||
detail: '解析 20 个物品和每个物品的 5 个形态,并上传透明 PNG。',
|
||||
weight: 18,
|
||||
},
|
||||
{
|
||||
id: 'match3d-write-draft',
|
||||
label: '写入草稿页',
|
||||
detail: '保存素材、背景、容器和作品草稿。',
|
||||
detail: '保存关卡整图、派生图集、20 种物品素材和作品草稿。',
|
||||
weight: 2,
|
||||
},
|
||||
] as const satisfies ReadonlyArray<MiniGameStepDefinition>;
|
||||
|
||||
const MATCH3D_ESTIMATED_WAIT_MS = 510_000;
|
||||
const MATCH3D_ESTIMATED_WAIT_MS = 460_000;
|
||||
|
||||
const MATCH3D_PHASE_ORDER: Partial<
|
||||
Record<MiniGameDraftGenerationPhase, number>
|
||||
> = {
|
||||
'match3d-work-title': 0,
|
||||
'match3d-item-names': 1,
|
||||
'match3d-background-prompt': 2,
|
||||
'match3d-level-scene': 2,
|
||||
'match3d-derived-assets': 3,
|
||||
'match3d-parse-spritesheet': 4,
|
||||
'match3d-write-draft': 5,
|
||||
// 中文注释:旧生成页阶段在恢复生成中草稿时归并到新流程对应阶段。
|
||||
'match3d-background-prompt': 1,
|
||||
'match3d-material-sheet': 3,
|
||||
'match3d-slice-images': 4,
|
||||
'match3d-upload-images': 5,
|
||||
'match3d-generate-views': 6,
|
||||
'match3d-background-image': 7,
|
||||
'match3d-write-draft': 8,
|
||||
'match3d-upload-images': 4,
|
||||
'match3d-generate-views': 4,
|
||||
'match3d-background-image': 3,
|
||||
};
|
||||
|
||||
function normalizeMatch3DGenerationPhase(
|
||||
phase: MiniGameDraftGenerationPhase,
|
||||
): MiniGameDraftGenerationPhase {
|
||||
switch (phase) {
|
||||
case 'match3d-background-prompt':
|
||||
return 'match3d-item-names';
|
||||
case 'match3d-material-sheet':
|
||||
case 'match3d-background-image':
|
||||
return 'match3d-derived-assets';
|
||||
case 'match3d-slice-images':
|
||||
case 'match3d-upload-images':
|
||||
case 'match3d-generate-views':
|
||||
return 'match3d-parse-spritesheet';
|
||||
default:
|
||||
return phase;
|
||||
}
|
||||
}
|
||||
|
||||
const BABY_OBJECT_MATCH_STEPS = [
|
||||
{
|
||||
id: 'baby-object-draft',
|
||||
@@ -319,7 +401,7 @@ function clampProgress(value: number) {
|
||||
|
||||
function getStepDefinitions(kind: MiniGameDraftGenerationKind) {
|
||||
if (kind === 'puzzle') {
|
||||
return PUZZLE_STEPS;
|
||||
return buildPuzzleSteps(createMiniGameDraftGenerationState('puzzle'));
|
||||
}
|
||||
if (kind === 'square-hole') {
|
||||
return SQUARE_HOLE_STEPS;
|
||||
@@ -429,26 +511,21 @@ function resolveMatch3DPhaseByElapsedMs(
|
||||
currentPhase: MiniGameDraftGenerationPhase,
|
||||
): MiniGameDraftGenerationPhase {
|
||||
const elapsedPhase =
|
||||
elapsedMs >= 492_000
|
||||
elapsedMs >= 450_000
|
||||
? 'match3d-write-draft'
|
||||
: elapsedMs >= 370_000
|
||||
? 'match3d-background-image'
|
||||
: elapsedMs >= 340_000
|
||||
? 'match3d-generate-views'
|
||||
: elapsedMs >= 260_000
|
||||
? 'match3d-upload-images'
|
||||
: elapsedMs >= 210_000
|
||||
? 'match3d-slice-images'
|
||||
: elapsedMs >= 28_000
|
||||
? 'match3d-material-sheet'
|
||||
: elapsedMs >= 12_000
|
||||
? 'match3d-background-prompt'
|
||||
: elapsedMs >= 4_000
|
||||
? 'match3d-item-names'
|
||||
: 'match3d-work-title';
|
||||
: elapsedMs >= 360_000
|
||||
? 'match3d-parse-spritesheet'
|
||||
: elapsedMs >= 118_000
|
||||
? 'match3d-derived-assets'
|
||||
: elapsedMs >= 28_000
|
||||
? 'match3d-level-scene'
|
||||
: elapsedMs >= 8_000
|
||||
? 'match3d-item-names'
|
||||
: 'match3d-work-title';
|
||||
const elapsedOrder = MATCH3D_PHASE_ORDER[elapsedPhase] ?? 0;
|
||||
const currentOrder = MATCH3D_PHASE_ORDER[currentPhase] ?? -1;
|
||||
return currentOrder > elapsedOrder ? currentPhase : elapsedPhase;
|
||||
const normalizedCurrentPhase = normalizeMatch3DGenerationPhase(currentPhase);
|
||||
const currentOrder = MATCH3D_PHASE_ORDER[normalizedCurrentPhase] ?? -1;
|
||||
return currentOrder > elapsedOrder ? normalizedCurrentPhase : elapsedPhase;
|
||||
}
|
||||
|
||||
function resolveBabyObjectMatchPhaseByElapsedMs(
|
||||
@@ -481,10 +558,13 @@ function resolveJumpHopPhaseByElapsedMs(
|
||||
return 'jump-hop-draft';
|
||||
}
|
||||
|
||||
function resolvePuzzleTimelineByElapsedMs(elapsedMs: number) {
|
||||
function resolvePuzzleTimelineByElapsedMs(
|
||||
elapsedMs: number,
|
||||
state: MiniGameDraftGenerationState,
|
||||
) {
|
||||
let elapsedBeforePhase = 0;
|
||||
|
||||
for (const item of PUZZLE_PHASE_TIMELINE) {
|
||||
for (const item of buildPuzzlePhaseTimeline(state)) {
|
||||
const elapsedInPhase = elapsedMs - elapsedBeforePhase;
|
||||
|
||||
if (elapsedInPhase < item.durationMs) {
|
||||
@@ -523,7 +603,7 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
state.kind === 'puzzle' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? resolvePuzzleTimelineByElapsedMs(elapsedMs)
|
||||
? resolvePuzzleTimelineByElapsedMs(elapsedMs, state)
|
||||
: null;
|
||||
const normalizedState =
|
||||
puzzleTimeline != null
|
||||
@@ -568,7 +648,10 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
}
|
||||
: state;
|
||||
|
||||
const steps = getStepDefinitions(normalizedState.kind);
|
||||
const steps =
|
||||
normalizedState.kind === 'puzzle'
|
||||
? buildPuzzleSteps(normalizedState)
|
||||
: getStepDefinitions(normalizedState.kind);
|
||||
const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase);
|
||||
const completedWeight = steps
|
||||
.slice(
|
||||
@@ -641,7 +724,7 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
normalizedState.phase === 'ready'
|
||||
? 0
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? Math.max(0, PUZZLE_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
? Math.max(0, resolvePuzzleEstimatedWaitMs(normalizedState) - elapsedMs)
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? Math.max(0, 7_000 - elapsedMs)
|
||||
: normalizedState.kind === 'square-hole'
|
||||
@@ -812,9 +895,6 @@ export function buildMatch3DGenerationAnchorEntries(
|
||||
}
|
||||
|
||||
const config = session?.config;
|
||||
const clearCount = formPayload?.clearCount ?? config?.clearCount ?? null;
|
||||
const difficulty = formPayload?.difficulty ?? config?.difficulty ?? null;
|
||||
const itemCount = resolveMatch3DGeneratedItemCount(clearCount, difficulty);
|
||||
const entries: Array<MiniGameAnchorSource | null> = [
|
||||
{
|
||||
key: 'match3d-theme',
|
||||
@@ -827,8 +907,8 @@ export function buildMatch3DGenerationAnchorEntries(
|
||||
},
|
||||
{
|
||||
key: 'match3d-items',
|
||||
label: '物品数量',
|
||||
value: `${itemCount} 件`,
|
||||
label: '素材数量',
|
||||
value: `${resolveMatch3DGeneratedItemCount()} 种素材`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -843,22 +923,10 @@ export function buildMatch3DGenerationAnchorEntries(
|
||||
}
|
||||
|
||||
function resolveMatch3DGeneratedItemCount(
|
||||
clearCount: number | null | undefined,
|
||||
difficulty: number | null | undefined,
|
||||
_clearCount: number | null | undefined = null,
|
||||
_difficulty: number | null | undefined = null,
|
||||
) {
|
||||
const roundToSheet = (count: number) => Math.ceil(count / 5) * 5;
|
||||
if (clearCount === 8) return roundToSheet(3);
|
||||
if (clearCount === 12) return roundToSheet(9);
|
||||
if (clearCount === 16) return roundToSheet(15);
|
||||
if (clearCount === 20 || clearCount === 21) return roundToSheet(21);
|
||||
const normalizedDifficulty =
|
||||
typeof difficulty === 'number' && Number.isFinite(difficulty)
|
||||
? Math.max(1, Math.min(10, Math.round(difficulty)))
|
||||
: 4;
|
||||
if (normalizedDifficulty <= 2) return roundToSheet(3);
|
||||
if (normalizedDifficulty <= 4) return roundToSheet(9);
|
||||
if (normalizedDifficulty <= 6) return roundToSheet(15);
|
||||
return roundToSheet(21);
|
||||
return 20;
|
||||
}
|
||||
|
||||
export function buildBabyObjectMatchGenerationAnchorEntries(
|
||||
|
||||
Reference in New Issue
Block a user