Image-Text-to-Text
Transformers
Safetensors
English
gemma4
aerial-vlm
drone
uav
vision-language-model
aerial-imagery
scene-understanding
visual-question-answering
visual-grounding
robotics
edge-ai
structured-output
open-weights
miril
conversational
Instructions to use MirilAI/Miril-DroneVLM-2B-2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use MirilAI/Miril-DroneVLM-2B-2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="MirilAI/Miril-DroneVLM-2B-2") 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("MirilAI/Miril-DroneVLM-2B-2") model = AutoModelForMultimodalLM.from_pretrained("MirilAI/Miril-DroneVLM-2B-2", 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 MirilAI/Miril-DroneVLM-2B-2 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "MirilAI/Miril-DroneVLM-2B-2" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "MirilAI/Miril-DroneVLM-2B-2", "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/MirilAI/Miril-DroneVLM-2B-2
- SGLang
How to use MirilAI/Miril-DroneVLM-2B-2 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 "MirilAI/Miril-DroneVLM-2B-2" \ --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": "MirilAI/Miril-DroneVLM-2B-2", "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 "MirilAI/Miril-DroneVLM-2B-2" \ --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": "MirilAI/Miril-DroneVLM-2B-2", "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 MirilAI/Miril-DroneVLM-2B-2 with Docker Model Runner:
docker model run hf.co/MirilAI/Miril-DroneVLM-2B-2
Stage Miril-DroneVLM-2B-2 release
Browse files- .gitattributes +3 -0
- LICENSE +201 -0
- NOTICE +15 -0
- README.md +571 -0
- assets/cleaned_test_comparison.svg +168 -0
- assets/deployment_comparison.svg +168 -0
- assets/full_validation_comparison.svg +168 -0
- assets/generation_comparison.svg +81 -0
- assets/qualitative_examples.png +3 -0
- assets/spoken_input_comparison.svg +81 -0
- chat_template.jinja +386 -0
- config.json +191 -0
- generation_config.json +14 -0
- image_overlay.py +84 -0
- inference.py +273 -0
- mlx_inference.py +75 -0
- model-00001-of-00003.safetensors +3 -0
- model-00002-of-00003.safetensors +3 -0
- model-00003-of-00003.safetensors +3 -0
- model.safetensors.index.json +0 -0
- processor_config.json +75 -0
- requirements.txt +9 -0
- router_contract.py +257 -0
- shared_kv_export_report.json +458 -0
- technical_report.pdf +3 -0
- tokenizer.json +3 -0
- tokenizer_config.json +142 -0
- video_overlay.py +529 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/qualitative_examples.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
technical_report.pdf filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
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.
|
NOTICE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Miril-DroneVLM-2B-2
|
| 2 |
+
Copyright 2026 Miril
|
| 3 |
+
|
| 4 |
+
Miril-DroneVLM-2B-2 is an aerial vision-language model derived from
|
| 5 |
+
google/gemma-4-E2B-it by Google DeepMind.
|
| 6 |
+
|
| 7 |
+
The base model google/gemma-4-E2B-it is published by Google DeepMind
|
| 8 |
+
under the Apache License, Version 2.0.
|
| 9 |
+
|
| 10 |
+
Miril modifications adapt the base model for aerial scene understanding,
|
| 11 |
+
drone visual question answering, typed JSON routing, and structured
|
| 12 |
+
image-relative coordinate outputs.
|
| 13 |
+
|
| 14 |
+
This distribution is not endorsed by or affiliated with Google,
|
| 15 |
+
Google DeepMind, or the Gemma authors.
|
README.md
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: google/gemma-4-E2B-it
|
| 4 |
+
library_name: transformers
|
| 5 |
+
pipeline_tag: image-text-to-text
|
| 6 |
+
language:
|
| 7 |
+
- en
|
| 8 |
+
tags:
|
| 9 |
+
- aerial-vlm
|
| 10 |
+
- drone
|
| 11 |
+
- uav
|
| 12 |
+
- vision-language-model
|
| 13 |
+
- aerial-imagery
|
| 14 |
+
- scene-understanding
|
| 15 |
+
- visual-question-answering
|
| 16 |
+
- visual-grounding
|
| 17 |
+
- robotics
|
| 18 |
+
- edge-ai
|
| 19 |
+
- structured-output
|
| 20 |
+
- open-weights
|
| 21 |
+
- miril
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
# Miril-DroneVLM-2B-2
|
| 25 |
+
|
| 26 |
+
### Drones can talk.
|
| 27 |
+
|
| 28 |
+
**Enabling drones to reason and talk about their environment.**
|
| 29 |
+
|
| 30 |
+
Ask a drone what it sees. Get an answer software can use.
|
| 31 |
+
|
| 32 |
+
Miril-DroneVLM-2B-2 is an open-weight, 2B-class aerial vision-language model from [Miril.ai](https://miril.ai). Give it an overhead image and an ordinary English question. It returns one of four machine-readable JSON responses: a whole-scene caption, a factual answer, a landing or delivery location, or a point on one visible target.
|
| 33 |
+
|
| 34 |
+
A drone camera can collect more imagery than a person can inspect frame by frame. This model explores a practical interface for that stream: ask what is present, what is happening, where an object is, or which part of the frame deserves review, then pass the typed result to an operator interface or separately validated application.
|
| 35 |
+
|
| 36 |
+
The target applications are civilian: delivery, first response, infrastructure inspection, agriculture, construction, mapping, and disaster recovery. Miril-DroneVLM-2B-2 is an edge-oriented research model for testing how those systems can become easier to supervise and integrate. It is a perception component, not a flight controller.
|
| 37 |
+
|
| 38 |
+
Try the image and video demo:
|
| 39 |
+
|
| 40 |
+
**[Miril-DroneVLM-2B-2-Demo](https://huggingface.co/spaces/MirilAI/Miril-DroneVLM-2B-2-Demo)**
|
| 41 |
+
|
| 42 |
+
## Four Questions, Four Usable Results
|
| 43 |
+
|
| 44 |
+

|
| 45 |
+
|
| 46 |
+
These are deterministic release-checkpoint MLX 8-bit outputs on attributed Pexels images
|
| 47 |
+
outside the training corpus. Each panel shows the ordinary user question, the
|
| 48 |
+
parsed response, and any drawable model coordinate. The examples were manually
|
| 49 |
+
screened for presentation and are not an accuracy sample; the complete
|
| 50 |
+
held-out results appear below.
|
| 51 |
+
|
| 52 |
+
## One Question In, One Typed Result Out
|
| 53 |
+
|
| 54 |
+
The user does **not** choose a schema or append formatting instructions. The user asks a normal question:
|
| 55 |
+
|
| 56 |
+
```text
|
| 57 |
+
What am I looking at?
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
```text
|
| 61 |
+
How many light vehicles are visible?
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
```text
|
| 65 |
+
Choose a place to lower this parcel.
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
```text
|
| 69 |
+
Track the white car.
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
The application supplies the fixed system contract from [`router_contract.py`](./router_contract.py). The model interprets the requested operation and returns exactly one bare JSON object.
|
| 73 |
+
|
| 74 |
+
The public runtime contract is `typed_json_router_v2`. Pin that contract file with the model revision when integrating the model into another application.
|
| 75 |
+
|
| 76 |
+
| Requested result | JSON `type` | Secondary key | Downstream use |
|
| 77 |
+
| --- | --- | --- | --- |
|
| 78 |
+
| General read of the whole image | `caption` | none | Display or index a scene description |
|
| 79 |
+
| Fact, count, presence, appearance, hazard, or verbal explanation | `answer` | none | Display or consume a textual answer |
|
| 80 |
+
| Choose one landing or delivery area | `location` | `intent: landing` or `delivery` | Review a selected point or broad direction |
|
| 81 |
+
| Locate, mark, or track one target | `pointing` | `action: locate`, `point`, or `track` | Draw a point or initialize downstream tracking |
|
| 82 |
+
|
| 83 |
+
This separation is the point of the release. The response identifies its own type, so downstream code can dispatch deterministically instead of trying to classify free-form prose after generation.
|
| 84 |
+
|
| 85 |
+
## Prompting In Plain English
|
| 86 |
+
|
| 87 |
+
Prompt for the result you need, not for the JSON structure:
|
| 88 |
+
|
| 89 |
+
| What you want | Natural examples | Response |
|
| 90 |
+
| --- | --- | --- |
|
| 91 |
+
| A read of the complete frame | “What am I looking at?”, “Describe the whole scene.” | `caption` |
|
| 92 |
+
| A fact or explanation | “Any buses?”, “How many people?”, “What should I avoid here?” | `answer` |
|
| 93 |
+
| A candidate landing or delivery area | “Where could I land?”, “Pick a clear place for this parcel.” | `location` |
|
| 94 |
+
| One visible target | “Where is the white car?”, “Mark the storage tank.”, “Track that truck.” | `pointing` |
|
| 95 |
+
|
| 96 |
+
These are intent families, not a keyword list. For example, “Where are the cars concentrated?” asks for a verbal answer, while “Where is the white car?” asks for one point.
|
| 97 |
+
|
| 98 |
+
### `caption`
|
| 99 |
+
|
| 100 |
+
Use a general request for the whole frame:
|
| 101 |
+
|
| 102 |
+
```text
|
| 103 |
+
What am I looking at?
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
```json
|
| 107 |
+
{
|
| 108 |
+
"type": "caption",
|
| 109 |
+
"caption": "From above, one built-up structure anchors the upper-left corner of a tidy property, bordered by asphalt, bright grass, driveways, and trimmed greenery."
|
| 110 |
+
}
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### `answer`
|
| 114 |
+
|
| 115 |
+
Use a question whose result should be words rather than one image point. This includes counts, object presence, colors, scene type, relative position, risks, suitability, and areas to avoid.
|
| 116 |
+
|
| 117 |
+
```text
|
| 118 |
+
Give the visible tally for utility poles.
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
```json
|
| 122 |
+
{
|
| 123 |
+
"type": "answer",
|
| 124 |
+
"answer": "Six utility poles can be seen around the scene."
|
| 125 |
+
}
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
`Where` does not automatically mean pointing. “Where are the vehicles concentrated?” asks for a verbal description and routes to `answer`. “Where is the white car?” asks for one concrete image point and routes to `pointing`.
|
| 129 |
+
|
| 130 |
+
### `location`
|
| 131 |
+
|
| 132 |
+
Use this only when asking the model to choose an area for landing a drone or placing a delivery. A location response may contain a precise point, a broad directional cue, or no point at all. Its status tells the application which interpretation is valid.
|
| 133 |
+
|
| 134 |
+
```text
|
| 135 |
+
Find a usable parcel drop location, if the view contains one.
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
A broad directional result can look like this:
|
| 139 |
+
|
| 140 |
+
```json
|
| 141 |
+
{
|
| 142 |
+
"type": "location",
|
| 143 |
+
"intent": "delivery",
|
| 144 |
+
"caption": "Coarse grid direction only, not a recommended landing or delivery spot. General safest-looking region: the open bare area on the right side of the road, away from vegetation and the building.",
|
| 145 |
+
"coordinate_system": "gemma_relative_0_1000_yx",
|
| 146 |
+
"point_2d": [500, 750],
|
| 147 |
+
"point_semantics": "representative_point",
|
| 148 |
+
"pointing_mode": "coarse_grid_direction",
|
| 149 |
+
"status": "coarse_direction",
|
| 150 |
+
"x": 750,
|
| 151 |
+
"y": 500
|
| 152 |
+
}
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
That output identifies a direction in the frame. It does **not** identify an approved parcel drop or landing spot.
|
| 156 |
+
|
| 157 |
+
### `pointing`
|
| 158 |
+
|
| 159 |
+
Use this when one concrete visible point is the requested result.
|
| 160 |
+
|
| 161 |
+
```text
|
| 162 |
+
Where in the image is the center of the white-roofed structure?
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
```json
|
| 166 |
+
{
|
| 167 |
+
"type": "pointing",
|
| 168 |
+
"action": "locate",
|
| 169 |
+
"caption": "The white-roofed structure is centered in the lower-right.",
|
| 170 |
+
"coordinate_system": "gemma_relative_0_1000_yx",
|
| 171 |
+
"point_2d": [936, 956],
|
| 172 |
+
"point_semantics": "specific_point",
|
| 173 |
+
"pointing_mode": "precise_point",
|
| 174 |
+
"status": "target_found",
|
| 175 |
+
"x": 956,
|
| 176 |
+
"y": 936
|
| 177 |
+
}
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
The action follows the user’s intent:
|
| 181 |
+
|
| 182 |
+
| User intent | Typical wording | `action` |
|
| 183 |
+
| --- | --- | --- |
|
| 184 |
+
| Find or look at one target | “Where is…”, “Find…”, “Look at…” | `locate` |
|
| 185 |
+
| Put a marker on one target | “Point at…”, “Mark…”, “Highlight…” | `point` |
|
| 186 |
+
| Supply the first point for temporal tracking | “Track…”, “Follow…”, “Lock on…” | `track` |
|
| 187 |
+
|
| 188 |
+
The model provides only the initial image point for `track`. Your application performs tracking across later video frames. The included live demo starts from a 60 x 60 region in the processed model frame, maps that region back to source-video pixels, and removes the marker if optical flow fails or any edge of the tracked box touches the frame.
|
| 189 |
+
|
| 190 |
+
## Coordinate Contract
|
| 191 |
+
|
| 192 |
+
Spatial responses use `gemma_relative_0_1000_yx`:
|
| 193 |
+
|
| 194 |
+
- `x=0` is the left edge and `x=1000` is the right edge.
|
| 195 |
+
- `y=0` is the top edge and `y=1000` is the bottom edge.
|
| 196 |
+
- `point_2d` is `[y, x]`.
|
| 197 |
+
- `x` and `y` repeat the same values for simpler downstream access.
|
| 198 |
+
|
| 199 |
+
There are three visual outcomes. They are deliberately different because a broad cue, a grounded point, and an abstention must not look interchangeable in an operator interface.
|
| 200 |
+
|
| 201 |
+
### Precise point
|
| 202 |
+
|
| 203 |
+
`pointing_mode: precise_point` is drawable only when the response is schema-valid and uses `status: target_found`. It represents one image-grounded point. A UI may draw a compact reticle there. For a `track` request it may also initialize a tracker.
|
| 204 |
+
|
| 205 |
+
### Coarse direction
|
| 206 |
+
|
| 207 |
+
`pointing_mode: coarse_grid_direction` is allowed only for `location`. It uses a 3 x 3 grid where both coordinates are exactly `250`, `500`, or `750`.
|
| 208 |
+
|
| 209 |
+
Treat it as “look in this part of the frame,” not “land here.” Draw a broad region cue, never a precision reticle. Do not initialize a tracker or send it to a controller as a target.
|
| 210 |
+
|
| 211 |
+
### No point
|
| 212 |
+
|
| 213 |
+
`pointing_mode: none`, invalid JSON, an invalid schema, or a non-target status means there is no drawable point. Coordinate fields must be null. If a malformed response combines a non-target status with coordinates, ignore those coordinates.
|
| 214 |
+
|
| 215 |
+
Interpret `status` by response type:
|
| 216 |
+
|
| 217 |
+
| Response | Status | Meaning |
|
| 218 |
+
| --- | --- | --- |
|
| 219 |
+
| `location` | `target_found` | One image-grounded landing or delivery candidate was selected |
|
| 220 |
+
| `location` | `coarse_direction` | Broad 3 x 3 directional context only |
|
| 221 |
+
| `location` | `no_safe_area` | No usable area should be marked |
|
| 222 |
+
| `location` | `unknown` | The image does not support a reliable judgment |
|
| 223 |
+
| `pointing` | `target_found` | One requested target was grounded |
|
| 224 |
+
| `pointing` | `no_target` | The requested object is absent |
|
| 225 |
+
| `pointing` | `no_matching_target` | Objects may be present, but none satisfy the requested relation |
|
| 226 |
+
| `pointing` | `ambiguous_target` | More than one target fits and the request does not identify one |
|
| 227 |
+
| `pointing` | `unknown` | The image does not support a reliable point |
|
| 228 |
+
|
| 229 |
+
The included validator enforces these rules before returning a dispatch-safe payload. Applications should still retain the original model text for audit rather than silently repairing it.
|
| 230 |
+
|
| 231 |
+
## Mixed Requests
|
| 232 |
+
|
| 233 |
+
The fixed router applies this precedence:
|
| 234 |
+
|
| 235 |
+
1. A request to choose a landing or delivery area routes to `location`.
|
| 236 |
+
2. Otherwise, an explicit request for one concrete point routes to `pointing`.
|
| 237 |
+
3. Otherwise, a factual or verbal question routes to `answer`.
|
| 238 |
+
4. Otherwise, a whole-scene read routes to `caption`.
|
| 239 |
+
|
| 240 |
+
If several indistinguishable targets are named, the model should preserve the requested pointing action but return `ambiguous_target` with null coordinates instead of choosing arbitrarily. If different pointing actions are mixed for one target, `track` takes priority over `point`, and `point` takes priority over `locate`.
|
| 241 |
+
|
| 242 |
+
## Loader Compatibility
|
| 243 |
+
|
| 244 |
+
Use `transformers>=5.12.1`. Gemma 4 E2B declares 35 language layers and 20 shared-KV layers. Layers 15 through 34 reuse key/value states, so a correct export does not contain separate `k_proj`, `v_proj`, or `k_norm` tensors for those shared layers. Release validation checks this layout directly and rejects both genuinely missing owner tensors and redundant shared-layer tensors.
|
| 245 |
+
|
| 246 |
+
The retained [shared-KV export audit](./shared_kv_export_report.json) records
|
| 247 |
+
the expected and observed tensor owners for this exact artifact.
|
| 248 |
+
|
| 249 |
+
## Quickstart
|
| 250 |
+
|
| 251 |
+
Download the small runtime helpers from this repository, install their dependencies, and ask a plain-English question:
|
| 252 |
+
|
| 253 |
+
```bash
|
| 254 |
+
hf download MirilAI/Miril-DroneVLM-2B-2 inference.py router_contract.py requirements.txt --local-dir miril-drone-runtime
|
| 255 |
+
python -m pip install -r miril-drone-runtime/requirements.txt
|
| 256 |
+
python miril-drone-runtime/inference.py \
|
| 257 |
+
--image drone_frame.jpg \
|
| 258 |
+
--prompt "Where is the white car?"
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
The script:
|
| 262 |
+
|
| 263 |
+
- sends the exact fixed router prompt as a system message,
|
| 264 |
+
- sends the image and ordinary English question as the user message,
|
| 265 |
+
- decodes deterministically,
|
| 266 |
+
- requires valid bare JSON,
|
| 267 |
+
- validates the complete response schema,
|
| 268 |
+
- refuses to dispatch or draw malformed coordinates.
|
| 269 |
+
|
| 270 |
+
For direct integration, import `TYPED_JSON_ROUTER_SYSTEM_PROMPT`, `typed_response_errors`, and `drawable_point` from [`router_contract.py`](./router_contract.py).
|
| 271 |
+
|
| 272 |
+
### Experimental spoken question
|
| 273 |
+
|
| 274 |
+
The inherited Gemma audio path can be exercised with the same image and router contract. This checkpoint was fine-tuned with typed questions, so use the published typed-versus-spoken benchmark to judge the modality gap rather than assuming parity. Install `ffmpeg`, then run:
|
| 275 |
+
|
| 276 |
+
```bash
|
| 277 |
+
python miril-drone-runtime/inference.py \
|
| 278 |
+
--image drone_frame.jpg \
|
| 279 |
+
--audio spoken_question.mp3 \
|
| 280 |
+
--audio-transcript "Where is the white car?"
|
| 281 |
+
```
|
| 282 |
+
|
| 283 |
+
`--audio-transcript` is optional provenance for the returned audit record. It is never supplied to the model; the user turn contains the image and decoded audio waveform only.
|
| 284 |
+
|
| 285 |
+
## Image And Video Overlays
|
| 286 |
+
|
| 287 |
+
The same black-side-panel presentation used by the demo is included as runnable code. Labels are bold green, every response is shown beside the question that produced it, and the panel is half the image width.
|
| 288 |
+
|
| 289 |
+
```bash
|
| 290 |
+
hf download MirilAI/Miril-DroneVLM-2B-2 \
|
| 291 |
+
inference.py router_contract.py image_overlay.py video_overlay.py requirements.txt \
|
| 292 |
+
--local-dir miril-drone-runtime
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
Image:
|
| 296 |
+
|
| 297 |
+
```bash
|
| 298 |
+
python miril-drone-runtime/image_overlay.py \
|
| 299 |
+
--image drone_frame.jpg \
|
| 300 |
+
--output overlay.jpg \
|
| 301 |
+
--prompt "What am I looking at?" \
|
| 302 |
+
--prompt "Are any people or light vehicles visible?" \
|
| 303 |
+
--prompt "Where is the white car?"
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
Video, with one inference pass every five seconds:
|
| 307 |
+
|
| 308 |
+
```bash
|
| 309 |
+
python miril-drone-runtime/video_overlay.py \
|
| 310 |
+
--input drone_video.mp4 \
|
| 311 |
+
--output drone_video_overlay.mp4 \
|
| 312 |
+
--interval-seconds 5 \
|
| 313 |
+
--crop center-square \
|
| 314 |
+
--crop-size 1000 \
|
| 315 |
+
--prompt "What am I looking at?" \
|
| 316 |
+
--prompt "Track the white car."
|
| 317 |
+
```
|
| 318 |
+
|
| 319 |
+
The renderer preserves the source frame rate, reveals each answer over 12 frames without changing its font size during the reveal, tracks valid precise points between inference calls, hides failed or edge-adjacent tracks, and never tracks a coarse direction or non-target response.
|
| 320 |
+
|
| 321 |
+
## Evaluation
|
| 322 |
+
|
| 323 |
+
Release evaluation has three complementary scopes:
|
| 324 |
+
|
| 325 |
+
1. **Complete held-out validation:** every V2 release artifact is evaluated on every accepted validation request. This is the primary quantization-regression check for merged BF16, CUDA bnb8, CUDA bnb4, MLX 8-bit, and MLX 4-bit.
|
| 326 |
+
2. **Independent route benchmark:** a separately authored held-out benchmark balances captioning, factual answers, landing selection, delivery selection, object location, visual pointing, and tracker initialization. It supports controlled V1/V2 and cross-runtime comparison without reusing training questions.
|
| 327 |
+
3. **Cleaned held-out audit:** every unchanged release artifact is evaluated after a stricter review removes pointing targets that are too small under the model's actual image-preprocessing contract. Validation and test are complete, and strict-cleaned and coverage-matched views are reported separately.
|
| 328 |
+
|
| 329 |
+
All scopes check route selection, strict JSON, schema validity, text overlap, status and mode agreement, null-coordinate discipline, coarse-grid agreement, and precise-coordinate recovery. No single aggregate can show all of those behaviors, so the release reports them separately. Publication requires complete prediction coverage and the exact case-file hash for every reported artifact.
|
| 330 |
+
|
| 331 |
+
<!-- MIRIL_RELEASE_METRICS_START -->
|
| 332 |
+

|
| 333 |
+
|
| 334 |
+
### Generation comparison
|
| 335 |
+
|
| 336 |
+
| Metric | V1 merged BF16 | Merged BF16 |
|
| 337 |
+
|---|---:|---:|
|
| 338 |
+
| Valid JSON | 92.0% | 100.0% |
|
| 339 |
+
| Schema valid | 21.8% | 96.1% |
|
| 340 |
+
| Route accuracy | 21.8% | 94.8% |
|
| 341 |
+
| Caption / answer F1 | 26.2% | 38.2% |
|
| 342 |
+
| Spatial status | 2.9% | 79.2% |
|
| 343 |
+
| Precise target retained | 7.5% | 50.2% |
|
| 344 |
+
| Point within 100 | 3.9% | 33.4% |
|
| 345 |
+
| Coarse direction exact | 0.0% | 35.0% |
|
| 346 |
+
| No-target discipline | 43.9% | 93.4% |
|
| 347 |
+
|
| 348 |
+

|
| 349 |
+
|
| 350 |
+
### Deployment comparison
|
| 351 |
+
|
| 352 |
+
| Metric | Merged BF16 | CUDA bnb8 | CUDA bnb4 | MLX 8-bit | MLX 4-bit |
|
| 353 |
+
|---|---:|---:|---:|---:|---:|
|
| 354 |
+
| Valid JSON | 100.0% | 99.1% | 96.7% | 100.0% | 100.0% |
|
| 355 |
+
| Schema valid | 96.1% | 93.6% | 92.1% | 94.6% | 55.8% |
|
| 356 |
+
| Route accuracy | 94.8% | 91.6% | 91.2% | 93.4% | 56.9% |
|
| 357 |
+
| Caption / answer F1 | 38.2% | 19.6% | 37.2% | 37.3% | 35.1% |
|
| 358 |
+
| Spatial status | 79.2% | 63.4% | 69.7% | 77.9% | 77.3% |
|
| 359 |
+
| Precise target retained | 50.2% | 42.4% | 53.1% | 51.2% | 46.9% |
|
| 360 |
+
| Point within 100 | 33.4% | 8.0% | 29.7% | 31.0% | 25.1% |
|
| 361 |
+
| Coarse direction exact | 35.0% | 9.3% | 30.2% | 32.1% | 27.6% |
|
| 362 |
+
| No-target discipline | 93.4% | 89.6% | 87.5% | 92.6% | 91.6% |
|
| 363 |
+
|
| 364 |
+
### Complete held-out validation
|
| 365 |
+
|
| 366 |
+

|
| 367 |
+
|
| 368 |
+
| Metric | Merged BF16 | CUDA bnb8 | CUDA bnb4 | MLX 8-bit | MLX 4-bit |
|
| 369 |
+
|---|---:|---:|---:|---:|---:|
|
| 370 |
+
| Valid JSON | 99.9% | 99.7% | 99.0% | 100.0% | 100.0% |
|
| 371 |
+
| Schema valid | 99.9% | 99.7% | 98.7% | 100.0% | 53.0% |
|
| 372 |
+
| Route accuracy | 99.9% | 99.6% | 98.6% | 100.0% | 53.2% |
|
| 373 |
+
| Caption / answer F1 | 38.6% | 23.0% | 37.2% | 37.8% | 35.6% |
|
| 374 |
+
| Spatial status | 86.1% | 70.1% | 79.8% | 85.3% | 82.8% |
|
| 375 |
+
| Precise target retained | 71.7% | 60.7% | 73.6% | 71.2% | 68.8% |
|
| 376 |
+
| Point within 100 | 38.3% | 9.2% | 34.0% | 33.6% | 27.8% |
|
| 377 |
+
| Coarse direction exact | 34.4% | 11.3% | 31.2% | 33.0% | 25.8% |
|
| 378 |
+
| No-target discipline | 96.3% | 93.3% | 92.4% | 95.6% | 95.5% |
|
| 379 |
+
|
| 380 |
+
### Cleaned held-out deployment audit
|
| 381 |
+
|
| 382 |
+

|
| 383 |
+
|
| 384 |
+
After training, a stricter held-out audit removed pointing rows whose targets fall below the model-visible size threshold, then ran every release artifact on the complete revised validation and test splits. Strict-cleaned rows use only accepted evidence. Coverage-matched rows add evidence-preserving questions on the same held-out images to restore the earlier route and pointing action/status mix; they do not recreate the earlier object-class histogram.
|
| 385 |
+
|
| 386 |
+
#### Final held-out test
|
| 387 |
+
|
| 388 |
+
##### Strict-cleaned evidence
|
| 389 |
+
|
| 390 |
+
| Metric | Merged BF16 - Strict cleaned test | CUDA bnb8 - Strict cleaned test | CUDA bnb4 - Strict cleaned test | MLX 8-bit - Strict cleaned test | MLX 4-bit - Strict cleaned test |
|
| 391 |
+
|---|---:|---:|---:|---:|---:|
|
| 392 |
+
| Valid JSON | 99.8% | 99.6% | 89.3% | 100.0% | 100.0% |
|
| 393 |
+
| Schema valid | 99.8% | 99.6% | 89.2% | 100.0% | 62.8% |
|
| 394 |
+
| Route accuracy | 99.8% | 99.5% | 89.1% | 100.0% | 63.3% |
|
| 395 |
+
| Reference text F1 | 60.9% | 47.4% | 53.5% | 60.0% | 57.2% |
|
| 396 |
+
| Spatial status | 88.0% | 76.7% | 72.0% | 86.1% | 83.8% |
|
| 397 |
+
| Coordinate quality | 89.7% | 84.0% | 78.9% | 88.4% | 86.1% |
|
| 398 |
+
| Precise target retained | 65.2% | 52.6% | 60.3% | 64.8% | 61.3% |
|
| 399 |
+
| Point within 100 | 42.6% | 9.4% | 34.3% | 39.5% | 31.4% |
|
| 400 |
+
| No-target discipline | 96.5% | 97.1% | 85.1% | 95.1% | 93.4% |
|
| 401 |
+
|
| 402 |
+
##### Coverage-matched evidence
|
| 403 |
+
|
| 404 |
+
| Metric | Merged BF16 - Coverage-matched test | CUDA bnb8 - Coverage-matched test | CUDA bnb4 - Coverage-matched test | MLX 8-bit - Coverage-matched test | MLX 4-bit - Coverage-matched test |
|
| 405 |
+
|---|---:|---:|---:|---:|---:|
|
| 406 |
+
| Valid JSON | 99.9% | 99.6% | 89.4% | 100.0% | 100.0% |
|
| 407 |
+
| Schema valid | 99.9% | 99.5% | 89.3% | 100.0% | 50.2% |
|
| 408 |
+
| Route accuracy | 99.9% | 99.5% | 89.1% | 100.0% | 50.5% |
|
| 409 |
+
| Reference text F1 | 64.3% | 50.3% | 56.6% | 63.4% | 60.6% |
|
| 410 |
+
| Spatial status | 84.2% | 64.1% | 70.8% | 82.5% | 79.0% |
|
| 411 |
+
| Coordinate quality | 84.9% | 77.6% | 74.7% | 83.6% | 80.9% |
|
| 412 |
+
| Precise target retained | 63.5% | 49.9% | 60.0% | 62.8% | 59.7% |
|
| 413 |
+
| Point within 100 | 42.8% | 7.8% | 34.4% | 39.2% | 31.4% |
|
| 414 |
+
| No-target discipline | 96.4% | 97.6% | 84.1% | 95.2% | 93.1% |
|
| 415 |
+
|
| 416 |
+
#### Validation
|
| 417 |
+
|
| 418 |
+
##### Strict-cleaned evidence
|
| 419 |
+
|
| 420 |
+
| Metric | Merged BF16 - Strict cleaned validation | CUDA bnb8 - Strict cleaned validation | CUDA bnb4 - Strict cleaned validation | MLX 8-bit - Strict cleaned validation | MLX 4-bit - Strict cleaned validation |
|
| 421 |
+
|---|---:|---:|---:|---:|---:|
|
| 422 |
+
| Valid JSON | 99.9% | 99.6% | 96.4% | 100.0% | 100.0% |
|
| 423 |
+
| Schema valid | 99.9% | 99.5% | 96.1% | 100.0% | 64.3% |
|
| 424 |
+
| Route accuracy | 99.9% | 99.5% | 96.0% | 100.0% | 64.6% |
|
| 425 |
+
| Reference text F1 | 59.9% | 48.1% | 56.8% | 59.0% | 56.7% |
|
| 426 |
+
| Spatial status | 89.6% | 80.1% | 80.2% | 88.5% | 86.3% |
|
| 427 |
+
| Coordinate quality | 89.8% | 84.9% | 85.5% | 88.9% | 87.4% |
|
| 428 |
+
| Precise target retained | 65.8% | 55.7% | 67.8% | 65.8% | 62.9% |
|
| 429 |
+
| Point within 100 | 39.3% | 9.6% | 34.6% | 35.0% | 28.6% |
|
| 430 |
+
| No-target discipline | 96.7% | 97.2% | 91.8% | 96.1% | 95.2% |
|
| 431 |
+
|
| 432 |
+
##### Coverage-matched evidence
|
| 433 |
+
|
| 434 |
+
| Metric | Merged BF16 - Coverage-matched validation | CUDA bnb8 - Coverage-matched validation | CUDA bnb4 - Coverage-matched validation | MLX 8-bit - Coverage-matched validation | MLX 4-bit - Coverage-matched validation |
|
| 435 |
+
|---|---:|---:|---:|---:|---:|
|
| 436 |
+
| Valid JSON | 99.9% | 99.6% | 96.3% | 100.0% | 100.0% |
|
| 437 |
+
| Schema valid | 99.9% | 99.5% | 96.0% | 100.0% | 53.0% |
|
| 438 |
+
| Route accuracy | 99.9% | 99.4% | 95.9% | 100.0% | 53.2% |
|
| 439 |
+
| Reference text F1 | 62.6% | 50.3% | 59.4% | 61.8% | 59.5% |
|
| 440 |
+
| Spatial status | 84.5% | 66.7% | 76.6% | 83.5% | 80.7% |
|
| 441 |
+
| Coordinate quality | 84.5% | 78.7% | 80.1% | 83.4% | 81.7% |
|
| 442 |
+
| Precise target retained | 60.5% | 49.7% | 62.1% | 60.7% | 58.8% |
|
| 443 |
+
| Point within 100 | 38.4% | 8.1% | 34.0% | 33.9% | 28.0% |
|
| 444 |
+
| No-target discipline | 96.1% | 97.4% | 90.0% | 95.5% | 94.1% |
|
| 445 |
+
|
| 446 |
+
Validation supports comparison and model selection; test is the final held-out report. These automated scores measure contract and reference agreement, not flight safety.
|
| 447 |
+
|
| 448 |
+
### Experimental spoken-query input
|
| 449 |
+
|
| 450 |
+

|
| 451 |
+
|
| 452 |
+
| Metric | Typed question | Spoken question |
|
| 453 |
+
|---|---:|---:|
|
| 454 |
+
| Valid JSON | 99.9% | 100.0% |
|
| 455 |
+
| Schema valid | 99.9% | 98.1% |
|
| 456 |
+
| Route accuracy | 99.9% | 97.6% |
|
| 457 |
+
| Caption / answer F1 | 39.3% | 39.2% |
|
| 458 |
+
| Spatial status | 71.3% | 65.3% |
|
| 459 |
+
| Precise target retained | 50.5% | 39.3% |
|
| 460 |
+
| Point within 100 | 36.3% | 31.9% |
|
| 461 |
+
| Coarse direction exact | 34.7% | 34.3% |
|
| 462 |
+
| No-target discipline | 88.2% | 88.2% |
|
| 463 |
+
|
| 464 |
+
The image, ordinary-English wording, accepted JSON target, route, and case identity are matched; only the question modality changes. This checkpoint was trained on typed questions, so spoken input is an experimental zero-shot capability boundary rather than parity-tested speech support. A future generation will train directly on paired image-plus-audio questions.
|
| 465 |
+
|
| 466 |
+
The independent benchmark rows use identical cases. Higher is better. Reference-overlap and coordinate scores are regression signals, not physical-world safety evidence.
|
| 467 |
+
<!-- MIRIL_RELEASE_METRICS_END -->
|
| 468 |
+
|
| 469 |
+
These automated metrics are regression signals, not proof of physical-world safety. Aerial reasoning and pointing still require human review and scenario-specific testing.
|
| 470 |
+
|
| 471 |
+
## Technical Report
|
| 472 |
+
|
| 473 |
+
The methods report explains how detector annotations became image-question-JSON supervision, why the four response envelopes exist, how observed model failures became preference data, and how the evidence gate prevents partial evaluations from becoming release claims:
|
| 474 |
+
|
| 475 |
+
**[Miril-DroneVLM-2B-2: Compiling Detector Annotations into a Typed Aerial VLM](./technical_report.pdf)**
|
| 476 |
+
|
| 477 |
+
The report treats data design as the central engineering contribution and clearly separates verified dataset properties from model-quality and operational-safety claims.
|
| 478 |
+
|
| 479 |
+
## Citation
|
| 480 |
+
|
| 481 |
+
```bibtex
|
| 482 |
+
@techreport{sturges2026mirildrone2b2,
|
| 483 |
+
title = {Miril-DroneVLM-2B-2: Compiling Detector Annotations into a Typed Aerial VLM},
|
| 484 |
+
author = {Sturges, Stephan},
|
| 485 |
+
institution = {Miril.ai},
|
| 486 |
+
year = {2026},
|
| 487 |
+
url = {https://huggingface.co/MirilAI/Miril-DroneVLM-2B-2/blob/main/technical_report.pdf}
|
| 488 |
+
}
|
| 489 |
+
```
|
| 490 |
+
|
| 491 |
+
GPT-5.5 and GPT-5.6 Sol contributed to the research, engineering, analysis, and editing. Stephan Sturges directed the work and accepts responsibility for the report and release.
|
| 492 |
+
|
| 493 |
+
## Deployment Variants
|
| 494 |
+
|
| 495 |
+
- [Miril-DroneVLM-2B-2-bnb8](https://huggingface.co/MirilAI/Miril-DroneVLM-2B-2-bnb8): 8-bit CUDA deployment.
|
| 496 |
+
- [Miril-DroneVLM-2B-2-bnb4](https://huggingface.co/MirilAI/Miril-DroneVLM-2B-2-bnb4): 4-bit CUDA deployment.
|
| 497 |
+
- [Miril-DroneVLM-2B-2-MLX-8bit](https://huggingface.co/MirilAI/Miril-DroneVLM-2B-2-MLX-8bit): 8-bit Apple Silicon deployment.
|
| 498 |
+
- [Miril-DroneVLM-2B-2-MLX-4bit](https://huggingface.co/MirilAI/Miril-DroneVLM-2B-2-MLX-4bit): compact Apple Silicon deployment with a documented strict-schema routing regression.
|
| 499 |
+
|
| 500 |
+
Variant cards report artifact size, recommended deployment memory, and
|
| 501 |
+
complete same-case benchmark deltas.
|
| 502 |
+
|
| 503 |
+
Treat those measured deltas as authoritative. The CUDA 8-bit and 4-bit
|
| 504 |
+
packages use different bitsandbytes algorithms, so bit width alone does not
|
| 505 |
+
rank their multimodal fidelity.
|
| 506 |
+
|
| 507 |
+
The MLX 4-bit artifact remains available as a measured compact variant, not as an equivalent replacement for MLX 8-bit. On the complete balanced independent benchmark it preserves valid JSON at 100.0% after rounding but drops to 55.8% exact schema validity and 56.9% route accuracy, predominantly because pointing responses use `intent` where the contract requires `action`. The variant card contains the route-level evidence, operational warning, and the mixed 4/8-bit experiment planned for generation three.
|
| 508 |
+
|
| 509 |
+
## WALDO Lineage
|
| 510 |
+
|
| 511 |
+
Miril-DroneVLM-2B-2 is a separate VLM, but its aerial vocabulary grows from the [WALDO30 overhead detector](https://huggingface.co/StephanST/WALDO30). The two systems are complementary: WALDO supplies fast detection boxes; Miril-DroneVLM-2B-2 supplies language reasoning and typed responses.
|
| 512 |
+
|
| 513 |
+
During dataset construction, WALDO annotations are treated as authoritative evidence for class presence, counts, and geometry after audit. A broad class name is not treated as proof of a narrower noun: an `UPole` box may contain a utility pole, antenna, dish, mast, or related fixture, and a `Boat` box may contain different kinds of watercraft. Narrow language is used only when reviewed image evidence supports it. This preserves the geometric value of the detector labels without asking the model to memorize invented detail.
|
| 514 |
+
|
| 515 |
+
| WALDO class | What the class covers |
|
| 516 |
+
| --- | --- |
|
| 517 |
+
| `LightVehicle` | Cars, sedans, rickshaws, vans, and similar small personal road vehicles. Trucks and buses remain separate classes. |
|
| 518 |
+
| `Person` | People and human figures visible from above. |
|
| 519 |
+
| `Building` | Buildings and roofed structures, including sheds, shacks, pergolas, canopies, sunshades, and tents when supported by the image. |
|
| 520 |
+
| `UPole` | Utility and telegraph poles, power-line fixtures, lamp posts, street fixtures, satellite dishes, antennas, and thin truss structures. |
|
| 521 |
+
| `Boat` | Boats and watercraft such as ships, canoes, kayaks, and sailboats; surfboards may also occur in the source ontology. |
|
| 522 |
+
| `Bike` | Bicycles, motorbikes, mopeds, scooters, and other two-wheeled vehicles. |
|
| 523 |
+
| `Container` | Storage and shipping containers. |
|
| 524 |
+
| `Truck` | Trucks, lorries, transport vehicles, semitrailers, articulated trucks, flatbeds, and larger industrial road vehicles. |
|
| 525 |
+
| `Gastank` | Storage tanks, including liquid-storage and cylindrical tanks. |
|
| 526 |
+
| `Digger` | Diggers, excavators, tractors when explicitly supported, and other construction vehicles or equipment. |
|
| 527 |
+
| `Solarpanels` | Individual solar panels and panel arrays. |
|
| 528 |
+
| `Bus` | Buses and bus-like passenger vehicles. |
|
| 529 |
+
|
| 530 |
+
The VLM receives no hidden detector boxes or object-count metadata during inference. It sees only the image, fixed router system message, and user question.
|
| 531 |
+
|
| 532 |
+
## Model Scope
|
| 533 |
+
|
| 534 |
+
This checkpoint is derived from `google/gemma-4-E2B-it` and distributed as merged weights. Its aerial fine-tuning used drone-view images with typed English questions and the typed JSON contract above.
|
| 535 |
+
|
| 536 |
+
The inherited Gemma architecture also accepts audio. We evaluate image-plus-spoken-question inference separately from matched typed questions and report that result as an experimental zero-shot capability, not as speech-training parity. Native temporal video understanding is not part of this release contract: the video demo samples individual frames and uses OpenCV between model calls.
|
| 537 |
+
|
| 538 |
+
## Limits And Safety
|
| 539 |
+
|
| 540 |
+
Miril-DroneVLM-2B-2 is a perception and reasoning model. It is not a flight controller, certified detect-and-avoid system, landing certifier, or standalone safety system.
|
| 541 |
+
|
| 542 |
+
It can miss small people or objects, confuse similar classes, misread scale, return an incorrect answer, select an unsuitable region, or produce invalid JSON. Performance varies with altitude, compression, motion blur, weather, camera angle, and domain shift.
|
| 543 |
+
|
| 544 |
+
For physical deployment:
|
| 545 |
+
|
| 546 |
+
- validate the exact camera, altitude range, geography, and operating conditions,
|
| 547 |
+
- independently check terrain, obstacles, people, vehicles, trees, wires, weather, and maps,
|
| 548 |
+
- keep geofencing and vehicle constraints outside the model,
|
| 549 |
+
- reject invalid JSON and non-target coordinates,
|
| 550 |
+
- treat coarse directions as review cues only,
|
| 551 |
+
- keep a human or separately validated planner in the decision loop,
|
| 552 |
+
- log prompts, outputs, validation failures, and downstream actions.
|
| 553 |
+
|
| 554 |
+
## Intended Use
|
| 555 |
+
|
| 556 |
+
Good research and prototyping fits include:
|
| 557 |
+
|
| 558 |
+
- searching and summarizing aerial imagery for human review,
|
| 559 |
+
- inspection triage for infrastructure, agriculture, construction, and mapping,
|
| 560 |
+
- first-response and disaster-recovery situational awareness,
|
| 561 |
+
- delivery or landing-area pre-screening with explicit abstention,
|
| 562 |
+
- object localization and tracker initialization under application control, and
|
| 563 |
+
- typed-output experiments for physical AI.
|
| 564 |
+
|
| 565 |
+
Do not use the model for direct unsupervised flight control, certified safety decisions, identifying people, or safety-critical operation without independent sensing and validation.
|
| 566 |
+
|
| 567 |
+
## License
|
| 568 |
+
|
| 569 |
+
Apache License 2.0. See [`LICENSE`](./LICENSE) and [`NOTICE`](./NOTICE).
|
| 570 |
+
|
| 571 |
+
This model is derived from `google/gemma-4-E2B-it`. Review the upstream model documentation and applicable terms before deployment.
|
assets/cleaned_test_comparison.svg
ADDED
|
|
assets/deployment_comparison.svg
ADDED
|
|
assets/full_validation_comparison.svg
ADDED
|
|
assets/generation_comparison.svg
ADDED
|
|
assets/qualitative_examples.png
ADDED
|
Git LFS Details
|
assets/spoken_input_comparison.svg
ADDED
|
|
chat_template.jinja
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{#
|
| 2 |
+
Template: Google Gemma 4 Canonical Chat Template
|
| 3 |
+
Author: Google Gemma Engineering Team
|
| 4 |
+
Published: 2026-07-09
|
| 5 |
+
Context: Fixed tool-calling loops, turn closures, and thinking content-ordering.
|
| 6 |
+
#}
|
| 7 |
+
{%- macro format_parameters(properties, required, filter_keys=false) -%}
|
| 8 |
+
{%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
|
| 9 |
+
{%- set ns = namespace(found_first=false) -%}
|
| 10 |
+
{%- for key, value in properties | dictsort -%}
|
| 11 |
+
{%- set add_comma = false -%}
|
| 12 |
+
{%- if not filter_keys or key not in standard_keys -%}
|
| 13 |
+
{%- if ns.found_first %},{% endif -%}
|
| 14 |
+
{%- set ns.found_first = true -%}
|
| 15 |
+
{{ key }}:{
|
| 16 |
+
{%- if value['description'] -%}
|
| 17 |
+
description:<|"|>{{ value['description'] }}<|"|>
|
| 18 |
+
{%- set add_comma = true -%}
|
| 19 |
+
{%- endif -%}
|
| 20 |
+
{%- if value['type'] | upper == 'STRING' -%}
|
| 21 |
+
{%- if value['enum'] -%}
|
| 22 |
+
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
| 23 |
+
enum:{{ format_argument(value['enum']) }}
|
| 24 |
+
{%- endif -%}
|
| 25 |
+
{%- elif value['type'] | upper == 'ARRAY' -%}
|
| 26 |
+
{%- if value['items'] is mapping and value['items'] -%}
|
| 27 |
+
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
| 28 |
+
items:{
|
| 29 |
+
{%- set ns_items = namespace(found_first=false) -%}
|
| 30 |
+
{%- for item_key, item_value in value['items'] | dictsort -%}
|
| 31 |
+
{%- if item_value is not none -%}
|
| 32 |
+
{%- if ns_items.found_first %},{% endif -%}
|
| 33 |
+
{%- set ns_items.found_first = true -%}
|
| 34 |
+
{%- if item_key == 'properties' -%}
|
| 35 |
+
properties:{
|
| 36 |
+
{%- if item_value is mapping -%}
|
| 37 |
+
{{- format_parameters(item_value, value['items']['required'] | default([])) -}}
|
| 38 |
+
{%- endif -%}
|
| 39 |
+
}
|
| 40 |
+
{%- elif item_key == 'required' -%}
|
| 41 |
+
required:[
|
| 42 |
+
{%- for req_item in item_value -%}
|
| 43 |
+
<|"|>{{- req_item -}}<|"|>
|
| 44 |
+
{%- if not loop.last %},{% endif -%}
|
| 45 |
+
{%- endfor -%}
|
| 46 |
+
]
|
| 47 |
+
{%- elif item_key == 'type' -%}
|
| 48 |
+
{%- if item_value is string -%}
|
| 49 |
+
type:{{ format_argument(item_value | upper) }}
|
| 50 |
+
{%- else -%}
|
| 51 |
+
type:{{ format_argument(item_value | map('upper') | list) }}
|
| 52 |
+
{%- endif -%}
|
| 53 |
+
{%- else -%}
|
| 54 |
+
{{ item_key }}:{{ format_argument(item_value) }}
|
| 55 |
+
{%- endif -%}
|
| 56 |
+
{%- endif -%}
|
| 57 |
+
{%- endfor -%}
|
| 58 |
+
}
|
| 59 |
+
{%- endif -%}
|
| 60 |
+
{%- endif -%}
|
| 61 |
+
{%- if value['nullable'] %}
|
| 62 |
+
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
| 63 |
+
nullable:true
|
| 64 |
+
{%- endif -%}
|
| 65 |
+
{%- if value['type'] | upper == 'OBJECT' -%}
|
| 66 |
+
{%- if value['properties'] is defined and value['properties'] is mapping -%}
|
| 67 |
+
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
| 68 |
+
properties:{
|
| 69 |
+
{{- format_parameters(value['properties'], value['required'] | default([])) -}}
|
| 70 |
+
}
|
| 71 |
+
{%- elif value is mapping -%}
|
| 72 |
+
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
| 73 |
+
properties:{
|
| 74 |
+
{{- format_parameters(value, value['required'] | default([]), filter_keys=true) -}}
|
| 75 |
+
}
|
| 76 |
+
{%- endif -%}
|
| 77 |
+
{%- if value['required'] -%}
|
| 78 |
+
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
| 79 |
+
required:[
|
| 80 |
+
{%- for item in value['required'] | default([]) -%}
|
| 81 |
+
<|"|>{{- item -}}<|"|>
|
| 82 |
+
{%- if not loop.last %},{% endif -%}
|
| 83 |
+
{%- endfor -%}
|
| 84 |
+
]
|
| 85 |
+
{%- endif -%}
|
| 86 |
+
{%- endif -%}
|
| 87 |
+
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
| 88 |
+
type:<|"|>{{ value['type'] | upper }}<|"|>}
|
| 89 |
+
{%- endif -%}
|
| 90 |
+
{%- endfor -%}
|
| 91 |
+
{%- endmacro -%}
|
| 92 |
+
{%- macro format_function_declaration(tool_data) -%}
|
| 93 |
+
declaration:{{- tool_data['function']['name'] -}}{description:<|"|>{{- tool_data['function']['description'] -}}<|"|>
|
| 94 |
+
{%- set params = tool_data['function']['parameters'] -%}
|
| 95 |
+
{%- if params -%}
|
| 96 |
+
,parameters:{
|
| 97 |
+
{%- if params['properties'] -%}
|
| 98 |
+
properties:{ {{- format_parameters(params['properties'], params['required']) -}} },
|
| 99 |
+
{%- endif -%}
|
| 100 |
+
{%- if params['required'] -%}
|
| 101 |
+
required:[
|
| 102 |
+
{%- for item in params['required'] -%}
|
| 103 |
+
<|"|>{{- item -}}<|"|>
|
| 104 |
+
{{- ',' if not loop.last -}}
|
| 105 |
+
{%- endfor -%}
|
| 106 |
+
],
|
| 107 |
+
{%- endif -%}
|
| 108 |
+
{%- if params['type'] -%}
|
| 109 |
+
type:<|"|>{{- params['type'] | upper -}}<|"|>}
|
| 110 |
+
{%- endif -%}
|
| 111 |
+
{%- endif -%}
|
| 112 |
+
{%- if 'response' in tool_data['function'] -%}
|
| 113 |
+
{%- set response_declaration = tool_data['function']['response'] -%}
|
| 114 |
+
,response:{
|
| 115 |
+
{%- if response_declaration['description'] -%}
|
| 116 |
+
description:<|"|>{{- response_declaration['description'] -}}<|"|>,
|
| 117 |
+
{%- endif -%}
|
| 118 |
+
{%- if response_declaration['type'] | upper == 'OBJECT' -%}
|
| 119 |
+
type:<|"|>{{- response_declaration['type'] | upper -}}<|"|>}
|
| 120 |
+
{%- endif -%}
|
| 121 |
+
{%- endif -%}
|
| 122 |
+
}
|
| 123 |
+
{%- endmacro -%}
|
| 124 |
+
{%- macro format_argument(argument, escape_keys=True) -%}
|
| 125 |
+
{%- if argument is none -%}
|
| 126 |
+
{{- 'null' -}}
|
| 127 |
+
{%- elif argument is string -%}
|
| 128 |
+
{{- '<|"|>' + argument + '<|"|>' -}}
|
| 129 |
+
{%- elif argument is boolean -%}
|
| 130 |
+
{{- 'true' if argument else 'false' -}}
|
| 131 |
+
{%- elif argument is mapping -%}
|
| 132 |
+
{{- '{' -}}
|
| 133 |
+
{%- set ns = namespace(found_first=false) -%}
|
| 134 |
+
{%- for key, value in argument | dictsort -%}
|
| 135 |
+
{%- if ns.found_first %},{% endif -%}
|
| 136 |
+
{%- set ns.found_first = true -%}
|
| 137 |
+
{%- if escape_keys -%}
|
| 138 |
+
{{- '<|"|>' + key + '<|"|>' -}}
|
| 139 |
+
{%- else -%}
|
| 140 |
+
{{- key -}}
|
| 141 |
+
{%- endif -%}
|
| 142 |
+
:{{- format_argument(value, escape_keys=escape_keys) -}}
|
| 143 |
+
{%- endfor -%}
|
| 144 |
+
{{- '}' -}}
|
| 145 |
+
{%- elif argument is sequence -%}
|
| 146 |
+
{{- '[' -}}
|
| 147 |
+
{%- for item in argument -%}
|
| 148 |
+
{{- format_argument(item, escape_keys=escape_keys) -}}
|
| 149 |
+
{%- if not loop.last %},{% endif -%}
|
| 150 |
+
{%- endfor -%}
|
| 151 |
+
{{- ']' -}}
|
| 152 |
+
{%- else -%}
|
| 153 |
+
{{- argument -}}
|
| 154 |
+
{%- endif -%}
|
| 155 |
+
{%- endmacro -%}
|
| 156 |
+
{%- macro strip_thinking(text) -%}
|
| 157 |
+
{%- set ns = namespace(result='') -%}
|
| 158 |
+
{%- for part in text.split('<channel|>') -%}
|
| 159 |
+
{%- if '<|channel>' in part -%}
|
| 160 |
+
{%- set ns.result = ns.result + part.split('<|channel>')[0] -%}
|
| 161 |
+
{%- else -%}
|
| 162 |
+
{%- set ns.result = ns.result + part -%}
|
| 163 |
+
{%- endif -%}
|
| 164 |
+
{%- endfor -%}
|
| 165 |
+
{{- ns.result | trim -}}
|
| 166 |
+
{%- endmacro -%}
|
| 167 |
+
|
| 168 |
+
{%- macro format_tool_response_block(tool_name, response) -%}
|
| 169 |
+
{{- '<|tool_response>' -}}
|
| 170 |
+
{%- if response is mapping -%}
|
| 171 |
+
{{- 'response:' + tool_name + '{' -}}
|
| 172 |
+
{%- for key, value in response | dictsort -%}
|
| 173 |
+
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
|
| 174 |
+
{%- if not loop.last %},{% endif -%}
|
| 175 |
+
{%- endfor -%}
|
| 176 |
+
{{- '}' -}}
|
| 177 |
+
{%- else -%}
|
| 178 |
+
{{- 'response:' + tool_name + '{value:' + format_argument(response, escape_keys=False) + '}' -}}
|
| 179 |
+
{%- endif -%}
|
| 180 |
+
{{- '<tool_response|>' -}}
|
| 181 |
+
{%- endmacro -%}
|
| 182 |
+
|
| 183 |
+
{#- ===== SETUP ===== -#}
|
| 184 |
+
{%- set ns = namespace(prev_message_type=None, prev_non_tool_role=None) -%}
|
| 185 |
+
{%- set loop_messages = messages -%}
|
| 186 |
+
{%- set enable_thinking = enable_thinking | default(false) -%}
|
| 187 |
+
{%- set preserve_thinking = preserve_thinking | default(false) -%}
|
| 188 |
+
{{- bos_token -}}
|
| 189 |
+
{#- Handle System/Tool Definitions Block -#}
|
| 190 |
+
{%- if enable_thinking or tools or (messages and messages[0]['role'] in ['system', 'developer']) -%}
|
| 191 |
+
{{- '<|turn>system\n' -}}
|
| 192 |
+
{#- Inject Thinking token at the very top of the FIRST system turn -#}
|
| 193 |
+
{%- if enable_thinking -%}
|
| 194 |
+
{{- '<|think|>\n' -}}
|
| 195 |
+
{%- set ns.prev_message_type = 'think' -%}
|
| 196 |
+
{%- endif -%}
|
| 197 |
+
{%- if messages and messages[0]['role'] in ['system', 'developer'] -%}
|
| 198 |
+
{%- if messages[0]['content'] is string -%}
|
| 199 |
+
{{- messages[0]['content'] | trim -}}
|
| 200 |
+
{%- elif messages[0]['content'] is sequence -%}
|
| 201 |
+
{%- for item in messages[0]['content'] -%}
|
| 202 |
+
{{- item['text'] | trim + ' '-}}
|
| 203 |
+
{%- endfor -%}
|
| 204 |
+
{%- endif -%}
|
| 205 |
+
{%- set loop_messages = messages[1:] -%}
|
| 206 |
+
{%- endif -%}
|
| 207 |
+
{%- if tools -%}
|
| 208 |
+
{%- for tool in tools %}
|
| 209 |
+
{{- '<|tool>' -}}
|
| 210 |
+
{{- format_function_declaration(tool) | trim -}}
|
| 211 |
+
{{- '<tool|>' -}}
|
| 212 |
+
{%- endfor %}
|
| 213 |
+
{%- set ns.prev_message_type = 'tool' -%}
|
| 214 |
+
{%- endif -%}
|
| 215 |
+
{{- '<turn|>\n' -}}
|
| 216 |
+
{%- endif %}
|
| 217 |
+
|
| 218 |
+
{#- Pre-scan: find last user message index for reasoning guard -#}
|
| 219 |
+
{%- set ns_turn = namespace(last_user_idx=-1) -%}
|
| 220 |
+
{%- for i in range(loop_messages | length) -%}
|
| 221 |
+
{%- if loop_messages[i]['role'] == 'user' -%}
|
| 222 |
+
{%- set ns_turn.last_user_idx = i -%}
|
| 223 |
+
{%- endif -%}
|
| 224 |
+
{%- endfor -%}
|
| 225 |
+
|
| 226 |
+
{#- Loop through messages -#}
|
| 227 |
+
{%- for message in loop_messages -%}
|
| 228 |
+
{%- if message['role'] != 'tool' -%}
|
| 229 |
+
{%- set ns.prev_message_type = None -%}
|
| 230 |
+
{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
|
| 231 |
+
{#- Detect continuation using tracked state — O(1) instead of O(n) backward scan -#}
|
| 232 |
+
{%- set continue_same_model_turn = (role == 'model' and ns.prev_non_tool_role == 'assistant') -%}
|
| 233 |
+
{%- if not continue_same_model_turn -%}
|
| 234 |
+
{{- '<|turn>' + role + '\n' }}
|
| 235 |
+
{%- endif -%}
|
| 236 |
+
|
| 237 |
+
{#- Render reasoning/reasoning_content as thinking channel -#}
|
| 238 |
+
{%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
|
| 239 |
+
{%- set thinking_gate = (loop.index0 > ns_turn.last_user_idx) or (preserve_thinking and message.get('tool_calls')) -%}
|
| 240 |
+
{%- if thinking_text and thinking_gate -%}
|
| 241 |
+
{{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
|
| 242 |
+
{%- endif -%}
|
| 243 |
+
|
| 244 |
+
{%- if message.get('tool_calls') -%}
|
| 245 |
+
{%- for tool_call in message.get('tool_calls') -%}
|
| 246 |
+
{%- set function = tool_call['function'] -%}
|
| 247 |
+
{{- '<|tool_call>call:' + function['name'] + '{' -}}
|
| 248 |
+
{%- if function['arguments'] is mapping -%}
|
| 249 |
+
{%- set ns_args = namespace(found_first=false) -%}
|
| 250 |
+
{%- for key, value in function['arguments'] | dictsort -%}
|
| 251 |
+
{%- if ns_args.found_first %},{% endif -%}
|
| 252 |
+
{%- set ns_args.found_first = true -%}
|
| 253 |
+
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
|
| 254 |
+
{%- endfor -%}
|
| 255 |
+
{%- elif function['arguments'] is none -%}
|
| 256 |
+
{%- else -%}
|
| 257 |
+
{{- raise_exception(
|
| 258 |
+
"chat_template: tool_calls[].function.arguments must be a "
|
| 259 |
+
"JSON object (mapping), not a string. Deserialize arguments "
|
| 260 |
+
"before passing to the template."
|
| 261 |
+
) -}}
|
| 262 |
+
{%- endif -%}
|
| 263 |
+
{{- '}<tool_call|>' -}}
|
| 264 |
+
{%- endfor -%}
|
| 265 |
+
{%- set ns.prev_message_type = 'tool_call' -%}
|
| 266 |
+
{%- endif -%}
|
| 267 |
+
|
| 268 |
+
{%- set ns_tr_out = namespace(flag=false) -%}
|
| 269 |
+
{%- if message.get('tool_responses') -%}
|
| 270 |
+
{#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
|
| 271 |
+
{%- for tool_response in message.get('tool_responses') -%}
|
| 272 |
+
{{- format_tool_response_block(tool_response['name'] | default('unknown', true), tool_response['response']) -}}
|
| 273 |
+
{%- set ns_tr_out.flag = true -%}
|
| 274 |
+
{%- set ns.prev_message_type = 'tool_response' -%}
|
| 275 |
+
{%- endfor -%}
|
| 276 |
+
{%- elif message.get('tool_calls') -%}
|
| 277 |
+
{#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#}
|
| 278 |
+
{%- set ns_tool_scan = namespace(stopped=false) -%}
|
| 279 |
+
{%- for k in range(loop.index0 + 1, loop_messages | length) -%}
|
| 280 |
+
{%- if ns_tool_scan.stopped -%}
|
| 281 |
+
{%- elif loop_messages[k]['role'] != 'tool' -%}
|
| 282 |
+
{%- set ns_tool_scan.stopped = true -%}
|
| 283 |
+
{%- else -%}
|
| 284 |
+
{%- set follow = loop_messages[k] -%}
|
| 285 |
+
{#- Resolve tool_call_id to function name -#}
|
| 286 |
+
{%- set ns_tname = namespace(name=follow.get('name') or 'unknown') -%}
|
| 287 |
+
{%- for tc in message.get('tool_calls') -%}
|
| 288 |
+
{%- if tc.get('id') == follow.get('tool_call_id') -%}
|
| 289 |
+
{%- set ns_tname.name = tc['function']['name'] -%}
|
| 290 |
+
{%- endif -%}
|
| 291 |
+
{%- endfor -%}
|
| 292 |
+
{#- Handle content as string or content-parts array -#}
|
| 293 |
+
{%- set tool_body = follow.get('content') -%}
|
| 294 |
+
{%- if tool_body is string -%}
|
| 295 |
+
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
|
| 296 |
+
{%- elif tool_body is sequence and tool_body is not string -%}
|
| 297 |
+
{%- set ns_txt = namespace(s='') -%}
|
| 298 |
+
{%- for part in tool_body -%}
|
| 299 |
+
{%- if part.get('type') == 'text' -%}
|
| 300 |
+
{%- set ns_txt.s = ns_txt.s + (part.get('text') | default('')) -%}
|
| 301 |
+
{%- endif -%}
|
| 302 |
+
{%- endfor -%}
|
| 303 |
+
{{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
|
| 304 |
+
{%- for part in tool_body -%}
|
| 305 |
+
{%- if part.get('type') in ['image', 'image_url'] -%}
|
| 306 |
+
{{- '<|image|>' -}}
|
| 307 |
+
{%- elif part.get('type') in ['audio', 'input_audio'] -%}
|
| 308 |
+
{{- '<|audio|>' -}}
|
| 309 |
+
{%- elif part.get('type') == 'video' -%}
|
| 310 |
+
{{- '<|video|>' -}}
|
| 311 |
+
{%- endif -%}
|
| 312 |
+
{%- endfor -%}
|
| 313 |
+
{%- else -%}
|
| 314 |
+
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
|
| 315 |
+
{%- endif -%}
|
| 316 |
+
{%- set ns_tr_out.flag = true -%}
|
| 317 |
+
{%- set ns.prev_message_type = 'tool_response' -%}
|
| 318 |
+
{%- endif -%}
|
| 319 |
+
{%- endfor -%}
|
| 320 |
+
{%- endif -%}
|
| 321 |
+
|
| 322 |
+
{%- set captured_content -%}
|
| 323 |
+
{%- if message.get('content') is string -%}
|
| 324 |
+
{%- if role == 'model' -%}
|
| 325 |
+
{{- strip_thinking(message['content']) -}}
|
| 326 |
+
{%- else -%}
|
| 327 |
+
{{- message['content'] | trim -}}
|
| 328 |
+
{%- endif -%}
|
| 329 |
+
{%- elif message.get('content') is sequence -%}
|
| 330 |
+
{%- for item in message['content'] -%}
|
| 331 |
+
{%- if item.get('type') == 'text' -%}
|
| 332 |
+
{%- if role == 'model' -%}
|
| 333 |
+
{{- strip_thinking(item['text']) -}}
|
| 334 |
+
{%- else -%}
|
| 335 |
+
{{- item['text'] | trim -}}
|
| 336 |
+
{%- endif -%}
|
| 337 |
+
{%- elif item.get('type') in ['image', 'image_url'] -%}
|
| 338 |
+
{{- '<|image|>' -}}
|
| 339 |
+
{%- elif item.get('type') in ['audio', 'input_audio'] -%}
|
| 340 |
+
{{- '<|audio|>' -}}
|
| 341 |
+
{%- elif item.get('type') == 'video' -%}
|
| 342 |
+
{{- '<|video|>' -}}
|
| 343 |
+
{%- endif -%}
|
| 344 |
+
{%- endfor -%}
|
| 345 |
+
{%- endif -%}
|
| 346 |
+
{%- endset -%}
|
| 347 |
+
|
| 348 |
+
{{- captured_content -}}
|
| 349 |
+
{%- set has_content = captured_content | trim | length > 0 -%}
|
| 350 |
+
|
| 351 |
+
{#- Forward-scan: find next non-tool message role for continuation detection -#}
|
| 352 |
+
{%- set next_nt = namespace(role=None, found=false) -%}
|
| 353 |
+
{%- for j in range(loop.index0 + 1, loop_messages | length) -%}
|
| 354 |
+
{%- if not next_nt.found -%}
|
| 355 |
+
{%- if loop_messages[j]['role'] != 'tool' -%}
|
| 356 |
+
{%- set next_nt.role = loop_messages[j]['role'] -%}
|
| 357 |
+
{%- set next_nt.found = true -%}
|
| 358 |
+
{%- endif -%}
|
| 359 |
+
{%- endif -%}
|
| 360 |
+
{%- endfor -%}
|
| 361 |
+
|
| 362 |
+
{%- set continues_into_next = (
|
| 363 |
+
role == 'model'
|
| 364 |
+
and next_nt.role == 'assistant'
|
| 365 |
+
and (not message.get('tool_calls') or ns_tr_out.flag)
|
| 366 |
+
) -%}
|
| 367 |
+
|
| 368 |
+
{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
|
| 369 |
+
{{- '<|tool_response>' -}}
|
| 370 |
+
{%- elif continues_into_next -%}
|
| 371 |
+
{%- elif not (ns_tr_out.flag and not has_content and not next_nt.found) -%}
|
| 372 |
+
{{- '<turn|>\n' -}}
|
| 373 |
+
{%- endif -%}
|
| 374 |
+
|
| 375 |
+
{#- Track previous non-tool role for next iteration (avoids O(n) backward scan) -#}
|
| 376 |
+
{%- set ns.prev_non_tool_role = message['role'] -%}
|
| 377 |
+
{%- endif -%}
|
| 378 |
+
{%- endfor -%}
|
| 379 |
+
|
| 380 |
+
{%- if add_generation_prompt -%}
|
| 381 |
+
{%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
|
| 382 |
+
{{- '<|turn>model\n' -}}
|
| 383 |
+
{%- elif ns.prev_message_type == 'tool_response' and enable_thinking -%}
|
| 384 |
+
{{- '<|channel>thought\n' -}}
|
| 385 |
+
{%- endif -%}
|
| 386 |
+
{%- endif -%}
|
config.json
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"Gemma4ForConditionalGeneration"
|
| 4 |
+
],
|
| 5 |
+
"audio_config": {
|
| 6 |
+
"_name_or_path": "",
|
| 7 |
+
"architectures": null,
|
| 8 |
+
"attention_chunk_size": 12,
|
| 9 |
+
"attention_context_left": 13,
|
| 10 |
+
"attention_context_right": 0,
|
| 11 |
+
"attention_invalid_logits_value": -1000000000.0,
|
| 12 |
+
"attention_logit_cap": 50.0,
|
| 13 |
+
"chunk_size_feed_forward": 0,
|
| 14 |
+
"conv_kernel_size": 5,
|
| 15 |
+
"dtype": "bfloat16",
|
| 16 |
+
"gradient_clipping": 10000000000.0,
|
| 17 |
+
"hidden_act": "silu",
|
| 18 |
+
"hidden_size": 1024,
|
| 19 |
+
"id2label": {
|
| 20 |
+
"0": "LABEL_0",
|
| 21 |
+
"1": "LABEL_1"
|
| 22 |
+
},
|
| 23 |
+
"initializer_range": 0.02,
|
| 24 |
+
"is_encoder_decoder": false,
|
| 25 |
+
"label2id": {
|
| 26 |
+
"LABEL_0": 0,
|
| 27 |
+
"LABEL_1": 1
|
| 28 |
+
},
|
| 29 |
+
"model_type": "gemma4_audio",
|
| 30 |
+
"num_attention_heads": 8,
|
| 31 |
+
"num_hidden_layers": 12,
|
| 32 |
+
"output_attentions": false,
|
| 33 |
+
"output_hidden_states": false,
|
| 34 |
+
"output_proj_dims": 1536,
|
| 35 |
+
"problem_type": null,
|
| 36 |
+
"residual_weight": 0.5,
|
| 37 |
+
"return_dict": true,
|
| 38 |
+
"rms_norm_eps": 1e-06,
|
| 39 |
+
"subsampling_conv_channels": [
|
| 40 |
+
128,
|
| 41 |
+
32
|
| 42 |
+
],
|
| 43 |
+
"use_clipped_linears": true
|
| 44 |
+
},
|
| 45 |
+
"audio_token_id": 258881,
|
| 46 |
+
"boa_token_id": 256000,
|
| 47 |
+
"boi_token_id": 255999,
|
| 48 |
+
"dtype": "bfloat16",
|
| 49 |
+
"eoa_token_id": 258883,
|
| 50 |
+
"eoa_token_index": 258883,
|
| 51 |
+
"eoi_token_id": 258882,
|
| 52 |
+
"eos_token_id": [
|
| 53 |
+
1,
|
| 54 |
+
106
|
| 55 |
+
],
|
| 56 |
+
"image_token_id": 258880,
|
| 57 |
+
"initializer_range": 0.02,
|
| 58 |
+
"model_type": "gemma4",
|
| 59 |
+
"text_config": {
|
| 60 |
+
"attention_bias": false,
|
| 61 |
+
"attention_dropout": 0.0,
|
| 62 |
+
"attention_k_eq_v": false,
|
| 63 |
+
"bos_token_id": 2,
|
| 64 |
+
"dtype": "bfloat16",
|
| 65 |
+
"enable_moe_block": false,
|
| 66 |
+
"eos_token_id": 1,
|
| 67 |
+
"expert_intermediate_size": null,
|
| 68 |
+
"final_logit_softcapping": 30.0,
|
| 69 |
+
"global_head_dim": 512,
|
| 70 |
+
"head_dim": 256,
|
| 71 |
+
"hidden_activation": "gelu_pytorch_tanh",
|
| 72 |
+
"hidden_size": 1536,
|
| 73 |
+
"hidden_size_per_layer_input": 256,
|
| 74 |
+
"initializer_range": 0.02,
|
| 75 |
+
"intermediate_size": 6144,
|
| 76 |
+
"layer_types": [
|
| 77 |
+
"sliding_attention",
|
| 78 |
+
"sliding_attention",
|
| 79 |
+
"sliding_attention",
|
| 80 |
+
"sliding_attention",
|
| 81 |
+
"full_attention",
|
| 82 |
+
"sliding_attention",
|
| 83 |
+
"sliding_attention",
|
| 84 |
+
"sliding_attention",
|
| 85 |
+
"sliding_attention",
|
| 86 |
+
"full_attention",
|
| 87 |
+
"sliding_attention",
|
| 88 |
+
"sliding_attention",
|
| 89 |
+
"sliding_attention",
|
| 90 |
+
"sliding_attention",
|
| 91 |
+
"full_attention",
|
| 92 |
+
"sliding_attention",
|
| 93 |
+
"sliding_attention",
|
| 94 |
+
"sliding_attention",
|
| 95 |
+
"sliding_attention",
|
| 96 |
+
"full_attention",
|
| 97 |
+
"sliding_attention",
|
| 98 |
+
"sliding_attention",
|
| 99 |
+
"sliding_attention",
|
| 100 |
+
"sliding_attention",
|
| 101 |
+
"full_attention",
|
| 102 |
+
"sliding_attention",
|
| 103 |
+
"sliding_attention",
|
| 104 |
+
"sliding_attention",
|
| 105 |
+
"sliding_attention",
|
| 106 |
+
"full_attention",
|
| 107 |
+
"sliding_attention",
|
| 108 |
+
"sliding_attention",
|
| 109 |
+
"sliding_attention",
|
| 110 |
+
"sliding_attention",
|
| 111 |
+
"full_attention"
|
| 112 |
+
],
|
| 113 |
+
"max_position_embeddings": 131072,
|
| 114 |
+
"model_type": "gemma4_text",
|
| 115 |
+
"moe_intermediate_size": null,
|
| 116 |
+
"num_attention_heads": 8,
|
| 117 |
+
"num_experts": null,
|
| 118 |
+
"num_global_key_value_heads": null,
|
| 119 |
+
"num_hidden_layers": 35,
|
| 120 |
+
"num_key_value_heads": 1,
|
| 121 |
+
"num_kv_shared_layers": 20,
|
| 122 |
+
"pad_token_id": 0,
|
| 123 |
+
"rms_norm_eps": 1e-06,
|
| 124 |
+
"rope_parameters": {
|
| 125 |
+
"full_attention": {
|
| 126 |
+
"partial_rotary_factor": 0.25,
|
| 127 |
+
"rope_theta": 1000000.0,
|
| 128 |
+
"rope_type": "proportional"
|
| 129 |
+
},
|
| 130 |
+
"sliding_attention": {
|
| 131 |
+
"rope_theta": 10000.0,
|
| 132 |
+
"rope_type": "default"
|
| 133 |
+
}
|
| 134 |
+
},
|
| 135 |
+
"sliding_window": 512,
|
| 136 |
+
"tie_word_embeddings": true,
|
| 137 |
+
"top_k_experts": null,
|
| 138 |
+
"use_bidirectional_attention": null,
|
| 139 |
+
"use_cache": true,
|
| 140 |
+
"use_double_wide_mlp": true,
|
| 141 |
+
"vocab_size": 262144,
|
| 142 |
+
"vocab_size_per_layer_input": 262144
|
| 143 |
+
},
|
| 144 |
+
"tie_word_embeddings": true,
|
| 145 |
+
"transformers_version": "5.12.1",
|
| 146 |
+
"video_token_id": 258884,
|
| 147 |
+
"vision_config": {
|
| 148 |
+
"_name_or_path": "",
|
| 149 |
+
"architectures": null,
|
| 150 |
+
"attention_bias": false,
|
| 151 |
+
"attention_dropout": 0.0,
|
| 152 |
+
"chunk_size_feed_forward": 0,
|
| 153 |
+
"default_output_length": 280,
|
| 154 |
+
"dtype": "bfloat16",
|
| 155 |
+
"global_head_dim": 64,
|
| 156 |
+
"head_dim": 64,
|
| 157 |
+
"hidden_activation": "gelu_pytorch_tanh",
|
| 158 |
+
"hidden_size": 768,
|
| 159 |
+
"id2label": {
|
| 160 |
+
"0": "LABEL_0",
|
| 161 |
+
"1": "LABEL_1"
|
| 162 |
+
},
|
| 163 |
+
"initializer_range": 0.02,
|
| 164 |
+
"intermediate_size": 3072,
|
| 165 |
+
"is_encoder_decoder": false,
|
| 166 |
+
"label2id": {
|
| 167 |
+
"LABEL_0": 0,
|
| 168 |
+
"LABEL_1": 1
|
| 169 |
+
},
|
| 170 |
+
"max_position_embeddings": 131072,
|
| 171 |
+
"model_type": "gemma4_vision",
|
| 172 |
+
"num_attention_heads": 12,
|
| 173 |
+
"num_hidden_layers": 16,
|
| 174 |
+
"num_key_value_heads": 12,
|
| 175 |
+
"output_attentions": false,
|
| 176 |
+
"output_hidden_states": false,
|
| 177 |
+
"patch_size": 16,
|
| 178 |
+
"pooling_kernel_size": 3,
|
| 179 |
+
"position_embedding_size": 10240,
|
| 180 |
+
"problem_type": null,
|
| 181 |
+
"return_dict": true,
|
| 182 |
+
"rms_norm_eps": 1e-06,
|
| 183 |
+
"rope_parameters": {
|
| 184 |
+
"rope_theta": 100.0,
|
| 185 |
+
"rope_type": "default"
|
| 186 |
+
},
|
| 187 |
+
"standardize": false,
|
| 188 |
+
"use_clipped_linears": true
|
| 189 |
+
},
|
| 190 |
+
"vision_soft_tokens_per_image": 280
|
| 191 |
+
}
|
generation_config.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bos_token_id": 2,
|
| 3 |
+
"do_sample": true,
|
| 4 |
+
"eos_token_id": [
|
| 5 |
+
1,
|
| 6 |
+
106,
|
| 7 |
+
50
|
| 8 |
+
],
|
| 9 |
+
"pad_token_id": 0,
|
| 10 |
+
"temperature": 1.0,
|
| 11 |
+
"top_k": 64,
|
| 12 |
+
"top_p": 0.95,
|
| 13 |
+
"transformers_version": "5.12.1"
|
| 14 |
+
}
|
image_overlay.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Render Miril-DroneVLM-2B-2 responses beside one image."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from PIL import Image
|
| 11 |
+
|
| 12 |
+
from inference import load_model
|
| 13 |
+
from video_overlay import find_visual_payload, infer_responses, initial_center, render
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def parse_args() -> argparse.Namespace:
|
| 17 |
+
parser = argparse.ArgumentParser()
|
| 18 |
+
parser.add_argument("--image", required=True)
|
| 19 |
+
parser.add_argument("--output", required=True)
|
| 20 |
+
parser.add_argument("--prompt", action="append", required=True)
|
| 21 |
+
parser.add_argument("--model-id", default="MirilAI/Miril-DroneVLM-2B-2")
|
| 22 |
+
parser.add_argument("--processor-id")
|
| 23 |
+
parser.add_argument("--max-new-tokens", type=int, default=384)
|
| 24 |
+
parser.add_argument("--load-4bit", action="store_true")
|
| 25 |
+
return parser.parse_args()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def main() -> int:
|
| 29 |
+
args = parse_args()
|
| 30 |
+
prompts = [prompt.strip() for prompt in args.prompt if prompt.strip()][:3]
|
| 31 |
+
if not prompts:
|
| 32 |
+
raise ValueError("at least one non-empty --prompt is required")
|
| 33 |
+
model, processor = load_model(
|
| 34 |
+
args.model_id,
|
| 35 |
+
args.processor_id or args.model_id,
|
| 36 |
+
args.load_4bit,
|
| 37 |
+
)
|
| 38 |
+
image = Image.open(args.image).convert("RGB")
|
| 39 |
+
responses = infer_responses(
|
| 40 |
+
model,
|
| 41 |
+
processor,
|
| 42 |
+
image,
|
| 43 |
+
prompts,
|
| 44 |
+
args.max_new_tokens,
|
| 45 |
+
)
|
| 46 |
+
precise, coarse = find_visual_payload(responses)
|
| 47 |
+
center = initial_center(precise, image.width, image.height)
|
| 48 |
+
rendered = render(
|
| 49 |
+
image,
|
| 50 |
+
responses,
|
| 51 |
+
tracked_center=center,
|
| 52 |
+
precise_payload=precise,
|
| 53 |
+
coarse_payload=coarse,
|
| 54 |
+
reveal_index=12,
|
| 55 |
+
)
|
| 56 |
+
output = Path(args.output)
|
| 57 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 58 |
+
rendered.save(output)
|
| 59 |
+
output.with_suffix(output.suffix + ".json").write_text(
|
| 60 |
+
json.dumps(
|
| 61 |
+
{
|
| 62 |
+
"model_id": args.model_id,
|
| 63 |
+
"image": args.image,
|
| 64 |
+
"output": str(output),
|
| 65 |
+
"responses": [
|
| 66 |
+
{
|
| 67 |
+
"prompt": response.prompt,
|
| 68 |
+
"payload": response.payload,
|
| 69 |
+
"validation_errors": response.errors,
|
| 70 |
+
}
|
| 71 |
+
for response in responses
|
| 72 |
+
],
|
| 73 |
+
},
|
| 74 |
+
indent=2,
|
| 75 |
+
ensure_ascii=False,
|
| 76 |
+
)
|
| 77 |
+
+ "\n",
|
| 78 |
+
encoding="utf-8",
|
| 79 |
+
)
|
| 80 |
+
return 0
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
if __name__ == "__main__":
|
| 84 |
+
raise SystemExit(main())
|
inference.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Single-image inference with strict Miril-DroneVLM-2B-2 response validation."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import subprocess
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
from PIL import Image
|
| 14 |
+
|
| 15 |
+
from router_contract import (
|
| 16 |
+
TYPED_JSON_ROUTER_SYSTEM_PROMPT,
|
| 17 |
+
drawable_point,
|
| 18 |
+
typed_response_errors,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def parse_args() -> argparse.Namespace:
|
| 23 |
+
parser = argparse.ArgumentParser()
|
| 24 |
+
parser.add_argument("--image", required=True)
|
| 25 |
+
query = parser.add_mutually_exclusive_group(required=True)
|
| 26 |
+
query.add_argument("--prompt")
|
| 27 |
+
query.add_argument("--audio", help="Experimental spoken-question audio file.")
|
| 28 |
+
parser.add_argument(
|
| 29 |
+
"--audio-transcript",
|
| 30 |
+
help="Optional audit/display transcript; it is not supplied to the model.",
|
| 31 |
+
)
|
| 32 |
+
parser.add_argument("--audio-sample-rate", type=int, default=16000)
|
| 33 |
+
parser.add_argument("--max-audio-seconds", type=float, default=30.0)
|
| 34 |
+
parser.add_argument("--model-id", default="MirilAI/Miril-DroneVLM-2B-2")
|
| 35 |
+
parser.add_argument("--processor-id")
|
| 36 |
+
parser.add_argument("--max-new-tokens", type=int, default=384)
|
| 37 |
+
parser.add_argument("--load-4bit", action="store_true")
|
| 38 |
+
parser.add_argument("--output-json")
|
| 39 |
+
return parser.parse_args()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def load_audio_mono_float32(
|
| 43 |
+
path: str | Path,
|
| 44 |
+
*,
|
| 45 |
+
sample_rate: int = 16000,
|
| 46 |
+
max_seconds: float = 30.0,
|
| 47 |
+
) -> Any:
|
| 48 |
+
import numpy as np
|
| 49 |
+
|
| 50 |
+
audio_path = Path(path).expanduser()
|
| 51 |
+
if not audio_path.is_file():
|
| 52 |
+
raise FileNotFoundError(f"audio file does not exist: {audio_path}")
|
| 53 |
+
if sample_rate <= 0 or max_seconds <= 0:
|
| 54 |
+
raise ValueError("audio sample rate and maximum duration must be positive")
|
| 55 |
+
process = subprocess.run(
|
| 56 |
+
[
|
| 57 |
+
"ffmpeg",
|
| 58 |
+
"-hide_banner",
|
| 59 |
+
"-loglevel",
|
| 60 |
+
"error",
|
| 61 |
+
"-i",
|
| 62 |
+
str(audio_path),
|
| 63 |
+
"-f",
|
| 64 |
+
"f32le",
|
| 65 |
+
"-acodec",
|
| 66 |
+
"pcm_f32le",
|
| 67 |
+
"-ac",
|
| 68 |
+
"1",
|
| 69 |
+
"-ar",
|
| 70 |
+
str(sample_rate),
|
| 71 |
+
"pipe:1",
|
| 72 |
+
],
|
| 73 |
+
check=True,
|
| 74 |
+
stdout=subprocess.PIPE,
|
| 75 |
+
)
|
| 76 |
+
waveform = np.frombuffer(process.stdout, dtype="<f4").copy()
|
| 77 |
+
if waveform.size == 0:
|
| 78 |
+
raise ValueError(f"audio file decoded to zero samples: {audio_path}")
|
| 79 |
+
max_samples = int(round(sample_rate * max_seconds))
|
| 80 |
+
if waveform.size > max_samples:
|
| 81 |
+
raise ValueError(
|
| 82 |
+
f"audio exceeds --max-audio-seconds ({waveform.size / sample_rate:.2f}s)"
|
| 83 |
+
)
|
| 84 |
+
return waveform
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def parse_bare_json(text: str) -> tuple[dict[str, Any] | None, list[str]]:
|
| 88 |
+
stripped = text.strip()
|
| 89 |
+
try:
|
| 90 |
+
payload = json.loads(stripped)
|
| 91 |
+
except Exception as exc:
|
| 92 |
+
return None, [f"invalid bare JSON: {exc}"]
|
| 93 |
+
if not isinstance(payload, dict):
|
| 94 |
+
return None, ["response must be a JSON object"]
|
| 95 |
+
errors = typed_response_errors(payload)
|
| 96 |
+
return (payload if not errors else None), errors
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def dispatch_summary(payload: dict[str, Any]) -> dict[str, Any]:
|
| 100 |
+
response_type = payload["type"]
|
| 101 |
+
summary: dict[str, Any] = {
|
| 102 |
+
"type": response_type,
|
| 103 |
+
"dispatch_key": response_type,
|
| 104 |
+
"text": payload.get("caption")
|
| 105 |
+
if response_type != "answer"
|
| 106 |
+
else payload["answer"],
|
| 107 |
+
"drawable": False,
|
| 108 |
+
"trackable": False,
|
| 109 |
+
}
|
| 110 |
+
if response_type == "location":
|
| 111 |
+
summary["dispatch_key"] = f"location:{payload['intent']}"
|
| 112 |
+
elif response_type == "pointing":
|
| 113 |
+
summary["dispatch_key"] = f"pointing:{payload['action']}"
|
| 114 |
+
|
| 115 |
+
point = drawable_point(payload)
|
| 116 |
+
if point is not None:
|
| 117 |
+
x, y, visual_mode = point
|
| 118 |
+
summary.update(
|
| 119 |
+
{
|
| 120 |
+
"drawable": True,
|
| 121 |
+
"visual_mode": visual_mode,
|
| 122 |
+
"normalized_xy": [x, y],
|
| 123 |
+
"trackable": visual_mode == "precise_target",
|
| 124 |
+
}
|
| 125 |
+
)
|
| 126 |
+
return summary
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def load_model(model_id: str, processor_id: str, load_4bit: bool) -> tuple[Any, Any]:
|
| 130 |
+
import torch
|
| 131 |
+
import transformers
|
| 132 |
+
from transformers import AutoProcessor
|
| 133 |
+
|
| 134 |
+
processor = AutoProcessor.from_pretrained(processor_id)
|
| 135 |
+
model_class = None
|
| 136 |
+
for class_name in (
|
| 137 |
+
"AutoModelForMultimodalLM",
|
| 138 |
+
"AutoModelForImageTextToText",
|
| 139 |
+
"AutoModelForVision2Seq",
|
| 140 |
+
):
|
| 141 |
+
model_class = getattr(transformers, class_name, None)
|
| 142 |
+
if model_class is not None:
|
| 143 |
+
break
|
| 144 |
+
if model_class is None:
|
| 145 |
+
raise RuntimeError(
|
| 146 |
+
"Installed transformers has no supported multimodal auto-model class"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
model_kwargs: dict[str, Any] = {
|
| 150 |
+
"device_map": "auto",
|
| 151 |
+
"dtype": torch.bfloat16 if torch.cuda.is_available() else torch.float32,
|
| 152 |
+
}
|
| 153 |
+
if load_4bit:
|
| 154 |
+
from transformers import BitsAndBytesConfig
|
| 155 |
+
|
| 156 |
+
model_kwargs["quantization_config"] = BitsAndBytesConfig(
|
| 157 |
+
load_in_4bit=True,
|
| 158 |
+
bnb_4bit_quant_type="nf4",
|
| 159 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 160 |
+
)
|
| 161 |
+
model = model_class.from_pretrained(model_id, **model_kwargs)
|
| 162 |
+
model.eval()
|
| 163 |
+
return model, processor
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def generate(
|
| 167 |
+
model: Any,
|
| 168 |
+
processor: Any,
|
| 169 |
+
image: Image.Image,
|
| 170 |
+
prompt: str | None,
|
| 171 |
+
*,
|
| 172 |
+
audio_waveform: Any | None = None,
|
| 173 |
+
max_new_tokens: int,
|
| 174 |
+
) -> str:
|
| 175 |
+
import torch
|
| 176 |
+
|
| 177 |
+
if (prompt is None) == (audio_waveform is None):
|
| 178 |
+
raise ValueError("provide exactly one of prompt or audio_waveform")
|
| 179 |
+
user_content: list[dict[str, Any]] = [{"type": "image", "image": image}]
|
| 180 |
+
if audio_waveform is not None:
|
| 181 |
+
user_content.append({"type": "audio", "audio": audio_waveform})
|
| 182 |
+
else:
|
| 183 |
+
user_content.append({"type": "text", "text": prompt})
|
| 184 |
+
messages = [
|
| 185 |
+
{
|
| 186 |
+
"role": "system",
|
| 187 |
+
"content": [{"type": "text", "text": TYPED_JSON_ROUTER_SYSTEM_PROMPT}],
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"role": "user",
|
| 191 |
+
"content": user_content,
|
| 192 |
+
},
|
| 193 |
+
]
|
| 194 |
+
rendered = processor.apply_chat_template(
|
| 195 |
+
messages,
|
| 196 |
+
tokenize=False,
|
| 197 |
+
add_generation_prompt=True,
|
| 198 |
+
)
|
| 199 |
+
processor_kwargs: dict[str, Any] = {
|
| 200 |
+
"text": [rendered],
|
| 201 |
+
"images": [[image]],
|
| 202 |
+
"return_tensors": "pt",
|
| 203 |
+
"padding": True,
|
| 204 |
+
}
|
| 205 |
+
if audio_waveform is not None:
|
| 206 |
+
processor_kwargs["audio"] = [audio_waveform]
|
| 207 |
+
inputs = processor(**processor_kwargs)
|
| 208 |
+
device = next(model.parameters()).device
|
| 209 |
+
inputs = inputs.to(device)
|
| 210 |
+
with torch.no_grad():
|
| 211 |
+
output = model.generate(
|
| 212 |
+
**inputs,
|
| 213 |
+
max_new_tokens=max_new_tokens,
|
| 214 |
+
do_sample=False,
|
| 215 |
+
num_beams=1,
|
| 216 |
+
)
|
| 217 |
+
generated = output[:, inputs["input_ids"].shape[-1] :]
|
| 218 |
+
return processor.batch_decode(generated, skip_special_tokens=True)[0].strip()
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def main() -> int:
|
| 222 |
+
args = parse_args()
|
| 223 |
+
if args.audio_transcript and not args.audio:
|
| 224 |
+
raise ValueError("--audio-transcript requires --audio")
|
| 225 |
+
processor_id = args.processor_id or args.model_id
|
| 226 |
+
model, processor = load_model(args.model_id, processor_id, args.load_4bit)
|
| 227 |
+
image = Image.open(args.image).convert("RGB")
|
| 228 |
+
audio_waveform = (
|
| 229 |
+
load_audio_mono_float32(
|
| 230 |
+
args.audio,
|
| 231 |
+
sample_rate=args.audio_sample_rate,
|
| 232 |
+
max_seconds=args.max_audio_seconds,
|
| 233 |
+
)
|
| 234 |
+
if args.audio
|
| 235 |
+
else None
|
| 236 |
+
)
|
| 237 |
+
raw = generate(
|
| 238 |
+
model,
|
| 239 |
+
processor,
|
| 240 |
+
image,
|
| 241 |
+
args.prompt,
|
| 242 |
+
audio_waveform=audio_waveform,
|
| 243 |
+
max_new_tokens=args.max_new_tokens,
|
| 244 |
+
)
|
| 245 |
+
payload, errors = parse_bare_json(raw)
|
| 246 |
+
result = {
|
| 247 |
+
"ok": payload is not None,
|
| 248 |
+
"model_id": args.model_id,
|
| 249 |
+
"input_modality": "image_audio" if args.audio else "image_text",
|
| 250 |
+
"prompt": args.prompt,
|
| 251 |
+
"audio_path": str(Path(args.audio).expanduser()) if args.audio else None,
|
| 252 |
+
"audio_transcript_for_audit_only": args.audio_transcript,
|
| 253 |
+
"raw": raw,
|
| 254 |
+
"payload": payload,
|
| 255 |
+
"validation_errors": errors,
|
| 256 |
+
"dispatch": dispatch_summary(payload) if payload is not None else None,
|
| 257 |
+
}
|
| 258 |
+
rendered = json.dumps(result, indent=2, ensure_ascii=False)
|
| 259 |
+
print(rendered)
|
| 260 |
+
if args.output_json:
|
| 261 |
+
output_path = Path(args.output_json)
|
| 262 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 263 |
+
output_path.write_text(rendered + "\n", encoding="utf-8")
|
| 264 |
+
if errors:
|
| 265 |
+
print(
|
| 266 |
+
"Response rejected; do not dispatch or draw coordinates.", file=sys.stderr
|
| 267 |
+
)
|
| 268 |
+
return 2
|
| 269 |
+
return 0
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
if __name__ == "__main__":
|
| 273 |
+
raise SystemExit(main())
|
mlx_inference.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Apple Silicon inference for Miril-DroneVLM-2B-2 MLX variants."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from inference import dispatch_summary, parse_bare_json
|
| 11 |
+
from router_contract import TYPED_JSON_ROUTER_SYSTEM_PROMPT
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def parse_args() -> argparse.Namespace:
|
| 15 |
+
parser = argparse.ArgumentParser()
|
| 16 |
+
parser.add_argument("--image", required=True)
|
| 17 |
+
parser.add_argument("--prompt", required=True)
|
| 18 |
+
parser.add_argument("--model-id", default="MirilAI/Miril-DroneVLM-2B-2-MLX-4bit")
|
| 19 |
+
parser.add_argument("--max-tokens", type=int, default=384)
|
| 20 |
+
parser.add_argument("--output-json")
|
| 21 |
+
return parser.parse_args()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def main() -> int:
|
| 25 |
+
from mlx_vlm import generate, load
|
| 26 |
+
|
| 27 |
+
args = parse_args()
|
| 28 |
+
model, processor = load(args.model_id, strict=True)
|
| 29 |
+
messages = [
|
| 30 |
+
{
|
| 31 |
+
"role": "system",
|
| 32 |
+
"content": [{"type": "text", "text": TYPED_JSON_ROUTER_SYSTEM_PROMPT}],
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"role": "user",
|
| 36 |
+
"content": [
|
| 37 |
+
{"type": "image"},
|
| 38 |
+
{"type": "text", "text": args.prompt},
|
| 39 |
+
],
|
| 40 |
+
},
|
| 41 |
+
]
|
| 42 |
+
rendered_prompt = processor.apply_chat_template(
|
| 43 |
+
messages, add_generation_prompt=True
|
| 44 |
+
)
|
| 45 |
+
generated = generate(
|
| 46 |
+
model,
|
| 47 |
+
processor,
|
| 48 |
+
prompt=rendered_prompt,
|
| 49 |
+
image=args.image,
|
| 50 |
+
max_tokens=args.max_tokens,
|
| 51 |
+
temperature=0.0,
|
| 52 |
+
verbose=False,
|
| 53 |
+
)
|
| 54 |
+
raw = getattr(generated, "text", str(generated)).strip()
|
| 55 |
+
payload, errors = parse_bare_json(raw)
|
| 56 |
+
result = {
|
| 57 |
+
"ok": payload is not None,
|
| 58 |
+
"model_id": args.model_id,
|
| 59 |
+
"prompt": args.prompt,
|
| 60 |
+
"raw": raw,
|
| 61 |
+
"payload": payload,
|
| 62 |
+
"validation_errors": errors,
|
| 63 |
+
"dispatch": dispatch_summary(payload) if payload is not None else None,
|
| 64 |
+
}
|
| 65 |
+
rendered = json.dumps(result, indent=2, ensure_ascii=False)
|
| 66 |
+
print(rendered)
|
| 67 |
+
if args.output_json:
|
| 68 |
+
output_path = Path(args.output_json)
|
| 69 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 70 |
+
output_path.write_text(rendered + "\n", encoding="utf-8")
|
| 71 |
+
return 0 if not errors else 2
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
raise SystemExit(main())
|
model-00001-of-00003.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:03e6df4f0e3b2c20a0234ca1911f5095407f8040ebb6a76d04dfa34104b05430
|
| 3 |
+
size 1422130776
|
model-00002-of-00003.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:18d62274c80f49cc4ce4eee3234f641b97e80d75128af376173153c97ac12f37
|
| 3 |
+
size 4987105346
|
model-00003-of-00003.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3a76df230ed93d375a03bed2549eadb1ac498c478414e4f04486157482299800
|
| 3 |
+
size 3799615716
|
model.safetensors.index.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
processor_config.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio_ms_per_token": 40,
|
| 3 |
+
"audio_seq_length": 750,
|
| 4 |
+
"feature_extractor": {
|
| 5 |
+
"dither": 0.0,
|
| 6 |
+
"feature_extractor_type": "Gemma4AudioFeatureExtractor",
|
| 7 |
+
"feature_size": 128,
|
| 8 |
+
"fft_length": 512,
|
| 9 |
+
"fft_overdrive": false,
|
| 10 |
+
"frame_length": 320,
|
| 11 |
+
"hop_length": 160,
|
| 12 |
+
"input_scale_factor": 1.0,
|
| 13 |
+
"max_frequency": 8000.0,
|
| 14 |
+
"mel_floor": 0.001,
|
| 15 |
+
"min_frequency": 0.0,
|
| 16 |
+
"padding_side": "right",
|
| 17 |
+
"padding_value": 0.0,
|
| 18 |
+
"per_bin_mean": null,
|
| 19 |
+
"per_bin_stddev": null,
|
| 20 |
+
"preemphasis": 0.0,
|
| 21 |
+
"preemphasis_htk_flavor": true,
|
| 22 |
+
"return_attention_mask": true,
|
| 23 |
+
"sampling_rate": 16000
|
| 24 |
+
},
|
| 25 |
+
"image_processor": {
|
| 26 |
+
"do_convert_rgb": true,
|
| 27 |
+
"do_normalize": false,
|
| 28 |
+
"do_rescale": true,
|
| 29 |
+
"do_resize": true,
|
| 30 |
+
"image_mean": [
|
| 31 |
+
0.0,
|
| 32 |
+
0.0,
|
| 33 |
+
0.0
|
| 34 |
+
],
|
| 35 |
+
"image_processor_type": "Gemma4ImageProcessor",
|
| 36 |
+
"image_seq_length": 280,
|
| 37 |
+
"image_std": [
|
| 38 |
+
1.0,
|
| 39 |
+
1.0,
|
| 40 |
+
1.0
|
| 41 |
+
],
|
| 42 |
+
"max_soft_tokens": 280,
|
| 43 |
+
"patch_size": 16,
|
| 44 |
+
"pooling_kernel_size": 3,
|
| 45 |
+
"resample": 3,
|
| 46 |
+
"rescale_factor": 0.00392156862745098
|
| 47 |
+
},
|
| 48 |
+
"image_seq_length": 280,
|
| 49 |
+
"processor_class": "Gemma4Processor",
|
| 50 |
+
"video_processor": {
|
| 51 |
+
"do_convert_rgb": true,
|
| 52 |
+
"do_normalize": true,
|
| 53 |
+
"do_rescale": true,
|
| 54 |
+
"do_resize": true,
|
| 55 |
+
"do_sample_frames": true,
|
| 56 |
+
"image_mean": [
|
| 57 |
+
0.0,
|
| 58 |
+
0.0,
|
| 59 |
+
0.0
|
| 60 |
+
],
|
| 61 |
+
"image_std": [
|
| 62 |
+
1.0,
|
| 63 |
+
1.0,
|
| 64 |
+
1.0
|
| 65 |
+
],
|
| 66 |
+
"max_soft_tokens": 70,
|
| 67 |
+
"num_frames": 32,
|
| 68 |
+
"patch_size": 16,
|
| 69 |
+
"pooling_kernel_size": 3,
|
| 70 |
+
"resample": 3,
|
| 71 |
+
"rescale_factor": 0.00392156862745098,
|
| 72 |
+
"return_metadata": false,
|
| 73 |
+
"video_processor_type": "Gemma4VideoProcessor"
|
| 74 |
+
}
|
| 75 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
accelerate
|
| 2 |
+
bitsandbytes
|
| 3 |
+
huggingface_hub
|
| 4 |
+
numpy
|
| 5 |
+
opencv-python-headless
|
| 6 |
+
pillow
|
| 7 |
+
torch
|
| 8 |
+
torchvision
|
| 9 |
+
transformers>=5.12.1
|
router_contract.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Immutable public inference contract for Miril-DroneVLM-2B-2."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
TYPED_RESPONSE_CONTRACT = "typed_json_router_v2"
|
| 8 |
+
|
| 9 |
+
TYPED_JSON_ROUTER_SYSTEM_PROMPT = (
|
| 10 |
+
"You are a visual assistant for overhead and drone imagery. Inspect the image, understand the "
|
| 11 |
+
"user's ordinary English request, and return exactly one valid bare JSON object. Never use "
|
| 12 |
+
"Markdown or add text outside the JSON object. Choose the response type from the result the user "
|
| 13 |
+
"is asking for, not from one keyword in isolation. "
|
| 14 |
+
'(1) Use {"type":"caption","caption":string} when the user wants a general description, '
|
| 15 |
+
"summary, caption, or open-ended read of the whole image. "
|
| 16 |
+
'(2) Use {"type":"answer","answer":string} when the requested result is words rather than one '
|
| 17 |
+
"image point. This includes counts, presence, appearance, color, activity, scene classification, "
|
| 18 |
+
"hazards, suitability, explanations, and prose descriptions of where a group or feature is. A "
|
| 19 |
+
"question about whether a named area is safe is an answer; a request to choose an area is a "
|
| 20 |
+
"location. A question containing 'where' is still an answer when it asks for a relative or general "
|
| 21 |
+
"location in words. "
|
| 22 |
+
'(3) Use type "location" with intent "landing" or "delivery" only when the user asks you to '
|
| 23 |
+
"choose an area for landing a drone or placing a delivery. Landing or delivery intent takes "
|
| 24 |
+
"priority even if the request also says show, mark, point, or pick. "
|
| 25 |
+
'(4) Use type "pointing" only when the user asks for one concrete image point. The point may '
|
| 26 |
+
"identify one visible object or instance, or it may be an explicitly requested representative "
|
| 27 |
+
"point such as the center of one visible group. Set action to "
|
| 28 |
+
'"locate" for find/where/show-location requests, "point" for '
|
| 29 |
+
'point/mark/highlight/select/pin requests, or "track" for track/follow/watch/keep-on/lock-on '
|
| 30 |
+
"requests. Look-at and focus-on requests for one target use locate. The "
|
| 31 |
+
"model supplies an initial point for track requests; a downstream application performs temporal "
|
| 32 |
+
"tracking. Requests about where several objects are concentrated or how two things are positioned "
|
| 33 |
+
"use type answer unless the user explicitly asks for one representative center point. If a point, "
|
| 34 |
+
"locate, or track request names multiple or ambiguous instances without identifying one instance "
|
| 35 |
+
"or one representative point, preserve the requested pointing action but return status "
|
| 36 |
+
"ambiguous_target with null "
|
| 37 |
+
"coordinates rather than choosing one arbitrarily. "
|
| 38 |
+
"If one request mixes output types, use this precedence: choose location for landing or delivery "
|
| 39 |
+
"selection; otherwise choose pointing when one concrete point is explicitly requested; otherwise "
|
| 40 |
+
"choose answer for a factual or verbal result; otherwise choose caption for a whole-scene read. "
|
| 41 |
+
"If the user genuinely asks for both a landing selection and a delivery selection, use the "
|
| 42 |
+
"selection requested first. If one target is assigned more than one pointing action, use track "
|
| 43 |
+
"before point and point before locate. If pointing actions name different targets, use that same "
|
| 44 |
+
"action precedence but return ambiguous_target with null coordinates rather than choosing one "
|
| 45 |
+
"target arbitrarily. "
|
| 46 |
+
"Do not invent extra keys to combine unrelated tasks. "
|
| 47 |
+
"Every location or pointing object must contain exactly type, its intent or action, caption, "
|
| 48 |
+
"coordinate_system, point_2d, point_semantics, pointing_mode, status, x, and y. Coordinates use "
|
| 49 |
+
'"gemma_relative_0_1000_yx": every non-null coordinate is a number from 0 through 1000, '
|
| 50 |
+
"point_2d is [y,x], and x and y repeat those values. Use "
|
| 51 |
+
'pointing_mode "precise_point" only for an image-grounded point. Use point_semantics '
|
| 52 |
+
'"specific_point" for a concrete target and "representative_point" only when the point represents '
|
| 53 |
+
"a broader visible target. A location response alone may use "
|
| 54 |
+
'"coarse_grid_direction" with status "coarse_direction" for a broad directional cue; x and y '
|
| 55 |
+
"must each be exactly 250, 500, or 750. A coarse cue is never a landing point, delivery point, "
|
| 56 |
+
"object point, or tracking target. When no reliable target or "
|
| 57 |
+
'direction applies, use pointing_mode "none", point_semantics "not_applicable", and null '
|
| 58 |
+
"coordinate_system, point_2d, x, and y. For location, use target_found for a selected precise "
|
| 59 |
+
"point, no_safe_area when no usable area exists, coarse_direction only for the broad grid cue "
|
| 60 |
+
"described above, or unknown when the image is insufficient. For pointing, use target_found "
|
| 61 |
+
"for one grounded target, no_target when the requested object is absent, no_matching_target when "
|
| 62 |
+
"a requested relation has no match, ambiguous_target when one target cannot be selected, or "
|
| 63 |
+
"unknown when the image is insufficient. Do not omit required keys or add extra keys."
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
COORDINATE_SYSTEM = "gemma_relative_0_1000_yx"
|
| 67 |
+
GRID_VALUES = {250.0, 500.0, 750.0}
|
| 68 |
+
LOCATION_INTENTS = {"landing", "delivery"}
|
| 69 |
+
POINTING_ACTIONS = {"locate", "point", "track"}
|
| 70 |
+
LOCATION_STATUSES = {"target_found", "coarse_direction", "no_safe_area", "unknown"}
|
| 71 |
+
POINTING_STATUSES = {
|
| 72 |
+
"target_found",
|
| 73 |
+
"no_target",
|
| 74 |
+
"no_matching_target",
|
| 75 |
+
"ambiguous_target",
|
| 76 |
+
"unknown",
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def typed_response_errors(payload: Any) -> list[str]:
|
| 81 |
+
"""Return contract violations; an empty list means the payload is dispatch-safe."""
|
| 82 |
+
|
| 83 |
+
if not isinstance(payload, dict):
|
| 84 |
+
return ["response must be a JSON object"]
|
| 85 |
+
response_type = payload.get("type")
|
| 86 |
+
if response_type == "caption":
|
| 87 |
+
return _text_response_errors(payload, text_key="caption")
|
| 88 |
+
if response_type == "answer":
|
| 89 |
+
return _text_response_errors(payload, text_key="answer")
|
| 90 |
+
if response_type == "location":
|
| 91 |
+
return _spatial_response_errors(
|
| 92 |
+
payload,
|
| 93 |
+
discriminator_key="intent",
|
| 94 |
+
discriminator_values=LOCATION_INTENTS,
|
| 95 |
+
statuses=LOCATION_STATUSES,
|
| 96 |
+
allow_coarse=True,
|
| 97 |
+
)
|
| 98 |
+
if response_type == "pointing":
|
| 99 |
+
return _spatial_response_errors(
|
| 100 |
+
payload,
|
| 101 |
+
discriminator_key="action",
|
| 102 |
+
discriminator_values=POINTING_ACTIONS,
|
| 103 |
+
statuses=POINTING_STATUSES,
|
| 104 |
+
allow_coarse=False,
|
| 105 |
+
)
|
| 106 |
+
return ["type must be caption, answer, location, or pointing"]
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _text_response_errors(payload: dict[str, Any], *, text_key: str) -> list[str]:
|
| 110 |
+
required = {"type", text_key}
|
| 111 |
+
errors: list[str] = []
|
| 112 |
+
if set(payload) != required:
|
| 113 |
+
errors.append(f"expected exactly {sorted(required)}, got {sorted(payload)}")
|
| 114 |
+
value = payload.get(text_key)
|
| 115 |
+
if not isinstance(value, str) or not value.strip():
|
| 116 |
+
errors.append(f"{text_key} must be a non-empty string")
|
| 117 |
+
return errors
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _spatial_response_errors(
|
| 121 |
+
payload: dict[str, Any],
|
| 122 |
+
*,
|
| 123 |
+
discriminator_key: str,
|
| 124 |
+
discriminator_values: set[str],
|
| 125 |
+
statuses: set[str],
|
| 126 |
+
allow_coarse: bool,
|
| 127 |
+
) -> list[str]:
|
| 128 |
+
required = {
|
| 129 |
+
"type",
|
| 130 |
+
discriminator_key,
|
| 131 |
+
"caption",
|
| 132 |
+
"coordinate_system",
|
| 133 |
+
"point_2d",
|
| 134 |
+
"point_semantics",
|
| 135 |
+
"pointing_mode",
|
| 136 |
+
"status",
|
| 137 |
+
"x",
|
| 138 |
+
"y",
|
| 139 |
+
}
|
| 140 |
+
errors: list[str] = []
|
| 141 |
+
if set(payload) != required:
|
| 142 |
+
errors.append(f"expected exactly {sorted(required)}, got {sorted(payload)}")
|
| 143 |
+
if payload.get(discriminator_key) not in discriminator_values:
|
| 144 |
+
errors.append(
|
| 145 |
+
f"{discriminator_key} must be one of {sorted(discriminator_values)}"
|
| 146 |
+
)
|
| 147 |
+
if not isinstance(payload.get("caption"), str) or not payload["caption"].strip():
|
| 148 |
+
errors.append("caption must be a non-empty string")
|
| 149 |
+
if payload.get("status") not in statuses:
|
| 150 |
+
errors.append(f"status must be one of {sorted(statuses)}")
|
| 151 |
+
|
| 152 |
+
mode = payload.get("pointing_mode")
|
| 153 |
+
if mode == "precise_point":
|
| 154 |
+
if payload.get("status") != "target_found":
|
| 155 |
+
errors.append("precise_point requires status target_found")
|
| 156 |
+
if payload.get("point_semantics") not in {
|
| 157 |
+
"specific_point",
|
| 158 |
+
"representative_point",
|
| 159 |
+
}:
|
| 160 |
+
errors.append(
|
| 161 |
+
"precise_point requires specific_point or representative_point semantics"
|
| 162 |
+
)
|
| 163 |
+
errors.extend(_coordinate_errors(payload))
|
| 164 |
+
elif mode == "coarse_grid_direction":
|
| 165 |
+
if not allow_coarse:
|
| 166 |
+
errors.append("pointing responses cannot use coarse_grid_direction")
|
| 167 |
+
if payload.get("status") != "coarse_direction":
|
| 168 |
+
errors.append("coarse_grid_direction requires status coarse_direction")
|
| 169 |
+
if payload.get("point_semantics") != "representative_point":
|
| 170 |
+
errors.append(
|
| 171 |
+
"coarse_grid_direction requires representative_point semantics"
|
| 172 |
+
)
|
| 173 |
+
errors.extend(_coordinate_errors(payload))
|
| 174 |
+
if _complete_coordinates(payload) and (
|
| 175 |
+
float(payload["x"]) not in GRID_VALUES
|
| 176 |
+
or float(payload["y"]) not in GRID_VALUES
|
| 177 |
+
):
|
| 178 |
+
errors.append("coarse coordinates must use the 250/500/750 grid")
|
| 179 |
+
elif mode == "none":
|
| 180 |
+
if payload.get("point_semantics") != "not_applicable":
|
| 181 |
+
errors.append("none requires not_applicable semantics")
|
| 182 |
+
if not _null_coordinates(payload):
|
| 183 |
+
errors.append("none requires null coordinate_system, point_2d, x, and y")
|
| 184 |
+
if payload.get("status") in {"target_found", "coarse_direction"}:
|
| 185 |
+
errors.append("none cannot use a target-bearing status")
|
| 186 |
+
else:
|
| 187 |
+
allowed = ["precise_point", "none"]
|
| 188 |
+
if allow_coarse:
|
| 189 |
+
allowed.insert(1, "coarse_grid_direction")
|
| 190 |
+
errors.append(f"pointing_mode must be one of {allowed}")
|
| 191 |
+
return errors
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def _coordinate_errors(payload: dict[str, Any]) -> list[str]:
|
| 195 |
+
errors: list[str] = []
|
| 196 |
+
if payload.get("coordinate_system") != COORDINATE_SYSTEM:
|
| 197 |
+
errors.append(f"coordinate_system must be {COORDINATE_SYSTEM}")
|
| 198 |
+
point = payload.get("point_2d")
|
| 199 |
+
x = payload.get("x")
|
| 200 |
+
y = payload.get("y")
|
| 201 |
+
if not _coordinate(x) or not _coordinate(y):
|
| 202 |
+
errors.append("x and y must be numbers from 0 through 1000")
|
| 203 |
+
if (
|
| 204 |
+
not isinstance(point, list)
|
| 205 |
+
or len(point) != 2
|
| 206 |
+
or not all(_coordinate(v) for v in point)
|
| 207 |
+
):
|
| 208 |
+
errors.append("point_2d must be [y, x] with values from 0 through 1000")
|
| 209 |
+
elif (
|
| 210 |
+
_coordinate(x)
|
| 211 |
+
and _coordinate(y)
|
| 212 |
+
and (float(point[0]) != float(y) or float(point[1]) != float(x))
|
| 213 |
+
):
|
| 214 |
+
errors.append("point_2d must exactly duplicate [y, x]")
|
| 215 |
+
return errors
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def _coordinate(value: Any) -> bool:
|
| 219 |
+
return (
|
| 220 |
+
isinstance(value, (int, float))
|
| 221 |
+
and not isinstance(value, bool)
|
| 222 |
+
and 0 <= value <= 1000
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _complete_coordinates(payload: dict[str, Any]) -> bool:
|
| 227 |
+
point = payload.get("point_2d")
|
| 228 |
+
return (
|
| 229 |
+
payload.get("coordinate_system") == COORDINATE_SYSTEM
|
| 230 |
+
and _coordinate(payload.get("x"))
|
| 231 |
+
and _coordinate(payload.get("y"))
|
| 232 |
+
and isinstance(point, list)
|
| 233 |
+
and len(point) == 2
|
| 234 |
+
and all(_coordinate(value) for value in point)
|
| 235 |
+
and float(point[0]) == float(payload["y"])
|
| 236 |
+
and float(point[1]) == float(payload["x"])
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _null_coordinates(payload: dict[str, Any]) -> bool:
|
| 241 |
+
return all(
|
| 242 |
+
payload.get(key) is None for key in ("coordinate_system", "point_2d", "x", "y")
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def drawable_point(payload: Any) -> tuple[float, float, str] | None:
|
| 247 |
+
"""Return normalized x/y plus visual mode only for a safe drawable response."""
|
| 248 |
+
|
| 249 |
+
if typed_response_errors(payload):
|
| 250 |
+
return None
|
| 251 |
+
if payload.get("type") not in {"location", "pointing"}:
|
| 252 |
+
return None
|
| 253 |
+
if payload["pointing_mode"] == "precise_point":
|
| 254 |
+
return float(payload["x"]), float(payload["y"]), "precise_target"
|
| 255 |
+
if payload["pointing_mode"] == "coarse_grid_direction":
|
| 256 |
+
return float(payload["x"]), float(payload["y"]), "coarse_direction"
|
| 257 |
+
return None
|
shared_kv_export_report.json
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"applicable": true,
|
| 3 |
+
"artifact": "hf_merged",
|
| 4 |
+
"checks": [
|
| 5 |
+
{
|
| 6 |
+
"expected_layer_ids": [
|
| 7 |
+
0,
|
| 8 |
+
1,
|
| 9 |
+
2,
|
| 10 |
+
3,
|
| 11 |
+
4,
|
| 12 |
+
5,
|
| 13 |
+
6,
|
| 14 |
+
7,
|
| 15 |
+
8,
|
| 16 |
+
9,
|
| 17 |
+
10,
|
| 18 |
+
11,
|
| 19 |
+
12,
|
| 20 |
+
13,
|
| 21 |
+
14,
|
| 22 |
+
15,
|
| 23 |
+
16,
|
| 24 |
+
17,
|
| 25 |
+
18,
|
| 26 |
+
19,
|
| 27 |
+
20,
|
| 28 |
+
21,
|
| 29 |
+
22,
|
| 30 |
+
23,
|
| 31 |
+
24,
|
| 32 |
+
25,
|
| 33 |
+
26,
|
| 34 |
+
27,
|
| 35 |
+
28,
|
| 36 |
+
29,
|
| 37 |
+
30,
|
| 38 |
+
31,
|
| 39 |
+
32,
|
| 40 |
+
33,
|
| 41 |
+
34
|
| 42 |
+
],
|
| 43 |
+
"missing_layer_ids": [],
|
| 44 |
+
"name": "language.q_proj",
|
| 45 |
+
"observed_layer_ids": [
|
| 46 |
+
0,
|
| 47 |
+
1,
|
| 48 |
+
2,
|
| 49 |
+
3,
|
| 50 |
+
4,
|
| 51 |
+
5,
|
| 52 |
+
6,
|
| 53 |
+
7,
|
| 54 |
+
8,
|
| 55 |
+
9,
|
| 56 |
+
10,
|
| 57 |
+
11,
|
| 58 |
+
12,
|
| 59 |
+
13,
|
| 60 |
+
14,
|
| 61 |
+
15,
|
| 62 |
+
16,
|
| 63 |
+
17,
|
| 64 |
+
18,
|
| 65 |
+
19,
|
| 66 |
+
20,
|
| 67 |
+
21,
|
| 68 |
+
22,
|
| 69 |
+
23,
|
| 70 |
+
24,
|
| 71 |
+
25,
|
| 72 |
+
26,
|
| 73 |
+
27,
|
| 74 |
+
28,
|
| 75 |
+
29,
|
| 76 |
+
30,
|
| 77 |
+
31,
|
| 78 |
+
32,
|
| 79 |
+
33,
|
| 80 |
+
34
|
| 81 |
+
],
|
| 82 |
+
"ok": true,
|
| 83 |
+
"unexpected_layer_ids": []
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"expected_layer_ids": [
|
| 87 |
+
0,
|
| 88 |
+
1,
|
| 89 |
+
2,
|
| 90 |
+
3,
|
| 91 |
+
4,
|
| 92 |
+
5,
|
| 93 |
+
6,
|
| 94 |
+
7,
|
| 95 |
+
8,
|
| 96 |
+
9,
|
| 97 |
+
10,
|
| 98 |
+
11,
|
| 99 |
+
12,
|
| 100 |
+
13,
|
| 101 |
+
14,
|
| 102 |
+
15,
|
| 103 |
+
16,
|
| 104 |
+
17,
|
| 105 |
+
18,
|
| 106 |
+
19,
|
| 107 |
+
20,
|
| 108 |
+
21,
|
| 109 |
+
22,
|
| 110 |
+
23,
|
| 111 |
+
24,
|
| 112 |
+
25,
|
| 113 |
+
26,
|
| 114 |
+
27,
|
| 115 |
+
28,
|
| 116 |
+
29,
|
| 117 |
+
30,
|
| 118 |
+
31,
|
| 119 |
+
32,
|
| 120 |
+
33,
|
| 121 |
+
34
|
| 122 |
+
],
|
| 123 |
+
"missing_layer_ids": [],
|
| 124 |
+
"name": "language.o_proj",
|
| 125 |
+
"observed_layer_ids": [
|
| 126 |
+
0,
|
| 127 |
+
1,
|
| 128 |
+
2,
|
| 129 |
+
3,
|
| 130 |
+
4,
|
| 131 |
+
5,
|
| 132 |
+
6,
|
| 133 |
+
7,
|
| 134 |
+
8,
|
| 135 |
+
9,
|
| 136 |
+
10,
|
| 137 |
+
11,
|
| 138 |
+
12,
|
| 139 |
+
13,
|
| 140 |
+
14,
|
| 141 |
+
15,
|
| 142 |
+
16,
|
| 143 |
+
17,
|
| 144 |
+
18,
|
| 145 |
+
19,
|
| 146 |
+
20,
|
| 147 |
+
21,
|
| 148 |
+
22,
|
| 149 |
+
23,
|
| 150 |
+
24,
|
| 151 |
+
25,
|
| 152 |
+
26,
|
| 153 |
+
27,
|
| 154 |
+
28,
|
| 155 |
+
29,
|
| 156 |
+
30,
|
| 157 |
+
31,
|
| 158 |
+
32,
|
| 159 |
+
33,
|
| 160 |
+
34
|
| 161 |
+
],
|
| 162 |
+
"ok": true,
|
| 163 |
+
"unexpected_layer_ids": []
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"expected_layer_ids": [
|
| 167 |
+
0,
|
| 168 |
+
1,
|
| 169 |
+
2,
|
| 170 |
+
3,
|
| 171 |
+
4,
|
| 172 |
+
5,
|
| 173 |
+
6,
|
| 174 |
+
7,
|
| 175 |
+
8,
|
| 176 |
+
9,
|
| 177 |
+
10,
|
| 178 |
+
11,
|
| 179 |
+
12,
|
| 180 |
+
13,
|
| 181 |
+
14,
|
| 182 |
+
15,
|
| 183 |
+
16,
|
| 184 |
+
17,
|
| 185 |
+
18,
|
| 186 |
+
19,
|
| 187 |
+
20,
|
| 188 |
+
21,
|
| 189 |
+
22,
|
| 190 |
+
23,
|
| 191 |
+
24,
|
| 192 |
+
25,
|
| 193 |
+
26,
|
| 194 |
+
27,
|
| 195 |
+
28,
|
| 196 |
+
29,
|
| 197 |
+
30,
|
| 198 |
+
31,
|
| 199 |
+
32,
|
| 200 |
+
33,
|
| 201 |
+
34
|
| 202 |
+
],
|
| 203 |
+
"missing_layer_ids": [],
|
| 204 |
+
"name": "language.q_norm",
|
| 205 |
+
"observed_layer_ids": [
|
| 206 |
+
0,
|
| 207 |
+
1,
|
| 208 |
+
2,
|
| 209 |
+
3,
|
| 210 |
+
4,
|
| 211 |
+
5,
|
| 212 |
+
6,
|
| 213 |
+
7,
|
| 214 |
+
8,
|
| 215 |
+
9,
|
| 216 |
+
10,
|
| 217 |
+
11,
|
| 218 |
+
12,
|
| 219 |
+
13,
|
| 220 |
+
14,
|
| 221 |
+
15,
|
| 222 |
+
16,
|
| 223 |
+
17,
|
| 224 |
+
18,
|
| 225 |
+
19,
|
| 226 |
+
20,
|
| 227 |
+
21,
|
| 228 |
+
22,
|
| 229 |
+
23,
|
| 230 |
+
24,
|
| 231 |
+
25,
|
| 232 |
+
26,
|
| 233 |
+
27,
|
| 234 |
+
28,
|
| 235 |
+
29,
|
| 236 |
+
30,
|
| 237 |
+
31,
|
| 238 |
+
32,
|
| 239 |
+
33,
|
| 240 |
+
34
|
| 241 |
+
],
|
| 242 |
+
"ok": true,
|
| 243 |
+
"unexpected_layer_ids": []
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"expected_layer_ids": [
|
| 247 |
+
0,
|
| 248 |
+
1,
|
| 249 |
+
2,
|
| 250 |
+
3,
|
| 251 |
+
4,
|
| 252 |
+
5,
|
| 253 |
+
6,
|
| 254 |
+
7,
|
| 255 |
+
8,
|
| 256 |
+
9,
|
| 257 |
+
10,
|
| 258 |
+
11,
|
| 259 |
+
12,
|
| 260 |
+
13,
|
| 261 |
+
14
|
| 262 |
+
],
|
| 263 |
+
"missing_layer_ids": [],
|
| 264 |
+
"name": "language.k_proj",
|
| 265 |
+
"observed_layer_ids": [
|
| 266 |
+
0,
|
| 267 |
+
1,
|
| 268 |
+
2,
|
| 269 |
+
3,
|
| 270 |
+
4,
|
| 271 |
+
5,
|
| 272 |
+
6,
|
| 273 |
+
7,
|
| 274 |
+
8,
|
| 275 |
+
9,
|
| 276 |
+
10,
|
| 277 |
+
11,
|
| 278 |
+
12,
|
| 279 |
+
13,
|
| 280 |
+
14
|
| 281 |
+
],
|
| 282 |
+
"ok": true,
|
| 283 |
+
"unexpected_layer_ids": []
|
| 284 |
+
},
|
| 285 |
+
{
|
| 286 |
+
"expected_layer_ids": [
|
| 287 |
+
0,
|
| 288 |
+
1,
|
| 289 |
+
2,
|
| 290 |
+
3,
|
| 291 |
+
4,
|
| 292 |
+
5,
|
| 293 |
+
6,
|
| 294 |
+
7,
|
| 295 |
+
8,
|
| 296 |
+
9,
|
| 297 |
+
10,
|
| 298 |
+
11,
|
| 299 |
+
12,
|
| 300 |
+
13,
|
| 301 |
+
14
|
| 302 |
+
],
|
| 303 |
+
"missing_layer_ids": [],
|
| 304 |
+
"name": "language.v_proj",
|
| 305 |
+
"observed_layer_ids": [
|
| 306 |
+
0,
|
| 307 |
+
1,
|
| 308 |
+
2,
|
| 309 |
+
3,
|
| 310 |
+
4,
|
| 311 |
+
5,
|
| 312 |
+
6,
|
| 313 |
+
7,
|
| 314 |
+
8,
|
| 315 |
+
9,
|
| 316 |
+
10,
|
| 317 |
+
11,
|
| 318 |
+
12,
|
| 319 |
+
13,
|
| 320 |
+
14
|
| 321 |
+
],
|
| 322 |
+
"ok": true,
|
| 323 |
+
"unexpected_layer_ids": []
|
| 324 |
+
},
|
| 325 |
+
{
|
| 326 |
+
"expected_layer_ids": [
|
| 327 |
+
0,
|
| 328 |
+
1,
|
| 329 |
+
2,
|
| 330 |
+
3,
|
| 331 |
+
4,
|
| 332 |
+
5,
|
| 333 |
+
6,
|
| 334 |
+
7,
|
| 335 |
+
8,
|
| 336 |
+
9,
|
| 337 |
+
10,
|
| 338 |
+
11,
|
| 339 |
+
12,
|
| 340 |
+
13,
|
| 341 |
+
14
|
| 342 |
+
],
|
| 343 |
+
"missing_layer_ids": [],
|
| 344 |
+
"name": "language.k_norm",
|
| 345 |
+
"observed_layer_ids": [
|
| 346 |
+
0,
|
| 347 |
+
1,
|
| 348 |
+
2,
|
| 349 |
+
3,
|
| 350 |
+
4,
|
| 351 |
+
5,
|
| 352 |
+
6,
|
| 353 |
+
7,
|
| 354 |
+
8,
|
| 355 |
+
9,
|
| 356 |
+
10,
|
| 357 |
+
11,
|
| 358 |
+
12,
|
| 359 |
+
13,
|
| 360 |
+
14
|
| 361 |
+
],
|
| 362 |
+
"ok": true,
|
| 363 |
+
"unexpected_layer_ids": []
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"expected_layer_ids": [
|
| 367 |
+
0,
|
| 368 |
+
1,
|
| 369 |
+
2,
|
| 370 |
+
3,
|
| 371 |
+
4,
|
| 372 |
+
5,
|
| 373 |
+
6,
|
| 374 |
+
7,
|
| 375 |
+
8,
|
| 376 |
+
9,
|
| 377 |
+
10,
|
| 378 |
+
11,
|
| 379 |
+
12,
|
| 380 |
+
13,
|
| 381 |
+
14,
|
| 382 |
+
15
|
| 383 |
+
],
|
| 384 |
+
"missing_layer_ids": [],
|
| 385 |
+
"name": "vision.k_norm",
|
| 386 |
+
"observed_layer_ids": [
|
| 387 |
+
0,
|
| 388 |
+
1,
|
| 389 |
+
2,
|
| 390 |
+
3,
|
| 391 |
+
4,
|
| 392 |
+
5,
|
| 393 |
+
6,
|
| 394 |
+
7,
|
| 395 |
+
8,
|
| 396 |
+
9,
|
| 397 |
+
10,
|
| 398 |
+
11,
|
| 399 |
+
12,
|
| 400 |
+
13,
|
| 401 |
+
14,
|
| 402 |
+
15
|
| 403 |
+
],
|
| 404 |
+
"ok": true,
|
| 405 |
+
"unexpected_layer_ids": []
|
| 406 |
+
}
|
| 407 |
+
],
|
| 408 |
+
"errors": [],
|
| 409 |
+
"expected_language_k_norm_count": 15,
|
| 410 |
+
"expected_vision_k_norm_count": 16,
|
| 411 |
+
"explanation": "Gemma 4 serializes K/V projections and k_norm only for the leading non-shared language layers. Later language layers reuse shared K/V states and must not be treated as missing tensors.",
|
| 412 |
+
"kv_owner_layer_ids": [
|
| 413 |
+
0,
|
| 414 |
+
1,
|
| 415 |
+
2,
|
| 416 |
+
3,
|
| 417 |
+
4,
|
| 418 |
+
5,
|
| 419 |
+
6,
|
| 420 |
+
7,
|
| 421 |
+
8,
|
| 422 |
+
9,
|
| 423 |
+
10,
|
| 424 |
+
11,
|
| 425 |
+
12,
|
| 426 |
+
13,
|
| 427 |
+
14
|
| 428 |
+
],
|
| 429 |
+
"kv_shared_layer_ids": [
|
| 430 |
+
15,
|
| 431 |
+
16,
|
| 432 |
+
17,
|
| 433 |
+
18,
|
| 434 |
+
19,
|
| 435 |
+
20,
|
| 436 |
+
21,
|
| 437 |
+
22,
|
| 438 |
+
23,
|
| 439 |
+
24,
|
| 440 |
+
25,
|
| 441 |
+
26,
|
| 442 |
+
27,
|
| 443 |
+
28,
|
| 444 |
+
29,
|
| 445 |
+
30,
|
| 446 |
+
31,
|
| 447 |
+
32,
|
| 448 |
+
33,
|
| 449 |
+
34
|
| 450 |
+
],
|
| 451 |
+
"model_type": "gemma4",
|
| 452 |
+
"num_hidden_layers": 35,
|
| 453 |
+
"num_kv_shared_layers": 20,
|
| 454 |
+
"ok": true,
|
| 455 |
+
"schema_version": 1,
|
| 456 |
+
"serialized_tensor_count": 1951,
|
| 457 |
+
"serialized_weight_source": "model.safetensors.index.json"
|
| 458 |
+
}
|
technical_report.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2314e40d060b4386ad1d396a2bf92e313167bbf1250cdd81bd829a2c8d620848
|
| 3 |
+
size 1266065
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cc8d3a0ce36466ccc1278bf987df5f71db1719b9ca6b4118264f45cb627bfe0f
|
| 3 |
+
size 32169626
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio_token": "<|audio|>",
|
| 3 |
+
"backend": "tokenizers",
|
| 4 |
+
"boa_token": "<|audio>",
|
| 5 |
+
"boi_token": "<|image>",
|
| 6 |
+
"bos_token": "<bos>",
|
| 7 |
+
"eoa_token": "<audio|>",
|
| 8 |
+
"eoc_token": "<channel|>",
|
| 9 |
+
"eoi_token": "<image|>",
|
| 10 |
+
"eos_token": "<eos>",
|
| 11 |
+
"eot_token": "<turn|>",
|
| 12 |
+
"escape_token": "<|\"|>",
|
| 13 |
+
"etc_token": "<tool_call|>",
|
| 14 |
+
"etd_token": "<tool|>",
|
| 15 |
+
"etr_token": "<tool_response|>",
|
| 16 |
+
"extra_special_tokens": [
|
| 17 |
+
"<|video|>"
|
| 18 |
+
],
|
| 19 |
+
"image_token": "<|image|>",
|
| 20 |
+
"is_local": false,
|
| 21 |
+
"local_files_only": false,
|
| 22 |
+
"mask_token": "<mask>",
|
| 23 |
+
"model_max_length": 1000000000000000019884624838656,
|
| 24 |
+
"model_specific_special_tokens": {
|
| 25 |
+
"audio_token": "<|audio|>",
|
| 26 |
+
"boa_token": "<|audio>",
|
| 27 |
+
"boi_token": "<|image>",
|
| 28 |
+
"eoa_token": "<audio|>",
|
| 29 |
+
"eoc_token": "<channel|>",
|
| 30 |
+
"eoi_token": "<image|>",
|
| 31 |
+
"eot_token": "<turn|>",
|
| 32 |
+
"escape_token": "<|\"|>",
|
| 33 |
+
"etc_token": "<tool_call|>",
|
| 34 |
+
"etd_token": "<tool|>",
|
| 35 |
+
"etr_token": "<tool_response|>",
|
| 36 |
+
"image_token": "<|image|>",
|
| 37 |
+
"soc_token": "<|channel>",
|
| 38 |
+
"sot_token": "<|turn>",
|
| 39 |
+
"stc_token": "<|tool_call>",
|
| 40 |
+
"std_token": "<|tool>",
|
| 41 |
+
"str_token": "<|tool_response>",
|
| 42 |
+
"think_token": "<|think|>"
|
| 43 |
+
},
|
| 44 |
+
"pad_token": "<pad>",
|
| 45 |
+
"padding_side": "left",
|
| 46 |
+
"processor_class": "Gemma4Processor",
|
| 47 |
+
"response_schema": {
|
| 48 |
+
"properties": {
|
| 49 |
+
"content": {
|
| 50 |
+
"type": "string"
|
| 51 |
+
},
|
| 52 |
+
"role": {
|
| 53 |
+
"const": "assistant"
|
| 54 |
+
},
|
| 55 |
+
"thinking": {
|
| 56 |
+
"type": "string"
|
| 57 |
+
},
|
| 58 |
+
"tool_calls": {
|
| 59 |
+
"items": {
|
| 60 |
+
"properties": {
|
| 61 |
+
"function": {
|
| 62 |
+
"properties": {
|
| 63 |
+
"arguments": {
|
| 64 |
+
"additionalProperties": {},
|
| 65 |
+
"type": "object",
|
| 66 |
+
"x-parser": "gemma4-tool-call"
|
| 67 |
+
},
|
| 68 |
+
"name": {
|
| 69 |
+
"type": "string"
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
"type": "object",
|
| 73 |
+
"x-regex": "call\\:(?P<name>\\w+)(?P<arguments>\\{.*\\})"
|
| 74 |
+
},
|
| 75 |
+
"type": {
|
| 76 |
+
"const": "function"
|
| 77 |
+
}
|
| 78 |
+
},
|
| 79 |
+
"type": "object"
|
| 80 |
+
},
|
| 81 |
+
"type": "array",
|
| 82 |
+
"x-regex-iterator": "<\\|tool_call>(.*?)<tool_call\\|>"
|
| 83 |
+
}
|
| 84 |
+
},
|
| 85 |
+
"type": "object",
|
| 86 |
+
"x-regex": "(\\<\\|channel\\>thought\\n(?P<thinking>.*?)\\<channel\\|\\>)?(?P<tool_calls>\\<\\|tool_call\\>.*\\<tool_call\\|\\>)?(?P<content>(?:(?!\\<turn\\|\\>)(?!\\<\\|tool_response\\>).)+)?(?:\\<turn\\|\\>|\\<\\|tool_response\\>)?"
|
| 87 |
+
},
|
| 88 |
+
"response_template": {
|
| 89 |
+
"defaults": {
|
| 90 |
+
"role": "assistant"
|
| 91 |
+
},
|
| 92 |
+
"fields": {
|
| 93 |
+
"content": {
|
| 94 |
+
"close": [
|
| 95 |
+
"<turn|>",
|
| 96 |
+
"<|tool_response>",
|
| 97 |
+
"<eos>"
|
| 98 |
+
],
|
| 99 |
+
"content": "text"
|
| 100 |
+
},
|
| 101 |
+
"thinking": {
|
| 102 |
+
"close": "<channel|>",
|
| 103 |
+
"content": "text",
|
| 104 |
+
"open": "<|channel>thought\n"
|
| 105 |
+
},
|
| 106 |
+
"tool_calls": {
|
| 107 |
+
"close": "<tool_call|>",
|
| 108 |
+
"content": "json",
|
| 109 |
+
"content_args": {
|
| 110 |
+
"string_delims": [
|
| 111 |
+
[
|
| 112 |
+
"<|\"|>",
|
| 113 |
+
"<|\"|>"
|
| 114 |
+
]
|
| 115 |
+
],
|
| 116 |
+
"unquoted_keys": true
|
| 117 |
+
},
|
| 118 |
+
"open_pattern": "<\\|tool_call>call:(?P<name>\\w+)",
|
| 119 |
+
"repeats": true,
|
| 120 |
+
"transform": {
|
| 121 |
+
"function": {
|
| 122 |
+
"arguments": "{content}",
|
| 123 |
+
"name": "{name}"
|
| 124 |
+
},
|
| 125 |
+
"type": "function"
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
},
|
| 129 |
+
"start_anchor": [
|
| 130 |
+
"<|turn>model\n",
|
| 131 |
+
"<tool_response|>"
|
| 132 |
+
]
|
| 133 |
+
},
|
| 134 |
+
"soc_token": "<|channel>",
|
| 135 |
+
"sot_token": "<|turn>",
|
| 136 |
+
"stc_token": "<|tool_call>",
|
| 137 |
+
"std_token": "<|tool>",
|
| 138 |
+
"str_token": "<|tool_response>",
|
| 139 |
+
"think_token": "<|think|>",
|
| 140 |
+
"tokenizer_class": "GemmaTokenizer",
|
| 141 |
+
"unk_token": "<unk>"
|
| 142 |
+
}
|
video_overlay.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Render Miril-DroneVLM-2B-2 JSON over a video with safe pointing and OpenCV tracking."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import shutil
|
| 9 |
+
import subprocess
|
| 10 |
+
import tempfile
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
import cv2
|
| 16 |
+
import numpy as np
|
| 17 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 18 |
+
|
| 19 |
+
from inference import generate, load_model, parse_bare_json
|
| 20 |
+
from router_contract import drawable_point
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
GREEN = (42, 255, 128)
|
| 24 |
+
WHITE = (232, 238, 235)
|
| 25 |
+
PANEL = (7, 10, 11)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class Response:
|
| 30 |
+
prompt: str
|
| 31 |
+
label: str
|
| 32 |
+
text: str
|
| 33 |
+
payload: dict[str, Any] | None
|
| 34 |
+
errors: list[str]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def parse_args() -> argparse.Namespace:
|
| 38 |
+
parser = argparse.ArgumentParser()
|
| 39 |
+
parser.add_argument("--input", required=True)
|
| 40 |
+
parser.add_argument("--output", required=True)
|
| 41 |
+
parser.add_argument("--prompt", action="append", required=True)
|
| 42 |
+
parser.add_argument("--model-id", default="MirilAI/Miril-DroneVLM-2B-2")
|
| 43 |
+
parser.add_argument("--processor-id")
|
| 44 |
+
parser.add_argument("--interval-seconds", type=float, default=5.0)
|
| 45 |
+
parser.add_argument(
|
| 46 |
+
"--crop", choices=("native", "center-square"), default="center-square"
|
| 47 |
+
)
|
| 48 |
+
parser.add_argument("--crop-size", type=int, default=1000)
|
| 49 |
+
parser.add_argument("--max-seconds", type=float, default=0.0)
|
| 50 |
+
parser.add_argument("--max-new-tokens", type=int, default=384)
|
| 51 |
+
parser.add_argument("--load-4bit", action="store_true")
|
| 52 |
+
return parser.parse_args()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def process_frame(frame: np.ndarray, crop: str, crop_size: int) -> Image.Image:
|
| 56 |
+
image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
| 57 |
+
if crop == "native":
|
| 58 |
+
return image
|
| 59 |
+
width, height = image.size
|
| 60 |
+
side = min(width, height)
|
| 61 |
+
left = (width - side) // 2
|
| 62 |
+
top = (height - side) // 2
|
| 63 |
+
image = image.crop((left, top, left + side, top + side))
|
| 64 |
+
return image.resize((crop_size, crop_size), Image.Resampling.LANCZOS)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def response_label(payload: dict[str, Any] | None, index: int) -> str:
|
| 68 |
+
if not payload:
|
| 69 |
+
return f"Prompt {index}"
|
| 70 |
+
response_type = payload.get("type")
|
| 71 |
+
if response_type == "caption":
|
| 72 |
+
return "Scene"
|
| 73 |
+
if response_type == "answer":
|
| 74 |
+
return "Answer"
|
| 75 |
+
if response_type == "location":
|
| 76 |
+
return f"Location/{payload.get('intent', 'unknown')}"
|
| 77 |
+
if response_type == "pointing":
|
| 78 |
+
return f"Pointing/{payload.get('action', 'unknown')}"
|
| 79 |
+
return f"Prompt {index}"
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def response_text(raw: str, payload: dict[str, Any] | None, errors: list[str]) -> str:
|
| 83 |
+
if errors or payload is None:
|
| 84 |
+
return f"REJECTED: {'; '.join(errors) or 'invalid JSON'}"
|
| 85 |
+
response_type = payload["type"]
|
| 86 |
+
if response_type == "caption":
|
| 87 |
+
return payload["caption"]
|
| 88 |
+
if response_type == "answer":
|
| 89 |
+
return payload["answer"]
|
| 90 |
+
point = drawable_point(payload)
|
| 91 |
+
if point is None:
|
| 92 |
+
return f"{payload['status']} / no point: {payload['caption']}"
|
| 93 |
+
x, y, mode = point
|
| 94 |
+
mode_text = "precise target" if mode == "precise_target" else "broad direction only"
|
| 95 |
+
return f"{payload['status']} / {mode_text} / yx=[{y:.0f}, {x:.0f}]: {payload['caption']}"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def infer_responses(
|
| 99 |
+
model: Any,
|
| 100 |
+
processor: Any,
|
| 101 |
+
image: Image.Image,
|
| 102 |
+
prompts: list[str],
|
| 103 |
+
max_new_tokens: int,
|
| 104 |
+
) -> list[Response]:
|
| 105 |
+
responses: list[Response] = []
|
| 106 |
+
for index, prompt in enumerate(prompts, start=1):
|
| 107 |
+
raw = generate(
|
| 108 |
+
model,
|
| 109 |
+
processor,
|
| 110 |
+
image,
|
| 111 |
+
prompt,
|
| 112 |
+
max_new_tokens=max_new_tokens,
|
| 113 |
+
)
|
| 114 |
+
payload, errors = parse_bare_json(raw)
|
| 115 |
+
responses.append(
|
| 116 |
+
Response(
|
| 117 |
+
prompt=prompt,
|
| 118 |
+
label=response_label(payload, index),
|
| 119 |
+
text=response_text(raw, payload, errors),
|
| 120 |
+
payload=payload,
|
| 121 |
+
errors=errors,
|
| 122 |
+
)
|
| 123 |
+
)
|
| 124 |
+
return responses
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def find_visual_payload(
|
| 128 |
+
responses: list[Response],
|
| 129 |
+
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
| 130 |
+
precise = None
|
| 131 |
+
coarse = None
|
| 132 |
+
for response in responses:
|
| 133 |
+
point = drawable_point(response.payload)
|
| 134 |
+
if point is None:
|
| 135 |
+
continue
|
| 136 |
+
if point[2] == "precise_target" and precise is None:
|
| 137 |
+
precise = response.payload
|
| 138 |
+
elif point[2] == "coarse_direction" and coarse is None:
|
| 139 |
+
coarse = response.payload
|
| 140 |
+
return precise, coarse
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def font(size: int, bold: bool = False) -> ImageFont.ImageFont:
|
| 144 |
+
names = (
|
| 145 |
+
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"
|
| 146 |
+
if bold
|
| 147 |
+
else "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
| 148 |
+
"/Library/Fonts/Arial Bold.ttf" if bold else "/Library/Fonts/Arial.ttf",
|
| 149 |
+
)
|
| 150 |
+
for name in names:
|
| 151 |
+
try:
|
| 152 |
+
return ImageFont.truetype(name, size)
|
| 153 |
+
except Exception:
|
| 154 |
+
pass
|
| 155 |
+
return ImageFont.load_default()
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def wrap(
|
| 159 |
+
draw: ImageDraw.ImageDraw, text: str, face: ImageFont.ImageFont, width: int
|
| 160 |
+
) -> list[str]:
|
| 161 |
+
words = text.split()
|
| 162 |
+
if not words:
|
| 163 |
+
return [""]
|
| 164 |
+
lines: list[str] = []
|
| 165 |
+
current = words[0]
|
| 166 |
+
for word in words[1:]:
|
| 167 |
+
candidate = f"{current} {word}"
|
| 168 |
+
if draw.textbbox((0, 0), candidate, font=face)[2] <= width:
|
| 169 |
+
current = candidate
|
| 170 |
+
else:
|
| 171 |
+
lines.append(current)
|
| 172 |
+
current = word
|
| 173 |
+
lines.append(current)
|
| 174 |
+
return lines
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def visible_text(text: str, reveal_index: int) -> str:
|
| 178 |
+
if reveal_index >= 12:
|
| 179 |
+
return text
|
| 180 |
+
words = text.split()
|
| 181 |
+
keep = max(1, (len(words) * (reveal_index + 1) + 11) // 12)
|
| 182 |
+
return " ".join(words[:keep])
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def layout_lines(
|
| 186 |
+
draw: ImageDraw.ImageDraw,
|
| 187 |
+
responses: list[Response],
|
| 188 |
+
panel_width: int,
|
| 189 |
+
height: int,
|
| 190 |
+
) -> tuple[ImageFont.ImageFont, ImageFont.ImageFont, int]:
|
| 191 |
+
available_width = panel_width - 36
|
| 192 |
+
for size in range(19, 8, -1):
|
| 193 |
+
regular = font(size)
|
| 194 |
+
bold = font(size, bold=True)
|
| 195 |
+
line_height = size + 6
|
| 196 |
+
line_count = 0
|
| 197 |
+
for response in responses:
|
| 198 |
+
line_count += len(
|
| 199 |
+
wrap(
|
| 200 |
+
draw,
|
| 201 |
+
f"{response.label} query: {response.prompt}",
|
| 202 |
+
regular,
|
| 203 |
+
available_width,
|
| 204 |
+
)
|
| 205 |
+
)
|
| 206 |
+
line_count += len(
|
| 207 |
+
wrap(
|
| 208 |
+
draw, f"{response.label}: {response.text}", regular, available_width
|
| 209 |
+
)
|
| 210 |
+
)
|
| 211 |
+
line_count += 1
|
| 212 |
+
if line_count * line_height <= height - 70:
|
| 213 |
+
return regular, bold, line_height
|
| 214 |
+
return font(9), font(9, bold=True), 15
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def draw_marker(
|
| 218 |
+
draw: ImageDraw.ImageDraw,
|
| 219 |
+
payload: dict[str, Any],
|
| 220 |
+
width: int,
|
| 221 |
+
height: int,
|
| 222 |
+
*,
|
| 223 |
+
tracked_center: tuple[float, float] | None,
|
| 224 |
+
) -> None:
|
| 225 |
+
point = drawable_point(payload)
|
| 226 |
+
if point is None:
|
| 227 |
+
return
|
| 228 |
+
x, y, mode = point
|
| 229 |
+
px = int(round(x / 1000.0 * width))
|
| 230 |
+
py = int(round(y / 1000.0 * height))
|
| 231 |
+
if mode == "precise_target" and tracked_center is not None:
|
| 232 |
+
px, py = map(lambda value: int(round(value)), tracked_center)
|
| 233 |
+
if mode == "coarse_direction":
|
| 234 |
+
radius = max(40, min(width, height) // 11)
|
| 235 |
+
draw.ellipse(
|
| 236 |
+
(px - radius, py - radius, px + radius, py + radius),
|
| 237 |
+
fill=(0, 230, 120, 34),
|
| 238 |
+
outline=(0, 230, 120, 225),
|
| 239 |
+
width=3,
|
| 240 |
+
)
|
| 241 |
+
return
|
| 242 |
+
half = 25
|
| 243 |
+
radius = 31
|
| 244 |
+
draw.ellipse(
|
| 245 |
+
(px - radius, py - radius, px + radius, py + radius),
|
| 246 |
+
fill=(0, 230, 120, 40),
|
| 247 |
+
outline=(0, 230, 120, 230),
|
| 248 |
+
width=2,
|
| 249 |
+
)
|
| 250 |
+
draw.rectangle(
|
| 251 |
+
(px - half, py - half, px + half, py + half), outline=(0, 230, 120), width=2
|
| 252 |
+
)
|
| 253 |
+
draw.line((px - 10, py, px + 10, py), fill=(0, 230, 120), width=2)
|
| 254 |
+
draw.line((px, py - 10, px, py + 10), fill=(0, 230, 120), width=2)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def render(
|
| 258 |
+
image: Image.Image,
|
| 259 |
+
responses: list[Response],
|
| 260 |
+
*,
|
| 261 |
+
tracked_center: tuple[float, float] | None,
|
| 262 |
+
precise_payload: dict[str, Any] | None,
|
| 263 |
+
coarse_payload: dict[str, Any] | None,
|
| 264 |
+
reveal_index: int,
|
| 265 |
+
) -> Image.Image:
|
| 266 |
+
width, height = image.size
|
| 267 |
+
panel_width = width // 2
|
| 268 |
+
canvas = Image.new("RGB", (width + panel_width, height), PANEL)
|
| 269 |
+
layer = Image.new("RGBA", image.size, (0, 0, 0, 0))
|
| 270 |
+
marker_draw = ImageDraw.Draw(layer)
|
| 271 |
+
if precise_payload is not None and tracked_center is not None:
|
| 272 |
+
draw_marker(
|
| 273 |
+
marker_draw, precise_payload, width, height, tracked_center=tracked_center
|
| 274 |
+
)
|
| 275 |
+
elif coarse_payload is not None and reveal_index < 12:
|
| 276 |
+
draw_marker(marker_draw, coarse_payload, width, height, tracked_center=None)
|
| 277 |
+
canvas.paste(
|
| 278 |
+
Image.alpha_composite(image.convert("RGBA"), layer).convert("RGB"), (0, 0)
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
draw = ImageDraw.Draw(canvas)
|
| 282 |
+
regular, bold, line_height = layout_lines(draw, responses, panel_width, height)
|
| 283 |
+
max_width = panel_width - 36
|
| 284 |
+
y = 18
|
| 285 |
+
overflow = False
|
| 286 |
+
for response in responses:
|
| 287 |
+
blocks = (
|
| 288 |
+
(f"{response.label} query:", response.prompt),
|
| 289 |
+
(f"{response.label}:", visible_text(response.text, reveal_index)),
|
| 290 |
+
)
|
| 291 |
+
for prefix, body in blocks:
|
| 292 |
+
first = True
|
| 293 |
+
for line in wrap(draw, f"{prefix} {body}", regular, max_width):
|
| 294 |
+
if y + line_height > height - 48:
|
| 295 |
+
draw.text((width + 18, y), "...", fill=WHITE, font=regular)
|
| 296 |
+
overflow = True
|
| 297 |
+
break
|
| 298 |
+
if first and line.startswith(prefix):
|
| 299 |
+
draw.text((width + 18, y), prefix, fill=GREEN, font=bold)
|
| 300 |
+
prefix_width = draw.textbbox((0, 0), prefix, font=bold)[2]
|
| 301 |
+
draw.text(
|
| 302 |
+
(width + 22 + prefix_width, y),
|
| 303 |
+
line[len(prefix) :].lstrip(),
|
| 304 |
+
fill=WHITE,
|
| 305 |
+
font=regular,
|
| 306 |
+
)
|
| 307 |
+
else:
|
| 308 |
+
draw.text((width + 18, y), line, fill=WHITE, font=regular)
|
| 309 |
+
first = False
|
| 310 |
+
y += line_height
|
| 311 |
+
if overflow:
|
| 312 |
+
break
|
| 313 |
+
if overflow:
|
| 314 |
+
break
|
| 315 |
+
y += line_height
|
| 316 |
+
brand = "Miril-DroneVLM-2B-2 / miril.ai"
|
| 317 |
+
small = font(max(10, min(15, height // 55)))
|
| 318 |
+
brand_width = draw.textbbox((0, 0), brand, font=small)[2]
|
| 319 |
+
draw.text(
|
| 320 |
+
(width + panel_width - brand_width - 18, height - 30),
|
| 321 |
+
brand,
|
| 322 |
+
fill=WHITE,
|
| 323 |
+
font=small,
|
| 324 |
+
)
|
| 325 |
+
return canvas
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def initial_center(
|
| 329 |
+
payload: dict[str, Any] | None, width: int, height: int
|
| 330 |
+
) -> tuple[float, float] | None:
|
| 331 |
+
point = drawable_point(payload)
|
| 332 |
+
if point is None or point[2] != "precise_target":
|
| 333 |
+
return None
|
| 334 |
+
center = (point[0] / 1000.0 * width, point[1] / 1000.0 * height)
|
| 335 |
+
return center if inside_margin(center, width, height) else None
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def inside_margin(
|
| 339 |
+
center: tuple[float, float], width: int, height: int, margin: int = 50
|
| 340 |
+
) -> bool:
|
| 341 |
+
return margin < center[0] < width - margin and margin < center[1] < height - margin
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def track_center(
|
| 345 |
+
previous: Image.Image,
|
| 346 |
+
current: Image.Image,
|
| 347 |
+
center: tuple[float, float],
|
| 348 |
+
) -> tuple[float, float] | None:
|
| 349 |
+
width, height = current.size
|
| 350 |
+
if not inside_margin(center, width, height):
|
| 351 |
+
return None
|
| 352 |
+
half = 50
|
| 353 |
+
left = int(round(center[0])) - half
|
| 354 |
+
top = int(round(center[1])) - half
|
| 355 |
+
right = left + 100
|
| 356 |
+
bottom = top + 100
|
| 357 |
+
if left < 0 or top < 0 or right > width or bottom > height:
|
| 358 |
+
return None
|
| 359 |
+
points = np.asarray(
|
| 360 |
+
[
|
| 361 |
+
[x, y]
|
| 362 |
+
for y in np.linspace(top + 12, bottom - 12, 4)
|
| 363 |
+
for x in np.linspace(left + 12, right - 12, 4)
|
| 364 |
+
],
|
| 365 |
+
dtype=np.float32,
|
| 366 |
+
).reshape(-1, 1, 2)
|
| 367 |
+
previous_gray = cv2.cvtColor(np.asarray(previous), cv2.COLOR_RGB2GRAY)
|
| 368 |
+
current_gray = cv2.cvtColor(np.asarray(current), cv2.COLOR_RGB2GRAY)
|
| 369 |
+
moved, status, _ = cv2.calcOpticalFlowPyrLK(
|
| 370 |
+
previous_gray, current_gray, points, None
|
| 371 |
+
)
|
| 372 |
+
if moved is None or status is None:
|
| 373 |
+
return None
|
| 374 |
+
mask = status.reshape(-1) == 1
|
| 375 |
+
if int(mask.sum()) < 4:
|
| 376 |
+
return None
|
| 377 |
+
delta = np.median(moved[mask].reshape(-1, 2) - points[mask].reshape(-1, 2), axis=0)
|
| 378 |
+
updated = (float(center[0] + delta[0]), float(center[1] + delta[1]))
|
| 379 |
+
return updated if inside_margin(updated, width, height) else None
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def mux_h264(silent_path: Path, source_path: Path, output_path: Path) -> None:
|
| 383 |
+
ffmpeg = shutil.which("ffmpeg")
|
| 384 |
+
if ffmpeg is None:
|
| 385 |
+
shutil.move(silent_path, output_path)
|
| 386 |
+
return
|
| 387 |
+
subprocess.run(
|
| 388 |
+
[
|
| 389 |
+
ffmpeg,
|
| 390 |
+
"-y",
|
| 391 |
+
"-i",
|
| 392 |
+
str(silent_path),
|
| 393 |
+
"-i",
|
| 394 |
+
str(source_path),
|
| 395 |
+
"-map",
|
| 396 |
+
"0:v:0",
|
| 397 |
+
"-map",
|
| 398 |
+
"1:a?",
|
| 399 |
+
"-c:v",
|
| 400 |
+
"libx264",
|
| 401 |
+
"-crf",
|
| 402 |
+
"20",
|
| 403 |
+
"-preset",
|
| 404 |
+
"medium",
|
| 405 |
+
"-c:a",
|
| 406 |
+
"aac",
|
| 407 |
+
"-shortest",
|
| 408 |
+
str(output_path),
|
| 409 |
+
],
|
| 410 |
+
check=True,
|
| 411 |
+
stdout=subprocess.DEVNULL,
|
| 412 |
+
stderr=subprocess.DEVNULL,
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def main() -> int:
|
| 417 |
+
args = parse_args()
|
| 418 |
+
prompts = [prompt.strip() for prompt in args.prompt if prompt.strip()][:3]
|
| 419 |
+
if not prompts:
|
| 420 |
+
raise ValueError("at least one non-empty --prompt is required")
|
| 421 |
+
if args.interval_seconds < 1 or args.interval_seconds > 10:
|
| 422 |
+
raise ValueError("--interval-seconds must be from 1 through 10")
|
| 423 |
+
source_path = Path(args.input)
|
| 424 |
+
output_path = Path(args.output)
|
| 425 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 426 |
+
model, processor = load_model(
|
| 427 |
+
args.model_id,
|
| 428 |
+
args.processor_id or args.model_id,
|
| 429 |
+
args.load_4bit,
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
capture = cv2.VideoCapture(str(source_path))
|
| 433 |
+
if not capture.isOpened():
|
| 434 |
+
raise ValueError(f"could not open {source_path}")
|
| 435 |
+
fps = float(capture.get(cv2.CAP_PROP_FPS) or 24.0)
|
| 436 |
+
max_frames = int(args.max_seconds * fps) if args.max_seconds > 0 else None
|
| 437 |
+
interval_frames = max(1, int(round(args.interval_seconds * fps)))
|
| 438 |
+
temporary_dir = Path(tempfile.mkdtemp(prefix="miril_drone_overlay_"))
|
| 439 |
+
silent_path = temporary_dir / "silent.mp4"
|
| 440 |
+
writer: Any = None
|
| 441 |
+
previous: Image.Image | None = None
|
| 442 |
+
responses: list[Response] = []
|
| 443 |
+
precise_payload = None
|
| 444 |
+
coarse_payload = None
|
| 445 |
+
center = None
|
| 446 |
+
segment_index = 0
|
| 447 |
+
frame_index = 0
|
| 448 |
+
sidecar_samples: list[dict[str, Any]] = []
|
| 449 |
+
|
| 450 |
+
while max_frames is None or frame_index < max_frames:
|
| 451 |
+
ok, frame = capture.read()
|
| 452 |
+
if not ok:
|
| 453 |
+
break
|
| 454 |
+
image = process_frame(frame, args.crop, args.crop_size)
|
| 455 |
+
if frame_index % interval_frames == 0 or not responses:
|
| 456 |
+
responses = infer_responses(
|
| 457 |
+
model,
|
| 458 |
+
processor,
|
| 459 |
+
image,
|
| 460 |
+
prompts,
|
| 461 |
+
args.max_new_tokens,
|
| 462 |
+
)
|
| 463 |
+
precise_payload, coarse_payload = find_visual_payload(responses)
|
| 464 |
+
center = initial_center(precise_payload, image.width, image.height)
|
| 465 |
+
segment_index = 0
|
| 466 |
+
sidecar_samples.append(
|
| 467 |
+
{
|
| 468 |
+
"timestamp_s": round(frame_index / fps, 3),
|
| 469 |
+
"responses": [
|
| 470 |
+
{
|
| 471 |
+
"prompt": response.prompt,
|
| 472 |
+
"payload": response.payload,
|
| 473 |
+
"validation_errors": response.errors,
|
| 474 |
+
}
|
| 475 |
+
for response in responses
|
| 476 |
+
],
|
| 477 |
+
}
|
| 478 |
+
)
|
| 479 |
+
elif center is not None and previous is not None:
|
| 480 |
+
center = track_center(previous, image, center)
|
| 481 |
+
|
| 482 |
+
rendered = render(
|
| 483 |
+
image,
|
| 484 |
+
responses,
|
| 485 |
+
tracked_center=center,
|
| 486 |
+
precise_payload=precise_payload,
|
| 487 |
+
coarse_payload=coarse_payload,
|
| 488 |
+
reveal_index=segment_index,
|
| 489 |
+
)
|
| 490 |
+
if writer is None:
|
| 491 |
+
writer = cv2.VideoWriter(
|
| 492 |
+
str(silent_path),
|
| 493 |
+
cv2.VideoWriter_fourcc(*"mp4v"),
|
| 494 |
+
fps,
|
| 495 |
+
rendered.size,
|
| 496 |
+
)
|
| 497 |
+
writer.write(cv2.cvtColor(np.asarray(rendered), cv2.COLOR_RGB2BGR))
|
| 498 |
+
previous = image
|
| 499 |
+
segment_index += 1
|
| 500 |
+
frame_index += 1
|
| 501 |
+
|
| 502 |
+
capture.release()
|
| 503 |
+
if writer is not None:
|
| 504 |
+
writer.release()
|
| 505 |
+
if frame_index == 0:
|
| 506 |
+
raise ValueError("no frames were rendered")
|
| 507 |
+
mux_h264(silent_path, source_path, output_path)
|
| 508 |
+
sidecar = {
|
| 509 |
+
"model_id": args.model_id,
|
| 510 |
+
"input": str(source_path),
|
| 511 |
+
"output": str(output_path),
|
| 512 |
+
"prompts": prompts,
|
| 513 |
+
"interval_seconds": args.interval_seconds,
|
| 514 |
+
"source_fps": fps,
|
| 515 |
+
"rendered_frames": frame_index,
|
| 516 |
+
"tracking_box_px": 100,
|
| 517 |
+
"edge_hide_margin_px": 50,
|
| 518 |
+
"samples": sidecar_samples,
|
| 519 |
+
}
|
| 520 |
+
output_path.with_suffix(output_path.suffix + ".json").write_text(
|
| 521 |
+
json.dumps(sidecar, indent=2, ensure_ascii=False) + "\n",
|
| 522 |
+
encoding="utf-8",
|
| 523 |
+
)
|
| 524 |
+
shutil.rmtree(temporary_dir, ignore_errors=True)
|
| 525 |
+
return 0
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
if __name__ == "__main__":
|
| 529 |
+
raise SystemExit(main())
|