Use bundled Linux Do picture templates

This commit is contained in:
boos
2026-06-03 10:55:43 +08:00
parent 5765ea48b0
commit fb5fc9f1ae
3 changed files with 12853 additions and 221 deletions
File diff suppressed because one or more lines are too long
+11 -170
View File
@@ -14,17 +14,15 @@ use axum::{
routing::{get, post}, routing::{get, post},
}; };
use reqwest::Client; use reqwest::Client;
use reqwest::Url;
use serde::Deserialize; use serde::Deserialize;
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::time::Duration; use std::time::Duration;
const TEMPLATE_GALLERY_PAGE_URL: &str = const TEMPLATE_GALLERY_PAGE_URL: &str =
"https://weiyyin.github.io/linux-do-gpt-image/prompt_gallery.html"; "https://weiyyin.github.io/linux-do-gpt-image/prompt_gallery.html";
const TEMPLATE_GALLERY_FALLBACK_DATA_URL: &str = const LINUX_DO_TEMPLATE_JSON: &str =
"https://weiyyin.github.io/linux-do-gpt-image/linux_do_2036927_prompts_images.with_tags.json"; include_str!("../../assets/picture-agent/linux_do_2036927_prompts_images.with_tags.json");
const TASK_TYPE_PICTURE_GENERATION: &str = "picture-agent-generation"; const TASK_TYPE_PICTURE_GENERATION: &str = "picture-agent-generation";
const TEMPLATE_GALLERY_TIMEOUT_SECS: u64 = 30;
const PICTURE_GENERATION_TIMEOUT_SECS: u64 = 300; const PICTURE_GENERATION_TIMEOUT_SECS: u64 = 300;
const PICTURE_GENERATION_RETRY_SECS: u64 = 180; const PICTURE_GENERATION_RETRY_SECS: u64 = 180;
const STALE_RUNNING_JOB_MINUTES: i64 = 12; const STALE_RUNNING_JOB_MINUTES: i64 = 12;
@@ -289,22 +287,6 @@ mod tests {
assert!(!is_picture_model_pattern("qwen-plus")); assert!(!is_picture_model_pattern("qwen-plus"));
} }
#[test]
fn extracts_gallery_data_url_from_page_script() {
let html = r#"
<script>
const DATA_URL = "./linux_do_2036927_prompts_images.with_tags.json";
</script>
"#;
assert_eq!(
extract_gallery_data_url(html).as_deref(),
Some("./linux_do_2036927_prompts_images.with_tags.json")
);
assert_eq!(
absolute_gallery_url("./linux_do_2036927_prompts_images.with_tags.json").unwrap(),
"https://weiyyin.github.io/linux-do-gpt-image/linux_do_2036927_prompts_images.with_tags.json"
);
}
} }
fn prompt_title(prompt: &str, post_number: Option<i64>) -> String { fn prompt_title(prompt: &str, post_number: Option<i64>) -> String {
@@ -364,52 +346,28 @@ async fn templates_handler(
) -> AppResult<Json<Value>> { ) -> AppResult<Json<Value>> {
let _auth = resolve_picture_agent_auth(&headers, &state).await?; let _auth = resolve_picture_agent_auth(&headers, &state).await?;
let limit = query.limit.unwrap_or(144).clamp(1, 240); let limit = query.limit.unwrap_or(144).clamp(1, 240);
let mut templates = builtin_picture_templates(limit); let (templates, local_status) = match parse_gallery_templates(LINUX_DO_TEMPLATE_JSON, limit) {
let remote_status = match fetch_remote_gallery_templates( Ok(templates) => (templates, json!({ "ok": true, "source": "local-json" })),
&state.config,
limit.saturating_sub(templates.len()),
)
.await
{
Ok(mut remote_templates) => {
templates.append(&mut remote_templates);
json!({ "ok": true })
}
Err(err) => { Err(err) => {
tracing::warn!(%err, "Picture Agent template gallery unavailable; using built-in templates"); tracing::warn!(%err, "Picture Agent local Linux Do template gallery unavailable");
json!({ "ok": false, "message": err }) (Vec::new(), json!({ "ok": false, "message": err }))
} }
}; };
templates.truncate(limit);
Ok(Json(json!({ Ok(Json(json!({
"success": true, "success": true,
"sourceUrl": TEMPLATE_GALLERY_PAGE_URL, "sourceUrl": TEMPLATE_GALLERY_PAGE_URL,
"fallback": remote_status.get("ok").and_then(Value::as_bool) != Some(true), "source": local_status,
"remote": remote_status,
"templates": templates, "templates": templates,
}))) })))
} }
async fn fetch_remote_gallery_templates( fn parse_gallery_templates(raw: &str, limit: usize) -> Result<Vec<Value>, String> {
config: &crate::config::AppConfig,
limit: usize,
) -> Result<Vec<Value>, String> {
if limit == 0 { if limit == 0 {
return Ok(Vec::new()); return Ok(Vec::new());
} }
let client = crate::template_http::client(config, TEMPLATE_GALLERY_TIMEOUT_SECS); let items = serde_json::from_str::<Vec<GalleryItem>>(raw)
let data_url = resolve_gallery_data_url(&client).await?; .map_err(|err| format!("failed to parse local Linux Do templates: {err}"))?;
let items = client
.get(&data_url)
.send()
.await
.map_err(|err| format!("failed to fetch templates from {data_url}: {err}"))?
.error_for_status()
.map_err(|err| format!("template gallery data returned {err}"))?
.json::<Vec<GalleryItem>>()
.await
.map_err(|err| format!("failed to parse templates: {err}"))?;
Ok(items Ok(items
.into_iter() .into_iter()
@@ -439,7 +397,7 @@ async fn fetch_remote_gallery_templates(
"prompt": prompt, "prompt": prompt,
"source": { "source": {
"name": "Linux Do GPT Image Gallery", "name": "Linux Do GPT Image Gallery",
"url": "https://weiyyin.github.io/linux-do-gpt-image/prompt_gallery.html", "url": TEMPLATE_GALLERY_PAGE_URL,
"postNumber": post_number, "postNumber": post_number,
"username": item.username, "username": item.username,
}, },
@@ -449,123 +407,6 @@ async fn fetch_remote_gallery_templates(
.collect()) .collect())
} }
async fn resolve_gallery_data_url(client: &Client) -> Result<String, String> {
let html = client
.get(TEMPLATE_GALLERY_PAGE_URL)
.send()
.await
.map_err(|err| format!("failed to fetch template gallery page: {err}"))?
.error_for_status()
.map_err(|err| format!("template gallery page returned {err}"))?
.text()
.await
.map_err(|err| format!("failed to read template gallery page: {err}"))?;
let data_url = extract_gallery_data_url(&html)
.ok_or_else(|| "template gallery page did not expose DATA_URL".to_string())?;
absolute_gallery_url(&data_url)
}
fn extract_gallery_data_url(html: &str) -> Option<String> {
let regex = regex::Regex::new(r#"(?m)\bDATA_URL\s*=\s*["']([^"']+)["']"#).ok()?;
regex
.captures(html)
.and_then(|captures| captures.get(1))
.map(|match_| match_.as_str().trim().to_string())
.filter(|value| !value.is_empty())
}
fn absolute_gallery_url(value: &str) -> Result<String, String> {
if value.starts_with("http://") || value.starts_with("https://") {
return Ok(value.to_string());
}
let base = Url::parse(TEMPLATE_GALLERY_PAGE_URL)
.map_err(|err| format!("invalid template gallery page URL: {err}"))?;
base.join(value)
.map(|url| url.to_string())
.or_else(|_| Url::parse(TEMPLATE_GALLERY_FALLBACK_DATA_URL).map(|url| url.to_string()))
.map_err(|err| format!("invalid template gallery data URL: {err}"))
}
fn builtin_picture_templates(limit: usize) -> Vec<Value> {
[
(
"boosapi-product-glass-robot",
"玻璃机器人产品照",
"商业",
"1024x1024",
"A precise editorial product photo of a translucent glass robot painting a small neon city, sharp focus, clean background, controlled studio lighting, premium advertising composition, ultra detailed",
),
(
"boosapi-hanfu-cat-golden-hour",
"汉服与白猫",
"国风",
"1024x1536",
"A breathtakingly beautiful ancient Chinese woman in flowing jade-green and blush pink hanfu, golden peony embroidery, sitting by a moon-shaped window in warm afternoon sunlight, gently teasing a fluffy white cat with a silk tassel, celadon tea set and ink calligraphy nearby, serene romantic dreamlike Song Dynasty aesthetics, cinematic depth of field, ultra detailed digital art",
),
(
"boosapi-cyberpunk-street",
"赛博街景海报",
"海报",
"1536x1024",
"A cinematic cyberpunk street market after rain, neon signs reflected on wet pavement, a courier in a translucent raincoat walking through steam, layered storefronts and holographic menus, dramatic teal and amber lighting, editorial poster composition, high detail",
),
(
"boosapi-character-turnaround",
"角色四视图",
"角色",
"1536x1024",
"Character design sheet, same original fantasy explorer shown in front view, side view, back view and three-quarter view, consistent outfit and proportions, clean light background, readable silhouette, detailed fabric and accessories, professional concept art",
),
(
"boosapi-food-photography",
"高级餐饮摄影",
"商业",
"1024x1024",
"Premium food photography of a porcelain bowl of hand-pulled noodles with chili oil, scallions and steam, dark stone tabletop, soft side lighting, shallow depth of field, appetizing texture, refined restaurant campaign style",
),
(
"boosapi-architecture-interior",
"现代茶室空间",
"空间",
"1536x1024",
"A quiet contemporary Chinese tea room interior with warm timber lattice, low stone tea table, linen cushions, bamboo courtyard visible through sliding doors, natural morning light, calm luxury hospitality design, realistic architectural visualization",
),
(
"boosapi-kids-book-dragon",
"绘本小龙",
"插画",
"1024x1024",
"A gentle children's book illustration of a tiny green dragon learning to fly above a village garden, soft watercolor texture, expressive friendly eyes, warm pastel palette, charming composition, lots of small story details",
),
(
"boosapi-film-still-office",
"悬疑电影帧",
"电影",
"1536x1024",
"A suspenseful cinematic film still in a dim modern office at night, a figure with a glowing screen-like face stands motionless near frosted glass, subtle volumetric light, muted colors, realistic lens depth, unsettling quiet atmosphere",
),
]
.into_iter()
.take(limit)
.map(|(id, title, tag, size, prompt)| {
json!({
"id": id,
"title": title,
"tag": tag,
"size": size,
"image": Value::Null,
"prompt": prompt,
"source": {
"name": "BoosAPI 内置模板",
"url": Value::Null,
},
})
})
.collect()
}
fn picture_generation_client() -> Client { fn picture_generation_client() -> Client {
picture_generation_client_with_timeout(PICTURE_GENERATION_TIMEOUT_SECS) picture_generation_client_with_timeout(PICTURE_GENERATION_TIMEOUT_SECS)
} }
+5 -51
View File
@@ -105,51 +105,6 @@ const STYLE_PRESETS = [
prompt: 'modern Chinese illustration style, elegant linework, vivid but balanced colors, poster composition', prompt: 'modern Chinese illustration style, elegant linework, vivid but balanced colors, poster composition',
}, },
]; ];
const FALLBACK_TEMPLATES: PromptTemplate[] = [
{
id: 'local-product-glass-robot',
title: '玻璃机器人产品照',
tag: '商业',
size: '1024x1024',
prompt: 'A precise editorial product photo of a translucent glass robot painting a small neon city, sharp focus, clean background, controlled studio lighting, premium advertising composition, ultra detailed',
},
{
id: 'local-hanfu-cat-golden-hour',
title: '汉服与白猫',
tag: '国风',
size: '1024x1536',
prompt: 'A breathtakingly beautiful ancient Chinese woman in flowing jade-green and blush pink hanfu, golden peony embroidery, sitting by a moon-shaped window in warm afternoon sunlight, gently teasing a fluffy white cat with a silk tassel, celadon tea set and ink calligraphy nearby, serene romantic dreamlike Song Dynasty aesthetics, cinematic depth of field, ultra detailed digital art',
},
{
id: 'local-cyberpunk-street',
title: '赛博街景海报',
tag: '海报',
size: '1536x1024',
prompt: 'A cinematic cyberpunk street market after rain, neon signs reflected on wet pavement, a courier in a translucent raincoat walking through steam, layered storefronts and holographic menus, dramatic teal and amber lighting, editorial poster composition, high detail',
},
{
id: 'local-character-turnaround',
title: '角色四视图',
tag: '角色',
size: '1536x1024',
prompt: 'Character design sheet, same original fantasy explorer shown in front view, side view, back view and three-quarter view, consistent outfit and proportions, clean light background, readable silhouette, detailed fabric and accessories, professional concept art',
},
{
id: 'local-food-photography',
title: '高级餐饮摄影',
tag: '商业',
size: '1024x1024',
prompt: 'Premium food photography of a porcelain bowl of hand-pulled noodles with chili oil, scallions and steam, dark stone tabletop, soft side lighting, shallow depth of field, appetizing texture, refined restaurant campaign style',
},
{
id: 'local-tea-room',
title: '现代茶室空间',
tag: '空间',
size: '1536x1024',
prompt: 'A quiet contemporary Chinese tea room interior with warm timber lattice, low stone tea table, linen cushions, bamboo courtyard visible through sliding doors, natural morning light, calm luxury hospitality design, realistic architectural visualization',
},
];
function makeId() { function makeId() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID(); return crypto.randomUUID();
@@ -430,9 +385,9 @@ export default function PictureAgent() {
keyName: '加载密钥中', keyName: '加载密钥中',
}); });
setModel((current) => current || 'gpt-image-2'); setModel((current) => current || 'gpt-image-2');
setTemplates(FALLBACK_TEMPLATES); setTemplates([]);
setTemplatesError(''); setTemplatesError('');
setTemplatesLoading(false); setTemplatesLoading(true);
try { try {
const [statusResult, modelsResult, jobsResult] = await Promise.allSettled([ const [statusResult, modelsResult, jobsResult] = await Promise.allSettled([
authedFetch('/api/picture-agent/status', { timeoutMs: 8_000 }), authedFetch('/api/picture-agent/status', { timeoutMs: 8_000 }),
@@ -484,7 +439,6 @@ export default function PictureAgent() {
} }
try { try {
if (!ignore) setTemplatesLoading(true);
const templatesRes = await authedFetch(`/api/picture-agent/templates?limit=${TEMPLATE_LIMIT}`, { timeoutMs: 10_000 }); const templatesRes = await authedFetch(`/api/picture-agent/templates?limit=${TEMPLATE_LIMIT}`, { timeoutMs: 10_000 });
if (ignore) return; if (ignore) return;
if (!templatesRes.ok) { if (!templatesRes.ok) {
@@ -928,10 +882,10 @@ export default function PictureAgent() {
{templatesLoading && templates.length === 0 {templatesLoading && templates.length === 0
? '正在加载参考模板...' ? '正在加载参考模板...'
: templatesLoading : templatesLoading
? '本地模板可用,正在后台补充图库' ? '正在读取本地 Linux Do 模板库'
: templatesError : templatesError
? '远程图库暂不可用,已使用本地模板' ? '本地模板库加载失败'
: '来自 Linux Do GPT Image Gallery'} : '来自本地 Linux Do GPT Image Gallery'}
</span> </span>
</div> </div>
<div className="picture-agent-template-list"> <div className="picture-agent-template-list">