Add complete Tr006 V2 Segment pipeline bundle
Browse filesRaw video to blue intervals, fixed80 V1, blue-clean V2 train input, exact ms-swift source, training, checkpoint merge, Docker inference, and local evaluation snapshots.
This view is limited to 50 files because it contains too many changes. See raw diff
- pipelines/Tr006_V2/README.md +129 -0
- pipelines/Tr006_V2/annotations/blue_screen/data/intervals.csv +324 -0
- pipelines/Tr006_V2/annotations/blue_screen/metadata/provenance.json +43 -0
- pipelines/Tr006_V2/annotations/blue_screen/metadata/summary.json +23 -0
- pipelines/Tr006_V2/annotations/blue_screen/metadata/validation.json +30 -0
- pipelines/Tr006_V2/annotations/blue_screen/metadata/videos.jsonl +0 -0
- pipelines/Tr006_V2/docker/segment-algorithm/.dockerignore +6 -0
- pipelines/Tr006_V2/docker/segment-algorithm/.gitignore +2 -0
- pipelines/Tr006_V2/docker/segment-algorithm/Dockerfile +102 -0
- pipelines/Tr006_V2/docker/segment-algorithm/do_build.sh +11 -0
- pipelines/Tr006_V2/docker/segment-algorithm/do_save.sh +50 -0
- pipelines/Tr006_V2/docker/segment-algorithm/do_test_run.sh +86 -0
- pipelines/Tr006_V2/docker/segment-algorithm/inference.py +393 -0
- pipelines/Tr006_V2/docker/segment-algorithm/requirements.txt +21 -0
- pipelines/Tr006_V2/docker/segment-algorithm/resources/README.md +43 -0
- pipelines/Tr006_V2/docker/segment-algorithm/resources/dummy_weights.pt +3 -0
- pipelines/Tr006_V2/docker/segment-algorithm/resources/model.py +20 -0
- pipelines/Tr006_V2/docker/segment-algorithm/resources/model_provenance.json +17 -0
- pipelines/Tr006_V2/docker/segment-algorithm/test/.gitignore +1 -0
- pipelines/Tr006_V2/expected_outputs/v1/provenance.json +59 -0
- pipelines/Tr006_V2/expected_outputs/v1/validation_report.json +11 -0
- pipelines/Tr006_V2/expected_outputs/v2/provenance.json +27 -0
- pipelines/Tr006_V2/expected_outputs/v2/validation_report.json +24 -0
- pipelines/Tr006_V2/manifest/pipeline_manifest.json +60 -0
- pipelines/Tr006_V2/manifest/pipeline_requirements.txt +15 -0
- pipelines/Tr006_V2/project/data_preprocessing/README.md +92 -0
- pipelines/Tr006_V2/project/data_preprocessing/process_blue_screen_dataset.py +294 -0
- pipelines/Tr006_V2/project/ms-swift/LICENSE +201 -0
- pipelines/Tr006_V2/project/ms-swift/MANIFEST.in +1 -0
- pipelines/Tr006_V2/project/ms-swift/requirements.txt +1 -0
- pipelines/Tr006_V2/project/ms-swift/requirements/framework.txt +39 -0
- pipelines/Tr006_V2/project/ms-swift/requirements/swanlab.txt +1 -0
- pipelines/Tr006_V2/project/ms-swift/setup.cfg +32 -0
- pipelines/Tr006_V2/project/ms-swift/setup.py +166 -0
- pipelines/Tr006_V2/project/ms-swift/swift/__init__.py +66 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/__init__.py +3 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/base.py +261 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/deepseek_v3_1.py +87 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/deepseek_v4.py +145 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/extra.py +36 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/gemma4.py +210 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/glm4.py +194 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/hermes.py +144 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/hy_v3.py +107 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/kimi_k25.py +119 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/kimi_k3.py +151 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/llama.py +74 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/mapping.py +63 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/minicpm5.py +114 -0
- pipelines/Tr006_V2/project/ms-swift/swift/agent_template/minimax_m2.py +171 -0
pipelines/Tr006_V2/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Tr006 V2 Segment pipeline bundle
|
| 2 |
+
|
| 3 |
+
这个目录对应 Hugging Face 仓库 `Gbone3176/interval` 中的
|
| 4 |
+
`pipelines/Tr006_V2/`。权重位于同仓库的
|
| 5 |
+
`weights/Tr006_V2/checkpoint-6013/`。
|
| 6 |
+
|
| 7 |
+
## 范围
|
| 8 |
+
|
| 9 |
+
包含 Tr006 V2 实际使用的完整处理链:
|
| 10 |
+
|
| 11 |
+
1. 原始 HeiCo 视频生成冻结蓝屏区间。
|
| 12 |
+
2. 官方原始 Segment parquet 和完整视频生成 V1 fixed80 数据。
|
| 13 |
+
3. 使用冻结蓝屏区间生成 V2 blue-clean fixed80 数据及 ms-swift `train.jsonl`。
|
| 14 |
+
4. ms-swift 绝对时间戳模板、训练入口和实际 Tr006 配置。
|
| 15 |
+
5. checkpoint-6013 LoRA merge。
|
| 16 |
+
6. Segment Docker 的预处理、推理及答案后处理。
|
| 17 |
+
7. 本地 vLLM 评测和时间容差复算脚本,仅用于本地核对;它们不是官方 evaluator。
|
| 18 |
+
|
| 19 |
+
不包含官方原始视频、官方 parquet、base model 或合并后的模型。Farm04 必须使用与原服务器相同版本的官方原始数据和同一个 Qwen3.5-9B base model。
|
| 20 |
+
|
| 21 |
+
## 原始数据目录契约
|
| 22 |
+
|
| 23 |
+
```text
|
| 24 |
+
RAW_ROOT/
|
| 25 |
+
├── heico-focus-vqa/
|
| 26 |
+
│ ├── data/segment/{train,test}.parquet
|
| 27 |
+
│ └── videos/*
|
| 28 |
+
└── lapchole-focus-vqa/
|
| 29 |
+
├── data/segment/{train,test}.parquet
|
| 30 |
+
└── videos/*
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
先用 `manifest/pipeline_manifest.json` 中的 parquet SHA256 核对版本。
|
| 34 |
+
|
| 35 |
+
## 从完全原始数据生成训练输入
|
| 36 |
+
|
| 37 |
+
### 1. 可重复生成蓝屏区间
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
python project/data_preprocessing/process_blue_screen_dataset.py \
|
| 41 |
+
--input-dir "$RAW_ROOT/heico-focus-vqa/videos" \
|
| 42 |
+
--output-dir "$WORK_ROOT/blue_screen_detected" \
|
| 43 |
+
--sample-seconds 1 \
|
| 44 |
+
--min-static-seconds 6 \
|
| 45 |
+
--blue-ratio 0.45 \
|
| 46 |
+
--pad-seconds 5 \
|
| 47 |
+
--coarse-size 96 \
|
| 48 |
+
--fine-size 160
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
Tr006 的精确复现应使用 bundle 内冻结的
|
| 52 |
+
`annotations/blue_screen/data/intervals.csv`。重新检测用于验证原始到标注的完整链路;只有生成文件的 SHA256 与 manifest 一致时,才可替代冻结文件。
|
| 53 |
+
|
| 54 |
+
### 2. 原始数据生成 V1 fixed80
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
python project/scripts/preprocess_segmentqa_fixed_frames.py \
|
| 58 |
+
--data-root "$RAW_ROOT" \
|
| 59 |
+
--output-dir "$WORK_ROOT/orena_segmentqa_fixed80_420x224_absmeta_v1/clean" \
|
| 60 |
+
--splits train test \
|
| 61 |
+
--frames 80 \
|
| 62 |
+
--width 420 \
|
| 63 |
+
--height 224 \
|
| 64 |
+
--workers 8
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### 3. V1 生成 Tr006 V2 训练数据
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
python project/scripts/preprocess_segmentqa_blueclean_v2.py \
|
| 71 |
+
--data-root "$RAW_ROOT" \
|
| 72 |
+
--v1-dir "$WORK_ROOT/orena_segmentqa_fixed80_420x224_absmeta_v1/clean" \
|
| 73 |
+
--blue-intervals "$BUNDLE_ROOT/annotations/blue_screen/data/intervals.csv" \
|
| 74 |
+
--output-dir "$WORK_ROOT/orena_segmentqa_fixed80_420x224_absmeta_v2/clean" \
|
| 75 |
+
--splits train test \
|
| 76 |
+
--frames 80 \
|
| 77 |
+
--width 420 \
|
| 78 |
+
--height 224 \
|
| 79 |
+
--workers 8 \
|
| 80 |
+
--min-train-valid-frames 16
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
验收值:18,478 个 segment、1,215 个蓝屏重叠 segment、13,746 条训练问题、6,254 条测试问题、80 帧、420x224、绝对毫秒 PTS。最终训练入口是 V2 目录中的 `clean/train.jsonl`。
|
| 84 |
+
|
| 85 |
+
`project/` 保留原项目相对布局;其中已经包含 Tr006 实际使用的
|
| 86 |
+
`ms-swift/swift/template/templates/qwen.py`。如果改用另一个 ms-swift checkout,必须把该文件覆盖到 commit
|
| 87 |
+
`b66edaeba3c484c431d785f95a7fc81cffa530c0` 的对应位置,否则预计算绝对时间戳不会进入 Qwen3VL processor。
|
| 88 |
+
|
| 89 |
+
## Merge checkpoint-6013
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
python project/scripts/merge_lora_model.py \
|
| 93 |
+
--base "$BASE_MODEL" \
|
| 94 |
+
--adapter "$HF_ROOT/weights/Tr006_V2/checkpoint-6013" \
|
| 95 |
+
--output "$OFFICIAL_SEGMENT_DIR/resources/model" \
|
| 96 |
+
--max-shard-size 4GB
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
验收 `resources/model/merge_report.json`:
|
| 100 |
+
|
| 101 |
+
- `remaining_lora_modules_after_merge == 0`
|
| 102 |
+
- language model、visual、visual merger 的代表权重 delta 均非零
|
| 103 |
+
- 输出含完整 processor 配置和 safetensors index
|
| 104 |
+
|
| 105 |
+
## 使用官方目录重新打包
|
| 106 |
+
|
| 107 |
+
继续使用官方原始 `do_build.sh`、`do_test_run.sh` 和 `do_save.sh`。把 bundle 中以下三个文件覆盖到官方 Segment 目录:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
cp docker/segment-algorithm/inference.py "$OFFICIAL_SEGMENT_DIR/inference.py"
|
| 111 |
+
cp docker/segment-algorithm/requirements.txt "$OFFICIAL_SEGMENT_DIR/requirements.txt"
|
| 112 |
+
cp docker/segment-algorithm/Dockerfile "$OFFICIAL_SEGMENT_DIR/Dockerfile"
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
完成 merge 后执行:
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
cd "$OFFICIAL_SEGMENT_DIR"
|
| 119 |
+
bash do_build.sh
|
| 120 |
+
bash do_test_run.sh
|
| 121 |
+
bash do_save.sh
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
不要使用当前线上版本的 `np.linspace` 推理预处理作为“已与 V2 对齐”的证据。必须先在公开样本上比较训练 V2 的采样 PTS、420x224 帧、processor 输入和 Docker 输入;`docker/segment-algorithm/inference.py` 是已提交版本的事实快照,保留它是为了复现和定位差异。
|
| 125 |
+
|
| 126 |
+
## 本地评测边界
|
| 127 |
+
|
| 128 |
+
`project/scripts/eval_segmentqa_vllm.py` 和
|
| 129 |
+
`project/scripts/evaluate_segmentqa_predictions.py` 使用本地 `correct()` 和时间容差代理,不是官方十桶 ID/OOD evaluator,也没有运行官方 LLM judge。禁止把其分数直接当成 leaderboard score。
|
pipelines/Tr006_V2/annotations/blue_screen/data/intervals.csv
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
video_id,video_filename,interval_index,start_frame,end_frame,start_seconds,end_seconds,duration_seconds,start_time,end_time,frame_count,mean_blue_ratio,confidence,exact_repeated_hash
|
| 2 |
+
0000,0000 - Heico - Prokto - 1.avi,1,5524,5938,220.96,237.56,16.6,00:03:40.960,00:03:57.560,415,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 3 |
+
0000,0000 - Heico - Prokto - 1.avi,2,9296,9548,371.84,381.96,10.12,00:06:11.840,00:06:21.960,253,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 4 |
+
0000,0000 - Heico - Prokto - 1.avi,3,10249,10494,409.96,419.8,9.84,00:06:49.960,00:06:59.800,246,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 5 |
+
0000,0000 - Heico - Prokto - 1.avi,4,105966,106309,4238.64,4252.4,13.76,01:10:38.640,01:10:52.400,344,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 6 |
+
0000,0000 - Heico - Prokto - 1.avi,5,153266,153973,6130.64,6158.96,28.32,01:42:10.640,01:42:38.960,708,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 7 |
+
0000,0000 - Heico - Prokto - 1.avi,6,216719,217948,8668.76,8717.96,49.2,02:24:28.760,02:25:17.960,1230,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 8 |
+
0000,0000 - Heico - Prokto - 1.avi,7,232574,311655,9302.96,12466.24,3163.28,02:35:02.960,03:27:46.240,79082,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 9 |
+
0001,0001 - Heico - Prokto - 2.avi,1,110844,111882,4433.76,4475.32,41.56,01:13:53.760,01:14:35.320,1039,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 10 |
+
0001,0001 - Heico - Prokto - 2.avi,2,112792,113563,4511.68,4542.56,30.88,01:15:11.680,01:15:42.560,772,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 11 |
+
0001,0001 - Heico - Prokto - 2.avi,3,114462,115183,4578.48,4607.36,28.88,01:16:18.480,01:16:47.360,722,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 12 |
+
0001,0001 - Heico - Prokto - 2.avi,4,137203,138843,5488.12,5553.76,65.64,01:31:28.120,01:32:33.760,1641,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 13 |
+
0001,0001 - Heico - Prokto - 2.avi,5,147830,148183,5913.2,5927.36,14.16,01:38:33.200,01:38:47.360,354,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 14 |
+
0001,0001 - Heico - Prokto - 2.avi,6,154539,155178,6181.56,6207.16,25.6,01:43:01.560,01:43:27.160,640,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 15 |
+
0001,0001 - Heico - Prokto - 2.avi,7,222864,223263,8914.56,8930.56,16.0,02:28:34.560,02:28:50.560,400,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 16 |
+
0001,0001 - Heico - Prokto - 2.avi,8,237013,316761,9480.52,12670.48,3189.96,02:38:00.520,03:31:10.480,79749,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 17 |
+
0002,0002 - Heico - Prokto - 3.avi,1,5009,5212,200.36,208.52,8.16,00:03:20.360,00:03:28.520,204,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 18 |
+
0002,0002 - Heico - Prokto - 3.avi,2,5326,5704,213.04,228.2,15.16,00:03:33.040,00:03:48.200,379,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 19 |
+
0002,0002 - Heico - Prokto - 3.avi,3,26052,27664,1042.08,1106.6,64.52,00:17:22.080,00:18:26.600,1613,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 20 |
+
0002,0002 - Heico - Prokto - 3.avi,4,115654,115924,4626.16,4637.0,10.84,01:17:06.160,01:17:17.000,271,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 21 |
+
0002,0002 - Heico - Prokto - 3.avi,5,136596,136848,5463.84,5473.96,10.12,01:31:03.840,01:31:13.960,253,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 22 |
+
0002,0002 - Heico - Prokto - 3.avi,6,144722,144905,5788.88,5796.24,7.36,01:36:28.880,01:36:36.240,184,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 23 |
+
0002,0002 - Heico - Prokto - 3.avi,7,159926,217858,6397.04,8714.36,2317.32,01:46:37.040,02:25:14.360,57933,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 24 |
+
0002,0002 - Heico - Prokto - 3.avi,8,227798,229000,9111.92,9160.04,48.12,02:31:51.920,02:32:40.040,1203,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 25 |
+
0002,0002 - Heico - Prokto - 3.avi,9,248690,251825,9947.6,10073.04,125.44,02:45:47.600,02:47:53.040,3136,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 26 |
+
0002,0002 - Heico - Prokto - 3.avi,10,252911,254146,10116.44,10165.88,49.44,02:48:36.440,02:49:25.880,1236,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 27 |
+
0002,0002 - Heico - Prokto - 3.avi,11,255848,256281,10233.92,10251.28,17.36,02:50:33.920,02:50:51.280,434,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 28 |
+
0003,0003 - Heico - Prokto - 4.avi,1,643,869,25.72,34.8,9.08,00:00:25.720,00:00:34.800,227,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 29 |
+
0003,0003 - Heico - Prokto - 4.avi,2,1267,1562,50.68,62.52,11.84,00:00:50.680,00:01:02.520,296,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 30 |
+
0003,0003 - Heico - Prokto - 4.avi,3,11885,12122,475.4,484.92,9.52,00:07:55.400,00:08:04.920,238,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 31 |
+
0003,0003 - Heico - Prokto - 4.avi,4,82913,83622,3316.52,3344.92,28.4,00:55:16.520,00:55:44.920,710,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 32 |
+
0003,0003 - Heico - Prokto - 4.avi,5,127912,128071,5116.48,5122.88,6.4,01:25:16.480,01:25:22.880,160,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 33 |
+
0003,0003 - Heico - Prokto - 4.avi,6,141561,200111,5662.44,8004.48,2342.04,01:34:22.440,02:13:24.480,58551,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 34 |
+
0003,0003 - Heico - Prokto - 4.avi,7,205346,206231,8213.84,8249.28,35.44,02:16:53.840,02:17:29.280,886,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 35 |
+
0003,0003 - Heico - Prokto - 4.avi,8,233424,237433,9336.96,9497.36,160.4,02:35:36.960,02:38:17.360,4010,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 36 |
+
0003,0003 - Heico - Prokto - 4.avi,9,254178,255182,10167.12,10207.32,40.2,02:49:27.120,02:50:07.320,1005,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 37 |
+
0003,0003 - Heico - Prokto - 4.avi,10,255278,255922,10211.12,10236.92,25.8,02:50:11.120,02:50:36.920,645,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 38 |
+
0004,0004 - Heico - Prokto - 5.avi,1,811,1005,32.44,40.24,7.8,00:00:32.440,00:00:40.240,195,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 39 |
+
0004,0004 - Heico - Prokto - 5.avi,2,2507,2827,100.28,113.12,12.84,00:01:40.280,00:01:53.120,321,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 40 |
+
0004,0004 - Heico - Prokto - 5.avi,3,2911,3179,116.44,127.2,10.76,00:01:56.440,00:02:07.200,269,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 41 |
+
0004,0004 - Heico - Prokto - 5.avi,4,5065,5270,202.6,210.84,8.24,00:03:22.600,00:03:30.840,206,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 42 |
+
0004,0004 - Heico - Prokto - 5.avi,5,5487,5670,219.48,226.84,7.36,00:03:39.480,00:03:46.840,184,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 43 |
+
0004,0004 - Heico - Prokto - 5.avi,6,153423,153582,6136.92,6143.32,6.4,01:42:16.920,01:42:23.320,160,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 44 |
+
0004,0004 - Heico - Prokto - 5.avi,7,166711,237718,6668.44,9508.76,2840.32,01:51:08.440,02:38:28.760,71008,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 45 |
+
0004,0004 - Heico - Prokto - 5.avi,8,274728,274887,10989.12,10995.52,6.4,03:03:09.120,03:03:15.520,160,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 46 |
+
0004,0004 - Heico - Prokto - 5.avi,9,279748,280329,11189.92,11213.2,23.28,03:06:29.920,03:06:53.200,582,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 47 |
+
0004,0004 - Heico - Prokto - 5.avi,10,281024,281672,11240.96,11266.92,25.96,03:07:20.960,03:07:46.920,649,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 48 |
+
0004,0004 - Heico - Prokto - 5.avi,11,295193,298502,11807.72,11940.12,132.4,03:16:47.720,03:19:00.120,3310,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 49 |
+
0004,0004 - Heico - Prokto - 5.avi,12,299039,299984,11961.56,11999.4,37.84,03:19:21.560,03:19:59.400,946,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 50 |
+
0004,0004 - Heico - Prokto - 5.avi,13,301545,302528,12061.8,12101.16,39.36,03:21:01.800,03:21:41.160,984,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 51 |
+
0004,0004 - Heico - Prokto - 5.avi,14,304448,305829,12177.92,12233.2,55.28,03:22:57.920,03:23:53.200,1382,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 52 |
+
0004,0004 - Heico - Prokto - 5.avi,15,306273,312447,12250.92,12497.92,247.0,03:24:10.920,03:28:17.920,6175,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 53 |
+
0004,0004 - Heico - Prokto - 5.avi,16,313573,313830,12542.92,12553.24,10.32,03:29:02.920,03:29:13.240,258,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 54 |
+
0004,0004 - Heico - Prokto - 5.avi,17,315328,315503,12613.12,12620.16,7.04,03:30:13.120,03:30:20.160,176,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 55 |
+
0005,0005 - Heico - Prokto - 6.avi,1,63,977,2.52,39.12,36.6,00:00:02.520,00:00:39.120,915,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 56 |
+
0005,0005 - Heico - Prokto - 6.avi,2,2600,2834,104.0,113.4,9.4,00:01:44.000,00:01:53.400,235,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 57 |
+
0005,0005 - Heico - Prokto - 6.avi,3,88472,88932,3538.88,3557.32,18.44,00:58:58.880,00:59:17.320,461,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 58 |
+
0005,0005 - Heico - Prokto - 6.avi,4,89225,89747,3569.0,3589.92,20.92,00:59:29.000,00:59:49.920,523,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 59 |
+
0005,0005 - Heico - Prokto - 6.avi,5,89971,90170,3598.84,3606.84,8.0,00:59:58.840,01:00:06.840,200,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 60 |
+
0005,0005 - Heico - Prokto - 6.avi,6,117330,166097,4693.2,6643.92,1950.72,01:18:13.200,01:50:43.920,48768,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 61 |
+
0005,0005 - Heico - Prokto - 6.avi,7,168608,168844,6744.32,6753.8,9.48,01:52:24.320,01:52:33.800,237,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 62 |
+
0005,0005 - Heico - Prokto - 6.avi,8,184972,185273,7398.88,7410.96,12.08,02:03:18.880,02:03:30.960,302,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 63 |
+
0005,0005 - Heico - Prokto - 6.avi,9,199269,199539,7970.76,7981.6,10.84,02:12:50.760,02:13:01.600,271,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 64 |
+
0006,0006 - Heico - Prokto - 7.avi,1,54370,54766,2174.8,2190.68,15.88,00:36:14.800,00:36:30.680,397,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 65 |
+
0006,0006 - Heico - Prokto - 7.avi,2,68517,68799,2740.68,2752.0,11.32,00:45:40.680,00:45:52.000,283,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 66 |
+
0006,0006 - Heico - Prokto - 7.avi,3,131005,192543,5240.2,7701.76,2461.56,01:27:20.200,02:08:21.760,61539,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 67 |
+
0006,0006 - Heico - Prokto - 7.avi,4,192597,192916,7703.88,7716.68,12.8,02:08:23.880,02:08:36.680,320,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 68 |
+
0006,0006 - Heico - Prokto - 7.avi,5,196217,197106,7848.68,7884.28,35.6,02:10:48.680,02:11:24.280,890,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 69 |
+
0006,0006 - Heico - Prokto - 7.avi,6,198352,198542,7934.08,7941.72,7.64,02:12:14.080,02:12:21.720,191,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 70 |
+
0006,0006 - Heico - Prokto - 7.avi,7,204030,204189,8161.2,8167.6,6.4,02:16:01.200,02:16:07.600,160,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 71 |
+
0006,0006 - Heico - Prokto - 7.avi,8,222489,222898,8899.56,8915.96,16.4,02:28:19.560,02:28:35.960,410,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 72 |
+
0006,0006 - Heico - Prokto - 7.avi,9,239305,239671,9572.2,9586.88,14.68,02:39:32.200,02:39:46.880,367,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 73 |
+
0006,0006 - Heico - Prokto - 7.avi,10,241199,241485,9647.96,9659.44,11.48,02:40:47.960,02:40:59.440,287,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 74 |
+
0006,0006 - Heico - Prokto - 7.avi,11,247032,252252,9881.28,10090.12,208.84,02:44:41.280,02:48:10.120,5221,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 75 |
+
0007,0007 - Heico - Prokto - 8.avi,1,2119,2344,84.76,93.8,9.04,00:01:24.760,00:01:33.800,226,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 76 |
+
0007,0007 - Heico - Prokto - 8.avi,2,2520,2930,100.8,117.24,16.44,00:01:40.800,00:01:57.240,411,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 77 |
+
0007,0007 - Heico - Prokto - 8.avi,3,77279,77433,3091.16,3097.36,6.2,00:51:31.160,00:51:37.360,155,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 78 |
+
0007,0007 - Heico - Prokto - 8.avi,4,125114,126789,5004.56,5071.6,67.04,01:23:24.560,01:24:31.600,1676,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 79 |
+
0007,0007 - Heico - Prokto - 8.avi,5,150927,209117,6037.08,8364.72,2327.64,01:40:37.080,02:19:24.720,58191,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 80 |
+
0007,0007 - Heico - Prokto - 8.avi,6,209372,209595,8374.88,8383.84,8.96,02:19:34.880,02:19:43.840,224,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 81 |
+
0007,0007 - Heico - Prokto - 8.avi,7,237291,237838,9491.64,9513.56,21.92,02:38:11.640,02:38:33.560,548,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 82 |
+
0007,0007 - Heico - Prokto - 8.avi,8,238028,238568,9521.12,9542.76,21.64,02:38:41.120,02:39:02.760,541,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 83 |
+
0007,0007 - Heico - Prokto - 8.avi,9,238736,238982,9549.44,9559.32,9.88,02:39:09.440,02:39:19.320,247,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 84 |
+
0007,0007 - Heico - Prokto - 8.avi,10,252643,254287,10105.72,10171.52,65.8,02:48:25.720,02:49:31.520,1645,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 85 |
+
0008,0008 - Heico - Prokto - 9.avi,1,2459,2677,98.36,107.12,8.76,00:01:38.360,00:01:47.120,219,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 86 |
+
0008,0008 - Heico - Prokto - 9.avi,2,2871,3130,114.84,125.24,10.4,00:01:54.840,00:02:05.240,260,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 87 |
+
0008,0008 - Heico - Prokto - 9.avi,3,5009,5173,200.36,206.96,6.6,00:03:20.360,00:03:26.960,165,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 88 |
+
0008,0008 - Heico - Prokto - 9.avi,4,5444,5697,217.76,227.92,10.16,00:03:37.760,00:03:47.920,254,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 89 |
+
0008,0008 - Heico - Prokto - 9.avi,5,166621,237773,6664.84,9510.96,2846.12,01:51:04.840,02:38:30.960,71153,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 90 |
+
0008,0008 - Heico - Prokto - 9.avi,6,238250,238399,9530.0,9536.0,6.0,02:38:50.000,02:38:56.000,150,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 91 |
+
0008,0008 - Heico - Prokto - 9.avi,7,279716,280051,11188.64,11202.08,13.44,03:06:28.640,03:06:42.080,336,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 92 |
+
0008,0008 - Heico - Prokto - 9.avi,8,280996,281540,11239.84,11261.64,21.8,03:07:19.840,03:07:41.640,545,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 93 |
+
0008,0008 - Heico - Prokto - 9.avi,9,295157,298333,11806.28,11933.36,127.08,03:16:46.280,03:18:53.360,3177,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 94 |
+
0008,0008 - Heico - Prokto - 9.avi,10,299481,299873,11979.24,11994.96,15.72,03:19:39.240,03:19:54.960,393,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 95 |
+
0008,0008 - Heico - Prokto - 9.avi,11,301505,302399,12060.2,12096.0,35.8,03:21:00.200,03:21:36.000,895,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 96 |
+
0008,0008 - Heico - Prokto - 9.avi,12,304394,305695,12175.76,12227.84,52.08,03:22:55.760,03:23:47.840,1302,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 97 |
+
0008,0008 - Heico - Prokto - 9.avi,13,306196,310438,12247.84,12417.56,169.72,03:24:07.840,03:26:57.560,4243,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 98 |
+
0008,0008 - Heico - Prokto - 9.avi,14,313476,313750,12539.04,12550.04,11.0,03:28:59.040,03:29:10.040,275,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 99 |
+
0008,0008 - Heico - Prokto - 9.avi,15,315281,315430,12611.24,12617.24,6.0,03:30:11.240,03:30:17.240,150,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 100 |
+
0009,0009 - Heico - Prokto - 10.avi,1,7060,8007,282.4,320.32,37.92,00:04:42.400,00:05:20.320,948,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 101 |
+
0009,0009 - Heico - Prokto - 10.avi,2,8244,8518,329.76,340.76,11.0,00:05:29.760,00:05:40.760,275,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 102 |
+
0009,0009 - Heico - Prokto - 10.avi,3,10905,11723,436.2,468.96,32.76,00:07:16.200,00:07:48.960,819,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 103 |
+
0009,0009 - Heico - Prokto - 10.avi,4,12679,12928,507.16,517.16,10.0,00:08:27.160,00:08:37.160,250,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 104 |
+
0009,0009 - Heico - Prokto - 10.avi,5,13404,13908,536.16,556.36,20.2,00:08:56.160,00:09:16.360,505,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 105 |
+
0009,0009 - Heico - Prokto - 10.avi,6,53068,58670,2122.72,2346.84,224.12,00:35:22.720,00:39:06.840,5603,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 106 |
+
0009,0009 - Heico - Prokto - 10.avi,7,63976,65220,2559.04,2608.84,49.8,00:42:39.040,00:43:28.840,1245,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 107 |
+
0009,0009 - Heico - Prokto - 10.avi,8,65405,65853,2616.2,2634.16,17.96,00:43:36.200,00:43:54.160,449,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 108 |
+
0009,0009 - Heico - Prokto - 10.avi,9,66390,67030,2655.6,2681.24,25.64,00:44:15.600,00:44:41.240,641,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 109 |
+
0009,0009 - Heico - Prokto - 10.avi,10,85249,86243,3409.96,3449.76,39.8,00:56:49.960,00:57:29.760,995,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 110 |
+
0009,0009 - Heico - Prokto - 10.avi,11,91754,92359,3670.16,3694.4,24.24,01:01:10.160,01:01:34.400,606,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 111 |
+
0009,0009 - Heico - Prokto - 10.avi,12,92636,93048,3705.44,3721.96,16.52,01:01:45.440,01:02:01.960,413,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 112 |
+
0009,0009 - Heico - Prokto - 10.avi,13,94012,94245,3760.48,3769.84,9.36,01:02:40.480,01:02:49.840,234,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 113 |
+
0009,0009 - Heico - Prokto - 10.avi,14,125101,126017,5004.04,5040.72,36.68,01:23:24.040,01:24:00.720,917,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 114 |
+
0009,0009 - Heico - Prokto - 10.avi,15,126104,126615,5044.16,5064.64,20.48,01:24:04.160,01:24:24.640,512,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 115 |
+
0009,0009 - Heico - Prokto - 10.avi,16,145892,146607,5835.68,5864.32,28.64,01:37:15.680,01:37:44.320,716,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 116 |
+
0009,0009 - Heico - Prokto - 10.avi,17,147498,148638,5899.92,5945.56,45.64,01:38:19.920,01:39:05.560,1141,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 117 |
+
0009,0009 - Heico - Prokto - 10.avi,18,151144,153672,6045.76,6146.92,101.16,01:40:45.760,01:42:26.920,2529,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 118 |
+
0009,0009 - Heico - Prokto - 10.avi,19,153819,154897,6152.76,6195.92,43.16,01:42:32.760,01:43:15.920,1079,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 119 |
+
0009,0009 - Heico - Prokto - 10.avi,20,156864,157369,6274.56,6294.8,20.24,01:44:34.560,01:44:54.800,506,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 120 |
+
0009,0009 - Heico - Prokto - 10.avi,21,158007,158420,6320.28,6336.84,16.56,01:45:20.280,01:45:36.840,414,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 121 |
+
0009,0009 - Heico - Prokto - 10.avi,22,171517,171713,6860.68,6868.56,7.88,01:54:20.680,01:54:28.560,197,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 122 |
+
0009,0009 - Heico - Prokto - 10.avi,23,185776,187204,7431.04,7488.2,57.16,02:03:51.040,02:04:48.200,1429,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 123 |
+
0009,0009 - Heico - Prokto - 10.avi,24,187469,187733,7498.76,7509.36,10.6,02:04:58.760,02:05:09.360,265,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 124 |
+
0009,0009 - Heico - Prokto - 10.avi,25,206430,207747,8257.2,8309.92,52.72,02:17:37.200,02:18:29.920,1318,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 125 |
+
0009,0009 - Heico - Prokto - 10.avi,26,207944,208182,8317.76,8327.32,9.56,02:18:37.760,02:18:47.320,239,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 126 |
+
0009,0009 - Heico - Prokto - 10.avi,27,214536,214878,8581.44,8595.16,13.72,02:23:01.440,02:23:15.160,343,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 127 |
+
0009,0009 - Heico - Prokto - 10.avi,28,214972,215438,8598.88,8617.56,18.68,02:23:18.880,02:23:37.560,467,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 128 |
+
0009,0009 - Heico - Prokto - 10.avi,29,216309,216908,8652.36,8676.36,24.0,02:24:12.360,02:24:36.360,600,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 129 |
+
0009,0009 - Heico - Prokto - 10.avi,30,219259,219518,8770.36,8780.76,10.4,02:26:10.360,02:26:20.760,260,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 130 |
+
0009,0009 - Heico - Prokto - 10.avi,31,223627,223803,8945.08,8952.16,7.08,02:29:05.080,02:29:12.160,177,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 131 |
+
0009,0009 - Heico - Prokto - 10.avi,32,229414,229663,9176.56,9186.56,10.0,02:32:56.560,02:33:06.560,250,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 132 |
+
0009,0009 - Heico - Prokto - 10.avi,33,280676,283232,11227.04,11329.32,102.28,03:07:07.040,03:08:49.320,2557,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 133 |
+
0009,0009 - Heico - Prokto - 10.avi,34,308970,373914,12358.8,14956.6,2597.8,03:25:58.800,04:09:16.600,64945,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 134 |
+
0009,0009 - Heico - Prokto - 10.avi,35,374164,374543,14966.56,14981.76,15.2,04:09:26.560,04:09:41.760,380,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 135 |
+
0009,0009 - Heico - Prokto - 10.avi,36,387421,388107,15496.84,15524.32,27.48,04:18:16.840,04:18:44.320,687,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 136 |
+
0009,0009 - Heico - Prokto - 10.avi,37,404192,404443,16167.68,16177.76,10.08,04:29:27.680,04:29:37.760,252,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 137 |
+
0009,0009 - Heico - Prokto - 10.avi,38,407105,407310,16284.2,16292.44,8.24,04:31:24.200,04:31:32.440,206,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 138 |
+
0009,0009 - Heico - Prokto - 10.avi,39,432003,441298,17280.12,17651.96,371.84,04:48:00.120,04:54:11.960,9296,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 139 |
+
0009,0009 - Heico - Prokto - 10.avi,40,442866,443022,17714.64,17720.92,6.28,04:55:14.640,04:55:20.920,157,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 140 |
+
0010,0010 - Heico - Rektum - 1.avi,1,41235,41710,1649.4,1668.44,19.04,00:27:29.400,00:27:48.440,476,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 141 |
+
0010,0010 - Heico - Rektum - 1.avi,2,49196,50101,1967.84,2004.08,36.24,00:32:47.840,00:33:24.080,906,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 142 |
+
0010,0010 - Heico - Rektum - 1.avi,3,157223,158078,6288.92,6323.16,34.24,01:44:48.920,01:45:23.160,856,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 143 |
+
0010,0010 - Heico - Rektum - 1.avi,4,160177,161891,6407.08,6475.68,68.6,01:46:47.080,01:47:55.680,1715,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 144 |
+
0010,0010 - Heico - Rektum - 1.avi,5,165333,165802,6613.32,6632.12,18.8,01:50:13.320,01:50:32.120,470,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 145 |
+
0010,0010 - Heico - Rektum - 1.avi,6,265363,265733,10614.52,10629.36,14.84,02:56:54.520,02:57:09.360,371,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 146 |
+
0010,0010 - Heico - Rektum - 1.avi,7,265907,336405,10636.28,13456.24,2819.96,02:57:16.280,03:44:16.240,70499,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 147 |
+
0010,0010 - Heico - Rektum - 1.avi,8,354902,355116,14196.08,14204.68,8.6,03:56:36.080,03:56:44.680,215,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 148 |
+
0010,0010 - Heico - Rektum - 1.avi,9,371415,371767,14856.6,14870.72,14.12,04:07:36.600,04:07:50.720,353,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 149 |
+
0010,0010 - Heico - Rektum - 1.avi,10,373541,373981,14941.64,14959.28,17.64,04:09:01.640,04:09:19.280,441,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 150 |
+
0010,0010 - Heico - Rektum - 1.avi,11,376427,376726,15057.08,15069.08,12.0,04:10:57.080,04:11:09.080,300,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 151 |
+
0010,0010 - Heico - Rektum - 1.avi,12,397827,398219,15913.08,15928.8,15.72,04:25:13.080,04:25:28.800,393,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 152 |
+
0010,0010 - Heico - Rektum - 1.avi,13,421164,429147,16846.56,17165.92,319.36,04:40:46.560,04:46:05.920,7984,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 153 |
+
0011,0011 - Heico - Rektum - 2.avi,1,7043,7277,281.72,291.12,9.4,00:04:41.720,00:04:51.120,235,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 154 |
+
0011,0011 - Heico - Rektum - 2.avi,2,176925,177260,7077.0,7090.44,13.44,01:57:57.000,01:58:10.440,336,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 155 |
+
0011,0011 - Heico - Rektum - 2.avi,3,183595,232058,7343.8,9282.36,1938.56,02:02:23.800,02:34:42.360,48464,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 156 |
+
0011,0011 - Heico - Rektum - 2.avi,4,239127,240057,9565.08,9602.32,37.24,02:39:25.080,02:40:02.320,931,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 157 |
+
0011,0011 - Heico - Rektum - 2.avi,5,240627,241173,9625.08,9646.96,21.88,02:40:25.080,02:40:46.960,547,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 158 |
+
0011,0011 - Heico - Rektum - 2.avi,6,247248,247415,9889.92,9896.64,6.72,02:44:49.920,02:44:56.640,168,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 159 |
+
0011,0011 - Heico - Rektum - 2.avi,7,251653,254734,10066.12,10189.4,123.28,02:47:46.120,02:49:49.400,3082,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 160 |
+
0011,0011 - Heico - Rektum - 2.avi,8,256223,256422,10248.92,10256.92,8.0,02:50:48.920,02:50:56.920,200,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 161 |
+
0011,0011 - Heico - Rektum - 2.avi,9,266223,266459,10648.92,10658.4,9.48,02:57:28.920,02:57:38.400,237,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 162 |
+
0011,0011 - Heico - Rektum - 2.avi,10,309058,309278,12362.32,12371.16,8.84,03:26:02.320,03:26:11.160,221,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 163 |
+
0011,0011 - Heico - Rektum - 2.avi,11,330794,331069,13231.76,13242.8,11.04,03:40:31.760,03:40:42.800,276,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 164 |
+
0012,0012 - Heico - Rektum - 3.avi,1,18110,18492,724.4,739.72,15.32,00:12:04.400,00:12:19.720,383,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 165 |
+
0012,0012 - Heico - Rektum - 3.avi,2,150983,191811,6039.32,7672.48,1633.16,01:40:39.320,02:07:52.480,40829,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 166 |
+
0012,0012 - Heico - Rektum - 3.avi,3,211660,211895,8466.4,8475.84,9.44,02:21:06.400,02:21:15.840,236,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 167 |
+
0013,0013 - Heico - Rektum - 4.avi,1,8189,8713,327.56,348.56,21.0,00:05:27.560,00:05:48.560,525,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 168 |
+
0013,0013 - Heico - Rektum - 4.avi,2,9174,9696,366.96,387.88,20.92,00:06:06.960,00:06:27.880,523,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 169 |
+
0013,0013 - Heico - Rektum - 4.avi,3,12253,13148,490.12,525.96,35.84,00:08:10.120,00:08:45.960,896,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 170 |
+
0013,0013 - Heico - Rektum - 4.avi,4,16396,16578,655.84,663.16,7.32,00:10:55.840,00:11:03.160,183,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 171 |
+
0013,0013 - Heico - Rektum - 4.avi,5,164401,164911,6576.04,6596.48,20.44,01:49:36.040,01:49:56.480,511,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 172 |
+
0013,0013 - Heico - Rektum - 4.avi,6,168791,208270,6751.64,8330.84,1579.2,01:52:31.640,02:18:50.840,39480,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 173 |
+
0014,0014 - Heico - Rektum - 5.avi,1,1061,1842,42.44,73.72,31.28,00:00:42.440,00:01:13.720,782,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 174 |
+
0014,0014 - Heico - Rektum - 5.avi,2,7887,8186,315.48,327.48,12.0,00:05:15.480,00:05:27.480,300,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 175 |
+
0014,0014 - Heico - Rektum - 5.avi,3,9287,9578,371.48,383.16,11.68,00:06:11.480,00:06:23.160,292,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 176 |
+
0014,0014 - Heico - Rektum - 5.avi,4,12362,12576,494.48,503.08,8.6,00:08:14.480,00:08:23.080,215,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 177 |
+
0014,0014 - Heico - Rektum - 5.avi,5,12932,13344,517.28,533.8,16.52,00:08:37.280,00:08:53.800,413,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 178 |
+
0014,0014 - Heico - Rektum - 5.avi,6,14932,15101,597.28,604.08,6.8,00:09:57.280,00:10:04.080,170,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 179 |
+
0014,0014 - Heico - Rektum - 5.avi,7,20627,20781,825.08,831.28,6.2,00:13:45.080,00:13:51.280,155,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 180 |
+
0014,0014 - Heico - Rektum - 5.avi,8,46852,47131,1874.08,1885.28,11.2,00:31:14.080,00:31:25.280,280,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 181 |
+
0014,0014 - Heico - Rektum - 5.avi,9,83957,84631,3358.28,3385.28,27.0,00:55:58.280,00:56:25.280,675,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 182 |
+
0014,0014 - Heico - Rektum - 5.avi,10,152532,152808,6101.28,6112.36,11.08,01:41:41.280,01:41:52.360,277,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 183 |
+
0014,0014 - Heico - Rektum - 5.avi,11,159772,159947,6390.88,6397.92,7.04,01:46:30.880,01:46:37.920,176,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 184 |
+
0014,0014 - Heico - Rektum - 5.avi,12,161457,161771,6458.28,6470.88,12.6,01:47:38.280,01:47:50.880,315,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 185 |
+
0014,0014 - Heico - Rektum - 5.avi,13,164407,164931,6576.28,6597.28,21.0,01:49:36.280,01:49:57.280,525,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 186 |
+
0014,0014 - Heico - Rektum - 5.avi,14,165100,165406,6604.0,6616.28,12.28,01:50:04.000,01:50:16.280,307,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 187 |
+
0014,0014 - Heico - Rektum - 5.avi,15,170881,171031,6835.24,6841.28,6.04,01:53:55.240,01:54:01.280,151,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 188 |
+
0014,0014 - Heico - Rektum - 5.avi,16,214275,256687,8571.0,10267.52,1696.52,02:22:51.000,02:51:07.520,42413,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 189 |
+
0014,0014 - Heico - Rektum - 5.avi,17,269272,270063,10770.88,10802.56,31.68,02:59:30.880,03:00:02.560,792,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 190 |
+
0014,0014 - Heico - Rektum - 5.avi,18,286533,300706,11461.32,12028.28,566.96,03:11:01.320,03:20:28.280,14174,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 191 |
+
0015,0015 - Heico - Rektum - 6.avi,1,98,871,3.92,34.88,30.96,00:00:03.920,00:00:34.880,774,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 192 |
+
0015,0015 - Heico - Rektum - 6.avi,2,1030,1304,41.2,52.2,11.0,00:00:41.200,00:00:52.200,275,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 193 |
+
0015,0015 - Heico - Rektum - 6.avi,3,4210,4569,168.4,182.8,14.4,00:02:48.400,00:03:02.800,360,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 194 |
+
0015,0015 - Heico - Rektum - 6.avi,4,24926,25082,997.04,1003.32,6.28,00:16:37.040,00:16:43.320,157,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 195 |
+
0015,0015 - Heico - Rektum - 6.avi,5,25652,26954,1026.08,1078.2,52.12,00:17:06.080,00:17:58.200,1303,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 196 |
+
0015,0015 - Heico - Rektum - 6.avi,6,71242,71463,2849.68,2858.56,8.88,00:47:29.680,00:47:38.560,222,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 197 |
+
0015,0015 - Heico - Rektum - 6.avi,7,72854,73042,2914.16,2921.72,7.56,00:48:34.160,00:48:41.720,189,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 198 |
+
0015,0015 - Heico - Rektum - 6.avi,8,118161,118453,4726.44,4738.16,11.72,01:18:46.440,01:18:58.160,293,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 199 |
+
0015,0015 - Heico - Rektum - 6.avi,9,154118,154421,6164.72,6176.88,12.16,01:42:44.720,01:42:56.880,304,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 200 |
+
0015,0015 - Heico - Rektum - 6.avi,10,225395,225584,9015.8,9023.4,7.6,02:30:15.800,02:30:23.400,190,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 201 |
+
0015,0015 - Heico - Rektum - 6.avi,11,238226,238394,9529.04,9535.8,6.76,02:38:49.040,02:38:55.800,169,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 202 |
+
0015,0015 - Heico - Rektum - 6.avi,12,239171,239361,9566.84,9574.48,7.64,02:39:26.840,02:39:34.480,191,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 203 |
+
0015,0015 - Heico - Rektum - 6.avi,13,252669,252901,10106.76,10116.08,9.32,02:48:26.760,02:48:36.080,233,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 204 |
+
0015,0015 - Heico - Rektum - 6.avi,14,254945,369078,10197.8,14763.16,4565.36,02:49:57.800,04:06:03.160,114134,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 205 |
+
0015,0015 - Heico - Rektum - 6.avi,15,380814,381016,15232.56,15240.68,8.12,04:13:52.560,04:14:00.680,203,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 206 |
+
0016,0016 - Heico - Rektum - 7.avi,1,286,504,11.44,20.2,8.76,00:00:11.440,00:00:20.200,219,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 207 |
+
0016,0016 - Heico - Rektum - 7.avi,2,1535,2679,61.4,107.2,45.8,00:01:01.400,00:01:47.200,1145,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 208 |
+
0016,0016 - Heico - Rektum - 7.avi,3,3366,3528,134.64,141.16,6.52,00:02:14.640,00:02:21.160,163,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 209 |
+
0016,0016 - Heico - Rektum - 7.avi,4,76469,77327,3058.76,3093.12,34.36,00:50:58.760,00:51:33.120,859,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 210 |
+
0016,0016 - Heico - Rektum - 7.avi,5,111617,111991,4464.68,4479.68,15.0,01:14:24.680,01:14:39.680,375,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 211 |
+
0016,0016 - Heico - Rektum - 7.avi,6,176683,226219,7067.32,9048.8,1981.48,01:57:47.320,02:30:48.800,49537,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 212 |
+
0016,0016 - Heico - Rektum - 7.avi,7,230857,231447,9234.28,9257.92,23.64,02:33:54.280,02:34:17.920,591,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 213 |
+
0016,0016 - Heico - Rektum - 7.avi,8,245796,246039,9831.84,9841.6,9.76,02:43:51.840,02:44:01.600,244,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 214 |
+
0016,0016 - Heico - Rektum - 7.avi,9,256669,257081,10266.76,10283.28,16.52,02:51:06.760,02:51:23.280,413,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 215 |
+
0016,0016 - Heico - Rektum - 7.avi,10,262393,262789,10495.72,10511.6,15.88,02:54:55.720,02:55:11.600,397,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 216 |
+
0016,0016 - Heico - Rektum - 7.avi,11,273830,274150,10953.2,10966.04,12.84,03:02:33.200,03:02:46.040,321,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 217 |
+
0016,0016 - Heico - Rektum - 7.avi,12,275856,276843,11034.24,11073.76,39.52,03:03:54.240,03:04:33.760,988,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 218 |
+
0016,0016 - Heico - Rektum - 7.avi,13,306912,317030,12276.48,12681.24,404.76,03:24:36.480,03:31:21.240,10119,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 219 |
+
0016,0016 - Heico - Rektum - 7.avi,14,325605,337482,13024.2,13499.32,475.12,03:37:04.200,03:44:59.320,11878,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 220 |
+
0016,0016 - Heico - Rektum - 7.avi,15,337785,349126,13511.4,13965.08,453.68,03:45:11.400,03:52:45.080,11342,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 221 |
+
0016,0016 - Heico - Rektum - 7.avi,16,380708,381065,15228.32,15242.64,14.32,04:13:48.320,04:14:02.640,358,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 222 |
+
0017,0017 - Heico - Rektum - 8.avi,1,3448,3630,137.92,145.24,7.32,00:02:17.920,00:02:25.240,183,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 223 |
+
0017,0017 - Heico - Rektum - 8.avi,2,3746,4105,149.84,164.24,14.4,00:02:29.840,00:02:44.240,360,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 224 |
+
0017,0017 - Heico - Rektum - 8.avi,3,5012,5189,200.48,207.6,7.12,00:03:20.480,00:03:27.600,178,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 225 |
+
0017,0017 - Heico - Rektum - 8.avi,4,10783,11192,431.32,447.72,16.4,00:07:11.320,00:07:27.720,410,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 226 |
+
0017,0017 - Heico - Rektum - 8.avi,5,143141,185332,5725.64,7413.32,1687.68,01:35:25.640,02:03:33.320,42192,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 227 |
+
0017,0017 - Heico - Rektum - 8.avi,6,210088,210262,8403.52,8410.52,7.0,02:20:03.520,02:20:10.520,175,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 228 |
+
0018,0018 - Heico - Rektum - 9.avi,1,4605,6290,184.2,251.64,67.44,00:03:04.200,00:04:11.640,1686,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 229 |
+
0018,0018 - Heico - Rektum - 9.avi,2,196494,196665,7859.76,7866.64,6.88,02:10:59.760,02:11:06.640,172,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 230 |
+
0018,0018 - Heico - Rektum - 9.avi,3,207338,260298,8293.52,10411.96,2118.44,02:18:13.520,02:53:31.960,52961,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 231 |
+
0018,0018 - Heico - Rektum - 9.avi,4,267668,268079,10706.72,10723.2,16.48,02:58:26.720,02:58:43.200,412,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 232 |
+
0018,0018 - Heico - Rektum - 9.avi,5,269857,270216,10794.28,10808.68,14.4,02:59:54.280,03:00:08.680,360,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 233 |
+
0018,0018 - Heico - Rektum - 9.avi,6,345674,346337,13826.96,13853.52,26.56,03:50:26.960,03:50:53.520,664,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 234 |
+
0019,0019 - Heico - Rektum - 10.avi,1,88458,88727,3538.32,3549.12,10.8,00:58:58.320,00:59:09.120,270,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 235 |
+
0019,0019 - Heico - Rektum - 10.avi,2,101634,155868,4065.36,6234.76,2169.4,01:07:45.360,01:43:54.760,54235,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 236 |
+
0019,0019 - Heico - Rektum - 10.avi,3,157074,157625,6282.96,6305.04,22.08,01:44:42.960,01:45:05.040,552,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 237 |
+
0020,0020 - Heico - Sigma - 1.avi,1,76741,120650,3069.64,4826.04,1756.4,00:51:09.640,01:20:26.040,43910,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 238 |
+
0021,0021 - Heico - Sigma - 2.avi,1,1403,1602,56.12,64.12,8.0,00:00:56.120,00:01:04.120,200,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 239 |
+
0021,0021 - Heico - Sigma - 2.avi,2,75202,129434,3008.08,5177.4,2169.32,00:50:08.080,01:26:17.400,54233,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 240 |
+
0021,0021 - Heico - Sigma - 2.avi,3,142160,142314,5686.4,5692.6,6.2,01:34:46.400,01:34:52.600,155,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 241 |
+
0021,0021 - Heico - Sigma - 2.avi,4,145460,145669,5818.4,5826.8,8.4,01:36:58.400,01:37:06.800,210,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 242 |
+
0022,0022 - Heico - Sigma - 3.avi,1,4196,4772,167.84,190.92,23.08,00:02:47.840,00:03:10.920,577,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 243 |
+
0022,0022 - Heico - Sigma - 3.avi,2,58606,59151,2344.24,2366.08,21.84,00:39:04.240,00:39:26.080,546,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 244 |
+
0022,0022 - Heico - Sigma - 3.avi,3,108376,109561,4335.04,4382.48,47.44,01:12:15.040,01:13:02.480,1186,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 245 |
+
0022,0022 - Heico - Sigma - 3.avi,4,150566,151065,6022.64,6042.64,20.0,01:40:22.640,01:40:42.640,500,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 246 |
+
0022,0022 - Heico - Sigma - 3.avi,5,213564,213819,8542.56,8552.8,10.24,02:22:22.560,02:22:32.800,256,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 247 |
+
0022,0022 - Heico - Sigma - 3.avi,6,213927,214506,8557.08,8580.28,23.2,02:22:37.080,02:23:00.280,580,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 248 |
+
0022,0022 - Heico - Sigma - 3.avi,7,235981,292296,9439.24,11691.88,2252.64,02:37:19.240,03:14:51.880,56316,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 249 |
+
0022,0022 - Heico - Sigma - 3.avi,8,308248,308950,12329.92,12358.04,28.12,03:25:29.920,03:25:58.040,703,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 250 |
+
0022,0022 - Heico - Sigma - 3.avi,9,313811,314209,12552.44,12568.4,15.96,03:29:12.440,03:29:28.400,399,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 251 |
+
0022,0022 - Heico - Sigma - 3.avi,10,331328,332053,13253.12,13282.16,29.04,03:40:53.120,03:41:22.160,726,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 252 |
+
0023,0023 - Heico - Sigma - 4.avi,1,22393,22586,895.72,903.48,7.76,00:14:55.720,00:15:03.480,194,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 253 |
+
0023,0023 - Heico - Sigma - 4.avi,2,23387,23797,935.48,951.92,16.44,00:15:35.480,00:15:51.920,411,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 254 |
+
0023,0023 - Heico - Sigma - 4.avi,3,58098,58324,2323.92,2333.0,9.08,00:38:43.920,00:38:53.000,227,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 255 |
+
0023,0023 - Heico - Sigma - 4.avi,4,89007,89196,3560.28,3567.88,7.6,00:59:20.280,00:59:27.880,190,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 256 |
+
0023,0023 - Heico - Sigma - 4.avi,5,135563,136671,5422.52,5466.88,44.36,01:30:22.520,01:31:06.880,1109,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 257 |
+
0023,0023 - Heico - Sigma - 4.avi,6,136822,188918,5472.88,7556.76,2083.88,01:31:12.880,02:05:56.760,52097,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 258 |
+
0023,0023 - Heico - Sigma - 4.avi,7,189491,189698,7579.64,7587.96,8.32,02:06:19.640,02:06:27.960,208,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 259 |
+
0023,0023 - Heico - Sigma - 4.avi,8,216348,216559,8653.92,8662.4,8.48,02:24:13.920,02:24:22.400,212,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 260 |
+
0024,0024 - Heico - Sigma - 5.avi,1,3523,3791,140.92,151.68,10.76,00:02:20.920,00:02:31.680,269,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 261 |
+
0024,0024 - Heico - Sigma - 5.avi,2,49240,49671,1969.6,1986.88,17.28,00:32:49.600,00:33:06.880,432,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 262 |
+
0024,0024 - Heico - Sigma - 5.avi,3,72121,129067,2884.84,5162.72,2277.88,00:48:04.840,01:26:02.720,56947,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 263 |
+
0025,0025 - Heico - Sigma - 6.avi,1,769,971,30.76,38.88,8.12,00:00:30.760,00:00:38.880,203,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 264 |
+
0025,0025 - Heico - Sigma - 6.avi,2,5809,5986,232.36,239.48,7.12,00:03:52.360,00:03:59.480,178,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 265 |
+
0025,0025 - Heico - Sigma - 6.avi,3,53622,54258,2144.88,2170.36,25.48,00:35:44.880,00:36:10.360,637,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 266 |
+
0025,0025 - Heico - Sigma - 6.avi,4,96792,96976,3871.68,3879.08,7.4,01:04:31.680,01:04:39.080,185,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 267 |
+
0025,0025 - Heico - Sigma - 6.avi,5,119669,218770,4786.76,8750.84,3964.08,01:19:46.760,02:25:50.840,99102,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 268 |
+
0025,0025 - Heico - Sigma - 6.avi,6,228355,230160,9134.2,9206.44,72.24,02:32:14.200,02:33:26.440,1806,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 269 |
+
0025,0025 - Heico - Sigma - 6.avi,7,250996,252051,10039.84,10082.08,42.24,02:47:19.840,02:48:02.080,1056,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 270 |
+
0025,0025 - Heico - Sigma - 6.avi,8,254176,254633,10167.04,10185.36,18.32,02:49:27.040,02:49:45.360,458,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 271 |
+
0025,0025 - Heico - Sigma - 6.avi,9,294134,294341,11765.36,11773.68,8.32,03:16:05.360,03:16:13.680,208,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 272 |
+
0025,0025 - Heico - Sigma - 6.avi,10,305932,306158,12237.28,12246.36,9.08,03:23:57.280,03:24:06.360,227,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 273 |
+
0026,0026 - Heico - Sigma - 7.avi,1,104407,104647,4176.28,4185.92,9.64,01:09:36.280,01:09:45.920,241,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 274 |
+
0026,0026 - Heico - Sigma - 7.avi,2,106008,106558,4240.32,4262.36,22.04,01:10:40.320,01:11:02.360,551,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 275 |
+
0026,0026 - Heico - Sigma - 7.avi,3,135166,135550,5406.64,5422.04,15.4,01:30:06.640,01:30:22.040,385,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 276 |
+
0026,0026 - Heico - Sigma - 7.avi,4,162447,162857,6497.88,6514.32,16.44,01:48:17.880,01:48:34.320,411,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 277 |
+
0026,0026 - Heico - Sigma - 7.avi,5,179005,180062,7160.2,7202.52,42.32,01:59:20.200,02:00:02.520,1058,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 278 |
+
0026,0026 - Heico - Sigma - 7.avi,6,184200,224505,7368.0,8980.24,1612.24,02:02:48.000,02:29:40.240,40306,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 279 |
+
0026,0026 - Heico - Sigma - 7.avi,7,236957,238289,9478.28,9531.6,53.32,02:37:58.280,02:38:51.600,1333,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 280 |
+
0027,0027 - Heico - Sigma - 8.avi,1,1424,3592,56.96,143.72,86.76,00:00:56.960,00:02:23.720,2169,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 281 |
+
0027,0027 - Heico - Sigma - 8.avi,2,5305,5598,212.2,223.96,11.76,00:03:32.200,00:03:43.960,294,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 282 |
+
0027,0027 - Heico - Sigma - 8.avi,3,34580,34741,1383.2,1389.68,6.48,00:23:03.200,00:23:09.680,162,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 283 |
+
0027,0027 - Heico - Sigma - 8.avi,4,35894,36166,1435.76,1446.68,10.92,00:23:55.760,00:24:06.680,273,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 284 |
+
0027,0027 - Heico - Sigma - 8.avi,5,76803,119032,3072.12,4761.32,1689.2,00:51:12.120,01:19:21.320,42230,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 285 |
+
0027,0027 - Heico - Sigma - 8.avi,6,136180,136435,5447.2,5457.44,10.24,01:30:47.200,01:30:57.440,256,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 286 |
+
0027,0027 - Heico - Sigma - 8.avi,7,136515,136760,5460.6,5470.44,9.84,01:31:00.600,01:31:10.440,246,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 287 |
+
0027,0027 - Heico - Sigma - 8.avi,8,136865,137086,5474.6,5483.48,8.88,01:31:14.600,01:31:23.480,222,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 288 |
+
0027,0027 - Heico - Sigma - 8.avi,9,138468,138680,5538.72,5547.24,8.52,01:32:18.720,01:32:27.240,213,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 289 |
+
0027,0027 - Heico - Sigma - 8.avi,10,154029,154743,6161.16,6189.76,28.6,01:42:41.160,01:43:09.760,715,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 290 |
+
0027,0027 - Heico - Sigma - 8.avi,11,155735,155951,6229.4,6238.08,8.68,01:43:49.400,01:43:58.080,217,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 291 |
+
0027,0027 - Heico - Sigma - 8.avi,12,170430,171242,6817.2,6849.72,32.52,01:53:37.200,01:54:09.720,813,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 292 |
+
0027,0027 - Heico - Sigma - 8.avi,13,171760,173589,6870.4,6943.6,73.2,01:54:30.400,01:55:43.600,1830,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 293 |
+
0027,0027 - Heico - Sigma - 8.avi,14,175565,176292,7022.6,7051.72,29.12,01:57:02.600,01:57:31.720,728,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 294 |
+
0027,0027 - Heico - Sigma - 8.avi,15,188701,189011,7548.04,7560.48,12.44,02:05:48.040,02:06:00.480,311,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 295 |
+
0027,0027 - Heico - Sigma - 8.avi,16,193088,193299,7723.52,7732.0,8.48,02:08:43.520,02:08:52.000,212,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 296 |
+
0027,0027 - Heico - Sigma - 8.avi,17,193457,193676,7738.28,7747.08,8.8,02:08:58.280,02:09:07.080,220,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 297 |
+
0027,0027 - Heico - Sigma - 8.avi,18,204905,205339,8196.2,8213.6,17.4,02:16:36.200,02:16:53.600,435,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 298 |
+
0028,0028 - Heico - Sigma - 9.avi,1,2902,4698,116.08,187.96,71.88,00:01:56.080,00:03:07.960,1797,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 299 |
+
0028,0028 - Heico - Sigma - 9.avi,2,4812,5000,192.48,200.04,7.56,00:03:12.480,00:03:20.040,189,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 300 |
+
0028,0028 - Heico - Sigma - 9.avi,3,5332,5945,213.28,237.84,24.56,00:03:33.280,00:03:57.840,614,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 301 |
+
0028,0028 - Heico - Sigma - 9.avi,4,6263,6645,250.52,265.84,15.32,00:04:10.520,00:04:25.840,383,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 302 |
+
0028,0028 - Heico - Sigma - 9.avi,5,81620,82270,3264.8,3290.84,26.04,00:54:24.800,00:54:50.840,651,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 303 |
+
0028,0028 - Heico - Sigma - 9.avi,6,83668,84298,3346.72,3371.96,25.24,00:55:46.720,00:56:11.960,631,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 304 |
+
0028,0028 - Heico - Sigma - 9.avi,7,91175,92008,3647.0,3680.36,33.36,01:00:47.000,01:01:20.360,834,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 305 |
+
0028,0028 - Heico - Sigma - 9.avi,8,98941,99826,3957.64,3993.08,35.44,01:05:57.640,01:06:33.080,886,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 306 |
+
0028,0028 - Heico - Sigma - 9.avi,9,99972,100157,3998.88,4006.32,7.44,01:06:38.880,01:06:46.320,186,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 307 |
+
0028,0028 - Heico - Sigma - 9.avi,10,106430,107644,4257.2,4305.8,48.6,01:10:57.200,01:11:45.800,1215,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 308 |
+
0028,0028 - Heico - Sigma - 9.avi,11,108850,109555,4354.0,4382.24,28.24,01:12:34.000,01:13:02.240,706,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 309 |
+
0028,0028 - Heico - Sigma - 9.avi,12,111863,113239,4474.52,4529.6,55.08,01:14:34.520,01:15:29.600,1377,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 310 |
+
0028,0028 - Heico - Sigma - 9.avi,13,113515,114657,4540.6,4586.32,45.72,01:15:40.600,01:16:26.320,1143,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 311 |
+
0028,0028 - Heico - Sigma - 9.avi,14,129364,146585,5174.56,5863.44,688.88,01:26:14.560,01:37:43.440,17222,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 312 |
+
0028,0028 - Heico - Sigma - 9.avi,15,157535,157730,6301.4,6309.24,7.84,01:45:01.400,01:45:09.240,196,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 313 |
+
0028,0028 - Heico - Sigma - 9.avi,16,164683,165551,6587.32,6622.08,34.76,01:49:47.320,01:50:22.080,869,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 314 |
+
0028,0028 - Heico - Sigma - 9.avi,17,178354,178553,7134.16,7142.16,8.0,01:58:54.160,01:59:02.160,200,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 315 |
+
0028,0028 - Heico - Sigma - 9.avi,18,183895,184094,7355.8,7363.8,8.0,02:02:35.800,02:02:43.800,200,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 316 |
+
0028,0028 - Heico - Sigma - 9.avi,19,188618,234529,7544.72,9381.2,1836.48,02:05:44.720,02:36:21.200,45912,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 317 |
+
0028,0028 - Heico - Sigma - 9.avi,20,284965,286101,11398.6,11444.08,45.48,03:09:58.600,03:10:44.080,1137,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 318 |
+
0028,0028 - Heico - Sigma - 9.avi,21,315904,316225,12636.16,12649.04,12.88,03:30:36.160,03:30:49.040,322,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 319 |
+
0028,0028 - Heico - Sigma - 9.avi,22,319567,364856,12782.68,14594.28,1811.6,03:33:02.680,04:03:14.280,45290,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 320 |
+
0028,0028 - Heico - Sigma - 9.avi,23,381302,381504,15252.08,15260.2,8.12,04:14:12.080,04:14:20.200,203,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 321 |
+
0029,0029 - Heico - Sigma - 10.avi,1,446,620,17.84,24.84,7.0,00:00:17.840,00:00:24.840,175,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 322 |
+
0029,0029 - Heico - Sigma - 10.avi,2,1218,1586,48.72,63.48,14.76,00:00:48.720,00:01:03.480,369,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 323 |
+
0029,0029 - Heico - Sigma - 10.avi,3,168562,217347,6742.48,8693.92,1951.44,01:52:22.480,02:24:53.920,48786,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
| 324 |
+
0029,0029 - Heico - Sigma - 10.avi,4,222032,222368,8881.28,8894.76,13.48,02:28:01.280,02:28:14.760,337,1.0,high,00c9758b75e5aaadf2d346b15f90db76
|
pipelines/Tr006_V2/annotations/blue_screen/metadata/provenance.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source_dataset": "heico-focus-vqa",
|
| 3 |
+
"source_videos_included": false,
|
| 4 |
+
"run_id": "debug015",
|
| 5 |
+
"run_name": "blue_screen_coarse_to_fine_20260825",
|
| 6 |
+
"task_type": "debug",
|
| 7 |
+
"status": "completed",
|
| 8 |
+
"purpose": "Detect frozen blue-screen intervals in all 30 HeiCo endoscopy videos with 1-second coarse hashing and native-FPS exact boundary refinement.",
|
| 9 |
+
"code_commit": "a23500eae96b97f4b9aff3d00d08c3b8e7482aba",
|
| 10 |
+
"entrypoint": "data_preprocessing/process_blue_screen_dataset.py",
|
| 11 |
+
"parameters": {
|
| 12 |
+
"sample_seconds": 1.0,
|
| 13 |
+
"min_static_seconds": 6.0,
|
| 14 |
+
"blue_ratio": 0.45,
|
| 15 |
+
"pad_seconds": 5.0,
|
| 16 |
+
"coarse_size": 96,
|
| 17 |
+
"fine_size": 160,
|
| 18 |
+
"fine_resolution": "native FPS (25 FPS for inspected dataset)"
|
| 19 |
+
},
|
| 20 |
+
"precision": "uint8 decoded frames",
|
| 21 |
+
"started_at": "2026-08-25T00:22:36+08:00",
|
| 22 |
+
"finished_at": "2026-08-25T02:54:37+08:00",
|
| 23 |
+
"results_summary": {
|
| 24 |
+
"video_count": 30,
|
| 25 |
+
"completed_video_count": 30,
|
| 26 |
+
"failed_video_count": 0,
|
| 27 |
+
"total_frames": 8681449,
|
| 28 |
+
"total_decoded_frames": 8681449,
|
| 29 |
+
"interval_count": 323,
|
| 30 |
+
"total_video_seconds": 347257.96,
|
| 31 |
+
"total_blue_screen_seconds": 82209.52,
|
| 32 |
+
"elapsed_seconds": 9119.6,
|
| 33 |
+
"validation": "PASS"
|
| 34 |
+
},
|
| 35 |
+
"issues": [],
|
| 36 |
+
"acceptance": {
|
| 37 |
+
"all_30_videos_accounted_for": true,
|
| 38 |
+
"no_source_video_modification": true,
|
| 39 |
+
"frame_level_boundaries": true,
|
| 40 |
+
"complete_log": true,
|
| 41 |
+
"aggregate_counts_match_per_video_results": true
|
| 42 |
+
}
|
| 43 |
+
}
|
pipelines/Tr006_V2/annotations/blue_screen/metadata/summary.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"status": "completed",
|
| 3 |
+
"started_at": "2026-08-25T00:22:37.492532+08:00",
|
| 4 |
+
"finished_at": "2026-08-25T02:54:37.111581+08:00",
|
| 5 |
+
"elapsed_seconds": 9119.599936511368,
|
| 6 |
+
"host": "farm02",
|
| 7 |
+
"config": {
|
| 8 |
+
"sample_seconds": 1.0,
|
| 9 |
+
"min_static_seconds": 6.0,
|
| 10 |
+
"blue_ratio": 0.45,
|
| 11 |
+
"pad_seconds": 5.0,
|
| 12 |
+
"coarse_size": 96,
|
| 13 |
+
"fine_size": 160,
|
| 14 |
+
"definition": "exact repeated decoded ROI frames with blue ratio above threshold"
|
| 15 |
+
},
|
| 16 |
+
"video_count": 30,
|
| 17 |
+
"completed_video_count": 30,
|
| 18 |
+
"failed_video_count": 0,
|
| 19 |
+
"videos_with_blue_screen": 30,
|
| 20 |
+
"interval_count": 323,
|
| 21 |
+
"total_video_seconds": 347257.9600000001,
|
| 22 |
+
"total_blue_screen_seconds": 82209.52000000003
|
| 23 |
+
}
|
pipelines/Tr006_V2/annotations/blue_screen/metadata/validation.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"status": "PASS",
|
| 3 |
+
"validated_at": "2026-08-25T02:54:37+08:00",
|
| 4 |
+
"source_video_count": 30,
|
| 5 |
+
"per_video_json_count": 30,
|
| 6 |
+
"videos_jsonl_count": 30,
|
| 7 |
+
"completed_video_count": 30,
|
| 8 |
+
"failed_video_count": 0,
|
| 9 |
+
"interval_count": 323,
|
| 10 |
+
"csv_interval_count": 323,
|
| 11 |
+
"evidence_jpeg_count": 1291,
|
| 12 |
+
"total_frames": 8681449,
|
| 13 |
+
"total_decoded_frames": 8681449,
|
| 14 |
+
"total_video_seconds": 347257.96,
|
| 15 |
+
"total_blue_screen_seconds": 82209.52,
|
| 16 |
+
"checks": {
|
| 17 |
+
"source_paths_exactly_covered": true,
|
| 18 |
+
"all_videos_completed": true,
|
| 19 |
+
"all_frames_decoded": true,
|
| 20 |
+
"intervals_ordered_non_overlapping_and_in_bounds": true,
|
| 21 |
+
"interval_duration_matches_frame_bounds": true,
|
| 22 |
+
"all_intervals_meet_hash_and_blue_criteria": true,
|
| 23 |
+
"aggregate_csv_matches_per_video_results": true,
|
| 24 |
+
"all_non_boundary_evidence_frames_present": true,
|
| 25 |
+
"run_log_exit_code_zero": true
|
| 26 |
+
},
|
| 27 |
+
"notes": [
|
| 28 |
+
"Video 0022 has one interval ending at the final source frame, so an after-boundary evidence frame does not exist by definition."
|
| 29 |
+
]
|
| 30 |
+
}
|
pipelines/Tr006_V2/annotations/blue_screen/metadata/videos.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pipelines/Tr006_V2/docker/segment-algorithm/.dockerignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Keep the build context small. do_save.sh writes multi-gigabyte *.tar.gz
|
| 2 |
+
# archives into this folder, and without this file Docker would send them
|
| 3 |
+
# (and the sample batch) to the daemon on every build.
|
| 4 |
+
*.tar.gz
|
| 5 |
+
test/
|
| 6 |
+
__pycache__/
|
pipelines/Tr006_V2/docker/segment-algorithm/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.tar.gz
|
| 2 |
+
__pycache__/
|
pipelines/Tr006_V2/docker/segment-algorithm/Dockerfile
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM --platform=linux/amd64 pytorch/pytorch:2.11.0-cuda12.8-cudnn9-runtime AS segment-algorithm-amd64
|
| 2 |
+
|
| 3 |
+
# torch 2.11.0 + torchvision are pre-installed in the base image (CUDA 12.8).
|
| 4 |
+
# requirements.txt only lists additional dependencies.
|
| 5 |
+
#
|
| 6 |
+
# This base image is the recommended default, not a hard requirement. It is chosen
|
| 7 |
+
# because it is the ONE PyTorch build that runs on both evaluation GPUs unmodified:
|
| 8 |
+
# an NVIDIA RTX PRO 6000 Blackwell (compute capability sm_120) and an NVIDIA L40S
|
| 9 |
+
# (sm_89). Its CUDA 12.8 kernels cover sm_75/80/86/90/100/120 — Blackwell natively
|
| 10 |
+
# and the L40S via its sm_86 kernels (a CUDA binary runs on any later minor
|
| 11 |
+
# revision of the same major capability). So one image serves whichever GPU you
|
| 12 |
+
# select on the platform, with no rebuild.
|
| 13 |
+
#
|
| 14 |
+
# If you change it, know the two limits it steers around: CUDA 12.4/12.6 wheels
|
| 15 |
+
# stop at sm_90 and abort on Blackwell ("not compatible with the current PyTorch
|
| 16 |
+
# installation"); CUDA 13.0 wheels cover both GPUs but need a host driver >= 580 —
|
| 17 |
+
# the RTX PRO 6000 fleet has it (595), the L40S fleet does not. If your submission
|
| 18 |
+
# targets ONLY the RTX PRO 6000 (offered on the SEGMENT and PROCEDURE tracks), a
|
| 19 |
+
# newer CUDA/torch is a valid choice — see the guard below for the one line to relax.
|
| 20 |
+
|
| 21 |
+
ENV PYTHONUNBUFFERED=1
|
| 22 |
+
|
| 23 |
+
# ── System packages ───────────────────────────────────────────────────────────
|
| 24 |
+
# The slim -runtime base image ships neither the shared libraries that
|
| 25 |
+
# opencv-python (a dependency of orena-focus) needs at import time, nor any
|
| 26 |
+
# compiler toolchain. Install system packages HERE, while the build still runs
|
| 27 |
+
# as root — everything below runs as the unprivileged 'user'. If one of your
|
| 28 |
+
# pip dependencies compiles from source (e.g. flash-attn), add build-essential.
|
| 29 |
+
RUN apt-get update \
|
| 30 |
+
&& apt-get install -y --no-install-recommends \
|
| 31 |
+
libgl1 \
|
| 32 |
+
libglib2.0-0 \
|
| 33 |
+
libxcb1 \
|
| 34 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 35 |
+
|
| 36 |
+
RUN groupadd -r user && useradd -m --no-log-init -r -g user user
|
| 37 |
+
USER user
|
| 38 |
+
|
| 39 |
+
WORKDIR /opt/app
|
| 40 |
+
|
| 41 |
+
COPY --chown=user:user requirements.txt /opt/app/
|
| 42 |
+
|
| 43 |
+
# If one of your dependencies compiles CUDA extensions during the build (e.g.
|
| 44 |
+
# flash-attn from source), uncomment this so it emits kernels for both evaluation
|
| 45 |
+
# GPUs rather than only for the card in your build machine:
|
| 46 |
+
# ENV TORCH_CUDA_ARCH_LIST="8.0;8.6;8.9;9.0;12.0+PTX"
|
| 47 |
+
|
| 48 |
+
# --break-system-packages: this base image's Python is Ubuntu's system Python,
|
| 49 |
+
# which is marked externally-managed (PEP 668), so pip refuses to install without
|
| 50 |
+
# it. Nothing is actually broken — `--user` still installs into the unprivileged
|
| 51 |
+
# user's own ~/.local, leaving the system packages untouched.
|
| 52 |
+
RUN python -m pip install \
|
| 53 |
+
--user \
|
| 54 |
+
--break-system-packages \
|
| 55 |
+
--no-cache-dir \
|
| 56 |
+
--no-color \
|
| 57 |
+
--requirement /opt/app/requirements.txt
|
| 58 |
+
|
| 59 |
+
# ── Guard: is torch still the recommended CUDA 12.8 build? ────────────────────
|
| 60 |
+
# This enforces the recommendation above so an ACCIDENTAL swap cannot reach the
|
| 61 |
+
# platform unnoticed. If one of your dependencies needs a torch version this image
|
| 62 |
+
# does not have, pip quietly replaces the CUDA 12.8 build with the default PyPI
|
| 63 |
+
# wheel (built against CUDA 13.0). That wheel needs a host driver >= 580.65.06; the
|
| 64 |
+
# L40S fleet runs 570.211.01, so it cannot start there. Failing the build now beats
|
| 65 |
+
# failing every job after you submit.
|
| 66 |
+
#
|
| 67 |
+
# Both halves matter. The CUDA version is what the driver floor depends on, and it
|
| 68 |
+
# is NOT visible in the architecture list: the CUDA 13.0 wheel advertises the very
|
| 69 |
+
# same sm_ list as the 12.8 one. The arch list is still checked because it is what
|
| 70 |
+
# an older CUDA 12.4/12.6 wheel gets wrong. get_arch_list() returns [] with no GPU
|
| 71 |
+
# visible — a docker build never has one — so the flags compiled into the wheel are
|
| 72 |
+
# read directly.
|
| 73 |
+
#
|
| 74 |
+
# Targeting the RTX PRO 6000 only, and want a newer CUDA? Then this default no
|
| 75 |
+
# longer applies to you: drop the `cuda.startswith('12.8')` assert below (keep the
|
| 76 |
+
# sm_120 arch assert, so a build with no Blackwell kernels is still caught).
|
| 77 |
+
RUN python -c "import torch; \
|
| 78 |
+
flags = torch._C._cuda_getArchFlags() or ''; \
|
| 79 |
+
cuda = torch.version.cuda or ''; \
|
| 80 |
+
print('torch', torch.__version__, '| cuda', cuda, '|', flags); \
|
| 81 |
+
assert cuda.startswith('12.8'), 'torch was replaced by a CUDA ' + cuda + ' build; the L40S driver cannot run it'; \
|
| 82 |
+
assert 'sm_120' in flags and 'sm_86' in flags, 'torch build misses a required GPU architecture: ' + repr(flags)"
|
| 83 |
+
|
| 84 |
+
# ── Model definition + weights ────────────────────────────────────────────────
|
| 85 |
+
# resources/ holds the model definition (model.py) and a small committed dummy
|
| 86 |
+
# checkpoint (dummy_weights.pt). For a real submission, replace dummy_weights.pt
|
| 87 |
+
# with your trained checkpoint.
|
| 88 |
+
#
|
| 89 |
+
# ECR layer-size limit: a single image layer must not exceed 50 GB, and each
|
| 90 |
+
# COPY instruction produces exactly one layer. A checkpoint large enough to push
|
| 91 |
+
# one COPY past that limit must be split into chunks and copied with several COPY
|
| 92 |
+
# instructions (one layer each), then reassembled at runtime — e.g. pre-split
|
| 93 |
+
# with `split -b 45G weights.pt resources/weights.part-` and:
|
| 94 |
+
# COPY --chown=user:user resources/weights.part-aa /opt/app/resources/
|
| 95 |
+
# COPY --chown=user:user resources/weights.part-ab /opt/app/resources/
|
| 96 |
+
# COPY --chown=user:user resources/weights.part-ac /opt/app/resources/
|
| 97 |
+
# reassembling with `cat resources/weights.part-* > weights.pt` before load.
|
| 98 |
+
COPY --chown=user:user resources/ /opt/app/resources/
|
| 99 |
+
|
| 100 |
+
COPY --chown=user:user inference.py /opt/app/
|
| 101 |
+
|
| 102 |
+
ENTRYPOINT ["python", "inference.py"]
|
pipelines/Tr006_V2/docker/segment-algorithm/do_build.sh
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
|
| 3 |
+
set -e
|
| 4 |
+
|
| 5 |
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
| 6 |
+
DOCKER_IMAGE_TAG="segment-algorithm"
|
| 7 |
+
|
| 8 |
+
docker build \
|
| 9 |
+
--platform=linux/amd64 \
|
| 10 |
+
--tag "$DOCKER_IMAGE_TAG" \
|
| 11 |
+
"$SCRIPT_DIR" 2>&1
|
pipelines/Tr006_V2/docker/segment-algorithm/do_save.sh
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
|
| 3 |
+
set -eo pipefail
|
| 4 |
+
|
| 5 |
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
| 6 |
+
|
| 7 |
+
DOCKER_IMAGE_TAG="segment-algorithm"
|
| 8 |
+
|
| 9 |
+
echo "=+= (Re)build the container"
|
| 10 |
+
source "${SCRIPT_DIR}/do_build.sh"
|
| 11 |
+
|
| 12 |
+
build_timestamp=$( docker inspect --format='{{ .Created }}' "$DOCKER_IMAGE_TAG")
|
| 13 |
+
|
| 14 |
+
if [ -z "$build_timestamp" ]; then
|
| 15 |
+
echo "Error: Failed to retrieve build information for container $DOCKER_IMAGE_TAG"
|
| 16 |
+
exit 1
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
formatted_build_info=$(echo $build_timestamp | sed -E 's/(.*)T(.*)\..*Z/\1_\2/' | sed 's/[-,:]/-/g')
|
| 20 |
+
|
| 21 |
+
output_filename="${SCRIPT_DIR}/${DOCKER_IMAGE_TAG}_${formatted_build_info}.tar.gz"
|
| 22 |
+
|
| 23 |
+
echo "==+=="
|
| 24 |
+
echo "Saving the container image as ${output_filename}. This can take a while."
|
| 25 |
+
echo ""
|
| 26 |
+
|
| 27 |
+
# Use pigz (parallel gzip) if available — much faster than gzip on a large image.
|
| 28 |
+
if command -v pigz >/dev/null 2>&1; then
|
| 29 |
+
compressor=(pigz -c)
|
| 30 |
+
else
|
| 31 |
+
compressor=(gzip -c)
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
# Show a progress bar with pv if available, sized by the image's uncompressed size.
|
| 35 |
+
image_size=$(docker image inspect "$DOCKER_IMAGE_TAG" --format '{{.Size}}' 2>/dev/null || true)
|
| 36 |
+
|
| 37 |
+
if command -v pv >/dev/null 2>&1; then
|
| 38 |
+
if [[ "$image_size" =~ ^[0-9]+$ && "$image_size" -gt 0 ]]; then
|
| 39 |
+
docker save "$DOCKER_IMAGE_TAG" \
|
| 40 |
+
| pv --size "$image_size" --progress --timer --eta --rate --bytes \
|
| 41 |
+
| "${compressor[@]}" > "$output_filename"
|
| 42 |
+
else
|
| 43 |
+
docker save "$DOCKER_IMAGE_TAG" | pv --timer --rate --bytes | "${compressor[@]}" > "$output_filename"
|
| 44 |
+
fi
|
| 45 |
+
else
|
| 46 |
+
echo "Tip: install 'pv' (e.g. sudo apt-get install pv) for a progress bar."
|
| 47 |
+
docker save "$DOCKER_IMAGE_TAG" | "${compressor[@]}" > "$output_filename"
|
| 48 |
+
fi
|
| 49 |
+
echo "Container image saved as ${output_filename}"
|
| 50 |
+
echo "==+=="
|
pipelines/Tr006_V2/docker/segment-algorithm/do_test_run.sh
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
|
| 3 |
+
set -e
|
| 4 |
+
|
| 5 |
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
| 6 |
+
DOCKER_IMAGE_TAG="segment-algorithm"
|
| 7 |
+
|
| 8 |
+
DOCKER_NOOP_VOLUME="${DOCKER_IMAGE_TAG}-volume"
|
| 9 |
+
|
| 10 |
+
INPUT_DIR="${SCRIPT_DIR}/test/input"
|
| 11 |
+
OUTPUT_DIR="${SCRIPT_DIR}/test/output"
|
| 12 |
+
|
| 13 |
+
# Use the GPU when Docker's NVIDIA runtime is available; fall back to CPU
|
| 14 |
+
# otherwise (the dummy runs fine on CPU — the platform always provides a GPU).
|
| 15 |
+
GPU_FLAGS=()
|
| 16 |
+
if docker info --format '{{range $k, $v := .Runtimes}}{{$k}} {{end}}' 2>/dev/null | grep -qw nvidia; then
|
| 17 |
+
GPU_FLAGS=(--gpus all)
|
| 18 |
+
else
|
| 19 |
+
echo "=+= No NVIDIA container runtime detected - running on CPU"
|
| 20 |
+
fi
|
| 21 |
+
|
| 22 |
+
echo "=+= (Re)build the container"
|
| 23 |
+
source "${SCRIPT_DIR}/do_build.sh"
|
| 24 |
+
|
| 25 |
+
cleanup() {
|
| 26 |
+
echo "=+= Cleaning permissions ..."
|
| 27 |
+
docker run --rm \
|
| 28 |
+
--platform=linux/amd64 \
|
| 29 |
+
--quiet \
|
| 30 |
+
--volume "$OUTPUT_DIR":/output \
|
| 31 |
+
--entrypoint /bin/sh \
|
| 32 |
+
$DOCKER_IMAGE_TAG \
|
| 33 |
+
-c "chmod -R -f o+rwX /output/* || true"
|
| 34 |
+
|
| 35 |
+
docker volume rm "$DOCKER_NOOP_VOLUME" > /dev/null
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
chmod -R -f o+rX "$INPUT_DIR"
|
| 39 |
+
|
| 40 |
+
if [ -d "${OUTPUT_DIR}/interface_1" ]; then
|
| 41 |
+
chmod -f o+rwX "${OUTPUT_DIR}/interface_1"
|
| 42 |
+
|
| 43 |
+
echo "=+= Cleaning up any earlier output"
|
| 44 |
+
docker run --rm \
|
| 45 |
+
--platform=linux/amd64 \
|
| 46 |
+
--quiet \
|
| 47 |
+
--volume "${OUTPUT_DIR}/interface_1":/output \
|
| 48 |
+
--entrypoint /bin/sh \
|
| 49 |
+
$DOCKER_IMAGE_TAG \
|
| 50 |
+
-c "rm -rf /output/* || true"
|
| 51 |
+
else
|
| 52 |
+
mkdir -p -m o+rwX "${OUTPUT_DIR}/interface_1"
|
| 53 |
+
fi
|
| 54 |
+
|
| 55 |
+
docker volume create "$DOCKER_NOOP_VOLUME" > /dev/null
|
| 56 |
+
|
| 57 |
+
trap cleanup EXIT
|
| 58 |
+
|
| 59 |
+
run_docker_forward_pass() {
|
| 60 |
+
local interface_dir="$1"
|
| 61 |
+
|
| 62 |
+
echo "=+= Doing a forward pass on ${interface_dir}"
|
| 63 |
+
|
| 64 |
+
## Note the extra arguments that are passed here:
|
| 65 |
+
# '--network none'
|
| 66 |
+
# entails there is no internet connection
|
| 67 |
+
# '--volume <NAME>:/tmp'
|
| 68 |
+
# is added because on the ORena FOCUS Challenge platform this directory cannot be used to store permanent files
|
| 69 |
+
# '--shm-size' is left at Docker's default (64 MB). A PyTorch DataLoader with
|
| 70 |
+
# num_workers > 0 needs more shared memory — if you use one, add e.g.
|
| 71 |
+
# '--shm-size=2g' here.
|
| 72 |
+
docker run --rm \
|
| 73 |
+
--platform=linux/amd64 \
|
| 74 |
+
--network none \
|
| 75 |
+
"${GPU_FLAGS[@]}" \
|
| 76 |
+
--volume "${INPUT_DIR}/${interface_dir}":/input:ro \
|
| 77 |
+
--volume "${OUTPUT_DIR}/${interface_dir}":/output \
|
| 78 |
+
--volume "$DOCKER_NOOP_VOLUME":/tmp \
|
| 79 |
+
"$DOCKER_IMAGE_TAG"
|
| 80 |
+
|
| 81 |
+
echo "=+= Wrote results to ${OUTPUT_DIR}/${interface_dir}"
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
run_docker_forward_pass "interface_1"
|
| 85 |
+
|
| 86 |
+
echo "=+= Save this image for uploading via ./do_save.sh"
|
pipelines/Tr006_V2/docker/segment-algorithm/inference.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Dummy algorithm for the ORena SAVE FOCUS challenge — SEGMENT track.
|
| 3 |
+
|
| 4 |
+
Replace the marked blocks with your model. Every spot to change carries a ``TODO``.
|
| 5 |
+
|
| 6 |
+
What one run receives
|
| 7 |
+
---------------------
|
| 8 |
+
A single container run answers a **batch of questions**.
|
| 9 |
+
Inputs are mounted read-only at ``/input``:
|
| 10 |
+
|
| 11 |
+
request.json a LIST of focus.Request objects — one per question
|
| 12 |
+
plain/<qID>.mp4 the video clip for question <qID>
|
| 13 |
+
overlayed/<qID>.mp4 the same clip with an HH:MM:SS timestamp burned in
|
| 14 |
+
FO_definitions.json the foreign-object class definitions, as plain text
|
| 15 |
+
batch.json a convenience index of the batch (qIDs and layout)
|
| 16 |
+
|
| 17 |
+
and one file is written to ``/output``:
|
| 18 |
+
|
| 19 |
+
answer.json a LIST of focus.Response objects — one per question
|
| 20 |
+
|
| 21 |
+
``request.json`` is authoritative: iterate it, and answer every ``qID`` it lists.
|
| 22 |
+
|
| 23 |
+
Each question carries its own clip
|
| 24 |
+
----------------------------------
|
| 25 |
+
Every question has a **separate video file, already trimmed to that question's
|
| 26 |
+
window**, and no two questions in a batch come from the same source video. Your
|
| 27 |
+
answer to one question can therefore never depend on another question's footage.
|
| 28 |
+
|
| 29 |
+
Two consequences for your pipeline:
|
| 30 |
+
|
| 31 |
+
* ``request.start_time`` / ``end_time`` refer to the **original, untrimmed
|
| 32 |
+
procedure video** — the same timeline shown in the overlay variant. The clip you
|
| 33 |
+
are given has already been cut to that window, so decode the clip from its own
|
| 34 |
+
beginning; do **not** seek to ``start_time`` inside it. ``request.duration``
|
| 35 |
+
still tells you how long the window is.
|
| 36 |
+
* There is nothing to share between questions — no common video to cache or encode
|
| 37 |
+
once. Each clip is decoded on its own.
|
| 38 |
+
|
| 39 |
+
Both variants show the same footage. Use the plain clip, the overlay, or both.
|
| 40 |
+
|
| 41 |
+
Clip encoding
|
| 42 |
+
-------------
|
| 43 |
+
Clips are H.264 MP4 at exactly **5 fps** (one frame every 0.2 s), height-normalised
|
| 44 |
+
to at most 576 px. Widths follow the source's aspect ratio (e.g., 1024x576, 720x576 or
|
| 45 |
+
640x360) — do not assume a fixed resolution. A
|
| 46 |
+
keyframe every 5 s keeps seeking cheap. The overlay clock shows the source
|
| 47 |
+
timeline's second, floored.
|
| 48 |
+
|
| 49 |
+
Load your model once
|
| 50 |
+
--------------------
|
| 51 |
+
Model loading, imports and warm-up happen **once per container run**, not once per
|
| 52 |
+
question. The latency budget is built around that, so keep model loading outside
|
| 53 |
+
the per-question loop — as this file does. This is the single most important thing
|
| 54 |
+
to get right for speed.
|
| 55 |
+
|
| 56 |
+
The latency budget
|
| 57 |
+
------------------
|
| 58 |
+
The platform measures the execution time of the whole run itself; the ``latency``
|
| 59 |
+
field you write into each Response is informational and is not used for scoring.
|
| 60 |
+
|
| 61 |
+
The budget is pooled across the batch. For a batch of B questions:
|
| 62 |
+
|
| 63 |
+
allowed = 120 s + B x 15 s
|
| 64 |
+
^setup ^per-question processing (the SEGMENT budget)
|
| 65 |
+
|
| 66 |
+
Going over does not fail the run outright. Instead, a share of your questions is
|
| 67 |
+
**forfeited** (scored incorrect), growing with the overrun: a small overrun costs a
|
| 68 |
+
few questions, and exceeding the budget by 20% forfeits the whole batch.
|
| 69 |
+
|
| 70 |
+
The forfeited questions are selected for you — not necessarily the slow ones — so an
|
| 71 |
+
overrun costs you questions you would have answered correctly. If you are running out
|
| 72 |
+
of budget, answer the rest quickly rather than overrun.
|
| 73 |
+
|
| 74 |
+
Keep going when a question fails
|
| 75 |
+
--------------------------------
|
| 76 |
+
One bad question should not cost you the batch. Catch errors per question, still
|
| 77 |
+
emit a Response for that ``qID``, and carry on — as the loop below does. A question
|
| 78 |
+
with no answer is simply scored incorrect.
|
| 79 |
+
|
| 80 |
+
Frame sampling
|
| 81 |
+
--------------
|
| 82 |
+
The clip is decoded with `decord <https://pypi.org/project/decord/>`_ using a single
|
| 83 |
+
batched read of exactly 80 evenly spaced frame indices. The pixels come from the
|
| 84 |
+
trimmed clip, while their timestamps are reconstructed on the original timeline
|
| 85 |
+
from ``Request.start_time``. Decoding and processor work count towards latency.
|
| 86 |
+
|
| 87 |
+
Trying it out
|
| 88 |
+
-------------
|
| 89 |
+
A sample batch (``test/input/interface_1/``) ships with the template. This package
|
| 90 |
+
stores the merged Tr006 checkpoint under ``resources/model``; runtime inference is
|
| 91 |
+
fully local and does not download model files.
|
| 92 |
+
|
| 93 |
+
./do_test_run.sh # build the image and run it on the sample batch
|
| 94 |
+
./do_save.sh # build and save a .tar.gz for upload
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
import json
|
| 98 |
+
import logging
|
| 99 |
+
import sys
|
| 100 |
+
import time
|
| 101 |
+
from pathlib import Path
|
| 102 |
+
|
| 103 |
+
import numpy as np
|
| 104 |
+
import torch
|
| 105 |
+
|
| 106 |
+
# decord must be imported AFTER torch: importing it first breaks CUDA
|
| 107 |
+
# initialisation (a known decord issue — torch.cuda init then fails with
|
| 108 |
+
# "random_device could not be read" or a segfault).
|
| 109 |
+
import decord
|
| 110 |
+
from focus import Request, Response, load_requests, save_items
|
| 111 |
+
from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration
|
| 112 |
+
|
| 113 |
+
logging.basicConfig(
|
| 114 |
+
stream=sys.stdout,
|
| 115 |
+
level=logging.DEBUG,
|
| 116 |
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
| 117 |
+
datefmt="%Y-%m-%d %H:%M:%S",
|
| 118 |
+
)
|
| 119 |
+
log = logging.getLogger(__name__)
|
| 120 |
+
|
| 121 |
+
RESOURCES_PATH = Path(__file__).parent / "resources"
|
| 122 |
+
MODEL_PATH = RESOURCES_PATH / "model"
|
| 123 |
+
|
| 124 |
+
INPUT_PATH = Path("/input")
|
| 125 |
+
OUTPUT_PATH = Path("/output")
|
| 126 |
+
N_FRAMES = 80
|
| 127 |
+
MAX_NEW_TOKENS = 64
|
| 128 |
+
USE_OVERLAY = False # The trained Tr006 V2 model used plain frames + metadata.
|
| 129 |
+
VIDEO_DIR = INPUT_PATH / ("overlayed" if USE_OVERLAY else "plain")
|
| 130 |
+
|
| 131 |
+
# Keep this prompt byte-for-byte aligned with the Tr006 V2 fixed80 training
|
| 132 |
+
# manifest. The official package supplies FO_definitions separately; it is
|
| 133 |
+
# deliberately not concatenated to the model prompt because the training rows
|
| 134 |
+
# used this system message and the question only.
|
| 135 |
+
SYSTEM_PROMPT = (
|
| 136 |
+
"You are a surgical video analysis assistant. You are shown a short clip from a laparoscopic procedure and asked a single question about it.\n\n"
|
| 137 |
+
"The clip is given as a sequence of frames in chronological order. Each frame is preceded by its timestamp in the source video, written as "
|
| 138 |
+
"<SECONDS seconds> -- a frame marked <1234.5 seconds> was taken 1234.5 seconds (00:20:34) after the video began. Consecutive frames may be several seconds "
|
| 139 |
+
"apart, so an event can happen between two of them.\n\n"
|
| 140 |
+
"A foreign object (FO) is any object fully introduced into the patient's body\n"
|
| 141 |
+
"cavity during surgery that must be retrieved or accounted for. Importantly,\n"
|
| 142 |
+
"standard surgical instruments that remain connected to the external environment\n"
|
| 143 |
+
"(e.g., graspers, scissors, trocars, staplers, cameras) are not considered foreign\n"
|
| 144 |
+
"objects. Furthermore, we exclude detachable parts of surgical instruments,\n"
|
| 145 |
+
"particularly anvil components of staplers.\n\n"
|
| 146 |
+
"The foreign object classes are exactly: Sponge, Clip, Specimen Bag, Silicone Loop, External Drain, Needle, Gallstone, Specimen, Mesh, Absorbable Hemostatic Agent.\n\n"
|
| 147 |
+
"Reply with the answer and nothing else -- no reasoning, no preamble, no\n"
|
| 148 |
+
"explanation, no restating the question. A single short line.\n\n"
|
| 149 |
+
"Rules for the answer:\n"
|
| 150 |
+
"- Write the value only. No sentence, no explanation, no units, no trailing\n"
|
| 151 |
+
" period, and never repeat the question.\n"
|
| 152 |
+
"- Asks yes or no -> write exactly: yes or no\n"
|
| 153 |
+
"- Asks how many / for a count -> write digits only, e.g. 0 or 1 or 2.\n"
|
| 154 |
+
"- Asks which foreign object class(es) -> write class names exactly as spelled\n"
|
| 155 |
+
" in the list above, comma-separated (e.g. Clip, Sponge), or exactly: none\n"
|
| 156 |
+
" Never answer with a generic description such as \"surgical instrument\".\n"
|
| 157 |
+
"- Asks when something happens -> write hh:mm:ss, read off the frame timestamps.\n"
|
| 158 |
+
" Those timestamps count from the START OF THE VIDEO, not from the start of this\n"
|
| 159 |
+
" clip, so <1234.5 seconds> is 00:20:34. Convert carefully: 3600 seconds is one\n"
|
| 160 |
+
" hour, 60 seconds is one minute. If the event falls between two frames, give\n"
|
| 161 |
+
" your best estimate between their timestamps rather than snapping to one.\n"
|
| 162 |
+
" For several time points, separate them with commas: 00:09:19, 00:12:44\n"
|
| 163 |
+
"- Asks for a percentage or a share -> write the number only, e.g. 40\n"
|
| 164 |
+
"- Lists options to choose from -> copy exactly one of those options, verbatim.\n"
|
| 165 |
+
"- Anything else -> a short phrase, at most a few words.\n\n"
|
| 166 |
+
"If you are unsure, still commit to your single best answer in the required\n"
|
| 167 |
+
"form. An empty, hedged, or explanatory answer is scored as wrong.\n"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def log_environment(device: torch.device) -> None:
|
| 172 |
+
"""Report the CUDA stack this container ended up with — once, at startup.
|
| 173 |
+
|
| 174 |
+
A torch build carrying no kernels for the GPU it was handed is the classic
|
| 175 |
+
reason a run dies on the platform, and it stays invisible until the first
|
| 176 |
+
CUDA call. Printing this makes a failed job diagnosable from the logs alone.
|
| 177 |
+
Keep it: it costs milliseconds, once per run.
|
| 178 |
+
"""
|
| 179 |
+
log.info("--- Environment ---")
|
| 180 |
+
log.info(" torch : %s (built against CUDA %s)", torch.__version__, torch.version.cuda)
|
| 181 |
+
|
| 182 |
+
# get_arch_list() is empty when no GPU is visible; the architecture flags
|
| 183 |
+
# compiled into the wheel can be read either way.
|
| 184 |
+
arch = torch.cuda.get_arch_list() or (torch._C._cuda_getArchFlags() or "").split()
|
| 185 |
+
log.info(" torch kernels : %s", " ".join(arch) or "unknown")
|
| 186 |
+
|
| 187 |
+
driver_file = Path("/proc/driver/nvidia/version")
|
| 188 |
+
log.info(
|
| 189 |
+
" host driver : %s",
|
| 190 |
+
driver_file.read_text().strip().splitlines()[0]
|
| 191 |
+
if driver_file.exists()
|
| 192 |
+
else "no NVIDIA driver visible",
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
if device.type != "cuda":
|
| 196 |
+
log.warning(" No GPU visible — running on CPU. The platform always provides one.")
|
| 197 |
+
return
|
| 198 |
+
|
| 199 |
+
capability = torch.cuda.get_device_capability(0)
|
| 200 |
+
free, total = torch.cuda.mem_get_info(0)
|
| 201 |
+
log.info(" GPU : %s", torch.cuda.get_device_name(0))
|
| 202 |
+
log.info(" capability : %s (sm_%d%d)", capability, *capability)
|
| 203 |
+
log.info(" VRAM : %.1f GiB free of %.1f GiB", free / 1024**3, total / 1024**3)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def clip_path_for(req: Request) -> Path:
|
| 207 |
+
"""The video clip belonging to this question.
|
| 208 |
+
|
| 209 |
+
Every question has exactly one clip, named after its ``qID`` and already cut to
|
| 210 |
+
that question's ``[start_time, end_time]`` window.
|
| 211 |
+
"""
|
| 212 |
+
return VIDEO_DIR / f"{req.qID}.mp4"
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def your_preprocessing(video_path: Path, req: Request) -> tuple[np.ndarray, dict]:
|
| 216 |
+
"""Decode exactly ``N_FRAMES`` and attach original-timeline millisecond PTS.
|
| 217 |
+
|
| 218 |
+
Official clips are already trimmed and encoded at 5fps. The local frame
|
| 219 |
+
indices are therefore sampled across the whole clip, while ``frames_indices``
|
| 220 |
+
are rebuilt from Request.start_time so Qwen's processor emits absolute
|
| 221 |
+
``<... seconds>`` anchors, matching the Tr006 fixed80 training contract.
|
| 222 |
+
"""
|
| 223 |
+
log.debug("Decoding %s (size on disk: %d bytes)", video_path, video_path.stat().st_size)
|
| 224 |
+
|
| 225 |
+
vr = decord.VideoReader(str(video_path), ctx=decord.cpu(0), num_threads=1)
|
| 226 |
+
base_fps = float(vr.get_avg_fps())
|
| 227 |
+
n_frames = len(vr)
|
| 228 |
+
if n_frames <= 0 or base_fps <= 0:
|
| 229 |
+
raise RuntimeError(f"invalid video stream: n_frames={n_frames}, fps={base_fps}")
|
| 230 |
+
# np.linspace + round mirrors the fixed-frame preprocessing and supports
|
| 231 |
+
# short clips by repeating the nearest decoded frame.
|
| 232 |
+
indices = np.linspace(0, n_frames - 1, N_FRAMES).round().astype(np.int64)
|
| 233 |
+
log.debug("Video: %d frames @ %.3f fps; sampling %d across %.2f s", n_frames, base_fps, N_FRAMES, n_frames / base_fps)
|
| 234 |
+
frames = vr.get_batch(indices).asnumpy() # (T, H, W, 3) uint8 RGB — one batched decode
|
| 235 |
+
del vr
|
| 236 |
+
|
| 237 |
+
# Qwen3's processor averages timestamps per temporal patch (2 frames), so
|
| 238 |
+
# metadata must have one entry per decoded frame and use the absolute source
|
| 239 |
+
# timeline. The request endpoint is in seconds; the training manifest used
|
| 240 |
+
# the same millisecond grid and ``precomputed_absolute_pts`` backend.
|
| 241 |
+
absolute_ms = np.rint((float(req.start_time) + indices / base_fps) * 1000.0).astype(np.int64)
|
| 242 |
+
metadata = {
|
| 243 |
+
"fps": 1000,
|
| 244 |
+
"frames_indices": absolute_ms.tolist(),
|
| 245 |
+
"total_num_frames": int(absolute_ms.max()) + 1,
|
| 246 |
+
"width": int(frames.shape[2]),
|
| 247 |
+
"height": int(frames.shape[1]),
|
| 248 |
+
"duration": (int(absolute_ms.max()) + 1) / 1000.0,
|
| 249 |
+
"video_backend": "precomputed_absolute_pts",
|
| 250 |
+
}
|
| 251 |
+
return frames, metadata
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def answer_one(model, processor, req: Request) -> str:
|
| 255 |
+
"""Run the merged Qwen3.5 checkpoint on one official request."""
|
| 256 |
+
frames, metadata = your_preprocessing(clip_path_for(req), req)
|
| 257 |
+
messages = [
|
| 258 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 259 |
+
{"role": "user", "content": [{"type": "video"}, {"type": "text", "text": req.question}]},
|
| 260 |
+
]
|
| 261 |
+
text = processor.apply_chat_template(
|
| 262 |
+
messages, tokenize=False, add_generation_prompt=True, enable_thinking=False
|
| 263 |
+
)
|
| 264 |
+
inputs = processor(
|
| 265 |
+
text=[text],
|
| 266 |
+
videos=[frames],
|
| 267 |
+
video_metadata=[metadata],
|
| 268 |
+
do_sample_frames=False,
|
| 269 |
+
return_tensors="pt",
|
| 270 |
+
)
|
| 271 |
+
model_device = next(model.parameters()).device
|
| 272 |
+
inputs = inputs.to(model_device)
|
| 273 |
+
with torch.inference_mode():
|
| 274 |
+
generated = model.generate(**inputs, max_new_tokens=MAX_NEW_TOKENS, do_sample=False)
|
| 275 |
+
new_tokens = generated[:, inputs["input_ids"].shape[1] :]
|
| 276 |
+
answer = processor.batch_decode(
|
| 277 |
+
new_tokens, skip_special_tokens=True, clean_up_tokenization_spaces=False
|
| 278 |
+
)[0].strip()
|
| 279 |
+
# The official evaluator consumes the content verbatim; only remove the
|
| 280 |
+
# optional reasoning wrapper if a checkpoint emits it despite disabled
|
| 281 |
+
# thinking, preserving all answer text otherwise.
|
| 282 |
+
if "</think>" in answer:
|
| 283 |
+
answer = answer.split("</think>", 1)[1].strip()
|
| 284 |
+
return answer
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def run() -> int:
|
| 288 |
+
t_start = time.monotonic()
|
| 289 |
+
log.info("=== ORena SAVE FOCUS — inference start ===")
|
| 290 |
+
|
| 291 |
+
# ── Device ────────────────────────────────────────────────────────────────
|
| 292 |
+
# On the platform you get one GPU, chosen by you when you create the algorithm:
|
| 293 |
+
# an NVIDIA L40S (48 GB VRAM, 4 vCPUs, 32 GiB RAM) or an NVIDIA RTX PRO 6000
|
| 294 |
+
# Blackwell (96 GB VRAM, 8 vCPUs, 64 GiB RAM). This image runs on both — see
|
| 295 |
+
# the hardware table in the README.
|
| 296 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 297 |
+
log.info("Device: %s", device)
|
| 298 |
+
log_environment(device)
|
| 299 |
+
|
| 300 |
+
# ── Inputs ─────────────────────────────────────────────────────────────────
|
| 301 |
+
log.info("--- Loading inputs ---")
|
| 302 |
+
for name in ("batch.json", "request.json", "FO_definitions.json"):
|
| 303 |
+
p = INPUT_PATH / name
|
| 304 |
+
status = f"{p.stat().st_size} bytes" if p.exists() else "MISSING"
|
| 305 |
+
log.info(" %-24s %s", name, status)
|
| 306 |
+
log.info(" %-24s %s", f"{VIDEO_DIR.name}/", f"{len(list(VIDEO_DIR.glob('*.mp4')))} clip(s)" if VIDEO_DIR.is_dir() else "MISSING")
|
| 307 |
+
|
| 308 |
+
# request.json holds the whole batch: answer every question it lists.
|
| 309 |
+
requests = load_requests(INPUT_PATH / "request.json")
|
| 310 |
+
if not requests:
|
| 311 |
+
log.error("request.json contains no requests")
|
| 312 |
+
return 1
|
| 313 |
+
log.info("Batch of %d question(s)", len(requests))
|
| 314 |
+
|
| 315 |
+
# FO_definitions.json holds plain text, JSON-encoded; json.loads() gives the
|
| 316 |
+
# text back. The definitions are the same for every question.
|
| 317 |
+
fo_definitions = json.loads((INPUT_PATH / "FO_definitions.json").read_text())
|
| 318 |
+
log.info("FO definitions: %d chars", len(fo_definitions))
|
| 319 |
+
|
| 320 |
+
# ── Model — loaded ONCE for the whole batch ────────────────────────────────
|
| 321 |
+
# Keep this outside the per-question loop. Loading the model is paid once per
|
| 322 |
+
# run, and the latency budget grants a setup allowance to cover it.
|
| 323 |
+
log.info("--- Loading model (once for the batch) ---")
|
| 324 |
+
if not MODEL_PATH.exists():
|
| 325 |
+
log.error("Merged model directory not found: %s", MODEL_PATH)
|
| 326 |
+
return 1
|
| 327 |
+
log.info("Merged model path: %s", MODEL_PATH)
|
| 328 |
+
processor = AutoProcessor.from_pretrained(MODEL_PATH, local_files_only=True)
|
| 329 |
+
model = Qwen3_5ForConditionalGeneration.from_pretrained(
|
| 330 |
+
MODEL_PATH,
|
| 331 |
+
torch_dtype=torch.bfloat16,
|
| 332 |
+
device_map="auto",
|
| 333 |
+
low_cpu_mem_usage=True,
|
| 334 |
+
local_files_only=True,
|
| 335 |
+
)
|
| 336 |
+
model.eval()
|
| 337 |
+
n_params = sum(p.numel() for p in model.parameters())
|
| 338 |
+
log.info("Model loaded and set to eval mode: %d parameters (setup %.2f s)", n_params, time.monotonic() - t_start)
|
| 339 |
+
|
| 340 |
+
if device.type == "cuda":
|
| 341 |
+
allocated = torch.cuda.memory_allocated(0) / 1024**2
|
| 342 |
+
reserved = torch.cuda.memory_reserved(0) / 1024**2
|
| 343 |
+
log.info(" VRAM after model load: %.1f MiB allocated / %.1f MiB reserved", allocated, reserved)
|
| 344 |
+
|
| 345 |
+
# ── Inference — one pass per question ──────────────────────────────────────
|
| 346 |
+
log.info("--- Running inference over %d question(s) ---", len(requests))
|
| 347 |
+
responses = []
|
| 348 |
+
n_failed = 0
|
| 349 |
+
t_batch = time.monotonic()
|
| 350 |
+
|
| 351 |
+
for i, req in enumerate(requests, start=1):
|
| 352 |
+
log.info(
|
| 353 |
+
"[%d/%d] qID=%s videoID=%s window=[%.2f, %.2f] s (%.2f s)",
|
| 354 |
+
i, len(requests), req.qID, req.videoID, req.start_time, req.end_time, req.duration,
|
| 355 |
+
)
|
| 356 |
+
t0 = time.monotonic()
|
| 357 |
+
try:
|
| 358 |
+
answer = answer_one(model, processor, req)
|
| 359 |
+
except Exception:
|
| 360 |
+
# Keep going: one failed question should not cost you the rest of the
|
| 361 |
+
# batch. An empty answer is simply scored incorrect.
|
| 362 |
+
n_failed += 1
|
| 363 |
+
log.exception("[%d/%d] qID=%s failed; emitting empty answer", i, len(requests), req.qID)
|
| 364 |
+
answer = ""
|
| 365 |
+
latency = time.monotonic() - t0
|
| 366 |
+
|
| 367 |
+
# Informational only. The platform times the run itself and scores against
|
| 368 |
+
# that measurement — see "The latency budget" in the module docstring.
|
| 369 |
+
responses.append(Response(qID=req.qID, content=answer, latency=latency))
|
| 370 |
+
log.info("[%d/%d] qID=%s answered in %.4f s: %r", i, len(requests), req.qID, latency, answer)
|
| 371 |
+
|
| 372 |
+
batch_seconds = time.monotonic() - t_batch
|
| 373 |
+
log.info(
|
| 374 |
+
"Inference complete: %d answered (%d failed) in %.2f s (%.3f s/question)",
|
| 375 |
+
len(responses), n_failed, batch_seconds, batch_seconds / max(len(responses), 1),
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
if device.type == "cuda":
|
| 379 |
+
allocated = torch.cuda.memory_allocated(0) / 1024**2
|
| 380 |
+
log.debug(" VRAM after inference: %.1f MiB allocated", allocated)
|
| 381 |
+
|
| 382 |
+
# ── Output ─────────────────────────────────────────────────────────────────
|
| 383 |
+
log.info("--- Writing output ---")
|
| 384 |
+
OUTPUT_PATH.mkdir(parents=True, exist_ok=True)
|
| 385 |
+
output_path = OUTPUT_PATH / "answer.json"
|
| 386 |
+
save_items(responses, output_path)
|
| 387 |
+
log.info("Wrote %d response(s) to %s", len(responses), output_path)
|
| 388 |
+
log.info("=== inference done in %.2f s total ===", time.monotonic() - t_start)
|
| 389 |
+
return 0
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
if __name__ == "__main__":
|
| 393 |
+
raise SystemExit(run())
|
pipelines/Tr006_V2/docker/segment-algorithm/requirements.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pins the torch already in the base image — the recommended CUDA 12.8 build (see
|
| 2 |
+
# the Dockerfile for why it is the default). Nothing is downloaded: pip treats the
|
| 3 |
+
# preinstalled build as satisfying this. Its only job is to turn "a dependency of
|
| 4 |
+
# mine wants a different torch" into an immediate, readable pip error naming that
|
| 5 |
+
# dependency, rather than a silent swap to the CUDA 13.0 wheel that cannot run on
|
| 6 |
+
# the L40S. Changing torch is allowed — if you do, edit this pin knowingly, and the
|
| 7 |
+
# Dockerfile's guard will tell you whether the result still suits the GPU you target.
|
| 8 |
+
torch==2.11.0
|
| 9 |
+
|
| 10 |
+
orena-focus>=0.3.4
|
| 11 |
+
|
| 12 |
+
# Direct imports of inference.py. They also arrive as transitive dependencies
|
| 13 |
+
# of orena-focus, but anything imported directly is declared explicitly.
|
| 14 |
+
decord>=0.6.0
|
| 15 |
+
numpy>=1.26.0
|
| 16 |
+
|
| 17 |
+
# Qwen3.5 support (including Qwen3_5ForConditionalGeneration and the
|
| 18 |
+
# timestamp-aware Qwen3VLProcessor) landed after the 4.x line. The merged
|
| 19 |
+
# checkpoint was produced and verified with this exact version.
|
| 20 |
+
transformers==5.15.0
|
| 21 |
+
accelerate>=1.10.1
|
pipelines/Tr006_V2/docker/segment-algorithm/resources/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# `resources/` — your model lives here
|
| 2 |
+
|
| 3 |
+
Everything in this folder is **copied into the container image** at build time and
|
| 4 |
+
is available at inference. This is where your trained model belongs.
|
| 5 |
+
|
| 6 |
+
| File | What it is | Replace it? |
|
| 7 |
+
|:--|:--|:--|
|
| 8 |
+
| `model.py` | the model definition (`DummyModel`, a trivial CNN) | **yes** — with your architecture |
|
| 9 |
+
| `dummy_weights.pt` | a tiny checkpoint so the template runs out of the box | **yes** — with your trained weights |
|
| 10 |
+
|
| 11 |
+
`inference.py` imports from here:
|
| 12 |
+
|
| 13 |
+
```python
|
| 14 |
+
from resources.model import DummyModel
|
| 15 |
+
|
| 16 |
+
WEIGHTS_PATH = RESOURCES_PATH / "dummy_weights.pt"
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
Keep that wiring, or point it at whatever you name your own files.
|
| 20 |
+
|
| 21 |
+
## Things to get right
|
| 22 |
+
|
| 23 |
+
**There is no internet at inference time.** The container runs with networking
|
| 24 |
+
disabled, so nothing can be downloaded at runtime — no model hubs, no checkpoints,
|
| 25 |
+
no tokenizer files, no fonts. Everything your model touches must be inside the
|
| 26 |
+
image. If you use a library that lazily fetches weights on first call (many
|
| 27 |
+
`transformers`, `timm`, and `open_clip` entry points do), pre-download during the
|
| 28 |
+
Docker build and load from a local path instead.
|
| 29 |
+
|
| 30 |
+
**Load your model once.** One container run answers a whole batch of questions, so
|
| 31 |
+
the checkpoint is loaded once and reused across every question in that batch. Keep
|
| 32 |
+
model loading outside the per-question loop in `inference.py` — that is where the
|
| 33 |
+
setup allowance in the latency budget comes from.
|
| 34 |
+
|
| 35 |
+
**Large checkpoints need splitting.** A single image layer cannot exceed **50 GB**,
|
| 36 |
+
and each Docker `COPY` is one layer. If your weights are bigger than that, split
|
| 37 |
+
them into chunks, copy them with several `COPY` instructions, and reassemble at
|
| 38 |
+
runtime — see the comment in the `Dockerfile`.
|
| 39 |
+
|
| 40 |
+
**Big weights make big images.** The whole image is pulled before your code starts.
|
| 41 |
+
That pull is infrastructure overhead and is not charged against your latency
|
| 42 |
+
budget, but it does slow every submission cycle, so keep the image only as large as
|
| 43 |
+
it needs to be.
|
pipelines/Tr006_V2/docker/segment-algorithm/resources/dummy_weights.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:907b28b86b4d3262750ad946ab0384a40887e239b5be4027a53592817206dc2a
|
| 3 |
+
size 26973
|
pipelines/Tr006_V2/docker/segment-algorithm/resources/model.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class DummyModel(nn.Module):
|
| 5 |
+
"""Minimal CNN for verifying GPU execution. Replace with your real model."""
|
| 6 |
+
|
| 7 |
+
def __init__(self):
|
| 8 |
+
super().__init__()
|
| 9 |
+
self.features = nn.Sequential(
|
| 10 |
+
nn.Conv2d(3, 16, kernel_size=5, padding=1),
|
| 11 |
+
nn.ReLU(inplace=True),
|
| 12 |
+
nn.AdaptiveAvgPool2d(8),
|
| 13 |
+
nn.Conv2d(16, 32, kernel_size=3, padding=1),
|
| 14 |
+
nn.ReLU(inplace=True),
|
| 15 |
+
nn.AdaptiveAvgPool2d(1),
|
| 16 |
+
)
|
| 17 |
+
self.classifier = nn.Linear(32, 2)
|
| 18 |
+
|
| 19 |
+
def forward(self, x):
|
| 20 |
+
return self.classifier(self.features(x).flatten(1))
|
pipelines/Tr006_V2/docker/segment-algorithm/resources/model_provenance.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_family": "Qwen3.5-9B",
|
| 3 |
+
"weight_form": "merged_lora",
|
| 4 |
+
"lora_merged": true,
|
| 5 |
+
"source_base": "/cpfs01/projects-HDD/cfff-906dc71fafda_HDD/gbw_21307130160/cache/modelscope/models/Qwen--Qwen3.5-9B/snapshots/master",
|
| 6 |
+
"source_adapter_checkpoint": "/cpfs01/projects-HDD/cfff-906dc71fafda_HDD/gbw_21307130160/challenges/FARM-ORENA/experiments/Qwen3.5-9B/Tr/Tr006_offline_fixed80_absmeta_v2_lora64_bs4_10ep_20260828/results/checkpoint_archive/checkpoint-6013",
|
| 7 |
+
"source_step": 6013,
|
| 8 |
+
"source_epoch": 7.0,
|
| 9 |
+
"merge_report": "model/merge_report.json",
|
| 10 |
+
"merge_validation": {
|
| 11 |
+
"remaining_lora_modules_after_merge": 0,
|
| 12 |
+
"adapted_components": ["language_model", "visual", "visual_merger"]
|
| 13 |
+
},
|
| 14 |
+
"runtime_model_class": "Qwen3_5ForConditionalGeneration",
|
| 15 |
+
"runtime_processor_class": "Qwen3VLProcessor",
|
| 16 |
+
"runtime_transformers": "5.15.0"
|
| 17 |
+
}
|
pipelines/Tr006_V2/docker/segment-algorithm/test/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
output/
|
pipelines/Tr006_V2/expected_outputs/v1/provenance.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "fixed_frames_v1",
|
| 3 |
+
"created_at_utc": "2026-08-26T23:09:17.066162+00:00",
|
| 4 |
+
"command": "/cpfs01/projects-HDD/cfff-906dc71fafda_HDD/gbw_21307130160/challenges/FARM-ORENA/scripts/preprocess_segmentqa_fixed_frames.py --data-root /cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge --output-dir /cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/derived/orena_segmentqa_fixed80_420x224_absmeta_v1/clean --splits train test --frames 80 --width 420 --height 224 --workers 8 --resume",
|
| 5 |
+
"inputs": {
|
| 6 |
+
"heico-focus-vqa": {
|
| 7 |
+
"train": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/heico-focus-vqa/data/segment/train.parquet",
|
| 8 |
+
"test": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/heico-focus-vqa/data/segment/test.parquet",
|
| 9 |
+
"videos": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/heico-focus-vqa/videos"
|
| 10 |
+
},
|
| 11 |
+
"lapchole-focus-vqa": {
|
| 12 |
+
"train": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/lapchole-focus-vqa/data/segment/train.parquet",
|
| 13 |
+
"test": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/lapchole-focus-vqa/data/segment/test.parquet",
|
| 14 |
+
"videos": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/lapchole-focus-vqa/videos"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"splits": [
|
| 18 |
+
"train",
|
| 19 |
+
"test"
|
| 20 |
+
],
|
| 21 |
+
"question_count": 20000,
|
| 22 |
+
"unique_segment_count": 18478,
|
| 23 |
+
"failed_count": 0,
|
| 24 |
+
"frame_count": 80,
|
| 25 |
+
"sampling_rule": "uniform PTS bin centers in [start,end), nearest real source PTS",
|
| 26 |
+
"resize": "proportional letterbox 420x224",
|
| 27 |
+
"encoding": {
|
| 28 |
+
"container": "mp4",
|
| 29 |
+
"codec": "H.264/libx264",
|
| 30 |
+
"audio": "none",
|
| 31 |
+
"pix_fmt": "yuv420p"
|
| 32 |
+
},
|
| 33 |
+
"metadata_contract": {
|
| 34 |
+
"fps": 1000,
|
| 35 |
+
"frames_indices": "round(abs_pts*1000)",
|
| 36 |
+
"total_num_frames": "max(frames_indices)+1",
|
| 37 |
+
"width": 420,
|
| 38 |
+
"height": 224,
|
| 39 |
+
"duration": "total_num_frames/fps",
|
| 40 |
+
"video_backend": "precomputed_absolute_pts"
|
| 41 |
+
},
|
| 42 |
+
"system_prompt_source": {
|
| 43 |
+
"author_repo_commit": "3fc0850f9f76261c529e75c78ad2272558ff9e4e",
|
| 44 |
+
"source": "https://github.com/AzJ24/Orena/blob/3fc0850f9f76261c529e75c78ad2272558ff9e4e/orena_sft/prompts.py",
|
| 45 |
+
"function": "build_system_prompt",
|
| 46 |
+
"parameters": {
|
| 47 |
+
"include_definitions": false,
|
| 48 |
+
"style": "direct",
|
| 49 |
+
"track": "segment"
|
| 50 |
+
},
|
| 51 |
+
"foreign_objects_source": "orena-focus 0.3.4 wheel: focus/foreign_objects.py"
|
| 52 |
+
},
|
| 53 |
+
"system_prompt": "You are a surgical video analysis assistant. You are shown a short clip from a laparoscopic procedure and asked a single question about it.\n\nThe clip is given as a sequence of frames in chronological order. Each frame is preceded by its timestamp in the source video, written as <SECONDS seconds> -- a frame marked <1234.5 seconds> was taken 1234.5 seconds (00:20:34) after the video began. Consecutive frames may be several seconds apart, so an event can happen between two of them.\n\nA foreign object (FO) is any object fully introduced into the patient's body\ncavity during surgery that must be retrieved or accounted for. Importantly,\nstandard surgical instruments that remain connected to the external environment\n(e.g., graspers, scissors, trocars, staplers, cameras) are not considered foreign\nobjects. Furthermore, we exclude detachable parts of surgical instruments,\nparticularly anvil components of staplers.\n\nThe foreign object classes are exactly: Sponge, Clip, Specimen Bag, Silicone Loop, External Drain, Needle, Gallstone, Specimen, Mesh, Absorbable Hemostatic Agent.\n\nReply with the answer and nothing else -- no reasoning, no preamble, no\nexplanation, no restating the question. A single short line.\n\nRules for the answer:\n- Write the value only. No sentence, no explanation, no units, no trailing\n period, and never repeat the question.\n- Asks yes or no -> write exactly: yes or no\n- Asks how many / for a count -> write digits only, e.g. 0 or 1 or 2.\n- Asks which foreign object class(es) -> write class names exactly as spelled\n in the list above, comma-separated (e.g. Clip, Sponge), or exactly: none\n Never answer with a generic description such as \"surgical instrument\".\n- Asks when something happens -> write hh:mm:ss, read off the frame timestamps.\n Those timestamps count from the START OF THE VIDEO, not from the start of this\n clip, so <1234.5 seconds> is 00:20:34. Convert carefully: 3600 seconds is one\n hour, 60 seconds is one minute. If the event falls between two frames, give\n your best estimate between their timestamps rather than snapping to one.\n For several time points, separate them with commas: 00:09:19, 00:12:44\n- Asks for a percentage or a share -> write the number only, e.g. 40\n- Lists options to choose from -> copy exactly one of those options, verbatim.\n- Anything else -> a short phrase, at most a few words.\n\nIf you are unsure, still commit to your single best answer in the required\nform. An empty, hedged, or explanatory answer is scored as wrong.\n",
|
| 54 |
+
"output_dir": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/derived/orena_segmentqa_fixed80_420x224_absmeta_v1/clean",
|
| 55 |
+
"resume": true,
|
| 56 |
+
"workers": 8,
|
| 57 |
+
"limit": null,
|
| 58 |
+
"seed": 42
|
| 59 |
+
}
|
pipelines/Tr006_V2/expected_outputs/v1/validation_report.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"status": "pass",
|
| 3 |
+
"expected_frame_count": 80,
|
| 4 |
+
"expected_width": 420,
|
| 5 |
+
"expected_height": 224,
|
| 6 |
+
"segments": 18478,
|
| 7 |
+
"success": 18478,
|
| 8 |
+
"missing": 0,
|
| 9 |
+
"failures": 0,
|
| 10 |
+
"checked_at_utc": "2026-08-26T23:09:17.066162+00:00"
|
| 11 |
+
}
|
pipelines/Tr006_V2/expected_outputs/v2/provenance.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "fixed_frames_blueclean_v2",
|
| 3 |
+
"created_at_utc": "2026-08-27T03:55:02.119997+00:00",
|
| 4 |
+
"command": "scripts/preprocess_segmentqa_blueclean_v2.py --data-root /cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge --output-dir /cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/derived/orena_segmentqa_fixed80_420x224_absmeta_v2/clean --splits train test --frames 80 --width 420 --height 224 --workers 8 --min-train-valid-frames 16 --resume",
|
| 5 |
+
"data_root": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge",
|
| 6 |
+
"v1_dir": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/derived/orena_segmentqa_fixed80_420x224_absmeta_v1/clean",
|
| 7 |
+
"blue_intervals": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/heico-focus-vqa/blue_screen_temporal_annotations/data/intervals.csv",
|
| 8 |
+
"output_dir": "/cpfs01/projects-SSD/cfff-906dc71fafda_SSD/gbw_21307130160/dataset/challenge/derived/orena_segmentqa_fixed80_420x224_absmeta_v2/clean",
|
| 9 |
+
"splits": [
|
| 10 |
+
"train",
|
| 11 |
+
"test"
|
| 12 |
+
],
|
| 13 |
+
"frame_count": 80,
|
| 14 |
+
"resize": "proportional letterbox 420x224",
|
| 15 |
+
"encoding": {
|
| 16 |
+
"container": "mp4",
|
| 17 |
+
"codec": "H.264/libx264",
|
| 18 |
+
"audio": "none"
|
| 19 |
+
},
|
| 20 |
+
"sampling_rule": "80 uniform bin centers over the non-blue complement inside the original [start,end)",
|
| 21 |
+
"min_train_valid_frames": 16,
|
| 22 |
+
"unaffected_copy": "cp --reflink=auto; byte-identical verification; no hardlinks",
|
| 23 |
+
"test_answers_used_for_decisions": false,
|
| 24 |
+
"resume": true,
|
| 25 |
+
"workers": 8,
|
| 26 |
+
"limit": null
|
| 27 |
+
}
|
pipelines/Tr006_V2/expected_outputs/v2/validation_report.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"status": "pass",
|
| 3 |
+
"expected_frame_count": 80,
|
| 4 |
+
"expected_width": 420,
|
| 5 |
+
"expected_height": 224,
|
| 6 |
+
"segments_total": 18478,
|
| 7 |
+
"affected_temporal_overlap": 1215,
|
| 8 |
+
"unaffected": 17263,
|
| 9 |
+
"included_segments": 18478,
|
| 10 |
+
"excluded_train_segments": 0,
|
| 11 |
+
"excluded_train_questions": 0,
|
| 12 |
+
"train_questions": 13746,
|
| 13 |
+
"test_questions": 6254,
|
| 14 |
+
"test_unique_segments": 5661,
|
| 15 |
+
"actions": {
|
| 16 |
+
"copied": 17263,
|
| 17 |
+
"resampled_excluding_blue": 1214,
|
| 18 |
+
"retained_test_insufficient_valid": 1
|
| 19 |
+
},
|
| 20 |
+
"missing": 0,
|
| 21 |
+
"failures": 0,
|
| 22 |
+
"full_dataset_contract": true,
|
| 23 |
+
"checked_at_utc": "2026-08-27T03:55:02.119997+00:00"
|
| 24 |
+
}
|
pipelines/Tr006_V2/manifest/pipeline_manifest.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bundle_id": "Tr006_V2_checkpoint-6013",
|
| 3 |
+
"track": "segment",
|
| 4 |
+
"project_commit": "b66edaeba3c484c431d785f95a7fc81cffa530c0",
|
| 5 |
+
"code_state": "dirty; exact files included in this bundle are authoritative",
|
| 6 |
+
"base_model": "Qwen3.5-9B snapshots/master; not included",
|
| 7 |
+
"adapter_path": "weights/Tr006_V2/checkpoint-6013",
|
| 8 |
+
"raw_data": {
|
| 9 |
+
"included": false,
|
| 10 |
+
"required_layout": "README.md#原始数据目录契约",
|
| 11 |
+
"parquet_sha256": {
|
| 12 |
+
"heico-focus-vqa/data/segment/train.parquet": "ee6b1385a708ebe284cca5368e918bc62d72fb3bad2d9fc342d7a4fe4562d4fe",
|
| 13 |
+
"heico-focus-vqa/data/segment/test.parquet": "da7891b52269d27b89e21237efdaea7f51b3ee3ad0f82573eebdf6fbbf6985e9",
|
| 14 |
+
"lapchole-focus-vqa/data/segment/train.parquet": "f2198a516e9bb051a9071c2a80c1389fc9f283d81357d4e52f4d280f18f6eb70",
|
| 15 |
+
"lapchole-focus-vqa/data/segment/test.parquet": "f4522f845ceb6a3847f45d90f0bba84aa7df98abc4dd5e4cfd4fb914ceccac95"
|
| 16 |
+
}
|
| 17 |
+
},
|
| 18 |
+
"blue_intervals": {
|
| 19 |
+
"count": 323,
|
| 20 |
+
"sha256": "d523a529226f9bb8ec06147874a6321e1f5c201700d55a09fd8baecdb1240f03"
|
| 21 |
+
},
|
| 22 |
+
"v1_expected": {
|
| 23 |
+
"segments": 18478,
|
| 24 |
+
"frames": 80,
|
| 25 |
+
"width": 420,
|
| 26 |
+
"height": 224,
|
| 27 |
+
"train_jsonl_sha256": "a84cb85db66665a7b37d7827104ae053c1ef710a748b56d5d90198a71078f7c2",
|
| 28 |
+
"segment_manifest_sha256": "1a61c9e60647af24f7019e9fa0209aed10d63e2a0bf9d7ccb5cccd75eb3d06a6"
|
| 29 |
+
},
|
| 30 |
+
"v2_expected": {
|
| 31 |
+
"segments": 18478,
|
| 32 |
+
"affected_temporal_overlap": 1215,
|
| 33 |
+
"train_questions": 13746,
|
| 34 |
+
"test_questions": 6254,
|
| 35 |
+
"frames": 80,
|
| 36 |
+
"width": 420,
|
| 37 |
+
"height": 224,
|
| 38 |
+
"metadata_backend": "precomputed_absolute_pts",
|
| 39 |
+
"train_jsonl_sha256": "197800db3323b311a8ffb2c6459919d3b4db3ca1487b2f8790c7c5eacf16a4e2",
|
| 40 |
+
"segment_manifest_sha256": "6098c0abb1843be6ac24498914fb9c43c19d8bc4c2d034fbffb0d66456ad6414"
|
| 41 |
+
},
|
| 42 |
+
"training": {
|
| 43 |
+
"transformers": "5.15.0",
|
| 44 |
+
"ms_swift": "4.5.0.dev0",
|
| 45 |
+
"epochs": 10,
|
| 46 |
+
"selected_checkpoint_step": 6013,
|
| 47 |
+
"selected_checkpoint_epoch": 7.0,
|
| 48 |
+
"frames": 80,
|
| 49 |
+
"resolution": "420x224 letterbox",
|
| 50 |
+
"precision": "bfloat16",
|
| 51 |
+
"learning_rate": "1e-4",
|
| 52 |
+
"lora_rank": 64,
|
| 53 |
+
"lora_alpha": 128
|
| 54 |
+
},
|
| 55 |
+
"submission_image_snapshot": {
|
| 56 |
+
"image_id": "sha256:3ea66ddf64a40c9d2389f7c78326ab3728d54d6bb287b0cfa27cdee391827c5f",
|
| 57 |
+
"runtime_backend": "transformers Qwen3_5ForConditionalGeneration",
|
| 58 |
+
"actual_gpu_interface_smoke_before_submission": false
|
| 59 |
+
}
|
| 60 |
+
}
|
pipelines/Tr006_V2/manifest/pipeline_requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Raw blue-screen detection
|
| 2 |
+
opencv-python==4.11.0.86
|
| 3 |
+
numpy==2.4.6
|
| 4 |
+
|
| 5 |
+
# Segment fixed80 V1/V2 generation
|
| 6 |
+
av==18.1.0
|
| 7 |
+
pandas==2.2.3
|
| 8 |
+
Pillow==11.3.0
|
| 9 |
+
pyarrow
|
| 10 |
+
|
| 11 |
+
# Training/merge versions recorded for Tr006
|
| 12 |
+
ms-swift==4.5.0.dev0
|
| 13 |
+
transformers==5.15.0
|
| 14 |
+
peft==0.19.1
|
| 15 |
+
safetensors==0.8.0
|
pipelines/Tr006_V2/project/data_preprocessing/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 蓝屏时序预处理脚本说明
|
| 2 |
+
|
| 3 |
+
本目录用于检测胃肠镜视频中的冻结蓝屏区段、细化时间边界,并将结果整理为可发布的数据集。所有检测脚本均以原始视频为只读输入,不会裁剪、覆盖或移动源视频。
|
| 4 |
+
|
| 5 |
+
## 推荐流程
|
| 6 |
+
|
| 7 |
+
```text
|
| 8 |
+
视频目录
|
| 9 |
+
-> process_blue_screen_dataset.py
|
| 10 |
+
-> results/{intervals.csv,videos.jsonl,per_video/,evidence/,summary.json}
|
| 11 |
+
-> 结果验收及 validation.json
|
| 12 |
+
-> prepare_blue_screen_hf_release.py
|
| 13 |
+
-> 可上传的 HF Dataset 目录
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
正式批量处理优先使用 `process_blue_screen_dataset.py`。`blue_screen_detector.py`、`fast_blue_screen_detector.py` 和 `refine_blue_intervals.py` 主要用于方法验证、单视频诊断和边界复核,不应替代正式批处理入口。
|
| 17 |
+
|
| 18 |
+
## 脚本清单
|
| 19 |
+
|
| 20 |
+
### `process_blue_screen_dataset.py`
|
| 21 |
+
|
| 22 |
+
正式的全数据集粗到细检测入口。
|
| 23 |
+
|
| 24 |
+
- 输入:包含 `.avi`、`.mp4`、`.mov` 或 `.mkv` 视频的目录。
|
| 25 |
+
- 粗检:顺序解码视频,每秒抽取一次圆形内镜 ROI,对 ROI 像素计算哈希和蓝色比例,定位连续静止蓝屏候选区段。
|
| 26 |
+
- 精修:只对候选区段及其前后缓冲区逐帧扫描,以原生 FPS 确定准确的开始帧和结束帧。
|
| 27 |
+
- 判定:ROI 解码帧哈希完全相同,同时蓝色比例达到阈值并满足最短持续时间。
|
| 28 |
+
- 支持 `--resume`,可复用已经完成的逐视频 JSON 结果。
|
| 29 |
+
- 输出:
|
| 30 |
+
- `intervals.csv`:每个蓝屏区段一行,是主要时序标注。
|
| 31 |
+
- `videos.jsonl`:每个视频一行,包含视频信息和全部区段。
|
| 32 |
+
- `per_video/<video_id>.json`:逐视频检查点和详细结果。
|
| 33 |
+
- `evidence/<video_id>/`:每个区段的前一帧、起始帧、结束帧和后一帧证据图。
|
| 34 |
+
- `summary.json`:全数据集汇总统计。
|
| 35 |
+
|
| 36 |
+
正式 HeiCo 运行使用的主要参数为:1 秒粗采样、最短静止 6 秒、蓝色比例阈值 0.45、候选区段前后各扩展 5 秒、粗检尺寸 96、精修尺寸 160。
|
| 37 |
+
|
| 38 |
+
### `fast_blue_screen_detector.py`
|
| 39 |
+
|
| 40 |
+
单视频快速哈希检测工具,用于验证“蓝屏期间抽样帧是否完全相同”的假设。
|
| 41 |
+
|
| 42 |
+
- 顺序解码视频,但默认只每秒生成一个 ROI 哈希和蓝色比例。
|
| 43 |
+
- 将连续相同哈希且持续时间足够长的样本合并为粗粒度区段。
|
| 44 |
+
- 可通过重复传入 `--reference START END` 与用户提供的参考区段计算近似秒级 precision/recall。
|
| 45 |
+
- 输出单个 JSON;时间边界精度由 `--sample-seconds` 决定,不能代替逐帧精修结果。
|
| 46 |
+
|
| 47 |
+
### `refine_blue_intervals.py`
|
| 48 |
+
|
| 49 |
+
针对已知粗区段的单视频逐帧边界精修工具。
|
| 50 |
+
|
| 51 |
+
- 通过一个或多个 `--interval START END` 接收粗区段。
|
| 52 |
+
- 在粗区段前后增加 `--pad` 秒扫描范围。
|
| 53 |
+
- 逐帧计算圆形 ROI 蓝色比例,并通过连续确认帧抑制孤立噪声。
|
| 54 |
+
- 输出精修后的帧号、秒级时间、持续时长和平均蓝色比例。
|
| 55 |
+
- 适合人工复核或早期实验;正式批处理采用 `process_blue_screen_dataset.py` 内置的“完全重复帧哈希 + 蓝色比例”精修实现。
|
| 56 |
+
|
| 57 |
+
### `blue_screen_detector.py`
|
| 58 |
+
|
| 59 |
+
早期单视频候选检测工具,用于验证基于颜色和纹理的蓝屏判定方法。
|
| 60 |
+
|
| 61 |
+
- 自动估计圆形内镜 ROI,忽略外围黑色区域。
|
| 62 |
+
- 稀疏抽帧后计算蓝色比例、拉普拉斯纹理强度、平均色相和有效像素比例。
|
| 63 |
+
- 将高蓝色比例、低纹理且持续时间足够长的样本合并为候选区段。
|
| 64 |
+
- 输出详细 JSON 和区段 CSV。
|
| 65 |
+
- 该方法没有完全相同帧哈希约束,结果属于候选范围,不作为当前正式蓝屏时序标注。
|
| 66 |
+
|
| 67 |
+
### `prepare_blue_screen_hf_release.py`
|
| 68 |
+
|
| 69 |
+
将已经验收通过的运行结果整理为可移植的 Hugging Face Dataset 发布目录。
|
| 70 |
+
|
| 71 |
+
- 要求源运行包含 `run.json`、`results/intervals.csv`、`videos.jsonl`、`summary.json`、`validation.json` 和 `evidence/`。
|
| 72 |
+
- 只有 `validation.json` 状态为 `PASS` 时才允许打包。
|
| 73 |
+
- 删除结果中的服务器绝对视频路径,改用 `video_id` 和 `video_filename`。
|
| 74 |
+
- 复制边界证据图并生成便携的 provenance、summary、validation 和 release manifest。
|
| 75 |
+
- 对视频数、区段数和证据图数量进行一致性核验。
|
| 76 |
+
- 为避免误覆盖,目标输出目录已存在时会直接停止。
|
| 77 |
+
|
| 78 |
+
## 时间区间语义
|
| 79 |
+
|
| 80 |
+
蓝屏区段统一使用半开区间 `[start_seconds, end_seconds)`:开始时间包含在区段内,结束时间指向蓝屏结束后的第一个时间位置。逐帧结果同时提供 `start_frame` 和 `end_frame`,其中帧号边界为首尾均包含。
|
| 81 |
+
|
| 82 |
+
## 依赖与运行环境
|
| 83 |
+
|
| 84 |
+
- Python 3
|
| 85 |
+
- OpenCV (`cv2`)
|
| 86 |
+
- NumPy
|
| 87 |
+
|
| 88 |
+
FVQA 项目中的正式运行应使用 `fvqa-harness.yaml` 指定的 Conda 环境。蓝屏检测只使用 CPU,不需要占用 GPU。
|
| 89 |
+
|
| 90 |
+
## 当前正式结果
|
| 91 |
+
|
| 92 |
+
HeiCo 30 个视频的已验收结果位于 HF Dataset 仓库 `Gbone3176/orena-seg` 的 `blue_screen_temporal_annotations/` 子目录:共 323 个蓝屏区段和 1,291 张边界证据图。该结果通过了文件覆盖、逐帧解码、区��边界和汇总一致性检查;这里的 `PASS` 表示产物一致性验收通过,不代表基于独立人工金标准计算的检测准确率。
|
pipelines/Tr006_V2/project/data_preprocessing/process_blue_screen_dataset.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Coarse-to-fine frozen blue-screen detection for endoscopy videos.
|
| 3 |
+
|
| 4 |
+
Stage 1 decodes each video sequentially and hashes one circular-ROI frame per
|
| 5 |
+
second. Stage 2 re-reads only candidate windows and locates exact repeated-frame
|
| 6 |
+
runs at native FPS. Inputs are read-only; all results go below --output-dir.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import csv
|
| 12 |
+
import hashlib
|
| 13 |
+
import json
|
| 14 |
+
import socket
|
| 15 |
+
import time
|
| 16 |
+
from datetime import datetime, timezone
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
import cv2
|
| 20 |
+
import numpy as np
|
| 21 |
+
|
| 22 |
+
VIDEO_SUFFIXES = {".avi", ".mp4", ".mov", ".mkv"}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def utc_now() -> str:
|
| 26 |
+
return datetime.now(timezone.utc).astimezone().isoformat()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def hhmmss(seconds: float) -> str:
|
| 30 |
+
ms = int(round(seconds * 1000))
|
| 31 |
+
hours, rem = divmod(ms, 3_600_000)
|
| 32 |
+
minutes, rem = divmod(rem, 60_000)
|
| 33 |
+
secs, millis = divmod(rem, 1000)
|
| 34 |
+
return f"{hours:02d}:{minutes:02d}:{secs:02d}.{millis:03d}"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def roi_mask(height: int, width: int) -> np.ndarray:
|
| 38 |
+
yy, xx = np.ogrid[:height, :width]
|
| 39 |
+
return (((xx - width / 2) / (width * 0.47)) ** 2 +
|
| 40 |
+
((yy - height / 2) / (height * 0.47)) ** 2 <= 1)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def frame_features(frame: np.ndarray, mask: np.ndarray, size: int) -> tuple[str, float]:
|
| 44 |
+
small = cv2.resize(frame, (size, size), interpolation=cv2.INTER_AREA)
|
| 45 |
+
small_mask = cv2.resize(mask.astype(np.uint8), (size, size),
|
| 46 |
+
interpolation=cv2.INTER_NEAREST).astype(bool)
|
| 47 |
+
pixels = small[small_mask]
|
| 48 |
+
digest = hashlib.md5(pixels.tobytes()).hexdigest()
|
| 49 |
+
hsv = cv2.cvtColor(small, cv2.COLOR_BGR2HSV)
|
| 50 |
+
h, s, v = hsv[..., 0], hsv[..., 1], hsv[..., 2]
|
| 51 |
+
valid = small_mask & (v > 18)
|
| 52 |
+
if not valid.any():
|
| 53 |
+
return digest, 0.0
|
| 54 |
+
blue = valid & (h >= 88) & (h <= 140) & (s >= 55) & (v >= 35)
|
| 55 |
+
return digest, float(blue.sum() / valid.sum())
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def coarse_scan(video: Path, sample_seconds: float, min_static_seconds: float,
|
| 59 |
+
blue_threshold: float, size: int) -> tuple[dict, list[dict]]:
|
| 60 |
+
cap = cv2.VideoCapture(str(video))
|
| 61 |
+
if not cap.isOpened():
|
| 62 |
+
raise RuntimeError("cannot open video")
|
| 63 |
+
fps = float(cap.get(cv2.CAP_PROP_FPS) or 0)
|
| 64 |
+
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
| 65 |
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0)
|
| 66 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0)
|
| 67 |
+
if fps <= 0 or frame_count <= 0 or width <= 0 or height <= 0:
|
| 68 |
+
cap.release()
|
| 69 |
+
raise RuntimeError(f"invalid metadata fps={fps} frames={frame_count} size={width}x{height}")
|
| 70 |
+
mask = roi_mask(height, width)
|
| 71 |
+
step = max(1, int(round(fps * sample_seconds)))
|
| 72 |
+
samples: list[dict] = []
|
| 73 |
+
frame_idx = 0
|
| 74 |
+
next_sample = 0
|
| 75 |
+
while True:
|
| 76 |
+
ok, frame = cap.read()
|
| 77 |
+
if not ok:
|
| 78 |
+
break
|
| 79 |
+
if frame_idx >= next_sample:
|
| 80 |
+
digest, ratio = frame_features(frame, mask, size)
|
| 81 |
+
samples.append({"frame": frame_idx, "time": frame_idx / fps,
|
| 82 |
+
"hash": digest, "blue_ratio": ratio})
|
| 83 |
+
next_sample += step
|
| 84 |
+
frame_idx += 1
|
| 85 |
+
cap.release()
|
| 86 |
+
if frame_idx < frame_count * 0.99:
|
| 87 |
+
raise RuntimeError(f"early decode stop decoded={frame_idx} metadata_frames={frame_count}")
|
| 88 |
+
|
| 89 |
+
min_samples = max(2, int(np.ceil(min_static_seconds / sample_seconds)))
|
| 90 |
+
candidates: list[dict] = []
|
| 91 |
+
run_start = 0
|
| 92 |
+
for i in range(1, len(samples) + 1):
|
| 93 |
+
if i == len(samples) or samples[i]["hash"] != samples[run_start]["hash"]:
|
| 94 |
+
chunk = samples[run_start:i]
|
| 95 |
+
mean_blue = float(np.mean([x["blue_ratio"] for x in chunk]))
|
| 96 |
+
if len(chunk) >= min_samples and mean_blue >= blue_threshold:
|
| 97 |
+
candidates.append({
|
| 98 |
+
"start_seconds": chunk[0]["time"],
|
| 99 |
+
"end_seconds": chunk[-1]["time"] + sample_seconds,
|
| 100 |
+
"sample_count": len(chunk),
|
| 101 |
+
"mean_blue_ratio": mean_blue,
|
| 102 |
+
"hash": chunk[0]["hash"],
|
| 103 |
+
})
|
| 104 |
+
run_start = i
|
| 105 |
+
metadata = {"fps": fps, "frame_count": frame_count, "width": width,
|
| 106 |
+
"height": height, "duration_seconds": frame_count / fps,
|
| 107 |
+
"decoded_frames": frame_idx, "coarse_samples": len(samples)}
|
| 108 |
+
return metadata, candidates
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def refine_candidate(video: Path, metadata: dict, candidate: dict, pad_seconds: float,
|
| 112 |
+
min_static_seconds: float, blue_threshold: float,
|
| 113 |
+
size: int) -> list[dict]:
|
| 114 |
+
fps = metadata["fps"]
|
| 115 |
+
first = max(0, int(np.floor((candidate["start_seconds"] - pad_seconds) * fps)))
|
| 116 |
+
last = min(metadata["frame_count"] - 1,
|
| 117 |
+
int(np.ceil((candidate["end_seconds"] + pad_seconds) * fps)))
|
| 118 |
+
cap = cv2.VideoCapture(str(video))
|
| 119 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, first)
|
| 120 |
+
mask = roi_mask(metadata["height"], metadata["width"])
|
| 121 |
+
samples: list[dict] = []
|
| 122 |
+
frame_idx = first
|
| 123 |
+
while frame_idx <= last:
|
| 124 |
+
ok, frame = cap.read()
|
| 125 |
+
if not ok:
|
| 126 |
+
break
|
| 127 |
+
digest, ratio = frame_features(frame, mask, size)
|
| 128 |
+
samples.append({"frame": frame_idx, "hash": digest, "blue_ratio": ratio})
|
| 129 |
+
frame_idx += 1
|
| 130 |
+
cap.release()
|
| 131 |
+
|
| 132 |
+
min_frames = max(2, int(np.ceil(min_static_seconds * fps)))
|
| 133 |
+
runs: list[dict] = []
|
| 134 |
+
run_start = 0
|
| 135 |
+
for i in range(1, len(samples) + 1):
|
| 136 |
+
if i == len(samples) or samples[i]["hash"] != samples[run_start]["hash"]:
|
| 137 |
+
chunk = samples[run_start:i]
|
| 138 |
+
mean_blue = float(np.mean([x["blue_ratio"] for x in chunk]))
|
| 139 |
+
if len(chunk) >= min_frames and mean_blue >= blue_threshold:
|
| 140 |
+
start_frame = chunk[0]["frame"]
|
| 141 |
+
end_frame = chunk[-1]["frame"]
|
| 142 |
+
runs.append({
|
| 143 |
+
"start_frame": start_frame,
|
| 144 |
+
"end_frame": end_frame,
|
| 145 |
+
"start_seconds": start_frame / fps,
|
| 146 |
+
"end_seconds": (end_frame + 1) / fps,
|
| 147 |
+
"duration_seconds": len(chunk) / fps,
|
| 148 |
+
"start_time": hhmmss(start_frame / fps),
|
| 149 |
+
"end_time": hhmmss((end_frame + 1) / fps),
|
| 150 |
+
"frame_count": len(chunk),
|
| 151 |
+
"mean_blue_ratio": mean_blue,
|
| 152 |
+
"exact_repeated_hash": chunk[0]["hash"],
|
| 153 |
+
"confidence": "high",
|
| 154 |
+
})
|
| 155 |
+
run_start = i
|
| 156 |
+
return runs
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def merge_duplicate_runs(runs: list[dict]) -> list[dict]:
|
| 160 |
+
by_key: dict[tuple[int, int], dict] = {}
|
| 161 |
+
for run in runs:
|
| 162 |
+
by_key[(run["start_frame"], run["end_frame"])] = run
|
| 163 |
+
return sorted(by_key.values(), key=lambda x: x["start_frame"])
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def save_evidence(video: Path, intervals: list[dict], output_dir: Path, fps: float) -> None:
|
| 167 |
+
if not intervals:
|
| 168 |
+
return
|
| 169 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 170 |
+
cap = cv2.VideoCapture(str(video))
|
| 171 |
+
for index, interval in enumerate(intervals, 1):
|
| 172 |
+
points = {
|
| 173 |
+
"before": max(0, interval["start_frame"] - 1),
|
| 174 |
+
"start": interval["start_frame"],
|
| 175 |
+
"end": interval["end_frame"],
|
| 176 |
+
"after": interval["end_frame"] + 1,
|
| 177 |
+
}
|
| 178 |
+
evidence = {}
|
| 179 |
+
for label, frame_idx in points.items():
|
| 180 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
| 181 |
+
ok, frame = cap.read()
|
| 182 |
+
if ok:
|
| 183 |
+
name = f"interval_{index:03d}_{label}_f{frame_idx}.jpg"
|
| 184 |
+
cv2.imwrite(str(output_dir / name), frame)
|
| 185 |
+
evidence[label] = str((output_dir / name).name)
|
| 186 |
+
interval["evidence_frames"] = evidence
|
| 187 |
+
cap.release()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def write_aggregate(output_dir: Path, records: list[dict], started_at: str,
|
| 191 |
+
elapsed: float, input_dir: Path, config: dict) -> None:
|
| 192 |
+
intervals = []
|
| 193 |
+
for record in records:
|
| 194 |
+
for idx, interval in enumerate(record.get("intervals", []), 1):
|
| 195 |
+
intervals.append({"video_id": record["video_id"],
|
| 196 |
+
"video_path": record["video_path"],
|
| 197 |
+
"interval_index": idx, **interval})
|
| 198 |
+
fields = ["video_id", "video_path", "interval_index", "start_frame", "end_frame",
|
| 199 |
+
"start_seconds", "end_seconds", "duration_seconds", "start_time", "end_time",
|
| 200 |
+
"frame_count", "mean_blue_ratio", "confidence", "exact_repeated_hash"]
|
| 201 |
+
with (output_dir / "intervals.csv").open("w", newline="", encoding="utf-8") as handle:
|
| 202 |
+
writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
|
| 203 |
+
writer.writeheader(); writer.writerows(intervals)
|
| 204 |
+
with (output_dir / "videos.jsonl").open("w", encoding="utf-8") as handle:
|
| 205 |
+
for record in records:
|
| 206 |
+
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
| 207 |
+
summary = {
|
| 208 |
+
"status": "completed" if all(r["status"] == "completed" for r in records) else "failed",
|
| 209 |
+
"started_at": started_at, "finished_at": utc_now(), "elapsed_seconds": elapsed,
|
| 210 |
+
"host": socket.gethostname(), "input_dir": str(input_dir), "config": config,
|
| 211 |
+
"video_count": len(records),
|
| 212 |
+
"completed_video_count": sum(r["status"] == "completed" for r in records),
|
| 213 |
+
"failed_video_count": sum(r["status"] == "failed" for r in records),
|
| 214 |
+
"videos_with_blue_screen": sum(bool(r.get("intervals")) for r in records),
|
| 215 |
+
"interval_count": len(intervals),
|
| 216 |
+
"total_video_seconds": sum(r.get("duration_seconds", 0) for r in records),
|
| 217 |
+
"total_blue_screen_seconds": sum(x["duration_seconds"] for x in intervals),
|
| 218 |
+
}
|
| 219 |
+
(output_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def main() -> None:
|
| 223 |
+
parser = argparse.ArgumentParser()
|
| 224 |
+
parser.add_argument("--input-dir", type=Path, required=True)
|
| 225 |
+
parser.add_argument("--output-dir", type=Path, required=True)
|
| 226 |
+
parser.add_argument("--sample-seconds", type=float, default=1.0)
|
| 227 |
+
parser.add_argument("--min-static-seconds", type=float, default=6.0)
|
| 228 |
+
parser.add_argument("--blue-ratio", type=float, default=0.45)
|
| 229 |
+
parser.add_argument("--pad-seconds", type=float, default=5.0)
|
| 230 |
+
parser.add_argument("--coarse-size", type=int, default=96)
|
| 231 |
+
parser.add_argument("--fine-size", type=int, default=160)
|
| 232 |
+
parser.add_argument("--resume", action="store_true")
|
| 233 |
+
args = parser.parse_args()
|
| 234 |
+
videos = sorted(p for p in args.input_dir.rglob("*") if p.suffix.lower() in VIDEO_SUFFIXES)
|
| 235 |
+
if not videos:
|
| 236 |
+
raise SystemExit("no videos found")
|
| 237 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 238 |
+
per_video_dir = args.output_dir / "per_video"
|
| 239 |
+
evidence_root = args.output_dir / "evidence"
|
| 240 |
+
per_video_dir.mkdir(exist_ok=True)
|
| 241 |
+
started_at = utc_now(); t0 = time.perf_counter()
|
| 242 |
+
config = {"sample_seconds": args.sample_seconds,
|
| 243 |
+
"min_static_seconds": args.min_static_seconds,
|
| 244 |
+
"blue_ratio": args.blue_ratio, "pad_seconds": args.pad_seconds,
|
| 245 |
+
"coarse_size": args.coarse_size, "fine_size": args.fine_size,
|
| 246 |
+
"definition": "exact repeated decoded ROI frames with blue ratio above threshold"}
|
| 247 |
+
records = []
|
| 248 |
+
for number, video in enumerate(videos, 1):
|
| 249 |
+
video_id = video.stem.split(" ", 1)[0]
|
| 250 |
+
result_path = per_video_dir / f"{video_id}.json"
|
| 251 |
+
if args.resume and result_path.exists():
|
| 252 |
+
records.append(json.loads(result_path.read_text(encoding="utf-8")))
|
| 253 |
+
print(f"[{number}/{len(videos)}] resume {video.name}", flush=True)
|
| 254 |
+
continue
|
| 255 |
+
print(f"[{number}/{len(videos)}] start {video.name} {utc_now()}", flush=True)
|
| 256 |
+
item_t0 = time.perf_counter()
|
| 257 |
+
record = {"video_id": video_id, "video_path": str(video), "status": "failed",
|
| 258 |
+
"started_at": utc_now(), "intervals": []}
|
| 259 |
+
try:
|
| 260 |
+
metadata, candidates = coarse_scan(video, args.sample_seconds,
|
| 261 |
+
args.min_static_seconds,
|
| 262 |
+
args.blue_ratio, args.coarse_size)
|
| 263 |
+
fine_runs = []
|
| 264 |
+
for candidate in candidates:
|
| 265 |
+
fine_runs.extend(refine_candidate(video, metadata, candidate,
|
| 266 |
+
args.pad_seconds,
|
| 267 |
+
args.min_static_seconds,
|
| 268 |
+
args.blue_ratio, args.fine_size))
|
| 269 |
+
fine_runs = merge_duplicate_runs(fine_runs)
|
| 270 |
+
save_evidence(video, fine_runs, evidence_root / video_id, metadata["fps"])
|
| 271 |
+
blue_seconds = sum(x["duration_seconds"] for x in fine_runs)
|
| 272 |
+
record.update(metadata)
|
| 273 |
+
record.update({"status": "completed", "coarse_candidate_count": len(candidates),
|
| 274 |
+
"interval_count": len(fine_runs), "blue_screen": bool(fine_runs),
|
| 275 |
+
"blue_screen_seconds": blue_seconds,
|
| 276 |
+
"blue_screen_ratio": blue_seconds / metadata["duration_seconds"],
|
| 277 |
+
"intervals": fine_runs})
|
| 278 |
+
except Exception as exc:
|
| 279 |
+
record["error"] = f"{type(exc).__name__}: {exc}"
|
| 280 |
+
record["finished_at"] = utc_now()
|
| 281 |
+
record["elapsed_seconds"] = time.perf_counter() - item_t0
|
| 282 |
+
result_path.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 283 |
+
records.append(record)
|
| 284 |
+
write_aggregate(args.output_dir, records, started_at,
|
| 285 |
+
time.perf_counter() - t0, args.input_dir, config)
|
| 286 |
+
print(f"[{number}/{len(videos)}] {record['status']} intervals={len(record['intervals'])} elapsed={record['elapsed_seconds']:.1f}s", flush=True)
|
| 287 |
+
write_aggregate(args.output_dir, records, started_at,
|
| 288 |
+
time.perf_counter() - t0, args.input_dir, config)
|
| 289 |
+
if any(r["status"] != "completed" for r in records):
|
| 290 |
+
raise SystemExit(2)
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
if __name__ == "__main__":
|
| 294 |
+
main()
|
pipelines/Tr006_V2/project/ms-swift/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
pipelines/Tr006_V2/project/ms-swift/MANIFEST.in
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
recursive-include requirements *.txt
|
pipelines/Tr006_V2/project/ms-swift/requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
-r requirements/framework.txt
|
pipelines/Tr006_V2/project/ms-swift/requirements/framework.txt
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
accelerate
|
| 2 |
+
addict
|
| 3 |
+
aiohttp
|
| 4 |
+
attrdict
|
| 5 |
+
binpacking
|
| 6 |
+
charset_normalizer
|
| 7 |
+
cpm_kernels
|
| 8 |
+
dacite
|
| 9 |
+
datasets>=3.0,<4.8.5
|
| 10 |
+
einops
|
| 11 |
+
fastapi
|
| 12 |
+
gradio>=3.40.0,<6.0
|
| 13 |
+
importlib_metadata
|
| 14 |
+
json_repair
|
| 15 |
+
matplotlib
|
| 16 |
+
modelscope>=1.23
|
| 17 |
+
nltk
|
| 18 |
+
numpy
|
| 19 |
+
openai
|
| 20 |
+
oss2
|
| 21 |
+
pandas
|
| 22 |
+
peft>=0.11,<0.20
|
| 23 |
+
pillow
|
| 24 |
+
PyYAML>=5.4
|
| 25 |
+
requests
|
| 26 |
+
rouge
|
| 27 |
+
safetensors
|
| 28 |
+
scipy
|
| 29 |
+
sentencepiece
|
| 30 |
+
simplejson>=3.3.0
|
| 31 |
+
sortedcontainers>=1.5.9
|
| 32 |
+
tensorboard
|
| 33 |
+
tiktoken
|
| 34 |
+
tqdm
|
| 35 |
+
transformers>=4.33,<5.17.0
|
| 36 |
+
transformers_stream_generator
|
| 37 |
+
trl>=0.15,<1.0
|
| 38 |
+
uvicorn
|
| 39 |
+
zstandard
|
pipelines/Tr006_V2/project/ms-swift/requirements/swanlab.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
swanlab
|
pipelines/Tr006_V2/project/ms-swift/setup.cfg
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[isort]
|
| 2 |
+
line_length = 120
|
| 3 |
+
multi_line_output = 0
|
| 4 |
+
known_standard_library = setuptools
|
| 5 |
+
known_first_party = swift
|
| 6 |
+
known_third_party = json,yaml
|
| 7 |
+
no_lines_before = STDLIB,LOCALFOLDER
|
| 8 |
+
default_section = THIRDPARTY
|
| 9 |
+
|
| 10 |
+
[yapf]
|
| 11 |
+
BASED_ON_STYLE = pep8
|
| 12 |
+
COLUMN_LIMIT = 120
|
| 13 |
+
BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF = true
|
| 14 |
+
SPLIT_BEFORE_EXPRESSION_AFTER_OPENING_PAREN = true
|
| 15 |
+
SPLIT_BEFORE_ARITHMETIC_OPERATOR = true
|
| 16 |
+
|
| 17 |
+
[codespell]
|
| 18 |
+
skip = *.ipynb
|
| 19 |
+
quiet-level = 3
|
| 20 |
+
ignore-words-list = patten,nd,ty,mot,hist,formating,winn,gool,datas,wan,confids
|
| 21 |
+
|
| 22 |
+
[flake8]
|
| 23 |
+
max-line-length = 120
|
| 24 |
+
select = B,C,E,F,P,T4,W,B9
|
| 25 |
+
ignore = F401,F403,F405,F821,W503,E251,W504,E126
|
| 26 |
+
exclude = docs/src,*.pyi,.git,peft.py
|
| 27 |
+
|
| 28 |
+
[darglint]
|
| 29 |
+
ignore=DAR101
|
| 30 |
+
|
| 31 |
+
[easy_install]
|
| 32 |
+
index-url=https://pypi.tuna.tsinghua.edu.cn/simple
|
pipelines/Tr006_V2/project/ms-swift/setup.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
# !/usr/bin/env python
|
| 3 |
+
from setuptools import find_packages, setup
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
from typing import List
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def readme():
|
| 11 |
+
with open('README.md', encoding='utf-8') as f:
|
| 12 |
+
content = f.read()
|
| 13 |
+
return content
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
version_file = 'swift/version.py'
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_version():
|
| 20 |
+
with open(version_file, 'r', encoding='utf-8') as f:
|
| 21 |
+
return re.search(r'^__version__\s*=\s*["\'](.+?)["\']', f.read(), re.M).group(1)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def parse_requirements(fname='requirements.txt', with_version=True):
|
| 25 |
+
"""
|
| 26 |
+
Parse the package dependencies listed in a requirements file but strips
|
| 27 |
+
specific versioning information.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
fname (str): path to requirements file
|
| 31 |
+
with_version (bool, default=False): if True include version specs
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
List[str]: list of requirements items
|
| 35 |
+
|
| 36 |
+
CommandLine:
|
| 37 |
+
python -c "import setup; print(setup.parse_requirements())"
|
| 38 |
+
"""
|
| 39 |
+
import re
|
| 40 |
+
import sys
|
| 41 |
+
from os.path import exists
|
| 42 |
+
require_fpath = fname
|
| 43 |
+
|
| 44 |
+
def parse_line(line):
|
| 45 |
+
"""
|
| 46 |
+
Parse information from a line in a requirements text file
|
| 47 |
+
"""
|
| 48 |
+
if line.startswith('-r '):
|
| 49 |
+
# Allow specifying requirements in other files
|
| 50 |
+
target = line.split(' ')[1]
|
| 51 |
+
relative_base = os.path.dirname(fname)
|
| 52 |
+
absolute_target = os.path.join(relative_base, target)
|
| 53 |
+
for info in parse_require_file(absolute_target):
|
| 54 |
+
yield info
|
| 55 |
+
else:
|
| 56 |
+
info = {'line': line}
|
| 57 |
+
if line.startswith('-e '):
|
| 58 |
+
info['package'] = line.split('#egg=')[1]
|
| 59 |
+
else:
|
| 60 |
+
# Remove versioning from the package
|
| 61 |
+
pat = '(' + '|'.join(['>=', '==', '>']) + ')'
|
| 62 |
+
parts = re.split(pat, line, maxsplit=1)
|
| 63 |
+
parts = [p.strip() for p in parts]
|
| 64 |
+
|
| 65 |
+
info['package'] = parts[0]
|
| 66 |
+
if len(parts) > 1:
|
| 67 |
+
op, rest = parts[1:]
|
| 68 |
+
if ';' in rest:
|
| 69 |
+
# Handle platform specific dependencies
|
| 70 |
+
# http://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies
|
| 71 |
+
version, platform_deps = map(str.strip, rest.split(';'))
|
| 72 |
+
info['platform_deps'] = platform_deps
|
| 73 |
+
else:
|
| 74 |
+
version = rest # NOQA
|
| 75 |
+
info['version'] = (op, version)
|
| 76 |
+
yield info
|
| 77 |
+
|
| 78 |
+
def parse_require_file(fpath):
|
| 79 |
+
with open(fpath, 'r', encoding='utf-8') as f:
|
| 80 |
+
for line in f.readlines():
|
| 81 |
+
line = line.strip()
|
| 82 |
+
if line.startswith('http'):
|
| 83 |
+
print('skip http requirements %s' % line)
|
| 84 |
+
continue
|
| 85 |
+
if line and not line.startswith('#') and not line.startswith('--'):
|
| 86 |
+
for info in parse_line(line):
|
| 87 |
+
yield info
|
| 88 |
+
elif line and line.startswith('--find-links'):
|
| 89 |
+
eles = line.split()
|
| 90 |
+
for e in eles:
|
| 91 |
+
e = e.strip()
|
| 92 |
+
if 'http' in e:
|
| 93 |
+
info = dict(dependency_links=e)
|
| 94 |
+
yield info
|
| 95 |
+
|
| 96 |
+
def gen_packages_items():
|
| 97 |
+
items = []
|
| 98 |
+
deps_link = []
|
| 99 |
+
if exists(require_fpath):
|
| 100 |
+
for info in parse_require_file(require_fpath):
|
| 101 |
+
if 'dependency_links' not in info:
|
| 102 |
+
parts = [info['package']]
|
| 103 |
+
if with_version and 'version' in info:
|
| 104 |
+
parts.extend(info['version'])
|
| 105 |
+
if not sys.version.startswith('3.4'):
|
| 106 |
+
# apparently package_deps are broken in 3.4
|
| 107 |
+
platform_deps = info.get('platform_deps')
|
| 108 |
+
if platform_deps is not None:
|
| 109 |
+
parts.append(';' + platform_deps)
|
| 110 |
+
item = ''.join(parts)
|
| 111 |
+
items.append(item)
|
| 112 |
+
else:
|
| 113 |
+
deps_link.append(info['dependency_links'])
|
| 114 |
+
return items, deps_link
|
| 115 |
+
|
| 116 |
+
return gen_packages_items()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
if __name__ == '__main__':
|
| 120 |
+
install_requires, deps_link = parse_requirements('requirements.txt')
|
| 121 |
+
extra_requires = {}
|
| 122 |
+
all_requires = []
|
| 123 |
+
extra_requires['megatron'], _ = parse_requirements('requirements/megatron.txt')
|
| 124 |
+
extra_requires['eval'], _ = parse_requirements('requirements/eval.txt')
|
| 125 |
+
extra_requires['swanlab'], _ = parse_requirements('requirements/swanlab.txt')
|
| 126 |
+
extra_requires['ray'], _ = parse_requirements('requirements/ray.txt')
|
| 127 |
+
all_requires.extend(install_requires)
|
| 128 |
+
all_requires.extend(extra_requires['eval'])
|
| 129 |
+
all_requires.extend(extra_requires['swanlab'])
|
| 130 |
+
all_requires.extend(extra_requires['ray'])
|
| 131 |
+
extra_requires['all'] = all_requires
|
| 132 |
+
|
| 133 |
+
setup(
|
| 134 |
+
name='ms_swift',
|
| 135 |
+
version=get_version(),
|
| 136 |
+
description='Swift: Scalable lightWeight Infrastructure for Fine-Tuning',
|
| 137 |
+
long_description=readme(),
|
| 138 |
+
long_description_content_type='text/markdown',
|
| 139 |
+
author='DAMO ModelScope teams',
|
| 140 |
+
author_email='contact@modelscope.cn',
|
| 141 |
+
keywords=['transformers', 'LLM', 'lora', 'megatron', 'grpo', 'sft'],
|
| 142 |
+
url='https://github.com/modelscope/ms-swift',
|
| 143 |
+
packages=find_packages(exclude=('tests', 'tests.*')),
|
| 144 |
+
include_package_data=True,
|
| 145 |
+
package_data={'': ['utils/*', 'dataset/data/*.*', 'config/*.json', 'loss_scale/config/*.json']},
|
| 146 |
+
python_requires='>=3.8.0',
|
| 147 |
+
classifiers=[
|
| 148 |
+
'Development Status :: 4 - Beta',
|
| 149 |
+
'License :: OSI Approved :: Apache Software License',
|
| 150 |
+
'Operating System :: OS Independent',
|
| 151 |
+
'Programming Language :: Python :: 3',
|
| 152 |
+
'Programming Language :: Python :: 3.8',
|
| 153 |
+
'Programming Language :: Python :: 3.9',
|
| 154 |
+
'Programming Language :: Python :: 3.10',
|
| 155 |
+
'Programming Language :: Python :: 3.11',
|
| 156 |
+
'Programming Language :: Python :: 3.12',
|
| 157 |
+
],
|
| 158 |
+
license='Apache License 2.0',
|
| 159 |
+
tests_require=parse_requirements('requirements/tests.txt'),
|
| 160 |
+
install_requires=install_requires,
|
| 161 |
+
extras_require=extra_requires,
|
| 162 |
+
entry_points={
|
| 163 |
+
'console_scripts': ['swift=swift.cli.main:cli_main', 'megatron=swift.cli._megatron.main:cli_main']
|
| 164 |
+
},
|
| 165 |
+
dependency_links=deps_link,
|
| 166 |
+
zip_safe=False)
|
pipelines/Tr006_V2/project/ms-swift/swift/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
from typing import TYPE_CHECKING
|
| 3 |
+
|
| 4 |
+
from .utils.import_utils import _LazyModule
|
| 5 |
+
|
| 6 |
+
if TYPE_CHECKING:
|
| 7 |
+
from .agent_template import BaseAgentTemplate, agent_template_map
|
| 8 |
+
from .arguments import (AppArguments, BaseArguments, DeployArguments, EvalArguments, ExportArguments,
|
| 9 |
+
InferArguments, PretrainArguments, RLHFArguments, RolloutArguments, SamplingArguments,
|
| 10 |
+
SftArguments)
|
| 11 |
+
from .callbacks import TrainerCallback, callbacks_map
|
| 12 |
+
from .dataset import EncodePreprocessor, load_dataset
|
| 13 |
+
from .infer_engine import (AdapterRequest, GRPOVllmEngine, InferClient, InferEngine, InferRequest, LmdeployEngine,
|
| 14 |
+
RequestConfig, SglangEngine, TransformersEngine, VllmEngine)
|
| 15 |
+
from .loss import BaseLoss, loss_map
|
| 16 |
+
from .loss_scale import ALL_BASE_STRATEGY, ConfigLossScale, LossScale, get_loss_scale, loss_scale_map
|
| 17 |
+
from .metrics import InferStats, MeanMetric, eval_metrics_map
|
| 18 |
+
from .model import get_model_processor, get_processor
|
| 19 |
+
from .optimizers import OptimizerCallback, optimizers_map
|
| 20 |
+
from .pipelines import (app_main, deploy_main, eval_main, export_main, infer_main, merge_lora, pretrain_main,
|
| 21 |
+
rlhf_main, rollout_main, run_deploy, sampling_main, sft_main)
|
| 22 |
+
from .template import get_template
|
| 23 |
+
from .trainers import Seq2SeqTrainer, Seq2SeqTrainingArguments, Trainer, TrainingArguments
|
| 24 |
+
from .tuner_plugin import PeftTuner, Tuner, tuners_map
|
| 25 |
+
from .tuners import Swift
|
| 26 |
+
from .utils import get_logger, safe_snapshot_download
|
| 27 |
+
from .version import __release_datetime__, __version__
|
| 28 |
+
else:
|
| 29 |
+
_import_structure = {
|
| 30 |
+
'version': ['__release_datetime__', '__version__'],
|
| 31 |
+
'tuners': ['Swift'],
|
| 32 |
+
'tuner_plugin': ['Tuner', 'PeftTuner', 'tuners_map'],
|
| 33 |
+
'infer_engine': [
|
| 34 |
+
'TransformersEngine', 'VllmEngine', 'SglangEngine', 'LmdeployEngine', 'InferRequest', 'RequestConfig',
|
| 35 |
+
'AdapterRequest', 'InferEngine', 'InferClient', 'GRPOVllmEngine'
|
| 36 |
+
],
|
| 37 |
+
'trainers': ['TrainingArguments', 'Seq2SeqTrainingArguments', 'Trainer', 'Seq2SeqTrainer'],
|
| 38 |
+
'arguments': [
|
| 39 |
+
'PretrainArguments', 'SftArguments', 'RLHFArguments', 'ExportArguments', 'InferArguments', 'AppArguments',
|
| 40 |
+
'EvalArguments', 'SamplingArguments', 'RolloutArguments', 'DeployArguments', 'BaseArguments'
|
| 41 |
+
],
|
| 42 |
+
'pipelines': [
|
| 43 |
+
'sft_main', 'pretrain_main', 'infer_main', 'rlhf_main', 'export_main', 'app_main', 'eval_main',
|
| 44 |
+
'sampling_main', 'rollout_main', 'deploy_main', 'merge_lora', 'run_deploy'
|
| 45 |
+
],
|
| 46 |
+
'model': ['get_model_processor', 'get_processor'],
|
| 47 |
+
'template': ['get_template'],
|
| 48 |
+
'dataset': ['load_dataset', 'EncodePreprocessor'],
|
| 49 |
+
'utils': ['get_logger', 'safe_snapshot_download'],
|
| 50 |
+
'agent_template': ['agent_template_map', 'BaseAgentTemplate'],
|
| 51 |
+
'loss': ['loss_map', 'BaseLoss'],
|
| 52 |
+
'metrics': ['eval_metrics_map', 'InferStats', 'MeanMetric'],
|
| 53 |
+
'optimizers': ['optimizers_map', 'OptimizerCallback'],
|
| 54 |
+
'callbacks': ['callbacks_map', 'TrainerCallback'],
|
| 55 |
+
'loss_scale': ['loss_scale_map', 'LossScale', 'get_loss_scale', 'ALL_BASE_STRATEGY', 'ConfigLossScale'],
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
import sys
|
| 59 |
+
|
| 60 |
+
sys.modules[__name__] = _LazyModule(
|
| 61 |
+
__name__,
|
| 62 |
+
globals()['__file__'],
|
| 63 |
+
_import_structure,
|
| 64 |
+
module_spec=__spec__,
|
| 65 |
+
extra_objects={},
|
| 66 |
+
)
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
from .base import BaseAgentTemplate
|
| 3 |
+
from .mapping import agent_template_map
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/base.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
"""
|
| 3 |
+
Agent template module for handling tool calling and function execution.
|
| 4 |
+
|
| 5 |
+
This module provides base classes and utilities for creating agent templates
|
| 6 |
+
that support tool calling in conversational AI systems. It includes support
|
| 7 |
+
for various agent formats like ReAct, function calling, and parallel execution.
|
| 8 |
+
"""
|
| 9 |
+
import ast
|
| 10 |
+
import json
|
| 11 |
+
from abc import ABC, abstractmethod
|
| 12 |
+
from dataclasses import asdict, dataclass
|
| 13 |
+
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
| 14 |
+
|
| 15 |
+
from swift.infer_engine import Function
|
| 16 |
+
from swift.template import Prompt, split_str_parts_by
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class AgentKeyword:
|
| 21 |
+
action: str = 'Action:'
|
| 22 |
+
action_input: str = 'Action Input:'
|
| 23 |
+
observation: str = 'Observation:'
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class ToolDesc:
|
| 28 |
+
name_for_model: str
|
| 29 |
+
name_for_human: str
|
| 30 |
+
description_for_model: str
|
| 31 |
+
parameters: str
|
| 32 |
+
args_format: str
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ReactCompatMixin:
|
| 36 |
+
"""
|
| 37 |
+
Mixin class providing ReAct-style agent compatibility.
|
| 38 |
+
|
| 39 |
+
This mixin handles parsing and formatting of tool calls in the ReAct format,
|
| 40 |
+
where actions and inputs are marked with specific keywords in the text.
|
| 41 |
+
"""
|
| 42 |
+
keyword = AgentKeyword()
|
| 43 |
+
|
| 44 |
+
@staticmethod
|
| 45 |
+
def _split_action_action_input(response: str, keyword: AgentKeyword) -> List[Function]:
|
| 46 |
+
agent_parts = split_str_parts_by(response, list(asdict(keyword).values()))
|
| 47 |
+
functions = []
|
| 48 |
+
action_content = None
|
| 49 |
+
|
| 50 |
+
for part in agent_parts:
|
| 51 |
+
key, content = part['key'].lower(), part['content']
|
| 52 |
+
if action_content is None and key == keyword.action.lower():
|
| 53 |
+
action_content = content
|
| 54 |
+
elif action_content is not None and key == keyword.action_input.lower():
|
| 55 |
+
functions.append(Function(name=action_content, arguments=content))
|
| 56 |
+
action_content = None
|
| 57 |
+
|
| 58 |
+
return functions
|
| 59 |
+
|
| 60 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 61 |
+
"""
|
| 62 |
+
Extract tool calls from an agent response.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
response: The agent's response text.
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
List of Function objects representing tool calls.
|
| 69 |
+
"""
|
| 70 |
+
functions = self._split_action_action_input(response, self.keyword)
|
| 71 |
+
if len(functions) == 0 and self.keyword != ReactCompatMixin.keyword:
|
| 72 |
+
# compat react
|
| 73 |
+
functions = self._split_action_action_input(response, ReactCompatMixin.keyword)
|
| 74 |
+
return functions
|
| 75 |
+
|
| 76 |
+
def _format_tool_responses(
|
| 77 |
+
self,
|
| 78 |
+
assistant_content: str,
|
| 79 |
+
tool_messages,
|
| 80 |
+
) -> Tuple[str, 'Prompt']:
|
| 81 |
+
"""
|
| 82 |
+
Format tool execution results into the conversation.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
assistant_content: The assistant's message containing tool calls.
|
| 86 |
+
tool_messages: List of tool execution result messages.
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Tuple of (formatted assistant content, formatted tool responses).
|
| 90 |
+
"""
|
| 91 |
+
assert len(tool_messages) > 0
|
| 92 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 93 |
+
if with_action:
|
| 94 |
+
if not assistant_content.endswith(self.keyword.observation):
|
| 95 |
+
if not assistant_content.endswith('\n'):
|
| 96 |
+
assistant_content += '\n'
|
| 97 |
+
assistant_content += self.keyword.observation
|
| 98 |
+
res = []
|
| 99 |
+
for i, tool_message in enumerate(tool_messages):
|
| 100 |
+
if i > 0:
|
| 101 |
+
res.append(self.keyword.observation)
|
| 102 |
+
tool_content = tool_message['content']
|
| 103 |
+
res.append(tool_content)
|
| 104 |
+
if not tool_content.endswith('\n'):
|
| 105 |
+
res.append('\n')
|
| 106 |
+
else:
|
| 107 |
+
res = []
|
| 108 |
+
for tool_message in tool_messages:
|
| 109 |
+
res.append(tool_message['content'])
|
| 110 |
+
return assistant_content, res
|
| 111 |
+
|
| 112 |
+
def _format_react_standalone_tool_responses(self, tool_messages) -> 'Prompt':
|
| 113 |
+
assert len(tool_messages) > 0
|
| 114 |
+
res = []
|
| 115 |
+
for tool_message in tool_messages:
|
| 116 |
+
tool_content = tool_message['content']
|
| 117 |
+
res.extend(['\n', self.keyword.observation, '\n', tool_content])
|
| 118 |
+
return res
|
| 119 |
+
|
| 120 |
+
def _format_standalone_tool_responses(self, tool_messages) -> 'Prompt':
|
| 121 |
+
"""Format observations that do not follow an assistant tool call."""
|
| 122 |
+
raise NotImplementedError(
|
| 123 |
+
f'Agent template {self.__class__.__name__} does not support standalone tool responses.')
|
| 124 |
+
|
| 125 |
+
@staticmethod
|
| 126 |
+
def _parse_tool_call(content) -> Dict[str, Any]:
|
| 127 |
+
obj = BaseAgentTemplate._parse_json(content)
|
| 128 |
+
name = obj['name']
|
| 129 |
+
arguments = obj.get('arguments')
|
| 130 |
+
if arguments is None:
|
| 131 |
+
arguments = obj.get('parameters')
|
| 132 |
+
arguments = BaseAgentTemplate._parse_json(arguments)
|
| 133 |
+
assert arguments is not None, f'content: {content}'
|
| 134 |
+
return {'name': name, 'arguments': arguments}
|
| 135 |
+
|
| 136 |
+
def _format_tool_calls(self, tool_call_messages) -> str:
|
| 137 |
+
"""
|
| 138 |
+
Format tool call messages into ReAct format.
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
tool_call_messages: List of messages containing tool call information.
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
Formatted string with Action, Action Input, and Observation markers.
|
| 145 |
+
"""
|
| 146 |
+
# -> assistant_content
|
| 147 |
+
tool_calls = []
|
| 148 |
+
for message in tool_call_messages:
|
| 149 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 150 |
+
tool_calls.append(f'{self.keyword.action} {tool_call["name"]}\n'
|
| 151 |
+
f'{self.keyword.action_input} {tool_call["arguments"]}\n')
|
| 152 |
+
tool_calls.append(self.keyword.observation)
|
| 153 |
+
return ''.join(tool_calls)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class BaseAgentTemplate(ReactCompatMixin, ABC):
|
| 157 |
+
"""
|
| 158 |
+
Abstract base class for agent templates.
|
| 159 |
+
|
| 160 |
+
This class provides common functionality for parsing and formatting tools,
|
| 161 |
+
as well as handling tool calls in different formats. Subclasses must
|
| 162 |
+
implement the following methods to define their specific behavior:
|
| 163 |
+
- `_format_tools`: Format tool definitions for the prompt
|
| 164 |
+
- `_format_tool_calls`: Format tool call messages
|
| 165 |
+
- `_format_tool_responses`: Format tool execution results
|
| 166 |
+
- `get_toolcall`: Extract tool calls from agent responses
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
def _add_tool_call_prefix(self, tool_content: str, pre_message=None) -> str:
|
| 170 |
+
"""Hook to prepend a separator before tool_call content based on the preceding message.
|
| 171 |
+
|
| 172 |
+
Subclasses can override this to match their jinja template's separator logic
|
| 173 |
+
(e.g., Qwen3.5/3.6 inserts '\n\n' when assistant has effective content before tool_calls).
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
tool_content: The formatted tool_call string from _format_tool_calls.
|
| 177 |
+
pre_message: The message immediately before the tool_call block, or None.
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
tool_content with any necessary prefix prepended.
|
| 181 |
+
"""
|
| 182 |
+
return tool_content
|
| 183 |
+
|
| 184 |
+
@staticmethod
|
| 185 |
+
def _get_tool_name(tool):
|
| 186 |
+
return tool.get('name_for_model') or tool.get('name')
|
| 187 |
+
|
| 188 |
+
@staticmethod
|
| 189 |
+
def unwrap_tool(tool):
|
| 190 |
+
assert isinstance(tool, dict), f'tool: {tool}'
|
| 191 |
+
if 'type' in tool and 'function' in tool:
|
| 192 |
+
tool = tool['function']
|
| 193 |
+
return tool
|
| 194 |
+
|
| 195 |
+
@staticmethod
|
| 196 |
+
def wrap_tool(tool):
|
| 197 |
+
assert isinstance(tool, dict), f'tool: {tool}'
|
| 198 |
+
if 'type' not in tool and 'function' not in tool:
|
| 199 |
+
tool = {'type': 'function', 'function': tool}
|
| 200 |
+
return tool
|
| 201 |
+
|
| 202 |
+
@staticmethod
|
| 203 |
+
def _parse_tool(tool, lang: Literal['zh', 'en']) -> ToolDesc:
|
| 204 |
+
tool = BaseAgentTemplate.unwrap_tool(tool)
|
| 205 |
+
name_for_model = BaseAgentTemplate._get_tool_name(tool)
|
| 206 |
+
name_for_human = tool.get('name_for_human') or name_for_model
|
| 207 |
+
|
| 208 |
+
description = tool.get('description')
|
| 209 |
+
if description is None:
|
| 210 |
+
description = tool.get('description_for_model')
|
| 211 |
+
parameters = tool.get('parameters') or {}
|
| 212 |
+
parameters = parameters if isinstance(parameters, str) else json.dumps(parameters, ensure_ascii=False)
|
| 213 |
+
args_format = '此工具的输入应为JSON对象。' if lang == 'zh' else 'Format the arguments as a JSON object.'
|
| 214 |
+
tool_desc = ToolDesc(
|
| 215 |
+
name_for_model=name_for_model,
|
| 216 |
+
name_for_human=name_for_human,
|
| 217 |
+
description_for_model=description,
|
| 218 |
+
parameters=parameters,
|
| 219 |
+
args_format=args_format)
|
| 220 |
+
assert name_for_model is not None and description is not None, f'tool_desc: {tool_desc}'
|
| 221 |
+
return tool_desc
|
| 222 |
+
|
| 223 |
+
@staticmethod
|
| 224 |
+
def _parse_json(json_str: str) -> Optional[Any]:
|
| 225 |
+
"""
|
| 226 |
+
Parse a JSON string with fallback to ast.literal_eval.
|
| 227 |
+
|
| 228 |
+
Args:
|
| 229 |
+
json_str: String to parse, or already parsed object.
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
Parsed object, or None if parsing fails.
|
| 233 |
+
"""
|
| 234 |
+
if not isinstance(json_str, str):
|
| 235 |
+
return json_str
|
| 236 |
+
try:
|
| 237 |
+
res = json.loads(json_str)
|
| 238 |
+
except json.JSONDecodeError:
|
| 239 |
+
try:
|
| 240 |
+
res = ast.literal_eval(json_str)
|
| 241 |
+
except Exception:
|
| 242 |
+
return
|
| 243 |
+
return res
|
| 244 |
+
|
| 245 |
+
@abstractmethod
|
| 246 |
+
def _format_tools(self,
|
| 247 |
+
tools: List[Union[str, dict]],
|
| 248 |
+
system: Optional[str] = None,
|
| 249 |
+
user_message: Optional[dict] = None) -> str:
|
| 250 |
+
"""
|
| 251 |
+
Format tools for inclusion in the agent prompt.
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
tools: List of tool definitions (strings or dictionaries).
|
| 255 |
+
system: System prompt text.
|
| 256 |
+
user_message: Optional user message to incorporate.
|
| 257 |
+
|
| 258 |
+
Returns:
|
| 259 |
+
Formatted string to include in the prompt.
|
| 260 |
+
"""
|
| 261 |
+
pass
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/deepseek_v3_1.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class DeepSeekV31AgentTemplate(BaseAgentTemplate):
|
| 12 |
+
|
| 13 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 14 |
+
# Parse tool calls using the DSV3.1 format:
|
| 15 |
+
# <|tool▁calls▁begin|><|tool▁call▁begin|>name<|tool▁sep|>args<|tool▁call▁end|>
|
| 16 |
+
pattern = r'<|tool▁call▁begin|>(.*?)<|tool▁sep|>(.*?)<|tool▁call▁end|>'
|
| 17 |
+
res_list = re.findall(pattern, response, re.DOTALL)
|
| 18 |
+
functions = []
|
| 19 |
+
for name, arguments in res_list:
|
| 20 |
+
name = name.strip()
|
| 21 |
+
arguments = self._parse_json(arguments.strip())
|
| 22 |
+
if arguments is not None:
|
| 23 |
+
functions.append(Function(name=name, arguments=arguments))
|
| 24 |
+
|
| 25 |
+
if len(functions) == 0:
|
| 26 |
+
# compat react_en
|
| 27 |
+
return super().get_toolcall(response)
|
| 28 |
+
return functions
|
| 29 |
+
|
| 30 |
+
def _get_tool_responses(self, tool_messages):
|
| 31 |
+
return ''.join(f'<|tool▁output▁begin|>{tool_message["content"]}<|tool▁output▁end|>'
|
| 32 |
+
for tool_message in tool_messages)
|
| 33 |
+
|
| 34 |
+
def _get_tool_calls(self, tool_calls: List[str]):
|
| 35 |
+
return f'<|tool▁calls▁begin|>{"".join(tool_calls)}<|tool▁calls▁end|>'
|
| 36 |
+
|
| 37 |
+
def _format_tool_responses(
|
| 38 |
+
self,
|
| 39 |
+
assistant_content: str,
|
| 40 |
+
tool_messages,
|
| 41 |
+
) -> Tuple[str, 'Prompt']:
|
| 42 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 43 |
+
if with_action:
|
| 44 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 45 |
+
res = ['<|end▁of▁sentence|>', self._get_tool_responses(tool_messages)]
|
| 46 |
+
return assistant_content, res
|
| 47 |
+
|
| 48 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 49 |
+
tool_descs = []
|
| 50 |
+
system = system or ''
|
| 51 |
+
for tool in tools:
|
| 52 |
+
tool = self.unwrap_tool(tool)
|
| 53 |
+
tool_name = self._get_tool_name(tool)
|
| 54 |
+
description = tool.get('description', '')
|
| 55 |
+
parameters = tool.get('parameters', {})
|
| 56 |
+
|
| 57 |
+
tool_desc = f"""### {tool_name}
|
| 58 |
+
Description: {description}
|
| 59 |
+
|
| 60 |
+
Parameters: {json.dumps(parameters, ensure_ascii=False)}"""
|
| 61 |
+
tool_descs.append(tool_desc)
|
| 62 |
+
|
| 63 |
+
tools_section = '\n\n'.join(tool_descs)
|
| 64 |
+
|
| 65 |
+
return f"""{system}
|
| 66 |
+
|
| 67 |
+
## Tools
|
| 68 |
+
You have access to the following tools:
|
| 69 |
+
|
| 70 |
+
{tools_section}
|
| 71 |
+
|
| 72 |
+
IMPORTANT: ALWAYS adhere to this exact format for tool use:
|
| 73 |
+
<|tool▁calls▁begin|><|tool▁call▁begin|>tool_call_name<|tool▁sep|>tool_call_arguments<|tool▁call▁end|>{{additional_tool_calls}}<|tool▁calls▁end|>
|
| 74 |
+
|
| 75 |
+
Where:
|
| 76 |
+
- `tool_call_name` must be an exact match to one of the available tools
|
| 77 |
+
- `tool_call_arguments` must be valid JSON that strictly follows the tool's Parameters Schema
|
| 78 |
+
- For multiple tool calls, chain them directly without separators or spaces"""
|
| 79 |
+
|
| 80 |
+
def _format_tool_calls(self, tool_call_messages):
|
| 81 |
+
tool_calls = []
|
| 82 |
+
for message in tool_call_messages:
|
| 83 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 84 |
+
name = tool_call['name']
|
| 85 |
+
arguments = json.dumps(tool_call['arguments'], ensure_ascii=False)
|
| 86 |
+
tool_calls.append(f'<|tool▁call▁begin|>{name}<|tool▁sep|>{arguments}<|tool▁call▁end|>')
|
| 87 |
+
return self._get_tool_calls(tool_calls)
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/deepseek_v4.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
DSML_TOKEN = '|DSML|'
|
| 11 |
+
|
| 12 |
+
TOOLS_TEMPLATE = """## Tools
|
| 13 |
+
|
| 14 |
+
You have access to a set of tools to help answer the user's question. \
|
| 15 |
+
You can invoke tools by writing a "<{dsml_token}tool_calls>" block like the following:
|
| 16 |
+
|
| 17 |
+
<{dsml_token}tool_calls>
|
| 18 |
+
<{dsml_token}invoke name="$TOOL_NAME">
|
| 19 |
+
<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
|
| 20 |
+
...
|
| 21 |
+
</{dsml_token}invoke>
|
| 22 |
+
<{dsml_token}invoke name="$TOOL_NAME2">
|
| 23 |
+
...
|
| 24 |
+
</{dsml_token}invoke>
|
| 25 |
+
</{dsml_token}tool_calls>
|
| 26 |
+
|
| 27 |
+
String parameters should be specified as is and set `string="true"`. \
|
| 28 |
+
For all other types (numbers, booleans, arrays, objects), \
|
| 29 |
+
pass the value in JSON format and set `string="false"`.
|
| 30 |
+
|
| 31 |
+
If thinking_mode is enabled (triggered by <think>), \
|
| 32 |
+
you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.
|
| 33 |
+
|
| 34 |
+
Otherwise, output directly after </think> with tool calls or final response.
|
| 35 |
+
|
| 36 |
+
### Available Tool Schemas
|
| 37 |
+
|
| 38 |
+
{tool_schemas}
|
| 39 |
+
|
| 40 |
+
You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _to_json(value: Any) -> str:
|
| 45 |
+
try:
|
| 46 |
+
return json.dumps(value, ensure_ascii=False)
|
| 47 |
+
except Exception:
|
| 48 |
+
return json.dumps(value, ensure_ascii=True)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _encode_arguments_to_dsml(arguments: Dict[str, Any]) -> str:
|
| 52 |
+
"""Encode tool call arguments dict into DSML parameter lines."""
|
| 53 |
+
lines = []
|
| 54 |
+
for k, v in arguments.items():
|
| 55 |
+
is_str = 'true' if isinstance(v, str) else 'false'
|
| 56 |
+
val = v if isinstance(v, str) else _to_json(v)
|
| 57 |
+
lines.append(f'<{DSML_TOKEN}parameter name="{k}" string="{is_str}">{val}</{DSML_TOKEN}parameter>')
|
| 58 |
+
return '\n'.join(lines)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class DeepSeekV4AgentTemplate(BaseAgentTemplate):
|
| 62 |
+
|
| 63 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 64 |
+
# Parse DSML tool calls from model output
|
| 65 |
+
# Pattern: <|DSML|invoke name="tool_name">...params...</|DSML|invoke>
|
| 66 |
+
invoke_pattern = re.compile(
|
| 67 |
+
rf'<{re.escape(DSML_TOKEN)}invoke\s+name="([^"]+)">\s*(.*?)\s*</{re.escape(DSML_TOKEN)}invoke>', re.DOTALL)
|
| 68 |
+
param_pattern = re.compile(
|
| 69 |
+
rf'<{re.escape(DSML_TOKEN)}parameter\s+name="([^"]+)"\s+string="(true|false)">'
|
| 70 |
+
rf'(.*?)</{re.escape(DSML_TOKEN)}parameter>', re.DOTALL)
|
| 71 |
+
|
| 72 |
+
functions = []
|
| 73 |
+
for match in invoke_pattern.finditer(response):
|
| 74 |
+
tool_name = match.group(1)
|
| 75 |
+
params_block = match.group(2)
|
| 76 |
+
arguments = {}
|
| 77 |
+
for pm in param_pattern.finditer(params_block):
|
| 78 |
+
param_name = pm.group(1)
|
| 79 |
+
is_string = pm.group(2)
|
| 80 |
+
param_value = pm.group(3)
|
| 81 |
+
if is_string == 'false':
|
| 82 |
+
try:
|
| 83 |
+
param_value = json.loads(param_value)
|
| 84 |
+
except json.JSONDecodeError:
|
| 85 |
+
pass
|
| 86 |
+
arguments[param_name] = param_value
|
| 87 |
+
functions.append(Function(name=tool_name, arguments=json.dumps(arguments, ensure_ascii=False)))
|
| 88 |
+
|
| 89 |
+
if len(functions) == 0:
|
| 90 |
+
# Fallback to ReAct format
|
| 91 |
+
return super().get_toolcall(response)
|
| 92 |
+
return functions
|
| 93 |
+
|
| 94 |
+
def _get_tool_responses(self, tool_messages):
|
| 95 |
+
# The official encoding merges tool results into one user turn, joining the
|
| 96 |
+
# `<tool_result>` blocks with a blank line.
|
| 97 |
+
return '\n\n'.join(f'<tool_result>{tool_message["content"]}</tool_result>' for tool_message in tool_messages)
|
| 98 |
+
|
| 99 |
+
def _add_tool_call_prefix(self, tool_content: str, pre_message=None) -> str:
|
| 100 |
+
# The official encoding always renders the tool_calls block as `\n\n` + block,
|
| 101 |
+
# right after the assistant's (possibly empty) textual content.
|
| 102 |
+
return '\n\n' + tool_content
|
| 103 |
+
|
| 104 |
+
def _format_tool_responses(
|
| 105 |
+
self,
|
| 106 |
+
assistant_content: str,
|
| 107 |
+
tool_messages,
|
| 108 |
+
) -> Tuple[str, 'Prompt']:
|
| 109 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 110 |
+
if with_action:
|
| 111 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 112 |
+
res = [
|
| 113 |
+
'<|end▁of▁sentence|><|User|>',
|
| 114 |
+
self._get_tool_responses(tool_messages),
|
| 115 |
+
'<|Assistant|>',
|
| 116 |
+
]
|
| 117 |
+
return assistant_content, res
|
| 118 |
+
|
| 119 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 120 |
+
tool_schemas = []
|
| 121 |
+
for tool in tools:
|
| 122 |
+
tool = self.unwrap_tool(tool)
|
| 123 |
+
tool_schemas.append(_to_json(tool))
|
| 124 |
+
|
| 125 |
+
tools_section = TOOLS_TEMPLATE.format(
|
| 126 |
+
tool_schemas='\n'.join(tool_schemas),
|
| 127 |
+
dsml_token=DSML_TOKEN,
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
system = system or ''
|
| 131 |
+
return f'{system}\n\n{tools_section}' if system else tools_section
|
| 132 |
+
|
| 133 |
+
def _format_tool_calls(self, tool_call_messages) -> str:
|
| 134 |
+
invocations = []
|
| 135 |
+
for message in tool_call_messages:
|
| 136 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 137 |
+
name = tool_call['name']
|
| 138 |
+
arguments = tool_call['arguments']
|
| 139 |
+
if isinstance(arguments, str):
|
| 140 |
+
arguments = json.loads(arguments)
|
| 141 |
+
dsml_args = _encode_arguments_to_dsml(arguments)
|
| 142 |
+
invocations.append(f'<{DSML_TOKEN}invoke name="{name}">\n{dsml_args}\n</{DSML_TOKEN}invoke>')
|
| 143 |
+
|
| 144 |
+
tool_calls_str = '\n'.join(invocations)
|
| 145 |
+
return f'<{DSML_TOKEN}tool_calls>\n{tool_calls_str}\n</{DSML_TOKEN}tool_calls>'
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/extra.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
from typing import List, Optional, Union
|
| 3 |
+
|
| 4 |
+
from .base import BaseAgentTemplate
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class ReactGRPOAgentTemplate(BaseAgentTemplate):
|
| 8 |
+
|
| 9 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 10 |
+
tool_names = []
|
| 11 |
+
tool_descs = []
|
| 12 |
+
for tool in tools:
|
| 13 |
+
tool_desc = self._parse_tool(tool, 'en')
|
| 14 |
+
tool_names.append(tool_desc.name_for_model)
|
| 15 |
+
tool_descs.append(
|
| 16 |
+
f'{tool_desc.name_for_model}: Call this tool to interact with the {tool_desc.name_for_human} API. '
|
| 17 |
+
f'What is the {tool_desc.name_for_human} API useful for? {tool_desc.description_for_model} '
|
| 18 |
+
f'Parameters: {tool_desc.parameters} {tool_desc.args_format}')
|
| 19 |
+
|
| 20 |
+
return """A conversation for tool calling between User and Assistant. The user asks a question which may be solved by calling tools, and the Assistant solves it. The assistant first thinks about the reasoning process in the mind and then provides the user with the answer. The reasoning process should be enclosed within <think> </think>tags and answer should follow the ReACT format(Action:xxx\nAction Input:xxx), i.e., <think> reasoning process here </think> Action: action here\nAction Input: parameters here
|
| 21 |
+
|
| 22 |
+
Answer the following questions as best as you can. You have access to the following tools:
|
| 23 |
+
|
| 24 |
+
""" + '\n\n'.join(tool_descs) + f"""
|
| 25 |
+
|
| 26 |
+
Use the following format:
|
| 27 |
+
|
| 28 |
+
<think>you should always think about what to do</think>
|
| 29 |
+
Action: the action to take, should be one of [{','.join(tool_names)}]
|
| 30 |
+
Action Input: the input to the action
|
| 31 |
+
Observation: the result of the action, given by the actual calling
|
| 32 |
+
... (this Thought/Action/Action Input/Observation can be repeated zero or more times)
|
| 33 |
+
Final Answer: the final answer to the original input question
|
| 34 |
+
|
| 35 |
+
Begin!
|
| 36 |
+
""" # noqa
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/gemma4.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
QUOTE = '<|"|>'
|
| 11 |
+
_STANDARD_KEYS = {'description', 'type', 'properties', 'required', 'nullable'}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Gemma4AgentTemplate(BaseAgentTemplate):
|
| 15 |
+
"""Agent template for Google Gemma-4 models.
|
| 16 |
+
|
| 17 |
+
Reference: chat_template.jinja shipped with google/gemma-4-12B-it.
|
| 18 |
+
Tool definitions are wrapped in `<|tool>...<tool|>` and rendered with the
|
| 19 |
+
custom DSL described by the official chat template.
|
| 20 |
+
Tool calls follow `<|tool_call>call:NAME{key:value,...}<tool_call|>` and
|
| 21 |
+
tool responses follow `<|tool_response>response:NAME{...}<tool_response|>`.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
@classmethod
|
| 25 |
+
def _format_argument(cls, value: Any, escape_keys: bool = True) -> str:
|
| 26 |
+
if isinstance(value, bool):
|
| 27 |
+
return 'true' if value else 'false'
|
| 28 |
+
if isinstance(value, str):
|
| 29 |
+
return f'{QUOTE}{value}{QUOTE}'
|
| 30 |
+
if value is None:
|
| 31 |
+
return 'null'
|
| 32 |
+
if isinstance(value, dict):
|
| 33 |
+
items = []
|
| 34 |
+
for k in sorted(value.keys()):
|
| 35 |
+
v = value[k]
|
| 36 |
+
key_str = f'{QUOTE}{k}{QUOTE}' if escape_keys else str(k)
|
| 37 |
+
items.append(f'{key_str}:{cls._format_argument(v, escape_keys=escape_keys)}')
|
| 38 |
+
return '{' + ','.join(items) + '}'
|
| 39 |
+
if isinstance(value, (list, tuple)):
|
| 40 |
+
return '[' + ','.join(cls._format_argument(item, escape_keys=escape_keys) for item in value) + ']'
|
| 41 |
+
return str(value)
|
| 42 |
+
|
| 43 |
+
@classmethod
|
| 44 |
+
def _format_parameters(cls,
|
| 45 |
+
properties: Dict[str, Any],
|
| 46 |
+
required: Optional[List[str]] = None,
|
| 47 |
+
filter_keys: bool = False) -> str:
|
| 48 |
+
parts = []
|
| 49 |
+
for key in sorted(properties.keys()):
|
| 50 |
+
value = properties[key]
|
| 51 |
+
if filter_keys and key in _STANDARD_KEYS:
|
| 52 |
+
continue
|
| 53 |
+
if not isinstance(value, dict):
|
| 54 |
+
continue
|
| 55 |
+
inner: List[str] = []
|
| 56 |
+
type_upper = (value.get('type') or '').upper() if isinstance(value.get('type'), str) else ''
|
| 57 |
+
if value.get('description'):
|
| 58 |
+
inner.append(f'description:{QUOTE}{value["description"]}{QUOTE}')
|
| 59 |
+
if type_upper == 'STRING':
|
| 60 |
+
if value.get('enum'):
|
| 61 |
+
inner.append(f'enum:{cls._format_argument(value["enum"])}')
|
| 62 |
+
elif type_upper == 'ARRAY':
|
| 63 |
+
items_value = value.get('items')
|
| 64 |
+
if isinstance(items_value, dict) and items_value:
|
| 65 |
+
items_inner: List[str] = []
|
| 66 |
+
items_required = items_value.get('required', [])
|
| 67 |
+
for item_key in sorted(items_value.keys()):
|
| 68 |
+
item_value = items_value[item_key]
|
| 69 |
+
if item_value is None:
|
| 70 |
+
continue
|
| 71 |
+
if item_key == 'properties' and isinstance(item_value, dict):
|
| 72 |
+
items_inner.append(f'properties:{{{cls._format_parameters(item_value, items_required)}}}')
|
| 73 |
+
elif item_key == 'required':
|
| 74 |
+
req_str = ','.join(f'{QUOTE}{r}{QUOTE}' for r in item_value)
|
| 75 |
+
items_inner.append(f'required:[{req_str}]')
|
| 76 |
+
elif item_key == 'type':
|
| 77 |
+
if isinstance(item_value, str):
|
| 78 |
+
items_inner.append(f'type:{cls._format_argument(item_value.upper())}')
|
| 79 |
+
else:
|
| 80 |
+
items_inner.append(f'type:{cls._format_argument([str(t).upper() for t in item_value])}')
|
| 81 |
+
else:
|
| 82 |
+
items_inner.append(f'{item_key}:{cls._format_argument(item_value)}')
|
| 83 |
+
inner.append('items:{' + ','.join(items_inner) + '}')
|
| 84 |
+
if value.get('nullable'):
|
| 85 |
+
inner.append('nullable:true')
|
| 86 |
+
if type_upper == 'OBJECT':
|
| 87 |
+
inner_required = value.get('required', [])
|
| 88 |
+
if isinstance(value.get('properties'), dict):
|
| 89 |
+
inner.append(f'properties:{{{cls._format_parameters(value["properties"], inner_required)}}}')
|
| 90 |
+
else:
|
| 91 |
+
inner.append(f'properties:{{{cls._format_parameters(value, inner_required, filter_keys=True)}}}')
|
| 92 |
+
if value.get('required'):
|
| 93 |
+
req_str = ','.join(f'{QUOTE}{r}{QUOTE}' for r in value['required'])
|
| 94 |
+
inner.append(f'required:[{req_str}]')
|
| 95 |
+
inner.append(f'type:{QUOTE}{type_upper}{QUOTE}')
|
| 96 |
+
parts.append(f'{key}:{{{",".join(inner)}}}')
|
| 97 |
+
return ','.join(parts)
|
| 98 |
+
|
| 99 |
+
@classmethod
|
| 100 |
+
def _format_function_declaration(cls, tool: Dict[str, Any]) -> str:
|
| 101 |
+
function = tool['function']
|
| 102 |
+
name = function.get('name', '')
|
| 103 |
+
description = function.get('description', '') or ''
|
| 104 |
+
result = f'declaration:{name}{{description:{QUOTE}{description}{QUOTE}'
|
| 105 |
+
params = function.get('parameters')
|
| 106 |
+
if params:
|
| 107 |
+
param_parts: List[str] = []
|
| 108 |
+
properties = params.get('properties')
|
| 109 |
+
if properties:
|
| 110 |
+
param_parts.append(f'properties:{{{cls._format_parameters(properties, params.get("required", []))}}}')
|
| 111 |
+
if params.get('required'):
|
| 112 |
+
req_str = ','.join(f'{QUOTE}{r}{QUOTE}' for r in params['required'])
|
| 113 |
+
param_parts.append(f'required:[{req_str}]')
|
| 114 |
+
ptype = params.get('type')
|
| 115 |
+
if isinstance(ptype, str) and ptype:
|
| 116 |
+
param_parts.append(f'type:{QUOTE}{ptype.upper()}{QUOTE}')
|
| 117 |
+
if param_parts:
|
| 118 |
+
result += ',parameters:{' + ','.join(param_parts) + '}'
|
| 119 |
+
result += '}'
|
| 120 |
+
return result
|
| 121 |
+
|
| 122 |
+
def _format_tools(self,
|
| 123 |
+
tools: List[Union[str, dict]],
|
| 124 |
+
system: Optional[str] = None,
|
| 125 |
+
user_message: Optional[dict] = None) -> str:
|
| 126 |
+
tool_blocks: List[str] = []
|
| 127 |
+
for tool in tools:
|
| 128 |
+
tool = self.wrap_tool(tool)
|
| 129 |
+
tool_blocks.append(f'<|tool>{self._format_function_declaration(tool)}<tool|>')
|
| 130 |
+
system_text = (system or '').strip()
|
| 131 |
+
return system_text + ''.join(tool_blocks)
|
| 132 |
+
|
| 133 |
+
def _format_tool_calls(self, tool_call_messages) -> str:
|
| 134 |
+
invocations: List[str] = []
|
| 135 |
+
for message in tool_call_messages:
|
| 136 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 137 |
+
name = tool_call['name']
|
| 138 |
+
arguments = tool_call['arguments']
|
| 139 |
+
if isinstance(arguments, str):
|
| 140 |
+
arguments = self._parse_json(arguments) or {}
|
| 141 |
+
if isinstance(arguments, dict):
|
| 142 |
+
args_str = ','.join(f'{k}:{self._format_argument(arguments[k], escape_keys=False)}'
|
| 143 |
+
for k in sorted(arguments.keys()))
|
| 144 |
+
else:
|
| 145 |
+
args_str = ''
|
| 146 |
+
invocations.append(f'<|tool_call>call:{name}{{{args_str}}}<tool_call|>')
|
| 147 |
+
return ''.join(invocations)
|
| 148 |
+
|
| 149 |
+
def _get_tool_responses(self, tool_messages) -> str:
|
| 150 |
+
parts: List[str] = []
|
| 151 |
+
for tool_message in tool_messages:
|
| 152 |
+
tool_name = tool_message.get('name') or 'unknown'
|
| 153 |
+
tool_content = tool_message.get('content')
|
| 154 |
+
if isinstance(tool_content, dict):
|
| 155 |
+
inner = ','.join(f'{k}:{self._format_argument(tool_content[k], escape_keys=False)}'
|
| 156 |
+
for k in sorted(tool_content.keys()))
|
| 157 |
+
parts.append(f'<|tool_response>response:{tool_name}{{{inner}}}<tool_response|>')
|
| 158 |
+
else:
|
| 159 |
+
# Match jinja: treat string/other content as a single `value:` field.
|
| 160 |
+
value = '' if tool_content is None else tool_content
|
| 161 |
+
parts.append(f'<|tool_response>response:{tool_name}'
|
| 162 |
+
f'{{value:{self._format_argument(value, escape_keys=False)}}}<tool_response|>')
|
| 163 |
+
return ''.join(parts)
|
| 164 |
+
|
| 165 |
+
def _format_tool_responses(
|
| 166 |
+
self,
|
| 167 |
+
assistant_content: str,
|
| 168 |
+
tool_messages,
|
| 169 |
+
) -> Tuple[str, 'Prompt']:
|
| 170 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 171 |
+
if with_action:
|
| 172 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 173 |
+
# If the model hallucinated a trailing `<|tool_response>` opener (e.g. when stop
|
| 174 |
+
# tokens were not configured), strip it so the rendered turn does not contain
|
| 175 |
+
# `<|tool_response><|tool_response>response:...`.
|
| 176 |
+
if assistant_content.endswith('<|tool_response>'):
|
| 177 |
+
assistant_content = assistant_content[:-len('<|tool_response>')]
|
| 178 |
+
# In gemma4, tool_call/tool_response/follow-up assistant text all live in the
|
| 179 |
+
# same `<|turn>model ... <turn|>` block, so we do not open a new model turn here.
|
| 180 |
+
res: 'Prompt' = [self._get_tool_responses(tool_messages)]
|
| 181 |
+
return assistant_content, res
|
| 182 |
+
|
| 183 |
+
@classmethod
|
| 184 |
+
def _gemma_to_json(cls, s: str) -> str:
|
| 185 |
+
# `<|"|>` -> `"`; bare keys preceded by `{` or `,` get JSON-quoted.
|
| 186 |
+
s = s.replace(QUOTE, '"')
|
| 187 |
+
s = re.sub(r'(?<=[\{,])([A-Za-z_][\w\-]*)(?=:)', r'"\1"', s)
|
| 188 |
+
return s
|
| 189 |
+
|
| 190 |
+
@classmethod
|
| 191 |
+
def _parse_arguments(cls, args_body: str) -> Dict[str, Any]:
|
| 192 |
+
json_str = cls._gemma_to_json('{' + args_body + '}')
|
| 193 |
+
try:
|
| 194 |
+
parsed = json.loads(json_str)
|
| 195 |
+
if isinstance(parsed, dict):
|
| 196 |
+
return parsed
|
| 197 |
+
except json.JSONDecodeError:
|
| 198 |
+
pass
|
| 199 |
+
return {}
|
| 200 |
+
|
| 201 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 202 |
+
pattern = re.compile(r'<\|tool_call>call:([^\{]+)\{(.*?)\}<tool_call\|>', re.DOTALL)
|
| 203 |
+
functions: List[Function] = []
|
| 204 |
+
for match in pattern.finditer(response):
|
| 205 |
+
name = match.group(1).strip()
|
| 206 |
+
arguments = self._parse_arguments(match.group(2))
|
| 207 |
+
functions.append(Function(name=name, arguments=json.dumps(arguments, ensure_ascii=False)))
|
| 208 |
+
if not functions:
|
| 209 |
+
return super().get_toolcall(response)
|
| 210 |
+
return functions
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/glm4.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class ChatGLM4AgentTemplate(BaseAgentTemplate):
|
| 12 |
+
is_glm4_0414 = False
|
| 13 |
+
|
| 14 |
+
@staticmethod
|
| 15 |
+
def _find_function_call(single_content: str) -> Optional[Function]:
|
| 16 |
+
single_content = single_content.replace('<|observation|>', '')
|
| 17 |
+
pattern = re.compile(r'([^\n`]*?)\n({.*?})(?=\w*\n|$)', re.DOTALL)
|
| 18 |
+
matches = pattern.findall(single_content)
|
| 19 |
+
if not matches:
|
| 20 |
+
return
|
| 21 |
+
name, arguments = matches[0]
|
| 22 |
+
return Function(name=name, arguments=arguments)
|
| 23 |
+
|
| 24 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 25 |
+
toolcall_list = response.split('<|assistant|>')
|
| 26 |
+
functions = []
|
| 27 |
+
for toolcall in toolcall_list:
|
| 28 |
+
function = self._find_function_call(toolcall)
|
| 29 |
+
if function:
|
| 30 |
+
functions.append(function)
|
| 31 |
+
if len(functions) == 0:
|
| 32 |
+
# compat react_en
|
| 33 |
+
return super().get_toolcall(response)
|
| 34 |
+
return functions
|
| 35 |
+
|
| 36 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 37 |
+
tool_descs = []
|
| 38 |
+
for tool in tools:
|
| 39 |
+
tool = self.unwrap_tool(tool)
|
| 40 |
+
name = self._get_tool_name(tool)
|
| 41 |
+
tool_descs.append(f'## {name}\n\n{json.dumps(tool, ensure_ascii=False, indent=4)}\n'
|
| 42 |
+
'在调用上述函数时,请使用 Json 格式表示调用的参数。')
|
| 43 |
+
glm4_system = '你是一个名为 GLM-4 的人工智能助手。你是基于智谱AI训练的语言模型 GLM-4 模型开发的,你的任务是针对用户的问题和要求提供适当的答复和支持。\n\n' # noqa
|
| 44 |
+
return ('' if self.is_glm4_0414 else glm4_system) + """# 可用工具
|
| 45 |
+
|
| 46 |
+
""" + '\n'.join(tool_descs)
|
| 47 |
+
|
| 48 |
+
def _format_tool_responses(
|
| 49 |
+
self,
|
| 50 |
+
assistant_content: str,
|
| 51 |
+
tool_messages,
|
| 52 |
+
) -> Tuple[str, 'Prompt']:
|
| 53 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 54 |
+
if with_action:
|
| 55 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 56 |
+
res = ['\n']
|
| 57 |
+
for i, tool_message in enumerate(tool_messages):
|
| 58 |
+
tool_content = tool_message['content']
|
| 59 |
+
if i > 0:
|
| 60 |
+
res.append('<|observation|>\n')
|
| 61 |
+
res.append(tool_content)
|
| 62 |
+
res.append('<|assistant|>\n')
|
| 63 |
+
return assistant_content, res
|
| 64 |
+
|
| 65 |
+
def _format_standalone_tool_responses(self, tool_messages) -> 'Prompt':
|
| 66 |
+
res = []
|
| 67 |
+
for tool_message in tool_messages:
|
| 68 |
+
res.extend(['<|observation|>\n', tool_message['content']])
|
| 69 |
+
return res
|
| 70 |
+
|
| 71 |
+
def _format_tool_calls(self, tool_call_messages) -> str:
|
| 72 |
+
tool_calls = []
|
| 73 |
+
for message in tool_call_messages:
|
| 74 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 75 |
+
tool_calls.append(f'{tool_call["name"]}\n{tool_call["arguments"]}')
|
| 76 |
+
return '<|assistant|>'.join(tool_calls) + '<|observation|>'
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class GLM4AgentTemplate(ChatGLM4AgentTemplate):
|
| 80 |
+
is_glm4_0414 = True
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class GLM4_5AgentTemplate(BaseAgentTemplate):
|
| 84 |
+
model_type = 'glm4_5'
|
| 85 |
+
|
| 86 |
+
@staticmethod
|
| 87 |
+
def _find_function_call(single_content: str) -> Optional[Function]:
|
| 88 |
+
single_content = single_content.strip()
|
| 89 |
+
func_name_match = re.match(r'^([^\n<]+)', single_content)
|
| 90 |
+
if not func_name_match:
|
| 91 |
+
return None
|
| 92 |
+
func_name = func_name_match.group(1).strip()
|
| 93 |
+
keys = re.findall(r'<arg_key>(.*?)</arg_key>', single_content, re.DOTALL)
|
| 94 |
+
values = re.findall(r'<arg_value>(.*?)</arg_value>', single_content, re.DOTALL)
|
| 95 |
+
if len(keys) != len(values):
|
| 96 |
+
return None
|
| 97 |
+
args = {k.strip(): v.strip() for k, v in zip(keys, values)}
|
| 98 |
+
return Function(name=func_name, arguments=json.dumps(args, ensure_ascii=False))
|
| 99 |
+
|
| 100 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 101 |
+
toolcall_list = re.findall(r'<tool_call>(.*?)</tool_call>', response, re.DOTALL)
|
| 102 |
+
functions = []
|
| 103 |
+
for toolcall in toolcall_list:
|
| 104 |
+
function = self._find_function_call(toolcall)
|
| 105 |
+
if function:
|
| 106 |
+
functions.append(function)
|
| 107 |
+
if len(functions) == 0:
|
| 108 |
+
# compat react_en
|
| 109 |
+
return super().get_toolcall(response)
|
| 110 |
+
return functions
|
| 111 |
+
|
| 112 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 113 |
+
tool_descs = [
|
| 114 |
+
'# Tools\n\nYou may call one or more functions to assist with the user query.\n\n'
|
| 115 |
+
'You are provided with function signatures within <tools></tools> XML tags:\n<tools>'
|
| 116 |
+
]
|
| 117 |
+
for tool in tools:
|
| 118 |
+
if self.model_type == 'glm5_1':
|
| 119 |
+
tool = self.unwrap_tool(tool)
|
| 120 |
+
tool_descs.append(f'{json.dumps(tool, ensure_ascii=False)}')
|
| 121 |
+
if self.model_type == 'glm4_5':
|
| 122 |
+
tool_desc = ('</tools>\n\nFor each function call, output the function name and arguments within '
|
| 123 |
+
'the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n'
|
| 124 |
+
'<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n'
|
| 125 |
+
'<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>')
|
| 126 |
+
elif self.model_type in {'glm4_7', 'glm5_1'}:
|
| 127 |
+
tool_desc = ('</tools>\n\nFor each function call, output the function name and arguments within '
|
| 128 |
+
'the following XML format:\n<tool_call>{function-name}<arg_key>{arg-key-1}</arg_key>'
|
| 129 |
+
'<arg_value>{arg-value-1}</arg_value><arg_key>{arg-key-2}</arg_key><arg_value>'
|
| 130 |
+
'{arg-value-2}</arg_value>...</tool_call>')
|
| 131 |
+
else:
|
| 132 |
+
raise ValueError("model_type must be one of 'glm4_5', 'glm4_7', or 'glm5_1'.")
|
| 133 |
+
tool_descs.append(tool_desc)
|
| 134 |
+
tool_descs = '\n'.join(tool_descs)
|
| 135 |
+
if system is not None and system.strip():
|
| 136 |
+
tool_descs += '<|system|>\n' + system.strip()
|
| 137 |
+
elif self.model_type in {'glm4_7', 'glm5_1'} and not tool_descs.startswith('\n'):
|
| 138 |
+
tool_descs = '\n' + tool_descs
|
| 139 |
+
return tool_descs
|
| 140 |
+
|
| 141 |
+
def _format_tool_responses(
|
| 142 |
+
self,
|
| 143 |
+
assistant_content: str,
|
| 144 |
+
tool_messages,
|
| 145 |
+
) -> Tuple[str, 'Prompt']:
|
| 146 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 147 |
+
if with_action:
|
| 148 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 149 |
+
if self.model_type == 'glm4_5':
|
| 150 |
+
res = []
|
| 151 |
+
for tool_message in tool_messages:
|
| 152 |
+
tool_content = tool_message['content']
|
| 153 |
+
res.append(f'\n<tool_response>\n{tool_content}\n</tool_response>')
|
| 154 |
+
res.append('<|assistant|>\n')
|
| 155 |
+
elif self.model_type in {'glm4_7', 'glm5_1'}:
|
| 156 |
+
res = []
|
| 157 |
+
for tool_message in tool_messages:
|
| 158 |
+
tool_content = tool_message['content']
|
| 159 |
+
res.append(f'<tool_response>{tool_content}</tool_response>')
|
| 160 |
+
res.append('<|assistant|>')
|
| 161 |
+
return assistant_content, res
|
| 162 |
+
|
| 163 |
+
def _format_standalone_tool_responses(self, tool_messages) -> 'Prompt':
|
| 164 |
+
res = ['<|observation|>']
|
| 165 |
+
for tool_message in tool_messages:
|
| 166 |
+
tool_content = tool_message['content']
|
| 167 |
+
if self.model_type == 'glm4_5':
|
| 168 |
+
res.append(f'\n<tool_response>\n{tool_content}\n</tool_response>')
|
| 169 |
+
elif self.model_type in {'glm4_7', 'glm5_1'}:
|
| 170 |
+
res.append(f'<tool_response>{tool_content}</tool_response>')
|
| 171 |
+
return res
|
| 172 |
+
|
| 173 |
+
def _format_tool_calls(self, tool_call_messages) -> str:
|
| 174 |
+
tool_calls = []
|
| 175 |
+
for message in tool_call_messages:
|
| 176 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 177 |
+
tool_calls.append(f"<tool_call>{tool_call['name']}")
|
| 178 |
+
for arg_key, arg_value in tool_call['arguments'].items():
|
| 179 |
+
tool_calls.append(f'<arg_key>{arg_key}</arg_key>')
|
| 180 |
+
tool_calls.append(f'<arg_value>{arg_value}</arg_value>')
|
| 181 |
+
tool_calls.append('</tool_call>')
|
| 182 |
+
if self.model_type == 'glm4_5':
|
| 183 |
+
sep = '\n'
|
| 184 |
+
elif self.model_type in {'glm4_7', 'glm5_1'}:
|
| 185 |
+
sep = ''
|
| 186 |
+
return sep.join(tool_calls) + '<|observation|>'
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class GLM4_7AgentTemplate(GLM4_5AgentTemplate):
|
| 190 |
+
model_type = 'glm4_7'
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
class GLM5_1AgentTemplate(GLM4_5AgentTemplate):
|
| 194 |
+
model_type = 'glm5_1'
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/hermes.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class HermesAgentTemplate(BaseAgentTemplate):
|
| 12 |
+
|
| 13 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 14 |
+
res_list = re.findall(r'<tool_call>(.+?)</tool_call>', response, re.DOTALL)
|
| 15 |
+
functions = []
|
| 16 |
+
for res in res_list:
|
| 17 |
+
res = self._parse_json(res)
|
| 18 |
+
if isinstance(res, dict) and 'name' in res and 'arguments' in res:
|
| 19 |
+
functions.append(Function(name=res['name'], arguments=res['arguments']))
|
| 20 |
+
if len(functions) == 0:
|
| 21 |
+
# compat react_en
|
| 22 |
+
return super().get_toolcall(response)
|
| 23 |
+
return functions
|
| 24 |
+
|
| 25 |
+
def _get_tool_responses(self, tool_messages):
|
| 26 |
+
res_tool = []
|
| 27 |
+
for tool_message in tool_messages:
|
| 28 |
+
tool_content = tool_message['content']
|
| 29 |
+
res_tool.append(f'<tool_response>\n{tool_content}\n</tool_response>')
|
| 30 |
+
return '\n'.join(res_tool)
|
| 31 |
+
|
| 32 |
+
def _get_tool_calls(self, tool_calls: List[str]):
|
| 33 |
+
return '\n'.join(tool_calls)
|
| 34 |
+
|
| 35 |
+
def _format_tool_responses(
|
| 36 |
+
self,
|
| 37 |
+
assistant_content: str,
|
| 38 |
+
tool_messages,
|
| 39 |
+
) -> Tuple[str, 'Prompt']:
|
| 40 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 41 |
+
if with_action:
|
| 42 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 43 |
+
if hasattr(self, 'template_meta'):
|
| 44 |
+
prompt = self.template_meta.prompt
|
| 45 |
+
chat_sep = self.template_meta.chat_sep
|
| 46 |
+
else:
|
| 47 |
+
prompt = ['<|im_start|>user\n{{QUERY}}<|im_end|>\n<|im_start|>assistant\n']
|
| 48 |
+
chat_sep = ['<|im_end|>\n']
|
| 49 |
+
res = chat_sep.copy()
|
| 50 |
+
total_tool = self._get_tool_responses(tool_messages)
|
| 51 |
+
for context in prompt:
|
| 52 |
+
if isinstance(context, str):
|
| 53 |
+
context = context.replace('{{QUERY}}', total_tool)
|
| 54 |
+
res.append(context)
|
| 55 |
+
return assistant_content, res
|
| 56 |
+
|
| 57 |
+
def _format_standalone_tool_responses(self, tool_messages) -> 'Prompt':
|
| 58 |
+
"""Render standalone results as a native tool-response user turn."""
|
| 59 |
+
if not hasattr(self, 'template_meta'):
|
| 60 |
+
return super()._format_standalone_tool_responses(tool_messages)
|
| 61 |
+
|
| 62 |
+
res = (self.template_meta.chat_sep or []).copy()
|
| 63 |
+
total_tool = self._get_tool_responses(tool_messages)
|
| 64 |
+
for context in self.template_meta.prompt:
|
| 65 |
+
if isinstance(context, str) and '{{QUERY}}' in context:
|
| 66 |
+
query_prefix = context.split('{{QUERY}}', maxsplit=1)[0]
|
| 67 |
+
if query_prefix:
|
| 68 |
+
res.append(query_prefix)
|
| 69 |
+
res.append(total_tool)
|
| 70 |
+
return res
|
| 71 |
+
res.append(context)
|
| 72 |
+
raise ValueError(f'Template prompt does not contain {{{{QUERY}}}}: {self.template_meta.prompt}')
|
| 73 |
+
|
| 74 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 75 |
+
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
|
| 76 |
+
system = system or ''
|
| 77 |
+
return f"""{system}
|
| 78 |
+
|
| 79 |
+
# Tools
|
| 80 |
+
|
| 81 |
+
You may call one or more functions to assist with the user query.
|
| 82 |
+
|
| 83 |
+
You are provided with function signatures within <tools></tools> XML tags:
|
| 84 |
+
<tools>
|
| 85 |
+
""" + '\n'.join(tool_descs) + """
|
| 86 |
+
</tools>
|
| 87 |
+
|
| 88 |
+
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
|
| 89 |
+
<tool_call>
|
| 90 |
+
{"name": <function-name>, "arguments": <args-json-object>}
|
| 91 |
+
</tool_call>"""
|
| 92 |
+
|
| 93 |
+
def _format_tool_calls(self, tool_call_messages):
|
| 94 |
+
tool_calls = []
|
| 95 |
+
for message in tool_call_messages:
|
| 96 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 97 |
+
tool_calls.append(f'<tool_call>\n{json.dumps(tool_call, ensure_ascii=False)}\n</tool_call>')
|
| 98 |
+
return self._get_tool_calls(tool_calls)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class HunyuanHermesAgentTemplate(HermesAgentTemplate):
|
| 102 |
+
|
| 103 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 104 |
+
res_list = re.findall(r'<tool_call>(.+?)\n```json(.+?)```</tool_call>', response, re.DOTALL)
|
| 105 |
+
functions = []
|
| 106 |
+
for name, arguments in res_list:
|
| 107 |
+
arguments = self._parse_json(arguments)
|
| 108 |
+
functions.append(Function(name=name, arguments=arguments))
|
| 109 |
+
if len(functions) == 0:
|
| 110 |
+
# compat react_en
|
| 111 |
+
return super().get_toolcall(response)
|
| 112 |
+
return functions
|
| 113 |
+
|
| 114 |
+
def _get_tool_responses(self, tool_messages):
|
| 115 |
+
res_tool = []
|
| 116 |
+
for tool_message in tool_messages:
|
| 117 |
+
tool_content = tool_message['content']
|
| 118 |
+
res_tool.append(f'<tool_response>{tool_content}</tool_response>')
|
| 119 |
+
tool_responses = '\n'.join(res_tool)
|
| 120 |
+
return f'<tool_responses>{tool_responses}</tool_responses>'
|
| 121 |
+
|
| 122 |
+
def _get_tool_calls(self, tool_calls: List[str]):
|
| 123 |
+
tool_calls = '\n'.join(tool_calls)
|
| 124 |
+
return f'<tool_calls>\n{tool_calls}\n</tool_calls>'
|
| 125 |
+
|
| 126 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 127 |
+
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
|
| 128 |
+
system = system or ''
|
| 129 |
+
if system:
|
| 130 |
+
system = f'{system}\n\n'
|
| 131 |
+
return f"""{system}# Tools
|
| 132 |
+
|
| 133 |
+
You may call one or more functions to assist with the user query.
|
| 134 |
+
|
| 135 |
+
You are provided with function signatures within <tools></tools> XML tags:
|
| 136 |
+
<tools>
|
| 137 |
+
""" + '\n'.join(tool_descs) + """
|
| 138 |
+
</tools>
|
| 139 |
+
|
| 140 |
+
For function call returns, you should first print <tool_calls>For each function call, you should return object like:
|
| 141 |
+
<tool_call>function_name
|
| 142 |
+
```json
|
| 143 |
+
function_arguments_in_json_format
|
| 144 |
+
```</tool_call>At the end of function call returns, you should print </tool_calls>"""
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/hy_v3.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class HyV3PreviewAgentTemplate(BaseAgentTemplate):
|
| 12 |
+
HYTK = ''
|
| 13 |
+
|
| 14 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 15 |
+
# Parse tool calls from <tool_calls>...<tool_call>name<tool_sep>...<arg_key>...<arg_value>...</tool_call>...
|
| 16 |
+
tool_call_blocks = re.findall(rf'<tool_call{self.HYTK}>(.*?)</tool_call{self.HYTK}>', response, re.DOTALL)
|
| 17 |
+
functions = []
|
| 18 |
+
for block in tool_call_blocks:
|
| 19 |
+
# Extract function name: text before <tool_sep>
|
| 20 |
+
name_match = re.match(rf'(.*?)<tool_sep{self.HYTK}>', block, re.DOTALL)
|
| 21 |
+
if not name_match:
|
| 22 |
+
continue
|
| 23 |
+
name = name_match.group(1).strip()
|
| 24 |
+
# Extract arg_key/arg_value pairs together to avoid misalignment
|
| 25 |
+
pairs = re.findall(
|
| 26 |
+
rf'<arg_key{self.HYTK}>(.*?)</arg_key{self.HYTK}>\s*<arg_value{self.HYTK}>(.*?)</arg_value{self.HYTK}>',
|
| 27 |
+
block, re.DOTALL)
|
| 28 |
+
arguments = {}
|
| 29 |
+
for k, v in pairs:
|
| 30 |
+
k = k.strip()
|
| 31 |
+
v = v.strip()
|
| 32 |
+
parsed = self._parse_json(v)
|
| 33 |
+
arguments[k] = parsed if parsed is not None else v
|
| 34 |
+
functions.append(Function(name=name, arguments=arguments))
|
| 35 |
+
if len(functions) == 0:
|
| 36 |
+
# compat react_en
|
| 37 |
+
return super().get_toolcall(response)
|
| 38 |
+
return functions
|
| 39 |
+
|
| 40 |
+
def _get_tool_responses(self, tool_messages):
|
| 41 |
+
res_tool = []
|
| 42 |
+
for tool_message in tool_messages:
|
| 43 |
+
tool_content = tool_message['content']
|
| 44 |
+
res_tool.append(f'<tool_response{self.HYTK}>\n{tool_content}\n</tool_response{self.HYTK}>')
|
| 45 |
+
tool_responses = '\n'.join(res_tool)
|
| 46 |
+
return f'<tool_responses{self.HYTK}>\n{tool_responses}\n</tool_responses{self.HYTK}>'
|
| 47 |
+
|
| 48 |
+
def _get_tool_calls(self, tool_calls: List[str]):
|
| 49 |
+
tool_calls_str = '\n'.join(tool_calls)
|
| 50 |
+
return f'<tool_calls{self.HYTK}>\n{tool_calls_str}\n</tool_calls{self.HYTK}>'
|
| 51 |
+
|
| 52 |
+
def _format_tool_responses(
|
| 53 |
+
self,
|
| 54 |
+
assistant_content: str,
|
| 55 |
+
tool_messages,
|
| 56 |
+
) -> Tuple[str, 'Prompt']:
|
| 57 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 58 |
+
if with_action:
|
| 59 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 60 |
+
res = [f'<|hy_eos{self.HYTK}|>', self._get_tool_responses(tool_messages), f'<|hy_Assistant{self.HYTK}|>']
|
| 61 |
+
return assistant_content, res
|
| 62 |
+
|
| 63 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 64 |
+
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
|
| 65 |
+
system = system or ''
|
| 66 |
+
if system:
|
| 67 |
+
system = f'{system}\n\n'
|
| 68 |
+
return f"""{system}# Tools
|
| 69 |
+
|
| 70 |
+
You may call one or more functions to assist with the user query.
|
| 71 |
+
|
| 72 |
+
You are provided with function signatures within <tools></tools> XML tags:
|
| 73 |
+
<tools>
|
| 74 |
+
""" + '\n'.join(tool_descs) + f"""
|
| 75 |
+
</tools>
|
| 76 |
+
|
| 77 |
+
For function call returns, you should first print <tool_calls{self.HYTK}>
|
| 78 |
+
For each function call, you should return object like:
|
| 79 |
+
<tool_call{self.HYTK}>{{function-name}}<tool_sep{self.HYTK}>
|
| 80 |
+
<arg_key{self.HYTK}>{{arg-key-1}}</arg_key{self.HYTK}>
|
| 81 |
+
<arg_value{self.HYTK}>{{arg-value-1}}</arg_value{self.HYTK}>
|
| 82 |
+
<arg_key{self.HYTK}>{{arg-key-2}}</arg_key{self.HYTK}>
|
| 83 |
+
<arg_value{self.HYTK}>{{arg-value-2}}</arg_value{self.HYTK}>
|
| 84 |
+
...
|
| 85 |
+
</tool_call{self.HYTK}>
|
| 86 |
+
At the end of function call returns, you should print </tool_calls{self.HYTK}>"""
|
| 87 |
+
|
| 88 |
+
def _format_tool_calls(self, tool_call_messages):
|
| 89 |
+
tool_calls = []
|
| 90 |
+
for message in tool_call_messages:
|
| 91 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 92 |
+
name = tool_call['name']
|
| 93 |
+
arguments = tool_call['arguments']
|
| 94 |
+
arg_lines = []
|
| 95 |
+
if isinstance(arguments, dict):
|
| 96 |
+
for k, v in arguments.items():
|
| 97 |
+
if not isinstance(v, str):
|
| 98 |
+
v = json.dumps(v, ensure_ascii=False)
|
| 99 |
+
arg_lines.append(f'<arg_key{self.HYTK}>{k}</arg_key{self.HYTK}>\n'
|
| 100 |
+
f'<arg_value{self.HYTK}>{v}</arg_value{self.HYTK}>')
|
| 101 |
+
arg_str = '\n'.join(arg_lines)
|
| 102 |
+
tool_calls.append(f'<tool_call{self.HYTK}>{name}<tool_sep{self.HYTK}>\n{arg_str}\n</tool_call{self.HYTK}>')
|
| 103 |
+
return self._get_tool_calls(tool_calls)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class HyV3AgentTemplate(HyV3PreviewAgentTemplate):
|
| 107 |
+
HYTK = ':opensource'
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/kimi_k25.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class KimiK25AgentTemplate(BaseAgentTemplate):
|
| 12 |
+
"""Agent template for Kimi K2.5/K2.6 models.
|
| 13 |
+
|
| 14 |
+
Tool calling format:
|
| 15 |
+
- Tools are declared in a separate system message with role 'tool_declare'
|
| 16 |
+
using TypeScript namespace format.
|
| 17 |
+
- Tool calls:
|
| 18 |
+
<|tool_calls_section_begin|>
|
| 19 |
+
<|tool_call_begin|>{function_name}<|tool_call_argument_begin|>{args_json}<|tool_call_end|>
|
| 20 |
+
<|tool_calls_section_end|>
|
| 21 |
+
- Tool response:
|
| 22 |
+
<|im_system|>tool<|im_middle|>## Return of {tool_call_id}
|
| 23 |
+
{content}<|im_end|>
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
@staticmethod
|
| 27 |
+
def _json_type_to_ts(json_type):
|
| 28 |
+
type_map = {
|
| 29 |
+
'string': 'string',
|
| 30 |
+
'number': 'number',
|
| 31 |
+
'integer': 'number',
|
| 32 |
+
'boolean': 'boolean',
|
| 33 |
+
'array': 'any[]',
|
| 34 |
+
'object': 'object',
|
| 35 |
+
'null': 'null',
|
| 36 |
+
}
|
| 37 |
+
return type_map.get(json_type, 'any')
|
| 38 |
+
|
| 39 |
+
def _tools_to_typescript(self, tools):
|
| 40 |
+
parts = []
|
| 41 |
+
for tool in tools:
|
| 42 |
+
tool = self.unwrap_tool(tool)
|
| 43 |
+
name = self._get_tool_name(tool)
|
| 44 |
+
description = tool.get('description', '')
|
| 45 |
+
parameters = tool.get('parameters', {})
|
| 46 |
+
properties = parameters.get('properties', {})
|
| 47 |
+
|
| 48 |
+
lines = []
|
| 49 |
+
if description:
|
| 50 |
+
lines.append(f'// {description}')
|
| 51 |
+
|
| 52 |
+
if not properties:
|
| 53 |
+
lines.append(f'type {name} = (_: {{}}) => any;')
|
| 54 |
+
else:
|
| 55 |
+
lines.append(f'type {name} = (_: {{')
|
| 56 |
+
props = list(properties.items())
|
| 57 |
+
for i, (pname, pschema) in enumerate(props):
|
| 58 |
+
pdesc = pschema.get('description', '')
|
| 59 |
+
ptype = self._json_type_to_ts(pschema.get('type', ''))
|
| 60 |
+
if pdesc:
|
| 61 |
+
lines.append(f' // {pdesc}')
|
| 62 |
+
if i < len(props) - 1:
|
| 63 |
+
lines.append(f' {pname}: {ptype},')
|
| 64 |
+
else:
|
| 65 |
+
lines.append(f' {pname}: {ptype}')
|
| 66 |
+
lines.append('}) => any;')
|
| 67 |
+
parts.append('\n'.join(lines))
|
| 68 |
+
return '\n'.join(parts)
|
| 69 |
+
|
| 70 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 71 |
+
pattern = r'<\|tool_call_begin\|>(.*?)<\|tool_call_argument_begin\|>(.*?)<\|tool_call_end\|>'
|
| 72 |
+
res_list = re.findall(pattern, response, re.DOTALL)
|
| 73 |
+
functions = []
|
| 74 |
+
for name, arguments in res_list:
|
| 75 |
+
name = name.strip()
|
| 76 |
+
arguments = arguments.strip()
|
| 77 |
+
parsed_args = self._parse_json(arguments)
|
| 78 |
+
if parsed_args is not None:
|
| 79 |
+
functions.append(Function(name=name, arguments=parsed_args))
|
| 80 |
+
else:
|
| 81 |
+
functions.append(Function(name=name, arguments=arguments))
|
| 82 |
+
if len(functions) == 0:
|
| 83 |
+
# compat react_en
|
| 84 |
+
return super().get_toolcall(response)
|
| 85 |
+
return functions
|
| 86 |
+
|
| 87 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 88 |
+
ts_tools = self._tools_to_typescript(tools)
|
| 89 |
+
tool_content = f'# Tools\n\n## functions\nnamespace functions {{\n{ts_tools}\n}}\n'
|
| 90 |
+
system = system or ''
|
| 91 |
+
res = f'tool_declare<|im_middle|>{tool_content}'
|
| 92 |
+
if system:
|
| 93 |
+
res += f'<|im_end|><|im_system|>system<|im_middle|>{system}'
|
| 94 |
+
return res
|
| 95 |
+
|
| 96 |
+
def _format_tool_calls(self, tool_call_messages) -> str:
|
| 97 |
+
tool_calls = []
|
| 98 |
+
for message in tool_call_messages:
|
| 99 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 100 |
+
name = tool_call['name']
|
| 101 |
+
arguments = json.dumps(tool_call['arguments'], ensure_ascii=False)
|
| 102 |
+
tool_calls.append(f'<|tool_call_begin|>{name}<|tool_call_argument_begin|>{arguments}<|tool_call_end|>')
|
| 103 |
+
return f'<|tool_calls_section_begin|>{"".join(tool_calls)}<|tool_calls_section_end|>'
|
| 104 |
+
|
| 105 |
+
def _format_tool_responses(
|
| 106 |
+
self,
|
| 107 |
+
assistant_content: str,
|
| 108 |
+
tool_messages,
|
| 109 |
+
) -> Tuple[str, 'Prompt']:
|
| 110 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 111 |
+
if with_action:
|
| 112 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 113 |
+
res = ['<|im_end|>']
|
| 114 |
+
for tool_message in tool_messages:
|
| 115 |
+
tool_call_id = tool_message.get('tool_call_id', '')
|
| 116 |
+
tool_content = tool_message['content']
|
| 117 |
+
res.append(f'<|im_system|>tool<|im_middle|>## Return of {tool_call_id}\n{tool_content}<|im_end|>')
|
| 118 |
+
res.append('<|im_assistant|>assistant<|im_middle|>')
|
| 119 |
+
return assistant_content, res
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/kimi_k3.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class KimiK3AgentTemplate(BaseAgentTemplate):
|
| 12 |
+
"""Agent template for Kimi K3 models (XTML format).
|
| 13 |
+
|
| 14 |
+
Aligned with `encoding_k3.py` in the model repo:
|
| 15 |
+
- Tools are declared in a separate system message with `type="tool-declare"`,
|
| 16 |
+
carrying the deep-sorted JSONSchema in a compact ```json block.
|
| 17 |
+
- Tool calls follow the closed response channel:
|
| 18 |
+
<|close|>response<|sep|><|open|>tools<|sep|>
|
| 19 |
+
<|open|>call tool="{name}" index="{i}"<|sep|>
|
| 20 |
+
<|open|>argument key="{key}" type="{type}"<|sep|>{value}<|close|>argument<|sep|>
|
| 21 |
+
<|close|>call<|sep|><|close|>tools<|sep|>
|
| 22 |
+
- Tool response:
|
| 23 |
+
<|open|>message role="tool" tool="{name}" index="{i}"<|sep|>{content}<|close|>message<|sep|><|end_of_msg|>
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
@staticmethod
|
| 27 |
+
def _deep_sort(obj):
|
| 28 |
+
if isinstance(obj, dict):
|
| 29 |
+
return {k: KimiK3AgentTemplate._deep_sort(v) for k, v in sorted(obj.items())}
|
| 30 |
+
if isinstance(obj, list):
|
| 31 |
+
return [KimiK3AgentTemplate._deep_sort(item) for item in obj]
|
| 32 |
+
return obj
|
| 33 |
+
|
| 34 |
+
@staticmethod
|
| 35 |
+
def _escape_attr(value) -> str:
|
| 36 |
+
return str(value).replace('&', '&').replace('"', '"')
|
| 37 |
+
|
| 38 |
+
@staticmethod
|
| 39 |
+
def _unescape_attr(value: str) -> str:
|
| 40 |
+
return value.replace('"', '"').replace('&', '&')
|
| 41 |
+
|
| 42 |
+
@staticmethod
|
| 43 |
+
def _xtml_type(value) -> str:
|
| 44 |
+
if isinstance(value, bool):
|
| 45 |
+
return 'boolean'
|
| 46 |
+
if value is None:
|
| 47 |
+
return 'null'
|
| 48 |
+
if isinstance(value, (int, float)):
|
| 49 |
+
return 'number'
|
| 50 |
+
if isinstance(value, str):
|
| 51 |
+
return 'string'
|
| 52 |
+
if isinstance(value, dict):
|
| 53 |
+
return 'object'
|
| 54 |
+
return 'array'
|
| 55 |
+
|
| 56 |
+
@staticmethod
|
| 57 |
+
def _xtml_value(value) -> str:
|
| 58 |
+
if isinstance(value, str):
|
| 59 |
+
return value
|
| 60 |
+
return json.dumps(value, ensure_ascii=False)
|
| 61 |
+
|
| 62 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 63 |
+
call_pattern = r'<\|open\|>call tool="(.*?)"(?: index="\d+")?<\|sep\|>(.*?)<\|close\|>call<\|sep\|>'
|
| 64 |
+
arg_pattern = r'<\|open\|>argument key="(.*?)" type="(.*?)"<\|sep\|>(.*?)<\|close\|>argument<\|sep\|>'
|
| 65 |
+
json_pattern = r'<\|open\|>json type="object"<\|sep\|>(.*?)<\|close\|>json<\|sep\|>'
|
| 66 |
+
functions = []
|
| 67 |
+
for name, body in re.findall(call_pattern, response, re.DOTALL):
|
| 68 |
+
name = self._unescape_attr(name)
|
| 69 |
+
json_match = re.search(json_pattern, body, re.DOTALL)
|
| 70 |
+
if json_match:
|
| 71 |
+
arguments = self._parse_json(json_match.group(1))
|
| 72 |
+
if arguments is None:
|
| 73 |
+
arguments = json_match.group(1)
|
| 74 |
+
else:
|
| 75 |
+
arguments = {}
|
| 76 |
+
for key, arg_type, value in re.findall(arg_pattern, body, re.DOTALL):
|
| 77 |
+
key = self._unescape_attr(key)
|
| 78 |
+
if arg_type == 'string':
|
| 79 |
+
arguments[key] = value
|
| 80 |
+
elif arg_type == 'null':
|
| 81 |
+
arguments[key] = None
|
| 82 |
+
else:
|
| 83 |
+
parsed = self._parse_json(value)
|
| 84 |
+
arguments[key] = value if parsed is None else parsed
|
| 85 |
+
functions.append(Function(name=name, arguments=arguments))
|
| 86 |
+
if len(functions) == 0:
|
| 87 |
+
# compat react_en
|
| 88 |
+
return super().get_toolcall(response)
|
| 89 |
+
return functions
|
| 90 |
+
|
| 91 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 92 |
+
tools = self._deep_sort([self.wrap_tool(tool) for tool in tools])
|
| 93 |
+
tools_json = json.dumps(tools, ensure_ascii=False, separators=(',', ':'))
|
| 94 |
+
tool_content = ('# Tools\nHere are the available tools, described in JSONSchema.\n\n'
|
| 95 |
+
f'```json\n{tools_json}\n```')
|
| 96 |
+
# Hijack the `<|open|>message {{SYSTEM}}<|close|>message<|sep|><|end_of_msg|>` system
|
| 97 |
+
# prefix so the tool-declare message is rendered before the plain system message.
|
| 98 |
+
res = f'role="system" type="tool-declare"<|sep|>{tool_content}'
|
| 99 |
+
if system:
|
| 100 |
+
res += f'<|close|>message<|sep|><|end_of_msg|><|open|>message role="system"<|sep|>{system}'
|
| 101 |
+
return res
|
| 102 |
+
|
| 103 |
+
def _format_tool_calls(self, tool_call_messages) -> str:
|
| 104 |
+
calls = []
|
| 105 |
+
for index, message in enumerate(tool_call_messages, start=1):
|
| 106 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 107 |
+
name, arguments = tool_call['name'], tool_call['arguments']
|
| 108 |
+
call = [f'<|open|>call tool="{self._escape_attr(name)}" index="{index}"<|sep|>']
|
| 109 |
+
if isinstance(arguments, dict):
|
| 110 |
+
for key, value in arguments.items():
|
| 111 |
+
call.append(f'<|open|>argument key="{self._escape_attr(key)}" '
|
| 112 |
+
f'type="{self._xtml_type(value)}"<|sep|>'
|
| 113 |
+
f'{self._xtml_value(value)}<|close|>argument<|sep|>')
|
| 114 |
+
else:
|
| 115 |
+
# non-object arguments fall back to a raw json block
|
| 116 |
+
json_block = arguments if isinstance(arguments, str) else json.dumps(arguments, ensure_ascii=False)
|
| 117 |
+
call.append(f'<|open|>json type="object"<|sep|>{json_block}<|close|>json<|sep|>')
|
| 118 |
+
call.append('<|close|>call<|sep|>')
|
| 119 |
+
calls.append(''.join(call))
|
| 120 |
+
return f'<|open|>tools<|sep|>{"".join(calls)}<|close|>tools<|sep|>'
|
| 121 |
+
|
| 122 |
+
def _add_tool_call_prefix(self, tool_content: str, pre_message=None) -> str:
|
| 123 |
+
# Without a preceding assistant text message (whose converted content already ends
|
| 124 |
+
# with `<|close|>response<|sep|>`), the empty response channel must be closed here.
|
| 125 |
+
if pre_message is None or pre_message['role'] != 'assistant':
|
| 126 |
+
tool_content = '<|close|>response<|sep|>' + tool_content
|
| 127 |
+
return tool_content
|
| 128 |
+
|
| 129 |
+
def _format_tool_responses(
|
| 130 |
+
self,
|
| 131 |
+
assistant_content,
|
| 132 |
+
tool_messages,
|
| 133 |
+
) -> Tuple[str, 'Prompt']:
|
| 134 |
+
contents = assistant_content if isinstance(assistant_content, list) else [assistant_content]
|
| 135 |
+
joined = ''.join(c for c in contents if isinstance(c, str))
|
| 136 |
+
with_action = self.keyword.action in joined and self.keyword.action_input in joined
|
| 137 |
+
if with_action:
|
| 138 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 139 |
+
# `tool`/`index` attributes are resolved from the preceding assistant tool calls,
|
| 140 |
+
# in call order (aligned with the `tool_index` enumeration in encoding_k3.py).
|
| 141 |
+
names = re.findall(r'<\|open\|>call tool="(.*?)" index="\d+"<\|sep\|>', joined)
|
| 142 |
+
res = ['<|close|>message<|sep|><|end_of_msg|>']
|
| 143 |
+
for index, tool_message in enumerate(tool_messages, start=1):
|
| 144 |
+
if index <= len(names):
|
| 145 |
+
name = self._unescape_attr(names[index - 1])
|
| 146 |
+
else:
|
| 147 |
+
name = tool_message.get('tool') or tool_message.get('name') or ''
|
| 148 |
+
res.append(f'<|open|>message role="tool" tool="{self._escape_attr(name)}" index="{index}"<|sep|>'
|
| 149 |
+
f'{tool_message["content"]}<|close|>message<|sep|><|end_of_msg|>')
|
| 150 |
+
res.append('<|open|>message role="assistant"<|sep|>')
|
| 151 |
+
return assistant_content, res
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/llama.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Llama3AgentTemplate(BaseAgentTemplate):
|
| 12 |
+
eom_token = '<|eom_id|>'
|
| 13 |
+
start_token = '<|start_header_id|>'
|
| 14 |
+
end_token = '<|end_header_id|>'
|
| 15 |
+
eot_token = '<|eot_id|>'
|
| 16 |
+
|
| 17 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 18 |
+
if response.endswith(self.eom_token):
|
| 19 |
+
response = response[:-len(self.eom_token)]
|
| 20 |
+
functions = []
|
| 21 |
+
res_list = re.findall(r'{[^{]*?"name":.*?"parameters":\s*?{.*?}\s*?}', response, re.DOTALL)
|
| 22 |
+
for res in res_list:
|
| 23 |
+
res = self._parse_json(res)
|
| 24 |
+
if isinstance(res, dict) and 'name' in res and 'parameters' in res:
|
| 25 |
+
functions.append(Function(name=res['name'], arguments=res['parameters']))
|
| 26 |
+
if len(functions) == 0:
|
| 27 |
+
# compat react_en
|
| 28 |
+
return super().get_toolcall(response)
|
| 29 |
+
return functions
|
| 30 |
+
|
| 31 |
+
def _format_tool_responses(
|
| 32 |
+
self,
|
| 33 |
+
assistant_content: str,
|
| 34 |
+
tool_messages,
|
| 35 |
+
) -> Tuple[str, 'Prompt']:
|
| 36 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 37 |
+
if with_action:
|
| 38 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 39 |
+
res = [self.eot_token]
|
| 40 |
+
for tool_message in tool_messages:
|
| 41 |
+
tool_content = tool_message['content']
|
| 42 |
+
res.append(f'{self.start_token}tool{self.end_token}\n\n{tool_content}{self.eot_token}')
|
| 43 |
+
res.append(f'{self.start_token}assistant{self.end_token}\n\n')
|
| 44 |
+
return assistant_content, res
|
| 45 |
+
|
| 46 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 47 |
+
assert user_message is not None
|
| 48 |
+
user_content = user_message['content']
|
| 49 |
+
tool_descs = [json.dumps(tool, ensure_ascii=False, indent=4) for tool in tools]
|
| 50 |
+
new_user_content = """Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.
|
| 51 |
+
|
| 52 |
+
Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}. Do not use variables.
|
| 53 |
+
|
| 54 |
+
""" + '\n\n'.join(tool_descs) + f"""
|
| 55 |
+
|
| 56 |
+
{user_content}""" # noqa
|
| 57 |
+
user_message['content'] = new_user_content
|
| 58 |
+
return system or ''
|
| 59 |
+
|
| 60 |
+
def _format_tool_calls(self, tool_call_messages) -> str:
|
| 61 |
+
tool_calls = []
|
| 62 |
+
for message in tool_call_messages:
|
| 63 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 64 |
+
tool_call['parameters'] = tool_call.pop('arguments')
|
| 65 |
+
tool_calls.append(json.dumps(tool_call, ensure_ascii=False))
|
| 66 |
+
return '\n'.join(tool_calls)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class Llama4AgentTemplate(Llama3AgentTemplate):
|
| 70 |
+
eom_token = '<|eom|>'
|
| 71 |
+
start_token = '<|header_start|>'
|
| 72 |
+
end_token = '<|header_end|>'
|
| 73 |
+
eot_token = '<|eot|>'
|
| 74 |
+
toolcall_pattern = r'(.+?)<\|eom\|>'
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/mapping.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
from .deepseek_v3_1 import DeepSeekV31AgentTemplate
|
| 3 |
+
from .deepseek_v4 import DeepSeekV4AgentTemplate
|
| 4 |
+
from .extra import ReactGRPOAgentTemplate
|
| 5 |
+
from .gemma4 import Gemma4AgentTemplate
|
| 6 |
+
from .glm4 import (ChatGLM4AgentTemplate, GLM4_5AgentTemplate, GLM4_7AgentTemplate, GLM4AgentTemplate,
|
| 7 |
+
GLM5_1AgentTemplate)
|
| 8 |
+
from .hermes import HermesAgentTemplate, HunyuanHermesAgentTemplate
|
| 9 |
+
from .hy_v3 import HyV3AgentTemplate, HyV3PreviewAgentTemplate
|
| 10 |
+
from .kimi_k3 import KimiK3AgentTemplate
|
| 11 |
+
from .kimi_k25 import KimiK25AgentTemplate
|
| 12 |
+
from .llama import Llama3AgentTemplate, Llama4AgentTemplate
|
| 13 |
+
from .minicpm5 import MiniCPM5AgentTemplate
|
| 14 |
+
from .minimax_m2 import MinimaxM2AgentTemplate
|
| 15 |
+
from .minimax_m3 import MinimaxM3AgentTemplate
|
| 16 |
+
from .mistral import MistralAgentTemplate
|
| 17 |
+
from .qwen import QwenEnAgentTemplate, QwenEnParallelAgentTemplate, QwenZhAgentTemplate, QwenZhParallelAgentTemplate
|
| 18 |
+
from .qwen3_coder import Qwen3_5AgentTemplate, Qwen3CoderAgentTemplate
|
| 19 |
+
from .react import ReactEnAgentTemplate, ReactZnAgentTemplate
|
| 20 |
+
from .seed_oss import SeedAgentTemplate
|
| 21 |
+
from .toolbench import ToolBenchAgentTemplate
|
| 22 |
+
from .youtu import YoutuAgentTemplate
|
| 23 |
+
|
| 24 |
+
agent_template_map = {
|
| 25 |
+
# ref: https://qwen.readthedocs.io/zh-cn/latest/framework/function_call.html#function-calling-templates
|
| 26 |
+
'react_en': ReactEnAgentTemplate,
|
| 27 |
+
'react_zh': ReactZnAgentTemplate,
|
| 28 |
+
# ref: https://github.com/QwenLM/Qwen-Agent/blob/main/qwen_agent/llm/fncall_prompts/qwen_fncall_prompt.py
|
| 29 |
+
'qwen_en': QwenEnAgentTemplate,
|
| 30 |
+
'qwen_zh': QwenZhAgentTemplate,
|
| 31 |
+
'qwen_en_parallel': QwenEnParallelAgentTemplate,
|
| 32 |
+
'qwen_zh_parallel': QwenZhParallelAgentTemplate,
|
| 33 |
+
'qwen3_coder': Qwen3CoderAgentTemplate,
|
| 34 |
+
'qwen3_5': Qwen3_5AgentTemplate,
|
| 35 |
+
'hermes': HermesAgentTemplate,
|
| 36 |
+
'hunyuan_hermes': HunyuanHermesAgentTemplate,
|
| 37 |
+
'hy_v3_preview': HyV3PreviewAgentTemplate,
|
| 38 |
+
'hy_v3': HyV3AgentTemplate,
|
| 39 |
+
'toolbench': ToolBenchAgentTemplate, # ref: https://modelscope.cn/datasets/swift/ToolBench
|
| 40 |
+
'chatglm4': ChatGLM4AgentTemplate,
|
| 41 |
+
'glm4': GLM4AgentTemplate, # ref: https://modelscope.cn/models/ZhipuAI/GLM-4-9B-0414
|
| 42 |
+
'glm4_5': GLM4_5AgentTemplate,
|
| 43 |
+
'glm4_7': GLM4_7AgentTemplate,
|
| 44 |
+
'glm5_1': GLM5_1AgentTemplate,
|
| 45 |
+
'llama3': Llama3AgentTemplate,
|
| 46 |
+
'llama4': Llama4AgentTemplate,
|
| 47 |
+
# ref: https://huggingface.co/deepseek-ai/DeepSeek-V3.1
|
| 48 |
+
'deepseek_v3_1': DeepSeekV31AgentTemplate,
|
| 49 |
+
# ref: https://modelscope.cn/models/deepseek-ai/DeepSeek-V4-Flash
|
| 50 |
+
'deepseek_v4': DeepSeekV4AgentTemplate,
|
| 51 |
+
'minimax_m2': MinimaxM2AgentTemplate,
|
| 52 |
+
'minimax_m3': MinimaxM3AgentTemplate,
|
| 53 |
+
'seed_oss': SeedAgentTemplate,
|
| 54 |
+
# ref: https://modelscope.cn/models/google/gemma-4-12B-it
|
| 55 |
+
'gemma4': Gemma4AgentTemplate,
|
| 56 |
+
# extra
|
| 57 |
+
'react_grpo': ReactGRPOAgentTemplate,
|
| 58 |
+
'mistral': MistralAgentTemplate,
|
| 59 |
+
'youtu': YoutuAgentTemplate,
|
| 60 |
+
'kimi_k25': KimiK25AgentTemplate,
|
| 61 |
+
'kimi_k3': KimiK3AgentTemplate,
|
| 62 |
+
'minicpm5': MiniCPM5AgentTemplate,
|
| 63 |
+
}
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/minicpm5.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class MiniCPM5AgentTemplate(BaseAgentTemplate):
|
| 12 |
+
"""Agent template for MiniCPM5 models using XML-based function calling format.
|
| 13 |
+
|
| 14 |
+
Tool call format:
|
| 15 |
+
<function name="function-name"><param name="param-name">param-value</param></function>
|
| 16 |
+
|
| 17 |
+
Tool response format:
|
| 18 |
+
<tool_response>
|
| 19 |
+
response_content
|
| 20 |
+
</tool_response>
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 24 |
+
# Match <function name="...">...</function> blocks
|
| 25 |
+
func_pattern = re.compile(r'<function\s+name="([^"]+)">(.*?)</function>', re.DOTALL)
|
| 26 |
+
param_pattern = re.compile(r'<param\s+name="([^"]+)">'
|
| 27 |
+
r'(?:<!\[CDATA\[(.*?)\]\]>|([^<]*))'
|
| 28 |
+
r'</param>', re.DOTALL)
|
| 29 |
+
|
| 30 |
+
functions = []
|
| 31 |
+
for func_match in func_pattern.finditer(response):
|
| 32 |
+
func_name = func_match.group(1)
|
| 33 |
+
func_body = func_match.group(2)
|
| 34 |
+
arguments = {}
|
| 35 |
+
for param_match in param_pattern.finditer(func_body):
|
| 36 |
+
param_name = param_match.group(1)
|
| 37 |
+
# CDATA value or plain value
|
| 38 |
+
param_value = param_match.group(2) if param_match.group(2) is not None else param_match.group(3)
|
| 39 |
+
# Try to parse as JSON value (number, bool, etc.)
|
| 40 |
+
try:
|
| 41 |
+
param_value = json.loads(param_value)
|
| 42 |
+
except (json.JSONDecodeError, ValueError):
|
| 43 |
+
pass
|
| 44 |
+
arguments[param_name] = param_value
|
| 45 |
+
functions.append(Function(name=func_name, arguments=arguments))
|
| 46 |
+
|
| 47 |
+
if len(functions) == 0:
|
| 48 |
+
# Fallback to ReAct-style parsing
|
| 49 |
+
return super().get_toolcall(response)
|
| 50 |
+
return functions
|
| 51 |
+
|
| 52 |
+
def _get_tool_responses(self, tool_messages):
|
| 53 |
+
res_tool = []
|
| 54 |
+
for tool_message in tool_messages:
|
| 55 |
+
tool_content = tool_message['content']
|
| 56 |
+
res_tool.append(f'<tool_response>\n{tool_content}\n</tool_response>')
|
| 57 |
+
return '\n'.join(res_tool)
|
| 58 |
+
|
| 59 |
+
def _format_tool_responses(
|
| 60 |
+
self,
|
| 61 |
+
assistant_content: str,
|
| 62 |
+
tool_messages,
|
| 63 |
+
) -> Tuple[str, 'Prompt']:
|
| 64 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 65 |
+
if with_action:
|
| 66 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 67 |
+
if hasattr(self, 'template_meta'):
|
| 68 |
+
prompt = self.template_meta.prompt
|
| 69 |
+
chat_sep = self.template_meta.chat_sep
|
| 70 |
+
else:
|
| 71 |
+
prompt = ['<|im_start|>user\n{{QUERY}}<|im_end|>\n<|im_start|>assistant\n']
|
| 72 |
+
chat_sep = ['<|im_end|>\n']
|
| 73 |
+
res = chat_sep.copy()
|
| 74 |
+
total_tool = self._get_tool_responses(tool_messages)
|
| 75 |
+
for context in prompt:
|
| 76 |
+
if isinstance(context, str):
|
| 77 |
+
context = context.replace('{{QUERY}}', total_tool)
|
| 78 |
+
res.append(context)
|
| 79 |
+
return assistant_content, res
|
| 80 |
+
|
| 81 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 82 |
+
tool_descs = [json.dumps(self.wrap_tool(tool), ensure_ascii=False) for tool in tools]
|
| 83 |
+
system = system or ''
|
| 84 |
+
if system:
|
| 85 |
+
system = f'{system}\n\n'
|
| 86 |
+
return (f'{system}# Tools\n\n'
|
| 87 |
+
'You are provided with function signatures within <tools></tools> XML tags:\n'
|
| 88 |
+
'<tools>\n' + '\n'.join(tool_descs) + '\n</tools>\n\n'
|
| 89 |
+
'Tool usage guidelines:\n'
|
| 90 |
+
'- You may call zero or more functions. If no function calls are needed, '
|
| 91 |
+
'just answer normally and do not include any <function ... </function>.\n'
|
| 92 |
+
'- When calling a function, return an XML object within <function ... </function> using:\n'
|
| 93 |
+
'<function name="function-name"><param name="param-name">param-value</param></function>\n'
|
| 94 |
+
'- param-value may be multi-line. If it contains <, & or newline characters, '
|
| 95 |
+
'wrap it in a CDATA block: <param name="param-name"><![CDATA[...multi-line value...]]></param>')
|
| 96 |
+
|
| 97 |
+
def _format_tool_calls(self, tool_call_messages) -> str:
|
| 98 |
+
tool_calls = []
|
| 99 |
+
for message in tool_call_messages:
|
| 100 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 101 |
+
name = tool_call['name']
|
| 102 |
+
arguments = tool_call['arguments']
|
| 103 |
+
params_xml = ''
|
| 104 |
+
if isinstance(arguments, dict):
|
| 105 |
+
for param_name, param_value in arguments.items():
|
| 106 |
+
value_str = param_value if isinstance(param_value, str) else json.dumps(
|
| 107 |
+
param_value, ensure_ascii=False)
|
| 108 |
+
if isinstance(param_value, str) and ('<' in param_value or '&' in param_value
|
| 109 |
+
or '\n' in param_value):
|
| 110 |
+
params_xml += f'<param name="{param_name}"><![CDATA[{value_str}]]></param>'
|
| 111 |
+
else:
|
| 112 |
+
params_xml += f'<param name="{param_name}">{value_str}</param>'
|
| 113 |
+
tool_calls.append(f'<function name="{name}">{params_xml}</function>')
|
| 114 |
+
return '\n'.join(tool_calls)
|
pipelines/Tr006_V2/project/ms-swift/swift/agent_template/minimax_m2.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) ModelScope Contributors. All rights reserved.
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
from typing import List, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
from swift.infer_engine import Function
|
| 7 |
+
from swift.template import Prompt
|
| 8 |
+
from .base import BaseAgentTemplate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class MinimaxM2AgentTemplate(BaseAgentTemplate):
|
| 12 |
+
"""
|
| 13 |
+
Agent template for MiniMax-M2 series models.
|
| 14 |
+
|
| 15 |
+
This template handles tool calling in MiniMax's XML-based format:
|
| 16 |
+
<minimax:tool_call>
|
| 17 |
+
<invoke name="tool-name">
|
| 18 |
+
<parameter name="param-key">param-value</parameter>
|
| 19 |
+
</invoke>
|
| 20 |
+
</minimax:tool_call>
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def get_toolcall(self, response: str) -> List[Function]:
|
| 24 |
+
"""
|
| 25 |
+
Extract tool calls from MiniMax response format.
|
| 26 |
+
|
| 27 |
+
Format:
|
| 28 |
+
<minimax:tool_call>
|
| 29 |
+
<invoke name="tool-name">
|
| 30 |
+
<parameter name="param-key">param-value</parameter>
|
| 31 |
+
</invoke>
|
| 32 |
+
</minimax:tool_call>
|
| 33 |
+
"""
|
| 34 |
+
functions = []
|
| 35 |
+
|
| 36 |
+
# Find all tool_call blocks
|
| 37 |
+
tool_call_blocks = re.findall(r'<minimax:tool_call>(.*?)</minimax:tool_call>', response, re.DOTALL)
|
| 38 |
+
|
| 39 |
+
for block in tool_call_blocks:
|
| 40 |
+
# Find all invoke blocks within the tool_call
|
| 41 |
+
invoke_blocks = re.findall(r'<invoke name="([^"]+)">(.*?)</invoke>', block, re.DOTALL)
|
| 42 |
+
|
| 43 |
+
for tool_name, params_block in invoke_blocks:
|
| 44 |
+
# Extract parameters
|
| 45 |
+
params = {}
|
| 46 |
+
param_matches = re.findall(r'<parameter name="([^"]+)">(.*?)</parameter>', params_block, re.DOTALL)
|
| 47 |
+
|
| 48 |
+
for param_name, param_value in param_matches:
|
| 49 |
+
param_value = param_value.strip()
|
| 50 |
+
# Try to parse as JSON if it looks like a JSON structure
|
| 51 |
+
parsed_value = self._parse_json(param_value)
|
| 52 |
+
params[param_name] = parsed_value if parsed_value is not None else param_value
|
| 53 |
+
|
| 54 |
+
functions.append(Function(name=tool_name, arguments=params))
|
| 55 |
+
|
| 56 |
+
# Fallback to react format if no functions found
|
| 57 |
+
if len(functions) == 0:
|
| 58 |
+
return super().get_toolcall(response)
|
| 59 |
+
|
| 60 |
+
return functions
|
| 61 |
+
|
| 62 |
+
def _format_tool_responses(
|
| 63 |
+
self,
|
| 64 |
+
assistant_content: str,
|
| 65 |
+
tool_messages,
|
| 66 |
+
) -> Tuple[str, 'Prompt']:
|
| 67 |
+
"""
|
| 68 |
+
Format tool execution results in MiniMax format.
|
| 69 |
+
|
| 70 |
+
Tool responses are wrapped in <response></response> tags.
|
| 71 |
+
"""
|
| 72 |
+
# Check if using react format
|
| 73 |
+
with_action = self.keyword.action in assistant_content and self.keyword.action_input in assistant_content
|
| 74 |
+
if with_action:
|
| 75 |
+
return super()._format_tool_responses(assistant_content, tool_messages)
|
| 76 |
+
|
| 77 |
+
# Use template meta if available
|
| 78 |
+
if hasattr(self, 'template_meta'):
|
| 79 |
+
prompt = self.template_meta.prompt.copy()
|
| 80 |
+
chat_sep = self.template_meta.chat_sep
|
| 81 |
+
for i in range(len(prompt)):
|
| 82 |
+
if isinstance(prompt[i], str):
|
| 83 |
+
prompt[i] = prompt[i].replace('user', 'tool')
|
| 84 |
+
else:
|
| 85 |
+
# Default format based on the Jinja2 template
|
| 86 |
+
prompt = [']~b]tool\n{{QUERY}}[e~[\n']
|
| 87 |
+
chat_sep = ['[e~[\n']
|
| 88 |
+
|
| 89 |
+
res = chat_sep.copy()
|
| 90 |
+
|
| 91 |
+
# Format tool responses
|
| 92 |
+
tool_responses = []
|
| 93 |
+
for tool_message in tool_messages:
|
| 94 |
+
tool_content = tool_message['content']
|
| 95 |
+
tool_responses.append(f'<response>{tool_content}</response>')
|
| 96 |
+
|
| 97 |
+
total_tool = '\n'.join(tool_responses)
|
| 98 |
+
|
| 99 |
+
for context in prompt:
|
| 100 |
+
if isinstance(context, str):
|
| 101 |
+
context = context.replace('{{QUERY}}', total_tool)
|
| 102 |
+
res.append(context)
|
| 103 |
+
|
| 104 |
+
return assistant_content, res
|
| 105 |
+
|
| 106 |
+
def _format_tools(self, tools: List[Union[str, dict]], system: Optional[str] = None, user_message=None) -> str:
|
| 107 |
+
"""
|
| 108 |
+
Format tools in MiniMax format with JSONSchema and XML invocation examples.
|
| 109 |
+
"""
|
| 110 |
+
# Parse tools to JSONSchema format
|
| 111 |
+
tool_schemas = []
|
| 112 |
+
for tool in tools:
|
| 113 |
+
tool = self.unwrap_tool(tool)
|
| 114 |
+
tool_schemas.append(json.dumps(tool, ensure_ascii=False))
|
| 115 |
+
|
| 116 |
+
system = system or ''
|
| 117 |
+
|
| 118 |
+
return f"""{system}
|
| 119 |
+
|
| 120 |
+
# Tools
|
| 121 |
+
You may call one or more tools to assist with the user query.
|
| 122 |
+
Here are the tools available in JSONSchema format:
|
| 123 |
+
|
| 124 |
+
<tools>
|
| 125 |
+
""" + '\n'.join(f'<tool>{schema}</tool>' for schema in tool_schemas) + """
|
| 126 |
+
</tools>
|
| 127 |
+
|
| 128 |
+
When making tool calls, use XML format to invoke tools and pass parameters:
|
| 129 |
+
|
| 130 |
+
<minimax:tool_call>
|
| 131 |
+
<invoke name="tool-name-1">
|
| 132 |
+
<parameter name="param-key-1">param-value-1</parameter>
|
| 133 |
+
<parameter name="param-key-2">param-value-2</parameter>
|
| 134 |
+
...
|
| 135 |
+
</invoke>
|
| 136 |
+
</minimax:tool_call>"""
|
| 137 |
+
|
| 138 |
+
def _format_tool_calls(self, tool_call_messages):
|
| 139 |
+
"""
|
| 140 |
+
Format tool call messages into MiniMax XML format.
|
| 141 |
+
|
| 142 |
+
Args:
|
| 143 |
+
tool_call_messages: List of messages containing tool call information.
|
| 144 |
+
|
| 145 |
+
Returns:
|
| 146 |
+
Formatted string with tool calls in MiniMax XML format.
|
| 147 |
+
"""
|
| 148 |
+
tool_calls = []
|
| 149 |
+
|
| 150 |
+
for message in tool_call_messages:
|
| 151 |
+
tool_call = self._parse_tool_call(message['content'])
|
| 152 |
+
name = tool_call['name']
|
| 153 |
+
arguments = tool_call['arguments']
|
| 154 |
+
|
| 155 |
+
# Build parameter list
|
| 156 |
+
params = []
|
| 157 |
+
for key, value in arguments.items():
|
| 158 |
+
# Convert value to JSON string if it's not a string
|
| 159 |
+
if not isinstance(value, str):
|
| 160 |
+
value = json.dumps(value, ensure_ascii=False)
|
| 161 |
+
params.append(f'<parameter name="{key}">{value}</parameter>')
|
| 162 |
+
|
| 163 |
+
# Build invoke block
|
| 164 |
+
invoke_block = f'<invoke name="{name}">\n' + '\n'.join(params) + '\n</invoke>'
|
| 165 |
+
tool_calls.append(invoke_block)
|
| 166 |
+
|
| 167 |
+
# Wrap all invocations in tool_call tags
|
| 168 |
+
if tool_calls:
|
| 169 |
+
return '<minimax:tool_call>\n' + '\n'.join(tool_calls) + '\n</minimax:tool_call>'
|
| 170 |
+
|
| 171 |
+
return ''
|