This commit is contained in:
2026-02-17 12:29:20 +08:00
parent f6c361dd13
commit 0ab6f52525
4 changed files with 132 additions and 29 deletions

View File

@@ -80,6 +80,33 @@ CLEANUP_CONFIG = {
"interval": int(os.getenv("CLEANUP_INTERVAL_SECONDS", "600")) "interval": int(os.getenv("CLEANUP_INTERVAL_SECONDS", "600"))
} }
# 提示词配置 (Prompt Config)
PROMPTS = {
"translate": "请将以下描述翻译成简洁、精准的英文,用于图像分割模型(SAM)的提示词。直接返回英文,不要包含任何解释或其他文字。\n\n输入: {text}",
"tarot_card_dual": """这是一张塔罗牌的两个方向:
图1原始方向
图2旋转180度后的方向
请仔细对比两张图片的牌面内容(文字方向、人物站立方向、图案逻辑):
1. 识别这张牌的名字(中文)。
2. 判断哪一张图片展示了正确的“正位”Upright状态。
- 如果图1是正位说明原图就是正位。
- 如果图2是正位说明原图是逆位。
请以JSON格式返回包含 'name''position' 两个字段。
例如:{'name': '愚者', 'position': '正位'} 或 {'name': '倒吊人', 'position': '逆位'}。
不要包含Markdown代码块标记。""",
"tarot_card_single": "这是一张塔罗牌。请识别它的名字中文并判断它是正位还是逆位。请以JSON格式返回包含 'name''position' 两个字段。例如:{'name': '愚者', 'position': '正位'}。不要包含Markdown代码块标记。",
"tarot_spread": "这是一张包含多张塔罗牌的图片。请根据牌的排列方式识别这是什么牌阵例如圣三角、凯尔特十字、三张牌等。如果看不出明显的正规牌阵请返回“不是正规牌阵”。请以JSON格式返回包含 'spread_name''description' 两个字段。例如:{'spread_name': '圣三角', 'description': '常见的时间流占卜法'}。不要包含Markdown代码块标记。",
"face_analysis": """请仔细观察这张图片中的人物头部/面部特写:
1. 识别性别 (Gender):男性/女性
2. 预估年龄 (Age):请给出一个合理的年龄范围,例如 "25-30岁"
3. 简要描述:发型、发色、是否有眼镜等显著特征。
请以 JSON 格式返回,包含 'gender', 'age', 'description' 字段。
不要包含 Markdown 标记。"""
}
# API Tags (用于文档分类) # API Tags (用于文档分类)
TAG_GENERAL = "General Segmentation (通用分割)" TAG_GENERAL = "General Segmentation (通用分割)"
TAG_TAROT = "Tarot Analysis (塔罗牌分析)" TAG_TAROT = "Tarot Analysis (塔罗牌分析)"
@@ -265,11 +292,12 @@ def translate_to_sam3_prompt(text: str) -> str:
""" """
print(f"正在翻译提示词: {text}") print(f"正在翻译提示词: {text}")
try: try:
prompt_template = PROMPTS["translate"]
messages = [ messages = [
{ {
"role": "user", "role": "user",
"content": [ "content": [
{"text": f"请将以下描述翻译成简洁、精准的英文,用于图像分割模型(SAM)的提示词。直接返回英文,不要包含任何解释或其他文字。\n\n输入: {text}"} {"text": prompt_template.format(text=text)}
] ]
} }
] ]
@@ -503,19 +531,7 @@ def recognize_card_with_qwen(image_path: str) -> dict:
"content": [ "content": [
{"image": file_url}, # 图1 (原图) {"image": file_url}, # 图1 (原图)
{"image": rotated_file_url}, # 图2 (旋转180度) {"image": rotated_file_url}, # 图2 (旋转180度)
{"text": """这是一张塔罗牌的两个方向: {"text": PROMPTS["tarot_card_dual"]}
图1原始方向
图2旋转180度后的方向
请仔细对比两张图片的牌面内容(文字方向、人物站立方向、图案逻辑):
1. 识别这张牌的名字(中文)。
2. 判断哪一张图片展示了正确的“正位”Upright状态。
- 如果图1是正位说明原图就是正位。
- 如果图2是正位说明原图是逆位。
请以JSON格式返回包含 'name''position' 两个字段。
例如:{'name': '愚者', 'position': '正位'} 或 {'name': '倒吊人', 'position': '逆位'}。
不要包含Markdown代码块标记。"""}
] ]
} }
] ]
@@ -527,7 +543,7 @@ def recognize_card_with_qwen(image_path: str) -> dict:
"role": "user", "role": "user",
"content": [ "content": [
{"image": file_url}, {"image": file_url},
{"text": "这是一张塔罗牌。请识别它的名字中文并判断它是正位还是逆位。请以JSON格式返回包含 'name''position' 两个字段。例如:{'name': '愚者', 'position': '正位'}。不要包含Markdown代码块标记。"} {"text": PROMPTS["tarot_card_single"]}
] ]
} }
] ]
@@ -562,7 +578,7 @@ def recognize_spread_with_qwen(image_path: str) -> dict:
"role": "user", "role": "user",
"content": [ "content": [
{"image": file_url}, {"image": file_url},
{"text": "这是一张包含多张塔罗牌的图片。请根据牌的排列方式识别这是什么牌阵例如圣三角、凯尔特十字、三张牌等。如果看不出明显的正规牌阵请返回“不是正规牌阵”。请以JSON格式返回包含 'spread_name''description' 两个字段。例如:{'spread_name': '圣三角', 'description': '常见的时间流占卜法'}。不要包含Markdown代码块标记。"} {"text": PROMPTS["tarot_spread"]}
] ]
} }
] ]
@@ -1034,7 +1050,8 @@ async def segment_face(
image=image, image=image,
prompt=final_prompt, prompt=final_prompt,
output_base_dir=RESULT_IMAGE_DIR, output_base_dir=RESULT_IMAGE_DIR,
qwen_model=QWEN_MODEL qwen_model=QWEN_MODEL,
analysis_prompt=PROMPTS["face_analysis"]
) )
except Exception as e: except Exception as e:
import traceback import traceback
@@ -1268,6 +1285,27 @@ async def set_model(model: str = Form(...)):
QWEN_MODEL = model QWEN_MODEL = model
return {"status": "success", "message": f"Model switched to {model}", "current_model": QWEN_MODEL} return {"status": "success", "message": f"Model switched to {model}", "current_model": QWEN_MODEL}
@app.get("/admin/api/prompts", dependencies=[Depends(verify_admin)])
async def get_prompts():
"""
Get all prompts
"""
return PROMPTS
@app.post("/admin/api/prompts", dependencies=[Depends(verify_admin)])
async def update_prompts(
key: str = Form(...),
content: str = Form(...)
):
"""
Update a specific prompt
"""
if key not in PROMPTS:
raise HTTPException(status_code=400, detail="Invalid prompt key")
PROMPTS[key] = content
return {"status": "success", "message": f"Prompt '{key}' updated"}
# ========================================== # ==========================================
# 10. Main Entry Point (启动入口) # 10. Main Entry Point (启动入口)
# ========================================== # ==========================================

View File

@@ -95,7 +95,7 @@ def create_highlighted_visualization(image: Image.Image, masks, output_path: str
# Save # Save
Image.fromarray(result_np).save(output_path) Image.fromarray(result_np).save(output_path)
def analyze_demographics_with_qwen(image_path: str, model_name: str = 'qwen-vl-max') -> dict: def analyze_demographics_with_qwen(image_path: str, model_name: str = 'qwen-vl-max', prompt_template: str = None) -> dict:
""" """
调用 Qwen-VL 模型分析人物的年龄和性别 调用 Qwen-VL 模型分析人物的年龄和性别
""" """
@@ -104,19 +104,24 @@ def analyze_demographics_with_qwen(image_path: str, model_name: str = 'qwen-vl-m
abs_path = os.path.abspath(image_path) abs_path = os.path.abspath(image_path)
file_url = f"file://{abs_path}" file_url = f"file://{abs_path}"
# 默认 Prompt
default_prompt = """请仔细观察这张图片中的人物头部/面部特写:
1. 识别性别 (Gender):男性/女性
2. 预估年龄 (Age):请给出一个合理的年龄范围,例如 "25-30岁"
3. 简要描述:发型、发色、是否有眼镜等显著特征。
请以 JSON 格式返回,包含 'gender', 'age', 'description' 字段。
不要包含 Markdown 标记。"""
final_prompt = prompt_template if prompt_template else default_prompt
# 构造 Prompt # 构造 Prompt
messages = [ messages = [
{ {
"role": "user", "role": "user",
"content": [ "content": [
{"image": file_url}, {"image": file_url},
{"text": """请仔细观察这张图片中的人物头部/面部特写: {"text": final_prompt}
1. 识别性别 (Gender):男性/女性
2. 预估年龄 (Age):请给出一个合理的年龄范围,例如 "25-30岁"
3. 简要描述:发型、发色、是否有眼镜等显著特征。
请以 JSON 格式返回,包含 'gender', 'age', 'description' 字段。
不要包含 Markdown 标记。"""}
] ]
} }
] ]
@@ -144,7 +149,8 @@ def process_face_segmentation_and_analysis(
image: Image.Image, image: Image.Image,
prompt: str = "head", prompt: str = "head",
output_base_dir: str = "static/results", output_base_dir: str = "static/results",
qwen_model: str = "qwen-vl-max" qwen_model: str = "qwen-vl-max",
analysis_prompt: str = None
) -> dict: ) -> dict:
""" """
核心处理逻辑: 核心处理逻辑:
@@ -209,7 +215,7 @@ def process_face_segmentation_and_analysis(
cropped_img.save(save_path) cropped_img.save(save_path)
# 3. 识别 # 3. 识别
analysis = analyze_demographics_with_qwen(save_path, model_name=qwen_model) analysis = analyze_demographics_with_qwen(save_path, model_name=qwen_model, prompt_template=analysis_prompt)
# 构造返回结果 # 构造返回结果
# 注意URL 生成需要依赖外部的 request context这里只返回相对路径或文件名 # 注意URL 生成需要依赖外部的 request context这里只返回相对路径或文件名

View File

@@ -13,7 +13,7 @@ SCRIPT_NAME="fastAPI_tarot.py" # Python 启动脚本
LOG_FILE="${PROJECT_DIR}/log/monitor.log" # 监控日志文件 LOG_FILE="${PROJECT_DIR}/log/monitor.log" # 监控日志文件
APP_LOG_FILE="${PROJECT_DIR}/log/app.log" # 应用输出日志文件 APP_LOG_FILE="${PROJECT_DIR}/log/app.log" # 应用输出日志文件
PORT=55600 # 服务端口 PORT=55600 # 服务端口
CHECK_INTERVAL=20 # 检查间隔(秒) CHECK_INTERVAL=60 # 检查间隔(秒)
MAX_FAILURES=3 # 最大连续失败次数,超过则重启 MAX_FAILURES=3 # 最大连续失败次数,超过则重启
STARTUP_TIMEOUT=300 # 启动超时时间(秒),等待模型加载 STARTUP_TIMEOUT=300 # 启动超时时间(秒),等待模型加载
PYTHON_CMD="python" # Python 命令,根据环境可能是 python3 PYTHON_CMD="python" # Python 命令,根据环境可能是 python3

View File

@@ -47,6 +47,9 @@
<a href="#" @click.prevent="currentTab = 'files'" :class="{'bg-blue-600': currentTab === 'files', 'hover:bg-slate-700': currentTab !== 'files'}" class="block py-2.5 px-4 rounded transition duration-200"> <a href="#" @click.prevent="currentTab = 'files'" :class="{'bg-blue-600': currentTab === 'files', 'hover:bg-slate-700': currentTab !== 'files'}" class="block py-2.5 px-4 rounded transition duration-200">
<i class="fas fa-folder-open w-6"></i> 文件管理 <i class="fas fa-folder-open w-6"></i> 文件管理
</a> </a>
<a href="#" @click.prevent="currentTab = 'prompts'" :class="{'bg-blue-600': currentTab === 'prompts', 'hover:bg-slate-700': currentTab !== 'prompts'}" class="block py-2.5 px-4 rounded transition duration-200">
<i class="fas fa-comment-dots w-6"></i> 提示词管理
</a>
<a href="#" @click.prevent="currentTab = 'settings'" :class="{'bg-blue-600': currentTab === 'settings', 'hover:bg-slate-700': currentTab !== 'settings'}" class="block py-2.5 px-4 rounded transition duration-200"> <a href="#" @click.prevent="currentTab = 'settings'" :class="{'bg-blue-600': currentTab === 'settings', 'hover:bg-slate-700': currentTab !== 'settings'}" class="block py-2.5 px-4 rounded transition duration-200">
<i class="fas fa-cogs w-6"></i> 系统设置 <i class="fas fa-cogs w-6"></i> 系统设置
</a> </a>
@@ -164,6 +167,26 @@
</div> </div>
</div> </div>
<!-- 提示词管理 Prompts -->
<div v-if="currentTab === 'prompts'">
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-2">提示词管理</h2>
<div class="grid grid-cols-1 gap-6">
<div v-for="(content, key) in prompts" :key="key" class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-gray-700 flex items-center gap-2">
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded border border-blue-400 font-mono">{{ key }}</span>
<span class="text-sm font-normal text-gray-500">{{ getPromptDescription(key) }}</span>
</h3>
<button @click="savePrompt(key)" class="bg-green-500 hover:bg-green-600 text-white font-bold py-1 px-3 rounded text-sm transition">
<i class="fas fa-save mr-1"></i> 保存
</button>
</div>
<textarea v-model="prompts[key]" rows="6" class="w-full p-3 border rounded font-mono text-sm bg-gray-50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition"></textarea>
</div>
</div>
</div>
<!-- 系统设置 Settings --> <!-- 系统设置 Settings -->
<div v-if="currentTab === 'settings'"> <div v-if="currentTab === 'settings'">
<h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-2">系统设置</h2> <h2 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-2">系统设置</h2>
@@ -266,6 +289,7 @@
lifetime: 3600, lifetime: 3600,
interval: 600 interval: 600
}); });
const prompts = ref({});
// 检查登录状态 // 检查登录状态
const checkLogin = () => { const checkLogin = () => {
@@ -288,6 +312,7 @@
loginError.value = ''; loginError.value = '';
fetchHistory(); fetchHistory();
fetchSystemInfo(); fetchSystemInfo();
fetchPrompts();
} }
} catch (e) { } catch (e) {
loginError.value = '密码错误'; loginError.value = '密码错误';
@@ -347,6 +372,38 @@
} }
}; };
const fetchPrompts = async () => {
try {
const res = await axios.get('/admin/api/prompts');
prompts.value = res.data;
} catch (e) {
console.error(e);
}
};
const savePrompt = async (key) => {
try {
const formData = new FormData();
formData.append('key', key);
formData.append('content', prompts.value[key]);
const res = await axios.post('/admin/api/prompts', formData);
alert(res.data.message);
} catch (e) {
alert('保存失败: ' + (e.response?.data?.detail || e.message));
}
};
const getPromptDescription = (key) => {
const map = {
'translate': 'Prompt 翻译 (中文 -> 英文)',
'tarot_card_dual': '塔罗牌识别 (正/逆位对比模式)',
'tarot_card_single': '塔罗牌识别 (单图模式)',
'tarot_spread': '塔罗牌阵识别',
'face_analysis': '人脸/属性分析 (Qwen-VL)'
};
return map[key] || '';
};
const updateModel = async () => { const updateModel = async () => {
try { try {
const formData = new FormData(); const formData = new FormData();
@@ -439,6 +496,7 @@
Vue.watch(currentTab, (newTab) => { Vue.watch(currentTab, (newTab) => {
if (newTab === 'files') fetchFiles(); if (newTab === 'files') fetchFiles();
if (newTab === 'dashboard') fetchHistory(); if (newTab === 'dashboard') fetchHistory();
if (newTab === 'prompts') fetchPrompts();
}); });
onMounted(() => { onMounted(() => {
@@ -452,7 +510,8 @@
viewResult, previewImage, isImage, previewUrl, viewResult, previewImage, isImage, previewUrl,
formatDate, getTypeBadgeClass, cleaning, deviceInfo, formatDate, getTypeBadgeClass, cleaning, deviceInfo,
currentModel, availableModels, updateModel, currentModel, availableModels, updateModel,
cleanupConfig, saveCleanupConfig cleanupConfig, saveCleanupConfig,
prompts, fetchPrompts, savePrompt, getPromptDescription
}; };
} }
}).mount('#app'); }).mount('#app');