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
Upload 15 files
Browse files- .gitattributes +5 -0
- README.md +191 -0
- REPRODUCIBLE_PATHS.md +65 -0
- codabench_portrait_train.json +3 -0
- codabench_portrait_train_balanced_v2.json +3 -0
- codabench_portrait_train_multitask_v2.json +3 -0
- codabench_portrait_train_score_v2.json +3 -0
- codabench_portrait_val.json +0 -0
- codabench_portrait_val_multitask_v2.json +3 -0
- codabench_portrait_val_score_v2.json +0 -0
- convert_to_answer.py +566 -0
- portrait_composition_workflow_vector.svg +132 -0
- prepare_dataset.py +361 -0
- prepare_test_prompt_v2.py +639 -0
- train_track1_stage1_qwen3vl8b_v1.yaml +46 -0
- train_track1_stage2_qwen3vl8b_v1.yaml +48 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
codabench_portrait_train_balanced_v2.json filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
codabench_portrait_train_multitask_v2.json filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
codabench_portrait_train_score_v2.json filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
codabench_portrait_train.json filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
codabench_portrait_val_multitask_v2.json filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Portrait Composition Aesthetics Evaluation
|
| 2 |
+
|
| 3 |
+
This project provides an inference and fusion pipeline for portrait composition aesthetics evaluation. It converts the official test set into multi-prompt prediction inputs, runs prediction with Stage2 LoRA adapters, and fuses multiple prediction files into the final `track_1_test.json`.
|
| 4 |
+
|
| 5 |
+
## 1. Project Structure
|
| 6 |
+
|
| 7 |
+
```text
|
| 8 |
+
project_root/
|
| 9 |
+
prepare_test_prompt_v2.py # Build multi-prompt test inputs
|
| 10 |
+
convert_to_answer.py # Fuse multi-path prediction results
|
| 11 |
+
train_track1_stage1_qwen3vl8b_v1.yaml
|
| 12 |
+
train_track1_stage2_qwen3vl8b_v1.yaml
|
| 13 |
+
qwen3vl8b_r1stage2_seed_ensemble_fast_20260511_112825/
|
| 14 |
+
seed20260511_stage2/ # Stage2 adapter 1
|
| 15 |
+
seed20260512_stage2/ # Stage2 adapter 2
|
| 16 |
+
datasets/
|
| 17 |
+
images/ # Test images
|
| 18 |
+
original_annotations/
|
| 19 |
+
track_1_test.json # Official test template
|
| 20 |
+
outputs/
|
| 21 |
+
predictions/ # Model prediction outputs
|
| 22 |
+
submissions/answers/ # Final submission file
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
For path conventions, see `REPRODUCIBLE_PATHS.md`.
|
| 26 |
+
|
| 27 |
+
## 2. Environment
|
| 28 |
+
|
| 29 |
+
Use an environment that supports Qwen3-VL and LoRA inference. LLaMA-Factory is recommended for prediction.
|
| 30 |
+
|
| 31 |
+
Basic dependencies:
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
pip install torch transformers peft accelerate
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
If you use LLaMA-Factory, install it first and make sure `llamafactory-cli` is available.
|
| 38 |
+
|
| 39 |
+
Base model:
|
| 40 |
+
|
| 41 |
+
```text
|
| 42 |
+
Qwen/Qwen3-VL-8B-Instruct
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## 3. Prepare Test Data
|
| 46 |
+
|
| 47 |
+
Place the official test file at:
|
| 48 |
+
|
| 49 |
+
```text
|
| 50 |
+
datasets/original_annotations/track_1_test.json
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
Place test images under:
|
| 54 |
+
|
| 55 |
+
```text
|
| 56 |
+
datasets/images/
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Generate multi-prompt test inputs:
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
python prepare_test_prompt_v2.py \
|
| 63 |
+
--input_json datasets/original_annotations/track_1_test.json \
|
| 64 |
+
--image_dir datasets/images \
|
| 65 |
+
--output_json outputs/predictions/track1_test_convert.json \
|
| 66 |
+
--prompt_variants v1,v2,v3,v4,v6,v7,v8
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
This command generates:
|
| 70 |
+
|
| 71 |
+
```text
|
| 72 |
+
outputs/predictions/track1_test_convert_v1.json
|
| 73 |
+
outputs/predictions/track1_test_convert_v2.json
|
| 74 |
+
outputs/predictions/track1_test_convert_v3.json
|
| 75 |
+
outputs/predictions/track1_test_convert_v4.json
|
| 76 |
+
outputs/predictions/track1_test_convert_v6.json
|
| 77 |
+
outputs/predictions/track1_test_convert_v7.json
|
| 78 |
+
outputs/predictions/track1_test_convert_v8.json
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
## 4. Run Multi-Path Prediction
|
| 82 |
+
|
| 83 |
+
Use the Stage2 adapters to run prediction on the generated prompt variants:
|
| 84 |
+
|
| 85 |
+
```text
|
| 86 |
+
qwen3vl8b_r1stage2_seed_ensemble_fast_20260511_112825/seed20260511_stage2
|
| 87 |
+
qwen3vl8b_r1stage2_seed_ensemble_fast_20260511_112825/seed20260512_stage2
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
If you use LLaMA-Factory, register each test JSON in `dataset_info.json`. Example:
|
| 91 |
+
|
| 92 |
+
```json
|
| 93 |
+
{
|
| 94 |
+
"track1_test_v1": {
|
| 95 |
+
"file_name": "outputs/predictions/track1_test_convert_v1.json",
|
| 96 |
+
"formatting": "sharegpt",
|
| 97 |
+
"columns": {
|
| 98 |
+
"messages": "messages",
|
| 99 |
+
"images": "images"
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
Example prediction config:
|
| 106 |
+
|
| 107 |
+
```yaml
|
| 108 |
+
model_name_or_path: Qwen/Qwen3-VL-8B-Instruct
|
| 109 |
+
adapter_name_or_path: qwen3vl8b_r1stage2_seed_ensemble_fast_20260511_112825/seed20260511_stage2
|
| 110 |
+
template: qwen3_vl_nothink
|
| 111 |
+
stage: sft
|
| 112 |
+
finetuning_type: lora
|
| 113 |
+
|
| 114 |
+
dataset_dir: .
|
| 115 |
+
eval_dataset: track1_test_v1
|
| 116 |
+
do_predict: true
|
| 117 |
+
predict_with_generate: true
|
| 118 |
+
cutoff_len: 4096
|
| 119 |
+
image_max_pixels: 786432
|
| 120 |
+
per_device_eval_batch_size: 8
|
| 121 |
+
max_new_tokens: 384
|
| 122 |
+
do_sample: false
|
| 123 |
+
top_p: 1.0
|
| 124 |
+
repetition_penalty: 1.02
|
| 125 |
+
bf16: true
|
| 126 |
+
flash_attn: auto
|
| 127 |
+
|
| 128 |
+
output_dir: outputs/predictions/seed20260511_v1
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
Run prediction:
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
llamafactory-cli train predict_seed20260511_v1.yaml
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
Each prediction run should produce:
|
| 138 |
+
|
| 139 |
+
```text
|
| 140 |
+
outputs/predictions/<run_name>/generated_predictions.jsonl
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
## 5. Fuse Prediction Results
|
| 144 |
+
|
| 145 |
+
Copy or symlink the official test template to:
|
| 146 |
+
|
| 147 |
+
```text
|
| 148 |
+
outputs/predictions/track_1_test.json
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
Fuse prediction results:
|
| 152 |
+
|
| 153 |
+
```bash
|
| 154 |
+
python convert_to_answer.py \
|
| 155 |
+
--template_json outputs/predictions/track_1_test.json \
|
| 156 |
+
--predictions_jsonl \
|
| 157 |
+
outputs/predictions/seed20260511_v1/generated_predictions.jsonl \
|
| 158 |
+
outputs/predictions/seed20260511_v2/generated_predictions.jsonl \
|
| 159 |
+
outputs/predictions/seed20260511_v3/generated_predictions.jsonl \
|
| 160 |
+
outputs/predictions/seed20260511_v4/generated_predictions.jsonl \
|
| 161 |
+
outputs/predictions/seed20260511_v6/generated_predictions.jsonl \
|
| 162 |
+
outputs/predictions/seed20260511_v7/generated_predictions.jsonl \
|
| 163 |
+
outputs/predictions/seed20260511_v8/generated_predictions.jsonl \
|
| 164 |
+
--weights 1 1 1 1 1 1 1 \
|
| 165 |
+
--best_index 0 \
|
| 166 |
+
--total_score_fusion mean \
|
| 167 |
+
--output_json outputs/submissions/answers/track_1_test.json
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
To fuse results from both Stage2 adapters, add all corresponding `generated_predictions.jsonl` files to `--predictions_jsonl` and provide the same number of values in `--weights`.
|
| 171 |
+
|
| 172 |
+
## 6. Output
|
| 173 |
+
|
| 174 |
+
The final result is:
|
| 175 |
+
|
| 176 |
+
```text
|
| 177 |
+
outputs/submissions/answers/track_1_test.json
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
The file preserves the official template structure and fills in the fused:
|
| 181 |
+
|
| 182 |
+
- `criteria.level`
|
| 183 |
+
- `total_score`
|
| 184 |
+
- `answer`
|
| 185 |
+
|
| 186 |
+
## 7. Notes
|
| 187 |
+
|
| 188 |
+
- All paths are relative to the project root.
|
| 189 |
+
- Image paths use the format `datasets/images/<image_name>.jpg`.
|
| 190 |
+
- `convert_to_answer.py` supports multi-path prediction fusion, weighted answer voting, majority voting for levels, and mean fusion for `total_score`.
|
| 191 |
+
- If the model output is not strict JSON, the fusion script attempts to extract the JSON snippet and answer field from text.
|
REPRODUCIBLE_PATHS.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Reproducible Path Convention
|
| 2 |
+
|
| 3 |
+
This project no longer depends on machine-specific absolute paths. All code and data files use public model IDs or paths relative to the project root.
|
| 4 |
+
|
| 5 |
+
## Expected Directory Layout
|
| 6 |
+
|
| 7 |
+
```text
|
| 8 |
+
project_root/
|
| 9 |
+
codabench_portrait_train.json
|
| 10 |
+
codabench_portrait_val.json
|
| 11 |
+
prepare_dataset.py
|
| 12 |
+
prepare_test_prompt_v2.py
|
| 13 |
+
convert_to_answer.py
|
| 14 |
+
train_track1_stage1_qwen3vl8b_v1.yaml
|
| 15 |
+
train_track1_stage2_qwen3vl8b_v1.yaml
|
| 16 |
+
datasets/
|
| 17 |
+
images/
|
| 18 |
+
*.jpg
|
| 19 |
+
original_annotations/
|
| 20 |
+
track_1_test.json
|
| 21 |
+
outputs/
|
| 22 |
+
predictions/
|
| 23 |
+
submissions/
|
| 24 |
+
saves/
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## Path Rules
|
| 28 |
+
|
| 29 |
+
- Base model: `Qwen/Qwen3-VL-8B-Instruct`
|
| 30 |
+
- Training and validation image paths: `datasets/images/<image_name>.jpg`
|
| 31 |
+
- Official test template: `outputs/predictions/track_1_test.json` or `datasets/original_annotations/track_1_test.json`
|
| 32 |
+
- Prediction outputs: `outputs/predictions/`
|
| 33 |
+
- Submission outputs: `outputs/submissions/answers/`
|
| 34 |
+
- LoRA adapter outputs: `saves/`
|
| 35 |
+
- Data generation logs: `logs/`
|
| 36 |
+
|
| 37 |
+
## Typical Usage
|
| 38 |
+
|
| 39 |
+
Generate training data:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
python prepare_dataset.py \
|
| 43 |
+
--train_json codabench_portrait_train.json \
|
| 44 |
+
--val_json codabench_portrait_val.json \
|
| 45 |
+
--output_dir .
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
Generate multi-prompt test data:
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
python prepare_test_prompt_v2.py \
|
| 52 |
+
--input_json datasets/original_annotations/track_1_test.json \
|
| 53 |
+
--image_dir datasets/images \
|
| 54 |
+
--output_json outputs/predictions/track1_test_convert.json \
|
| 55 |
+
--prompt_variants v1,v2,v3,v4,v6,v7,v8
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
Fuse prediction results:
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
python convert_to_answer.py \
|
| 62 |
+
--template_json outputs/predictions/track_1_test.json \
|
| 63 |
+
--predictions_jsonl outputs/predictions/run_v1/generated_predictions.jsonl outputs/predictions/run_v2/generated_predictions.jsonl \
|
| 64 |
+
--output_json outputs/submissions/answers/track_1_test.json
|
| 65 |
+
```
|
codabench_portrait_train.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ead68273d71a50331d47af49f9b1194cfe2d4755abf1fed66d7b1999745878e2
|
| 3 |
+
size 82860499
|
codabench_portrait_train_balanced_v2.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d1c3bfe5b58b18736b429e8c452690e5930c636c50a39eebf23bf92d6f35c0e7
|
| 3 |
+
size 237557794
|
codabench_portrait_train_multitask_v2.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:02d06def9036381eda440c83e290bb9d73643d8e4d8beb9f975402f8f18fd478
|
| 3 |
+
size 96688216
|
codabench_portrait_train_score_v2.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f96f0445c5c5acd5a84fd2c6d029ca2317a52fc44185b4463dd0c845219f8d24
|
| 3 |
+
size 79931263
|
codabench_portrait_val.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
codabench_portrait_val_multitask_v2.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0b2556588585076c9be25afad59c0214c82cd3b2c77a51afe4768a09645fffd7
|
| 3 |
+
size 10742247
|
codabench_portrait_val_score_v2.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
convert_to_answer.py
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import copy
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
from collections import Counter
|
| 7 |
+
from glob import glob
|
| 8 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
DEFAULT_TEMPLATE_PATH = "outputs/predictions/track_1_test.json"
|
| 12 |
+
DEFAULT_PRED_PATH = "outputs/predictions/predict_ckpt2660/generated_predictions.jsonl"
|
| 13 |
+
DEFAULT_OUTPUT_PATH = "outputs/submissions/answers/track_1_test.json"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def load_json(path: str) -> Any:
|
| 17 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 18 |
+
return json.load(f)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def load_jsonl(path: str) -> List[Dict[str, Any]]:
|
| 22 |
+
rows: List[Dict[str, Any]] = []
|
| 23 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 24 |
+
for line_no, line in enumerate(f, start=1):
|
| 25 |
+
raw = line.strip()
|
| 26 |
+
if not raw:
|
| 27 |
+
continue
|
| 28 |
+
try:
|
| 29 |
+
rows.append(json.loads(raw))
|
| 30 |
+
except Exception:
|
| 31 |
+
print(f"[WARN] skip invalid jsonl line: {line_no}")
|
| 32 |
+
return rows
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def normalize_level(level: Any) -> Optional[str]:
|
| 36 |
+
if level is None:
|
| 37 |
+
return None
|
| 38 |
+
s = str(level).strip()
|
| 39 |
+
if not s:
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
m = {
|
| 43 |
+
"poor": "Poor",
|
| 44 |
+
"medium": "Medium",
|
| 45 |
+
"good": "Good",
|
| 46 |
+
"a": "A",
|
| 47 |
+
"b": "B",
|
| 48 |
+
"c": "C",
|
| 49 |
+
}
|
| 50 |
+
return m.get(s.lower(), s)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def score_to_submission_level(score: float, low_threshold: float = 5.0, high_threshold: float = 7.0) -> str:
|
| 54 |
+
# 比赛映射:A=Poor(0~5), B=Medium(5~7), C=Good(7~10)
|
| 55 |
+
if score < low_threshold:
|
| 56 |
+
return "A"
|
| 57 |
+
if score < high_threshold:
|
| 58 |
+
return "B"
|
| 59 |
+
return "C"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def to_submission_level(level: Any) -> Optional[str]:
|
| 63 |
+
"""
|
| 64 |
+
Submission mapping required by Track-1 template:
|
| 65 |
+
A -> Poor, B -> Medium, C -> Good
|
| 66 |
+
"""
|
| 67 |
+
norm = normalize_level(level)
|
| 68 |
+
if norm is None:
|
| 69 |
+
return None
|
| 70 |
+
m = {
|
| 71 |
+
"Poor": "A",
|
| 72 |
+
"Medium": "B",
|
| 73 |
+
"Good": "C",
|
| 74 |
+
"A": "A",
|
| 75 |
+
"B": "B",
|
| 76 |
+
"C": "C",
|
| 77 |
+
}
|
| 78 |
+
return m.get(norm)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def normalize_prediction_text(pred_row: Dict[str, Any]) -> str:
|
| 82 |
+
# 兼容不同推理后端字段名。
|
| 83 |
+
for k in ("predict", "response", "output", "text", "generation"):
|
| 84 |
+
if k in pred_row:
|
| 85 |
+
return str(pred_row.get(k, ""))
|
| 86 |
+
return str(pred_row)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def extract_level_from_text(pred_text: str, crit_key: str) -> Optional[str]:
|
| 90 |
+
pattern = rf'"{re.escape(crit_key)}"\s*:\s*\{{.*?"level"\s*:\s*"([^"]+)"'
|
| 91 |
+
m = re.search(pattern, pred_text, flags=re.IGNORECASE | re.DOTALL)
|
| 92 |
+
if not m:
|
| 93 |
+
return None
|
| 94 |
+
return to_submission_level(m.group(1))
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def try_parse_predict_json(text: str) -> Optional[Dict[str, Any]]:
|
| 98 |
+
text = str(text or "").strip()
|
| 99 |
+
if not text:
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
# 1) 直接解析
|
| 103 |
+
try:
|
| 104 |
+
obj = json.loads(text)
|
| 105 |
+
if isinstance(obj, dict):
|
| 106 |
+
return obj
|
| 107 |
+
except Exception:
|
| 108 |
+
pass
|
| 109 |
+
|
| 110 |
+
# 2) 尝试提取从首个 { 到最后一个 } 的片段
|
| 111 |
+
left = text.find("{")
|
| 112 |
+
right = text.rfind("}")
|
| 113 |
+
if left != -1 and right != -1 and left < right:
|
| 114 |
+
snippet = text[left:right + 1]
|
| 115 |
+
try:
|
| 116 |
+
obj = json.loads(snippet)
|
| 117 |
+
if isinstance(obj, dict):
|
| 118 |
+
return obj
|
| 119 |
+
except Exception:
|
| 120 |
+
pass
|
| 121 |
+
|
| 122 |
+
return None
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def extract_answer(pred_obj: Optional[Dict[str, Any]], pred_text: str) -> Optional[str]:
|
| 126 |
+
if pred_obj is not None:
|
| 127 |
+
answer = str(pred_obj.get("answer", "")).strip().upper()
|
| 128 |
+
if answer in {"A", "B", "C", "D"}:
|
| 129 |
+
return answer
|
| 130 |
+
|
| 131 |
+
m = re.search(r'"answer"\s*:\s*"([A-D])"', pred_text, re.IGNORECASE)
|
| 132 |
+
if m:
|
| 133 |
+
return m.group(1).upper()
|
| 134 |
+
|
| 135 |
+
# 兼容 QA-only 推理:模型可能只输出单个字母(如 "C")。
|
| 136 |
+
raw = str(pred_text or "").strip().upper()
|
| 137 |
+
if raw:
|
| 138 |
+
# 情况1:整行仅有一个候选字母(允许尾随标点)。
|
| 139 |
+
m = re.match(r"^\s*([A-D])(?:[\.\)\]::]|\s)*$", raw)
|
| 140 |
+
if m:
|
| 141 |
+
return m.group(1)
|
| 142 |
+
# 情况2:短文本中只出现唯一一个 A/B/C/D,且不包含常见 JSON/选项结构。
|
| 143 |
+
if len(raw) <= 12 and ("{" not in raw) and ("\"" not in raw):
|
| 144 |
+
hits = re.findall(r"[A-D]", raw)
|
| 145 |
+
if len(hits) == 1:
|
| 146 |
+
return hits[0]
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def extract_total_score(pred_obj: Optional[Dict[str, Any]], pred_text: str) -> Optional[float]:
|
| 151 |
+
val: Optional[float] = None
|
| 152 |
+
if pred_obj is not None and "total_score" in pred_obj:
|
| 153 |
+
try:
|
| 154 |
+
val = float(pred_obj["total_score"])
|
| 155 |
+
except Exception:
|
| 156 |
+
val = None
|
| 157 |
+
|
| 158 |
+
if val is None:
|
| 159 |
+
m = re.search(r'"total_score"\s*:\s*([0-9]+(?:\.[0-9]+)?)', pred_text)
|
| 160 |
+
if m:
|
| 161 |
+
val = float(m.group(1))
|
| 162 |
+
|
| 163 |
+
if val is None:
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
return max(0.0, min(100.0, float(val)))
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def merge_total_scores(total_votes: List[float], mode: str) -> Optional[int]:
|
| 170 |
+
if not total_votes:
|
| 171 |
+
return None
|
| 172 |
+
|
| 173 |
+
vals = [float(v) for v in total_votes]
|
| 174 |
+
if mode == "trim_mean" and len(vals) >= 3:
|
| 175 |
+
vals = sorted(vals)[1:-1]
|
| 176 |
+
merged = sum(vals) / len(vals)
|
| 177 |
+
return max(0, min(100, int(round(merged))))
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def parse_score_from_text(pred_text: str, crit_key: str) -> Optional[float]:
|
| 181 |
+
pattern = rf'"{re.escape(crit_key)}"\s*:\s*\{{.*?"score"\s*:\s*([0-9]+(?:\.[0-9]+)?)'
|
| 182 |
+
m = re.search(pattern, pred_text, flags=re.IGNORECASE | re.DOTALL)
|
| 183 |
+
if not m:
|
| 184 |
+
return None
|
| 185 |
+
try:
|
| 186 |
+
return float(m.group(1))
|
| 187 |
+
except Exception:
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def extract_one_criteria_level_and_score(
|
| 192 |
+
crit_key: str,
|
| 193 |
+
pred_obj: Optional[Dict[str, Any]],
|
| 194 |
+
pred_text: str,
|
| 195 |
+
) -> Tuple[Optional[str], Optional[float]]:
|
| 196 |
+
score: Optional[float] = None
|
| 197 |
+
level: Optional[str] = None
|
| 198 |
+
|
| 199 |
+
if isinstance(pred_obj, dict):
|
| 200 |
+
src = pred_obj.get("criteria", {})
|
| 201 |
+
if isinstance(src, dict) and crit_key in src:
|
| 202 |
+
value = src.get(crit_key)
|
| 203 |
+
if isinstance(value, dict):
|
| 204 |
+
if "score" in value:
|
| 205 |
+
try:
|
| 206 |
+
score = float(value["score"])
|
| 207 |
+
except Exception:
|
| 208 |
+
score = None
|
| 209 |
+
level = to_submission_level(value.get("level"))
|
| 210 |
+
else:
|
| 211 |
+
level = to_submission_level(value)
|
| 212 |
+
|
| 213 |
+
if score is None:
|
| 214 |
+
score = parse_score_from_text(pred_text, crit_key)
|
| 215 |
+
|
| 216 |
+
if level is None:
|
| 217 |
+
level = extract_level_from_text(pred_text, crit_key)
|
| 218 |
+
|
| 219 |
+
if level not in {"A", "B", "C"}:
|
| 220 |
+
level = None
|
| 221 |
+
|
| 222 |
+
return level, score
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def choose_majority_level(
|
| 226 |
+
levels: List[str],
|
| 227 |
+
scores: List[float],
|
| 228 |
+
fallback_level: Optional[str],
|
| 229 |
+
low_threshold: float,
|
| 230 |
+
high_threshold: float,
|
| 231 |
+
) -> str:
|
| 232 |
+
if levels:
|
| 233 |
+
cnt = Counter(levels)
|
| 234 |
+
top_n = max(cnt.values())
|
| 235 |
+
top_levels = sorted([k for k, v in cnt.items() if v == top_n])
|
| 236 |
+
if len(top_levels) == 1:
|
| 237 |
+
return top_levels[0]
|
| 238 |
+
|
| 239 |
+
# 平票时,用多次预测的均值 score 判定等级。
|
| 240 |
+
if scores:
|
| 241 |
+
mean_score = sum(scores) / len(scores)
|
| 242 |
+
return score_to_submission_level(mean_score, low_threshold=low_threshold, high_threshold=high_threshold)
|
| 243 |
+
|
| 244 |
+
if fallback_level in {"A", "B", "C"}:
|
| 245 |
+
return fallback_level
|
| 246 |
+
return "B"
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def parse_weights(raw_weights: Optional[List[float]], n_models: int) -> List[float]:
|
| 250 |
+
# 默认等权;若传入权重则要求和预测文件数一致。
|
| 251 |
+
if raw_weights is None:
|
| 252 |
+
return [1.0] * n_models
|
| 253 |
+
if len(raw_weights) != n_models:
|
| 254 |
+
raise ValueError(
|
| 255 |
+
f"--weights length ({len(raw_weights)}) must equal number of prediction files ({n_models})."
|
| 256 |
+
)
|
| 257 |
+
for w in raw_weights:
|
| 258 |
+
if w < 0:
|
| 259 |
+
raise ValueError("weights must be non-negative.")
|
| 260 |
+
# 全零没有意义,回退等权。
|
| 261 |
+
if sum(raw_weights) == 0:
|
| 262 |
+
return [1.0] * n_models
|
| 263 |
+
return raw_weights
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def load_thresholds(
|
| 267 |
+
thresholds_json: str,
|
| 268 |
+
) -> Tuple[Dict[str, Dict[str, float]], Dict[str, float]]:
|
| 269 |
+
"""
|
| 270 |
+
Accepts JSON in either format:
|
| 271 |
+
1) {"criteria": {"Color Harmony": {"low": 4.9, "high": 7.1}}, "default": {"low":5,"high":7}}
|
| 272 |
+
2) {"Color Harmony": {"low": 4.9, "high": 7.1}, ...}
|
| 273 |
+
"""
|
| 274 |
+
default = {"low": 5.0, "high": 7.0}
|
| 275 |
+
per_criteria: Dict[str, Dict[str, float]] = {}
|
| 276 |
+
|
| 277 |
+
if not thresholds_json:
|
| 278 |
+
return per_criteria, default
|
| 279 |
+
if not os.path.exists(thresholds_json):
|
| 280 |
+
print(f"[WARN] thresholds file not found: {thresholds_json}, fallback to default 5/7")
|
| 281 |
+
return per_criteria, default
|
| 282 |
+
|
| 283 |
+
obj = load_json(thresholds_json)
|
| 284 |
+
if not isinstance(obj, dict):
|
| 285 |
+
print(f"[WARN] invalid thresholds json format: {thresholds_json}, fallback to default 5/7")
|
| 286 |
+
return per_criteria, default
|
| 287 |
+
|
| 288 |
+
if "default" in obj and isinstance(obj.get("default"), dict):
|
| 289 |
+
d = obj["default"]
|
| 290 |
+
low = d.get("low", 5.0)
|
| 291 |
+
high = d.get("high", 7.0)
|
| 292 |
+
try:
|
| 293 |
+
low_f = float(low)
|
| 294 |
+
high_f = float(high)
|
| 295 |
+
if low_f < high_f:
|
| 296 |
+
default = {"low": low_f, "high": high_f}
|
| 297 |
+
except Exception:
|
| 298 |
+
pass
|
| 299 |
+
|
| 300 |
+
src = obj.get("criteria") if isinstance(obj.get("criteria"), dict) else obj
|
| 301 |
+
if isinstance(src, dict):
|
| 302 |
+
for k, v in src.items():
|
| 303 |
+
if not isinstance(v, dict):
|
| 304 |
+
continue
|
| 305 |
+
if "low" not in v or "high" not in v:
|
| 306 |
+
continue
|
| 307 |
+
try:
|
| 308 |
+
low = float(v["low"])
|
| 309 |
+
high = float(v["high"])
|
| 310 |
+
except Exception:
|
| 311 |
+
continue
|
| 312 |
+
if low < high:
|
| 313 |
+
per_criteria[str(k)] = {"low": low, "high": high}
|
| 314 |
+
|
| 315 |
+
return per_criteria, default
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def resolve_best_index(best_index: int, weights: List[float]) -> int:
|
| 319 |
+
# best_index=-1 表示自动选择权重最高的模型作为平票时的优先模型。
|
| 320 |
+
if best_index >= 0:
|
| 321 |
+
if best_index >= len(weights):
|
| 322 |
+
raise ValueError(f"--best_index out of range: {best_index}, num_models={len(weights)}")
|
| 323 |
+
return best_index
|
| 324 |
+
return max(range(len(weights)), key=lambda i: weights[i])
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def extract_criteria_voting(
|
| 328 |
+
template_item: Dict[str, Any],
|
| 329 |
+
pred_objs: List[Optional[Dict[str, Any]]],
|
| 330 |
+
pred_texts: List[str],
|
| 331 |
+
per_criteria_thresholds: Dict[str, Dict[str, float]],
|
| 332 |
+
default_thresholds: Dict[str, float],
|
| 333 |
+
) -> Dict[str, Dict[str, str]]:
|
| 334 |
+
out: Dict[str, Dict[str, str]] = {}
|
| 335 |
+
|
| 336 |
+
for crit_key, crit_val in template_item.get("criteria", {}).items():
|
| 337 |
+
thresholds = per_criteria_thresholds.get(crit_key, default_thresholds)
|
| 338 |
+
low = float(thresholds.get("low", 5.0))
|
| 339 |
+
high = float(thresholds.get("high", 7.0))
|
| 340 |
+
if not (low < high):
|
| 341 |
+
low, high = 5.0, 7.0
|
| 342 |
+
|
| 343 |
+
level_votes: List[str] = []
|
| 344 |
+
score_votes: List[float] = []
|
| 345 |
+
for obj, text in zip(pred_objs, pred_texts):
|
| 346 |
+
level, score = extract_one_criteria_level_and_score(crit_key, obj, text)
|
| 347 |
+
if level is not None:
|
| 348 |
+
level_votes.append(level)
|
| 349 |
+
if score is not None:
|
| 350 |
+
score_votes.append(score)
|
| 351 |
+
|
| 352 |
+
prev = str(crit_val.get("level", "")).strip() if isinstance(crit_val, dict) else ""
|
| 353 |
+
final_level = choose_majority_level(
|
| 354 |
+
level_votes,
|
| 355 |
+
score_votes,
|
| 356 |
+
prev,
|
| 357 |
+
low_threshold=low,
|
| 358 |
+
high_threshold=high,
|
| 359 |
+
)
|
| 360 |
+
out[crit_key] = {"level": final_level}
|
| 361 |
+
|
| 362 |
+
return out
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
def pick_default_predictions_path() -> str:
|
| 366 |
+
candidates = [
|
| 367 |
+
"outputs/predictions/predict_ckpt2660/generated_predictions.jsonl",
|
| 368 |
+
"outputs/predictions/generated_predictions.jsonl",
|
| 369 |
+
]
|
| 370 |
+
# 自动兜底:在预测目录里找最近一次 generated_predictions.jsonl。
|
| 371 |
+
dynamic = sorted(
|
| 372 |
+
glob("outputs/predictions/**/generated_predictions.jsonl", recursive=True),
|
| 373 |
+
key=lambda x: os.path.getmtime(x),
|
| 374 |
+
reverse=True,
|
| 375 |
+
)
|
| 376 |
+
candidates = dynamic + candidates
|
| 377 |
+
|
| 378 |
+
for p in candidates:
|
| 379 |
+
if os.path.exists(p):
|
| 380 |
+
return p
|
| 381 |
+
return DEFAULT_PRED_PATH
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def choose_weighted_answer(
|
| 385 |
+
votes_by_model: List[Optional[str]],
|
| 386 |
+
weights: List[float],
|
| 387 |
+
best_index: int,
|
| 388 |
+
tie_break_answer: Optional[str],
|
| 389 |
+
) -> str:
|
| 390 |
+
"""
|
| 391 |
+
更合理的答案融合策略:
|
| 392 |
+
1) 加权投票(按各变体可靠性权重)
|
| 393 |
+
2) 若平票且提供 tie-break 结果,则优先用 tie-break
|
| 394 |
+
3) 若仍平票,采用最佳变体(best_index)在平票选项中的答案
|
| 395 |
+
4) 最后才做稳定兜底(字母序)
|
| 396 |
+
"""
|
| 397 |
+
label_scores = {"A": 0.0, "B": 0.0, "C": 0.0, "D": 0.0}
|
| 398 |
+
for i, ans in enumerate(votes_by_model):
|
| 399 |
+
if ans in label_scores:
|
| 400 |
+
label_scores[ans] += weights[i]
|
| 401 |
+
|
| 402 |
+
max_score = max(label_scores.values())
|
| 403 |
+
if max_score <= 0:
|
| 404 |
+
return "A"
|
| 405 |
+
|
| 406 |
+
tied = sorted([k for k, v in label_scores.items() if v == max_score])
|
| 407 |
+
if len(tied) == 1:
|
| 408 |
+
return tied[0]
|
| 409 |
+
|
| 410 |
+
if tie_break_answer in tied:
|
| 411 |
+
return tie_break_answer # 用专门 tie-break 结果判平票
|
| 412 |
+
|
| 413 |
+
best_vote = votes_by_model[best_index]
|
| 414 |
+
if best_vote in tied:
|
| 415 |
+
return str(best_vote)
|
| 416 |
+
|
| 417 |
+
return tied[0]
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def main() -> None:
|
| 421 |
+
parser = argparse.ArgumentParser()
|
| 422 |
+
parser.add_argument("--template_json", type=str, default=DEFAULT_TEMPLATE_PATH)
|
| 423 |
+
parser.add_argument(
|
| 424 |
+
"--predictions_jsonl",
|
| 425 |
+
type=str,
|
| 426 |
+
nargs="+",
|
| 427 |
+
default=None,
|
| 428 |
+
help="One or more generated_predictions.jsonl paths for voting.",
|
| 429 |
+
)
|
| 430 |
+
parser.add_argument(
|
| 431 |
+
"--weights",
|
| 432 |
+
type=float,
|
| 433 |
+
nargs="+",
|
| 434 |
+
default=None,
|
| 435 |
+
help="Optional weights for prediction files (same length as --predictions_jsonl).",
|
| 436 |
+
)
|
| 437 |
+
parser.add_argument(
|
| 438 |
+
"--best_index",
|
| 439 |
+
type=int,
|
| 440 |
+
default=-1,
|
| 441 |
+
help="Best model index for tie fallback. -1 means auto argmax(weights).",
|
| 442 |
+
)
|
| 443 |
+
parser.add_argument(
|
| 444 |
+
"--tie_break_jsonl",
|
| 445 |
+
type=str,
|
| 446 |
+
default="",
|
| 447 |
+
help="Optional tie-break predictions file used only when weighted vote ties.",
|
| 448 |
+
)
|
| 449 |
+
parser.add_argument(
|
| 450 |
+
"--thresholds_json",
|
| 451 |
+
type=str,
|
| 452 |
+
default="",
|
| 453 |
+
help="Optional per-criterion thresholds json for score->A/B/C mapping.",
|
| 454 |
+
)
|
| 455 |
+
parser.add_argument(
|
| 456 |
+
"--total_score_fusion",
|
| 457 |
+
type=str,
|
| 458 |
+
default="mean",
|
| 459 |
+
choices=["mean", "trim_mean"],
|
| 460 |
+
help="Fusion mode for total_score across multi-prompt predictions.",
|
| 461 |
+
)
|
| 462 |
+
parser.add_argument("--output_json", type=str, default=DEFAULT_OUTPUT_PATH)
|
| 463 |
+
args = parser.parse_args()
|
| 464 |
+
|
| 465 |
+
pred_paths = args.predictions_jsonl if args.predictions_jsonl else [pick_default_predictions_path()]
|
| 466 |
+
pred_paths = [p for p in pred_paths if str(p).strip()]
|
| 467 |
+
if not pred_paths:
|
| 468 |
+
raise ValueError("No predictions_jsonl provided or discovered.")
|
| 469 |
+
|
| 470 |
+
template_data = load_json(args.template_json)
|
| 471 |
+
pred_sets = [load_jsonl(p) for p in pred_paths]
|
| 472 |
+
weights = parse_weights(args.weights, len(pred_sets))
|
| 473 |
+
best_index = resolve_best_index(args.best_index, weights)
|
| 474 |
+
per_criteria_thresholds, default_thresholds = load_thresholds(args.thresholds_json)
|
| 475 |
+
tie_break_rows: List[Dict[str, Any]] = []
|
| 476 |
+
if args.tie_break_jsonl:
|
| 477 |
+
tie_break_rows = load_jsonl(args.tie_break_jsonl)
|
| 478 |
+
|
| 479 |
+
if not isinstance(template_data, list):
|
| 480 |
+
raise ValueError("template_json must be a list.")
|
| 481 |
+
|
| 482 |
+
print(f"[INFO] template items: {len(template_data)}")
|
| 483 |
+
for p, rows in zip(pred_paths, pred_sets):
|
| 484 |
+
print(f"[INFO] prediction rows: {len(rows)} ({p})")
|
| 485 |
+
print(f"[INFO] answer weights: {weights}")
|
| 486 |
+
print(f"[INFO] answer best_index: {best_index}")
|
| 487 |
+
print(f"[INFO] criteria thresholds default: low={default_thresholds['low']}, high={default_thresholds['high']}")
|
| 488 |
+
if per_criteria_thresholds:
|
| 489 |
+
print(f"[INFO] criteria thresholds loaded: {len(per_criteria_thresholds)}")
|
| 490 |
+
if args.tie_break_jsonl:
|
| 491 |
+
print(f"[INFO] tie_break rows: {len(tie_break_rows)} ({args.tie_break_jsonl})")
|
| 492 |
+
|
| 493 |
+
final_submission: List[Dict[str, Any]] = []
|
| 494 |
+
parsed_ok = 0
|
| 495 |
+
filled = 0
|
| 496 |
+
|
| 497 |
+
for i, item in enumerate(template_data):
|
| 498 |
+
new_item = copy.deepcopy(item)
|
| 499 |
+
# 收集每次预测在第 i 条样本上的结果。
|
| 500 |
+
row_texts: List[str] = []
|
| 501 |
+
row_objs: List[Optional[Dict[str, Any]]] = []
|
| 502 |
+
answer_votes_by_model: List[Optional[str]] = []
|
| 503 |
+
total_votes: List[int] = []
|
| 504 |
+
|
| 505 |
+
for rows in pred_sets:
|
| 506 |
+
if i >= len(rows):
|
| 507 |
+
answer_votes_by_model.append(None)
|
| 508 |
+
continue
|
| 509 |
+
pred_text = normalize_prediction_text(rows[i])
|
| 510 |
+
pred_obj = try_parse_predict_json(pred_text)
|
| 511 |
+
if pred_obj is not None:
|
| 512 |
+
parsed_ok += 1
|
| 513 |
+
row_texts.append(pred_text)
|
| 514 |
+
row_objs.append(pred_obj)
|
| 515 |
+
|
| 516 |
+
ans = extract_answer(pred_obj, pred_text)
|
| 517 |
+
answer_votes_by_model.append(ans)
|
| 518 |
+
|
| 519 |
+
ts = extract_total_score(pred_obj, pred_text)
|
| 520 |
+
if ts is not None:
|
| 521 |
+
total_votes.append(ts)
|
| 522 |
+
|
| 523 |
+
if not row_texts:
|
| 524 |
+
final_submission.append(new_item)
|
| 525 |
+
continue
|
| 526 |
+
|
| 527 |
+
new_item["criteria"] = extract_criteria_voting(
|
| 528 |
+
new_item,
|
| 529 |
+
row_objs,
|
| 530 |
+
row_texts,
|
| 531 |
+
per_criteria_thresholds=per_criteria_thresholds,
|
| 532 |
+
default_thresholds=default_thresholds,
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
# total_score 用多次预测均值,减少单次抖动。
|
| 536 |
+
merged_total = merge_total_scores(total_votes, args.total_score_fusion)
|
| 537 |
+
if merged_total is not None:
|
| 538 |
+
new_item["total_score"] = max(0, min(100, merged_total))
|
| 539 |
+
|
| 540 |
+
tie_break_answer: Optional[str] = None
|
| 541 |
+
if i < len(tie_break_rows):
|
| 542 |
+
tb_text = normalize_prediction_text(tie_break_rows[i])
|
| 543 |
+
tb_obj = try_parse_predict_json(tb_text)
|
| 544 |
+
tie_break_answer = extract_answer(tb_obj, tb_text)
|
| 545 |
+
|
| 546 |
+
new_item["answer"] = choose_weighted_answer(
|
| 547 |
+
votes_by_model=answer_votes_by_model,
|
| 548 |
+
weights=weights,
|
| 549 |
+
best_index=best_index,
|
| 550 |
+
tie_break_answer=tie_break_answer,
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
final_submission.append(new_item)
|
| 554 |
+
filled += 1
|
| 555 |
+
|
| 556 |
+
os.makedirs(os.path.dirname(args.output_json), exist_ok=True)
|
| 557 |
+
with open(args.output_json, "w", encoding="utf-8") as f:
|
| 558 |
+
json.dump(final_submission, f, ensure_ascii=False, indent=2)
|
| 559 |
+
|
| 560 |
+
parsed_total = filled * len(pred_paths)
|
| 561 |
+
print(f"[INFO] parsed predict json ok: {parsed_ok}/{parsed_total}")
|
| 562 |
+
print(f"[INFO] saved submission: {args.output_json}")
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
if __name__ == "__main__":
|
| 566 |
+
main()
|
portrait_composition_workflow_vector.svg
ADDED
|
|
prepare_dataset.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import os
|
| 8 |
+
import random
|
| 9 |
+
from collections import Counter, defaultdict
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from typing import Any, Dict, List, Tuple
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
CRITERIA = [
|
| 15 |
+
"Color Harmony",
|
| 16 |
+
"Visual Style Consistency",
|
| 17 |
+
"Sharpness",
|
| 18 |
+
"Light and Shadow Modeling",
|
| 19 |
+
"Creativity and Originality",
|
| 20 |
+
"Exposure Control",
|
| 21 |
+
"Application of Classical Composition Principles",
|
| 22 |
+
"Depth of Field and Layering",
|
| 23 |
+
"Visual Center Stability",
|
| 24 |
+
"Visual Flow Guidance",
|
| 25 |
+
"Structural Support Stability",
|
| 26 |
+
"Appropriateness of Negative Space",
|
| 27 |
+
"Subject Integrity",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
PROMPT_SCORE = (
|
| 32 |
+
"You are an aesthetics expert. Evaluate the image on the following 13 criteria:\n"
|
| 33 |
+
"Color Harmony, Visual Style Consistency, Sharpness, Light and Shadow Modeling, "
|
| 34 |
+
"Creativity and Originality, Exposure Control, Application of Classical Composition Principles, "
|
| 35 |
+
"Depth of Field and Layering, Visual Center Stability, Visual Flow Guidance, "
|
| 36 |
+
"Structural Support Stability, Appropriateness of Negative Space, Subject Integrity.\n\n"
|
| 37 |
+
"For each criterion, output a numeric score in [0.0, 10.0].\n"
|
| 38 |
+
"Then output total_score as an integer in [0, 100].\n"
|
| 39 |
+
"Return STRICT JSON only.\n"
|
| 40 |
+
"JSON format:\n"
|
| 41 |
+
"{\n"
|
| 42 |
+
" \"criteria\": {\n"
|
| 43 |
+
" \"Color Harmony\": {\"score\": 0.0}\n"
|
| 44 |
+
" },\n"
|
| 45 |
+
" \"total_score\": 0\n"
|
| 46 |
+
"}\n\n"
|
| 47 |
+
"<image>"
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
PROMPT_MULTITASK = (
|
| 52 |
+
"You are an aesthetics expert. Evaluate the image on the following 13 criteria:\n"
|
| 53 |
+
"Color Harmony, Visual Style Consistency, Sharpness, Light and Shadow Modeling, "
|
| 54 |
+
"Creativity and Originality, Exposure Control, Application of Classical Composition Principles, "
|
| 55 |
+
"Depth of Field and Layering, Visual Center Stability, Visual Flow Guidance, "
|
| 56 |
+
"Structural Support Stability, Appropriateness of Negative Space, Subject Integrity.\n\n"
|
| 57 |
+
"For each criterion, output:\n"
|
| 58 |
+
"1) score in [0.0, 10.0]\n"
|
| 59 |
+
"2) level in {A,B,C} where A:<5, B:[5,7), C:>=7\n"
|
| 60 |
+
"Then output total_score as an integer in [0, 100].\n"
|
| 61 |
+
"Return STRICT JSON only.\n"
|
| 62 |
+
"JSON format:\n"
|
| 63 |
+
"{\n"
|
| 64 |
+
" \"criteria\": {\n"
|
| 65 |
+
" \"Color Harmony\": {\"score\": 0.0, \"level\": \"A|B|C\"}\n"
|
| 66 |
+
" },\n"
|
| 67 |
+
" \"total_score\": 0\n"
|
| 68 |
+
"}\n\n"
|
| 69 |
+
"<image>"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@dataclass
|
| 74 |
+
class Sample:
|
| 75 |
+
image_path: str
|
| 76 |
+
scores: Dict[str, float]
|
| 77 |
+
levels: Dict[str, str]
|
| 78 |
+
total_score: int
|
| 79 |
+
boundary_count: int
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def clamp_score(x: float) -> float:
|
| 83 |
+
return max(0.0, min(10.0, x))
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def clamp_int(x: int, lo: int, hi: int) -> int:
|
| 87 |
+
return max(lo, min(hi, x))
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def score_to_level(score: float) -> str:
|
| 91 |
+
if score < 5.0:
|
| 92 |
+
return "A"
|
| 93 |
+
if score < 7.0:
|
| 94 |
+
return "B"
|
| 95 |
+
return "C"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def safe_float(x: Any, default: float = 0.0) -> float:
|
| 99 |
+
try:
|
| 100 |
+
return float(x)
|
| 101 |
+
except Exception:
|
| 102 |
+
return default
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def safe_int(x: Any, default: int = 0) -> int:
|
| 106 |
+
try:
|
| 107 |
+
return int(round(float(x)))
|
| 108 |
+
except Exception:
|
| 109 |
+
return default
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def parse_assistant_payload(payload: str) -> Dict[str, Any]:
|
| 113 |
+
obj = json.loads(payload)
|
| 114 |
+
if not isinstance(obj, dict):
|
| 115 |
+
raise ValueError("assistant payload is not dict")
|
| 116 |
+
return obj
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def normalize_record(rec: Dict[str, Any], boundary_margin: float) -> Sample:
|
| 120 |
+
assistant = parse_assistant_payload(rec["messages"][1]["content"])
|
| 121 |
+
criteria = assistant.get("criteria", {})
|
| 122 |
+
|
| 123 |
+
scores: Dict[str, float] = {}
|
| 124 |
+
levels: Dict[str, str] = {}
|
| 125 |
+
boundary_count = 0
|
| 126 |
+
|
| 127 |
+
for c in CRITERIA:
|
| 128 |
+
v = (criteria.get(c, {}) or {}).get("score", 0.0)
|
| 129 |
+
s = round(clamp_score(safe_float(v, 0.0)), 1)
|
| 130 |
+
lv = score_to_level(s)
|
| 131 |
+
scores[c] = s
|
| 132 |
+
levels[c] = lv
|
| 133 |
+
if abs(s - 5.0) <= boundary_margin or abs(s - 7.0) <= boundary_margin:
|
| 134 |
+
boundary_count += 1
|
| 135 |
+
|
| 136 |
+
t = assistant.get("total_score", 0)
|
| 137 |
+
total_score = clamp_int(safe_int(t, 0), 0, 100)
|
| 138 |
+
|
| 139 |
+
img = rec.get("images", [""])
|
| 140 |
+
image_path = str(img[0]) if isinstance(img, list) and img else ""
|
| 141 |
+
|
| 142 |
+
return Sample(
|
| 143 |
+
image_path=image_path,
|
| 144 |
+
scores=scores,
|
| 145 |
+
levels=levels,
|
| 146 |
+
total_score=total_score,
|
| 147 |
+
boundary_count=boundary_count,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def build_score_record(s: Sample) -> Dict[str, Any]:
|
| 152 |
+
assistant = {
|
| 153 |
+
"criteria": {k: {"score": round(v, 1)} for k, v in s.scores.items()},
|
| 154 |
+
"total_score": int(s.total_score),
|
| 155 |
+
}
|
| 156 |
+
return {
|
| 157 |
+
"messages": [
|
| 158 |
+
{"role": "user", "content": PROMPT_SCORE},
|
| 159 |
+
{"role": "assistant", "content": json.dumps(assistant, ensure_ascii=False, indent=2)},
|
| 160 |
+
],
|
| 161 |
+
"images": [s.image_path],
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def build_multitask_record(s: Sample) -> Dict[str, Any]:
|
| 166 |
+
assistant = {
|
| 167 |
+
"criteria": {
|
| 168 |
+
k: {"score": round(s.scores[k], 1), "level": s.levels[k]} for k in CRITERIA
|
| 169 |
+
},
|
| 170 |
+
"total_score": int(s.total_score),
|
| 171 |
+
}
|
| 172 |
+
return {
|
| 173 |
+
"messages": [
|
| 174 |
+
{"role": "user", "content": PROMPT_MULTITASK},
|
| 175 |
+
{"role": "assistant", "content": json.dumps(assistant, ensure_ascii=False, indent=2)},
|
| 176 |
+
],
|
| 177 |
+
"images": [s.image_path],
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def compute_level_counts(samples: List[Sample]) -> Dict[str, Counter]:
|
| 182 |
+
counts: Dict[str, Counter] = {c: Counter() for c in CRITERIA}
|
| 183 |
+
for s in samples:
|
| 184 |
+
for c in CRITERIA:
|
| 185 |
+
counts[c][s.levels[c]] += 1
|
| 186 |
+
return counts
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def compute_sample_dup(
|
| 190 |
+
s: Sample,
|
| 191 |
+
level_counts: Dict[str, Counter],
|
| 192 |
+
max_class_weight: float,
|
| 193 |
+
boundary_bonus: float,
|
| 194 |
+
max_dup: int,
|
| 195 |
+
) -> int:
|
| 196 |
+
weights: List[float] = []
|
| 197 |
+
for c in CRITERIA:
|
| 198 |
+
cnt = level_counts[c]
|
| 199 |
+
max_freq = max(cnt.values()) if cnt else 1
|
| 200 |
+
cur = cnt.get(s.levels[c], 1)
|
| 201 |
+
w = math.sqrt(float(max_freq) / float(max(1, cur)))
|
| 202 |
+
w = min(max_class_weight, max(1.0, w))
|
| 203 |
+
weights.append(w)
|
| 204 |
+
|
| 205 |
+
avg_w = sum(weights) / len(weights)
|
| 206 |
+
raw_dup = avg_w + boundary_bonus * float(s.boundary_count)
|
| 207 |
+
dup = int(round(raw_dup))
|
| 208 |
+
return clamp_int(dup, 1, max_dup)
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def save_json(path: str, obj: Any) -> None:
|
| 212 |
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 213 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 214 |
+
json.dump(obj, f, ensure_ascii=False, indent=2)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def build_stats(
|
| 218 |
+
train_samples: List[Sample],
|
| 219 |
+
val_samples: List[Sample],
|
| 220 |
+
level_counts: Dict[str, Counter],
|
| 221 |
+
dup_counts: Counter,
|
| 222 |
+
balanced_size: int,
|
| 223 |
+
) -> Dict[str, Any]:
|
| 224 |
+
agg = Counter()
|
| 225 |
+
for c in CRITERIA:
|
| 226 |
+
agg.update(level_counts[c])
|
| 227 |
+
|
| 228 |
+
total_lv = sum(agg.values())
|
| 229 |
+
agg_ratio = {k: round(v / total_lv, 6) for k, v in agg.items()} if total_lv else {}
|
| 230 |
+
|
| 231 |
+
per_criterion = {}
|
| 232 |
+
for c in CRITERIA:
|
| 233 |
+
cnt = level_counts[c]
|
| 234 |
+
n = sum(cnt.values())
|
| 235 |
+
per_criterion[c] = {
|
| 236 |
+
"counts": dict(cnt),
|
| 237 |
+
"ratio": {k: round(cnt[k] / n, 6) if n else 0.0 for k in ["A", "B", "C"]},
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
train_total = [s.total_score for s in train_samples]
|
| 241 |
+
val_total = [s.total_score for s in val_samples]
|
| 242 |
+
|
| 243 |
+
return {
|
| 244 |
+
"train_count": len(train_samples),
|
| 245 |
+
"val_count": len(val_samples),
|
| 246 |
+
"balanced_train_count": balanced_size,
|
| 247 |
+
"level_distribution_aggregate": dict(agg),
|
| 248 |
+
"level_distribution_aggregate_ratio": agg_ratio,
|
| 249 |
+
"level_distribution_per_criterion": per_criterion,
|
| 250 |
+
"duplication_histogram": {str(k): v for k, v in sorted(dup_counts.items())},
|
| 251 |
+
"boundary_stats": {
|
| 252 |
+
"train_mean_boundary_count": round(sum(s.boundary_count for s in train_samples) / max(1, len(train_samples)), 4),
|
| 253 |
+
"train_max_boundary_count": max((s.boundary_count for s in train_samples), default=0),
|
| 254 |
+
},
|
| 255 |
+
"total_score": {
|
| 256 |
+
"train_mean": round(sum(train_total) / max(1, len(train_total)), 4),
|
| 257 |
+
"val_mean": round(sum(val_total) / max(1, len(val_total)), 4),
|
| 258 |
+
"train_min": min(train_total) if train_total else None,
|
| 259 |
+
"train_max": max(train_total) if train_total else None,
|
| 260 |
+
"val_min": min(val_total) if val_total else None,
|
| 261 |
+
"val_max": max(val_total) if val_total else None,
|
| 262 |
+
},
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def main() -> None:
|
| 267 |
+
parser = argparse.ArgumentParser()
|
| 268 |
+
parser.add_argument(
|
| 269 |
+
"--train_json",
|
| 270 |
+
type=str,
|
| 271 |
+
default="./codabench_portrait_train.json",
|
| 272 |
+
)
|
| 273 |
+
parser.add_argument(
|
| 274 |
+
"--val_json",
|
| 275 |
+
type=str,
|
| 276 |
+
default="./codabench_portrait_val.json",
|
| 277 |
+
)
|
| 278 |
+
parser.add_argument("--output_dir", type=str, default=".")
|
| 279 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 280 |
+
parser.add_argument("--boundary_margin", type=float, default=0.4)
|
| 281 |
+
parser.add_argument("--boundary_bonus", type=float, default=0.3)
|
| 282 |
+
parser.add_argument("--max_class_weight", type=float, default=4.0)
|
| 283 |
+
parser.add_argument("--max_dup", type=int, default=5)
|
| 284 |
+
parser.add_argument(
|
| 285 |
+
"--stats_json",
|
| 286 |
+
type=str,
|
| 287 |
+
default="logs/prepare_dataset_v2_stats.json",
|
| 288 |
+
)
|
| 289 |
+
args = parser.parse_args()
|
| 290 |
+
|
| 291 |
+
random.seed(args.seed)
|
| 292 |
+
|
| 293 |
+
with open(args.train_json, "r", encoding="utf-8") as f:
|
| 294 |
+
train_raw = json.load(f)
|
| 295 |
+
with open(args.val_json, "r", encoding="utf-8") as f:
|
| 296 |
+
val_raw = json.load(f)
|
| 297 |
+
|
| 298 |
+
train_samples = [normalize_record(x, args.boundary_margin) for x in train_raw]
|
| 299 |
+
val_samples = [normalize_record(x, args.boundary_margin) for x in val_raw]
|
| 300 |
+
|
| 301 |
+
level_counts = compute_level_counts(train_samples)
|
| 302 |
+
|
| 303 |
+
train_score = [build_score_record(s) for s in train_samples]
|
| 304 |
+
train_multitask = [build_multitask_record(s) for s in train_samples]
|
| 305 |
+
val_score = [build_score_record(s) for s in val_samples]
|
| 306 |
+
val_multitask = [build_multitask_record(s) for s in val_samples]
|
| 307 |
+
|
| 308 |
+
train_balanced: List[Dict[str, Any]] = []
|
| 309 |
+
dup_hist = Counter()
|
| 310 |
+
for s in train_samples:
|
| 311 |
+
dup = compute_sample_dup(
|
| 312 |
+
s=s,
|
| 313 |
+
level_counts=level_counts,
|
| 314 |
+
max_class_weight=args.max_class_weight,
|
| 315 |
+
boundary_bonus=args.boundary_bonus,
|
| 316 |
+
max_dup=args.max_dup,
|
| 317 |
+
)
|
| 318 |
+
dup_hist[dup] += 1
|
| 319 |
+
rec = build_multitask_record(s)
|
| 320 |
+
for _ in range(dup):
|
| 321 |
+
train_balanced.append(rec)
|
| 322 |
+
|
| 323 |
+
random.shuffle(train_balanced)
|
| 324 |
+
|
| 325 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 326 |
+
|
| 327 |
+
f_train_score = os.path.join(args.output_dir, "codabench_portrait_train_score_v2.json")
|
| 328 |
+
f_train_multitask = os.path.join(args.output_dir, "codabench_portrait_train_multitask_v2.json")
|
| 329 |
+
f_train_balanced = os.path.join(args.output_dir, "codabench_portrait_train_balanced_v2.json")
|
| 330 |
+
f_val_score = os.path.join(args.output_dir, "codabench_portrait_val_score_v2.json")
|
| 331 |
+
f_val_multitask = os.path.join(args.output_dir, "codabench_portrait_val_multitask_v2.json")
|
| 332 |
+
|
| 333 |
+
save_json(f_train_score, train_score)
|
| 334 |
+
save_json(f_train_multitask, train_multitask)
|
| 335 |
+
save_json(f_train_balanced, train_balanced)
|
| 336 |
+
save_json(f_val_score, val_score)
|
| 337 |
+
save_json(f_val_multitask, val_multitask)
|
| 338 |
+
|
| 339 |
+
stats = build_stats(
|
| 340 |
+
train_samples=train_samples,
|
| 341 |
+
val_samples=val_samples,
|
| 342 |
+
level_counts=level_counts,
|
| 343 |
+
dup_counts=dup_hist,
|
| 344 |
+
balanced_size=len(train_balanced),
|
| 345 |
+
)
|
| 346 |
+
save_json(args.stats_json, stats)
|
| 347 |
+
|
| 348 |
+
print("=" * 60)
|
| 349 |
+
print("saved:", f_train_score)
|
| 350 |
+
print("saved:", f_train_multitask)
|
| 351 |
+
print("saved:", f_train_balanced)
|
| 352 |
+
print("saved:", f_val_score)
|
| 353 |
+
print("saved:", f_val_multitask)
|
| 354 |
+
print("stats:", args.stats_json)
|
| 355 |
+
print("train_count:", len(train_samples), "balanced_count:", len(train_balanced), "val_count:", len(val_samples))
|
| 356 |
+
print("dup_hist:", dict(sorted(dup_hist.items())))
|
| 357 |
+
print("=" * 60)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
if __name__ == "__main__":
|
| 361 |
+
main()
|
prepare_test_prompt_v2.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from typing import Dict, List
|
| 5 |
+
|
| 6 |
+
CRITERIA = [
|
| 7 |
+
"Color Harmony",
|
| 8 |
+
"Visual Style Consistency",
|
| 9 |
+
"Sharpness",
|
| 10 |
+
"Light and Shadow Modeling",
|
| 11 |
+
"Creativity and Originality",
|
| 12 |
+
"Exposure Control",
|
| 13 |
+
"Application of Classical Composition Principles",
|
| 14 |
+
"Depth of Field and Layering",
|
| 15 |
+
"Visual Center Stability",
|
| 16 |
+
"Visual Flow Guidance",
|
| 17 |
+
"Structural Support Stability",
|
| 18 |
+
"Appropriateness of Negative Space",
|
| 19 |
+
"Subject Integrity",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
# 三组不同措辞的 prompt,用于后续多次推理投票。
|
| 23 |
+
# 输出 schema 完全一致,避免后处理复杂化。
|
| 24 |
+
PROMPT_VARIANTS = {
|
| 25 |
+
"v1": """You are a portrait-aesthetics judge focused on robust cross-image ranking.
|
| 26 |
+
|
| 27 |
+
Task:
|
| 28 |
+
- Score all 13 criteria with floats in [0.0, 10.0].
|
| 29 |
+
- Output total_score as an integer in [0, 100].
|
| 30 |
+
- Choose answer from A/B/C/D.
|
| 31 |
+
|
| 32 |
+
Scoring policy for total_score:
|
| 33 |
+
- Use holistic visual quality, not a plain arithmetic average of 13 scores.
|
| 34 |
+
- Keep strong separation when quality difference is obvious.
|
| 35 |
+
- Penalize major defects decisively (blur, broken anatomy, unstable structure, severe exposure failure).
|
| 36 |
+
- Reward clearly excellent lighting/composition/subject quality.
|
| 37 |
+
|
| 38 |
+
Typical anchors (not strict formula):
|
| 39 |
+
- severe failure: 0-30
|
| 40 |
+
- poor: 31-45
|
| 41 |
+
- fair: 46-60
|
| 42 |
+
- good: 61-75
|
| 43 |
+
- very good: 76-88
|
| 44 |
+
- excellent: 89-100
|
| 45 |
+
|
| 46 |
+
Hard output constraints:
|
| 47 |
+
- STRICT JSON only.
|
| 48 |
+
- No level/reason/x.
|
| 49 |
+
- No extra keys.
|
| 50 |
+
- One decimal place for criterion scores when possible.
|
| 51 |
+
|
| 52 |
+
Return exactly:
|
| 53 |
+
{
|
| 54 |
+
"criteria": {
|
| 55 |
+
"Color Harmony": {"score": 0.0},
|
| 56 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 57 |
+
"Sharpness": {"score": 0.0},
|
| 58 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 59 |
+
"Creativity and Originality": {"score": 0.0},
|
| 60 |
+
"Exposure Control": {"score": 0.0},
|
| 61 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 62 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 63 |
+
"Visual Center Stability": {"score": 0.0},
|
| 64 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 65 |
+
"Structural Support Stability": {"score": 0.0},
|
| 66 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 67 |
+
"Subject Integrity": {"score": 0.0}
|
| 68 |
+
},
|
| 69 |
+
"total_score": 0,
|
| 70 |
+
"answer": "A|B|C|D"
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
Question: __QUESTION__
|
| 74 |
+
Options:
|
| 75 |
+
A: __OPT_A__
|
| 76 |
+
B: __OPT_B__
|
| 77 |
+
C: __OPT_C__
|
| 78 |
+
D: __OPT_D__
|
| 79 |
+
|
| 80 |
+
<image>""",
|
| 81 |
+
"v2": """Act as a strict technical-quality portrait judge.
|
| 82 |
+
|
| 83 |
+
Priority:
|
| 84 |
+
1) Technical correctness first (sharpness, exposure, structure, subject integrity).
|
| 85 |
+
2) Then composition and visual flow.
|
| 86 |
+
3) Then style/creativity refinement.
|
| 87 |
+
|
| 88 |
+
Rules:
|
| 89 |
+
- If critical technical defects are obvious, lower affected criteria strongly and reduce total_score accordingly.
|
| 90 |
+
- Do not overuse middle scores.
|
| 91 |
+
- total_score must reflect defect severity and overall impression, not simple averaging.
|
| 92 |
+
- answer must be one of A/B/C/D using visible evidence.
|
| 93 |
+
|
| 94 |
+
Output constraints:
|
| 95 |
+
- criteria scores: [0.0, 10.0], one decimal when possible
|
| 96 |
+
- total_score: integer [0, 100]
|
| 97 |
+
- STRICT JSON only, no extra text/keys
|
| 98 |
+
- no level/reason/x
|
| 99 |
+
|
| 100 |
+
Return JSON schema:
|
| 101 |
+
{
|
| 102 |
+
"criteria": {
|
| 103 |
+
"Color Harmony": {"score": 0.0},
|
| 104 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 105 |
+
"Sharpness": {"score": 0.0},
|
| 106 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 107 |
+
"Creativity and Originality": {"score": 0.0},
|
| 108 |
+
"Exposure Control": {"score": 0.0},
|
| 109 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 110 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 111 |
+
"Visual Center Stability": {"score": 0.0},
|
| 112 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 113 |
+
"Structural Support Stability": {"score": 0.0},
|
| 114 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 115 |
+
"Subject Integrity": {"score": 0.0}
|
| 116 |
+
},
|
| 117 |
+
"total_score": 0,
|
| 118 |
+
"answer": "A|B|C|D"
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
Question: __QUESTION__
|
| 122 |
+
Options:
|
| 123 |
+
A: __OPT_A__
|
| 124 |
+
B: __OPT_B__
|
| 125 |
+
C: __OPT_C__
|
| 126 |
+
D: __OPT_D__
|
| 127 |
+
|
| 128 |
+
<image>""",
|
| 129 |
+
"v3": """You are a composition-and-lighting focused portrait evaluator for leaderboard ranking.
|
| 130 |
+
|
| 131 |
+
Focus:
|
| 132 |
+
- Composition principles, visual center stability, flow guidance, light-shadow modeling, depth layering.
|
| 133 |
+
- Keep technical criteria accurate as well.
|
| 134 |
+
- Use discriminative scoring; avoid central-score collapse.
|
| 135 |
+
|
| 136 |
+
total_score guidance:
|
| 137 |
+
- Reflect global aesthetics and scene coherence.
|
| 138 |
+
- Do NOT compute by fixed linear formula from criteria.
|
| 139 |
+
- If composition/light is outstanding and major defects are absent, allow high total_score.
|
| 140 |
+
- If visual organization is weak or unstable, lower total_score decisively.
|
| 141 |
+
|
| 142 |
+
Output:
|
| 143 |
+
- Strict JSON only, exact keys.
|
| 144 |
+
- criteria scores in [0.0,10.0], one decimal when possible.
|
| 145 |
+
- total_score integer [0,100].
|
| 146 |
+
- answer exactly A/B/C/D.
|
| 147 |
+
- no level/reason/x, no extra keys.
|
| 148 |
+
|
| 149 |
+
JSON schema:
|
| 150 |
+
{
|
| 151 |
+
"criteria": {
|
| 152 |
+
"Color Harmony": {"score": 0.0},
|
| 153 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 154 |
+
"Sharpness": {"score": 0.0},
|
| 155 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 156 |
+
"Creativity and Originality": {"score": 0.0},
|
| 157 |
+
"Exposure Control": {"score": 0.0},
|
| 158 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 159 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 160 |
+
"Visual Center Stability": {"score": 0.0},
|
| 161 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 162 |
+
"Structural Support Stability": {"score": 0.0},
|
| 163 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 164 |
+
"Subject Integrity": {"score": 0.0}
|
| 165 |
+
},
|
| 166 |
+
"total_score": 0,
|
| 167 |
+
"answer": "A|B|C|D"
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
Question: __QUESTION__
|
| 171 |
+
Options:
|
| 172 |
+
A: __OPT_A__
|
| 173 |
+
B: __OPT_B__
|
| 174 |
+
C: __OPT_C__
|
| 175 |
+
D: __OPT_D__
|
| 176 |
+
|
| 177 |
+
<image>""",
|
| 178 |
+
"v4": """You are a conservative-but-discriminative portrait quality judge.
|
| 179 |
+
|
| 180 |
+
Policy:
|
| 181 |
+
- Be strict on obvious flaws.
|
| 182 |
+
- Do not suppress clearly high-quality images.
|
| 183 |
+
- Keep ranking-sensitive separation: when two quality levels are visibly different, total_score should differ clearly.
|
| 184 |
+
|
| 185 |
+
Internal check before final JSON:
|
| 186 |
+
- Are penalized criteria exactly those with visible defects?
|
| 187 |
+
- Is total_score coherent with overall impression?
|
| 188 |
+
- Is answer choice visually supported?
|
| 189 |
+
|
| 190 |
+
Hard constraints:
|
| 191 |
+
- criteria score float in [0.0,10.0]
|
| 192 |
+
- total_score integer [0,100]
|
| 193 |
+
- answer in A/B/C/D
|
| 194 |
+
- strict JSON only
|
| 195 |
+
- no level/reason/x
|
| 196 |
+
- no extra keys
|
| 197 |
+
|
| 198 |
+
Schema:
|
| 199 |
+
{
|
| 200 |
+
"criteria": {
|
| 201 |
+
"Color Harmony": {"score": 0.0},
|
| 202 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 203 |
+
"Sharpness": {"score": 0.0},
|
| 204 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 205 |
+
"Creativity and Originality": {"score": 0.0},
|
| 206 |
+
"Exposure Control": {"score": 0.0},
|
| 207 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 208 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 209 |
+
"Visual Center Stability": {"score": 0.0},
|
| 210 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 211 |
+
"Structural Support Stability": {"score": 0.0},
|
| 212 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 213 |
+
"Subject Integrity": {"score": 0.0}
|
| 214 |
+
},
|
| 215 |
+
"total_score": 0,
|
| 216 |
+
"answer": "A|B|C|D"
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
Question: __QUESTION__
|
| 220 |
+
Options:
|
| 221 |
+
A: __OPT_A__
|
| 222 |
+
B: __OPT_B__
|
| 223 |
+
C: __OPT_C__
|
| 224 |
+
D: __OPT_D__
|
| 225 |
+
|
| 226 |
+
<image>""",
|
| 227 |
+
"v5": """You are an expert portrait evaluator optimized for SRCC/PLCC robustness.
|
| 228 |
+
|
| 229 |
+
Scoring intent:
|
| 230 |
+
- Produce stable, monotonic quality ranking across images.
|
| 231 |
+
- Use full score range when evidence is strong.
|
| 232 |
+
- Avoid producing similar total_score for clearly different quality.
|
| 233 |
+
- Keep criteria pattern and total_score logically aligned, but not by fixed linear averaging.
|
| 234 |
+
|
| 235 |
+
Practical guide:
|
| 236 |
+
- Multiple severe issues -> usually <=45.
|
| 237 |
+
- Noticeable technical/compositional weakness -> usually 46-60.
|
| 238 |
+
- Solid quality with minor issues -> usually 61-75.
|
| 239 |
+
- Strong quality with no major defects -> usually 76-88.
|
| 240 |
+
- Exceptional overall result -> usually 89-100.
|
| 241 |
+
|
| 242 |
+
Output rules:
|
| 243 |
+
- STRICT JSON only.
|
| 244 |
+
- No markdown, no explanation, no extra keys.
|
| 245 |
+
- No level/reason/x.
|
| 246 |
+
- criteria score float [0.0,10.0], one decimal when possible.
|
| 247 |
+
- total_score integer [0,100].
|
| 248 |
+
- answer exactly A/B/C/D.
|
| 249 |
+
|
| 250 |
+
Schema:
|
| 251 |
+
{
|
| 252 |
+
"criteria": {
|
| 253 |
+
"Color Harmony": {"score": 0.0},
|
| 254 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 255 |
+
"Sharpness": {"score": 0.0},
|
| 256 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 257 |
+
"Creativity and Originality": {"score": 0.0},
|
| 258 |
+
"Exposure Control": {"score": 0.0},
|
| 259 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 260 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 261 |
+
"Visual Center Stability": {"score": 0.0},
|
| 262 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 263 |
+
"Structural Support Stability": {"score": 0.0},
|
| 264 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 265 |
+
"Subject Integrity": {"score": 0.0}
|
| 266 |
+
},
|
| 267 |
+
"total_score": 0,
|
| 268 |
+
"answer": "A|B|C|D"
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
Question: __QUESTION__
|
| 272 |
+
Options:
|
| 273 |
+
A: __OPT_A__
|
| 274 |
+
B: __OPT_B__
|
| 275 |
+
C: __OPT_C__
|
| 276 |
+
D: __OPT_D__
|
| 277 |
+
|
| 278 |
+
<image>""",
|
| 279 |
+
"v6": """You are a defect-sensitive portrait aesthetics evaluator.
|
| 280 |
+
Prioritize technical reliability before style bonus.
|
| 281 |
+
|
| 282 |
+
Rules:
|
| 283 |
+
- Score 13 criteria in [0.0, 10.0], one decimal when possible.
|
| 284 |
+
- total_score must be an integer in [0, 100].
|
| 285 |
+
- answer must be exactly one of A/B/C/D.
|
| 286 |
+
- Do not output level/reason/x. Do not add extra keys.
|
| 287 |
+
- Output STRICT JSON only.
|
| 288 |
+
|
| 289 |
+
Scoring behavior:
|
| 290 |
+
- If severe defects exist (focus blur, broken anatomy, unstable structure, severe over/under exposure), reduce affected criteria decisively.
|
| 291 |
+
- Do not keep scores crowded in the middle.
|
| 292 |
+
- total_score should reflect overall perceptual quality and defect severity, not a fixed arithmetic formula.
|
| 293 |
+
|
| 294 |
+
Return exactly this schema:
|
| 295 |
+
{
|
| 296 |
+
"criteria": {
|
| 297 |
+
"Color Harmony": {"score": 0.0},
|
| 298 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 299 |
+
"Sharpness": {"score": 0.0},
|
| 300 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 301 |
+
"Creativity and Originality": {"score": 0.0},
|
| 302 |
+
"Exposure Control": {"score": 0.0},
|
| 303 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 304 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 305 |
+
"Visual Center Stability": {"score": 0.0},
|
| 306 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 307 |
+
"Structural Support Stability": {"score": 0.0},
|
| 308 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 309 |
+
"Subject Integrity": {"score": 0.0}
|
| 310 |
+
},
|
| 311 |
+
"total_score": 0,
|
| 312 |
+
"answer": "A|B|C|D"
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
Question: __QUESTION__
|
| 316 |
+
Options:
|
| 317 |
+
A: __OPT_A__
|
| 318 |
+
B: __OPT_B__
|
| 319 |
+
C: __OPT_C__
|
| 320 |
+
D: __OPT_D__
|
| 321 |
+
|
| 322 |
+
<image>""",
|
| 323 |
+
"v7": """You are a composition-and-lighting oriented portrait judge.
|
| 324 |
+
|
| 325 |
+
Output constraints:
|
| 326 |
+
- 13 criterion scores: float in [0.0, 10.0], one decimal when possible.
|
| 327 |
+
- total_score: integer in [0, 100].
|
| 328 |
+
- answer: A/B/C/D only.
|
| 329 |
+
- STRICT JSON only, no extra text, no extra keys, no level/reason/x.
|
| 330 |
+
|
| 331 |
+
Evaluation emphasis:
|
| 332 |
+
- Give strong weight to composition principles, light-shadow modeling, visual center stability, visual flow, depth layering.
|
| 333 |
+
- Technical failures must still be penalized clearly.
|
| 334 |
+
- Use a discriminative score range: clearly better images should get clearly higher total_score.
|
| 335 |
+
- Keep total_score globally coherent with visual impression.
|
| 336 |
+
|
| 337 |
+
Schema:
|
| 338 |
+
{
|
| 339 |
+
"criteria": {
|
| 340 |
+
"Color Harmony": {"score": 0.0},
|
| 341 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 342 |
+
"Sharpness": {"score": 0.0},
|
| 343 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 344 |
+
"Creativity and Originality": {"score": 0.0},
|
| 345 |
+
"Exposure Control": {"score": 0.0},
|
| 346 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 347 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 348 |
+
"Visual Center Stability": {"score": 0.0},
|
| 349 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 350 |
+
"Structural Support Stability": {"score": 0.0},
|
| 351 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 352 |
+
"Subject Integrity": {"score": 0.0}
|
| 353 |
+
},
|
| 354 |
+
"total_score": 0,
|
| 355 |
+
"answer": "A|B|C|D"
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
Question: __QUESTION__
|
| 359 |
+
Options:
|
| 360 |
+
A: __OPT_A__
|
| 361 |
+
B: __OPT_B__
|
| 362 |
+
C: __OPT_C__
|
| 363 |
+
D: __OPT_D__
|
| 364 |
+
|
| 365 |
+
<image>""",
|
| 366 |
+
"v8": """You are a strict cross-image ranking evaluator for portrait aesthetics.
|
| 367 |
+
|
| 368 |
+
Hard rules:
|
| 369 |
+
1) criteria scores in [0.0, 10.0]
|
| 370 |
+
2) total_score integer in [0, 100]
|
| 371 |
+
3) answer exactly A/B/C/D
|
| 372 |
+
4) strict JSON only
|
| 373 |
+
5) no level/reason/x and no extra keys
|
| 374 |
+
6) one decimal for criterion scores when possible
|
| 375 |
+
|
| 376 |
+
Ranking-oriented guidance:
|
| 377 |
+
- Avoid score collapse around 50-70.
|
| 378 |
+
- Apply stronger separation when quality difference is obvious.
|
| 379 |
+
- Penalize defects by impacted criteria instead of uniform reduction.
|
| 380 |
+
- total_score should be stable and monotonic with perceived overall quality.
|
| 381 |
+
|
| 382 |
+
Anchor ranges (guidance only):
|
| 383 |
+
- severe failure: 0-30
|
| 384 |
+
- weak: 31-45
|
| 385 |
+
- fair: 46-60
|
| 386 |
+
- good: 61-75
|
| 387 |
+
- very good: 76-88
|
| 388 |
+
- excellent: 89-100
|
| 389 |
+
|
| 390 |
+
Schema:
|
| 391 |
+
{
|
| 392 |
+
"criteria": {
|
| 393 |
+
"Color Harmony": {"score": 0.0},
|
| 394 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 395 |
+
"Sharpness": {"score": 0.0},
|
| 396 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 397 |
+
"Creativity and Originality": {"score": 0.0},
|
| 398 |
+
"Exposure Control": {"score": 0.0},
|
| 399 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 400 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 401 |
+
"Visual Center Stability": {"score": 0.0},
|
| 402 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 403 |
+
"Structural Support Stability": {"score": 0.0},
|
| 404 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 405 |
+
"Subject Integrity": {"score": 0.0}
|
| 406 |
+
},
|
| 407 |
+
"total_score": 0,
|
| 408 |
+
"answer": "A|B|C|D"
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
Question: __QUESTION__
|
| 412 |
+
Options:
|
| 413 |
+
A: __OPT_A__
|
| 414 |
+
B: __OPT_B__
|
| 415 |
+
C: __OPT_C__
|
| 416 |
+
D: __OPT_D__
|
| 417 |
+
|
| 418 |
+
<image>""",
|
| 419 |
+
"v9": """You are an evidence-driven portrait quality assessor.
|
| 420 |
+
|
| 421 |
+
Instruction:
|
| 422 |
+
- Judge visible evidence only.
|
| 423 |
+
- Be conservative on uncertain details, but do not under-score clearly excellent images.
|
| 424 |
+
- Distinguish quality levels clearly for robust ranking.
|
| 425 |
+
|
| 426 |
+
Output constraints:
|
| 427 |
+
- criteria scores: float [0.0, 10.0], one decimal when possible
|
| 428 |
+
- total_score: integer [0, 100]
|
| 429 |
+
- answer: one of A/B/C/D
|
| 430 |
+
- strict JSON only; no markdown/explanation
|
| 431 |
+
- no level/reason/x; no extra keys
|
| 432 |
+
|
| 433 |
+
Consistency checks (internal):
|
| 434 |
+
- Are low scores tied to actual flaws?
|
| 435 |
+
- Are high scores supported by visible strengths?
|
| 436 |
+
- Is total_score coherent with whole-image perception?
|
| 437 |
+
|
| 438 |
+
Schema:
|
| 439 |
+
{
|
| 440 |
+
"criteria": {
|
| 441 |
+
"Color Harmony": {"score": 0.0},
|
| 442 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 443 |
+
"Sharpness": {"score": 0.0},
|
| 444 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 445 |
+
"Creativity and Originality": {"score": 0.0},
|
| 446 |
+
"Exposure Control": {"score": 0.0},
|
| 447 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 448 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 449 |
+
"Visual Center Stability": {"score": 0.0},
|
| 450 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 451 |
+
"Structural Support Stability": {"score": 0.0},
|
| 452 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 453 |
+
"Subject Integrity": {"score": 0.0}
|
| 454 |
+
},
|
| 455 |
+
"total_score": 0,
|
| 456 |
+
"answer": "A|B|C|D"
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
Question: __QUESTION__
|
| 460 |
+
Options:
|
| 461 |
+
A: __OPT_A__
|
| 462 |
+
B: __OPT_B__
|
| 463 |
+
C: __OPT_C__
|
| 464 |
+
D: __OPT_D__
|
| 465 |
+
|
| 466 |
+
<image>""",
|
| 467 |
+
"v10": """You are an expert portrait aesthetics scorer optimized for robust cross-image ranking.
|
| 468 |
+
Produce high-fidelity structured scoring with strong separation.
|
| 469 |
+
|
| 470 |
+
Scoring policy:
|
| 471 |
+
- Use broad dynamic range where warranted.
|
| 472 |
+
- Penalize defects in the specific affected criteria rather than uniformly.
|
| 473 |
+
- Reward exceptional composition/light/style quality where clearly present.
|
| 474 |
+
- Keep total_score coherent with the 13-criterion profile and overall perception.
|
| 475 |
+
|
| 476 |
+
Mandatory output rules:
|
| 477 |
+
1) 13 criterion scores, each in [0.0, 10.0]
|
| 478 |
+
2) total_score integer in [0, 100]
|
| 479 |
+
3) answer exactly A/B/C/D
|
| 480 |
+
4) strict JSON only, no extra narration
|
| 481 |
+
5) do not output level/reason/x, do not add extra keys
|
| 482 |
+
|
| 483 |
+
Schema:
|
| 484 |
+
{
|
| 485 |
+
"criteria": {
|
| 486 |
+
"Color Harmony": {"score": 0.0},
|
| 487 |
+
"Visual Style Consistency": {"score": 0.0},
|
| 488 |
+
"Sharpness": {"score": 0.0},
|
| 489 |
+
"Light and Shadow Modeling": {"score": 0.0},
|
| 490 |
+
"Creativity and Originality": {"score": 0.0},
|
| 491 |
+
"Exposure Control": {"score": 0.0},
|
| 492 |
+
"Application of Classical Composition Principles": {"score": 0.0},
|
| 493 |
+
"Depth of Field and Layering": {"score": 0.0},
|
| 494 |
+
"Visual Center Stability": {"score": 0.0},
|
| 495 |
+
"Visual Flow Guidance": {"score": 0.0},
|
| 496 |
+
"Structural Support Stability": {"score": 0.0},
|
| 497 |
+
"Appropriateness of Negative Space": {"score": 0.0},
|
| 498 |
+
"Subject Integrity": {"score": 0.0}
|
| 499 |
+
},
|
| 500 |
+
"total_score": 0,
|
| 501 |
+
"answer": "A|B|C|D"
|
| 502 |
+
}
|
| 503 |
+
Use one decimal place for each score when possible.
|
| 504 |
+
|
| 505 |
+
Question: __QUESTION__
|
| 506 |
+
Options:
|
| 507 |
+
A: __OPT_A__
|
| 508 |
+
B: __OPT_B__
|
| 509 |
+
C: __OPT_C__
|
| 510 |
+
D: __OPT_D__
|
| 511 |
+
|
| 512 |
+
<image>
|
| 513 |
+
""",
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def resolve_input_path(path_arg: str) -> str:
|
| 518 |
+
if path_arg:
|
| 519 |
+
return path_arg
|
| 520 |
+
|
| 521 |
+
preferred = "outputs/predictions/track_1_test.json"
|
| 522 |
+
fallback = "datasets/original_annotations/track_1_test.json"
|
| 523 |
+
if os.path.exists(preferred):
|
| 524 |
+
return preferred
|
| 525 |
+
return fallback
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def resolve_image_path(raw_path: str, image_dir: str) -> str:
|
| 529 |
+
raw_path = str(raw_path).strip()
|
| 530 |
+
if raw_path and os.path.exists(raw_path):
|
| 531 |
+
return os.path.abspath(raw_path)
|
| 532 |
+
|
| 533 |
+
candidate = os.path.join(image_dir, os.path.basename(raw_path))
|
| 534 |
+
return os.path.abspath(candidate)
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
def parse_variants(raw: str) -> List[str]:
|
| 538 |
+
variants = [x.strip() for x in str(raw).split(",") if x.strip()]
|
| 539 |
+
if not variants:
|
| 540 |
+
variants = ["v1"]
|
| 541 |
+
|
| 542 |
+
invalid = [v for v in variants if v not in PROMPT_VARIANTS]
|
| 543 |
+
if invalid:
|
| 544 |
+
raise ValueError(
|
| 545 |
+
f"Unknown prompt variants: {invalid}. Available: {sorted(PROMPT_VARIANTS.keys())}"
|
| 546 |
+
)
|
| 547 |
+
return variants
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
def build_prompt(item: Dict, variant: str) -> str:
|
| 551 |
+
template = PROMPT_VARIANTS[variant]
|
| 552 |
+
return (
|
| 553 |
+
template.replace("__QUESTION__", str(item["question"]))
|
| 554 |
+
.replace("__OPT_A__", str(item["options"]["A"]))
|
| 555 |
+
.replace("__OPT_B__", str(item["options"]["B"]))
|
| 556 |
+
.replace("__OPT_C__", str(item["options"]["C"]))
|
| 557 |
+
.replace("__OPT_D__", str(item["options"]["D"]))
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def build_record(item: Dict, image_abs_path: str, variant: str) -> Dict:
|
| 562 |
+
return {
|
| 563 |
+
"messages": [
|
| 564 |
+
{"role": "user", "content": build_prompt(item, variant)},
|
| 565 |
+
{"role": "assistant", "content": ""},
|
| 566 |
+
],
|
| 567 |
+
"images": [image_abs_path],
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def output_path_for_variant(output_json: str, variant: str, multi_variant: bool) -> str:
|
| 572 |
+
if not multi_variant:
|
| 573 |
+
return output_json
|
| 574 |
+
|
| 575 |
+
root, ext = os.path.splitext(output_json)
|
| 576 |
+
if not ext:
|
| 577 |
+
ext = ".json"
|
| 578 |
+
return f"{root}_{variant}{ext}"
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
def main() -> None:
|
| 582 |
+
parser = argparse.ArgumentParser()
|
| 583 |
+
parser.add_argument(
|
| 584 |
+
"--input_json",
|
| 585 |
+
type=str,
|
| 586 |
+
default="",
|
| 587 |
+
help="Official test json path. If empty, auto-detect.",
|
| 588 |
+
)
|
| 589 |
+
parser.add_argument(
|
| 590 |
+
"--image_dir",
|
| 591 |
+
type=str,
|
| 592 |
+
default="./datasets/images",
|
| 593 |
+
)
|
| 594 |
+
parser.add_argument(
|
| 595 |
+
"--output_json",
|
| 596 |
+
type=str,
|
| 597 |
+
default="outputs/predictions/track1_test_convert.json",
|
| 598 |
+
)
|
| 599 |
+
parser.add_argument(
|
| 600 |
+
"--prompt_variants",
|
| 601 |
+
type=str,
|
| 602 |
+
default="v1",
|
| 603 |
+
help="Comma-separated variants, e.g. v1 or v1,v2,v3",
|
| 604 |
+
)
|
| 605 |
+
args = parser.parse_args()
|
| 606 |
+
|
| 607 |
+
input_json = resolve_input_path(args.input_json)
|
| 608 |
+
variants = parse_variants(args.prompt_variants)
|
| 609 |
+
multi_variant = len(variants) > 1
|
| 610 |
+
|
| 611 |
+
print(f"[INFO] reading test set: {input_json}")
|
| 612 |
+
with open(input_json, "r", encoding="utf-8") as f:
|
| 613 |
+
official_data = json.load(f)
|
| 614 |
+
|
| 615 |
+
os.makedirs(os.path.dirname(args.output_json), exist_ok=True)
|
| 616 |
+
|
| 617 |
+
for variant in variants:
|
| 618 |
+
missing_images = 0
|
| 619 |
+
llama_factory_data = []
|
| 620 |
+
for idx, item in enumerate(official_data):
|
| 621 |
+
img_path = resolve_image_path(item.get("image_path", ""), args.image_dir)
|
| 622 |
+
if not os.path.exists(img_path):
|
| 623 |
+
missing_images += 1
|
| 624 |
+
if missing_images <= 5:
|
| 625 |
+
print(f"[WARN][{variant}] missing image ({idx}): {img_path}")
|
| 626 |
+
|
| 627 |
+
record = build_record(item, img_path, variant)
|
| 628 |
+
llama_factory_data.append(record)
|
| 629 |
+
|
| 630 |
+
out_path = output_path_for_variant(args.output_json, variant, multi_variant)
|
| 631 |
+
with open(out_path, "w", encoding="utf-8") as f:
|
| 632 |
+
json.dump(llama_factory_data, f, ensure_ascii=False, indent=2)
|
| 633 |
+
|
| 634 |
+
print(f"[INFO] saved ({variant}): {out_path}")
|
| 635 |
+
print(f"[INFO] ({variant}) samples: {len(llama_factory_data)}, missing_images: {missing_images}")
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
if __name__ == "__main__":
|
| 639 |
+
main()
|
train_track1_stage1_qwen3vl8b_v1.yaml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_name_or_path: Qwen/Qwen3-VL-8B-Instruct
|
| 2 |
+
template: qwen3_vl_nothink
|
| 3 |
+
stage: sft
|
| 4 |
+
do_train: true
|
| 5 |
+
do_eval: true
|
| 6 |
+
finetuning_type: lora
|
| 7 |
+
trust_remote_code: true
|
| 8 |
+
|
| 9 |
+
dataset_dir: .
|
| 10 |
+
dataset: codabench_portrait_train_score_v2
|
| 11 |
+
eval_dataset: codabench_portrait_val_score_v2
|
| 12 |
+
max_samples: 1000000
|
| 13 |
+
cutoff_len: 4096
|
| 14 |
+
image_max_pixels: 786432
|
| 15 |
+
preprocessing_num_workers: 32
|
| 16 |
+
dataloader_num_workers: 8
|
| 17 |
+
per_device_eval_batch_size: 8
|
| 18 |
+
eval_strategy: steps
|
| 19 |
+
eval_steps: 120
|
| 20 |
+
|
| 21 |
+
learning_rate: 3.0e-5
|
| 22 |
+
num_train_epochs: 1.0
|
| 23 |
+
per_device_train_batch_size: 4
|
| 24 |
+
gradient_accumulation_steps: 3
|
| 25 |
+
lr_scheduler_type: cosine
|
| 26 |
+
warmup_ratio: 0.03
|
| 27 |
+
weight_decay: 0.01
|
| 28 |
+
max_grad_norm: 1.0
|
| 29 |
+
|
| 30 |
+
bf16: true
|
| 31 |
+
flash_attn: auto
|
| 32 |
+
gradient_checkpointing: true
|
| 33 |
+
ddp_timeout: 180000000
|
| 34 |
+
|
| 35 |
+
lora_rank: 64
|
| 36 |
+
lora_alpha: 128
|
| 37 |
+
lora_target: all
|
| 38 |
+
lora_dropout: 0.05
|
| 39 |
+
|
| 40 |
+
logging_steps: 10
|
| 41 |
+
save_steps: 120
|
| 42 |
+
save_total_limit: 6
|
| 43 |
+
plot_loss: true
|
| 44 |
+
overwrite_output_dir: true
|
| 45 |
+
report_to: none
|
| 46 |
+
output_dir: saves/qwen3vl8b_track1_stage1_v1
|
train_track1_stage2_qwen3vl8b_v1.yaml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_name_or_path: Qwen/Qwen3-VL-8B-Instruct
|
| 2 |
+
adapter_name_or_path: saves/qwen3vl8b_track1_stage1_v1
|
| 3 |
+
template: qwen3_vl_nothink
|
| 4 |
+
stage: sft
|
| 5 |
+
do_train: true
|
| 6 |
+
do_eval: true
|
| 7 |
+
finetuning_type: lora
|
| 8 |
+
create_new_adapter: false
|
| 9 |
+
trust_remote_code: true
|
| 10 |
+
|
| 11 |
+
dataset_dir: .
|
| 12 |
+
dataset: codabench_portrait_train_balanced_v2
|
| 13 |
+
eval_dataset: codabench_portrait_val_multitask_v2
|
| 14 |
+
max_samples: 1000000
|
| 15 |
+
cutoff_len: 4096
|
| 16 |
+
image_max_pixels: 786432
|
| 17 |
+
preprocessing_num_workers: 32
|
| 18 |
+
dataloader_num_workers: 8
|
| 19 |
+
per_device_eval_batch_size: 8
|
| 20 |
+
eval_strategy: steps
|
| 21 |
+
eval_steps: 120
|
| 22 |
+
|
| 23 |
+
learning_rate: 1.5e-5
|
| 24 |
+
num_train_epochs: 1.2
|
| 25 |
+
per_device_train_batch_size: 4
|
| 26 |
+
gradient_accumulation_steps: 3
|
| 27 |
+
lr_scheduler_type: cosine
|
| 28 |
+
warmup_ratio: 0.03
|
| 29 |
+
weight_decay: 0.01
|
| 30 |
+
max_grad_norm: 1.0
|
| 31 |
+
|
| 32 |
+
bf16: true
|
| 33 |
+
flash_attn: auto
|
| 34 |
+
gradient_checkpointing: true
|
| 35 |
+
ddp_timeout: 180000000
|
| 36 |
+
|
| 37 |
+
lora_rank: 64
|
| 38 |
+
lora_alpha: 128
|
| 39 |
+
lora_target: all
|
| 40 |
+
lora_dropout: 0.05
|
| 41 |
+
|
| 42 |
+
logging_steps: 10
|
| 43 |
+
save_steps: 120
|
| 44 |
+
save_total_limit: 6
|
| 45 |
+
plot_loss: true
|
| 46 |
+
overwrite_output_dir: true
|
| 47 |
+
report_to: none
|
| 48 |
+
output_dir: saves/qwen3vl8b_track1_stage2_v1
|