Single, response-aware codebase for HumOmni Track 1: EmpathyEval.
A bilingual README — English first (for reviewers), Chinese after (for the maintainer).
This is the final v5.1 archive: Task1 uses a pointwise-listwise, 6-prompt-branch,
two-stage-fusion pipeline; Task2 uses a single response-aware branch inherited unchanged
from v4. All data / model / test paths are resolved from two small JSON files
(data_paths.json, test_paths.json) — nothing about "test1" vs "stage2" (or any other
round name) is hard-coded anywhere in the scripts.
English
0. Quick Reproduction Guide (Full Pipeline: Data Processing → Training → Testing)
This section is a self-contained, copy-pasteable command list covering the entire
pipeline (not just test-time inference). If you only need to reproduce the final
submission from the checkpoints already shipped in outputs/models/, you can skip
straight to A.3/A.4 below.
(Optional) calibrate fusion weights on val — only needed if branches/prompts changed;
config.py's FUSION_CONFIG already holds the real values used for the submission:
single response-aware branch (task2_resp), unchanged since v4
Task1's method evolved through several prior versions (pairwise + bidirectional
de-biasing in v4.1/v4.2, then pointwise-listwise multi-branch fusion here in v5.1) — see
the technical report for the full rationale. This archive only ships the final v5.1
method for both tasks; earlier pairwise/bidirectional scripts are not included here.
2. Two path files — this is the only thing you ever need to edit
File
Used by
Purpose
data_paths.json
prepare_and_train.py
Where the official training data (+ optionally the base model) lives, for data processing and (re)training from scratch.
test_paths.json
test_submit.py
Where the test/release data you want to score lives, for inference. Works for any batch/round of release data — just point the paths at it, no code changes needed.
Both are read by a thin Python wrapper that turns their fields into the exact environment
variables config.py already knows how to consume (DATA_ROOT, MODEL_PATH,
TASK1_GIGASPEECH_DIR, etc.). No script in this codebase branches on a hard-coded
"test1"/"stage2" label; whichever data you point test_paths.json at is what gets scored.
3. Layout
code_v5.1/
├── data_paths.json # EDIT ME to (re)run data processing / training from scratch
├── prepare_and_train.py # wrapper: data_paths.json -> 01/02/02b + 03b(x6 branches)/03(task2)
├── test_paths.json # EDIT ME to run inference / produce a submission jsonl
├── test_submit.py # wrapper: test_paths.json -> 05_test_pointwise(x6)+07_fuse+05_test+06_merge
├── SUBMISSION.md # short human-readable version of the two workflows above
├── ENVIRONMENT_REPRO.md # verified Python/CUDA/package versions + vendored transformers install
├── requirements.txt
├── vendor/
│ └── transformers_qwen_omni_sitepkg.tar.gz # vendored Qwen2.5-Omni-compatible transformers build
│ # (the original GitHub preview commit used to build
│ # this is no longer fetchable from GitHub — see
│ # ENVIRONMENT_REPRO.md; always install from this tarball)
│
├── 01_prepare_finetune.py # flat jsonl -> finetune_task*.jsonl (adds "response" field)
├── 02_build_augmented.py # finetune_task*.jsonl -> task*_train.jsonl + task*_val.jsonl
│ # (hold-out val split, intra-group hard negatives, task1 A/B-swap aug)
├── 02b_build_pointwise.py # task1 only: binary train/val -> pointwise (per-candidate) train/val
├── 03_train.py # binary LoRA fine-tuning (used for task2; task1's older binary mode)
├── 03b_train_pointwise.py # task1 pointwise-listwise LoRA fine-tuning (trains ONE branch P0-P5)
├── 04_eval.py # binary validation-set evaluation (task2)
├── 04_eval_pointwise.py # task1 pointwise val scoring + fusion-weight calibration (tau/alpha grid search)
├── 05_test.py # test-set prediction, binary pipeline (used for task2)
├── 05_test_pointwise.py # test-set per-candidate scoring, ONE task1 branch at a time
├── 06_merge.py # merge per-subset predictions into one submission jsonl
├── 07_fuse_task1.py # merge all 6 task1 branch score files -> final task1 predictions
│ # (two-stage weighted fusion; see §5 of the technical report)
├── config.py # all paths + hyper-params + the 6-branch / fusion registry
├── utils/
│ ├── prompts.py # binary prompt builder (task2) + pointwise prompt builder (task1 P0-P5)
│ ├── audio_io.py # load_audio + LRU + optional .npy cache
│ ├── group_sampler.py # GroupedListwiseSampler (task1 pointwise: whole contrast group together)
│ └── pairwise_voting.py # MELD 3-choice -> C(N,2) pairwise scoring, optional bidirectional
├── exp_configs/
│ ├── task1_p0.yaml ... task1_p5.yaml # the 6 production task1 branches (pointwise_listwise mode)
│ ├── task1_baseline.yaml, task1_resp.yaml # legacy binary-pipeline configs (kept for reference;
│ │ no trained checkpoint for these ships in this archive)
│ ├── task2_baseline.yaml, task2_resp.yaml # task2_resp is the one actually used in the final submission
└── outputs/
├── data/ # 01/02/02b products (jsonl)
├── models/ # task1_p0..p5/ (6 branches) + task2_resp/ — each keeps ALL intermediate
│ # checkpoints (incl. optimizer.pt/scheduler.pt, so training can resume),
│ # plus best_ckpt.json recording the real val metric per checkpoint
├── predictions/ # per-branch score files + fused task1 predictions + task2 predictions
└── logs/ # real train/eval/fuse logs from the original run (27 files)
4. Quick start — reproduce the submission from the shipped checkpoints
This is the common case: you just want a submission_track1.jsonl from a batch of
release data, using the 7 checkpoints already included under outputs/models/.
Step 0 — environment. See ENVIRONMENT_REPRO.md for the exact verified versions
and requirements.txt for the pinned packages. In short:
bash
1conda create -n track1_repro python=3.10 -y
2conda activate track1_repro
3pip install --upgrade pip
4pip install -r requirements.txt
56# Install the vendored Qwen2.5-Omni-compatible transformers build7# (do NOT try to `pip install` the original GitHub preview commit — it is no8# longer fetchable, see ENVIRONMENT_REPRO.md for why):9SITE=$(python -c "import site; print(site.getsitepackages()[0])")10tar -xzf vendor/transformers_qwen_omni_sitepkg.tar.gz -C "$SITE"
Step 1 — edit test_paths.json to point at your batch of release data:
This runs, in order: 05_test_pointwise.py for each of the 6 task1 branches (P0-P5) →
07_fuse_task1.py (two-stage weighted fusion, fixed production weights, see §6) →
05_test.py for task2 (task2_resp) → 06_merge.py. The final jsonl is written to
output_submission. See SUBMISSION.md for the exact fixed fusion weights and flags
(--skip_task1_scores, --skip_task2) to re-run only part of the pipeline.
5. Full pipeline (data processing → training → testing)
5.1 Retraining from scratch. Edit data_paths.json (data_root = folder containing
the official flat jsonl + user_audio/response_audio; model_path optional), then:
bash
1python prepare_and_train.py # data (01/02/02b) + all 6 task1 branches + task22python prepare_and_train.py --skip_data # reuse existing outputs/data/*.jsonl3python prepare_and_train.py --only task1 --branches p4 p5 # retrain just these branches
5.2 Calibrating the fusion weights on val (optional). After training, run
04_eval_pointwise.py --branch p0 (... p5) to produce per-branch val score files, then
04_eval_pointwise.py --fuse_only to grid-search tau/alpha and print per-branch
accuracy for manual weight tuning. The values already in config.py's FUSION_CONFIG
are the real calibrated weights used for the final submission (see §6 below), so this
step is only needed if you change branches/prompts and want to re-calibrate.
5.3 Testing / merging. See §4 above (test_paths.json + test_submit.py), or call
05_test_pointwise.py / 07_fuse_task1.py / 05_test.py / 06_merge.py directly — each
script's own --help / module docstring documents its CLI flags.
6. Task1 method summary (final v5.1)
Each candidate audio is scored independently (score = logit(pos_token) - logit(neg_token)),
not compared pairwise. Six independently-trained LoRA branches (P0-P5), each with its own
prompt template (see utils/prompts.py and config.py's POINTWISE_BRANCHES), are combined
in two stages:
Stage 1: weighted average of all 6 branches — p0=0.23, p1=0.20, p2=0.06, p3=0.15, p4=0.14, p5=0.22 (FUSION_CONFIG.stage1_weights).
Stage 2 (hard samples only, where the stage-1 top1/top2 margin falls below the
20th percentile): re-score with p1=0.25, p3=0.20, p5=0.55
(FUSION_CONFIG.second_stage_weights), then blend final = 0.5*stage1 + 0.5*stage2.
These are the exact production values calibrated on the local val set and used for the
real submission — see the technical report (§3.5) for the full derivation and the
per-branch checkpoint table.
7. Environment notes
See ENVIRONMENT_REPRO.md for the full verified version table (Python 3.10, torch==2.5.1+cu121,
a vendored transformers build with Qwen2.5-Omni support, peft==0.19.1, etc.) and why the
vendored tarball — not a GitHub git-commit install — is the supported reproduction path.
8. What was removed from earlier working copies of this codebase
This archive keeps the full data-processing → training → testing pipeline, but drops
everything that was only useful during development and is not part of the shipped
method: the stage/round-specific --stage2 code paths in config.py/05_test.py/
06_merge.py/07_fuse_task1.py/05_test_pointwise.py (replaced by the generic
test_paths.json mechanism in §2/§4 above), the multi-prompt exploration scripts
(09_eval_task1_multiprompt.py, 10_test_task1_multiprompt.py) and their dedicated
utils/task1_prompt_variants.py/utils/task1_scoring.py, the post-hoc majority-vote/
weighted ensembling scripts (07_ensemble_task1.py, 08_ensemble_task2_weighted.py)
which belonged to the older non-pointwise pipeline, and one-off developer tools
(cache_audio_npy.py, convert_wav_to_pcm.py, inspect_pred.py,
translate_zh_comments.py, run_all.sh). None of these are needed to reproduce the
final v5.1 submission.
9. Troubleshooting
git clone/pip install of the original transformers preview commit fails: expected —
see ENVIRONMENT_REPRO.md. Use the vendored tarball in vendor/ instead.
05_test_pointwise.py picks the wrong checkpoint / errors on adapter load without --lora:
it auto-resolves from best_ckpt.json, falling back to a local-directory-name match if the
recorded absolute path (from the original training machine) doesn't exist here — see the
module docstring / _resolve_lora_dir for the exact 4-step priority order. If it still can't
find the right checkpoint, pass --lora outputs/models/task1_p*/checkpoint-... explicitly.
[ERROR] Some release json files were not found (from test_submit.py): the paths in
test_paths.json are wrong; edit them and re-run.
raw_jsonl 中无任何 response 字段: rerun 01_prepare_finetune.py — the source flat jsonl
must contain contexts[i].response (task1) / contexts["<emotion>_response"] (task2).