Commit ·
53ccd32
1
Parent(s): 717a9ac
feat: add weight to model/
Browse files- code_inference/all_in.py +87 -0
- code_inference/all_in_normal.py +101 -0
- code_inference/rule_11_vllm.py +298 -0
- code_inference/rule_12_vllm.py +267 -0
- code_inference/rule_13_vllm.py +279 -0
- code_inference/rule_14_vllm.py +269 -0
- code_inference/rule_17_vllm.py +262 -0
- code_inference/rule_18_vllm.py +269 -0
- code_inference/rule_19_vllm.py +319 -0
- code_inference/vllm_audit_summary_normal.csv +7 -0
- stage2_object_v8_1200/added_tokens.json +3 -0
- stage2_object_v8_1200/chat_template.jinja +120 -0
- stage2_object_v8_1200/config.json +3 -0
- stage2_object_v8_1200/generation_config.json +3 -0
- stage2_object_v8_1200/model-00001-of-00004.safetensors +3 -0
- stage2_object_v8_1200/model-00002-of-00004.safetensors +3 -0
- stage2_object_v8_1200/model-00003-of-00004.safetensors +3 -0
- stage2_object_v8_1200/model-00004-of-00004.safetensors +3 -0
- stage2_object_v8_1200/model.safetensors.index.json +3 -0
- stage2_object_v8_1200/preprocessor_config.json +3 -0
- stage2_object_v8_1200/special_tokens_map.json +3 -0
- stage2_object_v8_1200/tokenizer.json +3 -0
- stage2_object_v8_1200/tokenizer_config.json +3 -0
- stage2_object_v8_1200/video_preprocessor_config.json +3 -0
- stage2_object_v8_1200/vocab.json +3 -0
code_inference/all_in.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import subprocess
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
GLOBAL_MODEL_PATH = "../stage2_object_v8_1200"
|
| 8 |
+
|
| 9 |
+
##示例:每个都传入绝对路径
|
| 10 |
+
TASK_CONFIGS = [
|
| 11 |
+
("rule_11_vllm.py", "文字占比"),
|
| 12 |
+
("rule_12_vllm.py", "文字-样式数量合理"),
|
| 13 |
+
#("rule_13_vllm.py", "文字-排布位置"),
|
| 14 |
+
#("rule_14_vllm.py", "文字-设计搭配协调"),
|
| 15 |
+
("rule_17_vllm.py", "信息量"),
|
| 16 |
+
("rule_18_vllm.py", "排布间距"),
|
| 17 |
+
("rule_19_vllm.py", "内容构图"),
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
SUMMARY_OUTPUT = "vllm_audit_summary.csv"
|
| 21 |
+
|
| 22 |
+
def run_vllm_task(script, input_dir):
|
| 23 |
+
|
| 24 |
+
cmd = [
|
| 25 |
+
"python", script,
|
| 26 |
+
"--input_dir", input_dir,
|
| 27 |
+
"--model_path", GLOBAL_MODEL_PATH
|
| 28 |
+
]
|
| 29 |
+
print(f"\n[EXEC] 正在运行: {script}")
|
| 30 |
+
print(f"[PATH] 输入目录: {Path(input_dir).name}")
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
subprocess.run(cmd, check=True)
|
| 34 |
+
return True
|
| 35 |
+
except subprocess.CalledProcessError as e:
|
| 36 |
+
print(f"[ERROR] 脚本 {script} 运行失败: {e}")
|
| 37 |
+
return False
|
| 38 |
+
|
| 39 |
+
def extract_metrics(script, input_dir):
|
| 40 |
+
input_path = Path(input_dir)
|
| 41 |
+
json_file = input_path / f"audit_result_{input_path.name}.json"
|
| 42 |
+
|
| 43 |
+
if not json_file.exists():
|
| 44 |
+
return {
|
| 45 |
+
"规则名称": script,
|
| 46 |
+
"对应目录": input_path.name,
|
| 47 |
+
"结果": "未找到 JSON 文件"
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
with open(json_file, 'r', encoding='utf-8') as f:
|
| 51 |
+
data = json.load(f)
|
| 52 |
+
|
| 53 |
+
total = len(data)
|
| 54 |
+
unsuitable = sum(1 for item in data if item["label"] == "Unsuitable")
|
| 55 |
+
suitable = sum(1 for item in data if item["label"] == "Suitable")
|
| 56 |
+
recall = (unsuitable / total) if total > 0 else 0
|
| 57 |
+
|
| 58 |
+
return {
|
| 59 |
+
"规则名称": script.replace("_vllm.py", ""),
|
| 60 |
+
"对应目录": input_path.name,
|
| 61 |
+
"样本总数": total,
|
| 62 |
+
"不通过数(Unsuitable)": unsuitable,
|
| 63 |
+
"通过数(Suitable)": suitable,
|
| 64 |
+
"召回率(违规检出率)": f"{recall:.2%}",
|
| 65 |
+
"所用模型": Path(GLOBAL_MODEL_PATH).name
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
def main():
|
| 69 |
+
final_results = []
|
| 70 |
+
|
| 71 |
+
for script, input_dir in TASK_CONFIGS:
|
| 72 |
+
success = run_vllm_task(script, input_dir)
|
| 73 |
+
|
| 74 |
+
metrics = extract_metrics(script, input_dir)
|
| 75 |
+
final_results.append(metrics)
|
| 76 |
+
|
| 77 |
+
df = pd.DataFrame(final_results)
|
| 78 |
+
df.to_csv(SUMMARY_OUTPUT, index=False, encoding='utf-8-sig')
|
| 79 |
+
|
| 80 |
+
print("\n" + "="*70)
|
| 81 |
+
print(f"所有任务运行完毕!汇总报告已保存至: {SUMMARY_OUTPUT}")
|
| 82 |
+
print("-" * 70)
|
| 83 |
+
print(df.to_string(index=False))
|
| 84 |
+
print("="*70)
|
| 85 |
+
|
| 86 |
+
if __name__ == "__main__":
|
| 87 |
+
main()
|
code_inference/all_in_normal.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import subprocess
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import shutil
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
GLOBAL_MODEL_PATH = "../stage2_object_v8_1200"
|
| 9 |
+
GLOBAL_NORMAL_PATH = "正常-normal_data"###正常图的绝对路径
|
| 10 |
+
|
| 11 |
+
# 任务配置:(脚本名, 规则显示名称)
|
| 12 |
+
TASK_CONFIGS = [
|
| 13 |
+
("rule_11_vllm.py", "文字占比"),
|
| 14 |
+
("rule_12_vllm.py", "样式数量"),
|
| 15 |
+
#("rule_13_vllm.py", "排布位置"),
|
| 16 |
+
#("rule_14_vllm.py", "设计搭配"),
|
| 17 |
+
("rule_17_vllm.py", "信息量"),
|
| 18 |
+
("rule_18_vllm.py", "排布间距"),
|
| 19 |
+
("rule_19_vllm.py", "内容构图"),
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
SUMMARY_OUTPUT = "vllm_audit_summary_normal.csv"
|
| 23 |
+
|
| 24 |
+
def run_vllm_task(script, label_name):
|
| 25 |
+
normal_path = Path(GLOBAL_NORMAL_PATH)
|
| 26 |
+
original_json = normal_path / f"audit_result_{normal_path.name}.json"
|
| 27 |
+
unique_json = normal_path / f"temp_{script.replace('.py', '')}.json"
|
| 28 |
+
|
| 29 |
+
if unique_json.exists():
|
| 30 |
+
unique_json.unlink()
|
| 31 |
+
|
| 32 |
+
cmd = [
|
| 33 |
+
"python", script,
|
| 34 |
+
"--input_dir", GLOBAL_NORMAL_PATH,
|
| 35 |
+
"--model_path", GLOBAL_MODEL_PATH
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
print(f"\n[EXEC] 正在运行规则: {label_name} ({script})")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
subprocess.run(cmd, check=True)
|
| 42 |
+
|
| 43 |
+
if original_json.exists():
|
| 44 |
+
shutil.move(str(original_json), str(unique_json))
|
| 45 |
+
print(f"[DONE] 结果已固化至: {unique_json.name}")
|
| 46 |
+
return unique_json
|
| 47 |
+
else:
|
| 48 |
+
print(f"[ERROR] 脚本运行完成但未找到生成文件: {original_json}")
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
except subprocess.CalledProcessError as e:
|
| 52 |
+
print(f"[ERROR] 脚本 {script} 崩溃: {e}")
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
def extract_metrics(json_path, script, label_name):
|
| 56 |
+
"""从重命名后的唯一 JSON 文件中读取数据"""
|
| 57 |
+
if not json_path or not json_path.exists():
|
| 58 |
+
return {"规则名称": label_name, "状态": "失败"}
|
| 59 |
+
|
| 60 |
+
with open(json_path, 'r', encoding='utf-8') as f:
|
| 61 |
+
data = json.load(f)
|
| 62 |
+
|
| 63 |
+
total = len(data)
|
| 64 |
+
unsuitable = sum(1 for item in data if item["label"] == "Unsuitable")
|
| 65 |
+
suitable = sum(1 for item in data if item["label"] == "Suitable")
|
| 66 |
+
fp_rate = (unsuitable / total) if total > 0 else 0
|
| 67 |
+
|
| 68 |
+
return {
|
| 69 |
+
"规则脚本": script,
|
| 70 |
+
"规则维度": label_name,
|
| 71 |
+
"测试总数": total,
|
| 72 |
+
"误报数(Unsuitable)": unsuitable,
|
| 73 |
+
"正确通过数(Suitable)": suitable,
|
| 74 |
+
"误报率(FP Rate)": f"{fp_rate:.2%}",
|
| 75 |
+
"结果文件": json_path.name
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
def main():
|
| 79 |
+
final_results = []
|
| 80 |
+
|
| 81 |
+
for script, label_name in TASK_CONFIGS:
|
| 82 |
+
unique_json_path = run_vllm_task(script, label_name)
|
| 83 |
+
|
| 84 |
+
if unique_json_path:
|
| 85 |
+
metrics = extract_metrics(unique_json_path, script, label_name)
|
| 86 |
+
final_results.append(metrics)
|
| 87 |
+
|
| 88 |
+
if final_results:
|
| 89 |
+
df = pd.DataFrame(final_results)
|
| 90 |
+
df.to_csv(SUMMARY_OUTPUT, index=False, encoding='utf-8-sig')
|
| 91 |
+
|
| 92 |
+
print("\n" + "="*80)
|
| 93 |
+
print(f"测试完成!汇总已写入: {SUMMARY_OUTPUT}")
|
| 94 |
+
print("-" * 80)
|
| 95 |
+
print(df.to_string(index=False))
|
| 96 |
+
print("="*80)
|
| 97 |
+
else:
|
| 98 |
+
print("[CRITICAL] 没有收集到任何有效数据。")
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
main()
|
code_inference/rule_11_vllm.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
使用方法:
|
| 4 |
+
python rule_11_vllm.py \
|
| 5 |
+
--input_dir "/path/to/your/images" \
|
| 6 |
+
--model_path "/path/to/your/Qwen2.5-VL-7B-Instruct"
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
import json
|
| 12 |
+
import argparse
|
| 13 |
+
import multiprocessing
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Dict, List, Optional, Any
|
| 16 |
+
from tqdm import tqdm
|
| 17 |
+
from PIL import Image, ImageFile
|
| 18 |
+
from transformers import AutoProcessor
|
| 19 |
+
from vllm import LLM, SamplingParams
|
| 20 |
+
|
| 21 |
+
os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'
|
| 22 |
+
|
| 23 |
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 24 |
+
|
| 25 |
+
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
SYS_PROMPT_TEXT ="""You are a highly critical Senior Art Director and Visual Auditor.
|
| 29 |
+
Your task is to evaluate "Text Visual Weight & Layout Balance" to prevent visual overcrowding while allowing for artistic typographic choices.
|
| 30 |
+
|
| 31 |
+
INPUT: One image and one natural-language question about text density or layout balance.
|
| 32 |
+
|
| 33 |
+
YOUR TASK:
|
| 34 |
+
1. Analyze the visual weight of the text relative to the canvas (Area coverage + Visual heaviness).
|
| 35 |
+
2. Apply the "Aesthetic Filter": Distinguish between "Cheap Da Zi Bao" (Violation) and "High-End Artistic Text" (Safe).
|
| 36 |
+
3. Determine if the image is a **VIOLATION** (Unsuitable) or **SAFE** (Suitable).
|
| 37 |
+
4. Output a JSON object containing a rigorous Chain-of-Thought ("think") and a precise classification label ("answer").
|
| 38 |
+
|
| 39 |
+
OUTPUT FORMAT:
|
| 40 |
+
Return EXACTLY two blocks, no extra text:
|
| 41 |
+
<think>Detailed reasoning steps: 1. Estimate text area coverage -> 2. Assess design quality (Suffocating vs. Artistic) -> 3. Check for product obstruction...</think><answer>{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Text Visual Weight"}</answer>
|
| 42 |
+
|
| 43 |
+
=========================================
|
| 44 |
+
CORE PRINCIPLE: BALANCE VS. SUFFOCATION
|
| 45 |
+
=========================================
|
| 46 |
+
- **The Rule:** Marketing text should generally occupy < 25% of the visual weight.
|
| 47 |
+
- **The Exception:** Large text IS allowed if it is "Concise, Exquisite, and High-End" (Magazine Style).
|
| 48 |
+
- **The Prohibition:** Large text is FORBIDDEN if it is "Crowded, Aggressive, and Cheap" (Da Zi Bao Style).
|
| 49 |
+
- **Maximum Text Density:** Regardless of artistic quality, any image containing more than 6 lines of narrative text or 50 words is automatically a VIOLATION (Information Overload).
|
| 50 |
+
- **Literal Line Counting:** Each line in a bulleted list or paragraph counts as 1 line. A neatly organized list of 10 lines is still a VIOLATION of the 6-line limit.
|
| 51 |
+
|
| 52 |
+
=========================================
|
| 53 |
+
STRICT DECISION HIERARCHY (FOLLOW IN ORDER)
|
| 54 |
+
=========================================
|
| 55 |
+
1. HARD LIMIT CHECK:
|
| 56 |
+
- Does the image have > 6 lines of text total (including text inside phone/UI)?
|
| 57 |
+
- If YES -> Label: UNSUITABLE (Reason: Text Density Overload).
|
| 58 |
+
- Zero UI Exemption: Text inside phone screens or UI mockups is NOT background decoration; it is active text weight. If the phone screen is filled with more than 4-5 lines of content, the entire image is likely UNSUITABLE.
|
| 59 |
+
|
| 60 |
+
2. VISUAL WEIGHT CHECK:
|
| 61 |
+
- Does the text (and its background boxes/screens) occupy more than 30% of the canvas?
|
| 62 |
+
- If YES -> Label: UNSUITABLE (Reason: Excessive Visual Weight).
|
| 63 |
+
|
| 64 |
+
3. AESTHETIC FILTER (The "Premium" Test):
|
| 65 |
+
- Is it "Artistic Exception"? ONLY if text is < 3 lines AND elegantly integrated.
|
| 66 |
+
- Note: A phone screen filled with tiny text is NEVER "Artistic" or "High-End" in an ad context; it is a "Manual Page" (UNSUITABLE).
|
| 67 |
+
=========================================
|
| 68 |
+
CRITERIA FOR 'UNSUITABLE' (VIOLATION / OVERWHELMING)
|
| 69 |
+
=========================================
|
| 70 |
+
1. **Aggressive "Da Zi Bao" (大字报) Style:**
|
| 71 |
+
- **Visual Suffocation:** Massive, bold text occupies the central area with zero "breathing room" (negative space).
|
| 72 |
+
- **Cheap Aesthetic:** It looks like a spam flyer or a shouting warning sign rather than a professional ad.
|
| 73 |
+
- **Shouting Effect:** The font size is absurdly large relative to the canvas without any artistic justification.
|
| 74 |
+
|
| 75 |
+
2. **Visual Obstruction & Imbalance:**
|
| 76 |
+
- **Blocking the Hero:** Text covers the main product, model's face, or key visual storytelling elements.
|
| 77 |
+
- **Excessive Weight:** The text area visually dominates > 30-40% of the canvas in a messy, cluttered way.
|
| 78 |
+
|
| 79 |
+
3.**The "Manual/Article" Trap:**
|
| 80 |
+
- Images that look like an instruction manual page, a reading app screenshot, or a news article are automatically UNSUITABLE. Ads must remain "Visual-First," not "Text-First."
|
| 81 |
+
=========================================
|
| 82 |
+
CRITERIA FOR 'SUITABLE' (SAFE / BALANCED)
|
| 83 |
+
=========================================
|
| 84 |
+
1. **Standard Good Ratio:**
|
| 85 |
+
- **Balanced:** Text occupies a reasonable area (roughly < 25% of visual weight).
|
| 86 |
+
- **Clear Hierarchy:** The Product/Illustration is the HERO; the Text is the SUPPORT.
|
| 87 |
+
|
| 88 |
+
2. **The "Artistic Exception" (High-End Large Text):**
|
| 89 |
+
- **Premium Look:** Even if the headline is large, it is concise, elegant, and integrated well with the background.
|
| 90 |
+
- **Breathing Room:** The layout maintains generous margins and negative space. It feels like a Vogue cover or a movie poster, not a supermarket discount flyer.
|
| 91 |
+
|
| 92 |
+
=========================================
|
| 93 |
+
DECISION LOGIC
|
| 94 |
+
=========================================
|
| 95 |
+
- **Unsuitable**: If the text creates a "suffocating" effect, blocks the product, or looks like a cheap, crowded "Da Zi Bao".
|
| 96 |
+
- **Suitable**: If the text is minimal (<25%), OR if it is large but designed with high artistic quality and ample negative space.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
def collect_images(input_dir: Path) -> List[Dict[str, str]]:
|
| 100 |
+
if not input_dir.exists():
|
| 101 |
+
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
| 102 |
+
|
| 103 |
+
files = [p for p in input_dir.iterdir() if p.is_file() and p.suffix.lower() in IMG_EXTS]
|
| 104 |
+
files.sort()
|
| 105 |
+
|
| 106 |
+
print(f"[Info] Found {len(files)} images in {input_dir}")
|
| 107 |
+
return [{"path": str(p), "filename": p.name} for p in files]
|
| 108 |
+
|
| 109 |
+
def parse_llm_output(text: str) -> Dict[str, Any]:
|
| 110 |
+
default_res = {
|
| 111 |
+
"label": "Parse Error",
|
| 112 |
+
"think": "No reasoning found",
|
| 113 |
+
"raw": text
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
if not text:
|
| 117 |
+
return default_res
|
| 118 |
+
|
| 119 |
+
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
|
| 120 |
+
think_content = think_match.group(1).strip() if think_match else ""
|
| 121 |
+
|
| 122 |
+
answer_match = re.search(r'<answer>(.*?)</answer>', text, re.DOTALL)
|
| 123 |
+
|
| 124 |
+
extracted_label = "Parse Error"
|
| 125 |
+
|
| 126 |
+
if answer_match:
|
| 127 |
+
json_str = answer_match.group(1).strip()
|
| 128 |
+
try:
|
| 129 |
+
|
| 130 |
+
data = json.loads(json_str)
|
| 131 |
+
raw_ans = data.get("Answer", "")
|
| 132 |
+
|
| 133 |
+
if "unsuitable" in raw_ans.lower():
|
| 134 |
+
extracted_label = "Unsuitable"
|
| 135 |
+
elif "suitable" in raw_ans.lower():
|
| 136 |
+
extracted_label = "Suitable"
|
| 137 |
+
else:
|
| 138 |
+
extracted_label = raw_ans
|
| 139 |
+
|
| 140 |
+
except json.JSONDecodeError:
|
| 141 |
+
if "Unsuitable" in json_str:
|
| 142 |
+
extracted_label = "Unsuitable"
|
| 143 |
+
elif "Suitable" in json_str:
|
| 144 |
+
extracted_label = "Suitable"
|
| 145 |
+
else:
|
| 146 |
+
if "Unsuitable" in text:
|
| 147 |
+
extracted_label = "Unsuitable"
|
| 148 |
+
elif "Suitable" in text:
|
| 149 |
+
extracted_label = "Suitable"
|
| 150 |
+
|
| 151 |
+
return {
|
| 152 |
+
"label": extracted_label,
|
| 153 |
+
"think": think_content,
|
| 154 |
+
"raw": text
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
def prepare_vllm_inputs(batch_meta: List[Dict], processor) -> List[Dict]:
|
| 158 |
+
vllm_inputs = []
|
| 159 |
+
user_query = "Analyze this image against the design rules and return the JSON decision."
|
| 160 |
+
|
| 161 |
+
for item in batch_meta:
|
| 162 |
+
img_path = item["path"]
|
| 163 |
+
try:
|
| 164 |
+
image_obj = Image.open(img_path).convert("RGB")
|
| 165 |
+
|
| 166 |
+
messages = [
|
| 167 |
+
{"role": "system", "content": [{"type": "text", "text": SYS_PROMPT_TEXT}]},
|
| 168 |
+
{"role": "user", "content": [
|
| 169 |
+
{"type": "image", "image": img_path},
|
| 170 |
+
{"type": "text", "text": user_query}
|
| 171 |
+
]}
|
| 172 |
+
]
|
| 173 |
+
|
| 174 |
+
prompt_text = processor.apply_chat_template(
|
| 175 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
vllm_inputs.append({
|
| 179 |
+
"prompt": prompt_text,
|
| 180 |
+
"multi_modal_data": {"image": image_obj}
|
| 181 |
+
})
|
| 182 |
+
except Exception as e:
|
| 183 |
+
print(f"[Warning] Failed to load {img_path}: {e}")
|
| 184 |
+
vllm_inputs.append(None)
|
| 185 |
+
|
| 186 |
+
return vllm_inputs
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def main():
|
| 190 |
+
parser = argparse.ArgumentParser(description="AI Visual Comfort Auditor")
|
| 191 |
+
parser.add_argument("--input_dir", type=str, required=True, help="Folder containing images to check")
|
| 192 |
+
parser.add_argument("--model_path", type=str, required=True, help="Path to local Qwen-VL model")
|
| 193 |
+
parser.add_argument("--batch_size", type=int, default=512, help="Inference batch size")
|
| 194 |
+
parser.add_argument("--tp_size", type=int, default=2, help="Tensor Parallel size")
|
| 195 |
+
args = parser.parse_args()
|
| 196 |
+
|
| 197 |
+
input_path = Path(args.input_dir)
|
| 198 |
+
meta_data = collect_images(input_path)
|
| 199 |
+
|
| 200 |
+
if not meta_data:
|
| 201 |
+
print("[Info] No images found. Exiting.")
|
| 202 |
+
return
|
| 203 |
+
|
| 204 |
+
# ---------------------------
|
| 205 |
+
# 初始化模型
|
| 206 |
+
# ---------------------------
|
| 207 |
+
print(f"\n[Init] Loading Model: {args.model_path}")
|
| 208 |
+
|
| 209 |
+
llm = LLM(
|
| 210 |
+
model=args.model_path,
|
| 211 |
+
tokenizer=args.model_path,
|
| 212 |
+
trust_remote_code=True,
|
| 213 |
+
tensor_parallel_size=args.tp_size,
|
| 214 |
+
gpu_memory_utilization=0.90,
|
| 215 |
+
max_model_len=8192,
|
| 216 |
+
enforce_eager=True,
|
| 217 |
+
limit_mm_per_prompt={"image": 1}
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
processor = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True)
|
| 221 |
+
|
| 222 |
+
# 采样参数
|
| 223 |
+
sampling_params = SamplingParams(
|
| 224 |
+
temperature=0.7, # 稍微降低温度以获得更稳定的分类
|
| 225 |
+
max_tokens=1024,
|
| 226 |
+
top_p=0.9
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
# ---------------------------
|
| 230 |
+
# 批量推理
|
| 231 |
+
# ---------------------------
|
| 232 |
+
results = []
|
| 233 |
+
print(f"\n[Run] Starting Inference on {len(meta_data)} images...")
|
| 234 |
+
|
| 235 |
+
for i in tqdm(range(0, len(meta_data), args.batch_size), desc="Processing Batches"):
|
| 236 |
+
batch_meta = meta_data[i : i + args.batch_size]
|
| 237 |
+
batch_inputs = prepare_vllm_inputs(batch_meta, processor)
|
| 238 |
+
|
| 239 |
+
valid_inputs = [inp for inp in batch_inputs if inp is not None]
|
| 240 |
+
valid_indices = [idx for idx, inp in enumerate(batch_inputs) if inp is not None]
|
| 241 |
+
|
| 242 |
+
if not valid_inputs:
|
| 243 |
+
continue
|
| 244 |
+
|
| 245 |
+
outputs = llm.generate(valid_inputs, sampling_params=sampling_params, use_tqdm=False)
|
| 246 |
+
|
| 247 |
+
for local_idx, out in enumerate(outputs):
|
| 248 |
+
original_meta = batch_meta[valid_indices[local_idx]]
|
| 249 |
+
generated_text = out.outputs[0].text
|
| 250 |
+
|
| 251 |
+
# 解析结果
|
| 252 |
+
parsed = parse_llm_output(generated_text)
|
| 253 |
+
|
| 254 |
+
results.append({
|
| 255 |
+
"filename": original_meta["filename"],
|
| 256 |
+
"path": original_meta["path"],
|
| 257 |
+
"label": parsed["label"], # Suitable / Unsuitable
|
| 258 |
+
"think": parsed["think"], # 思维链
|
| 259 |
+
"raw_output": generated_text
|
| 260 |
+
})
|
| 261 |
+
|
| 262 |
+
# ---------------------------
|
| 263 |
+
# 统计与输出
|
| 264 |
+
# ---------------------------
|
| 265 |
+
total = len(results)
|
| 266 |
+
unsuitable_count = sum(1 for r in results if r["label"] == "Unsuitable")
|
| 267 |
+
suitable_count = sum(1 for r in results if r["label"] == "Suitable")
|
| 268 |
+
error_count = total - unsuitable_count - suitable_count
|
| 269 |
+
|
| 270 |
+
unsuitable_rate = (unsuitable_count / total * 100) if total > 0 else 0
|
| 271 |
+
suitable_rate = (suitable_count / total * 100) if total > 0 else 0
|
| 272 |
+
|
| 273 |
+
print("\n" + "="*60)
|
| 274 |
+
print(f"AUDIT REPORT FOR: {input_path.name}")
|
| 275 |
+
print("="*60)
|
| 276 |
+
print(f"{'Total Images':<25}: {total}")
|
| 277 |
+
print("-" * 60)
|
| 278 |
+
print(f"{'UNSUITABLE (Violation)':<25}: {unsuitable_count} ({unsuitable_rate:.2f}%)")
|
| 279 |
+
print(f"{'SUITABLE (Safe)':<25}: {suitable_count} ({suitable_rate:.2f}%)")
|
| 280 |
+
print(f"{'Parse Errors':<25}: {error_count}")
|
| 281 |
+
print("="*60)
|
| 282 |
+
|
| 283 |
+
# 保存结果
|
| 284 |
+
output_file = input_path / f"audit_result_{input_path.name}.json"
|
| 285 |
+
try:
|
| 286 |
+
with open(output_file, "w", encoding="utf-8") as f:
|
| 287 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 288 |
+
print(f"\n[Done] Detailed JSON report saved to:\n-> {output_file}")
|
| 289 |
+
except Exception as e:
|
| 290 |
+
print(f"[Error] Could not save JSON: {e}")
|
| 291 |
+
|
| 292 |
+
if __name__ == "__main__":
|
| 293 |
+
try:
|
| 294 |
+
multiprocessing.set_start_method('spawn', force=True)
|
| 295 |
+
except RuntimeError:
|
| 296 |
+
pass
|
| 297 |
+
|
| 298 |
+
main()
|
code_inference/rule_12_vllm.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
使用方法:
|
| 5 |
+
python rule_12_vllm.py \
|
| 6 |
+
--input_dir "/path/to/your/images" \
|
| 7 |
+
--model_path "/path/to/your/Qwen2.5-VL-7B-Instruct"
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import re
|
| 12 |
+
import json
|
| 13 |
+
import argparse
|
| 14 |
+
import multiprocessing
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Dict, List, Optional, Any
|
| 17 |
+
from tqdm import tqdm
|
| 18 |
+
from PIL import Image, ImageFile
|
| 19 |
+
from transformers import AutoProcessor
|
| 20 |
+
from vllm import LLM, SamplingParams
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'
|
| 24 |
+
|
| 25 |
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 26 |
+
|
| 27 |
+
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
SYS_PROMPT_TEXT ="""You are a highly critical Senior Art Director and Visual Auditor. Your core focus is Information Hierarchy and Typographic Purity.
|
| 32 |
+
You have ZERO TOLERANCE for "Visual Noise" caused by excessive font types that increase the cost of information filtering.
|
| 33 |
+
|
| 34 |
+
INPUT: One image and one natural-language question about typographic style and font count.
|
| 35 |
+
|
| 36 |
+
YOUR TASK:
|
| 37 |
+
1. Determine if the image is a **VIOLATION** (Unsuitable) or **SAFE** (Suitable) based on the criteria below.
|
| 38 |
+
2. Output a JSON object containing a rigorous Chain-of-Thought ("think") and a precise classification label ("answer").
|
| 39 |
+
|
| 40 |
+
CORE PRINCIPLE: The main text of an advertisement image must NOT exceed 2 different font categories.
|
| 41 |
+
|
| 42 |
+
OUTPUT FORMAT:
|
| 43 |
+
Return EXACTLY two blocks, no extra text:
|
| 44 |
+
<think>Detailed reasoning identifying the specific font categories used in the main text and counting the total variety...</think><answer>{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Font Style Consistency"}</answer>
|
| 45 |
+
|
| 46 |
+
=========================================
|
| 47 |
+
FONT CATEGORY DEFINITIONS (Total 4 Categories)
|
| 48 |
+
=========================================
|
| 49 |
+
1. **Sans-Serif (无衬线体):** Modern, uniform stroke thickness (e.g., Heiti/黑体, Youyuan/幼圆).
|
| 50 |
+
2. **Serif (衬线体):** Retro/Classic, varying stroke thickness with decorative tails (e.g., Songti/宋体).
|
| 51 |
+
3. **Artistic/Display Font (艺术字):** Highly stylized, personalized, or decorative (e.g., Gothic, bubble fonts, irregular proportions).
|
| 52 |
+
4. **Handwritten/Calligraphy (手写体/书法体):** Brush-like strokes, traditional or casual handwriting styles.
|
| 53 |
+
|
| 54 |
+
=========================================
|
| 55 |
+
STRICT VIOLATION CRITERIA (If ANY match -> Unsuitable)
|
| 56 |
+
=========================================
|
| 57 |
+
1. **Excessive Font Variety (字体种类超标):**
|
| 58 |
+
- **Violation:** The main text in the image uses **three or more (3+)** of the aforementioned font categories simultaneously (e.g., Sans-serif + Serif + Calligraphy all in one ad).
|
| 59 |
+
- **Exclusions:** This rule EXCLUDES text naturally printed on the product packaging, brand logos, and secondary small text (annotations/footnotes). Only the main promotional copy is evaluated.
|
| 60 |
+
- **Visual Effect:** The typography feels cluttered, inconsistent, or lacks a dominant style, creating visual noise.
|
| 61 |
+
|
| 62 |
+
=========================================
|
| 63 |
+
CRITERIA FOR 'SUITABLE' (NON-VIOLATION / GOOD DESIGN)
|
| 64 |
+
=========================================
|
| 65 |
+
- **Unified Style:** The main text strictly utilizes only **1 or 2** font categories (e.g., only Sans-serif, or Sans-serif body text + Calligraphy headline).
|
| 66 |
+
|
| 67 |
+
=========================================
|
| 68 |
+
DECISION LOGIC
|
| 69 |
+
=========================================
|
| 70 |
+
- **Unsuitable**: If the main promotional text mixes 3 or more distinct font categories, resulting in chaotic styling.
|
| 71 |
+
- **Suitable**: If the typography is restrained, using 1 to 2 font categories for a clean and cohesive information hierarchy.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
def collect_images(input_dir: Path) -> List[Dict[str, str]]:
|
| 75 |
+
if not input_dir.exists():
|
| 76 |
+
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
| 77 |
+
|
| 78 |
+
files = [p for p in input_dir.iterdir() if p.is_file() and p.suffix.lower() in IMG_EXTS]
|
| 79 |
+
files.sort()
|
| 80 |
+
|
| 81 |
+
print(f"[Info] Found {len(files)} images in {input_dir}")
|
| 82 |
+
return [{"path": str(p), "filename": p.name} for p in files]
|
| 83 |
+
|
| 84 |
+
def parse_llm_output(text: str) -> Dict[str, Any]:
|
| 85 |
+
default_res = {
|
| 86 |
+
"label": "Parse Error",
|
| 87 |
+
"think": "No reasoning found",
|
| 88 |
+
"raw": text
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if not text:
|
| 92 |
+
return default_res
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
|
| 96 |
+
think_content = think_match.group(1).strip() if think_match else ""
|
| 97 |
+
|
| 98 |
+
answer_match = re.search(r'<answer>(.*?)</answer>', text, re.DOTALL)
|
| 99 |
+
|
| 100 |
+
extracted_label = "Parse Error"
|
| 101 |
+
|
| 102 |
+
if answer_match:
|
| 103 |
+
json_str = answer_match.group(1).strip()
|
| 104 |
+
try:
|
| 105 |
+
|
| 106 |
+
data = json.loads(json_str)
|
| 107 |
+
|
| 108 |
+
raw_ans = data.get("Answer", "")
|
| 109 |
+
if "unsuitable" in raw_ans.lower():
|
| 110 |
+
extracted_label = "Unsuitable"
|
| 111 |
+
elif "suitable" in raw_ans.lower():
|
| 112 |
+
extracted_label = "Suitable"
|
| 113 |
+
else:
|
| 114 |
+
extracted_label = raw_ans
|
| 115 |
+
|
| 116 |
+
except json.JSONDecodeError:
|
| 117 |
+
if "Unsuitable" in json_str:
|
| 118 |
+
extracted_label = "Unsuitable"
|
| 119 |
+
elif "Suitable" in json_str:
|
| 120 |
+
extracted_label = "Suitable"
|
| 121 |
+
else:
|
| 122 |
+
|
| 123 |
+
if "Unsuitable" in text:
|
| 124 |
+
extracted_label = "Unsuitable"
|
| 125 |
+
elif "Suitable" in text:
|
| 126 |
+
extracted_label = "Suitable"
|
| 127 |
+
|
| 128 |
+
return {
|
| 129 |
+
"label": extracted_label,
|
| 130 |
+
"think": think_content,
|
| 131 |
+
"raw": text
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
def prepare_vllm_inputs(batch_meta: List[Dict], processor) -> List[Dict]:
|
| 135 |
+
vllm_inputs = []
|
| 136 |
+
|
| 137 |
+
user_query = "Is this image visually comfortable and suitable for information display?"
|
| 138 |
+
|
| 139 |
+
for item in batch_meta:
|
| 140 |
+
img_path = item["path"]
|
| 141 |
+
try:
|
| 142 |
+
image_obj = Image.open(img_path).convert("RGB")
|
| 143 |
+
|
| 144 |
+
messages = [
|
| 145 |
+
{"role": "system", "content": [{"type": "text", "text": SYS_PROMPT_TEXT}]},
|
| 146 |
+
{"role": "user", "content": [
|
| 147 |
+
{"type": "image", "image": img_path},
|
| 148 |
+
{"type": "text", "text": user_query}
|
| 149 |
+
]}
|
| 150 |
+
]
|
| 151 |
+
|
| 152 |
+
prompt_text = processor.apply_chat_template(
|
| 153 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
vllm_inputs.append({
|
| 157 |
+
"prompt": prompt_text,
|
| 158 |
+
"multi_modal_data": {"image": image_obj}
|
| 159 |
+
})
|
| 160 |
+
except Exception as e:
|
| 161 |
+
print(f"[Warning] Failed to load {img_path}: {e}")
|
| 162 |
+
vllm_inputs.append(None)
|
| 163 |
+
|
| 164 |
+
return vllm_inputs
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def main():
|
| 168 |
+
parser = argparse.ArgumentParser(description="AI Visual Comfort Auditor")
|
| 169 |
+
parser.add_argument("--input_dir", type=str, required=True, help="Folder containing images to check")
|
| 170 |
+
parser.add_argument("--model_path", type=str, required=True, help="Path to local Qwen-VL model")
|
| 171 |
+
parser.add_argument("--batch_size", type=int, default=512, help="Inference batch size")
|
| 172 |
+
parser.add_argument("--tp_size", type=int, default=2, help="Tensor Parallel size")
|
| 173 |
+
args = parser.parse_args()
|
| 174 |
+
|
| 175 |
+
input_path = Path(args.input_dir)
|
| 176 |
+
meta_data = collect_images(input_path)
|
| 177 |
+
|
| 178 |
+
if not meta_data:
|
| 179 |
+
print("[Info] No images found. Exiting.")
|
| 180 |
+
return
|
| 181 |
+
|
| 182 |
+
print(f"\n[Init] Loading Model: {args.model_path}")
|
| 183 |
+
|
| 184 |
+
llm = LLM(
|
| 185 |
+
model=args.model_path,
|
| 186 |
+
tokenizer=args.model_path,
|
| 187 |
+
trust_remote_code=True,
|
| 188 |
+
tensor_parallel_size=args.tp_size,
|
| 189 |
+
gpu_memory_utilization=0.90,
|
| 190 |
+
max_model_len=8192,
|
| 191 |
+
enforce_eager=True,
|
| 192 |
+
limit_mm_per_prompt={"image": 1}
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
processor = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
sampling_params = SamplingParams(
|
| 199 |
+
temperature=0.7,
|
| 200 |
+
max_tokens=1024,
|
| 201 |
+
top_p=0.9
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
results = []
|
| 205 |
+
print(f"\n[Run] Starting Inference on {len(meta_data)} images...")
|
| 206 |
+
|
| 207 |
+
for i in tqdm(range(0, len(meta_data), args.batch_size), desc="Processing Batches"):
|
| 208 |
+
batch_meta = meta_data[i : i + args.batch_size]
|
| 209 |
+
batch_inputs = prepare_vllm_inputs(batch_meta, processor)
|
| 210 |
+
|
| 211 |
+
valid_inputs = [inp for inp in batch_inputs if inp is not None]
|
| 212 |
+
valid_indices = [idx for idx, inp in enumerate(batch_inputs) if inp is not None]
|
| 213 |
+
|
| 214 |
+
if not valid_inputs:
|
| 215 |
+
continue
|
| 216 |
+
|
| 217 |
+
outputs = llm.generate(valid_inputs, sampling_params=sampling_params, use_tqdm=False)
|
| 218 |
+
|
| 219 |
+
for local_idx, out in enumerate(outputs):
|
| 220 |
+
original_meta = batch_meta[valid_indices[local_idx]]
|
| 221 |
+
generated_text = out.outputs[0].text
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
parsed = parse_llm_output(generated_text)
|
| 225 |
+
|
| 226 |
+
results.append({
|
| 227 |
+
"filename": original_meta["filename"],
|
| 228 |
+
"path": original_meta["path"],
|
| 229 |
+
"label": parsed["label"], # Suitable / Unsuitable
|
| 230 |
+
"think": parsed["think"],
|
| 231 |
+
"raw_output": generated_text
|
| 232 |
+
})
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
total = len(results)
|
| 236 |
+
unsuitable_count = sum(1 for r in results if r["label"] == "Unsuitable")
|
| 237 |
+
suitable_count = sum(1 for r in results if r["label"] == "Suitable")
|
| 238 |
+
error_count = total - unsuitable_count - suitable_count
|
| 239 |
+
|
| 240 |
+
unsuitable_rate = (unsuitable_count / total * 100) if total > 0 else 0
|
| 241 |
+
suitable_rate = (suitable_count / total * 100) if total > 0 else 0
|
| 242 |
+
|
| 243 |
+
print("\n" + "="*60)
|
| 244 |
+
print(f"AUDIT REPORT FOR: {input_path.name}")
|
| 245 |
+
print("="*60)
|
| 246 |
+
print(f"{'Total Images':<25}: {total}")
|
| 247 |
+
print("-" * 60)
|
| 248 |
+
print(f"{'UNSUITABLE (Violation)':<25}: {unsuitable_count} ({unsuitable_rate:.2f}%)")
|
| 249 |
+
print(f"{'SUITABLE (Safe)':<25}: {suitable_count} ({suitable_rate:.2f}%)")
|
| 250 |
+
print(f"{'Parse Errors':<25}: {error_count}")
|
| 251 |
+
print("="*60)
|
| 252 |
+
|
| 253 |
+
output_file = input_path / f"audit_result_{input_path.name}.json"
|
| 254 |
+
try:
|
| 255 |
+
with open(output_file, "w", encoding="utf-8") as f:
|
| 256 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 257 |
+
print(f"\n[Done] Detailed JSON report saved to:\n-> {output_file}")
|
| 258 |
+
except Exception as e:
|
| 259 |
+
print(f"[Error] Could not save JSON: {e}")
|
| 260 |
+
|
| 261 |
+
if __name__ == "__main__":
|
| 262 |
+
try:
|
| 263 |
+
multiprocessing.set_start_method('spawn', force=True)
|
| 264 |
+
except RuntimeError:
|
| 265 |
+
pass
|
| 266 |
+
|
| 267 |
+
main()
|
code_inference/rule_13_vllm.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
使用方法:
|
| 4 |
+
python rule_13_vllm.py \
|
| 5 |
+
--input_dir "/path/to/your/images" \
|
| 6 |
+
--model_path "/path/to/your/Qwen2.5-VL-7B-Instruct"
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
import json
|
| 12 |
+
import argparse
|
| 13 |
+
import multiprocessing
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Dict, List, Optional, Any
|
| 16 |
+
from tqdm import tqdm
|
| 17 |
+
from PIL import Image, ImageFile
|
| 18 |
+
from transformers import AutoProcessor
|
| 19 |
+
from vllm import LLM, SamplingParams
|
| 20 |
+
|
| 21 |
+
os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'
|
| 22 |
+
|
| 23 |
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 24 |
+
|
| 25 |
+
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
|
| 26 |
+
|
| 27 |
+
#排布位置
|
| 28 |
+
SYS_PROMPT_TEXT="""You are a highly critical Senior Art Director specializing in Layout and Visual Hierarchy.
|
| 29 |
+
Your job is to identify "Suffocating Designs"—creative pieces where elements are too cramped, lack breathing room, or feel disorganized due to poor spacing.
|
| 30 |
+
|
| 31 |
+
INPUT: One image and one natural-language question about layout composition and spacing.
|
| 32 |
+
|
| 33 |
+
YOUR TASK:
|
| 34 |
+
1. Determine if the image is a **VIOLATION** (Unsuitable) or **SAFE** (Suitable) based on the criteria below.
|
| 35 |
+
2. Output a JSON object containing a rigorous Chain-of-Thought ("think") and a precise classification label ("answer").
|
| 36 |
+
|
| 37 |
+
CORE JUDGMENT PRINCIPLE: A professional advertisement must have a clear "Sense of Breath" (Negative Space). If the elements feel "squeezed" or "crowded," it is UNSUITABLE.
|
| 38 |
+
|
| 39 |
+
OUTPUT FORMAT:
|
| 40 |
+
Return EXACTLY two blocks, no extra text:
|
| 41 |
+
<think>Detailed reasoning evaluating negative space, element proximity, visual path, and edge tension...</think><answer>{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Text Legibility and Placement"}</answer>
|
| 42 |
+
|
| 43 |
+
=========================================
|
| 44 |
+
STRICT VIOLATION CRITERIA (If ANY match -> Unsuitable)
|
| 45 |
+
=========================================
|
| 46 |
+
1. **Lack of Breathing Room (Core Crowding Violation):**
|
| 47 |
+
- **Major Module Conflict:** The main subject (product/hero), the headline text, and the logo are placed too close to each other, lacking deliberate negative space.
|
| 48 |
+
- **Claustrophobic Feel:** The overall design feels "heavy" or "squeezed" because these major elements are fighting for space.
|
| 49 |
+
|
| 50 |
+
2. **The "Clinging" Small Print (Secondary Text Violation):**
|
| 51 |
+
- **Tangency Risk:** While small text *can* be closer than headlines, it becomes a VIOLATION if it is "clinging" to, touching, or "tangent" to other elements or the image border.
|
| 52 |
+
- **Visual Noise:** Small text is squeezed into gaps without enough margin, looking like an afterthought rather than a design choice.
|
| 53 |
+
|
| 54 |
+
3. **Edge Tension (贴边风险):**
|
| 55 |
+
- Elements are "touching" the canvas border or each other without a clear, intentional overlap (e.g., accidental contact).
|
| 56 |
+
|
| 57 |
+
4. **Information Overload (信息堆砌):**
|
| 58 |
+
- **No Visual Path:** The layout is filled with too many text blocks or icons with no clear separation or hierarchy. The eye has nowhere to rest.
|
| 59 |
+
|
| 60 |
+
5. **Placement & Background Conflict (文字-排布位置与背景):** [NEW CRITICAL RULE]
|
| 61 |
+
- **Text on Noise (背景干扰):** Text is overlaid directly onto a complex, textured, or high-contrast background (e.g., tree branches, detailed patterns) without a drop shadow or mask, making it "hard to breathe/read".
|
| 62 |
+
- **Weak Visual Anchor (视线捕捉失败):** Important text (Headline) is placed in a "dead zone" (extreme edges/corners) or blends into the background, failing to capture the eye immediately.
|
| 63 |
+
|
| 64 |
+
=========================================
|
| 65 |
+
CRITERIA FOR 'SUITABLE' (NON-VIOLATION / GOOD DESIGN)
|
| 66 |
+
=========================================
|
| 67 |
+
1. **Generous White Space (Major Elements):**
|
| 68 |
+
- Clear and deliberate separation exists between the Headline, Main Subject, and Footer.
|
| 69 |
+
|
| 70 |
+
2. **Permissible Density (Small Print Exemption):**
|
| 71 |
+
- **Nuance:** Secondary small text (annotations/footnotes) IS ALLOWED to have smaller gaps relative to other elements (unlike headlines). As long as it doesn't touch/cling (see Violation #2), tighter spacing for small text is SAFE.
|
| 72 |
+
|
| 73 |
+
3. **Valid Exclusions:**
|
| 74 |
+
- **Product Packaging:** Text printed naturally on the product packaging is SAFE.
|
| 75 |
+
- **Artistic Integration:** Artistic fonts visually integrated *into* the product itself are SAFE.
|
| 76 |
+
- **Media Exemption:** Film stills or variety show photography are always SAFE.
|
| 77 |
+
|
| 78 |
+
=========================================
|
| 79 |
+
DECISION LOGIC
|
| 80 |
+
=========================================
|
| 81 |
+
- **Unsuitable**: If the design feels squeezed, suffers from edge tension, lacks a visual path, or if small text "clings" to edges/elements.
|
| 82 |
+
- **Suitable**: If major elements breathe well, OR if the density is strictly limited to allowed small print/packaging text that doesn't create tension.
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def collect_images(input_dir: Path) -> List[Dict[str, str]]:
|
| 87 |
+
"""扫描目录下所有图片"""
|
| 88 |
+
if not input_dir.exists():
|
| 89 |
+
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
| 90 |
+
|
| 91 |
+
files = [p for p in input_dir.iterdir() if p.is_file() and p.suffix.lower() in IMG_EXTS]
|
| 92 |
+
files.sort()
|
| 93 |
+
|
| 94 |
+
print(f"[Info] Found {len(files)} images in {input_dir}")
|
| 95 |
+
return [{"path": str(p), "filename": p.name} for p in files]
|
| 96 |
+
|
| 97 |
+
def parse_llm_output(text: str) -> Dict[str, Any]:
|
| 98 |
+
default_res = {
|
| 99 |
+
"label": "Parse Error",
|
| 100 |
+
"think": "No reasoning found",
|
| 101 |
+
"raw": text
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
if not text:
|
| 105 |
+
return default_res
|
| 106 |
+
|
| 107 |
+
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
|
| 108 |
+
think_content = think_match.group(1).strip() if think_match else ""
|
| 109 |
+
|
| 110 |
+
answer_match = re.search(r'<answer>(.*?)</answer>', text, re.DOTALL)
|
| 111 |
+
|
| 112 |
+
extracted_label = "Parse Error"
|
| 113 |
+
|
| 114 |
+
if answer_match:
|
| 115 |
+
json_str = answer_match.group(1).strip()
|
| 116 |
+
try:
|
| 117 |
+
|
| 118 |
+
data = json.loads(json_str)
|
| 119 |
+
|
| 120 |
+
raw_ans = data.get("Answer", "")
|
| 121 |
+
|
| 122 |
+
if "unsuitable" in raw_ans.lower():
|
| 123 |
+
extracted_label = "Unsuitable"
|
| 124 |
+
elif "suitable" in raw_ans.lower():
|
| 125 |
+
extracted_label = "Suitable"
|
| 126 |
+
else:
|
| 127 |
+
extracted_label = raw_ans
|
| 128 |
+
|
| 129 |
+
except json.JSONDecodeError:
|
| 130 |
+
if "Unsuitable" in json_str:
|
| 131 |
+
extracted_label = "Unsuitable"
|
| 132 |
+
elif "Suitable" in json_str:
|
| 133 |
+
extracted_label = "Suitable"
|
| 134 |
+
else:
|
| 135 |
+
if "Unsuitable" in text:
|
| 136 |
+
extracted_label = "Unsuitable"
|
| 137 |
+
elif "Suitable" in text:
|
| 138 |
+
extracted_label = "Suitable"
|
| 139 |
+
|
| 140 |
+
return {
|
| 141 |
+
"label": extracted_label,
|
| 142 |
+
"think": think_content,
|
| 143 |
+
"raw": text
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
def prepare_vllm_inputs(batch_meta: List[Dict], processor) -> List[Dict]:
|
| 147 |
+
|
| 148 |
+
vllm_inputs = []
|
| 149 |
+
user_query = "Analyze this image against the design rules and return the JSON decision."
|
| 150 |
+
|
| 151 |
+
for item in batch_meta:
|
| 152 |
+
img_path = item["path"]
|
| 153 |
+
try:
|
| 154 |
+
image_obj = Image.open(img_path).convert("RGB")
|
| 155 |
+
|
| 156 |
+
messages = [
|
| 157 |
+
{"role": "system", "content": [{"type": "text", "text": SYS_PROMPT_TEXT}]},
|
| 158 |
+
{"role": "user", "content": [
|
| 159 |
+
{"type": "image", "image": img_path},
|
| 160 |
+
{"type": "text", "text": user_query}
|
| 161 |
+
]}
|
| 162 |
+
]
|
| 163 |
+
|
| 164 |
+
prompt_text = processor.apply_chat_template(
|
| 165 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
vllm_inputs.append({
|
| 169 |
+
"prompt": prompt_text,
|
| 170 |
+
"multi_modal_data": {"image": image_obj}
|
| 171 |
+
})
|
| 172 |
+
except Exception as e:
|
| 173 |
+
print(f"[Warning] Failed to load {img_path}: {e}")
|
| 174 |
+
vllm_inputs.append(None)
|
| 175 |
+
|
| 176 |
+
return vllm_inputs
|
| 177 |
+
|
| 178 |
+
# ==========================================
|
| 179 |
+
# 主程序
|
| 180 |
+
# ==========================================
|
| 181 |
+
|
| 182 |
+
def main():
|
| 183 |
+
parser = argparse.ArgumentParser(description="AI Visual Comfort Auditor")
|
| 184 |
+
parser.add_argument("--input_dir", type=str, required=True, help="Folder containing images to check")
|
| 185 |
+
parser.add_argument("--model_path", type=str, required=True, help="Path to local Qwen-VL model")
|
| 186 |
+
parser.add_argument("--batch_size", type=int, default=512, help="Inference batch size")
|
| 187 |
+
parser.add_argument("--tp_size", type=int, default=2, help="Tensor Parallel size")
|
| 188 |
+
args = parser.parse_args()
|
| 189 |
+
|
| 190 |
+
input_path = Path(args.input_dir)
|
| 191 |
+
meta_data = collect_images(input_path)
|
| 192 |
+
|
| 193 |
+
if not meta_data:
|
| 194 |
+
print("[Info] No images found. Exiting.")
|
| 195 |
+
return
|
| 196 |
+
print(f"\n[Init] Loading Model: {args.model_path}")
|
| 197 |
+
|
| 198 |
+
llm = LLM(
|
| 199 |
+
model=args.model_path,
|
| 200 |
+
tokenizer=args.model_path,
|
| 201 |
+
trust_remote_code=True,
|
| 202 |
+
tensor_parallel_size=args.tp_size,
|
| 203 |
+
gpu_memory_utilization=0.90,
|
| 204 |
+
max_model_len=8192,
|
| 205 |
+
enforce_eager=True,
|
| 206 |
+
limit_mm_per_prompt={"image": 1}
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
processor = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
sampling_params = SamplingParams(
|
| 213 |
+
temperature=0.7,
|
| 214 |
+
max_tokens=1024,
|
| 215 |
+
top_p=0.9
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
results = []
|
| 219 |
+
print(f"\n[Run] Starting Inference on {len(meta_data)} images...")
|
| 220 |
+
|
| 221 |
+
for i in tqdm(range(0, len(meta_data), args.batch_size), desc="Processing Batches"):
|
| 222 |
+
batch_meta = meta_data[i : i + args.batch_size]
|
| 223 |
+
batch_inputs = prepare_vllm_inputs(batch_meta, processor)
|
| 224 |
+
|
| 225 |
+
valid_inputs = [inp for inp in batch_inputs if inp is not None]
|
| 226 |
+
valid_indices = [idx for idx, inp in enumerate(batch_inputs) if inp is not None]
|
| 227 |
+
|
| 228 |
+
if not valid_inputs:
|
| 229 |
+
continue
|
| 230 |
+
|
| 231 |
+
outputs = llm.generate(valid_inputs, sampling_params=sampling_params, use_tqdm=False)
|
| 232 |
+
|
| 233 |
+
for local_idx, out in enumerate(outputs):
|
| 234 |
+
original_meta = batch_meta[valid_indices[local_idx]]
|
| 235 |
+
generated_text = out.outputs[0].text
|
| 236 |
+
|
| 237 |
+
parsed = parse_llm_output(generated_text)
|
| 238 |
+
|
| 239 |
+
results.append({
|
| 240 |
+
"filename": original_meta["filename"],
|
| 241 |
+
"path": original_meta["path"],
|
| 242 |
+
"label": parsed["label"], # Suitable / Unsuitable
|
| 243 |
+
"think": parsed["think"],
|
| 244 |
+
"raw_output": generated_text
|
| 245 |
+
})
|
| 246 |
+
|
| 247 |
+
total = len(results)
|
| 248 |
+
unsuitable_count = sum(1 for r in results if r["label"] == "Unsuitable")
|
| 249 |
+
suitable_count = sum(1 for r in results if r["label"] == "Suitable")
|
| 250 |
+
error_count = total - unsuitable_count - suitable_count
|
| 251 |
+
|
| 252 |
+
unsuitable_rate = (unsuitable_count / total * 100) if total > 0 else 0
|
| 253 |
+
suitable_rate = (suitable_count / total * 100) if total > 0 else 0
|
| 254 |
+
|
| 255 |
+
print("\n" + "="*60)
|
| 256 |
+
print(f"AUDIT REPORT FOR: {input_path.name}")
|
| 257 |
+
print("="*60)
|
| 258 |
+
print(f"{'Total Images':<25}: {total}")
|
| 259 |
+
print("-" * 60)
|
| 260 |
+
print(f"{'UNSUITABLE (Violation)':<25}: {unsuitable_count} ({unsuitable_rate:.2f}%)")
|
| 261 |
+
print(f"{'SUITABLE (Safe)':<25}: {suitable_count} ({suitable_rate:.2f}%)")
|
| 262 |
+
print(f"{'Parse Errors':<25}: {error_count}")
|
| 263 |
+
print("="*60)
|
| 264 |
+
|
| 265 |
+
output_file = input_path / f"audit_result_{input_path.name}.json"
|
| 266 |
+
try:
|
| 267 |
+
with open(output_file, "w", encoding="utf-8") as f:
|
| 268 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 269 |
+
print(f"\n[Done] Detailed JSON report saved to:\n-> {output_file}")
|
| 270 |
+
except Exception as e:
|
| 271 |
+
print(f"[Error] Could not save JSON: {e}")
|
| 272 |
+
|
| 273 |
+
if __name__ == "__main__":
|
| 274 |
+
try:
|
| 275 |
+
multiprocessing.set_start_method('spawn', force=True)
|
| 276 |
+
except RuntimeError:
|
| 277 |
+
pass
|
| 278 |
+
|
| 279 |
+
main()
|
code_inference/rule_14_vllm.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
使用方法:
|
| 4 |
+
python rule_14_vllm.py \
|
| 5 |
+
--input_dir "/path/to/your/images" \
|
| 6 |
+
--model_path "/path/to/your/Qwen2.5-VL-7B-Instruct"
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
import json
|
| 12 |
+
import argparse
|
| 13 |
+
import multiprocessing
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Dict, List, Optional, Any
|
| 16 |
+
from tqdm import tqdm
|
| 17 |
+
from PIL import Image, ImageFile
|
| 18 |
+
from transformers import AutoProcessor
|
| 19 |
+
from vllm import LLM, SamplingParams
|
| 20 |
+
|
| 21 |
+
os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
SYS_PROMPT_TEXT="""You are a highly critical Senior Art Director. Your job is to flag "Low-Quality / Amateur" advertising designs. Crucial Context: You must distinguish between "Aggressive E-commerce Marketing" (Professional) and "Amateur Sloppiness" (Violation). High-resolution assets, standard platform badges, and professional 3D renders are SAFE.
|
| 32 |
+
|
| 33 |
+
INPUT: One image and one natural-language question about design aesthetic and text harmony.
|
| 34 |
+
|
| 35 |
+
YOUR TASK:
|
| 36 |
+
|
| 37 |
+
Determine if the image is a VIOLATION (Unsuitable) or SAFE (Suitable) based on the criteria below.
|
| 38 |
+
|
| 39 |
+
Output a JSON object containing a rigorous Chain-of-Thought ("think") and a precise classification label ("answer").
|
| 40 |
+
|
| 41 |
+
OUTPUT FORMAT: Return EXACTLY two blocks, no extra text: <think>Detailed reasoning evaluating font effects, background integration, and aesthetic consistency...</think><answer>{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Text Design Harmony"}</answer>
|
| 42 |
+
|
| 43 |
+
========================================= STRICT VIOLATION CRITERIA (If ANY match -> Unsuitable)
|
| 44 |
+
The "WordArt" Effect (廉价特效):
|
| 45 |
+
|
| 46 |
+
Technical Failure: ONLY flag if text is pixelated, jagged, or uses 1990s-style rainbow/neon gradients.
|
| 47 |
+
|
| 48 |
+
Distortion: Text is unprofessionally stretched or squeezed (breaking the font's aspect ratio).
|
| 49 |
+
|
| 50 |
+
Amateur Strokes: Thick, vibrating outlines that look like they were made in MS Paint, not professional design software.
|
| 51 |
+
|
| 52 |
+
Note: High-res 3D fonts, clean gold textures, and smooth gradients are PROFESSIONAL and SAFE.
|
| 53 |
+
|
| 54 |
+
Visual Clutter & Conflict (背景冲突与拼贴感):
|
| 55 |
+
|
| 56 |
+
Resolution Mismatch: A low-res/blurry graphic pasted onto a high-res photo.
|
| 57 |
+
|
| 58 |
+
Zero Integration: Elements that have NO shadows, NO lighting consistency, and look like accidental "floating" errors.
|
| 59 |
+
|
| 60 |
+
Legibility Loss: Text is truly unreadable due to background chaos without any masking.
|
| 61 |
+
|
| 62 |
+
Note: Standard UI elements (Pill buttons, Price tags, Promo badges like "百亿补贴") are INTENTIONAL overlays and are SAFE.
|
| 63 |
+
|
| 64 |
+
Inconsistent Aesthetic (风格割裂):
|
| 65 |
+
|
| 66 |
+
Flag ONLY if elements are accidentally mismatched (e.g., a hand-drawn sketch randomly appearing in a high-tech 3D render without stylistic intent).
|
| 67 |
+
|
| 68 |
+
Note: 3D mascots or cartoon characters placed on realistic backgrounds for marketing purposes are a VALID style and are SAFE.
|
| 69 |
+
|
| 70 |
+
========================================= CRITERIA FOR 'SUITABLE' (NON-VIOLATION / GOOD DESIGN)
|
| 71 |
+
Commercial Execution: High-resolution assets, clean font edges, and professional lighting/shadows.
|
| 72 |
+
|
| 73 |
+
Platform Legitimacy: Presence of brand logos (Alipay, Taobao, Banks, China Gold) and standard e-commerce UI components.
|
| 74 |
+
|
| 75 |
+
Intentional Hierarchy: Even if the design is "loud" (Red/Gold), it is Suitable if the text is aligned and the layout is purposeful.
|
| 76 |
+
|
| 77 |
+
========================================= DECISION LOGIC
|
| 78 |
+
Unsuitable: If the design shows Technical Failure (pixelation, distortion, 90s-style WordArt) or looks like a non-designer's mistake.
|
| 79 |
+
|
| 80 |
+
Suitable: If the design follows Commercial Logic (Standard e-commerce banners, High-res renders, Professional marketing layouts). When in doubt, if the image looks like it's from a major App, it is SUITABLE.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def collect_images(input_dir: Path) -> List[Dict[str, str]]:
|
| 86 |
+
if not input_dir.exists():
|
| 87 |
+
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
| 88 |
+
|
| 89 |
+
files = [p for p in input_dir.iterdir() if p.is_file() and p.suffix.lower() in IMG_EXTS]
|
| 90 |
+
files.sort()
|
| 91 |
+
|
| 92 |
+
print(f"[Info] Found {len(files)} images in {input_dir}")
|
| 93 |
+
return [{"path": str(p), "filename": p.name} for p in files]
|
| 94 |
+
|
| 95 |
+
def parse_llm_output(text: str) -> Dict[str, Any]:
|
| 96 |
+
default_res = {
|
| 97 |
+
"label": "Parse Error",
|
| 98 |
+
"think": "No reasoning found",
|
| 99 |
+
"raw": text
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if not text:
|
| 103 |
+
return default_res
|
| 104 |
+
|
| 105 |
+
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
|
| 106 |
+
think_content = think_match.group(1).strip() if think_match else ""
|
| 107 |
+
|
| 108 |
+
answer_match = re.search(r'<answer>(.*?)</answer>', text, re.DOTALL)
|
| 109 |
+
|
| 110 |
+
extracted_label = "Parse Error"
|
| 111 |
+
|
| 112 |
+
if answer_match:
|
| 113 |
+
json_str = answer_match.group(1).strip()
|
| 114 |
+
try:
|
| 115 |
+
data = json.loads(json_str)
|
| 116 |
+
raw_ans = data.get("Answer", "")
|
| 117 |
+
|
| 118 |
+
if "unsuitable" in raw_ans.lower():
|
| 119 |
+
extracted_label = "Unsuitable"
|
| 120 |
+
elif "suitable" in raw_ans.lower():
|
| 121 |
+
extracted_label = "Suitable"
|
| 122 |
+
else:
|
| 123 |
+
extracted_label = raw_ans
|
| 124 |
+
|
| 125 |
+
except json.JSONDecodeError:
|
| 126 |
+
if "Unsuitable" in json_str:
|
| 127 |
+
extracted_label = "Unsuitable"
|
| 128 |
+
elif "Suitable" in json_str:
|
| 129 |
+
extracted_label = "Suitable"
|
| 130 |
+
else:
|
| 131 |
+
if "Unsuitable" in text:
|
| 132 |
+
extracted_label = "Unsuitable"
|
| 133 |
+
elif "Suitable" in text:
|
| 134 |
+
extracted_label = "Suitable"
|
| 135 |
+
|
| 136 |
+
return {
|
| 137 |
+
"label": extracted_label,
|
| 138 |
+
"think": think_content,
|
| 139 |
+
"raw": text
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
def prepare_vllm_inputs(batch_meta: List[Dict], processor) -> List[Dict]:
|
| 143 |
+
vllm_inputs = []
|
| 144 |
+
user_query = "Analyze this image against the design rules and return the JSON decision."
|
| 145 |
+
|
| 146 |
+
for item in batch_meta:
|
| 147 |
+
img_path = item["path"]
|
| 148 |
+
try:
|
| 149 |
+
image_obj = Image.open(img_path).convert("RGB")
|
| 150 |
+
|
| 151 |
+
messages = [
|
| 152 |
+
{"role": "system", "content": [{"type": "text", "text": SYS_PROMPT_TEXT}]},
|
| 153 |
+
{"role": "user", "content": [
|
| 154 |
+
{"type": "image", "image": img_path},
|
| 155 |
+
{"type": "text", "text": user_query}
|
| 156 |
+
]}
|
| 157 |
+
]
|
| 158 |
+
|
| 159 |
+
prompt_text = processor.apply_chat_template(
|
| 160 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
vllm_inputs.append({
|
| 164 |
+
"prompt": prompt_text,
|
| 165 |
+
"multi_modal_data": {"image": image_obj}
|
| 166 |
+
})
|
| 167 |
+
except Exception as e:
|
| 168 |
+
print(f"[Warning] Failed to load {img_path}: {e}")
|
| 169 |
+
vllm_inputs.append(None)
|
| 170 |
+
|
| 171 |
+
return vllm_inputs
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def main():
|
| 175 |
+
parser = argparse.ArgumentParser(description="AI Visual Comfort Auditor")
|
| 176 |
+
parser.add_argument("--input_dir", type=str, required=True, help="Folder containing images to check")
|
| 177 |
+
parser.add_argument("--model_path", type=str, required=True, help="Path to local Qwen-VL model")
|
| 178 |
+
parser.add_argument("--batch_size", type=int, default=512, help="Inference batch size")
|
| 179 |
+
parser.add_argument("--tp_size", type=int, default=2, help="Tensor Parallel size")
|
| 180 |
+
args = parser.parse_args()
|
| 181 |
+
|
| 182 |
+
input_path = Path(args.input_dir)
|
| 183 |
+
meta_data = collect_images(input_path)
|
| 184 |
+
|
| 185 |
+
if not meta_data:
|
| 186 |
+
print("[Info] No images found. Exiting.")
|
| 187 |
+
return
|
| 188 |
+
print(f"\n[Init] Loading Model: {args.model_path}")
|
| 189 |
+
|
| 190 |
+
llm = LLM(
|
| 191 |
+
model=args.model_path,
|
| 192 |
+
tokenizer=args.model_path,
|
| 193 |
+
trust_remote_code=True,
|
| 194 |
+
tensor_parallel_size=args.tp_size,
|
| 195 |
+
gpu_memory_utilization=0.90,
|
| 196 |
+
max_model_len=8192,
|
| 197 |
+
enforce_eager=True,
|
| 198 |
+
limit_mm_per_prompt={"image": 1}
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
processor = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True)
|
| 202 |
+
|
| 203 |
+
sampling_params = SamplingParams(
|
| 204 |
+
temperature=0.7,
|
| 205 |
+
max_tokens=1024,
|
| 206 |
+
top_p=0.9
|
| 207 |
+
)
|
| 208 |
+
results = []
|
| 209 |
+
print(f"\n[Run] Starting Inference on {len(meta_data)} images...")
|
| 210 |
+
|
| 211 |
+
for i in tqdm(range(0, len(meta_data), args.batch_size), desc="Processing Batches"):
|
| 212 |
+
batch_meta = meta_data[i : i + args.batch_size]
|
| 213 |
+
batch_inputs = prepare_vllm_inputs(batch_meta, processor)
|
| 214 |
+
|
| 215 |
+
valid_inputs = [inp for inp in batch_inputs if inp is not None]
|
| 216 |
+
valid_indices = [idx for idx, inp in enumerate(batch_inputs) if inp is not None]
|
| 217 |
+
|
| 218 |
+
if not valid_inputs:
|
| 219 |
+
continue
|
| 220 |
+
|
| 221 |
+
outputs = llm.generate(valid_inputs, sampling_params=sampling_params, use_tqdm=False)
|
| 222 |
+
|
| 223 |
+
for local_idx, out in enumerate(outputs):
|
| 224 |
+
original_meta = batch_meta[valid_indices[local_idx]]
|
| 225 |
+
generated_text = out.outputs[0].text
|
| 226 |
+
|
| 227 |
+
parsed = parse_llm_output(generated_text)
|
| 228 |
+
|
| 229 |
+
results.append({
|
| 230 |
+
"filename": original_meta["filename"],
|
| 231 |
+
"path": original_meta["path"],
|
| 232 |
+
"label": parsed["label"], # Suitable / Unsuitable
|
| 233 |
+
"think": parsed["think"],
|
| 234 |
+
"raw_output": generated_text
|
| 235 |
+
})
|
| 236 |
+
|
| 237 |
+
total = len(results)
|
| 238 |
+
unsuitable_count = sum(1 for r in results if r["label"] == "Unsuitable")
|
| 239 |
+
suitable_count = sum(1 for r in results if r["label"] == "Suitable")
|
| 240 |
+
error_count = total - unsuitable_count - suitable_count
|
| 241 |
+
|
| 242 |
+
unsuitable_rate = (unsuitable_count / total * 100) if total > 0 else 0
|
| 243 |
+
suitable_rate = (suitable_count / total * 100) if total > 0 else 0
|
| 244 |
+
|
| 245 |
+
print("\n" + "="*60)
|
| 246 |
+
print(f"AUDIT REPORT FOR: {input_path.name}")
|
| 247 |
+
print("="*60)
|
| 248 |
+
print(f"{'Total Images':<25}: {total}")
|
| 249 |
+
print("-" * 60)
|
| 250 |
+
print(f"{'UNSUITABLE (Violation)':<25}: {unsuitable_count} ({unsuitable_rate:.2f}%)")
|
| 251 |
+
print(f"{'SUITABLE (Safe)':<25}: {suitable_count} ({suitable_rate:.2f}%)")
|
| 252 |
+
print(f"{'Parse Errors':<25}: {error_count}")
|
| 253 |
+
print("="*60)
|
| 254 |
+
|
| 255 |
+
output_file = input_path / f"audit_result_{input_path.name}.json"
|
| 256 |
+
try:
|
| 257 |
+
with open(output_file, "w", encoding="utf-8") as f:
|
| 258 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 259 |
+
print(f"\n[Done] Detailed JSON report saved to:\n-> {output_file}")
|
| 260 |
+
except Exception as e:
|
| 261 |
+
print(f"[Error] Could not save JSON: {e}")
|
| 262 |
+
|
| 263 |
+
if __name__ == "__main__":
|
| 264 |
+
try:
|
| 265 |
+
multiprocessing.set_start_method('spawn', force=True)
|
| 266 |
+
except RuntimeError:
|
| 267 |
+
pass
|
| 268 |
+
|
| 269 |
+
main()
|
code_inference/rule_17_vllm.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
使用方法:
|
| 5 |
+
python rule_17_vllm.py \
|
| 6 |
+
--input_dir "/path/to/your/images" \
|
| 7 |
+
--model_path "/path/to/your/Qwen2.5-VL-7B-Instruct"
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import re
|
| 12 |
+
import json
|
| 13 |
+
import argparse
|
| 14 |
+
import multiprocessing
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Dict, List, Optional, Any
|
| 17 |
+
from tqdm import tqdm
|
| 18 |
+
from PIL import Image, ImageFile
|
| 19 |
+
from transformers import AutoProcessor
|
| 20 |
+
from vllm import LLM, SamplingParams
|
| 21 |
+
|
| 22 |
+
os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'
|
| 23 |
+
|
| 24 |
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
|
| 28 |
+
SYS_PROMPT_TEXT = """You are an expert Art Director and Advertisement Quality Assessor.
|
| 29 |
+
Your task is to filter out low-quality, cluttered, or visually confusing advertisements based on the "Visual Comfort & Clarity" standard.
|
| 30 |
+
|
| 31 |
+
INPUT: One image and one natural-language question about visual suitability.
|
| 32 |
+
|
| 33 |
+
YOUR TASK:
|
| 34 |
+
1. Determine if the image is a **VIOLATION** (Unsuitable) or **SAFE** (Suitable) based on the criteria below.
|
| 35 |
+
2. Output a JSON object containing a rigorous Chain-of-Thought ("think") and a precise classification label ("answer").
|
| 36 |
+
|
| 37 |
+
OUTPUT FORMAT:
|
| 38 |
+
Return EXACTLY two blocks, no extra text:
|
| 39 |
+
<think>Detailed reasoning checking against the violation criteria (Background, Composition, Aesthetic, Text, Generic Assets)...</think><answer>{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Visual Comfort"}</answer>
|
| 40 |
+
|
| 41 |
+
=========================================
|
| 42 |
+
VIOLATION CRITERIA (If ANY match -> Unsuitable)
|
| 43 |
+
=========================================
|
| 44 |
+
1. **Background & Repetition (CRITICAL)**
|
| 45 |
+
- **Repetitive Clutter:** Dense array of repeated objects (e.g., wall of bottles) lacking a focal point.
|
| 46 |
+
- **Chaotic Background:** Filled with "floating debris" (flying coins, confetti) blending with text.
|
| 47 |
+
|
| 48 |
+
2. **Composition Check**
|
| 49 |
+
- **Collage/Grid Layout:** Split into distinct panels/grids showing different scenes.
|
| 50 |
+
- **No Focal Point:** Subjects placed in corners without hierarchy.
|
| 51 |
+
|
| 52 |
+
3. **Aesthetic Quality (The "Low Quality" Filter)**
|
| 53 |
+
- **Visual Overload:** Harsh, clashing high-saturation colors, cheap glowing effects, or cluttered 3D fonts.
|
| 54 |
+
- **Messy Alignment:** Elements touching edges, no margins, chaotic placement.
|
| 55 |
+
|
| 56 |
+
4. **Text & Hierarchy Balance**
|
| 57 |
+
- **Scattered Text:** Text scattered across 4+ different locations, creating a chaotic reading path.
|
| 58 |
+
|
| 59 |
+
5. **Generic Promotional Assets**
|
| 60 |
+
- **Spammy Visuals:** Large, generic 3D-rendered Red Packets or Gold Coins dominating the composition.
|
| 61 |
+
- **Wallpaper Effect:** Dense, repetitive pattern of festive icons leaving no negative space.
|
| 62 |
+
|
| 63 |
+
=========================================
|
| 64 |
+
DECISION LOGIC
|
| 65 |
+
=========================================
|
| 66 |
+
- **Unsuitable**: If the image triggers ANY of the Violation criteria above.
|
| 67 |
+
- **Suitable**: If it looks professional, clean, has a clear main subject, and Safe Layout (e.g. vertical alignment).
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def collect_images(input_dir: Path) -> List[Dict[str, str]]:
|
| 72 |
+
|
| 73 |
+
if not input_dir.exists():
|
| 74 |
+
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
| 75 |
+
|
| 76 |
+
files = [p for p in input_dir.iterdir() if p.is_file() and p.suffix.lower() in IMG_EXTS]
|
| 77 |
+
files.sort()
|
| 78 |
+
|
| 79 |
+
print(f"[Info] Found {len(files)} images in {input_dir}")
|
| 80 |
+
return [{"path": str(p), "filename": p.name} for p in files]
|
| 81 |
+
|
| 82 |
+
def parse_llm_output(text: str) -> Dict[str, Any]:
|
| 83 |
+
|
| 84 |
+
default_res = {
|
| 85 |
+
"label": "Parse Error",
|
| 86 |
+
"think": "No reasoning found",
|
| 87 |
+
"raw": text
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
if not text:
|
| 91 |
+
return default_res
|
| 92 |
+
|
| 93 |
+
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
|
| 94 |
+
think_content = think_match.group(1).strip() if think_match else ""
|
| 95 |
+
|
| 96 |
+
answer_match = re.search(r'<answer>(.*?)</answer>', text, re.DOTALL)
|
| 97 |
+
|
| 98 |
+
extracted_label = "Parse Error"
|
| 99 |
+
|
| 100 |
+
if answer_match:
|
| 101 |
+
json_str = answer_match.group(1).strip()
|
| 102 |
+
try:
|
| 103 |
+
data = json.loads(json_str)
|
| 104 |
+
raw_ans = data.get("Answer", "")
|
| 105 |
+
|
| 106 |
+
if "unsuitable" in raw_ans.lower():
|
| 107 |
+
extracted_label = "Unsuitable"
|
| 108 |
+
elif "suitable" in raw_ans.lower():
|
| 109 |
+
extracted_label = "Suitable"
|
| 110 |
+
else:
|
| 111 |
+
extracted_label = raw_ans
|
| 112 |
+
|
| 113 |
+
except json.JSONDecodeError:
|
| 114 |
+
|
| 115 |
+
if "Unsuitable" in json_str:
|
| 116 |
+
extracted_label = "Unsuitable"
|
| 117 |
+
elif "Suitable" in json_str:
|
| 118 |
+
extracted_label = "Suitable"
|
| 119 |
+
else:
|
| 120 |
+
if "Unsuitable" in text:
|
| 121 |
+
extracted_label = "Unsuitable"
|
| 122 |
+
elif "Suitable" in text:
|
| 123 |
+
extracted_label = "Suitable"
|
| 124 |
+
|
| 125 |
+
return {
|
| 126 |
+
"label": extracted_label,
|
| 127 |
+
"think": think_content,
|
| 128 |
+
"raw": text
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
def prepare_vllm_inputs(batch_meta: List[Dict], processor) -> List[Dict]:
|
| 132 |
+
vllm_inputs = []
|
| 133 |
+
|
| 134 |
+
user_query = "Analyze this image against the design rules and return the JSON decision."
|
| 135 |
+
|
| 136 |
+
for item in batch_meta:
|
| 137 |
+
img_path = item["path"]
|
| 138 |
+
try:
|
| 139 |
+
image_obj = Image.open(img_path).convert("RGB")
|
| 140 |
+
|
| 141 |
+
messages = [
|
| 142 |
+
{"role": "system", "content": [{"type": "text", "text": SYS_PROMPT_TEXT}]},
|
| 143 |
+
{"role": "user", "content": [
|
| 144 |
+
{"type": "image", "image": img_path},
|
| 145 |
+
{"type": "text", "text": user_query}
|
| 146 |
+
]}
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
prompt_text = processor.apply_chat_template(
|
| 150 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
vllm_inputs.append({
|
| 154 |
+
"prompt": prompt_text,
|
| 155 |
+
"multi_modal_data": {"image": image_obj}
|
| 156 |
+
})
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"[Warning] Failed to load {img_path}: {e}")
|
| 159 |
+
vllm_inputs.append(None)
|
| 160 |
+
|
| 161 |
+
return vllm_inputs
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def main():
|
| 165 |
+
parser = argparse.ArgumentParser(description="AI Visual Comfort Auditor")
|
| 166 |
+
parser.add_argument("--input_dir", type=str, required=True, help="Folder containing images to check")
|
| 167 |
+
parser.add_argument("--model_path", type=str, required=True, help="Path to local Qwen-VL model")
|
| 168 |
+
parser.add_argument("--batch_size", type=int, default=256, help="Inference batch size")
|
| 169 |
+
parser.add_argument("--tp_size", type=int, default=2, help="Tensor Parallel size")
|
| 170 |
+
args = parser.parse_args()
|
| 171 |
+
|
| 172 |
+
input_path = Path(args.input_dir)
|
| 173 |
+
meta_data = collect_images(input_path)
|
| 174 |
+
|
| 175 |
+
if not meta_data:
|
| 176 |
+
print("[Info] No images found. Exiting.")
|
| 177 |
+
return
|
| 178 |
+
print(f"\n[Init] Loading Model: {args.model_path}")
|
| 179 |
+
|
| 180 |
+
llm = LLM(
|
| 181 |
+
model=args.model_path,
|
| 182 |
+
tokenizer=args.model_path,
|
| 183 |
+
trust_remote_code=True,
|
| 184 |
+
tensor_parallel_size=args.tp_size,
|
| 185 |
+
gpu_memory_utilization=0.90,
|
| 186 |
+
max_model_len=8192,
|
| 187 |
+
enforce_eager=True,
|
| 188 |
+
limit_mm_per_prompt={"image": 1}
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
processor = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
sampling_params = SamplingParams(
|
| 195 |
+
temperature=0.7,
|
| 196 |
+
max_tokens=1024,
|
| 197 |
+
top_p=0.9
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
results = []
|
| 201 |
+
print(f"\n[Run] Starting Inference on {len(meta_data)} images...")
|
| 202 |
+
|
| 203 |
+
for i in tqdm(range(0, len(meta_data), args.batch_size), desc="Processing Batches"):
|
| 204 |
+
batch_meta = meta_data[i : i + args.batch_size]
|
| 205 |
+
batch_inputs = prepare_vllm_inputs(batch_meta, processor)
|
| 206 |
+
|
| 207 |
+
valid_inputs = [inp for inp in batch_inputs if inp is not None]
|
| 208 |
+
valid_indices = [idx for idx, inp in enumerate(batch_inputs) if inp is not None]
|
| 209 |
+
|
| 210 |
+
if not valid_inputs:
|
| 211 |
+
continue
|
| 212 |
+
|
| 213 |
+
outputs = llm.generate(valid_inputs, sampling_params=sampling_params, use_tqdm=False)
|
| 214 |
+
|
| 215 |
+
for local_idx, out in enumerate(outputs):
|
| 216 |
+
original_meta = batch_meta[valid_indices[local_idx]]
|
| 217 |
+
generated_text = out.outputs[0].text
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
parsed = parse_llm_output(generated_text)
|
| 221 |
+
|
| 222 |
+
results.append({
|
| 223 |
+
"filename": original_meta["filename"],
|
| 224 |
+
"path": original_meta["path"],
|
| 225 |
+
"label": parsed["label"], # Suitable / Unsuitable
|
| 226 |
+
"think": parsed["think"],
|
| 227 |
+
"raw_output": generated_text
|
| 228 |
+
})
|
| 229 |
+
|
| 230 |
+
total = len(results)
|
| 231 |
+
unsuitable_count = sum(1 for r in results if r["label"] == "Unsuitable")
|
| 232 |
+
suitable_count = sum(1 for r in results if r["label"] == "Suitable")
|
| 233 |
+
error_count = total - unsuitable_count - suitable_count
|
| 234 |
+
|
| 235 |
+
unsuitable_rate = (unsuitable_count / total * 100) if total > 0 else 0
|
| 236 |
+
suitable_rate = (suitable_count / total * 100) if total > 0 else 0
|
| 237 |
+
|
| 238 |
+
print("\n" + "="*60)
|
| 239 |
+
print(f"AUDIT REPORT FOR: {input_path.name}")
|
| 240 |
+
print("="*60)
|
| 241 |
+
print(f"{'Total Images':<25}: {total}")
|
| 242 |
+
print("-" * 60)
|
| 243 |
+
print(f"{'UNSUITABLE (Violation)':<25}: {unsuitable_count} ({unsuitable_rate:.2f}%)")
|
| 244 |
+
print(f"{'SUITABLE (Safe)':<25}: {suitable_count} ({suitable_rate:.2f}%)")
|
| 245 |
+
print(f"{'Parse Errors':<25}: {error_count}")
|
| 246 |
+
print("="*60)
|
| 247 |
+
|
| 248 |
+
output_file = input_path / f"audit_result_{input_path.name}.json"
|
| 249 |
+
try:
|
| 250 |
+
with open(output_file, "w", encoding="utf-8") as f:
|
| 251 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 252 |
+
print(f"\n[Done] Detailed JSON report saved to:\n-> {output_file}")
|
| 253 |
+
except Exception as e:
|
| 254 |
+
print(f"[Error] Could not save JSON: {e}")
|
| 255 |
+
|
| 256 |
+
if __name__ == "__main__":
|
| 257 |
+
try:
|
| 258 |
+
multiprocessing.set_start_method('spawn', force=True)
|
| 259 |
+
except RuntimeError:
|
| 260 |
+
pass
|
| 261 |
+
|
| 262 |
+
main()
|
code_inference/rule_18_vllm.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
使用方法:
|
| 5 |
+
python rule_18_vllm.py \
|
| 6 |
+
--input_dir "/path/to/your/images" \
|
| 7 |
+
--model_path "/path/to/your/Qwen2.5-VL-7B-Instruct"
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import re
|
| 12 |
+
import json
|
| 13 |
+
import argparse
|
| 14 |
+
import multiprocessing
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Dict, List, Optional, Any
|
| 17 |
+
from tqdm import tqdm
|
| 18 |
+
from PIL import Image, ImageFile
|
| 19 |
+
from transformers import AutoProcessor
|
| 20 |
+
from vllm import LLM, SamplingParams
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'
|
| 24 |
+
|
| 25 |
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 26 |
+
|
| 27 |
+
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
SYS_PROMPT_TEXT="""You are a highly critical Senior Art Director specializing in Layout and Visual Hierarchy.
|
| 32 |
+
Your job is to identify "Suffocating Designs"—creative pieces where elements are too cramped, lack breathing room, or feel disorganized due to poor spacing.
|
| 33 |
+
|
| 34 |
+
INPUT: One image and one natural-language question about layout composition and spacing.
|
| 35 |
+
|
| 36 |
+
YOUR TASK:
|
| 37 |
+
Analyze the image to determine if the layout violates professional "Composition & Spacing" standards.
|
| 38 |
+
|
| 39 |
+
CORE JUDGMENT PRINCIPLE: A professional advertisement must have a clear "Sense of Breath" (Negative Space). If the elements feel "squeezed" or "crowded," it is UNSUITABLE.
|
| 40 |
+
|
| 41 |
+
OUTPUT FORMAT:
|
| 42 |
+
Return EXACTLY two blocks, no extra text:
|
| 43 |
+
<think>Detailed reasoning evaluating negative space, element proximity, visual path, and edge tension...</think><answer>{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Layout Breathability Check"}</answer>
|
| 44 |
+
|
| 45 |
+
=========================================
|
| 46 |
+
STRICT VIOLATION CRITERIA (If ANY match -> Unsuitable)
|
| 47 |
+
=========================================
|
| 48 |
+
1. **Lack of Breathing Room (Core Crowding Violation):**
|
| 49 |
+
- Core Violation: The main subject (the specific product or hero character, excluding the background image), the headline text, and the logo are placed too close to each other.
|
| 50 |
+
- Exclusions: This criterion does not apply to text appearing physically on the product packaging or artistic fonts that are visually integrated into the product itself.
|
| 51 |
+
- The "Small Print" Nuance: While major design modules require significant negative space, secondary small text (such as annotations or footnotes) is permitted to have smaller gaps relative to other elements. However, these small characters must not be too close to other elements or edges; they must maintain a basic visual distance to avoid a sense of "clinging," "tangency," or extreme squeezing.
|
| 52 |
+
- Visual Feel: The overall design feels "heavy" or "claustrophobic" because major modules lack sufficient negative space between them.
|
| 53 |
+
|
| 54 |
+
2. **Edge Tension (贴边风险):**
|
| 55 |
+
- Elements are "touching" or "tangent" to each other or the border without intentional overlapping.
|
| 56 |
+
|
| 57 |
+
3. **Information Overload (信息堆砌):**
|
| 58 |
+
- The layout is filled with too many text blocks or icons with no clear separation.
|
| 59 |
+
- There is no clear "Visual Path"; the eye doesn't know where to rest because every element is competing for attention and space simultaneously.
|
| 60 |
+
|
| 61 |
+
=========================================
|
| 62 |
+
CRITERIA FOR 'SUITABLE' (NON-VIOLATION / GOOD DESIGN)
|
| 63 |
+
=========================================
|
| 64 |
+
1. **Generous White Space (Major Elements):**
|
| 65 |
+
- Clear and deliberate separation exists between the Headline, Main Subject, and Footer.
|
| 66 |
+
|
| 67 |
+
2. **Permissible Density (Small Print Exemption):**
|
| 68 |
+
- **Nuance:** Secondary small text (annotations/footnotes) IS ALLOWED to have smaller gaps relative to other elements (unlike headlines). As long as it doesn't touch/cling (see Violation #2), tighter spacing for small text is SAFE.
|
| 69 |
+
|
| 70 |
+
3. **Valid Exclusions:**
|
| 71 |
+
- **Product Packaging:** Text printed naturally on the product packaging is SAFE.
|
| 72 |
+
- **Artistic Integration:** Artistic fonts visually integrated *into* the product itself are SAFE.
|
| 73 |
+
- **Media Exemption:** Film stills or variety show photography are always SAFE.
|
| 74 |
+
|
| 75 |
+
=========================================
|
| 76 |
+
DECISION LOGIC
|
| 77 |
+
=========================================
|
| 78 |
+
- **Unsuitable**: If the design feels squeezed, suffers from edge tension, lacks a visual path, or if small text "clings" to edges/elements.
|
| 79 |
+
- **Suitable**: If major elements breathe well, OR if the density is strictly limited to allowed small print/packaging text that doesn't create tension.
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def collect_images(input_dir: Path) -> List[Dict[str, str]]:
|
| 84 |
+
if not input_dir.exists():
|
| 85 |
+
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
| 86 |
+
|
| 87 |
+
files = [p for p in input_dir.iterdir() if p.is_file() and p.suffix.lower() in IMG_EXTS]
|
| 88 |
+
files.sort()
|
| 89 |
+
|
| 90 |
+
print(f"[Info] Found {len(files)} images in {input_dir}")
|
| 91 |
+
return [{"path": str(p), "filename": p.name} for p in files]
|
| 92 |
+
|
| 93 |
+
def parse_llm_output(text: str) -> Dict[str, Any]:
|
| 94 |
+
default_res = {
|
| 95 |
+
"label": "Parse Error",
|
| 96 |
+
"think": "No reasoning found",
|
| 97 |
+
"raw": text
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if not text:
|
| 101 |
+
return default_res
|
| 102 |
+
|
| 103 |
+
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
|
| 104 |
+
think_content = think_match.group(1).strip() if think_match else ""
|
| 105 |
+
answer_match = re.search(r'<answer>(.*?)</answer>', text, re.DOTALL)
|
| 106 |
+
|
| 107 |
+
extracted_label = "Parse Error"
|
| 108 |
+
|
| 109 |
+
if answer_match:
|
| 110 |
+
json_str = answer_match.group(1).strip()
|
| 111 |
+
try:
|
| 112 |
+
data = json.loads(json_str)
|
| 113 |
+
raw_ans = data.get("Answer", "")
|
| 114 |
+
|
| 115 |
+
if "unsuitable" in raw_ans.lower():
|
| 116 |
+
extracted_label = "Unsuitable"
|
| 117 |
+
elif "suitable" in raw_ans.lower():
|
| 118 |
+
extracted_label = "Suitable"
|
| 119 |
+
else:
|
| 120 |
+
extracted_label = raw_ans
|
| 121 |
+
|
| 122 |
+
except json.JSONDecodeError:
|
| 123 |
+
|
| 124 |
+
if "Unsuitable" in json_str:
|
| 125 |
+
extracted_label = "Unsuitable"
|
| 126 |
+
elif "Suitable" in json_str:
|
| 127 |
+
extracted_label = "Suitable"
|
| 128 |
+
else:
|
| 129 |
+
if "Unsuitable" in text:
|
| 130 |
+
extracted_label = "Unsuitable"
|
| 131 |
+
elif "Suitable" in text:
|
| 132 |
+
extracted_label = "Suitable"
|
| 133 |
+
|
| 134 |
+
return {
|
| 135 |
+
"label": extracted_label,
|
| 136 |
+
"think": think_content,
|
| 137 |
+
"raw": text
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
def prepare_vllm_inputs(batch_meta: List[Dict], processor) -> List[Dict]:
|
| 141 |
+
vllm_inputs = []
|
| 142 |
+
user_query = "Analyze this image against the design rules and return the JSON decision."
|
| 143 |
+
|
| 144 |
+
for item in batch_meta:
|
| 145 |
+
img_path = item["path"]
|
| 146 |
+
try:
|
| 147 |
+
image_obj = Image.open(img_path).convert("RGB")
|
| 148 |
+
|
| 149 |
+
messages = [
|
| 150 |
+
{"role": "system", "content": [{"type": "text", "text": SYS_PROMPT_TEXT}]},
|
| 151 |
+
{"role": "user", "content": [
|
| 152 |
+
{"type": "image", "image": img_path},
|
| 153 |
+
{"type": "text", "text": user_query}
|
| 154 |
+
]}
|
| 155 |
+
]
|
| 156 |
+
|
| 157 |
+
prompt_text = processor.apply_chat_template(
|
| 158 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
vllm_inputs.append({
|
| 162 |
+
"prompt": prompt_text,
|
| 163 |
+
"multi_modal_data": {"image": image_obj}
|
| 164 |
+
})
|
| 165 |
+
except Exception as e:
|
| 166 |
+
print(f"[Warning] Failed to load {img_path}: {e}")
|
| 167 |
+
vllm_inputs.append(None)
|
| 168 |
+
|
| 169 |
+
return vllm_inputs
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def main():
|
| 173 |
+
parser = argparse.ArgumentParser(description="AI Visual Comfort Auditor")
|
| 174 |
+
parser.add_argument("--input_dir", type=str, required=True, help="Folder containing images to check")
|
| 175 |
+
parser.add_argument("--model_path", type=str, required=True, help="Path to local Qwen-VL model")
|
| 176 |
+
parser.add_argument("--batch_size", type=int, default=512, help="Inference batch size")
|
| 177 |
+
parser.add_argument("--tp_size", type=int, default=2, help="Tensor Parallel size")
|
| 178 |
+
args = parser.parse_args()
|
| 179 |
+
|
| 180 |
+
input_path = Path(args.input_dir)
|
| 181 |
+
meta_data = collect_images(input_path)
|
| 182 |
+
|
| 183 |
+
if not meta_data:
|
| 184 |
+
print("[Info] No images found. Exiting.")
|
| 185 |
+
return
|
| 186 |
+
|
| 187 |
+
print(f"\n[Init] Loading Model: {args.model_path}")
|
| 188 |
+
|
| 189 |
+
llm = LLM(
|
| 190 |
+
model=args.model_path,
|
| 191 |
+
tokenizer=args.model_path,
|
| 192 |
+
trust_remote_code=True,
|
| 193 |
+
tensor_parallel_size=args.tp_size,
|
| 194 |
+
gpu_memory_utilization=0.90,
|
| 195 |
+
max_model_len=8192,
|
| 196 |
+
enforce_eager=True,
|
| 197 |
+
limit_mm_per_prompt={"image": 1}
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
processor = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True)
|
| 201 |
+
|
| 202 |
+
sampling_params = SamplingParams(
|
| 203 |
+
temperature=0.7,
|
| 204 |
+
max_tokens=1024,
|
| 205 |
+
top_p=0.9
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
results = []
|
| 209 |
+
print(f"\n[Run] Starting Inference on {len(meta_data)} images...")
|
| 210 |
+
|
| 211 |
+
for i in tqdm(range(0, len(meta_data), args.batch_size), desc="Processing Batches"):
|
| 212 |
+
batch_meta = meta_data[i : i + args.batch_size]
|
| 213 |
+
batch_inputs = prepare_vllm_inputs(batch_meta, processor)
|
| 214 |
+
|
| 215 |
+
valid_inputs = [inp for inp in batch_inputs if inp is not None]
|
| 216 |
+
valid_indices = [idx for idx, inp in enumerate(batch_inputs) if inp is not None]
|
| 217 |
+
|
| 218 |
+
if not valid_inputs:
|
| 219 |
+
continue
|
| 220 |
+
|
| 221 |
+
outputs = llm.generate(valid_inputs, sampling_params=sampling_params, use_tqdm=False)
|
| 222 |
+
|
| 223 |
+
for local_idx, out in enumerate(outputs):
|
| 224 |
+
original_meta = batch_meta[valid_indices[local_idx]]
|
| 225 |
+
generated_text = out.outputs[0].text
|
| 226 |
+
|
| 227 |
+
parsed = parse_llm_output(generated_text)
|
| 228 |
+
|
| 229 |
+
results.append({
|
| 230 |
+
"filename": original_meta["filename"],
|
| 231 |
+
"path": original_meta["path"],
|
| 232 |
+
"label": parsed["label"], # Suitable / Unsuitable
|
| 233 |
+
"think": parsed["think"],
|
| 234 |
+
"raw_output": generated_text
|
| 235 |
+
})
|
| 236 |
+
|
| 237 |
+
total = len(results)
|
| 238 |
+
unsuitable_count = sum(1 for r in results if r["label"] == "Unsuitable")
|
| 239 |
+
suitable_count = sum(1 for r in results if r["label"] == "Suitable")
|
| 240 |
+
error_count = total - unsuitable_count - suitable_count
|
| 241 |
+
|
| 242 |
+
unsuitable_rate = (unsuitable_count / total * 100) if total > 0 else 0
|
| 243 |
+
suitable_rate = (suitable_count / total * 100) if total > 0 else 0
|
| 244 |
+
|
| 245 |
+
print("\n" + "="*60)
|
| 246 |
+
print(f"AUDIT REPORT FOR: {input_path.name}")
|
| 247 |
+
print("="*60)
|
| 248 |
+
print(f"{'Total Images':<25}: {total}")
|
| 249 |
+
print("-" * 60)
|
| 250 |
+
print(f"{'UNSUITABLE (Violation)':<25}: {unsuitable_count} ({unsuitable_rate:.2f}%)")
|
| 251 |
+
print(f"{'SUITABLE (Safe)':<25}: {suitable_count} ({suitable_rate:.2f}%)")
|
| 252 |
+
print(f"{'Parse Errors':<25}: {error_count}")
|
| 253 |
+
print("="*60)
|
| 254 |
+
|
| 255 |
+
output_file = input_path / f"audit_result_{input_path.name}.json"
|
| 256 |
+
try:
|
| 257 |
+
with open(output_file, "w", encoding="utf-8") as f:
|
| 258 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 259 |
+
print(f"\n[Done] Detailed JSON report saved to:\n-> {output_file}")
|
| 260 |
+
except Exception as e:
|
| 261 |
+
print(f"[Error] Could not save JSON: {e}")
|
| 262 |
+
|
| 263 |
+
if __name__ == "__main__":
|
| 264 |
+
try:
|
| 265 |
+
multiprocessing.set_start_method('spawn', force=True)
|
| 266 |
+
except RuntimeError:
|
| 267 |
+
pass
|
| 268 |
+
|
| 269 |
+
main()
|
code_inference/rule_19_vllm.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
使用方法:
|
| 4 |
+
python rule_19_vllm.py \
|
| 5 |
+
--input_dir "/path/to/your/images" \
|
| 6 |
+
--model_path "/path/to/your/Qwen2.5-VL-7B-Instruct"
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
import json
|
| 12 |
+
import argparse
|
| 13 |
+
import multiprocessing
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Dict, List, Optional, Any
|
| 16 |
+
from tqdm import tqdm
|
| 17 |
+
from PIL import Image, ImageFile
|
| 18 |
+
from transformers import AutoProcessor
|
| 19 |
+
from vllm import LLM, SamplingParams
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'
|
| 23 |
+
|
| 24 |
+
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
| 25 |
+
|
| 26 |
+
# 支持的图片格式
|
| 27 |
+
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
|
| 28 |
+
|
| 29 |
+
# ==========================================
|
| 30 |
+
# 【提示词工程】
|
| 31 |
+
# ==========================================
|
| 32 |
+
|
| 33 |
+
SYS_PROMPT_TEXT ="""You are a highly critical Senior Art Director. Your job is to flag "Low-Quality / Amateur" advertising designs.
|
| 34 |
+
You have ZERO TOLERANCE for "Cheap Ad Styles" (often called "Niu Pi Xian" in Chinese context).
|
| 35 |
+
|
| 36 |
+
INPUT: One image and one natural-language question about design aesthetic and text harmony.
|
| 37 |
+
|
| 38 |
+
YOUR TASK:
|
| 39 |
+
1. Determine if the image is a **VIOLATION** (Unsuitable) or **SAFE** (Suitable) based on the criteria below.
|
| 40 |
+
2. Output a JSON object containing a rigorous Chain-of-Thought ("think") and a precise classification label ("answer").
|
| 41 |
+
|
| 42 |
+
OUTPUT FORMAT:
|
| 43 |
+
Return EXACTLY two blocks, no extra text:
|
| 44 |
+
<think>Detailed reasoning evaluating font effects, background integration, and aesthetic consistency against the 'cheap design' criteria...</think><answer>{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Text-Design Harmony"}</answer>
|
| 45 |
+
|
| 46 |
+
=========================================
|
| 47 |
+
STRICT VIOLATION CRITERIA (If ANY match -> Unsuitable)
|
| 48 |
+
=========================================
|
| 49 |
+
1. **The "WordArt" Effect (廉价特效):**
|
| 50 |
+
- **Bad Strokes:** Text uses heavy, amateurish strokes (thick white/colored outlines) that look jagged or pixelated.
|
| 51 |
+
- **Fake 3D/Metal:** Outdated "Pseudo-3D" gradients (e.g., shiny gold/silver metal textures) that clash with a flat background.
|
| 52 |
+
- **Cheap Glow:** Aggressive "Outer Glow" (neon glow) that makes the text look blurry or radioactive.
|
| 53 |
+
- **Distortion:** Text is unprofessionally stretched, squeezed, or distorted strictly to fit a space.
|
| 54 |
+
|
| 55 |
+
2. **Visual Clutter & Conflict (背景冲突与拼贴感):**
|
| 56 |
+
- **Legibility Loss:** Text is placed directly on top of a "Busy Photograph" (leaves, city streets, crowds) without a sufficient background mask, making it hard to read.
|
| 57 |
+
- **Color Vibration:** Text color aggressively vibrates against the background (e.g., bright red text directly on bright green).
|
| 58 |
+
- **Patchwork Style:** The text background looks like a "sticker" arbitrarily pasted onto a photo, completely ignoring the photo's lighting and perspective.
|
| 59 |
+
|
| 60 |
+
3. **Inconsistent Aesthetic (风格割裂):**
|
| 61 |
+
- Foreground graphic elements (e.g., a cartoon/gaming style "Button" or "Banner") are superimposed on a realistic, high-res nature/human photograph. They do not belong in the same visual world.
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
=========================================
|
| 65 |
+
CRITERIA FOR 'SUITABLE' (NON-VIOLATION / GOOD DESIGN)
|
| 66 |
+
=========================================
|
| 67 |
+
1. **Clean Professionalism:** Professional typography with no cheap text effects (e.g., simple text like "xx折扣" is perfectly fine if the font is clean).
|
| 68 |
+
2. **Proper Integration:** Text placed on a solid, clean color background, or properly masked on a complex background.
|
| 69 |
+
3. **Cohesive Art Direction:** Clean, flat vector art that matches its surroundings visually.
|
| 70 |
+
|
| 71 |
+
=========================================
|
| 72 |
+
DECISION LOGIC
|
| 73 |
+
=========================================
|
| 74 |
+
- **Unsuitable**: If the design looks cheap, messy, outdated, features "WordArt" effects, or feels like a patched-together "Niu Pi Xian" ad.
|
| 75 |
+
- **Suitable**: If the design is clean, professional, and visually harmonious.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
SYS_PROMPT_TEXT="""You are a highly critical "Senior Art Director." Your goal is to evaluate the "Professional Polish" of splash ads. You have [ZERO TOLERANCE] for raw, unprocessed photos that look like amateur snapshots.
|
| 81 |
+
|
| 82 |
+
INPUT:
|
| 83 |
+
One image and one natural-language question about professional polish quality.
|
| 84 |
+
|
| 85 |
+
YOUR TASK:
|
| 86 |
+
1. Determine if the image is a **VIOLATION** (Unsuitable) or **SAFE** (Suitable) based on the criteria below.
|
| 87 |
+
2. Output a JSON object containing a rigorous Chain-of-Thought ("think") and a precise classification label ("answer").
|
| 88 |
+
|
| 89 |
+
OUTPUT FORMAT:
|
| 90 |
+
Return EXACTLY two blocks, no extra text:
|
| 91 |
+
|
| 92 |
+
<think>
|
| 93 |
+
Detailed reasoning evaluating lighting, color grading, and depth of field against the "passerby snapshot" criteria...
|
| 94 |
+
</think>
|
| 95 |
+
<answer>
|
| 96 |
+
{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Professional Polish"}
|
| 97 |
+
</answer>
|
| 98 |
+
|
| 99 |
+
=========================================
|
| 100 |
+
STRICT VIOLATION CRITERIA (If ANY match -> Unsuitable)
|
| 101 |
+
=========================================
|
| 102 |
+
1. **Lack of Professional Post-Processing:**
|
| 103 |
+
- The image appears to be a "Raw Photo" directly from a camera/phone without professional retouching.
|
| 104 |
+
|
| 105 |
+
2. **The "Amateur Snapshot" Aesthetic:**
|
| 106 |
+
- The image looks like something a "passerby" could easily capture. It lacks the sophisticated framing, high-end texture, and artistic polish required for premium advertising.
|
| 107 |
+
|
| 108 |
+
3. **Absence of Value Conveyance:**
|
| 109 |
+
- The image is visually "flat" and fails to evoke a sense of high quality. It does not use professional polish techniques to guide the viewer's emotions.
|
| 110 |
+
|
| 111 |
+
=========================================
|
| 112 |
+
CRITERIA FOR 'SUITABLE' (NON-VIOLATION / PREMIUM TEXTURE)
|
| 113 |
+
=========================================
|
| 114 |
+
1. **Professional Post-Processing:**
|
| 115 |
+
- **Masterful Retouching:** The image shows clear evidence of professional professional polish (not straight-out-of-camera).
|
| 116 |
+
|
| 117 |
+
2. **The "High-End" Aesthetic:**
|
| 118 |
+
- **Professionalism:** The image features a look that cannot be easily replicated by a passerby.
|
| 119 |
+
- **Superior Texture:** Displays deliberate set design and artistic polish.
|
| 120 |
+
|
| 121 |
+
4. **Media Exemption:** Film stills or variety show photography are always classified as SAFE (Suitable).
|
| 122 |
+
|
| 123 |
+
=========================================
|
| 124 |
+
DECISION LOGIC
|
| 125 |
+
=========================================
|
| 126 |
+
- **Unsuitable**: If the image looks like an unprocessed, amateur snapshot with flat lighting and no professional polish polish.
|
| 127 |
+
- **Suitable**: If the image shows professional professional polish, or is a professional film/variety show still.
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def collect_images(input_dir: Path) -> List[Dict[str, str]]:
|
| 134 |
+
if not input_dir.exists():
|
| 135 |
+
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
| 136 |
+
|
| 137 |
+
files = [p for p in input_dir.iterdir() if p.is_file() and p.suffix.lower() in IMG_EXTS]
|
| 138 |
+
files.sort()
|
| 139 |
+
|
| 140 |
+
print(f"[Info] Found {len(files)} images in {input_dir}")
|
| 141 |
+
return [{"path": str(p), "filename": p.name} for p in files]
|
| 142 |
+
|
| 143 |
+
def parse_llm_output(text: str) -> Dict[str, Any]:
|
| 144 |
+
default_res = {
|
| 145 |
+
"label": "Parse Error",
|
| 146 |
+
"think": "No reasoning found",
|
| 147 |
+
"raw": text
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if not text:
|
| 151 |
+
return default_res
|
| 152 |
+
|
| 153 |
+
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
|
| 154 |
+
think_content = think_match.group(1).strip() if think_match else ""
|
| 155 |
+
|
| 156 |
+
answer_match = re.search(r'<answer>(.*?)</answer>', text, re.DOTALL)
|
| 157 |
+
|
| 158 |
+
extracted_label = "Parse Error"
|
| 159 |
+
|
| 160 |
+
if answer_match:
|
| 161 |
+
json_str = answer_match.group(1).strip()
|
| 162 |
+
try:
|
| 163 |
+
data = json.loads(json_str)
|
| 164 |
+
raw_ans = data.get("Answer", "")
|
| 165 |
+
|
| 166 |
+
if "unsuitable" in raw_ans.lower():
|
| 167 |
+
extracted_label = "Unsuitable"
|
| 168 |
+
elif "suitable" in raw_ans.lower():
|
| 169 |
+
extracted_label = "Suitable"
|
| 170 |
+
else:
|
| 171 |
+
extracted_label = raw_ans
|
| 172 |
+
|
| 173 |
+
except json.JSONDecodeError:
|
| 174 |
+
if "Unsuitable" in json_str:
|
| 175 |
+
extracted_label = "Unsuitable"
|
| 176 |
+
elif "Suitable" in json_str:
|
| 177 |
+
extracted_label = "Suitable"
|
| 178 |
+
else:
|
| 179 |
+
if "Unsuitable" in text:
|
| 180 |
+
extracted_label = "Unsuitable"
|
| 181 |
+
elif "Suitable" in text:
|
| 182 |
+
extracted_label = "Suitable"
|
| 183 |
+
|
| 184 |
+
return {
|
| 185 |
+
"label": extracted_label,
|
| 186 |
+
"think": think_content,
|
| 187 |
+
"raw": text
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
def prepare_vllm_inputs(batch_meta: List[Dict], processor) -> List[Dict]:
|
| 191 |
+
vllm_inputs = []
|
| 192 |
+
|
| 193 |
+
user_query = "Analyze this image against the design rules and return the JSON decision."
|
| 194 |
+
|
| 195 |
+
for item in batch_meta:
|
| 196 |
+
img_path = item["path"]
|
| 197 |
+
try:
|
| 198 |
+
image_obj = Image.open(img_path).convert("RGB")
|
| 199 |
+
|
| 200 |
+
messages = [
|
| 201 |
+
{"role": "system", "content": [{"type": "text", "text": SYS_PROMPT_TEXT}]},
|
| 202 |
+
{"role": "user", "content": [
|
| 203 |
+
{"type": "image", "image": img_path},
|
| 204 |
+
{"type": "text", "text": user_query}
|
| 205 |
+
]}
|
| 206 |
+
]
|
| 207 |
+
|
| 208 |
+
prompt_text = processor.apply_chat_template(
|
| 209 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
vllm_inputs.append({
|
| 213 |
+
"prompt": prompt_text,
|
| 214 |
+
"multi_modal_data": {"image": image_obj}
|
| 215 |
+
})
|
| 216 |
+
except Exception as e:
|
| 217 |
+
print(f"[Warning] Failed to load {img_path}: {e}")
|
| 218 |
+
vllm_inputs.append(None)
|
| 219 |
+
|
| 220 |
+
return vllm_inputs
|
| 221 |
+
|
| 222 |
+
def main():
|
| 223 |
+
parser = argparse.ArgumentParser(description="AI Visual Comfort Auditor")
|
| 224 |
+
parser.add_argument("--input_dir", type=str, required=True, help="Folder containing images to check")
|
| 225 |
+
parser.add_argument("--model_path", type=str, required=True, help="Path to local Qwen-VL model")
|
| 226 |
+
parser.add_argument("--batch_size", type=int, default=512, help="Inference batch size")
|
| 227 |
+
parser.add_argument("--tp_size", type=int, default=2, help="Tensor Parallel size")
|
| 228 |
+
args = parser.parse_args()
|
| 229 |
+
|
| 230 |
+
input_path = Path(args.input_dir)
|
| 231 |
+
meta_data = collect_images(input_path)
|
| 232 |
+
|
| 233 |
+
if not meta_data:
|
| 234 |
+
print("[Info] No images found. Exiting.")
|
| 235 |
+
return
|
| 236 |
+
|
| 237 |
+
print(f"\n[Init] Loading Model: {args.model_path}")
|
| 238 |
+
|
| 239 |
+
llm = LLM(
|
| 240 |
+
model=args.model_path,
|
| 241 |
+
tokenizer=args.model_path,
|
| 242 |
+
trust_remote_code=True,
|
| 243 |
+
tensor_parallel_size=args.tp_size,
|
| 244 |
+
gpu_memory_utilization=0.90,
|
| 245 |
+
max_model_len=8192,
|
| 246 |
+
enforce_eager=True,
|
| 247 |
+
limit_mm_per_prompt={"image": 1}
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
processor = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True)
|
| 251 |
+
|
| 252 |
+
sampling_params = SamplingParams(
|
| 253 |
+
temperature=0.7,
|
| 254 |
+
max_tokens=1024,
|
| 255 |
+
top_p=0.9
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
results = []
|
| 259 |
+
print(f"\n[Run] Starting Inference on {len(meta_data)} images...")
|
| 260 |
+
|
| 261 |
+
for i in tqdm(range(0, len(meta_data), args.batch_size), desc="Processing Batches"):
|
| 262 |
+
batch_meta = meta_data[i : i + args.batch_size]
|
| 263 |
+
batch_inputs = prepare_vllm_inputs(batch_meta, processor)
|
| 264 |
+
|
| 265 |
+
valid_inputs = [inp for inp in batch_inputs if inp is not None]
|
| 266 |
+
valid_indices = [idx for idx, inp in enumerate(batch_inputs) if inp is not None]
|
| 267 |
+
|
| 268 |
+
if not valid_inputs:
|
| 269 |
+
continue
|
| 270 |
+
|
| 271 |
+
outputs = llm.generate(valid_inputs, sampling_params=sampling_params, use_tqdm=False)
|
| 272 |
+
|
| 273 |
+
for local_idx, out in enumerate(outputs):
|
| 274 |
+
original_meta = batch_meta[valid_indices[local_idx]]
|
| 275 |
+
generated_text = out.outputs[0].text
|
| 276 |
+
|
| 277 |
+
parsed = parse_llm_output(generated_text)
|
| 278 |
+
|
| 279 |
+
results.append({
|
| 280 |
+
"filename": original_meta["filename"],
|
| 281 |
+
"path": original_meta["path"],
|
| 282 |
+
"label": parsed["label"], # Suitable / Unsuitable
|
| 283 |
+
"think": parsed["think"],
|
| 284 |
+
"raw_output": generated_text
|
| 285 |
+
})
|
| 286 |
+
|
| 287 |
+
total = len(results)
|
| 288 |
+
unsuitable_count = sum(1 for r in results if r["label"] == "Unsuitable")
|
| 289 |
+
suitable_count = sum(1 for r in results if r["label"] == "Suitable")
|
| 290 |
+
error_count = total - unsuitable_count - suitable_count
|
| 291 |
+
|
| 292 |
+
unsuitable_rate = (unsuitable_count / total * 100) if total > 0 else 0
|
| 293 |
+
suitable_rate = (suitable_count / total * 100) if total > 0 else 0
|
| 294 |
+
|
| 295 |
+
print("\n" + "="*60)
|
| 296 |
+
print(f"AUDIT REPORT FOR: {input_path.name}")
|
| 297 |
+
print("="*60)
|
| 298 |
+
print(f"{'Total Images':<25}: {total}")
|
| 299 |
+
print("-" * 60)
|
| 300 |
+
print(f"{'UNSUITABLE (Violation)':<25}: {unsuitable_count} ({unsuitable_rate:.2f}%)")
|
| 301 |
+
print(f"{'SUITABLE (Safe)':<25}: {suitable_count} ({suitable_rate:.2f}%)")
|
| 302 |
+
print(f"{'Parse Errors':<25}: {error_count}")
|
| 303 |
+
print("="*60)
|
| 304 |
+
|
| 305 |
+
output_file = input_path / f"audit_result_{input_path.name}.json"
|
| 306 |
+
try:
|
| 307 |
+
with open(output_file, "w", encoding="utf-8") as f:
|
| 308 |
+
json.dump(results, f, ensure_ascii=False, indent=2)
|
| 309 |
+
print(f"\n[Done] Detailed JSON report saved to:\n-> {output_file}")
|
| 310 |
+
except Exception as e:
|
| 311 |
+
print(f"[Error] Could not save JSON: {e}")
|
| 312 |
+
|
| 313 |
+
if __name__ == "__main__":
|
| 314 |
+
try:
|
| 315 |
+
multiprocessing.set_start_method('spawn', force=True)
|
| 316 |
+
except RuntimeError:
|
| 317 |
+
pass
|
| 318 |
+
|
| 319 |
+
main()
|
code_inference/vllm_audit_summary_normal.csv
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
规则脚本,规则维度,测试总数,误报数(Unsuitable),正确通过数(Suitable),误报率(FP Rate),结果文件
|
| 2 |
+
rule_11_vllm.py,文字占比,134,3,131,2.24%,temp_rule_11_vllm.json
|
| 3 |
+
rule_12_vllm.py,样式数量,134,3,131,2.24%,temp_rule_12_vllm.json
|
| 4 |
+
rule_13_vllm.py,排布位置,134,9,125,6.72%,temp_rule_13_vllm.json
|
| 5 |
+
rule_14_vllm.py,设计搭配,134,6,128,4.48%,temp_rule_14_vllm.json
|
| 6 |
+
rule_18_vllm.py,排布间距,134,1,133,0.75%,temp_rule_18_vllm.json
|
| 7 |
+
rule_19_vllm.py,内容构图,134,3,131,2.24%,temp_rule_19_vllm.json
|
stage2_object_v8_1200/added_tokens.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c0284b582e14987fbd3d5a2cb2bd139084371ed9acbae488829a1c900833c680
|
| 3 |
+
size 707
|
stage2_object_v8_1200/chat_template.jinja
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{%- if tools %}
|
| 2 |
+
{{- '<|im_start|>system\n' }}
|
| 3 |
+
{%- if messages[0].role == 'system' %}
|
| 4 |
+
{%- if messages[0].content is string %}
|
| 5 |
+
{{- messages[0].content }}
|
| 6 |
+
{%- else %}
|
| 7 |
+
{%- for content in messages[0].content %}
|
| 8 |
+
{%- if 'text' in content %}
|
| 9 |
+
{{- content.text }}
|
| 10 |
+
{%- endif %}
|
| 11 |
+
{%- endfor %}
|
| 12 |
+
{%- endif %}
|
| 13 |
+
{{- '\n\n' }}
|
| 14 |
+
{%- endif %}
|
| 15 |
+
{{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
|
| 16 |
+
{%- for tool in tools %}
|
| 17 |
+
{{- "\n" }}
|
| 18 |
+
{{- tool | tojson }}
|
| 19 |
+
{%- endfor %}
|
| 20 |
+
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
|
| 21 |
+
{%- else %}
|
| 22 |
+
{%- if messages[0].role == 'system' %}
|
| 23 |
+
{{- '<|im_start|>system\n' }}
|
| 24 |
+
{%- if messages[0].content is string %}
|
| 25 |
+
{{- messages[0].content }}
|
| 26 |
+
{%- else %}
|
| 27 |
+
{%- for content in messages[0].content %}
|
| 28 |
+
{%- if 'text' in content %}
|
| 29 |
+
{{- content.text }}
|
| 30 |
+
{%- endif %}
|
| 31 |
+
{%- endfor %}
|
| 32 |
+
{%- endif %}
|
| 33 |
+
{{- '<|im_end|>\n' }}
|
| 34 |
+
{%- endif %}
|
| 35 |
+
{%- endif %}
|
| 36 |
+
{%- set image_count = namespace(value=0) %}
|
| 37 |
+
{%- set video_count = namespace(value=0) %}
|
| 38 |
+
{%- for message in messages %}
|
| 39 |
+
{%- if message.role == "user" %}
|
| 40 |
+
{{- '<|im_start|>' + message.role + '\n' }}
|
| 41 |
+
{%- if message.content is string %}
|
| 42 |
+
{{- message.content }}
|
| 43 |
+
{%- else %}
|
| 44 |
+
{%- for content in message.content %}
|
| 45 |
+
{%- if content.type == 'image' or 'image' in content or 'image_url' in content %}
|
| 46 |
+
{%- set image_count.value = image_count.value + 1 %}
|
| 47 |
+
{%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
|
| 48 |
+
<|vision_start|><|image_pad|><|vision_end|>
|
| 49 |
+
{%- elif content.type == 'video' or 'video' in content %}
|
| 50 |
+
{%- set video_count.value = video_count.value + 1 %}
|
| 51 |
+
{%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
|
| 52 |
+
<|vision_start|><|video_pad|><|vision_end|>
|
| 53 |
+
{%- elif 'text' in content %}
|
| 54 |
+
{{- content.text }}
|
| 55 |
+
{%- endif %}
|
| 56 |
+
{%- endfor %}
|
| 57 |
+
{%- endif %}
|
| 58 |
+
{{- '<|im_end|>\n' }}
|
| 59 |
+
{%- elif message.role == "assistant" %}
|
| 60 |
+
{{- '<|im_start|>' + message.role + '\n' }}
|
| 61 |
+
{%- if message.content is string %}
|
| 62 |
+
{{- message.content }}
|
| 63 |
+
{%- else %}
|
| 64 |
+
{%- for content_item in message.content %}
|
| 65 |
+
{%- if 'text' in content_item %}
|
| 66 |
+
{{- content_item.text }}
|
| 67 |
+
{%- endif %}
|
| 68 |
+
{%- endfor %}
|
| 69 |
+
{%- endif %}
|
| 70 |
+
{%- if message.tool_calls %}
|
| 71 |
+
{%- for tool_call in message.tool_calls %}
|
| 72 |
+
{%- if (loop.first and message.content) or (not loop.first) %}
|
| 73 |
+
{{- '\n' }}
|
| 74 |
+
{%- endif %}
|
| 75 |
+
{%- if tool_call.function %}
|
| 76 |
+
{%- set tool_call = tool_call.function %}
|
| 77 |
+
{%- endif %}
|
| 78 |
+
{{- '<tool_call>\n{"name": "' }}
|
| 79 |
+
{{- tool_call.name }}
|
| 80 |
+
{{- '", "arguments": ' }}
|
| 81 |
+
{%- if tool_call.arguments is string %}
|
| 82 |
+
{{- tool_call.arguments }}
|
| 83 |
+
{%- else %}
|
| 84 |
+
{{- tool_call.arguments | tojson }}
|
| 85 |
+
{%- endif %}
|
| 86 |
+
{{- '}\n</tool_call>' }}
|
| 87 |
+
{%- endfor %}
|
| 88 |
+
{%- endif %}
|
| 89 |
+
{{- '<|im_end|>\n' }}
|
| 90 |
+
{%- elif message.role == "tool" %}
|
| 91 |
+
{%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
|
| 92 |
+
{{- '<|im_start|>user' }}
|
| 93 |
+
{%- endif %}
|
| 94 |
+
{{- '\n<tool_response>\n' }}
|
| 95 |
+
{%- if message.content is string %}
|
| 96 |
+
{{- message.content }}
|
| 97 |
+
{%- else %}
|
| 98 |
+
{%- for content in message.content %}
|
| 99 |
+
{%- if content.type == 'image' or 'image' in content or 'image_url' in content %}
|
| 100 |
+
{%- set image_count.value = image_count.value + 1 %}
|
| 101 |
+
{%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
|
| 102 |
+
<|vision_start|><|image_pad|><|vision_end|>
|
| 103 |
+
{%- elif content.type == 'video' or 'video' in content %}
|
| 104 |
+
{%- set video_count.value = video_count.value + 1 %}
|
| 105 |
+
{%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
|
| 106 |
+
<|vision_start|><|video_pad|><|vision_end|>
|
| 107 |
+
{%- elif 'text' in content %}
|
| 108 |
+
{{- content.text }}
|
| 109 |
+
{%- endif %}
|
| 110 |
+
{%- endfor %}
|
| 111 |
+
{%- endif %}
|
| 112 |
+
{{- '\n</tool_response>' }}
|
| 113 |
+
{%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
|
| 114 |
+
{{- '<|im_end|>\n' }}
|
| 115 |
+
{%- endif %}
|
| 116 |
+
{%- endif %}
|
| 117 |
+
{%- endfor %}
|
| 118 |
+
{%- if add_generation_prompt %}
|
| 119 |
+
{{- '<|im_start|>assistant\n' }}
|
| 120 |
+
{%- endif %}
|
stage2_object_v8_1200/config.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b321be22f463f0af477f83cf7e63c566bb15645299208c6656a57ece7b9ffa87
|
| 3 |
+
size 1613
|
stage2_object_v8_1200/generation_config.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3ff2e83a0510cccccc85c8c96c7df4207985c098b526d798bc2bc68e50bb1a41
|
| 3 |
+
size 199
|
stage2_object_v8_1200/model-00001-of-00004.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b3b9200b783fed11909d1f8439cba31af946df3b191adc05ac0a829bf659f3d5
|
| 3 |
+
size 4998056552
|
stage2_object_v8_1200/model-00002-of-00004.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0f1fa56d2765458405054a76f9069a191e1a09f85d9574064d62282c5d8bbeb2
|
| 3 |
+
size 4915962464
|
stage2_object_v8_1200/model-00003-of-00004.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a93ae2c790a2f60ccc8133028da3b731a70b749c63c69fa33352095f252cb8f
|
| 3 |
+
size 4915962496
|
stage2_object_v8_1200/model-00004-of-00004.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fc7533549165d9745eaf88846df9fd5b44544af402af4613a1e5a3deeb2d95d6
|
| 3 |
+
size 2704357976
|
stage2_object_v8_1200/model.safetensors.index.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dbdf49cc47bf9a028d0b1aa914b25401527572f03b092c8dfd9d428c9172783f
|
| 3 |
+
size 67791
|
stage2_object_v8_1200/preprocessor_config.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:93585062a80db5e8ca038efc7726a3e6411d9db948472d81d63c6303993be8c5
|
| 3 |
+
size 782
|
stage2_object_v8_1200/special_tokens_map.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:76862e765266b85aa9459767e33cbaf13970f327a0e88d1c65846c2ddd3a1ecd
|
| 3 |
+
size 613
|
stage2_object_v8_1200/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4
|
| 3 |
+
size 11422654
|
stage2_object_v8_1200/tokenizer_config.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cf43a5bf1a49ee69ecced02f419b169e72559034dcf15af47cf775bd253830f0
|
| 3 |
+
size 5472
|
stage2_object_v8_1200/video_preprocessor_config.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:59c5c9eb52182eb14c06ffb10ca9effd29adce5f238a95de23ca14a38dbd2cb1
|
| 3 |
+
size 817
|
stage2_object_v8_1200/vocab.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910
|
| 3 |
+
size 2776833
|