Image-Text-to-Text
Transformers
Safetensors
qwen3_vl_moe
robotics
embodied-ai
video-understanding
progress-estimation
reward-modeling
qwen3-vl
conversational
Instructions to use InternRobotics/VLAC-Cut with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use InternRobotics/VLAC-Cut with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="InternRobotics/VLAC-Cut") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("InternRobotics/VLAC-Cut") model = AutoModelForMultimodalLM.from_pretrained("InternRobotics/VLAC-Cut", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use InternRobotics/VLAC-Cut with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "InternRobotics/VLAC-Cut" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "InternRobotics/VLAC-Cut", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/InternRobotics/VLAC-Cut
- SGLang
How to use InternRobotics/VLAC-Cut with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "InternRobotics/VLAC-Cut" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "InternRobotics/VLAC-Cut", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "InternRobotics/VLAC-Cut" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "InternRobotics/VLAC-Cut", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use InternRobotics/VLAC-Cut with Docker Model Runner:
docker model run hf.co/InternRobotics/VLAC-Cut
Update model card and repository split
Browse filesKeep model assets on the Hub and move examples/quick-start code to the GitHub repository.
- LICENSE +3 -0
- NOTICE +3 -0
- README.md +87 -25
- examples/example_01/episode.mp4 +0 -3
- examples/example_02/episode.mp4 +0 -3
- examples/example_03/episode.mp4 +0 -3
- examples/reference_outputs/example_01.jsonl +0 -1
- examples/reference_outputs/example_01_pred_progress.mp4 +0 -3
- examples/reference_outputs/example_02.jsonl +0 -1
- examples/reference_outputs/example_02_pred_progress.mp4 +0 -3
- examples/reference_outputs/example_03.jsonl +0 -1
- examples/reference_outputs/example_03_pred_progress.mp4 +0 -3
- quick_start/README.md +0 -93
- quick_start/render_prediction_video.py +0 -243
- quick_start/run_example.py +0 -360
LICENSE
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TODO: replace this file with the final VLAC-Cut model license before public release.
|
| 2 |
+
|
| 3 |
+
Do not publish this model repository as open source until the license text has been finalized.
|
NOTICE
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
VLAC-Cut model release
|
| 2 |
+
|
| 3 |
+
TODO: replace this file with the final model notice, base-model attribution, and third-party notices before public release.
|
README.md
CHANGED
|
@@ -1,26 +1,51 @@
|
|
| 1 |
---
|
| 2 |
library_name: transformers
|
| 3 |
pipeline_tag: image-text-to-text
|
|
|
|
| 4 |
tags:
|
| 5 |
- robotics
|
|
|
|
| 6 |
- video-understanding
|
| 7 |
-
- multimodal
|
| 8 |
- progress-estimation
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
# VLAC-Cut
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
## Highlights
|
| 18 |
|
| 19 |
-
-
|
| 20 |
-
-
|
| 21 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
```python
|
| 26 |
from transformers import AutoModelForImageTextToText, AutoProcessor
|
|
@@ -30,39 +55,76 @@ model_id = "InternRobotics/VLAC-Cut"
|
|
| 30 |
processor = AutoProcessor.from_pretrained(model_id)
|
| 31 |
model = AutoModelForImageTextToText.from_pretrained(
|
| 32 |
model_id,
|
| 33 |
-
|
| 34 |
device_map="auto",
|
| 35 |
)
|
|
|
|
| 36 |
```
|
| 37 |
|
| 38 |
## Quick Start
|
| 39 |
|
| 40 |
-
Run progress inference on
|
| 41 |
|
| 42 |
```bash
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
| 46 |
--task-instruction "<natural-language task instruction>" \
|
| 47 |
--task-plan $'<optional step-by-step task plan>' \
|
| 48 |
-
--output-jsonl
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
```
|
| 50 |
|
| 51 |
-
|
| 52 |
-
- `--task-instruction`: task description, unless `--prompt` is used instead.
|
| 53 |
-
- `--task-plan`: optional step-by-step plan text for the default `chunk_all` prompt.
|
| 54 |
-
- `--model-path`: optional. Defaults to the parent directory of `quick_start/`.
|
| 55 |
-
- `--output-jsonl`: optional. If omitted, the script prints the prediction only and does not save a JSONL file.
|
| 56 |
-
- `--prompt`: optional full prompt override.
|
| 57 |
-
- `--sample-hz`: optional inference control. Default: `2.0`.
|
| 58 |
|
| 59 |
-
|
| 60 |
|
| 61 |
```bash
|
| 62 |
-
python
|
| 63 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
```
|
| 65 |
|
| 66 |
-
|
| 67 |
|
| 68 |
-
|
|
|
|
| 1 |
---
|
| 2 |
library_name: transformers
|
| 3 |
pipeline_tag: image-text-to-text
|
| 4 |
+
base_model: Qwen/Qwen3-VL-30B-A3B-Instruct
|
| 5 |
tags:
|
| 6 |
- robotics
|
| 7 |
+
- embodied-ai
|
| 8 |
- video-understanding
|
|
|
|
| 9 |
- progress-estimation
|
| 10 |
+
- reward-modeling
|
| 11 |
+
- qwen3-vl
|
| 12 |
+
license: other
|
| 13 |
---
|
| 14 |
|
| 15 |
# VLAC-Cut
|
| 16 |
|
| 17 |
+
**Video-based task progress estimation model for robot manipulation.**
|
| 18 |
|
| 19 |
+
[Model](https://huggingface.co/InternRobotics/VLAC-Cut) · [Code](https://github.com/InternRobotics/VLAC-Cut) · [Paper](<PAPER_URL>) · [VPB Benchmark](https://huggingface.co/datasets/InternRobotics/VLAC-Cut-Benchmark)
|
| 20 |
+
|
| 21 |
+
VLAC-Cut predicts how a robot task progresses over time from a natural-language instruction and an execution video. Instead of assuming that progress always increases with time, the model captures non-monotonic execution patterns including advancement, stagnation, regression, and recovery.
|
| 22 |
+
|
| 23 |
+
The model is designed for long-horizon robot manipulation videos containing failed grasps, object drops, incorrect interactions, off-target placements, and corrective behavior.
|
| 24 |
|
| 25 |
## Highlights
|
| 26 |
|
| 27 |
+
- **Video-level progress understanding:** reasons over temporally ordered robot videos rather than isolated images or image pairs.
|
| 28 |
+
- **Non-monotonic process modeling:** recognizes advancement, stagnation, regression, and recovery.
|
| 29 |
+
- **Generative output:** produces timestamped progress predictions through the standard autoregressive language-model interface, without a task-specific regression head.
|
| 30 |
+
|
| 31 |
+
## Model at a Glance
|
| 32 |
+
|
| 33 |
+
| Property | Description |
|
| 34 |
+
|---|---|
|
| 35 |
+
| Base model | `Qwen/Qwen3-VL-30B-A3B-Instruct` |
|
| 36 |
+
| Input | Task instruction, optional task plan, and sampled video frames |
|
| 37 |
+
| Output | Timestamped task progress |
|
| 38 |
+
| Default video sampling rate | `2 Hz` |
|
| 39 |
+
|
| 40 |
+
## Model Design
|
| 41 |
+
|
| 42 |
+
VLAC-Cut is fine-tuned from Qwen3-VL-30B-A3B-Instruct and retains its original multimodal generation interface. The model receives a task instruction, an optional task plan, and sampled video frames, then generates task progress at different timestamps as text.
|
| 43 |
|
| 44 |
+
The training data also include supervision for robot behavior description, failure analysis, and correction planning, helping the model understand realistic robot execution processes.
|
| 45 |
+
|
| 46 |
+
This repository contains the model checkpoint and loading assets only. Inference scripts, examples, and VPB evaluation code are maintained in the GitHub repository.
|
| 47 |
+
|
| 48 |
+
## Load with Transformers
|
| 49 |
|
| 50 |
```python
|
| 51 |
from transformers import AutoModelForImageTextToText, AutoProcessor
|
|
|
|
| 55 |
processor = AutoProcessor.from_pretrained(model_id)
|
| 56 |
model = AutoModelForImageTextToText.from_pretrained(
|
| 57 |
model_id,
|
| 58 |
+
dtype="auto",
|
| 59 |
device_map="auto",
|
| 60 |
)
|
| 61 |
+
model.eval()
|
| 62 |
```
|
| 63 |
|
| 64 |
## Quick Start
|
| 65 |
|
| 66 |
+
Run progress inference on a local video using the GitHub code:
|
| 67 |
|
| 68 |
```bash
|
| 69 |
+
git clone https://github.com/InternRobotics/VLAC-Cut
|
| 70 |
+
cd VLAC-Cut
|
| 71 |
+
|
| 72 |
+
python scripts/run_example.py \
|
| 73 |
+
--model-path InternRobotics/VLAC-Cut \
|
| 74 |
+
--video-path examples/example_01/episode.mp4 \
|
| 75 |
--task-instruction "<natural-language task instruction>" \
|
| 76 |
--task-plan $'<optional step-by-step task plan>' \
|
| 77 |
+
--output-jsonl outputs/example_01.jsonl
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
Main arguments:
|
| 81 |
+
|
| 82 |
+
- `--model-path`: Hugging Face model id or local checkpoint path.
|
| 83 |
+
- `--video-path`: input video path.
|
| 84 |
+
- `--task-instruction`: natural-language task description.
|
| 85 |
+
- `--task-plan`: optional step-by-step task plan.
|
| 86 |
+
- `--sample-hz`: video sampling rate. Default: `2.0`.
|
| 87 |
+
- `--prompt`: optional full-prompt override.
|
| 88 |
+
- `--output-jsonl`: optional output path; if omitted, the model response is printed only.
|
| 89 |
+
|
| 90 |
+
Example response:
|
| 91 |
+
|
| 92 |
+
```text
|
| 93 |
+
时间: 0.0s, 进度: 0%
|
| 94 |
+
时间: 1.0s, 进度: 25%
|
| 95 |
+
时间: 2.0s, 进度: 40%
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Render a prediction preview video:
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
python scripts/utils/render_prediction_video.py \
|
| 102 |
+
--input-jsonl outputs/example_01.jsonl \
|
| 103 |
+
--output-video outputs/example_01_pred_progress.mp4
|
| 104 |
```
|
| 105 |
|
| 106 |
+
## Evaluation on VPB
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
+
VLAC-Cut is evaluated on the Video Progress Benchmark. The benchmark data are released at `InternRobotics/VLAC-Cut-Benchmark`, and the evaluator is provided in the GitHub repository:
|
| 109 |
|
| 110 |
```bash
|
| 111 |
+
python scripts/evaluate_vpb_predictions.py \
|
| 112 |
+
--benchmark-root /path/to/VLAC-Cut-Benchmark/splits \
|
| 113 |
+
--predictions predictions/vlac_cut_predictions.jsonl \
|
| 114 |
+
--out-json reports/vlac_cut_vpb_eval.json \
|
| 115 |
+
--out-md reports/vlac_cut_vpb_eval.md
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
Report final VPB numbers here before public release.
|
| 119 |
+
|
| 120 |
+
## Citation
|
| 121 |
+
|
| 122 |
+
Please replace the placeholder below with the final paper BibTeX before public release.
|
| 123 |
+
|
| 124 |
+
```bibtex
|
| 125 |
+
|
| 126 |
```
|
| 127 |
|
| 128 |
+
## License
|
| 129 |
|
| 130 |
+
See the license files distributed with this checkpoint. The base model and third-party training data remain subject to their original licenses and terms of use.
|
examples/example_01/episode.mp4
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:107c36cd11846dc0d622a2168fb0d2129d912dda5301c6748e0a1417ff5cde28
|
| 3 |
-
size 1477548
|
|
|
|
|
|
|
|
|
|
|
|
examples/example_02/episode.mp4
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:52c1494005556db7242d49982cc0ee2d2be31962e0ce9dd59a590f96b5462ec5
|
| 3 |
-
size 1908478
|
|
|
|
|
|
|
|
|
|
|
|
examples/example_03/episode.mp4
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:9221f9782d815175fd851555d78e3950d4bd42ff362569b5d66da7be8f7df724
|
| 3 |
-
size 1047570
|
|
|
|
|
|
|
|
|
|
|
|
examples/reference_outputs/example_01.jsonl
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"task_instruction": "抓取紫色方块使其从方形洞口落入积木桶中", "task_plan": "爪夹开始移动:0%\n爪夹接近紫色方块:20%\n爪夹抓紧紫色方块:40%\n爪夹抓紧紫色方块接近方形洞口:60%\n爪夹将紫色方块对准方形洞口:80%\n紫色方块从方形洞口落入积木桶中,爪夹移开:100%", "video_path": "examples/example_01/episode.mp4", "prompt_source": "task_instruction_plus_plan", "prompt_variant": "chunk_all", "input_sample_hz": 2.0, "input_frame_indices": [0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255], "input_timestamps_sec": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5], "input_frame_count": 18, "decoded_video_stats": {"decoded_frame_count": 258.0, "decoded_avg_fps": 30.0, "decoded_duration_sec": 8.566667}, "prompt": "任务描述和具体规划: 抓取紫色方块使其从方形洞口落入积木桶中\n爪夹开始移动:0%\n爪夹接近紫色方块:20%\n爪夹抓紧紫色方块:40%\n爪夹抓紧紫色方块接近方形洞口:60%\n爪夹将紫色方块对准方形洞口:80%\n紫色方块从方形洞口落入积木桶中,爪夹移开:100%\n\n请根据任务描述和具体规划,找到并逐点生成视频中的关键动作点和相应的进度标注。输出格式要求:每个关键点一行,格式为:\n时间: X.Xs, 进度: Y%\n\n请严格按照上述格式输出,不要输出额外说明。", "response": "时间: 1.3s, 进度: 0%\n时间: 2.7s, 进度: 10%\n时间: 3.7s, 进度: 15%\n时间: 5.5s, 进度: 10%\n时间: 6.3s, 进度: -15%\n时间: 7.1s, 进度: -25%\n时间: 8.0s, 进度: -15%", "pred_curve_parse_ok": true, "pred_curve_point_times_sec": [1.3, 2.7, 3.7, 5.5, 6.3, 7.1, 8.0], "pred_curve_point_progress": [0.0, 10.0, 15.0, 10.0, -15.0, -25.0, -15.0], "preview_video_path": "examples/reference_outputs/example_01_pred_progress.mp4"}
|
|
|
|
|
|
examples/reference_outputs/example_01_pred_progress.mp4
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:3ea6fb101f9823664cd7531ce5297f9490c40e6de7e5c1bb584b9d6fa5223301
|
| 3 |
-
size 2043716
|
|
|
|
|
|
|
|
|
|
|
|
examples/reference_outputs/example_02.jsonl
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"task_instruction": "把洋葱放进快递箱里。", "task_plan": "开始移动:0%\n爪夹靠近洋葱:20%\n爪夹抓取洋葱:40%\n爪夹抓住洋葱接近快递箱:60%\n爪夹将洋葱放入快递箱:80%\n爪夹松开,洋葱落入快递箱:100%", "video_path": "examples/example_02/episode.mp4", "prompt_source": "task_instruction_plus_plan", "prompt_variant": "chunk_all", "input_sample_hz": 2.0, "input_frame_indices": [0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255, 270, 285], "input_timestamps_sec": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5], "input_frame_count": 20, "decoded_video_stats": {"decoded_frame_count": 289.0, "decoded_avg_fps": 30.0, "decoded_duration_sec": 9.6}, "prompt": "任务描述和具体规划: 把洋葱放进快递箱里。\n开始移动:0%\n爪夹靠近洋葱:20%\n爪夹抓取洋葱:40%\n爪夹抓住洋葱接近快递箱:60%\n爪夹将洋葱放入快递箱:80%\n爪夹松开,洋葱落入快递箱:100%\n\n请根据任务描述和具体规划,找到并逐点生成视频中的关键动作点和相应的进度标注。输出格式要求:每个关键点一行,格式为:\n时间: X.Xs, 进度: Y%\n\n请严格按照上述格式输出,不要输出额外说明。", "response": "时间: 0.0s, 进度: 0%\n时间: 2.3s, 进度: 5%\n时间: 3.3s, 进度: 8%\n时间: 4.3s, 进度: 10%\n时间: 5.1s, 进度: -5%\n时间: 6.1s, 进度: -8%\n时间: 7.1s, 进度: -2%\n时间: 8.3s, 进度: 2%\n时间: 9.5s, 进度: 4%", "pred_curve_parse_ok": true, "pred_curve_point_times_sec": [0.0, 2.3, 3.3, 4.3, 5.1, 6.1, 7.1, 8.3, 9.5], "pred_curve_point_progress": [0.0, 5.0, 8.0, 10.0, -5.0, -8.0, -2.0, 2.0, 4.0], "preview_video_path": "examples/reference_outputs/example_02_pred_progress.mp4"}
|
|
|
|
|
|
examples/reference_outputs/example_02_pred_progress.mp4
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:691fe25a519518ac2c24ba86c71ab7714d41f5616482cc0f079b874f897f679f
|
| 3 |
-
size 2546146
|
|
|
|
|
|
|
|
|
|
|
|
examples/reference_outputs/example_03.jsonl
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"task_instruction": "将三角烧杯放在三脚架上。", "task_plan": "爪夹准备移动:0%\n爪夹开始移动:10%\n爪夹靠近三角烧杯:20%\n爪夹抓取三角烧杯:40%\n爪夹合拢并靠近三脚架:60%\n爪夹移动到三脚架的正上方:80%\n爪夹松开,三角烧杯落在三脚架上:100%", "video_path": "examples/example_03/episode.mp4", "prompt_source": "task_instruction_plus_plan", "prompt_variant": "chunk_all", "input_sample_hz": 2.0, "input_frame_indices": [0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165], "input_timestamps_sec": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5], "input_frame_count": 12, "decoded_video_stats": {"decoded_frame_count": 172.0, "decoded_avg_fps": 30.0, "decoded_duration_sec": 5.7}, "prompt": "任务描述和具体规划: 将三角烧杯放在三脚架上。\n爪夹准备移动:0%\n爪夹开始移动:10%\n爪夹靠近三角烧杯:20%\n爪夹抓取三角烧杯:40%\n爪夹合拢并靠近三脚架:60%\n爪夹移动到三脚架的正上方:80%\n爪夹松开,三角烧杯落在三脚架上:100%\n\n请根据任务描述和具体规划,找到并逐点生成视频中的关键动作点和相应的进度标注。输出格式要求:每个关键点一行,格式为:\n时间: X.Xs, 进度: Y%\n\n请严格按照上述格式输出,不要输出额外说明。", "response": "时间: 0.1s, 进度: 0%\n时间: 0.6s, 进度: 10%\n时间: 1.1s, 进度: 20%\n时间: 1.6s, 进度: 30%\n时间: 2.3s, 进度: 40%\n时间: 3.4s, 进度: 60%\n时间: 4.1s, 进度: 80%\n时间: 5.3s, 进度: 100%", "pred_curve_parse_ok": true, "pred_curve_point_times_sec": [0.1, 0.6, 1.1, 1.6, 2.3, 3.4, 4.1, 5.3], "pred_curve_point_progress": [0.0, 10.0, 20.0, 30.0, 40.0, 60.0, 80.0, 100.0], "preview_video_path": "examples/reference_outputs/example_03_pred_progress.mp4"}
|
|
|
|
|
|
examples/reference_outputs/example_03_pred_progress.mp4
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:211dfef38fd8e374f20b4bcf83a607fd6e3461fafb53a8228386995bbab118fe
|
| 3 |
-
size 1436259
|
|
|
|
|
|
|
|
|
|
|
|
quick_start/README.md
DELETED
|
@@ -1,93 +0,0 @@
|
|
| 1 |
-
# Quick Start
|
| 2 |
-
|
| 3 |
-
This quick start has two steps:
|
| 4 |
-
|
| 5 |
-
1. Run progress inference on a video. Optionally save one JSONL output.
|
| 6 |
-
2. Render a prediction-only preview video from a saved JSONL output.
|
| 7 |
-
|
| 8 |
-
## Requirements
|
| 9 |
-
|
| 10 |
-
- `python >= 3.10`
|
| 11 |
-
- `torch`
|
| 12 |
-
- `transformers >= 4.57`
|
| 13 |
-
- `ms-swift`
|
| 14 |
-
- `decord`
|
| 15 |
-
- `numpy`
|
| 16 |
-
- `Pillow`
|
| 17 |
-
- `opencv-python` for preview video rendering
|
| 18 |
-
- at least `1` GPU with enough memory for this `30B` checkpoint
|
| 19 |
-
|
| 20 |
-
## Usage
|
| 21 |
-
|
| 22 |
-
Run inference:
|
| 23 |
-
|
| 24 |
-
```bash
|
| 25 |
-
python quick_start/run_example.py \
|
| 26 |
-
--model-path /path/to/VLAC-Cut \
|
| 27 |
-
--video-path /path/to/video.mp4 \
|
| 28 |
-
--task-instruction "<natural-language task instruction>" \
|
| 29 |
-
--task-plan $'<optional step-by-step task plan>' \
|
| 30 |
-
--output-jsonl quick_start/outputs/video.jsonl
|
| 31 |
-
```
|
| 32 |
-
|
| 33 |
-
Important arguments:
|
| 34 |
-
|
| 35 |
-
- `--video-path`: required. Input video path.
|
| 36 |
-
- `--task-instruction`: optional if `--prompt` is provided. Natural-language task description.
|
| 37 |
-
- `--task-plan`: optional. Multi-line plan text appended after `--task-instruction` when building the default `chunk_all` prompt.
|
| 38 |
-
- `--model-path`: optional. Defaults to the parent directory of `quick_start/`.
|
| 39 |
-
- `--output-jsonl`: optional. If omitted, the script prints the prediction only and does not save a JSONL file.
|
| 40 |
-
- `--sample-hz`: optional. Video sampling rate for inference. Default: `2.0`.
|
| 41 |
-
- `--prompt`: optional full prompt override. If set, `--task-instruction` and `--task-plan` are ignored.
|
| 42 |
-
|
| 43 |
-
Render a preview video:
|
| 44 |
-
|
| 45 |
-
```bash
|
| 46 |
-
python quick_start/render_prediction_video.py \
|
| 47 |
-
--input-jsonl quick_start/outputs/video.jsonl \
|
| 48 |
-
--output-video quick_start/outputs/video_pred_progress.mp4
|
| 49 |
-
```
|
| 50 |
-
|
| 51 |
-
## Example Commands
|
| 52 |
-
|
| 53 |
-
`example_01`
|
| 54 |
-
|
| 55 |
-
```bash
|
| 56 |
-
python quick_start/run_example.py \
|
| 57 |
-
--video-path examples/example_01/episode.mp4 \
|
| 58 |
-
--task-instruction "抓取紫色方块使其从方形洞口落入积木桶中" \
|
| 59 |
-
--task-plan $'爪夹开始移动:0%\n爪夹接近紫色方块:20%\n爪夹抓紧紫色方块:40%\n爪夹抓紧紫色方块接近方形洞口:60%\n爪夹将紫色方块对准方形洞口:80%\n紫色方块从方形洞口落入积木桶中,爪夹移开:100%' \
|
| 60 |
-
--output-jsonl quick_start/outputs/example_01.jsonl
|
| 61 |
-
|
| 62 |
-
python quick_start/render_prediction_video.py \
|
| 63 |
-
--input-jsonl quick_start/outputs/example_01.jsonl \
|
| 64 |
-
--output-video quick_start/outputs/example_01_pred_progress.mp4
|
| 65 |
-
```
|
| 66 |
-
|
| 67 |
-
`example_02`
|
| 68 |
-
|
| 69 |
-
```bash
|
| 70 |
-
python quick_start/run_example.py \
|
| 71 |
-
--video-path examples/example_02/episode.mp4 \
|
| 72 |
-
--task-instruction "把洋葱放进快递箱里。" \
|
| 73 |
-
--task-plan $'开始移动:0%\n爪夹靠近洋葱:20%\n爪夹抓取洋葱:40%\n爪夹抓住洋葱接近快递箱:60%\n爪夹将洋葱放入快递箱:80%\n爪夹松开,洋葱落入快递箱:100%' \
|
| 74 |
-
--output-jsonl quick_start/outputs/example_02.jsonl
|
| 75 |
-
|
| 76 |
-
python quick_start/render_prediction_video.py \
|
| 77 |
-
--input-jsonl quick_start/outputs/example_02.jsonl \
|
| 78 |
-
--output-video quick_start/outputs/example_02_pred_progress.mp4
|
| 79 |
-
```
|
| 80 |
-
|
| 81 |
-
`example_03`
|
| 82 |
-
|
| 83 |
-
```bash
|
| 84 |
-
python quick_start/run_example.py \
|
| 85 |
-
--video-path examples/example_03/episode.mp4 \
|
| 86 |
-
--task-instruction "将三角烧杯放在三脚架上。" \
|
| 87 |
-
--task-plan $'爪夹准备移动:0%\n爪夹开始移动:10%\n爪夹靠近三角烧杯:20%\n爪夹抓取三角烧杯:40%\n爪夹合拢并靠近三脚架:60%\n爪夹移动到三脚架的正上方:80%\n爪夹松开,三角烧杯落在三脚架上:100%' \
|
| 88 |
-
--output-jsonl quick_start/outputs/example_03.jsonl
|
| 89 |
-
|
| 90 |
-
python quick_start/render_prediction_video.py \
|
| 91 |
-
--input-jsonl quick_start/outputs/example_03.jsonl \
|
| 92 |
-
--output-video quick_start/outputs/example_03_pred_progress.mp4
|
| 93 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
quick_start/render_prediction_video.py
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import argparse
|
| 5 |
-
import json
|
| 6 |
-
import math
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
|
| 9 |
-
import cv2
|
| 10 |
-
import numpy as np
|
| 11 |
-
|
| 12 |
-
THIS_DIR = Path(__file__).resolve().parent
|
| 13 |
-
MODEL_ROOT = THIS_DIR.parent
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def parse_args() -> argparse.Namespace:
|
| 17 |
-
parser = argparse.ArgumentParser(
|
| 18 |
-
description="Render a predicted-progress preview video from a quick_start JSONL output.",
|
| 19 |
-
)
|
| 20 |
-
parser.add_argument(
|
| 21 |
-
"--input-jsonl",
|
| 22 |
-
type=Path,
|
| 23 |
-
required=True,
|
| 24 |
-
help="Path to the quick_start JSONL output.",
|
| 25 |
-
)
|
| 26 |
-
parser.add_argument(
|
| 27 |
-
"--video-path",
|
| 28 |
-
type=Path,
|
| 29 |
-
default=None,
|
| 30 |
-
help="Optional input video override. Defaults to the video_path stored in the JSONL row.",
|
| 31 |
-
)
|
| 32 |
-
parser.add_argument(
|
| 33 |
-
"--output-video",
|
| 34 |
-
type=Path,
|
| 35 |
-
default=None,
|
| 36 |
-
help="Optional output path. Defaults to <input_jsonl_stem>_pred_progress.mp4",
|
| 37 |
-
)
|
| 38 |
-
parser.add_argument(
|
| 39 |
-
"--panel-height",
|
| 40 |
-
type=int,
|
| 41 |
-
default=260,
|
| 42 |
-
help="Height in pixels for the lower progress panel.",
|
| 43 |
-
)
|
| 44 |
-
return parser.parse_args()
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def load_row(path: Path) -> dict:
|
| 48 |
-
lines = path.read_text(encoding="utf-8").splitlines()
|
| 49 |
-
if not lines:
|
| 50 |
-
raise SystemExit(f"Empty JSONL file: {path}")
|
| 51 |
-
return json.loads(lines[0])
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def resolve_video_path(path_arg: Path | None, row: dict) -> Path:
|
| 55 |
-
if path_arg is not None:
|
| 56 |
-
path = path_arg.resolve()
|
| 57 |
-
if not path.exists():
|
| 58 |
-
raise SystemExit(f"Missing video: {path}")
|
| 59 |
-
return path
|
| 60 |
-
|
| 61 |
-
row_video_path = row.get("video_path")
|
| 62 |
-
if not row_video_path:
|
| 63 |
-
raise SystemExit("Missing video_path in JSONL row. Pass --video-path explicitly.")
|
| 64 |
-
candidate = Path(str(row_video_path))
|
| 65 |
-
if candidate.is_absolute():
|
| 66 |
-
path = candidate
|
| 67 |
-
else:
|
| 68 |
-
path = (MODEL_ROOT / candidate).resolve()
|
| 69 |
-
if not path.exists():
|
| 70 |
-
raise SystemExit(f"Missing video resolved from JSONL row: {path}")
|
| 71 |
-
return path
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def render_preview(
|
| 75 |
-
*,
|
| 76 |
-
row: dict,
|
| 77 |
-
video_path: Path,
|
| 78 |
-
output_path: Path,
|
| 79 |
-
panel_height: int,
|
| 80 |
-
) -> None:
|
| 81 |
-
pred_times = [float(x) for x in row.get("pred_curve_point_times_sec", [])]
|
| 82 |
-
pred_vals = [float(x) for x in row.get("pred_curve_point_progress", [])]
|
| 83 |
-
if not pred_times or not pred_vals:
|
| 84 |
-
raise SystemExit("No predicted progress points found in JSONL row.")
|
| 85 |
-
|
| 86 |
-
cap = cv2.VideoCapture(str(video_path))
|
| 87 |
-
if not cap.isOpened():
|
| 88 |
-
raise SystemExit(f"Failed to open video: {video_path}")
|
| 89 |
-
|
| 90 |
-
fps = float(cap.get(cv2.CAP_PROP_FPS))
|
| 91 |
-
frame_count = int(round(cap.get(cv2.CAP_PROP_FRAME_COUNT)))
|
| 92 |
-
frame_w = int(round(cap.get(cv2.CAP_PROP_FRAME_WIDTH)))
|
| 93 |
-
frame_h = int(round(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
|
| 94 |
-
if fps <= 0 or frame_count <= 0 or frame_w <= 0 or frame_h <= 0:
|
| 95 |
-
cap.release()
|
| 96 |
-
raise SystemExit(
|
| 97 |
-
f"Invalid decoded video stats from {video_path}: fps={fps}, frames={frame_count}, "
|
| 98 |
-
f"width={frame_w}, height={frame_h}"
|
| 99 |
-
)
|
| 100 |
-
|
| 101 |
-
total_h = frame_h + panel_height
|
| 102 |
-
writer = cv2.VideoWriter(str(output_path), cv2.VideoWriter_fourcc(*"mp4v"), fps, (frame_w, total_h))
|
| 103 |
-
if not writer.isOpened():
|
| 104 |
-
cap.release()
|
| 105 |
-
raise SystemExit(f"Failed to open output video writer: {output_path}")
|
| 106 |
-
|
| 107 |
-
duration = max(0.0, (frame_count - 1) / fps)
|
| 108 |
-
dense_t = np.arange(frame_count, dtype=np.float32) / fps
|
| 109 |
-
dense_v = np.interp(
|
| 110 |
-
dense_t,
|
| 111 |
-
np.array(pred_times, dtype=np.float32),
|
| 112 |
-
np.array(pred_vals, dtype=np.float32),
|
| 113 |
-
left=float(pred_vals[0]),
|
| 114 |
-
right=float(pred_vals[-1]),
|
| 115 |
-
)
|
| 116 |
-
|
| 117 |
-
y_min = min(-40.0, math.floor(min(pred_vals) / 10.0) * 10.0 - 10.0)
|
| 118 |
-
y_max = max(100.0, math.ceil(max(pred_vals) / 10.0) * 10.0 + 10.0)
|
| 119 |
-
left, right, top, bottom = 78, 24, 28, 52
|
| 120 |
-
chart_x0, chart_x1 = left, frame_w - right
|
| 121 |
-
chart_y0, chart_y1 = frame_h + top, total_h - bottom
|
| 122 |
-
tick_vals = np.linspace(y_min, y_max, num=6)
|
| 123 |
-
title = str(row.get("example_id") or video_path.stem)
|
| 124 |
-
|
| 125 |
-
def map_x(t: float) -> int:
|
| 126 |
-
if duration <= 1e-9:
|
| 127 |
-
return chart_x0
|
| 128 |
-
return int(round(chart_x0 + (chart_x1 - chart_x0) * (t / duration)))
|
| 129 |
-
|
| 130 |
-
def map_y(v: float) -> int:
|
| 131 |
-
ratio = (v - y_min) / max(1e-9, (y_max - y_min))
|
| 132 |
-
return int(round(chart_y1 - ratio * (chart_y1 - chart_y0)))
|
| 133 |
-
|
| 134 |
-
try:
|
| 135 |
-
idx = 0
|
| 136 |
-
while True:
|
| 137 |
-
ok, frame = cap.read()
|
| 138 |
-
if not ok:
|
| 139 |
-
break
|
| 140 |
-
if idx >= frame_count:
|
| 141 |
-
break
|
| 142 |
-
|
| 143 |
-
canvas = np.full((total_h, frame_w, 3), 255, dtype=np.uint8)
|
| 144 |
-
canvas[:frame_h, :, :] = frame
|
| 145 |
-
|
| 146 |
-
cv2.putText(
|
| 147 |
-
canvas,
|
| 148 |
-
f"{title} predicted progress so far",
|
| 149 |
-
(18, frame_h + 22),
|
| 150 |
-
cv2.FONT_HERSHEY_SIMPLEX,
|
| 151 |
-
0.65,
|
| 152 |
-
(20, 20, 20),
|
| 153 |
-
2,
|
| 154 |
-
cv2.LINE_AA,
|
| 155 |
-
)
|
| 156 |
-
cv2.rectangle(canvas, (chart_x0, chart_y0), (chart_x1, chart_y1), (210, 210, 210), 1)
|
| 157 |
-
|
| 158 |
-
for tick in tick_vals:
|
| 159 |
-
y = map_y(float(tick))
|
| 160 |
-
cv2.line(canvas, (chart_x0, y), (chart_x1, y), (238, 238, 238), 1)
|
| 161 |
-
cv2.putText(
|
| 162 |
-
canvas,
|
| 163 |
-
f"{int(round(tick))}",
|
| 164 |
-
(12, y + 5),
|
| 165 |
-
cv2.FONT_HERSHEY_SIMPLEX,
|
| 166 |
-
0.44,
|
| 167 |
-
(90, 90, 90),
|
| 168 |
-
1,
|
| 169 |
-
cv2.LINE_AA,
|
| 170 |
-
)
|
| 171 |
-
|
| 172 |
-
for frac in np.linspace(0.0, 1.0, num=5):
|
| 173 |
-
t_mark = duration * frac
|
| 174 |
-
x = map_x(float(t_mark))
|
| 175 |
-
cv2.line(canvas, (x, chart_y0), (x, chart_y1), (245, 245, 245), 1)
|
| 176 |
-
cv2.putText(
|
| 177 |
-
canvas,
|
| 178 |
-
f"{t_mark:.1f}s",
|
| 179 |
-
(x - 16, total_h - 16),
|
| 180 |
-
cv2.FONT_HERSHEY_SIMPLEX,
|
| 181 |
-
0.42,
|
| 182 |
-
(100, 100, 100),
|
| 183 |
-
1,
|
| 184 |
-
cv2.LINE_AA,
|
| 185 |
-
)
|
| 186 |
-
|
| 187 |
-
current_t = float(dense_t[idx])
|
| 188 |
-
current_v = float(dense_v[idx])
|
| 189 |
-
pts = [[map_x(float(dense_t[j])), map_y(float(dense_v[j]))] for j in range(idx + 1)]
|
| 190 |
-
if len(pts) >= 2:
|
| 191 |
-
cv2.polylines(canvas, [np.array(pts, dtype=np.int32)], False, (0, 140, 255), 3, cv2.LINE_AA)
|
| 192 |
-
elif len(pts) == 1:
|
| 193 |
-
cv2.circle(canvas, tuple(pts[0]), 3, (0, 140, 255), -1, cv2.LINE_AA)
|
| 194 |
-
|
| 195 |
-
for t, v in zip(pred_times, pred_vals):
|
| 196 |
-
if t <= current_t + 1e-6:
|
| 197 |
-
cv2.circle(canvas, (map_x(t), map_y(v)), 5, (20, 20, 20), -1, cv2.LINE_AA)
|
| 198 |
-
|
| 199 |
-
cx, cy = map_x(current_t), map_y(current_v)
|
| 200 |
-
cv2.line(canvas, (cx, chart_y0), (cx, chart_y1), (120, 120, 120), 1)
|
| 201 |
-
cv2.circle(canvas, (cx, cy), 6, (0, 90, 220), -1, cv2.LINE_AA)
|
| 202 |
-
cv2.putText(
|
| 203 |
-
canvas,
|
| 204 |
-
f"time={current_t:.2f}s pred={current_v:.1f}%",
|
| 205 |
-
(frame_w - 340, frame_h + 22),
|
| 206 |
-
cv2.FONT_HERSHEY_SIMPLEX,
|
| 207 |
-
0.55,
|
| 208 |
-
(30, 30, 30),
|
| 209 |
-
2,
|
| 210 |
-
cv2.LINE_AA,
|
| 211 |
-
)
|
| 212 |
-
|
| 213 |
-
writer.write(canvas)
|
| 214 |
-
idx += 1
|
| 215 |
-
finally:
|
| 216 |
-
cap.release()
|
| 217 |
-
writer.release()
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
def main() -> int:
|
| 221 |
-
args = parse_args()
|
| 222 |
-
input_jsonl = args.input_jsonl.resolve()
|
| 223 |
-
if not input_jsonl.exists():
|
| 224 |
-
raise SystemExit(f"Missing JSONL: {input_jsonl}")
|
| 225 |
-
row = load_row(input_jsonl)
|
| 226 |
-
video_path = resolve_video_path(args.video_path, row)
|
| 227 |
-
output_path = args.output_video or input_jsonl.with_name(f"{input_jsonl.stem}_pred_progress.mp4")
|
| 228 |
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 229 |
-
|
| 230 |
-
render_preview(
|
| 231 |
-
row=row,
|
| 232 |
-
video_path=video_path,
|
| 233 |
-
output_path=output_path,
|
| 234 |
-
panel_height=args.panel_height,
|
| 235 |
-
)
|
| 236 |
-
print(f"input_jsonl={input_jsonl}")
|
| 237 |
-
print(f"video_path={video_path}")
|
| 238 |
-
print(f"output_video={output_path.resolve()}")
|
| 239 |
-
return 0
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
if __name__ == "__main__":
|
| 243 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
quick_start/run_example.py
DELETED
|
@@ -1,360 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import argparse
|
| 5 |
-
import json
|
| 6 |
-
import os
|
| 7 |
-
import re
|
| 8 |
-
import tempfile
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
|
| 11 |
-
import numpy as np
|
| 12 |
-
from PIL import Image
|
| 13 |
-
|
| 14 |
-
THIS_DIR = Path(__file__).resolve().parent
|
| 15 |
-
MODEL_ROOT = THIS_DIR.parent
|
| 16 |
-
DEFAULT_SAMPLE_HZ = 2.0
|
| 17 |
-
|
| 18 |
-
SIGNED_NUM = r"([+-]?[0-9]+(?:\.[0-9]+)?)"
|
| 19 |
-
POINT_TIME_RE = re.compile(r"(?:Time|时间)[::]?\s*([0-9]+(?:\.[0-9]+)?)\s*s?", re.IGNORECASE)
|
| 20 |
-
POINT_PROGRESS_LINE_RE = re.compile(rf"(?im)^\s*(?:Progress|进度)[::]?\s*{SIGNED_NUM}\s*%")
|
| 21 |
-
INLINE_POINT_RE = re.compile(
|
| 22 |
-
rf"(?:Time|时间)[::]?\s*([0-9]+(?:\.[0-9]+)?)\s*s?\s*[,,]?\s*(?:Progress|进度)[::]?\s*{SIGNED_NUM}\s*%",
|
| 23 |
-
re.IGNORECASE,
|
| 24 |
-
)
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def parse_args() -> argparse.Namespace:
|
| 28 |
-
parser = argparse.ArgumentParser(
|
| 29 |
-
description="Run VLAC progress inference on an arbitrary input video.",
|
| 30 |
-
)
|
| 31 |
-
parser.add_argument(
|
| 32 |
-
"--model-path",
|
| 33 |
-
type=Path,
|
| 34 |
-
default=MODEL_ROOT,
|
| 35 |
-
help="Path to the released VLAC model directory.",
|
| 36 |
-
)
|
| 37 |
-
parser.add_argument(
|
| 38 |
-
"--video-path",
|
| 39 |
-
type=Path,
|
| 40 |
-
required=True,
|
| 41 |
-
help="Path to the input video.",
|
| 42 |
-
)
|
| 43 |
-
parser.add_argument(
|
| 44 |
-
"--task-instruction",
|
| 45 |
-
type=str,
|
| 46 |
-
default=None,
|
| 47 |
-
help="Task instruction used to build the default chunk_all prompt.",
|
| 48 |
-
)
|
| 49 |
-
parser.add_argument(
|
| 50 |
-
"--task-plan",
|
| 51 |
-
type=str,
|
| 52 |
-
default=None,
|
| 53 |
-
help="Optional task plan text appended after the task instruction.",
|
| 54 |
-
)
|
| 55 |
-
parser.add_argument(
|
| 56 |
-
"--prompt",
|
| 57 |
-
type=str,
|
| 58 |
-
default=None,
|
| 59 |
-
help="Optional full prompt override. If set, task instruction and plan are ignored.",
|
| 60 |
-
)
|
| 61 |
-
parser.add_argument(
|
| 62 |
-
"--sample-hz",
|
| 63 |
-
type=float,
|
| 64 |
-
default=DEFAULT_SAMPLE_HZ,
|
| 65 |
-
help="Video sampling rate in Hz. Defaults to 2.0.",
|
| 66 |
-
)
|
| 67 |
-
parser.add_argument(
|
| 68 |
-
"--output-jsonl",
|
| 69 |
-
type=Path,
|
| 70 |
-
default=None,
|
| 71 |
-
help="Optional output path. If omitted, print the response only and do not save a JSONL file.",
|
| 72 |
-
)
|
| 73 |
-
parser.add_argument(
|
| 74 |
-
"--max-new-tokens",
|
| 75 |
-
type=int,
|
| 76 |
-
default=1024,
|
| 77 |
-
help="Generation cap for the response.",
|
| 78 |
-
)
|
| 79 |
-
return parser.parse_args()
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def build_chunk_all_prompt(task_instruction: str, task_plan: str | None) -> str:
|
| 83 |
-
task_instruction = task_instruction.strip()
|
| 84 |
-
task_plan = (task_plan or "").strip()
|
| 85 |
-
task_and_plan = task_instruction if not task_plan else f"{task_instruction}\n{task_plan}"
|
| 86 |
-
return (
|
| 87 |
-
f"任务描述和具体规划: {task_and_plan}\n\n"
|
| 88 |
-
"请根据任务描述和具体规划,找到并逐点生成视频中的关键动作点和相应的进度标注。"
|
| 89 |
-
"输出格式要求:每个关键点一行,格式为:\n"
|
| 90 |
-
"时间: X.Xs, 进度: Y%\n\n"
|
| 91 |
-
"请严格按照上述格式输出,不要输出额外说明。"
|
| 92 |
-
)
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def resolve_prompt(args: argparse.Namespace) -> tuple[str, str, str | None, str | None]:
|
| 96 |
-
prompt = args.prompt.strip() if args.prompt else None
|
| 97 |
-
task_plan = args.task_plan.strip() if args.task_plan else None
|
| 98 |
-
task_instruction = args.task_instruction.strip() if args.task_instruction else None
|
| 99 |
-
|
| 100 |
-
if prompt:
|
| 101 |
-
return prompt, "raw_prompt", task_instruction, task_plan
|
| 102 |
-
|
| 103 |
-
if not task_instruction:
|
| 104 |
-
raise SystemExit(
|
| 105 |
-
"Either provide --prompt, or provide --task-instruction with optional --task-plan."
|
| 106 |
-
)
|
| 107 |
-
|
| 108 |
-
return build_chunk_all_prompt(task_instruction, task_plan), "task_instruction_plus_plan", task_instruction, task_plan
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def resolve_video_path(video_path_arg: Path) -> Path:
|
| 112 |
-
video_path = video_path_arg.resolve()
|
| 113 |
-
if not video_path.exists():
|
| 114 |
-
raise SystemExit(f"Missing video: {video_path}")
|
| 115 |
-
return video_path
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
def maybe_relativize_to_model_root(path: Path) -> str:
|
| 119 |
-
try:
|
| 120 |
-
return str(path.resolve().relative_to(MODEL_ROOT.resolve()))
|
| 121 |
-
except ValueError:
|
| 122 |
-
return str(path.resolve())
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def compute_sampled_indices(num_frames: int, fps: float, sample_hz: float) -> tuple[list[int], list[float]]:
|
| 126 |
-
if num_frames <= 0:
|
| 127 |
-
raise SystemExit(f"Invalid decoded frame count: {num_frames}")
|
| 128 |
-
if fps <= 0:
|
| 129 |
-
raise SystemExit(f"Invalid decoded fps: {fps}")
|
| 130 |
-
if sample_hz <= 0:
|
| 131 |
-
raise SystemExit(f"sample_hz must be positive, got {sample_hz}")
|
| 132 |
-
|
| 133 |
-
frame_ids = list(range(num_frames))
|
| 134 |
-
eligible_arr = np.array(frame_ids, dtype=float)
|
| 135 |
-
duration_sec = max(0.0, (num_frames - 1) / fps)
|
| 136 |
-
step_sec = 1.0 / sample_hz
|
| 137 |
-
|
| 138 |
-
target_times: list[float] = []
|
| 139 |
-
current = 0.0
|
| 140 |
-
eps = 1e-9
|
| 141 |
-
while current <= duration_sec + eps:
|
| 142 |
-
target_times.append(round(current, 6))
|
| 143 |
-
current += step_sec
|
| 144 |
-
if not target_times:
|
| 145 |
-
target_times = [0.0]
|
| 146 |
-
|
| 147 |
-
sampled_indices: list[int] = []
|
| 148 |
-
sampled_timestamps_sec: list[float] = []
|
| 149 |
-
seen = set()
|
| 150 |
-
for target_time in target_times:
|
| 151 |
-
target_frame = target_time * fps
|
| 152 |
-
pos = int(np.argmin(np.abs(eligible_arr - target_frame)))
|
| 153 |
-
idx = int(eligible_arr[pos])
|
| 154 |
-
if idx in seen:
|
| 155 |
-
continue
|
| 156 |
-
seen.add(idx)
|
| 157 |
-
sampled_indices.append(idx)
|
| 158 |
-
sampled_timestamps_sec.append(round(idx / fps, 6))
|
| 159 |
-
return sampled_indices, sampled_timestamps_sec
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def extract_sampled_frame_paths(
|
| 163 |
-
video_path: Path,
|
| 164 |
-
sample_hz: float,
|
| 165 |
-
*,
|
| 166 |
-
temp_dir: Path,
|
| 167 |
-
) -> tuple[list[str], dict[str, float], list[int], list[float]]:
|
| 168 |
-
try:
|
| 169 |
-
from decord import VideoReader, cpu
|
| 170 |
-
except ImportError as exc:
|
| 171 |
-
raise SystemExit("Missing dependency: decord is required for video sampling in quick_start.") from exc
|
| 172 |
-
|
| 173 |
-
vr = VideoReader(str(video_path), ctx=cpu(0), num_threads=1)
|
| 174 |
-
actual_frame_count = len(vr)
|
| 175 |
-
actual_fps = float(vr.get_avg_fps())
|
| 176 |
-
sampled_indices, sampled_timestamps_sec = compute_sampled_indices(actual_frame_count, actual_fps, sample_hz)
|
| 177 |
-
if not sampled_indices:
|
| 178 |
-
raise SystemExit("No sampled frame indices were generated.")
|
| 179 |
-
if sampled_indices[-1] >= actual_frame_count:
|
| 180 |
-
raise SystemExit(
|
| 181 |
-
f"Decoded video is shorter than expected. Need frame index {sampled_indices[-1]}, "
|
| 182 |
-
f"got {actual_frame_count} frames."
|
| 183 |
-
)
|
| 184 |
-
|
| 185 |
-
batch = vr.get_batch(sampled_indices).asnumpy()
|
| 186 |
-
frame_paths: list[str] = []
|
| 187 |
-
for idx, frame in zip(sampled_indices, batch, strict=True):
|
| 188 |
-
frame_path = temp_dir / f"frame_{idx:06d}.jpg"
|
| 189 |
-
Image.fromarray(frame).save(frame_path, quality=95)
|
| 190 |
-
frame_paths.append(str(frame_path.resolve()))
|
| 191 |
-
|
| 192 |
-
video_stats = {
|
| 193 |
-
"decoded_frame_count": float(actual_frame_count),
|
| 194 |
-
"decoded_avg_fps": actual_fps,
|
| 195 |
-
"decoded_duration_sec": round(max(0.0, (actual_frame_count - 1) / actual_fps), 6),
|
| 196 |
-
}
|
| 197 |
-
return frame_paths, video_stats, sampled_indices, sampled_timestamps_sec
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
def load_swift_runtime():
|
| 201 |
-
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 202 |
-
os.environ.setdefault("IMAGE_MAX_TOKEN_NUM", "256")
|
| 203 |
-
os.environ.setdefault("VIDEO_MAX_TOKEN_NUM", "256")
|
| 204 |
-
os.environ.setdefault("VIDEO_MIN_TOKEN_NUM", "4")
|
| 205 |
-
os.environ.setdefault("QWEN_VL_UTILS_MAX_FRAME_LIST", "0")
|
| 206 |
-
|
| 207 |
-
try:
|
| 208 |
-
import torch # noqa: F401
|
| 209 |
-
except ImportError as exc:
|
| 210 |
-
raise SystemExit("Missing dependency: torch is required for quick_start inference.") from exc
|
| 211 |
-
|
| 212 |
-
try:
|
| 213 |
-
from swift.llm import InferRequest, PtEngine, RequestConfig
|
| 214 |
-
except ImportError:
|
| 215 |
-
from swift.infer_engine import InferRequest, RequestConfig, TransformersEngine as PtEngine # type: ignore
|
| 216 |
-
return InferRequest, PtEngine, RequestConfig
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
def strip_code_fence(text: str) -> str:
|
| 220 |
-
cleaned = str(text or "").strip()
|
| 221 |
-
if cleaned.startswith("```") and cleaned.endswith("```"):
|
| 222 |
-
lines = cleaned.splitlines()
|
| 223 |
-
if len(lines) >= 3:
|
| 224 |
-
return "\n".join(lines[1:-1]).strip()
|
| 225 |
-
return cleaned
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
def dedupe_sorted_points(times: list[float], values: list[float]) -> tuple[list[float], list[float]]:
|
| 229 |
-
if not times:
|
| 230 |
-
return [], []
|
| 231 |
-
pairs = sorted(zip(times, values), key=lambda item: (item[0], item[1]))
|
| 232 |
-
out_times: list[float] = []
|
| 233 |
-
out_values: list[float] = []
|
| 234 |
-
cur_time = pairs[0][0]
|
| 235 |
-
bucket: list[float] = []
|
| 236 |
-
for time_val, progress_val in pairs:
|
| 237 |
-
if abs(time_val - cur_time) > 1e-9:
|
| 238 |
-
out_times.append(float(cur_time))
|
| 239 |
-
out_values.append(float(sum(bucket) / len(bucket)))
|
| 240 |
-
cur_time = time_val
|
| 241 |
-
bucket = [float(progress_val)]
|
| 242 |
-
else:
|
| 243 |
-
bucket.append(float(progress_val))
|
| 244 |
-
out_times.append(float(cur_time))
|
| 245 |
-
out_values.append(float(sum(bucket) / len(bucket)))
|
| 246 |
-
return out_times, out_values
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
def parse_point_blocks(text: str) -> tuple[list[float], list[float]]:
|
| 250 |
-
cleaned = strip_code_fence(text)
|
| 251 |
-
if not cleaned:
|
| 252 |
-
return [], []
|
| 253 |
-
|
| 254 |
-
inline_matches = INLINE_POINT_RE.findall(cleaned)
|
| 255 |
-
if inline_matches:
|
| 256 |
-
return dedupe_sorted_points(
|
| 257 |
-
[float(time_val) for time_val, _ in inline_matches],
|
| 258 |
-
[float(progress_val) for _, progress_val in inline_matches],
|
| 259 |
-
)
|
| 260 |
-
|
| 261 |
-
blocks = re.split(r"(?=(?:Time|时间)[::]?\s*[0-9])", cleaned, flags=re.IGNORECASE)
|
| 262 |
-
times: list[float] = []
|
| 263 |
-
values: list[float] = []
|
| 264 |
-
for block in blocks:
|
| 265 |
-
block = block.strip()
|
| 266 |
-
if not block:
|
| 267 |
-
continue
|
| 268 |
-
time_match = POINT_TIME_RE.search(block)
|
| 269 |
-
progress_match = POINT_PROGRESS_LINE_RE.search(block)
|
| 270 |
-
if time_match and progress_match:
|
| 271 |
-
times.append(float(time_match.group(1)))
|
| 272 |
-
values.append(float(progress_match.group(1)))
|
| 273 |
-
return dedupe_sorted_points(times, values)
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
def main() -> int:
|
| 277 |
-
args = parse_args()
|
| 278 |
-
model_path = args.model_path.resolve()
|
| 279 |
-
if not model_path.exists():
|
| 280 |
-
raise SystemExit(f"Missing model path: {model_path}")
|
| 281 |
-
|
| 282 |
-
prompt, prompt_source, task_instruction, task_plan = resolve_prompt(args)
|
| 283 |
-
video_path = resolve_video_path(args.video_path)
|
| 284 |
-
video_path_for_output = maybe_relativize_to_model_root(video_path)
|
| 285 |
-
output_path = args.output_jsonl.resolve() if args.output_jsonl is not None else None
|
| 286 |
-
if output_path is not None:
|
| 287 |
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 288 |
-
|
| 289 |
-
InferRequest, PtEngine, RequestConfig = load_swift_runtime()
|
| 290 |
-
engine = PtEngine(
|
| 291 |
-
str(model_path),
|
| 292 |
-
model_type="qwen3_moe_vl",
|
| 293 |
-
max_batch_size=1,
|
| 294 |
-
device_map="auto",
|
| 295 |
-
)
|
| 296 |
-
|
| 297 |
-
with tempfile.TemporaryDirectory(prefix="vlac_quick_start_frames_") as temp_dir_str:
|
| 298 |
-
temp_dir = Path(temp_dir_str)
|
| 299 |
-
frame_paths, video_stats, sampled_indices, sampled_timestamps_sec = extract_sampled_frame_paths(
|
| 300 |
-
video_path,
|
| 301 |
-
args.sample_hz,
|
| 302 |
-
temp_dir=temp_dir,
|
| 303 |
-
)
|
| 304 |
-
request = InferRequest(
|
| 305 |
-
messages=[
|
| 306 |
-
{
|
| 307 |
-
"role": "user",
|
| 308 |
-
"content": [
|
| 309 |
-
{"type": "video", "video": frame_paths},
|
| 310 |
-
{"type": "text", "text": prompt},
|
| 311 |
-
],
|
| 312 |
-
}
|
| 313 |
-
]
|
| 314 |
-
)
|
| 315 |
-
response = engine.infer(
|
| 316 |
-
[request],
|
| 317 |
-
RequestConfig(max_tokens=args.max_new_tokens, temperature=0.0, top_k=1, top_p=1.0),
|
| 318 |
-
)[0].choices[0].message.content
|
| 319 |
-
|
| 320 |
-
pred_point_times_sec, pred_point_progress = parse_point_blocks(response)
|
| 321 |
-
|
| 322 |
-
output_row: dict[str, object] = {}
|
| 323 |
-
if task_instruction is not None:
|
| 324 |
-
output_row["task_instruction"] = task_instruction
|
| 325 |
-
if task_plan:
|
| 326 |
-
output_row["task_plan"] = task_plan
|
| 327 |
-
|
| 328 |
-
output_row.update(
|
| 329 |
-
{
|
| 330 |
-
"video_path": video_path_for_output,
|
| 331 |
-
"prompt_source": prompt_source,
|
| 332 |
-
"prompt_variant": "chunk_all" if prompt_source != "raw_prompt" else "custom",
|
| 333 |
-
"input_sample_hz": float(args.sample_hz),
|
| 334 |
-
"input_frame_indices": sampled_indices,
|
| 335 |
-
"input_timestamps_sec": sampled_timestamps_sec,
|
| 336 |
-
"input_frame_count": len(sampled_indices),
|
| 337 |
-
"decoded_video_stats": video_stats,
|
| 338 |
-
"prompt": prompt,
|
| 339 |
-
"response": response,
|
| 340 |
-
"pred_curve_parse_ok": bool(pred_point_progress),
|
| 341 |
-
"pred_curve_point_times_sec": pred_point_times_sec,
|
| 342 |
-
"pred_curve_point_progress": pred_point_progress,
|
| 343 |
-
}
|
| 344 |
-
)
|
| 345 |
-
if output_path is not None:
|
| 346 |
-
output_path.write_text(json.dumps(output_row, ensure_ascii=False) + "\n", encoding="utf-8")
|
| 347 |
-
|
| 348 |
-
if output_path is not None:
|
| 349 |
-
print(f"video_path={video_path}")
|
| 350 |
-
print(f"output_jsonl={output_path}")
|
| 351 |
-
print(f"input_sample_hz={float(args.sample_hz):.4f}")
|
| 352 |
-
print(f"input_frame_count={len(sampled_indices)}")
|
| 353 |
-
print(f"pred_keypoint_count={len(pred_point_progress)}")
|
| 354 |
-
print()
|
| 355 |
-
print(response)
|
| 356 |
-
return 0
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
if __name__ == "__main__":
|
| 360 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|