1
This commit is contained in:
@@ -116,6 +116,8 @@ 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_REFERENCE_IMAGE_MAX_BYTES: usize = 8 * 1024 * 1024;
|
||||
const PUZZLE_VECTOR_ENGINE_IMAGE_EDIT_MODEL: &str = "gpt-image-2";
|
||||
|
||||
pub async fn create_puzzle_agent_session(
|
||||
State(state): State<AppState>,
|
||||
@@ -197,6 +199,7 @@ pub async fn generate_puzzle_onboarding_work(
|
||||
level_name.as_str(),
|
||||
prompt_text.as_str(),
|
||||
None,
|
||||
false,
|
||||
Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2),
|
||||
1,
|
||||
0,
|
||||
@@ -886,6 +889,7 @@ pub async fn execute_puzzle_agent_action(
|
||||
&target_level.level_name,
|
||||
&prompt,
|
||||
payload.reference_image_src.as_deref(),
|
||||
payload.ai_redraw.unwrap_or(true),
|
||||
payload.image_model.as_deref(),
|
||||
candidate_count,
|
||||
candidate_start_index,
|
||||
@@ -2349,6 +2353,7 @@ fn map_puzzle_leaderboard_entry_response(
|
||||
rank: entry.rank,
|
||||
nickname: entry.nickname,
|
||||
elapsed_ms: entry.elapsed_ms,
|
||||
visible_tags: entry.visible_tags,
|
||||
is_current_player: entry.is_current_player,
|
||||
}
|
||||
}
|
||||
@@ -2809,6 +2814,7 @@ fn normalize_puzzle_levels_json_for_module(value: Option<&str>) -> Result<Option
|
||||
"level_id": level.level_id,
|
||||
"level_name": level.level_name,
|
||||
"picture_description": level.picture_description,
|
||||
"picture_reference": level.picture_reference,
|
||||
"candidates": level
|
||||
.candidates
|
||||
.iter()
|
||||
@@ -3098,8 +3104,9 @@ fn build_puzzle_levels_with_primary_update(
|
||||
.or_else(|| (!levels.is_empty()).then_some(0))
|
||||
{
|
||||
levels[index].level_name = target_level.level_name.clone();
|
||||
if let Some(picture_reference) =
|
||||
picture_reference.map(str::trim).filter(|value| !value.is_empty())
|
||||
if let Some(picture_reference) = picture_reference
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
levels[index].picture_reference = Some(picture_reference.to_string());
|
||||
}
|
||||
@@ -3145,6 +3152,7 @@ async fn compile_puzzle_draft_with_initial_cover(
|
||||
&target_level.level_name,
|
||||
&image_prompt,
|
||||
reference_image_src,
|
||||
true,
|
||||
image_model,
|
||||
1,
|
||||
target_level.candidates.len(),
|
||||
@@ -4059,6 +4067,7 @@ async fn generate_puzzle_image_candidates(
|
||||
level_name: &str,
|
||||
prompt: &str,
|
||||
reference_image_src: Option<&str>,
|
||||
use_reference_image_edit: bool,
|
||||
image_model: Option<&str>,
|
||||
candidate_count: u32,
|
||||
candidate_start_index: usize,
|
||||
@@ -4066,12 +4075,14 @@ async fn generate_puzzle_image_candidates(
|
||||
let total_started_at = Instant::now();
|
||||
let count = candidate_count.clamp(1, 1);
|
||||
let resolved_model = resolve_puzzle_image_model(image_model);
|
||||
let actual_prompt = build_puzzle_image_prompt(level_name, prompt);
|
||||
let http_client = build_puzzle_image_http_client(state, resolved_model)?;
|
||||
let has_reference_image = reference_image_src
|
||||
.map(str::trim)
|
||||
.map(|value| !value.is_empty())
|
||||
.unwrap_or(false);
|
||||
let has_reference_image = has_puzzle_reference_image(reference_image_src);
|
||||
let should_use_reference_image_edit =
|
||||
should_use_puzzle_reference_image_edit(reference_image_src, use_reference_image_edit);
|
||||
let actual_prompt = build_puzzle_vector_engine_generation_prompt(
|
||||
build_puzzle_image_prompt(level_name, prompt).as_str(),
|
||||
should_use_reference_image_edit,
|
||||
);
|
||||
tracing::info!(
|
||||
provider = resolved_model.provider_name(),
|
||||
image_model = resolved_model.request_model_name(),
|
||||
@@ -4080,12 +4091,14 @@ async fn generate_puzzle_image_candidates(
|
||||
prompt_chars = prompt.chars().count(),
|
||||
actual_prompt_chars = actual_prompt.chars().count(),
|
||||
has_reference_image,
|
||||
use_reference_image_edit = should_use_reference_image_edit,
|
||||
"拼图图片生成请求已准备"
|
||||
);
|
||||
let reference_image_started_at = Instant::now();
|
||||
let reference_image = match reference_image_src
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.filter(|_| should_use_reference_image_edit)
|
||||
{
|
||||
Some(source) => {
|
||||
let resolved =
|
||||
@@ -4104,12 +4117,14 @@ async fn generate_puzzle_image_candidates(
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if !has_reference_image {
|
||||
if !should_use_reference_image_edit {
|
||||
tracing::info!(
|
||||
provider = resolved_model.provider_name(),
|
||||
image_model = resolved_model.request_model_name(),
|
||||
session_id,
|
||||
level_name,
|
||||
has_reference_image,
|
||||
use_reference_image_edit = should_use_reference_image_edit,
|
||||
elapsed_ms = reference_image_started_at.elapsed().as_millis() as u64,
|
||||
"拼图参考图解析跳过"
|
||||
);
|
||||
@@ -4118,19 +4133,36 @@ async fn generate_puzzle_image_candidates(
|
||||
// 中文注释:拼图作品资产统一按 1:1 正方形生成,前端运行时也按正方形棋盘切块承载。
|
||||
let settings = require_puzzle_vector_engine_settings(state)?;
|
||||
let vector_engine_started_at = Instant::now();
|
||||
let generated = create_puzzle_vector_engine_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
resolved_model,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
reference_image
|
||||
.as_ref()
|
||||
.map(|image| image.data_url.as_str()),
|
||||
)
|
||||
.await
|
||||
let generated = if should_use_reference_image_edit {
|
||||
let reference_image = reference_image.as_ref().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "puzzle",
|
||||
"field": "referenceImageSrc",
|
||||
"message": "AI 重绘需要提供参考图。",
|
||||
}))
|
||||
})?;
|
||||
create_puzzle_vector_engine_image_edit(
|
||||
&http_client,
|
||||
&settings,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
reference_image,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
create_puzzle_vector_engine_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
resolved_model,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.map_err(map_puzzle_generation_endpoint_error)?;
|
||||
tracing::info!(
|
||||
provider = resolved_model.provider_name(),
|
||||
@@ -4219,14 +4251,13 @@ mod tests {
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE,
|
||||
4,
|
||||
Some("data:image/png;base64,abcd"),
|
||||
);
|
||||
|
||||
assert_eq!(body["model"], VECTOR_ENGINE_GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(body["size"], PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE);
|
||||
assert_eq!(body["n"], 1);
|
||||
assert!(body.get("official_fallback").is_none());
|
||||
assert_eq!(body["image"][0], "data:image/png;base64,abcd");
|
||||
assert!(body.get("image").is_none());
|
||||
assert!(
|
||||
body["prompt"]
|
||||
.as_str()
|
||||
@@ -4235,6 +4266,61 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_vector_engine_edit_url_uses_images_edits_endpoint() {
|
||||
let settings = PuzzleVectorEngineSettings {
|
||||
base_url: "https://vector.example/v1".to_string(),
|
||||
api_key: "test-key".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
puzzle_vector_engine_images_edit_url(&settings),
|
||||
"https://vector.example/v1/images/edits"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_vector_engine_edit_response_decodes_b64_image() {
|
||||
let images = puzzle_images_from_base64(
|
||||
"edit-1".to_string(),
|
||||
vec![BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nrest")],
|
||||
1,
|
||||
);
|
||||
|
||||
assert_eq!(images.images.len(), 1);
|
||||
assert_eq!(images.images[0].mime_type, "image/png");
|
||||
assert_eq!(images.images[0].extension, "png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_vector_engine_prompt_strongly_uses_reference_image() {
|
||||
let prompt = build_puzzle_vector_engine_generation_prompt("请生成雨夜猫街。", true);
|
||||
|
||||
assert!(prompt.contains("参考图作为第一优先级"));
|
||||
assert!(prompt.contains("严格保留参考图的主要主体、构图关系、视角、姿态、配色和光影氛围"));
|
||||
assert!(prompt.contains("请生成雨夜猫街。"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_vector_engine_prompt_keeps_text_only_prompt_unchanged() {
|
||||
let prompt = build_puzzle_vector_engine_generation_prompt("请生成雨夜猫街。", false);
|
||||
|
||||
assert_eq!(prompt, "请生成雨夜猫街。");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_reference_image_edit_requires_ai_redraw() {
|
||||
assert!(!should_use_puzzle_reference_image_edit(None, true));
|
||||
assert!(!should_use_puzzle_reference_image_edit(
|
||||
Some("data:image/png;base64,abcd"),
|
||||
false
|
||||
));
|
||||
assert!(should_use_puzzle_reference_image_edit(
|
||||
Some("data:image/png;base64,abcd"),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_compile_error_preserves_vector_engine_unavailable_status() {
|
||||
let error = map_puzzle_compile_error(SpacetimeClientError::Runtime(
|
||||
@@ -4583,9 +4669,9 @@ struct PuzzleGeneratedImages {
|
||||
}
|
||||
|
||||
struct PuzzleResolvedReferenceImage {
|
||||
data_url: String,
|
||||
mime_type: String,
|
||||
bytes_len: usize,
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
struct GeneratedPuzzleImageCandidate {
|
||||
@@ -4721,7 +4807,6 @@ async fn create_puzzle_vector_engine_image_generation(
|
||||
negative_prompt: &str,
|
||||
size: &str,
|
||||
candidate_count: u32,
|
||||
reference_image: Option<&str>,
|
||||
) -> Result<PuzzleGeneratedImages, AppError> {
|
||||
let request_body = build_puzzle_vector_engine_image_request_body(
|
||||
image_model,
|
||||
@@ -4729,13 +4814,8 @@ async fn create_puzzle_vector_engine_image_generation(
|
||||
negative_prompt,
|
||||
size,
|
||||
candidate_count,
|
||||
reference_image,
|
||||
);
|
||||
let request_url = puzzle_vector_engine_images_generation_url(settings);
|
||||
let has_reference_image = reference_image
|
||||
.map(str::trim)
|
||||
.map(|value| !value.is_empty())
|
||||
.unwrap_or(false);
|
||||
let request_started_at = Instant::now();
|
||||
let response = http_client
|
||||
.post(request_url.as_str())
|
||||
@@ -4762,7 +4842,7 @@ async fn create_puzzle_vector_engine_image_generation(
|
||||
status = status.as_u16(),
|
||||
prompt_chars = prompt.chars().count(),
|
||||
size,
|
||||
has_reference_image,
|
||||
has_reference_image = false,
|
||||
elapsed_ms = upstream_elapsed_ms,
|
||||
"拼图 VectorEngine 图片生成 HTTP 返回"
|
||||
);
|
||||
@@ -4811,15 +4891,114 @@ async fn create_puzzle_vector_engine_image_generation(
|
||||
)
|
||||
}
|
||||
|
||||
async fn create_puzzle_vector_engine_image_edit(
|
||||
http_client: &reqwest::Client,
|
||||
settings: &PuzzleVectorEngineSettings,
|
||||
prompt: &str,
|
||||
negative_prompt: &str,
|
||||
size: &str,
|
||||
candidate_count: u32,
|
||||
reference_image: &PuzzleResolvedReferenceImage,
|
||||
) -> Result<PuzzleGeneratedImages, AppError> {
|
||||
let request_url = puzzle_vector_engine_images_edit_url(settings);
|
||||
let task_id = format!("vector-engine-edit-{}", current_utc_micros());
|
||||
let file_name = format!(
|
||||
"puzzle-reference.{}",
|
||||
puzzle_mime_to_extension(reference_image.mime_type.as_str())
|
||||
);
|
||||
let image_part = reqwest::multipart::Part::bytes(reference_image.bytes.clone())
|
||||
.file_name(file_name)
|
||||
.mime_str(reference_image.mime_type.as_str())
|
||||
.map_err(|error| {
|
||||
map_puzzle_vector_engine_request_error(format!(
|
||||
"构造拼图 VectorEngine 图片编辑参考图失败:{error}"
|
||||
))
|
||||
})?;
|
||||
let form = reqwest::multipart::Form::new()
|
||||
.part("image", image_part)
|
||||
.text("model", PUZZLE_VECTOR_ENGINE_IMAGE_EDIT_MODEL.to_string())
|
||||
.text(
|
||||
"prompt",
|
||||
build_puzzle_vector_engine_prompt(prompt, negative_prompt),
|
||||
)
|
||||
.text("n", candidate_count.clamp(1, 1).to_string())
|
||||
.text("size", size.to_string());
|
||||
let request_started_at = Instant::now();
|
||||
let response = http_client
|
||||
.post(request_url.as_str())
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
format!("Bearer {}", settings.api_key),
|
||||
)
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
map_puzzle_vector_engine_request_error(format!(
|
||||
"创建拼图 VectorEngine 图片编辑任务失败:{error}"
|
||||
))
|
||||
})?;
|
||||
let status = response.status();
|
||||
tracing::info!(
|
||||
provider = VECTOR_ENGINE_PROVIDER,
|
||||
image_model = PUZZLE_VECTOR_ENGINE_IMAGE_EDIT_MODEL,
|
||||
endpoint = %request_url,
|
||||
status = status.as_u16(),
|
||||
prompt_chars = prompt.chars().count(),
|
||||
size,
|
||||
reference_mime = %reference_image.mime_type,
|
||||
reference_bytes = reference_image.bytes_len,
|
||||
elapsed_ms = request_started_at.elapsed().as_millis() as u64,
|
||||
"拼图 VectorEngine 图片编辑 HTTP 返回"
|
||||
);
|
||||
let response_text = response.text().await.map_err(|error| {
|
||||
map_puzzle_vector_engine_request_error(format!(
|
||||
"读取拼图 VectorEngine 图片编辑响应失败:{error}"
|
||||
))
|
||||
})?;
|
||||
if !status.is_success() {
|
||||
return Err(map_puzzle_vector_engine_upstream_error(
|
||||
status,
|
||||
response_text.as_str(),
|
||||
"创建拼图 VectorEngine 图片编辑任务失败",
|
||||
));
|
||||
}
|
||||
|
||||
let payload = parse_puzzle_json_payload(
|
||||
response_text.as_str(),
|
||||
"解析拼图 VectorEngine 图片编辑响应失败",
|
||||
)?;
|
||||
let image_urls = extract_puzzle_image_urls(&payload);
|
||||
if !image_urls.is_empty() {
|
||||
return download_puzzle_images_from_urls(http_client, task_id, image_urls, candidate_count)
|
||||
.await;
|
||||
}
|
||||
let b64_images = extract_puzzle_b64_images(&payload);
|
||||
if !b64_images.is_empty() {
|
||||
return Ok(puzzle_images_from_base64(
|
||||
task_id,
|
||||
b64_images,
|
||||
candidate_count,
|
||||
));
|
||||
}
|
||||
|
||||
Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": "拼图 VectorEngine 图片编辑未返回图片",
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_puzzle_vector_engine_image_request_body(
|
||||
image_model: PuzzleImageModel,
|
||||
prompt: &str,
|
||||
negative_prompt: &str,
|
||||
size: &str,
|
||||
candidate_count: u32,
|
||||
reference_image: Option<&str>,
|
||||
) -> Value {
|
||||
let mut body = Map::from_iter([
|
||||
Value::Object(Map::from_iter([
|
||||
(
|
||||
"model".to_string(),
|
||||
Value::String(image_model.request_model_name().to_string()),
|
||||
@@ -4830,16 +5009,37 @@ fn build_puzzle_vector_engine_image_request_body(
|
||||
),
|
||||
("n".to_string(), json!(candidate_count.clamp(1, 1))),
|
||||
("size".to_string(), Value::String(size.to_string())),
|
||||
]);
|
||||
]))
|
||||
}
|
||||
|
||||
if let Some(reference_image) = reference_image
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
body.insert("image".to_string(), json!([reference_image]));
|
||||
fn build_puzzle_vector_engine_generation_prompt(prompt: &str, has_reference_image: bool) -> String {
|
||||
let prompt = prompt.trim();
|
||||
if !has_reference_image {
|
||||
return prompt.to_string();
|
||||
}
|
||||
|
||||
Value::Object(body)
|
||||
format!(
|
||||
concat!(
|
||||
"请以随请求提供的参考图作为第一优先级生成依据,严格保留参考图的主要主体、构图关系、视角、姿态、配色和光影氛围;",
|
||||
"允许按下面文字要求做风格化和细节增强,但不要改成与参考图无关的新画面。\n",
|
||||
"{prompt}"
|
||||
),
|
||||
prompt = prompt,
|
||||
)
|
||||
}
|
||||
|
||||
fn has_puzzle_reference_image(reference_image_src: Option<&str>) -> bool {
|
||||
reference_image_src
|
||||
.map(str::trim)
|
||||
.map(|value| !value.is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn should_use_puzzle_reference_image_edit(
|
||||
reference_image_src: Option<&str>,
|
||||
use_reference_image_edit: bool,
|
||||
) -> bool {
|
||||
use_reference_image_edit && has_puzzle_reference_image(reference_image_src)
|
||||
}
|
||||
|
||||
fn build_puzzle_vector_engine_prompt(prompt: &str, negative_prompt: &str) -> String {
|
||||
@@ -4860,6 +5060,14 @@ fn puzzle_vector_engine_images_generation_url(settings: &PuzzleVectorEngineSetti
|
||||
}
|
||||
}
|
||||
|
||||
fn puzzle_vector_engine_images_edit_url(settings: &PuzzleVectorEngineSettings) -> String {
|
||||
if settings.base_url.ends_with("/v1") {
|
||||
format!("{}/images/edits", settings.base_url)
|
||||
} else {
|
||||
format!("{}/v1/images/edits", settings.base_url)
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_puzzle_images_from_urls(
|
||||
http_client: &reqwest::Client,
|
||||
task_id: String,
|
||||
@@ -4894,15 +5102,21 @@ async fn resolve_puzzle_reference_image_as_data_url(
|
||||
|
||||
if let Some(parsed) = parse_puzzle_image_data_url(trimmed) {
|
||||
let bytes_len = parsed.bytes.len();
|
||||
let data_url = format!(
|
||||
"data:{};base64,{}",
|
||||
parsed.mime_type,
|
||||
BASE64_STANDARD.encode(&parsed.bytes)
|
||||
);
|
||||
if bytes_len > PUZZLE_REFERENCE_IMAGE_MAX_BYTES {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "puzzle",
|
||||
"field": "referenceImageSrc",
|
||||
"message": "参考图过大,请压缩后重试。",
|
||||
"maxBytes": PUZZLE_REFERENCE_IMAGE_MAX_BYTES,
|
||||
"actualBytes": bytes_len,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return Ok(PuzzleResolvedReferenceImage {
|
||||
data_url,
|
||||
mime_type: parsed.mime_type,
|
||||
bytes_len,
|
||||
bytes: parsed.bytes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4976,9 +5190,9 @@ async fn resolve_puzzle_reference_image_as_data_url(
|
||||
let mime_type = normalize_puzzle_downloaded_image_mime_type(content_type.as_str());
|
||||
let bytes_len = body.len();
|
||||
Ok(PuzzleResolvedReferenceImage {
|
||||
data_url: format!("data:{};base64,{}", mime_type, BASE64_STANDARD.encode(body)),
|
||||
mime_type,
|
||||
bytes_len,
|
||||
bytes: body.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5228,6 +5442,36 @@ fn extract_puzzle_image_urls(payload: &Value) -> Vec<String> {
|
||||
deduped
|
||||
}
|
||||
|
||||
fn extract_puzzle_b64_images(payload: &Value) -> Vec<String> {
|
||||
let mut values = Vec::new();
|
||||
collect_puzzle_strings_by_key(payload, "b64_json", &mut values);
|
||||
values
|
||||
}
|
||||
|
||||
fn puzzle_images_from_base64(
|
||||
task_id: String,
|
||||
b64_images: Vec<String>,
|
||||
candidate_count: u32,
|
||||
) -> PuzzleGeneratedImages {
|
||||
let images = b64_images
|
||||
.into_iter()
|
||||
.take(candidate_count.clamp(1, 1) as usize)
|
||||
.filter_map(|raw| decode_puzzle_generated_image_base64(raw.as_str()))
|
||||
.collect();
|
||||
|
||||
PuzzleGeneratedImages { task_id, images }
|
||||
}
|
||||
|
||||
fn decode_puzzle_generated_image_base64(raw: &str) -> Option<PuzzleDownloadedImage> {
|
||||
let bytes = BASE64_STANDARD.decode(raw.trim()).ok()?;
|
||||
let mime_type = infer_puzzle_image_mime_type(bytes.as_slice());
|
||||
Some(PuzzleDownloadedImage {
|
||||
extension: puzzle_mime_to_extension(mime_type.as_str()).to_string(),
|
||||
mime_type,
|
||||
bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn find_first_puzzle_string_by_key(payload: &Value, target_key: &str) -> Option<String> {
|
||||
let mut results = Vec::new();
|
||||
collect_puzzle_strings_by_key(payload, target_key, &mut results);
|
||||
@@ -5265,6 +5509,22 @@ fn collect_puzzle_string_values(payload: &Value, results: &mut Vec<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_puzzle_image_mime_type(bytes: &[u8]) -> String {
|
||||
if bytes.starts_with(b"\x89PNG\r\n\x1A\n") {
|
||||
return "image/png".to_string();
|
||||
}
|
||||
if bytes.starts_with(b"\xFF\xD8\xFF") {
|
||||
return "image/jpeg".to_string();
|
||||
}
|
||||
if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WEBP") {
|
||||
return "image/webp".to_string();
|
||||
}
|
||||
if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
|
||||
return "image/gif".to_string();
|
||||
}
|
||||
"image/png".to_string()
|
||||
}
|
||||
|
||||
fn normalize_puzzle_downloaded_image_mime_type(content_type: &str) -> String {
|
||||
let mime_type = content_type
|
||||
.split(';')
|
||||
|
||||
Reference in New Issue
Block a user