zry-research commited on
Commit
53ccd32
·
1 Parent(s): 717a9ac

feat: add weight to model/

Browse files
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