Image-Text-to-Text
PEFT
Safetensors
qwen3-vl
vision-language
portrait-aesthetics
aesthetics-evaluation
lora
llama-factory
Instructions to use Artoria0429/code_portrait_track_1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use Artoria0429/code_portrait_track_1 with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 19,045 Bytes
617bcee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 | import argparse
import copy
import json
import os
import re
from collections import Counter
from glob import glob
from typing import Any, Dict, List, Optional, Tuple
DEFAULT_TEMPLATE_PATH = "outputs/predictions/track_1_test.json"
DEFAULT_PRED_PATH = "outputs/predictions/predict_ckpt2660/generated_predictions.jsonl"
DEFAULT_OUTPUT_PATH = "outputs/submissions/answers/track_1_test.json"
def load_json(path: str) -> Any:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def load_jsonl(path: str) -> List[Dict[str, Any]]:
rows: List[Dict[str, Any]] = []
with open(path, "r", encoding="utf-8") as f:
for line_no, line in enumerate(f, start=1):
raw = line.strip()
if not raw:
continue
try:
rows.append(json.loads(raw))
except Exception:
print(f"[WARN] skip invalid jsonl line: {line_no}")
return rows
def normalize_level(level: Any) -> Optional[str]:
if level is None:
return None
s = str(level).strip()
if not s:
return None
m = {
"poor": "Poor",
"medium": "Medium",
"good": "Good",
"a": "A",
"b": "B",
"c": "C",
}
return m.get(s.lower(), s)
def score_to_submission_level(score: float, low_threshold: float = 5.0, high_threshold: float = 7.0) -> str:
# 比赛映射:A=Poor(0~5), B=Medium(5~7), C=Good(7~10)
if score < low_threshold:
return "A"
if score < high_threshold:
return "B"
return "C"
def to_submission_level(level: Any) -> Optional[str]:
"""
Submission mapping required by Track-1 template:
A -> Poor, B -> Medium, C -> Good
"""
norm = normalize_level(level)
if norm is None:
return None
m = {
"Poor": "A",
"Medium": "B",
"Good": "C",
"A": "A",
"B": "B",
"C": "C",
}
return m.get(norm)
def normalize_prediction_text(pred_row: Dict[str, Any]) -> str:
# 兼容不同推理后端字段名。
for k in ("predict", "response", "output", "text", "generation"):
if k in pred_row:
return str(pred_row.get(k, ""))
return str(pred_row)
def extract_level_from_text(pred_text: str, crit_key: str) -> Optional[str]:
pattern = rf'"{re.escape(crit_key)}"\s*:\s*\{{.*?"level"\s*:\s*"([^"]+)"'
m = re.search(pattern, pred_text, flags=re.IGNORECASE | re.DOTALL)
if not m:
return None
return to_submission_level(m.group(1))
def try_parse_predict_json(text: str) -> Optional[Dict[str, Any]]:
text = str(text or "").strip()
if not text:
return None
# 1) 直接解析
try:
obj = json.loads(text)
if isinstance(obj, dict):
return obj
except Exception:
pass
# 2) 尝试提取从首个 { 到最后一个 } 的片段
left = text.find("{")
right = text.rfind("}")
if left != -1 and right != -1 and left < right:
snippet = text[left:right + 1]
try:
obj = json.loads(snippet)
if isinstance(obj, dict):
return obj
except Exception:
pass
return None
def extract_answer(pred_obj: Optional[Dict[str, Any]], pred_text: str) -> Optional[str]:
if pred_obj is not None:
answer = str(pred_obj.get("answer", "")).strip().upper()
if answer in {"A", "B", "C", "D"}:
return answer
m = re.search(r'"answer"\s*:\s*"([A-D])"', pred_text, re.IGNORECASE)
if m:
return m.group(1).upper()
# 兼容 QA-only 推理:模型可能只输出单个字母(如 "C")。
raw = str(pred_text or "").strip().upper()
if raw:
# 情况1:整行仅有一个候选字母(允许尾随标点)。
m = re.match(r"^\s*([A-D])(?:[\.\)\]::]|\s)*$", raw)
if m:
return m.group(1)
# 情况2:短文本中只出现唯一一个 A/B/C/D,且不包含常见 JSON/选项结构。
if len(raw) <= 12 and ("{" not in raw) and ("\"" not in raw):
hits = re.findall(r"[A-D]", raw)
if len(hits) == 1:
return hits[0]
return None
def extract_total_score(pred_obj: Optional[Dict[str, Any]], pred_text: str) -> Optional[float]:
val: Optional[float] = None
if pred_obj is not None and "total_score" in pred_obj:
try:
val = float(pred_obj["total_score"])
except Exception:
val = None
if val is None:
m = re.search(r'"total_score"\s*:\s*([0-9]+(?:\.[0-9]+)?)', pred_text)
if m:
val = float(m.group(1))
if val is None:
return None
return max(0.0, min(100.0, float(val)))
def merge_total_scores(total_votes: List[float], mode: str) -> Optional[int]:
if not total_votes:
return None
vals = [float(v) for v in total_votes]
if mode == "trim_mean" and len(vals) >= 3:
vals = sorted(vals)[1:-1]
merged = sum(vals) / len(vals)
return max(0, min(100, int(round(merged))))
def parse_score_from_text(pred_text: str, crit_key: str) -> Optional[float]:
pattern = rf'"{re.escape(crit_key)}"\s*:\s*\{{.*?"score"\s*:\s*([0-9]+(?:\.[0-9]+)?)'
m = re.search(pattern, pred_text, flags=re.IGNORECASE | re.DOTALL)
if not m:
return None
try:
return float(m.group(1))
except Exception:
return None
def extract_one_criteria_level_and_score(
crit_key: str,
pred_obj: Optional[Dict[str, Any]],
pred_text: str,
) -> Tuple[Optional[str], Optional[float]]:
score: Optional[float] = None
level: Optional[str] = None
if isinstance(pred_obj, dict):
src = pred_obj.get("criteria", {})
if isinstance(src, dict) and crit_key in src:
value = src.get(crit_key)
if isinstance(value, dict):
if "score" in value:
try:
score = float(value["score"])
except Exception:
score = None
level = to_submission_level(value.get("level"))
else:
level = to_submission_level(value)
if score is None:
score = parse_score_from_text(pred_text, crit_key)
if level is None:
level = extract_level_from_text(pred_text, crit_key)
if level not in {"A", "B", "C"}:
level = None
return level, score
def choose_majority_level(
levels: List[str],
scores: List[float],
fallback_level: Optional[str],
low_threshold: float,
high_threshold: float,
) -> str:
if levels:
cnt = Counter(levels)
top_n = max(cnt.values())
top_levels = sorted([k for k, v in cnt.items() if v == top_n])
if len(top_levels) == 1:
return top_levels[0]
# 平票时,用多次预测的均值 score 判定等级。
if scores:
mean_score = sum(scores) / len(scores)
return score_to_submission_level(mean_score, low_threshold=low_threshold, high_threshold=high_threshold)
if fallback_level in {"A", "B", "C"}:
return fallback_level
return "B"
def parse_weights(raw_weights: Optional[List[float]], n_models: int) -> List[float]:
# 默认等权;若传入权重则要求和预测文件数一致。
if raw_weights is None:
return [1.0] * n_models
if len(raw_weights) != n_models:
raise ValueError(
f"--weights length ({len(raw_weights)}) must equal number of prediction files ({n_models})."
)
for w in raw_weights:
if w < 0:
raise ValueError("weights must be non-negative.")
# 全零没有意义,回退等权。
if sum(raw_weights) == 0:
return [1.0] * n_models
return raw_weights
def load_thresholds(
thresholds_json: str,
) -> Tuple[Dict[str, Dict[str, float]], Dict[str, float]]:
"""
Accepts JSON in either format:
1) {"criteria": {"Color Harmony": {"low": 4.9, "high": 7.1}}, "default": {"low":5,"high":7}}
2) {"Color Harmony": {"low": 4.9, "high": 7.1}, ...}
"""
default = {"low": 5.0, "high": 7.0}
per_criteria: Dict[str, Dict[str, float]] = {}
if not thresholds_json:
return per_criteria, default
if not os.path.exists(thresholds_json):
print(f"[WARN] thresholds file not found: {thresholds_json}, fallback to default 5/7")
return per_criteria, default
obj = load_json(thresholds_json)
if not isinstance(obj, dict):
print(f"[WARN] invalid thresholds json format: {thresholds_json}, fallback to default 5/7")
return per_criteria, default
if "default" in obj and isinstance(obj.get("default"), dict):
d = obj["default"]
low = d.get("low", 5.0)
high = d.get("high", 7.0)
try:
low_f = float(low)
high_f = float(high)
if low_f < high_f:
default = {"low": low_f, "high": high_f}
except Exception:
pass
src = obj.get("criteria") if isinstance(obj.get("criteria"), dict) else obj
if isinstance(src, dict):
for k, v in src.items():
if not isinstance(v, dict):
continue
if "low" not in v or "high" not in v:
continue
try:
low = float(v["low"])
high = float(v["high"])
except Exception:
continue
if low < high:
per_criteria[str(k)] = {"low": low, "high": high}
return per_criteria, default
def resolve_best_index(best_index: int, weights: List[float]) -> int:
# best_index=-1 表示自动选择权重最高的模型作为平票时的优先模型。
if best_index >= 0:
if best_index >= len(weights):
raise ValueError(f"--best_index out of range: {best_index}, num_models={len(weights)}")
return best_index
return max(range(len(weights)), key=lambda i: weights[i])
def extract_criteria_voting(
template_item: Dict[str, Any],
pred_objs: List[Optional[Dict[str, Any]]],
pred_texts: List[str],
per_criteria_thresholds: Dict[str, Dict[str, float]],
default_thresholds: Dict[str, float],
) -> Dict[str, Dict[str, str]]:
out: Dict[str, Dict[str, str]] = {}
for crit_key, crit_val in template_item.get("criteria", {}).items():
thresholds = per_criteria_thresholds.get(crit_key, default_thresholds)
low = float(thresholds.get("low", 5.0))
high = float(thresholds.get("high", 7.0))
if not (low < high):
low, high = 5.0, 7.0
level_votes: List[str] = []
score_votes: List[float] = []
for obj, text in zip(pred_objs, pred_texts):
level, score = extract_one_criteria_level_and_score(crit_key, obj, text)
if level is not None:
level_votes.append(level)
if score is not None:
score_votes.append(score)
prev = str(crit_val.get("level", "")).strip() if isinstance(crit_val, dict) else ""
final_level = choose_majority_level(
level_votes,
score_votes,
prev,
low_threshold=low,
high_threshold=high,
)
out[crit_key] = {"level": final_level}
return out
def pick_default_predictions_path() -> str:
candidates = [
"outputs/predictions/predict_ckpt2660/generated_predictions.jsonl",
"outputs/predictions/generated_predictions.jsonl",
]
# 自动兜底:在预测目录里找最近一次 generated_predictions.jsonl。
dynamic = sorted(
glob("outputs/predictions/**/generated_predictions.jsonl", recursive=True),
key=lambda x: os.path.getmtime(x),
reverse=True,
)
candidates = dynamic + candidates
for p in candidates:
if os.path.exists(p):
return p
return DEFAULT_PRED_PATH
def choose_weighted_answer(
votes_by_model: List[Optional[str]],
weights: List[float],
best_index: int,
tie_break_answer: Optional[str],
) -> str:
"""
更合理的答案融合策略:
1) 加权投票(按各变体可靠性权重)
2) 若平票且提供 tie-break 结果,则优先用 tie-break
3) 若仍平票,采用最佳变体(best_index)在平票选项中的答案
4) 最后才做稳定兜底(字母序)
"""
label_scores = {"A": 0.0, "B": 0.0, "C": 0.0, "D": 0.0}
for i, ans in enumerate(votes_by_model):
if ans in label_scores:
label_scores[ans] += weights[i]
max_score = max(label_scores.values())
if max_score <= 0:
return "A"
tied = sorted([k for k, v in label_scores.items() if v == max_score])
if len(tied) == 1:
return tied[0]
if tie_break_answer in tied:
return tie_break_answer # 用专门 tie-break 结果判平票
best_vote = votes_by_model[best_index]
if best_vote in tied:
return str(best_vote)
return tied[0]
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--template_json", type=str, default=DEFAULT_TEMPLATE_PATH)
parser.add_argument(
"--predictions_jsonl",
type=str,
nargs="+",
default=None,
help="One or more generated_predictions.jsonl paths for voting.",
)
parser.add_argument(
"--weights",
type=float,
nargs="+",
default=None,
help="Optional weights for prediction files (same length as --predictions_jsonl).",
)
parser.add_argument(
"--best_index",
type=int,
default=-1,
help="Best model index for tie fallback. -1 means auto argmax(weights).",
)
parser.add_argument(
"--tie_break_jsonl",
type=str,
default="",
help="Optional tie-break predictions file used only when weighted vote ties.",
)
parser.add_argument(
"--thresholds_json",
type=str,
default="",
help="Optional per-criterion thresholds json for score->A/B/C mapping.",
)
parser.add_argument(
"--total_score_fusion",
type=str,
default="mean",
choices=["mean", "trim_mean"],
help="Fusion mode for total_score across multi-prompt predictions.",
)
parser.add_argument("--output_json", type=str, default=DEFAULT_OUTPUT_PATH)
args = parser.parse_args()
pred_paths = args.predictions_jsonl if args.predictions_jsonl else [pick_default_predictions_path()]
pred_paths = [p for p in pred_paths if str(p).strip()]
if not pred_paths:
raise ValueError("No predictions_jsonl provided or discovered.")
template_data = load_json(args.template_json)
pred_sets = [load_jsonl(p) for p in pred_paths]
weights = parse_weights(args.weights, len(pred_sets))
best_index = resolve_best_index(args.best_index, weights)
per_criteria_thresholds, default_thresholds = load_thresholds(args.thresholds_json)
tie_break_rows: List[Dict[str, Any]] = []
if args.tie_break_jsonl:
tie_break_rows = load_jsonl(args.tie_break_jsonl)
if not isinstance(template_data, list):
raise ValueError("template_json must be a list.")
print(f"[INFO] template items: {len(template_data)}")
for p, rows in zip(pred_paths, pred_sets):
print(f"[INFO] prediction rows: {len(rows)} ({p})")
print(f"[INFO] answer weights: {weights}")
print(f"[INFO] answer best_index: {best_index}")
print(f"[INFO] criteria thresholds default: low={default_thresholds['low']}, high={default_thresholds['high']}")
if per_criteria_thresholds:
print(f"[INFO] criteria thresholds loaded: {len(per_criteria_thresholds)}")
if args.tie_break_jsonl:
print(f"[INFO] tie_break rows: {len(tie_break_rows)} ({args.tie_break_jsonl})")
final_submission: List[Dict[str, Any]] = []
parsed_ok = 0
filled = 0
for i, item in enumerate(template_data):
new_item = copy.deepcopy(item)
# 收集每次预测在第 i 条样本上的结果。
row_texts: List[str] = []
row_objs: List[Optional[Dict[str, Any]]] = []
answer_votes_by_model: List[Optional[str]] = []
total_votes: List[int] = []
for rows in pred_sets:
if i >= len(rows):
answer_votes_by_model.append(None)
continue
pred_text = normalize_prediction_text(rows[i])
pred_obj = try_parse_predict_json(pred_text)
if pred_obj is not None:
parsed_ok += 1
row_texts.append(pred_text)
row_objs.append(pred_obj)
ans = extract_answer(pred_obj, pred_text)
answer_votes_by_model.append(ans)
ts = extract_total_score(pred_obj, pred_text)
if ts is not None:
total_votes.append(ts)
if not row_texts:
final_submission.append(new_item)
continue
new_item["criteria"] = extract_criteria_voting(
new_item,
row_objs,
row_texts,
per_criteria_thresholds=per_criteria_thresholds,
default_thresholds=default_thresholds,
)
# total_score 用多次预测均值,减少单次抖动。
merged_total = merge_total_scores(total_votes, args.total_score_fusion)
if merged_total is not None:
new_item["total_score"] = max(0, min(100, merged_total))
tie_break_answer: Optional[str] = None
if i < len(tie_break_rows):
tb_text = normalize_prediction_text(tie_break_rows[i])
tb_obj = try_parse_predict_json(tb_text)
tie_break_answer = extract_answer(tb_obj, tb_text)
new_item["answer"] = choose_weighted_answer(
votes_by_model=answer_votes_by_model,
weights=weights,
best_index=best_index,
tie_break_answer=tie_break_answer,
)
final_submission.append(new_item)
filled += 1
os.makedirs(os.path.dirname(args.output_json), exist_ok=True)
with open(args.output_json, "w", encoding="utf-8") as f:
json.dump(final_submission, f, ensure_ascii=False, indent=2)
parsed_total = filled * len(pred_paths)
print(f"[INFO] parsed predict json ok: {parsed_ok}/{parsed_total}")
print(f"[INFO] saved submission: {args.output_json}")
if __name__ == "__main__":
main()
|