700 lines
25 KiB
Rust
700 lines
25 KiB
Rust
use super::*;
|
|
|
|
pub(super) async fn generate_square_hole_visual_assets_for_session(
|
|
state: &AppState,
|
|
request_context: &RequestContext,
|
|
authenticated: &AuthenticatedAccessToken,
|
|
session_id: String,
|
|
regenerate_visual_assets: bool,
|
|
visual_asset_slot: Option<String>,
|
|
visual_asset_option_id: Option<String>,
|
|
) -> Result<SquareHoleAgentSessionRecord, Response> {
|
|
let owner_user_id = authenticated.claims().user_id().to_string();
|
|
let session = state
|
|
.spacetime_client()
|
|
.get_square_hole_agent_session(session_id.clone(), owner_user_id.clone())
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(
|
|
request_context,
|
|
SQUARE_HOLE_AGENT_PROVIDER,
|
|
map_square_hole_client_error(error),
|
|
)
|
|
})?;
|
|
let profile_id = session
|
|
.draft
|
|
.as_ref()
|
|
.map(|draft| draft.profile_id.clone())
|
|
.ok_or_else(|| {
|
|
square_hole_bad_request(
|
|
request_context,
|
|
SQUARE_HOLE_AGENT_PROVIDER,
|
|
"square hole 草稿尚未编译,不能生成图片资产",
|
|
)
|
|
})?;
|
|
let mut work = state
|
|
.spacetime_client()
|
|
.get_square_hole_work_detail(profile_id.clone(), owner_user_id.clone())
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(
|
|
request_context,
|
|
SQUARE_HOLE_AGENT_PROVIDER,
|
|
map_square_hole_client_error(error),
|
|
)
|
|
})?;
|
|
|
|
let requested_slot = normalize_square_hole_visual_asset_slot(
|
|
visual_asset_slot.as_deref(),
|
|
visual_asset_option_id.as_deref(),
|
|
);
|
|
|
|
let cover_image_src = match work.cover_image_src.clone() {
|
|
Some(value)
|
|
if !should_generate_square_hole_cover_image(
|
|
requested_slot.as_ref(),
|
|
regenerate_visual_assets,
|
|
value.as_str(),
|
|
) =>
|
|
{
|
|
Some(value)
|
|
}
|
|
_ => Some(
|
|
generate_square_hole_image_data_url(
|
|
state,
|
|
request_context,
|
|
&owner_user_id,
|
|
&session_id,
|
|
profile_id.as_str(),
|
|
"cover",
|
|
SQUARE_HOLE_COVER_IMAGE_KIND,
|
|
build_square_hole_cover_prompt(&work).as_str(),
|
|
"16:9",
|
|
"生成方洞挑战封面图失败",
|
|
)
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(request_context, SQUARE_HOLE_AGENT_PROVIDER, error)
|
|
})?,
|
|
),
|
|
};
|
|
let background_image_src = match work.background_image_src.clone() {
|
|
Some(value)
|
|
if !should_generate_square_hole_background_image(
|
|
requested_slot.as_ref(),
|
|
regenerate_visual_assets,
|
|
value.as_str(),
|
|
) =>
|
|
{
|
|
Some(value)
|
|
}
|
|
_ => Some(
|
|
generate_square_hole_image_data_url(
|
|
state,
|
|
request_context,
|
|
&owner_user_id,
|
|
&session_id,
|
|
profile_id.as_str(),
|
|
"background",
|
|
SQUARE_HOLE_BACKGROUND_IMAGE_KIND,
|
|
build_square_hole_background_prompt(&work).as_str(),
|
|
"16:9",
|
|
"生成方洞挑战背景图失败",
|
|
)
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(request_context, SQUARE_HOLE_AGENT_PROVIDER, error)
|
|
})?,
|
|
),
|
|
};
|
|
let mut shape_options = work.shape_options.clone();
|
|
let prompt_work = work.clone();
|
|
for option in shape_options.iter_mut() {
|
|
if !should_generate_square_hole_shape_image(
|
|
requested_slot.as_ref(),
|
|
regenerate_visual_assets,
|
|
option,
|
|
) {
|
|
continue;
|
|
}
|
|
option.image_src = Some(
|
|
generate_square_hole_image_data_url(
|
|
state,
|
|
request_context,
|
|
&owner_user_id,
|
|
&session_id,
|
|
profile_id.as_str(),
|
|
option.option_id.as_str(),
|
|
SQUARE_HOLE_SHAPE_IMAGE_KIND,
|
|
build_square_hole_shape_prompt(&prompt_work, option).as_str(),
|
|
"1:1",
|
|
"生成方洞挑战形状贴图失败",
|
|
)
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(request_context, SQUARE_HOLE_AGENT_PROVIDER, error)
|
|
})?,
|
|
);
|
|
}
|
|
let mut hole_options = work.hole_options.clone();
|
|
for option in hole_options.iter_mut() {
|
|
if !should_generate_square_hole_hole_image(
|
|
requested_slot.as_ref(),
|
|
regenerate_visual_assets,
|
|
option,
|
|
) {
|
|
continue;
|
|
}
|
|
option.image_src = Some(
|
|
generate_square_hole_image_data_url(
|
|
state,
|
|
request_context,
|
|
&owner_user_id,
|
|
&session_id,
|
|
profile_id.as_str(),
|
|
option.hole_id.as_str(),
|
|
SQUARE_HOLE_HOLE_IMAGE_KIND,
|
|
build_square_hole_hole_prompt(&prompt_work, option).as_str(),
|
|
"1:1",
|
|
"生成方洞挑战洞口贴图失败",
|
|
)
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(request_context, SQUARE_HOLE_AGENT_PROVIDER, error)
|
|
})?,
|
|
);
|
|
}
|
|
|
|
work = state
|
|
.spacetime_client()
|
|
.update_square_hole_work(SquareHoleWorkUpdateRecordInput {
|
|
profile_id,
|
|
owner_user_id: owner_user_id.clone(),
|
|
game_name: work.game_name.clone(),
|
|
theme_text: work.theme_text.clone(),
|
|
twist_rule: work.twist_rule.clone(),
|
|
summary_text: work.summary.clone(),
|
|
tags_json: serde_json::to_string(&normalize_tags(work.tags.clone()))
|
|
.unwrap_or_default(),
|
|
cover_image_src: cover_image_src.clone().unwrap_or_default(),
|
|
background_prompt: work.background_prompt.clone(),
|
|
background_image_src: background_image_src.clone().unwrap_or_default(),
|
|
shape_options_json: serialize_square_hole_shape_option_records(&shape_options),
|
|
hole_options_json: serialize_square_hole_hole_option_records(&hole_options),
|
|
shape_count: work.shape_count,
|
|
difficulty: work.difficulty,
|
|
updated_at_micros: current_utc_micros(),
|
|
})
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(
|
|
request_context,
|
|
SQUARE_HOLE_AGENT_PROVIDER,
|
|
map_square_hole_client_error(error),
|
|
)
|
|
})?;
|
|
|
|
let mut next_session = state
|
|
.spacetime_client()
|
|
.get_square_hole_agent_session(session_id, owner_user_id)
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(
|
|
request_context,
|
|
SQUARE_HOLE_AGENT_PROVIDER,
|
|
map_square_hole_client_error(error),
|
|
)
|
|
})?;
|
|
if let Some(draft) = next_session.draft.as_mut() {
|
|
draft.cover_image_src = work.cover_image_src.clone();
|
|
draft.background_image_src = work.background_image_src.clone();
|
|
draft.background_prompt = work.background_prompt.clone();
|
|
draft.shape_options = work.shape_options.clone();
|
|
draft.hole_options = work.hole_options.clone();
|
|
}
|
|
Ok(next_session)
|
|
}
|
|
|
|
pub(super) async fn regenerate_square_hole_visual_asset_for_work(
|
|
state: &AppState,
|
|
request_context: &RequestContext,
|
|
owner_user_id: String,
|
|
profile_id: String,
|
|
visual_asset_slot: String,
|
|
visual_asset_option_id: Option<String>,
|
|
) -> Result<SquareHoleWorkProfileRecord, Response> {
|
|
let mut work = state
|
|
.spacetime_client()
|
|
.get_square_hole_work_detail(profile_id.clone(), owner_user_id.clone())
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(
|
|
request_context,
|
|
SQUARE_HOLE_WORKS_PROVIDER,
|
|
map_square_hole_client_error(error),
|
|
)
|
|
})?;
|
|
let requested_slot = normalize_square_hole_visual_asset_slot(
|
|
Some(visual_asset_slot.as_str()),
|
|
visual_asset_option_id.as_deref(),
|
|
)
|
|
.ok_or_else(|| {
|
|
square_hole_bad_request(
|
|
request_context,
|
|
SQUARE_HOLE_WORKS_PROVIDER,
|
|
"图片槽位不存在",
|
|
)
|
|
})?;
|
|
let synthetic_session_id = work
|
|
.source_session_id
|
|
.clone()
|
|
.filter(|value| !value.trim().is_empty())
|
|
.unwrap_or_else(|| profile_id.clone());
|
|
let prompt_work = work.clone();
|
|
match &requested_slot {
|
|
SquareHoleVisualAssetSlotRequest::Cover => {
|
|
work.cover_image_src = Some(
|
|
generate_square_hole_image_data_url(
|
|
state,
|
|
request_context,
|
|
owner_user_id.as_str(),
|
|
synthetic_session_id.as_str(),
|
|
profile_id.as_str(),
|
|
"cover",
|
|
SQUARE_HOLE_COVER_IMAGE_KIND,
|
|
build_square_hole_cover_prompt(&prompt_work).as_str(),
|
|
"16:9",
|
|
"生成方洞挑战封面图失败",
|
|
)
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(request_context, SQUARE_HOLE_WORKS_PROVIDER, error)
|
|
})?,
|
|
);
|
|
}
|
|
SquareHoleVisualAssetSlotRequest::Background => {
|
|
work.background_image_src = Some(
|
|
generate_square_hole_image_data_url(
|
|
state,
|
|
request_context,
|
|
owner_user_id.as_str(),
|
|
synthetic_session_id.as_str(),
|
|
profile_id.as_str(),
|
|
"background",
|
|
SQUARE_HOLE_BACKGROUND_IMAGE_KIND,
|
|
build_square_hole_background_prompt(&prompt_work).as_str(),
|
|
"16:9",
|
|
"生成方洞挑战背景图失败",
|
|
)
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(request_context, SQUARE_HOLE_WORKS_PROVIDER, error)
|
|
})?,
|
|
);
|
|
}
|
|
SquareHoleVisualAssetSlotRequest::Shape(option_id) => {
|
|
let Some(option) = work
|
|
.shape_options
|
|
.iter_mut()
|
|
.find(|option| option.option_id == *option_id)
|
|
else {
|
|
return Err(square_hole_bad_request(
|
|
request_context,
|
|
SQUARE_HOLE_WORKS_PROVIDER,
|
|
"形状图片槽位不存在",
|
|
));
|
|
};
|
|
option.image_src = Some(
|
|
generate_square_hole_image_data_url(
|
|
state,
|
|
request_context,
|
|
owner_user_id.as_str(),
|
|
synthetic_session_id.as_str(),
|
|
profile_id.as_str(),
|
|
option.option_id.as_str(),
|
|
SQUARE_HOLE_SHAPE_IMAGE_KIND,
|
|
build_square_hole_shape_prompt(&prompt_work, option).as_str(),
|
|
"1:1",
|
|
"生成方洞挑战形状贴图失败",
|
|
)
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(request_context, SQUARE_HOLE_WORKS_PROVIDER, error)
|
|
})?,
|
|
);
|
|
}
|
|
SquareHoleVisualAssetSlotRequest::Hole(hole_id) => {
|
|
let Some(option) = work
|
|
.hole_options
|
|
.iter_mut()
|
|
.find(|option| option.hole_id == *hole_id)
|
|
else {
|
|
return Err(square_hole_bad_request(
|
|
request_context,
|
|
SQUARE_HOLE_WORKS_PROVIDER,
|
|
"洞口图片槽位不存在",
|
|
));
|
|
};
|
|
option.image_src = Some(
|
|
generate_square_hole_image_data_url(
|
|
state,
|
|
request_context,
|
|
owner_user_id.as_str(),
|
|
synthetic_session_id.as_str(),
|
|
profile_id.as_str(),
|
|
option.hole_id.as_str(),
|
|
SQUARE_HOLE_HOLE_IMAGE_KIND,
|
|
build_square_hole_hole_prompt(&prompt_work, option).as_str(),
|
|
"1:1",
|
|
"生成方洞挑战洞口贴图失败",
|
|
)
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(request_context, SQUARE_HOLE_WORKS_PROVIDER, error)
|
|
})?,
|
|
);
|
|
}
|
|
}
|
|
|
|
state
|
|
.spacetime_client()
|
|
.update_square_hole_work(SquareHoleWorkUpdateRecordInput {
|
|
profile_id,
|
|
owner_user_id,
|
|
game_name: work.game_name.clone(),
|
|
theme_text: work.theme_text.clone(),
|
|
twist_rule: work.twist_rule.clone(),
|
|
summary_text: work.summary.clone(),
|
|
tags_json: serde_json::to_string(&normalize_tags(work.tags.clone()))
|
|
.unwrap_or_default(),
|
|
cover_image_src: work.cover_image_src.clone().unwrap_or_default(),
|
|
background_prompt: work.background_prompt.clone(),
|
|
background_image_src: work.background_image_src.clone().unwrap_or_default(),
|
|
shape_options_json: serialize_square_hole_shape_option_records(&work.shape_options),
|
|
hole_options_json: serialize_square_hole_hole_option_records(&work.hole_options),
|
|
shape_count: work.shape_count,
|
|
difficulty: work.difficulty,
|
|
updated_at_micros: current_utc_micros(),
|
|
})
|
|
.await
|
|
.map_err(|error| {
|
|
square_hole_error_response(
|
|
request_context,
|
|
SQUARE_HOLE_WORKS_PROVIDER,
|
|
map_square_hole_client_error(error),
|
|
)
|
|
})
|
|
}
|
|
|
|
async fn generate_square_hole_image_data_url(
|
|
state: &AppState,
|
|
request_context: &RequestContext,
|
|
owner_user_id: &str,
|
|
session_id: &str,
|
|
profile_id: &str,
|
|
slot: &str,
|
|
asset_kind: &str,
|
|
prompt: &str,
|
|
size: &str,
|
|
failure_context: &str,
|
|
) -> Result<String, AppError> {
|
|
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
|
|
request_context,
|
|
Some(owner_user_id.to_string()),
|
|
Some(profile_id.to_string()),
|
|
);
|
|
let http_client = build_openai_image_http_client(&settings)?;
|
|
let generated = create_openai_image_generation(
|
|
&http_client,
|
|
&settings,
|
|
prompt,
|
|
Some(build_square_hole_negative_prompt().as_str()),
|
|
size,
|
|
1,
|
|
&[],
|
|
failure_context,
|
|
)
|
|
.await?;
|
|
let image = generated.images.into_iter().next().ok_or_else(|| {
|
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
|
"provider": "vector-engine",
|
|
"message": format!("{failure_context}:上游未返回图片"),
|
|
}))
|
|
})?;
|
|
|
|
let fallback_data_url = format_square_hole_data_url(&image);
|
|
match persist_square_hole_generated_asset(
|
|
state,
|
|
owner_user_id,
|
|
session_id,
|
|
profile_id,
|
|
slot,
|
|
asset_kind,
|
|
generated.task_id.as_str(),
|
|
image,
|
|
current_utc_micros(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(image_src) => Ok(image_src),
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
provider = "square-hole-assets",
|
|
owner_user_id,
|
|
session_id,
|
|
profile_id,
|
|
slot,
|
|
asset_kind,
|
|
message = %error.body_text(),
|
|
"方洞图片已生成但资产持久化失败,降级回写 Data URL"
|
|
);
|
|
Ok(fallback_data_url)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn format_square_hole_data_url(image: &DownloadedOpenAiImage) -> String {
|
|
format!(
|
|
"data:{};base64,{}",
|
|
image.mime_type,
|
|
BASE64_STANDARD.encode(&image.bytes)
|
|
)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn persist_square_hole_generated_asset(
|
|
state: &AppState,
|
|
owner_user_id: &str,
|
|
session_id: &str,
|
|
profile_id: &str,
|
|
slot: &str,
|
|
asset_kind: &str,
|
|
task_id: &str,
|
|
image: DownloadedOpenAiImage,
|
|
generated_at_micros: i64,
|
|
) -> Result<String, AppError> {
|
|
let oss_client = state.oss_client().ok_or_else(|| {
|
|
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
|
"provider": "aliyun-oss",
|
|
"reason": "OSS 未完成环境变量配置",
|
|
}))
|
|
})?;
|
|
let http_client = reqwest::Client::new();
|
|
let storage_slot = sanitize_square_hole_asset_segment(slot, "slot");
|
|
let prepared =
|
|
GeneratedImageAssetAdapter::prepare_put_object(GeneratedImageAssetPersistInput {
|
|
prefix: LegacyAssetPrefix::SquareHoleAssets,
|
|
path_segments: vec![
|
|
sanitize_square_hole_asset_segment(session_id, "session"),
|
|
sanitize_square_hole_asset_segment(profile_id, "profile"),
|
|
sanitize_square_hole_asset_segment(asset_kind, "asset"),
|
|
storage_slot.clone(),
|
|
format!("asset-{generated_at_micros}"),
|
|
],
|
|
file_stem: "image".to_string(),
|
|
image: GeneratedImageAssetDataUrl {
|
|
format: normalize_generated_image_asset_mime(image.mime_type.as_str()),
|
|
bytes: image.bytes,
|
|
},
|
|
access: OssObjectAccess::Private,
|
|
metadata: GeneratedImageAssetAdapterMetadata {
|
|
asset_kind: Some(asset_kind.to_string()),
|
|
owner_user_id: Some(owner_user_id.to_string()),
|
|
entity_kind: Some(SQUARE_HOLE_ENTITY_KIND.to_string()),
|
|
entity_id: Some(profile_id.to_string()),
|
|
slot: Some(slot.to_string()),
|
|
provider: Some("openai".to_string()),
|
|
task_id: Some(task_id.to_string()),
|
|
},
|
|
extra_metadata: BTreeMap::from([("profile_id".to_string(), profile_id.to_string())]),
|
|
})
|
|
.map_err(map_square_hole_generated_image_asset_error)?;
|
|
let persisted_mime_type = prepared.format.mime_type.clone();
|
|
let put_result = oss_client
|
|
.put_object(&http_client, prepared.request)
|
|
.await
|
|
.map_err(map_square_hole_asset_oss_error)?;
|
|
let head = oss_client
|
|
.head_object(
|
|
&http_client,
|
|
OssHeadObjectRequest {
|
|
object_key: put_result.object_key.clone(),
|
|
},
|
|
)
|
|
.await
|
|
.map_err(map_square_hole_asset_oss_error)?;
|
|
|
|
match state
|
|
.spacetime_client()
|
|
.confirm_asset_object(
|
|
build_asset_object_upsert_input(
|
|
generate_asset_object_id(generated_at_micros),
|
|
head.bucket,
|
|
head.object_key,
|
|
AssetObjectAccessPolicy::Private,
|
|
head.content_type.or(Some(persisted_mime_type)),
|
|
head.content_length,
|
|
head.etag,
|
|
asset_kind.to_string(),
|
|
Some(task_id.to_string()),
|
|
Some(owner_user_id.to_string()),
|
|
Some(profile_id.to_string()),
|
|
Some(profile_id.to_string()),
|
|
generated_at_micros,
|
|
)
|
|
.map_err(map_square_hole_asset_field_error)?,
|
|
)
|
|
.await
|
|
{
|
|
Ok(asset_object) => {
|
|
if let Err(error) = state
|
|
.spacetime_client()
|
|
.bind_asset_object_to_entity(
|
|
build_asset_entity_binding_input(
|
|
generate_asset_binding_id(generated_at_micros),
|
|
asset_object.asset_object_id,
|
|
SQUARE_HOLE_ENTITY_KIND.to_string(),
|
|
profile_id.to_string(),
|
|
slot.to_string(),
|
|
asset_kind.to_string(),
|
|
Some(owner_user_id.to_string()),
|
|
Some(profile_id.to_string()),
|
|
generated_at_micros,
|
|
)
|
|
.map_err(map_square_hole_asset_field_error)?,
|
|
)
|
|
.await
|
|
{
|
|
tracing::warn!(
|
|
provider = "spacetimedb",
|
|
owner_user_id,
|
|
session_id,
|
|
profile_id,
|
|
slot,
|
|
asset_kind,
|
|
error = %error,
|
|
"方洞图片资产绑定失败,历史素材索引可能缺少绑定记录"
|
|
);
|
|
}
|
|
}
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
provider = "spacetimedb",
|
|
owner_user_id,
|
|
session_id,
|
|
profile_id,
|
|
slot,
|
|
asset_kind,
|
|
error = %error,
|
|
"方洞图片资产对象确认失败,历史素材索引可能缺少本次记录"
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(put_result.legacy_public_path)
|
|
}
|
|
|
|
fn map_square_hole_generated_image_asset_error(
|
|
error: crate::generated_image_assets::helpers::GeneratedImageAssetHelperError,
|
|
) -> AppError {
|
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
|
"provider": "generated-image-assets",
|
|
"message": format!("准备方洞图片资产上传请求失败:{error:?}"),
|
|
}))
|
|
}
|
|
|
|
fn map_square_hole_asset_oss_error(error: platform_oss::OssError) -> AppError {
|
|
map_oss_error(error, "aliyun-oss")
|
|
}
|
|
|
|
fn map_square_hole_asset_field_error(error: AssetObjectFieldError) -> AppError {
|
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
|
"provider": "square-hole-assets",
|
|
"message": error.to_string(),
|
|
}))
|
|
}
|
|
|
|
fn sanitize_square_hole_asset_segment(value: &str, fallback: &str) -> String {
|
|
let sanitized = value
|
|
.trim()
|
|
.chars()
|
|
.map(|ch| {
|
|
if ch.is_ascii_alphanumeric() || ('\u{4e00}'..='\u{9fff}').contains(&ch) {
|
|
ch
|
|
} else {
|
|
'-'
|
|
}
|
|
})
|
|
.collect::<String>()
|
|
.trim_matches('-')
|
|
.to_string();
|
|
if sanitized.is_empty() {
|
|
fallback.to_string()
|
|
} else {
|
|
sanitized
|
|
}
|
|
}
|
|
|
|
fn build_square_hole_cover_prompt(work: &SquareHoleWorkProfileRecord) -> String {
|
|
format!(
|
|
"移动端休闲游戏封面图。主题:{}。玩法反差:{}。画面主体是贴着主题图案的几何形状正在靠近不同洞口,视觉清晰、色彩明快、偏游戏资产质感。不要文字、不要 UI、不要水印。",
|
|
clean_prompt_text(&work.theme_text, "奇怪形状"),
|
|
clean_prompt_text(&work.twist_rule, "反直觉分拣")
|
|
)
|
|
}
|
|
|
|
fn build_square_hole_background_prompt(work: &SquareHoleWorkProfileRecord) -> String {
|
|
let custom_prompt = work.background_prompt.trim();
|
|
if !custom_prompt.is_empty() {
|
|
return format!(
|
|
"移动端休闲游戏运行背景。{}。画面中央预留清晰操作空间,边缘可有主题装饰,低噪声,不要文字、不要 UI、不要水印。",
|
|
custom_prompt
|
|
);
|
|
}
|
|
|
|
format!(
|
|
"移动端休闲游戏运行背景。主题:{}。柔和纵深、玩具盒或舞台感,中央预留清晰操作空间,不要文字、不要 UI、不要水印。",
|
|
clean_prompt_text(&work.theme_text, "奇怪形状")
|
|
)
|
|
}
|
|
|
|
fn build_square_hole_shape_prompt(
|
|
work: &SquareHoleWorkProfileRecord,
|
|
option: &SquareHoleShapeOptionRecord,
|
|
) -> String {
|
|
let image_prompt = option.image_prompt.trim();
|
|
let option_prompt = if image_prompt.is_empty() {
|
|
format!("{} 主题的 {}", work.theme_text, option.label)
|
|
} else {
|
|
image_prompt.to_string()
|
|
};
|
|
|
|
format!(
|
|
"单个游戏道具贴图,透明或干净浅色背景。几何形状:{}。主题贴图:{}。要求主体居中、边缘清晰、适合贴在可拖拽形状上,不要文字、不要 UI、不要水印。",
|
|
clean_prompt_text(&option.label, "形状"),
|
|
clean_prompt_text(&option_prompt, "主题图案")
|
|
)
|
|
}
|
|
|
|
fn build_square_hole_hole_prompt(
|
|
work: &SquareHoleWorkProfileRecord,
|
|
option: &SquareHoleHoleOptionRecord,
|
|
) -> String {
|
|
let image_prompt = option.image_prompt.trim();
|
|
let option_prompt = if image_prompt.is_empty() {
|
|
format!("{} 主题的 {}", work.theme_text, option.label)
|
|
} else {
|
|
image_prompt.to_string()
|
|
};
|
|
|
|
format!(
|
|
"单个游戏洞口贴图,透明或干净浅色背景。洞口名称:{}。主题贴图:{}。要求主体居中、边缘清晰、适合放在可接收拖拽形状的洞口平面上,不要文字、不要 UI、不要水印。",
|
|
clean_prompt_text(&option.label, "洞口"),
|
|
clean_prompt_text(&option_prompt, "主题洞口")
|
|
)
|
|
}
|
|
|
|
fn build_square_hole_negative_prompt() -> String {
|
|
"文字、水印、复杂 UI、真实人物、恐怖血腥、低清晰度、过度模糊、主体被裁切、多个主体".to_string()
|
|
}
|