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).
156 lines
9.2 KiB
Rust
156 lines
9.2 KiB
Rust
use std::{
|
||
collections::BTreeMap,
|
||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||
};
|
||
|
||
use axum::{
|
||
Json,
|
||
extract::{Extension, Path as AxumPath, State, rejection::JsonRejection},
|
||
http::{HeaderName, StatusCode, header},
|
||
response::{
|
||
IntoResponse, Response,
|
||
sse::{Event, Sse},
|
||
},
|
||
};
|
||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||
use image::ImageFormat;
|
||
use module_assets::{
|
||
AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input,
|
||
build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id,
|
||
};
|
||
use module_puzzle::{PuzzleGeneratedImageCandidate, PuzzleRuntimeLevelStatus};
|
||
use platform_llm::{LlmMessage, LlmMessageContentPart, LlmTextRequest};
|
||
use platform_oss::{LegacyAssetPrefix, OssSignedGetObjectUrlRequest};
|
||
use platform_oss::{OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest};
|
||
use serde_json::{Map, Value, json};
|
||
use shared_contracts::{
|
||
creation_audio::CreationAudioAsset,
|
||
puzzle_agent::{
|
||
CreatePuzzleAgentSessionRequest, ExecutePuzzleAgentActionRequest,
|
||
PuzzleAgentActionResponse, PuzzleAgentMessageResponse, PuzzleAgentOperationResponse,
|
||
PuzzleAgentSessionResponse, PuzzleAgentSessionSnapshotResponse,
|
||
PuzzleAgentSuggestedActionResponse, PuzzleAnchorItemResponse, PuzzleAnchorPackResponse,
|
||
PuzzleCreatorIntentResponse, PuzzleDraftLevelResponse, PuzzleFormDraftResponse,
|
||
PuzzleGeneratedImageCandidateResponse, PuzzleResultDraftResponse,
|
||
PuzzleResultPreviewBlockerResponse, PuzzleResultPreviewEnvelopeResponse,
|
||
PuzzleResultPreviewFindingResponse, SendPuzzleAgentMessageRequest,
|
||
},
|
||
puzzle_gallery::PuzzleGalleryDetailResponse,
|
||
puzzle_runtime::{
|
||
AdvancePuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse,
|
||
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse,
|
||
PuzzlePieceStateResponse, PuzzleRecommendedNextWorkResponse, PuzzleRunResponse,
|
||
PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest,
|
||
SubmitPuzzleLeaderboardRequest, SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest,
|
||
UsePuzzleRuntimePropRequest,
|
||
},
|
||
puzzle_works::{
|
||
PutPuzzleWorkRequest, PuzzleOnboardingGenerateRequest, PuzzleOnboardingGenerateResponse,
|
||
PuzzleOnboardingSaveRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
|
||
PuzzleWorkProfileResponse, PuzzleWorkSummaryResponse, PuzzleWorksResponse,
|
||
},
|
||
};
|
||
use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros};
|
||
use spacetime_client::{
|
||
PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput,
|
||
PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord,
|
||
PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord,
|
||
PuzzleAudioAssetRecord, PuzzleCreatorIntentRecord, PuzzleDraftLevelRecord,
|
||
PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput, PuzzleGalleryCardRecord,
|
||
PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput,
|
||
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput,
|
||
PuzzleRecommendedNextWorkRecord, PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord,
|
||
PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, PuzzleRunDragRecordInput,
|
||
PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord,
|
||
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleSelectCoverImageRecordInput,
|
||
PuzzleUiBackgroundSaveRecordInput, PuzzleWorkLikeReportRecordInput,
|
||
PuzzleWorkPointIncentiveClaimRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput,
|
||
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
|
||
};
|
||
use std::convert::Infallible;
|
||
|
||
use crate::{
|
||
ai_generation_drafts::{AiGenerationDraftContext, AiGenerationDraftWriter},
|
||
api_response::json_success_body,
|
||
asset_billing::{
|
||
execute_billable_asset_operation, execute_billable_asset_operation_with_cost,
|
||
should_skip_asset_operation_billing_for_connectivity,
|
||
},
|
||
auth::AuthenticatedAccessToken,
|
||
generated_asset_sheets::apply_generated_asset_sheet_green_screen_alpha,
|
||
http_error::AppError,
|
||
llm_model_routing::{CREATION_TEMPLATE_LLM_MODEL, PUZZLE_LEVEL_NAME_VISION_LLM_MODEL},
|
||
openai_image_generation::{
|
||
DownloadedOpenAiImage, GPT_IMAGE_2_MODEL, build_openai_image_http_client,
|
||
create_openai_image_generation, require_openai_image_settings,
|
||
},
|
||
platform_errors::map_oss_error,
|
||
prompt::puzzle::{
|
||
draft::{
|
||
PuzzleFormSeedPromptParts, build_puzzle_form_seed_prompt,
|
||
resolve_puzzle_draft_cover_prompt, resolve_puzzle_level_image_prompt,
|
||
},
|
||
image::{PUZZLE_DEFAULT_NEGATIVE_PROMPT, build_puzzle_image_prompt},
|
||
level_name::{
|
||
PUZZLE_FIRST_LEVEL_NAME_SYSTEM_PROMPT, build_puzzle_first_level_name_user_prompt,
|
||
build_puzzle_first_level_name_vision_user_text,
|
||
},
|
||
tags::{PUZZLE_TAG_GENERATION_SYSTEM_PROMPT, build_puzzle_tag_generation_user_prompt},
|
||
},
|
||
puzzle_agent_turn::{
|
||
PuzzleAgentTurnRequest, build_failed_finalize_record_input, build_finalize_record_input,
|
||
run_puzzle_agent_turn,
|
||
},
|
||
puzzle_gallery_cache::{build_puzzle_gallery_window_response, puzzle_gallery_cached_json},
|
||
request_context::RequestContext,
|
||
state::PuzzleApiState,
|
||
work_author::resolve_puzzle_work_author_by_user_id,
|
||
work_play_tracking::{WorkPlayTrackingDraft, record_puzzle_work_play_start_after_success},
|
||
};
|
||
|
||
const PUZZLE_AGENT_API_BASE_PROVIDER: &str = "puzzle-agent";
|
||
const PUZZLE_WORKS_PROVIDER: &str = "puzzle-works";
|
||
const PUZZLE_GALLERY_PROVIDER: &str = "puzzle-gallery";
|
||
const PUZZLE_RUNTIME_PROVIDER: &str = "puzzle-runtime";
|
||
const PUZZLE_IMAGE_MODEL_GPT_IMAGE_2: &str = "gpt-image-2";
|
||
const PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW: &str = "gemini-3.1-flash-image-preview";
|
||
const PUZZLE_IMAGE_GENERATION_POINTS_COST: u64 = 2;
|
||
const PUZZLE_ENTITY_KIND: &str = "puzzle_work";
|
||
#[cfg(test)]
|
||
const PUZZLE_GENERATED_IMAGE_SIZE: &str = "1024*1024";
|
||
const PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE: &str = "1024x1024";
|
||
const VECTOR_ENGINE_PROVIDER: &str = "vector-engine";
|
||
const PUZZLE_LEVEL_NAME_VISION_IMAGE_MAX_SIDE: u32 = 768;
|
||
const PUZZLE_LEVEL_NAME_VISION_MAX_TOKENS: u32 = 512;
|
||
const PUZZLE_REFERENCE_IMAGE_MAX_BYTES: usize = 8 * 1024 * 1024;
|
||
const PUZZLE_REFERENCE_IMAGE_SOURCE_LIMIT: usize = 5;
|
||
const PUZZLE_UI_BACKGROUND_PROMPT_FALLBACK_MARKER: &str =
|
||
"移动端拼图游戏纯背景,题材氛围清晰,不包含拼图槽或 UI 元素";
|
||
const PUZZLE_VECTOR_ENGINE_SQUARE_IMAGE_SIZE: &str = "1024x1024";
|
||
const PUZZLE_VECTOR_ENGINE_PORTRAIT_IMAGE_SIZE: &str = "1024x1536";
|
||
const PUZZLE_LEVEL_SCENE_IMAGE_PROMPT: &str = "参考图作为拼图画面,生成对应的拼图游戏关卡画面,要求画面中所有元素精致且风格高度一致,画面中所有UI细节饱满精致、完成度高、顶级游戏品质\n\n画面元素:\n返回按钮位于顶部左上角,顶部中间显示关卡标题“第1关 影”和倒计时时间,右上角显示设置按钮\n画面中间是一个正方形的3*3拼图,拼图区域宽度与画面宽度同宽,紧贴画面横向边缘,拼图区域边界带有边框装饰\n拼图区域下方包含一个下一关按钮,仅在关卡完成时显示\n底部是三个贴合画面主题的道具按钮分别为“提示”、“原图”、“冻结”\n道具按钮上不要显示次数标注,返回按钮和设置按钮旁禁止标注文字";
|
||
const PUZZLE_UI_SPRITESHEET_IMAGE_PROMPT: &str = "提取画面中的UI元素,将返回按钮、设置按钮、下一关按钮、提示按钮、原图按钮、冻结按钮整理成纯绿色绿幕背景的spritesheet。背景必须是统一纯绿色绿幕(高饱和亮绿色,接近 #00FF00),背景平整无纹理、无渐变、无阴影、无场景内容,后端会在生图后将绿幕扣成透明并把透明背景 PNG 存到 OSS。按钮顺序必须按原图位置从左到右、从上到下排列:返回、设置、下一关、提示、原图、冻结。按钮素材内必须保留对应中文文字,每个按钮必须是独立完整图形,按钮之间保留足够纯绿色绿幕空白,不能相互接触、重叠或连成一片,方便运行态按自动边界检测识别矩形素材。返回按钮和设置按钮不要额外画白色外圈、白底圆环或浮雕外框,直接画扁平图标本体。按钮自身不得使用接近 #00FF00 的高饱和纯绿;绿色题材只能使用深绿、橄榄绿、金绿或蓝绿,并用清晰描边与绿幕区分。禁止水印、数字、次数标注、透明背景、背景图、拼图块、棋盘、网格线、按钮外标签和额外按钮。";
|
||
const PUZZLE_LEVEL_BACKGROUND_IMAGE_PROMPT: &str = "移除参考图中所有UI元素、移除拼图画面,仅保留背景图,补全被覆盖的背景图内容。禁止在背景中出现人像或和拼图画面中主体一致的内容";
|
||
mod handlers;
|
||
pub(crate) use self::handlers::*;
|
||
|
||
mod mappers;
|
||
|
||
use self::mappers::*;
|
||
|
||
mod draft;
|
||
use self::draft::*;
|
||
|
||
mod tags;
|
||
|
||
use self::tags::*;
|
||
|
||
mod generation;
|
||
mod vector_engine;
|
||
|
||
use self::generation::*;
|
||
use self::vector_engine::*;
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|