feat: refine wooden fish runtime generation
This commit is contained in:
@@ -28,14 +28,15 @@ use spacetime_client::SpacetimeClientError;
|
||||
use crate::generated_image_assets::{
|
||||
GeneratedImageAssetAdapter, GeneratedImageAssetDataUrl,
|
||||
adapter::GeneratedImageAssetAdapterMetadata, adapter::GeneratedImageAssetPersistInput,
|
||||
normalize_generated_image_asset_mime,
|
||||
decode_generated_image_asset_data_url, normalize_generated_image_asset_mime,
|
||||
};
|
||||
use crate::{
|
||||
api_response::json_success_body,
|
||||
auth::AuthenticatedAccessToken,
|
||||
http_error::AppError,
|
||||
openai_image_generation::{
|
||||
DownloadedOpenAiImage, build_openai_image_http_client, create_openai_image_generation,
|
||||
DownloadedOpenAiImage, OpenAiReferenceImage, build_openai_image_http_client,
|
||||
create_openai_image_edit, create_openai_image_edit_with_references,
|
||||
require_openai_image_settings,
|
||||
},
|
||||
platform_errors::map_oss_error,
|
||||
@@ -58,9 +59,15 @@ const DEFAULT_HIT_OBJECT_IMAGE_SRC: &str = "/wooden-fish/default-hit-object.png"
|
||||
const WOODEN_FISH_ENTITY_KIND: &str = "wooden_fish_work";
|
||||
const WOODEN_FISH_HIT_OBJECT_SLOT: &str = "hit_object";
|
||||
const WOODEN_FISH_HIT_OBJECT_ASSET_KIND: &str = "wooden_fish_hit_object";
|
||||
const WOODEN_FISH_BACKGROUND_SLOT: &str = "background";
|
||||
const WOODEN_FISH_BACKGROUND_ASSET_KIND: &str = "wooden_fish_background";
|
||||
const WOODEN_FISH_HIT_SOUND_SLOT: &str = "hit_sound";
|
||||
const WOODEN_FISH_HIT_SOUND_ASSET_KIND: &str = "wooden_fish_hit_sound";
|
||||
const WOODEN_FISH_HIT_SOUND_DURATION_SECONDS: u8 = 3;
|
||||
const DEFAULT_HIT_OBJECT_REFERENCE_BYTES: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../../public/wooden-fish/default-hit-object.png"
|
||||
));
|
||||
|
||||
pub async fn create_wooden_fish_session(
|
||||
State(state): State<AppState>,
|
||||
@@ -372,6 +379,7 @@ fn build_wooden_fish_draft(payload: &WoodenFishWorkspaceCreateRequest) -> Wooden
|
||||
.or_else(|| Some(DEFAULT_HIT_SOUND_PROMPT.to_string())),
|
||||
floating_words: normalize_floating_words(payload.floating_words.clone()),
|
||||
hit_object_asset: None,
|
||||
background_asset: None,
|
||||
hit_sound_asset: payload.hit_sound_asset.clone(),
|
||||
cover_image_src: None,
|
||||
generation_status: WoodenFishGenerationStatus::Draft,
|
||||
@@ -410,7 +418,7 @@ async fn maybe_generate_hit_object_asset(
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
if payload.hit_object_asset.is_some() {
|
||||
if payload.hit_object_asset.is_some() && payload.background_asset.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -424,32 +432,21 @@ async fn maybe_generate_hit_object_asset(
|
||||
.map(|value| clean_string(value, DEFAULT_HIT_OBJECT_PROMPT))
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| DEFAULT_HIT_OBJECT_PROMPT.to_string());
|
||||
let reference_images = payload
|
||||
.hit_object_reference_image_src
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| vec![value.to_string()])
|
||||
.unwrap_or_default();
|
||||
|
||||
if reference_images.is_empty() && is_default_hit_object_prompt(prompt.as_str()) {
|
||||
payload.hit_object_asset = Some(default_wooden_fish_hit_object_asset());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let asset = generate_wooden_fish_hit_object_asset(
|
||||
let generated = generate_wooden_fish_image_assets(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id.as_str(),
|
||||
prompt.as_str(),
|
||||
reference_images.as_slice(),
|
||||
payload.hit_object_reference_image_src.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
wooden_fish_error_response(request_context, WOODEN_FISH_CREATION_PROVIDER, error)
|
||||
})?;
|
||||
payload.hit_object_asset = Some(asset);
|
||||
payload.hit_object_asset = Some(generated.hit_object_asset);
|
||||
payload.background_asset = Some(generated.background_asset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -469,8 +466,7 @@ fn default_wooden_fish_hit_object_asset() -> WoodenFishImageAsset {
|
||||
fn is_default_hit_object_prompt(prompt: &str) -> bool {
|
||||
let normalized = normalize_hit_object_prompt_for_default_match(prompt);
|
||||
normalized.is_empty()
|
||||
|| normalized
|
||||
== normalize_hit_object_prompt_for_default_match(DEFAULT_HIT_OBJECT_PROMPT)
|
||||
|| normalized == normalize_hit_object_prompt_for_default_match(DEFAULT_HIT_OBJECT_PROMPT)
|
||||
|| normalized
|
||||
== normalize_hit_object_prompt_for_default_match("卡通木鱼,圆润可爱,透明背景")
|
||||
|| normalized
|
||||
@@ -655,64 +651,229 @@ fn map_generated_creation_audio_to_wooden_fish_asset(
|
||||
})
|
||||
}
|
||||
|
||||
async fn generate_wooden_fish_hit_object_asset(
|
||||
struct WoodenFishGeneratedImageAssets {
|
||||
hit_object_asset: WoodenFishImageAsset,
|
||||
background_asset: WoodenFishImageAsset,
|
||||
}
|
||||
|
||||
async fn generate_wooden_fish_image_assets(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
session_id: &str,
|
||||
profile_id: &str,
|
||||
prompt: &str,
|
||||
reference_images: &[String],
|
||||
) -> Result<WoodenFishImageAsset, AppError> {
|
||||
hit_object_reference_image_src: Option<&str>,
|
||||
) -> Result<WoodenFishGeneratedImageAssets, AppError> {
|
||||
let settings = require_openai_image_settings(state)?;
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
let final_prompt = build_wooden_fish_hit_object_prompt(prompt);
|
||||
let generated = create_openai_image_generation(
|
||||
let clean_reference_image_src = hit_object_reference_image_src
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let theme = resolve_wooden_fish_generation_theme(prompt, clean_reference_image_src);
|
||||
let default_reference_image = default_wooden_fish_reference_image()?;
|
||||
let theme_reference_image =
|
||||
resolve_wooden_fish_theme_reference_image(clean_reference_image_src)?;
|
||||
|
||||
let (hit_object_asset, background_reference_image) =
|
||||
if should_generate_wooden_fish_hit_object(prompt, clean_reference_image_src) {
|
||||
let hit_object_prompt = build_wooden_fish_hit_object_prompt(theme.as_str());
|
||||
let mut reference_images = vec![default_reference_image.clone()];
|
||||
if let Some(reference_image) = theme_reference_image {
|
||||
reference_images.push(reference_image);
|
||||
}
|
||||
let generated = create_openai_image_edit_with_references(
|
||||
&http_client,
|
||||
&settings,
|
||||
hit_object_prompt.as_str(),
|
||||
None,
|
||||
"1:1",
|
||||
reference_images.as_slice(),
|
||||
"生成敲木鱼敲击物图案失败",
|
||||
)
|
||||
.await?;
|
||||
let task_id = generated.task_id.clone();
|
||||
let image = generated.images.into_iter().next().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "生成敲木鱼敲击物图案失败:上游未返回图片",
|
||||
}))
|
||||
})?;
|
||||
let background_reference_image =
|
||||
downloaded_wooden_fish_reference_image(&image, "wooden-fish-generated-hit-object");
|
||||
let hit_object_asset = persist_wooden_fish_image_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
task_id.as_str(),
|
||||
hit_object_prompt.as_str(),
|
||||
image,
|
||||
current_utc_micros(),
|
||||
WoodenFishImageSlotPersistSpec {
|
||||
slot: WOODEN_FISH_HIT_OBJECT_SLOT,
|
||||
asset_kind: WOODEN_FISH_HIT_OBJECT_ASSET_KIND,
|
||||
asset_id_part: "hit-object",
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
(hit_object_asset, background_reference_image)
|
||||
} else {
|
||||
(
|
||||
default_wooden_fish_hit_object_asset(),
|
||||
default_reference_image,
|
||||
)
|
||||
};
|
||||
|
||||
let background_prompt = build_wooden_fish_background_prompt(theme.as_str());
|
||||
let background_generated = create_openai_image_edit(
|
||||
&http_client,
|
||||
&settings,
|
||||
final_prompt.as_str(),
|
||||
Some(build_wooden_fish_hit_object_negative_prompt().as_str()),
|
||||
"1024x1024",
|
||||
1,
|
||||
reference_images,
|
||||
"生成敲木鱼敲击物图案失败",
|
||||
background_prompt.as_str(),
|
||||
None,
|
||||
"9:16",
|
||||
&background_reference_image,
|
||||
"生成敲木鱼背景环境图失败",
|
||||
)
|
||||
.await?;
|
||||
let task_id = generated.task_id.clone();
|
||||
let image = generated.images.into_iter().next().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "生成敲木鱼敲击物图案失败:上游未返回图片",
|
||||
}))
|
||||
})?;
|
||||
let generated_at_micros = current_utc_micros();
|
||||
let persisted = persist_wooden_fish_hit_object_asset(
|
||||
let background_task_id = background_generated.task_id.clone();
|
||||
let background_image = background_generated
|
||||
.images
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "生成敲木鱼背景环境图失败:上游未返回图片",
|
||||
}))
|
||||
})?;
|
||||
let background_asset = persist_wooden_fish_image_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
task_id.as_str(),
|
||||
&final_prompt,
|
||||
image,
|
||||
generated_at_micros,
|
||||
background_task_id.as_str(),
|
||||
background_prompt.as_str(),
|
||||
background_image,
|
||||
current_utc_micros(),
|
||||
WoodenFishImageSlotPersistSpec {
|
||||
slot: WOODEN_FISH_BACKGROUND_SLOT,
|
||||
asset_kind: WOODEN_FISH_BACKGROUND_ASSET_KIND,
|
||||
asset_id_part: "background",
|
||||
width: 1024,
|
||||
height: 1536,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(persisted)
|
||||
Ok(WoodenFishGeneratedImageAssets {
|
||||
hit_object_asset,
|
||||
background_asset,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_wooden_fish_hit_object_prompt(prompt: &str) -> String {
|
||||
format!(
|
||||
"请使用 gpt-image-2 生成一个适合点击敲击玩法的单个物品图案:{}。画面要求:单个主体,卡通插画风格,透明或纯净浅色背景,居中构图,圆润可爱,边缘清晰,适合移动端屏幕中央展示和点击动画缩放。不要包含文字、按钮、UI、边框、水印、品牌标识或人物手部。",
|
||||
"生成敲木鱼新样式,要求结构,画风与参考图保持高度一致,新样式颜色搭配使用新主题对应的颜色。\n新主题为:{}",
|
||||
clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT)
|
||||
)
|
||||
}
|
||||
|
||||
fn build_wooden_fish_hit_object_negative_prompt() -> String {
|
||||
"不要生成文字、Logo、水印、按钮、界面截图、复杂背景、多个主体、真实摄影质感、恐怖或血腥元素。"
|
||||
.to_string()
|
||||
fn build_wooden_fish_background_prompt(prompt: &str) -> String {
|
||||
format!(
|
||||
"生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。\n主题为:{}",
|
||||
clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT)
|
||||
)
|
||||
}
|
||||
|
||||
async fn persist_wooden_fish_hit_object_asset(
|
||||
fn should_generate_wooden_fish_hit_object(
|
||||
prompt: &str,
|
||||
hit_object_reference_image_src: Option<&str>,
|
||||
) -> bool {
|
||||
hit_object_reference_image_src.is_some() || !is_default_hit_object_prompt(prompt)
|
||||
}
|
||||
|
||||
fn resolve_wooden_fish_generation_theme(
|
||||
prompt: &str,
|
||||
hit_object_reference_image_src: Option<&str>,
|
||||
) -> String {
|
||||
let prompt = clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT);
|
||||
if !is_default_hit_object_prompt(prompt.as_str()) {
|
||||
return prompt;
|
||||
}
|
||||
if hit_object_reference_image_src.is_some() {
|
||||
return "用户提供参考图".to_string();
|
||||
}
|
||||
prompt
|
||||
}
|
||||
|
||||
fn default_wooden_fish_reference_image() -> Result<OpenAiReferenceImage, AppError> {
|
||||
let bytes = DEFAULT_HIT_OBJECT_REFERENCE_BYTES.to_vec();
|
||||
if bytes.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
"provider": WOODEN_FISH_CREATION_PROVIDER,
|
||||
"message": "敲木鱼默认参考图为空",
|
||||
})),
|
||||
);
|
||||
}
|
||||
Ok(OpenAiReferenceImage {
|
||||
bytes,
|
||||
mime_type: "image/png".to_string(),
|
||||
file_name: "wooden-fish-default-hit-object-reference.png".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_wooden_fish_theme_reference_image(
|
||||
source: Option<&str>,
|
||||
) -> Result<Option<OpenAiReferenceImage>, AppError> {
|
||||
let Some(source) = source.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !source.to_ascii_lowercase().starts_with("data:image/") {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": WOODEN_FISH_CREATION_PROVIDER,
|
||||
"field": "hitObjectReferenceImageSrc",
|
||||
"message": "敲木鱼参考图必须是 base64 图片 Data URL。",
|
||||
})),
|
||||
);
|
||||
}
|
||||
let decoded = decode_generated_image_asset_data_url(source).map_err(|_| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": WOODEN_FISH_CREATION_PROVIDER,
|
||||
"field": "hitObjectReferenceImageSrc",
|
||||
"message": "敲木鱼参考图必须是 base64 图片 Data URL。",
|
||||
}))
|
||||
})?;
|
||||
Ok(Some(OpenAiReferenceImage {
|
||||
file_name: format!("wooden-fish-theme-reference.{}", decoded.format.extension),
|
||||
mime_type: decoded.format.mime_type,
|
||||
bytes: decoded.bytes,
|
||||
}))
|
||||
}
|
||||
|
||||
fn downloaded_wooden_fish_reference_image(
|
||||
image: &DownloadedOpenAiImage,
|
||||
file_name_stem: &str,
|
||||
) -> OpenAiReferenceImage {
|
||||
OpenAiReferenceImage {
|
||||
bytes: image.bytes.clone(),
|
||||
mime_type: image.mime_type.clone(),
|
||||
file_name: format!("{file_name_stem}.{}", image.extension),
|
||||
}
|
||||
}
|
||||
|
||||
struct WoodenFishImageSlotPersistSpec {
|
||||
slot: &'static str,
|
||||
asset_kind: &'static str,
|
||||
asset_id_part: &'static str,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
async fn persist_wooden_fish_image_asset(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
session_id: &str,
|
||||
@@ -721,6 +882,7 @@ async fn persist_wooden_fish_hit_object_asset(
|
||||
prompt: &str,
|
||||
image: DownloadedOpenAiImage,
|
||||
generated_at_micros: i64,
|
||||
spec: WoodenFishImageSlotPersistSpec,
|
||||
) -> Result<WoodenFishImageAsset, AppError> {
|
||||
let oss_client = state.oss_client().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||||
@@ -735,7 +897,7 @@ async fn persist_wooden_fish_hit_object_asset(
|
||||
path_segments: vec![
|
||||
sanitize_wooden_fish_asset_segment(session_id, "session"),
|
||||
sanitize_wooden_fish_asset_segment(profile_id, "profile"),
|
||||
WOODEN_FISH_HIT_OBJECT_SLOT.to_string(),
|
||||
spec.slot.to_string(),
|
||||
format!("asset-{generated_at_micros}"),
|
||||
],
|
||||
file_stem: "image".to_string(),
|
||||
@@ -745,11 +907,11 @@ async fn persist_wooden_fish_hit_object_asset(
|
||||
},
|
||||
access: OssObjectAccess::Private,
|
||||
metadata: GeneratedImageAssetAdapterMetadata {
|
||||
asset_kind: Some(WOODEN_FISH_HIT_OBJECT_ASSET_KIND.to_string()),
|
||||
asset_kind: Some(spec.asset_kind.to_string()),
|
||||
owner_user_id: Some(owner_user_id.to_string()),
|
||||
entity_kind: Some(WOODEN_FISH_ENTITY_KIND.to_string()),
|
||||
entity_id: Some(profile_id.to_string()),
|
||||
slot: Some(WOODEN_FISH_HIT_OBJECT_SLOT.to_string()),
|
||||
slot: Some(spec.slot.to_string()),
|
||||
provider: Some("image2".to_string()),
|
||||
task_id: Some(task_id.to_string()),
|
||||
},
|
||||
@@ -784,7 +946,7 @@ async fn persist_wooden_fish_hit_object_asset(
|
||||
head.content_type.or(Some(persisted_mime_type)),
|
||||
head.content_length,
|
||||
head.etag,
|
||||
WOODEN_FISH_HIT_OBJECT_ASSET_KIND.to_string(),
|
||||
spec.asset_kind.to_string(),
|
||||
Some(task_id.to_string()),
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
@@ -808,8 +970,8 @@ async fn persist_wooden_fish_hit_object_asset(
|
||||
asset_object.asset_object_id.clone(),
|
||||
WOODEN_FISH_ENTITY_KIND.to_string(),
|
||||
profile_id.to_string(),
|
||||
WOODEN_FISH_HIT_OBJECT_SLOT.to_string(),
|
||||
WOODEN_FISH_HIT_OBJECT_ASSET_KIND.to_string(),
|
||||
spec.slot.to_string(),
|
||||
spec.asset_kind.to_string(),
|
||||
Some(owner_user_id.to_string()),
|
||||
Some(profile_id.to_string()),
|
||||
generated_at_micros,
|
||||
@@ -823,20 +985,21 @@ async fn persist_wooden_fish_hit_object_asset(
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
slot = spec.slot,
|
||||
error = %error,
|
||||
"敲木鱼图片资产绑定失败,历史素材索引可能缺少绑定记录"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(WoodenFishImageAsset {
|
||||
asset_id: format!("{profile_id}-hit-object-{generated_at_micros}"),
|
||||
asset_id: format!("{profile_id}-{}-{generated_at_micros}", spec.asset_id_part),
|
||||
image_src: put_result.legacy_public_path,
|
||||
image_object_key: head.object_key,
|
||||
asset_object_id: asset_object.asset_object_id,
|
||||
generation_provider: "image2".to_string(),
|
||||
prompt: prompt.to_string(),
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
width: spec.width,
|
||||
height: spec.height,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1027,15 +1190,51 @@ fn current_utc_micros() -> i64 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_hit_object_prompt_keeps_user_object_and_image2_constraints() {
|
||||
fn wooden_fish_hit_object_prompt_uses_hidden_image2_flow() {
|
||||
let prompt = build_wooden_fish_hit_object_prompt("赛博莲花木鱼");
|
||||
|
||||
assert!(prompt.contains("赛博莲花木鱼"));
|
||||
assert!(prompt.contains("gpt-image-2"));
|
||||
assert!(prompt.contains("单个主体"));
|
||||
assert!(prompt.contains("不要包含文字"));
|
||||
assert_eq!(
|
||||
prompt,
|
||||
"生成敲木鱼新样式,要求结构,画风与参考图保持高度一致,新样式颜色搭配使用新主题对应的颜色。\n新主题为:赛博莲花木鱼"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_background_prompt_uses_hidden_image2_flow() {
|
||||
let prompt = build_wooden_fish_background_prompt("赛博莲花木鱼");
|
||||
|
||||
assert_eq!(
|
||||
prompt,
|
||||
"生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。\n主题为:赛博莲花木鱼"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_theme_reference_image_decodes_data_url_for_image2() {
|
||||
let source = format!(
|
||||
"data:image/png;base64,{}",
|
||||
BASE64_STANDARD.encode(b"\x89PNG\r\n\x1A\nreference")
|
||||
);
|
||||
|
||||
let image = resolve_wooden_fish_theme_reference_image(Some(source.as_str()))
|
||||
.expect("data url should parse")
|
||||
.expect("reference image should exist");
|
||||
|
||||
assert_eq!(image.mime_type, "image/png");
|
||||
assert_eq!(image.file_name, "wooden-fish-theme-reference.png");
|
||||
assert!(image.bytes.starts_with(b"\x89PNG\r\n\x1A\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wooden_fish_theme_reference_image_rejects_non_data_url() {
|
||||
let error = resolve_wooden_fish_theme_reference_image(Some("/generated/example.png"))
|
||||
.expect_err("legacy path should not be accepted as direct image2 reference");
|
||||
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert!(error.body_text().contains("Data URL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1053,7 +1252,9 @@ mod tests {
|
||||
fn wooden_fish_default_prompt_matches_legacy_defaults() {
|
||||
assert!(is_default_hit_object_prompt(DEFAULT_HIT_OBJECT_PROMPT));
|
||||
assert!(is_default_hit_object_prompt("卡通木鱼,圆润可爱,透明背景"));
|
||||
assert!(is_default_hit_object_prompt("卡通木鱼,透明背景,居中,圆润可爱"));
|
||||
assert!(is_default_hit_object_prompt(
|
||||
"卡通木鱼,透明背景,居中,圆润可爱"
|
||||
));
|
||||
assert!(is_default_hit_object_prompt("卡通木鱼"));
|
||||
assert!(!is_default_hit_object_prompt("赛博莲花木鱼"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user