roysun2006 commited on
Commit
04bceb2
·
verified ·
1 Parent(s): f241b32

Initial release

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +8 -0
  2. LEGAL.md +7 -0
  3. LICENSE +201 -0
  4. README.md +650 -0
  5. README_zh.md +621 -0
  6. Realtime-Venus-Audio/NOTICE +9 -0
  7. Realtime-Venus-Audio/added_tokens.json +111 -0
  8. Realtime-Venus-Audio/assets/HT_ref_audio.wav +3 -0
  9. Realtime-Venus-Audio/assets/case_duplex.wav +3 -0
  10. Realtime-Venus-Audio/assets/case_offline.wav +3 -0
  11. Realtime-Venus-Audio/assets/token2wav/campplus.onnx +3 -0
  12. Realtime-Venus-Audio/assets/token2wav/flow.pt +3 -0
  13. Realtime-Venus-Audio/assets/token2wav/flow.yaml +34 -0
  14. Realtime-Venus-Audio/assets/token2wav/hift.pt +3 -0
  15. Realtime-Venus-Audio/assets/token2wav/speech_tokenizer_v2_25hz.onnx +3 -0
  16. Realtime-Venus-Audio/chat_template.jinja +88 -0
  17. Realtime-Venus-Audio/config.json +323 -0
  18. Realtime-Venus-Audio/configuration_minicpmo.py +261 -0
  19. Realtime-Venus-Audio/generation_config.json +13 -0
  20. Realtime-Venus-Audio/merges.txt +0 -0
  21. Realtime-Venus-Audio/model-00001-of-00004.safetensors +3 -0
  22. Realtime-Venus-Audio/model-00002-of-00004.safetensors +3 -0
  23. Realtime-Venus-Audio/model-00003-of-00004.safetensors +3 -0
  24. Realtime-Venus-Audio/model-00004-of-00004.safetensors +3 -0
  25. Realtime-Venus-Audio/model.safetensors.index.json +0 -0
  26. Realtime-Venus-Audio/modeling_minicpmo.py +0 -0
  27. Realtime-Venus-Audio/modeling_navit_siglip.py +981 -0
  28. Realtime-Venus-Audio/preprocessor_config.json +49 -0
  29. Realtime-Venus-Audio/processing_minicpmo.py +1666 -0
  30. Realtime-Venus-Audio/processor_config.json +6 -0
  31. Realtime-Venus-Audio/special_tokens_map.json +121 -0
  32. Realtime-Venus-Audio/tokenization_minicpmo_fast.py +121 -0
  33. Realtime-Venus-Audio/tokenizer.json +3 -0
  34. Realtime-Venus-Audio/tokenizer_config.json +990 -0
  35. Realtime-Venus-Audio/utils.py +2418 -0
  36. Realtime-Venus-Audio/vocab.json +0 -0
  37. Realtime-Venus-Omni/LICENSE +201 -0
  38. Realtime-Venus-Omni/NOTICE +10 -0
  39. Realtime-Venus-Omni/added_tokens.json +111 -0
  40. Realtime-Venus-Omni/assets/HT_ref_audio.wav +3 -0
  41. Realtime-Venus-Omni/assets/sample_1_real.mp4 +3 -0
  42. Realtime-Venus-Omni/assets/speech_in.mp4 +3 -0
  43. Realtime-Venus-Omni/assets/token2wav/campplus.onnx +3 -0
  44. Realtime-Venus-Omni/assets/token2wav/flow.pt +3 -0
  45. Realtime-Venus-Omni/assets/token2wav/flow.yaml +34 -0
  46. Realtime-Venus-Omni/assets/token2wav/hift.pt +3 -0
  47. Realtime-Venus-Omni/assets/token2wav/speech_tokenizer_v2_25hz.onnx +3 -0
  48. Realtime-Venus-Omni/config.json +285 -0
  49. Realtime-Venus-Omni/configuration_realtime_venus_omni.py +261 -0
  50. Realtime-Venus-Omni/generation_config.json +12 -0
.gitattributes CHANGED
@@ -33,3 +33,11 @@ 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
+ Realtime-Venus-Audio/assets/HT_ref_audio.wav filter=lfs diff=lfs merge=lfs -text
37
+ Realtime-Venus-Audio/assets/case_duplex.wav filter=lfs diff=lfs merge=lfs -text
38
+ Realtime-Venus-Audio/assets/case_offline.wav filter=lfs diff=lfs merge=lfs -text
39
+ Realtime-Venus-Audio/tokenizer.json filter=lfs diff=lfs merge=lfs -text
40
+ Realtime-Venus-Omni/assets/HT_ref_audio.wav filter=lfs diff=lfs merge=lfs -text
41
+ Realtime-Venus-Omni/assets/sample_1_real.mp4 filter=lfs diff=lfs merge=lfs -text
42
+ Realtime-Venus-Omni/assets/speech_in.mp4 filter=lfs diff=lfs merge=lfs -text
43
+ assets/venus-logo-white.gif filter=lfs diff=lfs merge=lfs -text
LEGAL.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Legal Disclaimer
2
+
3
+ Within this source code, the comments in Chinese shall be the original, governing version. Any comment in other languages are for reference only. In the event of any conflict between the Chinese language version comments and other language version comments, the Chinese language version shall prevail.
4
+
5
+ 法律免责声明
6
+
7
+ 关于代码注释部分,中文注释为官方版本,其它语言注释仅做参考。中文注释可能与其它语言注释存在不一致,当中文注释与其它语言注释存在不一致时,请以中文注释为准。
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 OpenBMB
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.
README.md ADDED
@@ -0,0 +1,650 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ - zh
6
+ library_name: transformers
7
+ pipeline_tag: any-to-any
8
+ tags:
9
+ - multimodal
10
+ - audio
11
+ - video
12
+ - speech
13
+ - streaming
14
+ - full-duplex
15
+ - long-video
16
+ - custom-code
17
+ ---
18
+
19
+ <div align="center">
20
+ <img src="./assets/venus-logo-white.gif" alt="Realtime-Venus logo" width="180">
21
+ <br>
22
+
23
+ # Realtime-Venus
24
+
25
+ **A full-duplex interaction system with asynchronous delegation**
26
+
27
+ **English** | [简体中文](./README_zh.md)
28
+
29
+ <br>
30
+
31
+ <a href="https://realtime-venus.github.io/"><img src="https://img.shields.io/badge/Project_Page-4c9aff.svg?logo=googlechrome&logoColor=white" alt="Project Page"></a>
32
+ <a href="https://huggingface.co/inclusionAI/Realtime-Venus"><img src="https://img.shields.io/badge/Hugging_Face-Realtime--Venus-FFD21E.svg?logo=huggingface&logoColor=000" alt="Realtime-Venus on Hugging Face"></a>
33
+ <a href="https://www.modelscope.cn/models/inclusionAI/Realtime-Venus"><img src="https://img.shields.io/badge/ModelScope-Realtime--Venus-624AFF.svg?logo=modelscope&logoColor=white" alt="Realtime-Venus on ModelScope"></a>
34
+ <a href="https://arxiv.org/abs/2609.13814"><img src="https://img.shields.io/badge/arXiv-2609.13814-b31b1b.svg?logo=arxiv&logoColor=white" alt="arXiv"></a>
35
+ <a href="https://github.com/inclusionAI/Realtime-Venus"><img src="https://img.shields.io/badge/GitHub-Realtime--Venus-181717.svg?logo=github&logoColor=white" alt="GitHub"></a>
36
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-0b7285.svg?logo=apache&logoColor=white" alt="Apache License 2.0"></a>
37
+
38
+ </div>
39
+
40
+ <p align="center">
41
+ <a href="https://arxiv.org/abs/2609.13814">
42
+ <img src="https://arxiv.org/html/2609.13814v1/case.png" alt="Example proactive, delegated, and full-duplex interactions with Realtime-Venus" width="100%">
43
+ </a>
44
+ </p>
45
+ <p align="center"><em>Realtime-Venus supports proactive audio-visual interaction, asynchronous delegation, and interruption-aware full-duplex dialogue.</em></p>
46
+
47
+ ## 1. 🧭 Overview
48
+
49
+ This repository hosts two checkpoints of the
50
+ [Realtime-Venus](https://realtime-venus.github.io/) system:
51
+
52
+ - **Realtime-Venus-Omni** (`Realtime-Venus-Omni/`): the 9B audio-visual
53
+ interaction model. It continuously watches and listens, decides whether and
54
+ when to respond, and generates text and speech on a shared causal timeline.
55
+ Adapted from MiniCPM-o 4.5, it supports proactive interaction, semantic
56
+ interruption handling, and training-free long-video memory.
57
+ - **Realtime-Venus-Audio** (`Realtime-Venus-Audio/`): the audio-focused
58
+ checkpoint on the same streaming backbone, for audio understanding and
59
+ audio-driven conversation with text or speech output.
60
+
61
+ Both directories contain model weights and custom Hugging Face Transformers
62
+ code. The asynchronous Realtime-Venus-Harness and its external tool
63
+ integrations live in the
64
+ [GitHub repository](https://github.com/inclusionAI/Realtime-Venus).
65
+
66
+ ## 2. ✨ Highlights
67
+
68
+ - **Native full-duplex conversation:** keeps perceiving while speaking and
69
+ distinguishes backchannels, interruptions, corrections, and redirections.
70
+ - **Omni-Proactive interaction:** continuously processes temporally aligned
71
+ video and audio, and initiates a response when an event warrants it — without
72
+ waiting for a user prompt.
73
+ - **Delegation:** emits in-stream `<delegate>` requests on the shared causal
74
+ timeline and consumes asynchronous backend results the same way, so external
75
+ tasks never block the ongoing conversation. (Executing requests requires the
76
+ Realtime-Venus-Harness runtime, available in the
77
+ [GitHub repository](https://github.com/inclusionAI/Realtime-Venus).)
78
+ - **Training-free long-video Memory:** archives visually informative moments,
79
+ retrieves query-relevant and non-redundant evidence, and reassembles the
80
+ corresponding audio-visual context — no additional training required.
81
+ - **Text and speech output:** generates response text together with native
82
+ speech through the bundled Token2wav resources and a reference voice.
83
+
84
+ ## 3. 📋 Model Details
85
+
86
+ | Item | Realtime-Venus-Omni | Realtime-Venus-Audio |
87
+ | --- | --- | --- |
88
+ | Parameters | 9B | 9B |
89
+ | Base architecture | MiniCPM-o 4.5 / Omni-Flow | MiniCPM-o 4.5 / Omni-Flow |
90
+ | Visual encoder | SigLIP2 | not used at inference |
91
+ | Audio encoder | Whisper-Medium | Whisper-Medium |
92
+ | Language backbone | Qwen3-8B | Qwen3-8B |
93
+ | Speech generation | Discrete S3 speech tokens with a streaming flow-matching decoder | same decoder, enabled in full-duplex mode |
94
+ | Inputs | Video/images, audio, and text | Audio and text |
95
+ | Outputs | Text and optional speech waveform | Text and speech waveform |
96
+ | Context length | 40,960 tokens | 40,960 tokens |
97
+ | Weight dtype | BF16 | BF16 |
98
+
99
+ ## 4. 📊 Evaluation
100
+
101
+ All values are reported in the
102
+ [Realtime-Venus technical report](https://arxiv.org/abs/2609.13814).
103
+
104
+ <p align="center"><img src="assets/paper-understanding.svg" width="100%" alt="Radar charts comparing video understanding for Omni and audio understanding for Audio" /><br /><sub>Figure 1. Video and audio understanding results from the <a href="https://arxiv.org/html/2609.13814v1#S0.F1">paper</a>.</sub></p>
105
+
106
+ <p align="center"><img src="assets/paper-duplex.svg" width="100%" alt="Full-duplex benchmark comparisons for interruption handling and continuation under different types of overlapping speech" /><br /><sub>Figure 2. Full-duplex interaction results from the <a href="https://arxiv.org/html/2609.13814v1#S0.F2">paper</a>.</sub></p>
107
+
108
+ ## 5. 🗂️ Repository Layout
109
+
110
+ ```text
111
+ .
112
+ ├── Realtime-Venus-Omni/ # Audio-visual full-duplex checkpoint
113
+ │ ├── model-*.safetensors # Sharded model weights
114
+ │ ├── config.json, *.py # Model config and custom Transformers code
115
+ │ ├── realtime_venus_omni_memory.py # Public Memory entry point
116
+ │ ├── memory_adapter/ # Chat and Duplex Memory runtime
117
+ │ ├── assets/ # Reference voice, Token2wav, demo videos
118
+ │ └── requirements.txt
119
+ ├── Realtime-Venus-Audio/ # Audio-focused checkpoint
120
+ │ ├── model-*.safetensors # Sharded model weights
121
+ │ ├── config.json, *.py # Model config and custom Transformers code
122
+ │ └── assets/ # Reference voice, Token2wav, demo audio
123
+ ├── assets/ # Brand resources (logo)
124
+ ├── README.md
125
+ ├── README_zh.md
126
+ └── LICENSE
127
+ ```
128
+
129
+ The examples below write generated media to `output/`. Use a new filename or a
130
+ new output directory when repeating an experiment.
131
+
132
+ ## 6. 🛠️ Installation
133
+
134
+ Requires Python 3.10, CUDA, and FFmpeg. Download the repository (the two
135
+ checkpoints live in its sub-directories) and install the Python dependencies:
136
+
137
+ ```bash
138
+ huggingface-cli download inclusionAI/Realtime-Venus --local-dir .
139
+ # or: modelscope download --model inclusionAI/Realtime-Venus --local_dir .
140
+
141
+ python -m pip install -r Realtime-Venus-Omni/requirements.txt
142
+ ```
143
+
144
+ All example paths below are relative to this repository's root directory.
145
+ `from_pretrained` does not address sub-directories of a Hub repo, so the
146
+ examples point at the local `Realtime-Venus-Omni/` and `Realtime-Venus-Audio/`
147
+ paths after the download.
148
+
149
+ ## 7. 🎙️ Realtime-Venus-Omni Usages
150
+
151
+ Runnable standalone versions of these examples live in the
152
+ [Omni cookbook](https://github.com/inclusionAI/Realtime-Venus/tree/main/frontend/Realtime-Venus-Omni)
153
+ on GitHub.
154
+
155
+ ### 7.1 🧱 Model Initialization
156
+
157
+ The examples below share the following model initialization; run each
158
+ example in a fresh Python process.
159
+ Chat and Duplex automatically load the default reference voice.
160
+
161
+ <details>
162
+ <summary>Click to show Omni model loading code.</summary>
163
+
164
+ ```python
165
+ from pathlib import Path
166
+
167
+ import torch
168
+ from transformers import AutoModel, set_seed
169
+
170
+ Path("output").mkdir(exist_ok=True)
171
+ set_seed(42)
172
+ print("Loading model ...")
173
+ model = AutoModel.from_pretrained(
174
+ "./Realtime-Venus-Omni", # or an absolute path to the sub-directory
175
+ trust_remote_code=True,
176
+ local_files_only=True,
177
+ attn_implementation="sdpa",
178
+ torch_dtype=torch.bfloat16,
179
+ )
180
+ model.eval().cuda()
181
+ print("Model loaded.")
182
+ ```
183
+
184
+ </details>
185
+
186
+ ### 7.2 🔊 Duplex Omni Mode
187
+
188
+ `model = model.as_duplex()` switches the model to full-duplex streaming:
189
+ `prepare()` initializes the session, then each second of input is handled by
190
+ one `streaming_prefill()` + `streaming_generate()` pair, and `as_simplex()`
191
+ switches back to offline mode. Set `MAX_NUM_FRAMES` before importing
192
+ `minicpmo.utils`, otherwise videos longer than 64 seconds are truncated to the
193
+ default frame cap.
194
+
195
+ Subtitle font note: Duplex examples burn the response text into the output
196
+ video through FFmpeg/libass, which resolves fonts via fontconfig. Rendering
197
+ non-Latin responses (e.g. Chinese) requires a CJK-capable font on the system,
198
+ otherwise those glyphs show up as empty boxes. On any Linux distribution,
199
+ install one without root and refresh the font cache:
200
+
201
+ ```bash
202
+ mkdir -p ~/.local/share/fonts
203
+ curl --fail --location --retry 3 \
204
+ --output ~/.local/share/fonts/NotoSansCJKsc-Regular.otf \
205
+ https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf
206
+ fc-cache -f
207
+ ```
208
+
209
+ Package-manager equivalents: `apt install -y fonts-noto-cjk` (Debian/Ubuntu) or
210
+ `yum install -y cjkuni-ukai-fonts cjkuni-uming-fonts` (RHEL/Alibaba Cloud Linux).
211
+ No code changes are needed.
212
+
213
+ #### 7.2.1 Duplex Chat
214
+
215
+ Stream the demo video second by second and inject text questions at the seconds
216
+ given by `question_times` (paired with `questions`). The model listens
217
+ continuously and speaks when it answers.
218
+
219
+ <details>
220
+ <summary>Click to show the Duplex Chat code.</summary>
221
+
222
+ ```python
223
+ import os
224
+
225
+ os.environ["MAX_NUM_FRAMES"] = "100000"
226
+
227
+ from minicpmo.utils import get_video_frame_audio_segments, generate_duplex_video
228
+
229
+ model = model.as_duplex() # switch to full-duplex streaming
230
+ model.prepare()
231
+
232
+ video_path = "Realtime-Venus-Omni/assets/sample_1_real.mp4"
233
+ # each question is injected at the corresponding second
234
+ question_times = [60, 128]
235
+ questions = [
236
+ "What do you see in the video so far?",
237
+ "What is the color of the cooler labeled PRIME near the team bench?",
238
+ ]
239
+ question_plan = dict(zip(question_times, questions))
240
+ print(f"Extracting per-second audio and frames from {video_path} ...")
241
+ frames, audios, _ = get_video_frame_audio_segments(
242
+ video_path, stack_frames=1, use_ffmpeg=True, adjust_audio_length=True
243
+ )
244
+ print(f"Streaming {len(audios)} seconds; questions are injected at {question_times}.")
245
+ results, output_audio = [], []
246
+ for second, (frame, audio) in enumerate(zip(frames, audios), start=1):
247
+ model.streaming_prefill(
248
+ audio_waveform=audio,
249
+ frame_list=[frame] if frame is not None else None,
250
+ text_list=[question_plan[second]] if second in question_plan else None,
251
+ )
252
+ result = model.streaming_generate()
253
+ print(
254
+ f"[{second}/{len(audios)}]",
255
+ "listen..." if result["is_listen"] else f"speak> {result['text']}",
256
+ flush=True,
257
+ )
258
+ results.append({"chunk_idx": second - 1, **result})
259
+ if result["audio_waveform"] is not None:
260
+ output_audio.append((second - 1, result["audio_waveform"]))
261
+
262
+ model = model.as_simplex()
263
+ print("Muxing the spoken responses into the output video ...")
264
+ generate_duplex_video(
265
+ video_path=video_path,
266
+ output_video_path="output/duplex_chat.mp4",
267
+ results_log=results,
268
+ timed_output_audio=output_audio,
269
+ )
270
+ ```
271
+
272
+ </details>
273
+
274
+ #### 7.2.2 Speech-In Duplex Chat
275
+
276
+ Same as above, except the question is spoken and already mixed into the video's
277
+ audio track (at ~3 s, asking for an alert when the water boils), so no text is
278
+ injected — the model must hear it.
279
+
280
+ <details>
281
+ <summary>Click to show the Speech-In Duplex Chat code.</summary>
282
+
283
+ ```python
284
+ import os
285
+
286
+ os.environ["MAX_NUM_FRAMES"] = "100000"
287
+
288
+ from minicpmo.utils import get_video_frame_audio_segments, generate_duplex_video
289
+
290
+ model = model.as_duplex() # switch to full-duplex streaming
291
+ model.prepare()
292
+
293
+ video_path = "Realtime-Venus-Omni/assets/speech_in.mp4"
294
+ print(f"Extracting per-second audio and frames from {video_path} ...")
295
+ frames, audios, _ = get_video_frame_audio_segments(
296
+ video_path, stack_frames=1, use_ffmpeg=True, adjust_audio_length=True
297
+ )
298
+ print(f"Streaming {len(audios)} seconds; the spoken question is already in the audio track.")
299
+ results, output_audio = [], []
300
+ for second, (frame, audio) in enumerate(zip(frames, audios), start=1):
301
+ model.streaming_prefill(
302
+ audio_waveform=audio,
303
+ frame_list=[frame] if frame is not None else None,
304
+ )
305
+ result = model.streaming_generate()
306
+ print(
307
+ f"[{second}/{len(audios)}]",
308
+ "listen..." if result["is_listen"] else result["text"],
309
+ flush=True,
310
+ )
311
+ results.append({"chunk_idx": second - 1, **result})
312
+ if result["audio_waveform"] is not None:
313
+ output_audio.append((second - 1, result["audio_waveform"]))
314
+
315
+ model = model.as_simplex()
316
+ print("Muxing the spoken responses into the output video ...")
317
+ generate_duplex_video(
318
+ video_path=video_path,
319
+ output_video_path="output/duplex_speech_in_chat.mp4",
320
+ results_log=results,
321
+ timed_output_audio=output_audio,
322
+ )
323
+ ```
324
+
325
+ </details>
326
+
327
+ #### 7.2.3 Memory Duplex Chat
328
+
329
+ `model.use_memory(memory_minutes=40)` enables the long-video Memory before
330
+ entering duplex mode.
331
+
332
+ <details>
333
+ <summary>Click to show the Memory Duplex Chat code.</summary>
334
+
335
+ ```python
336
+ import os
337
+
338
+ os.environ["MAX_NUM_FRAMES"] = "100000"
339
+
340
+ from minicpmo.utils import get_video_frame_audio_segments, generate_duplex_video
341
+
342
+ model.use_memory(memory_minutes=40) # enable long-video memory
343
+ model = model.as_duplex() # switch to full-duplex streaming
344
+ model.prepare()
345
+
346
+ video_path = "Realtime-Venus-Omni/assets/sample_1_real.mp4"
347
+ question = "What is the color of the cooler labeled PRIME near the team bench?"
348
+ print(f"Extracting per-second audio and frames from {video_path} ...")
349
+ frames, audios, _ = get_video_frame_audio_segments(
350
+ video_path, stack_frames=1, use_ffmpeg=True, adjust_audio_length=True
351
+ )
352
+ print(f"Streaming {len(audios)} seconds; the text question is injected at second 128.")
353
+ results, output_audio = [], []
354
+ for second, (frame, audio) in enumerate(zip(frames, audios), start=1):
355
+ model.streaming_prefill(
356
+ audio_waveform=audio,
357
+ frame_list=[frame] if frame is not None else None,
358
+ text_list=[question] if second == 128 else None,
359
+ )
360
+ result = model.streaming_generate()
361
+ print(
362
+ f"[{second}/{len(audios)}]",
363
+ "listen..." if result["is_listen"] else f"speak> {result['text']}",
364
+ flush=True,
365
+ )
366
+ results.append({"chunk_idx": second - 1, **result})
367
+ if result["audio_waveform"] is not None:
368
+ output_audio.append((second - 1, result["audio_waveform"]))
369
+
370
+ model = model.as_simplex()
371
+ print("Muxing the spoken responses into the output video ...")
372
+ generate_duplex_video(
373
+ video_path=video_path,
374
+ output_video_path="output/duplex_memory_chat.mp4",
375
+ results_log=results,
376
+ timed_output_audio=output_audio,
377
+ )
378
+ ```
379
+
380
+ </details>
381
+
382
+ ### 7.3 💬 Half-Duplex Omni Mode
383
+
384
+ `model.chat(...)` answers one turn at a time over the whole video.
385
+ `model.init_tts()` enables speech output.
386
+
387
+ #### 7.3.1 Offline Chat
388
+
389
+ Sampled frames, per-second audio, and the question go into a single `chat()`
390
+ call. The 128-frame cap (`MAX_NUM_FRAMES`) limits the visual load, while
391
+ `max_inp_length=32768` sets the input-token budget. Full audio is still
392
+ retained, so very long videos can exceed that budget even with frame sampling.
393
+
394
+ <details>
395
+ <summary>Click to show the Offline Chat code.</summary>
396
+
397
+ ```python
398
+ import os
399
+
400
+ os.environ.setdefault("MAX_NUM_FRAMES", "128")
401
+
402
+ from minicpmo.utils import get_video_frame_audio_segments
403
+
404
+ model.init_tts() # enable speech output
405
+
406
+ video_path = "Realtime-Venus-Omni/assets/sample_1_real.mp4"
407
+ question = "What is the color of the cooler labeled PRIME near the team bench?"
408
+ print(f"Extracting audio and frames from {video_path} ...")
409
+ frames, audios, _ = get_video_frame_audio_segments(
410
+ video_path, stack_frames=1
411
+ )
412
+ content = []
413
+ for frame, audio in zip(frames, audios):
414
+ if frame is not None:
415
+ content.append(frame)
416
+ content.append(audio)
417
+ content.append(question)
418
+
419
+ print("Running chat inference ...")
420
+ response = model.chat(
421
+ msgs=[{"role": "user", "content": content}],
422
+ max_new_tokens=4096,
423
+ max_inp_length=32768,
424
+ do_sample=True,
425
+ temperature=0.7,
426
+ use_image_id=False,
427
+ max_slice_nums=1,
428
+ use_tts_template=True,
429
+ enable_thinking=False,
430
+ omni_mode=True,
431
+ generate_audio=True,
432
+ output_audio_path="output/offline_chat.wav",
433
+ )
434
+ print(response)
435
+ ```
436
+
437
+ </details>
438
+
439
+ #### 7.3.2 Memory Offline Chat
440
+
441
+ `model.use_memory()` enables Memory before the chat call; retrieval selects up
442
+ to 96 historical frames plus 4 recent frames, each with ±1 s of audio.
443
+
444
+ <details>
445
+ <summary>Click to show the Memory Offline Chat code.</summary>
446
+
447
+ ```python
448
+ import os
449
+
450
+ os.environ["MAX_NUM_FRAMES"] = "100000"
451
+
452
+ from minicpmo.utils import get_video_frame_audio_segments
453
+
454
+ model.use_memory() # enable long-video memory
455
+ model.init_tts() # enable speech output
456
+
457
+ video_path = "Realtime-Venus-Omni/assets/sample_1_real.mp4"
458
+ question = "What is the color of the cooler labeled PRIME near the team bench?"
459
+ print(f"Extracting audio and frames from {video_path} ...")
460
+ frames, audios, _ = get_video_frame_audio_segments(
461
+ video_path, stack_frames=1, use_ffmpeg=True, adjust_audio_length=True
462
+ )
463
+ content = []
464
+ for frame, audio in zip(frames, audios):
465
+ if frame is not None:
466
+ content.append(frame)
467
+ content.append(audio)
468
+ content.append(question)
469
+
470
+ print("Running chat inference ...")
471
+ response = model.chat(
472
+ msgs=[{"role": "user", "content": content}],
473
+ max_new_tokens=4096,
474
+ max_inp_length=32768,
475
+ do_sample=True,
476
+ temperature=0.7,
477
+ use_image_id=False,
478
+ max_slice_nums=1,
479
+ use_tts_template=True,
480
+ enable_thinking=False,
481
+ omni_mode=True,
482
+ generate_audio=True,
483
+ output_audio_path="output/offline_memory_chat.wav",
484
+ )
485
+ print(response)
486
+ ```
487
+
488
+ </details>
489
+
490
+ ## 8. 🎧 Realtime-Venus-Audio Usages
491
+
492
+ Runnable standalone versions of these examples live in the
493
+ [Audio cookbook](https://github.com/inclusionAI/Realtime-Venus/tree/main/frontend/Realtime-Venus-Audio)
494
+ on GitHub.
495
+
496
+ The Audio checkpoint runs audio-only inference in two ways: turn-based
497
+ `model.chat` (text response) and the full-duplex streaming API (spoken
498
+ response). Inputs are decoded as 16 kHz mono audio from any audio or video
499
+ file.
500
+
501
+ ### 8.1 🧱 Model Initialization
502
+
503
+ Speech output is enabled with `init_tts=True` so the same `model` serves both
504
+ examples; use `init_tts=False` for text-only chat to load faster.
505
+
506
+ <details>
507
+ <summary>Click to show Audio model loading code.</summary>
508
+
509
+ ```python
510
+ from pathlib import Path
511
+
512
+ import torch
513
+ from transformers import AutoModel, AutoTokenizer, set_seed
514
+
515
+ Path("output").mkdir(exist_ok=True)
516
+ set_seed(42)
517
+ print("Loading model ...")
518
+ tokenizer = AutoTokenizer.from_pretrained(
519
+ "./Realtime-Venus-Audio", trust_remote_code=True, local_files_only=True,
520
+ fix_mistral_regex=True,
521
+ )
522
+ model = AutoModel.from_pretrained(
523
+ "./Realtime-Venus-Audio",
524
+ trust_remote_code=True,
525
+ local_files_only=True,
526
+ attn_implementation="sdpa",
527
+ torch_dtype=torch.bfloat16,
528
+ init_vision=False, # audio-only usage
529
+ init_audio=True,
530
+ init_tts=True, # speech output; set False for text-only chat
531
+ ).eval().cuda()
532
+ print("Model loaded.")
533
+ ```
534
+
535
+ </details>
536
+
537
+ ### 8.2 💭 Offline Chat
538
+
539
+ One deterministic turn over the full audio input: the audio (plus an optional
540
+ text instruction) goes into a single `model.chat()` call.
541
+
542
+ <details>
543
+ <summary>Click to show the Offline Chat code.</summary>
544
+
545
+ ```python
546
+ import librosa
547
+
548
+ print("Loading audio ...")
549
+ audio, _ = librosa.load(
550
+ "Realtime-Venus-Audio/assets/case_offline.wav", sr=16000, mono=True
551
+ )
552
+ msgs = [
553
+ {"role": "system", "content": "You are a helpful assistant."},
554
+ {"role": "user", "content": [audio, "What is the speaker asking about?"]},
555
+ ]
556
+
557
+ print("Running chat inference ...")
558
+ answer = model.chat(
559
+ msgs=msgs,
560
+ tokenizer=tokenizer,
561
+ do_sample=False,
562
+ max_new_tokens=2048,
563
+ enable_thinking=False,
564
+ use_tts_template=True,
565
+ generate_audio=False,
566
+ )
567
+ print(answer)
568
+ ```
569
+
570
+ </details>
571
+
572
+ ### 8.3 🎙️ Duplex Chat
573
+
574
+ `model.as_duplex(generate_audio=True)` switches to full-duplex streaming:
575
+ audio is fed second by second, the model listens continuously and speaks when
576
+ it answers. The example appends 10 s of trailing silence so the model can
577
+ finish its response after the input ends, and writes the generated speech to
578
+ `output/audio_full_duplex.wav`.
579
+
580
+ <details>
581
+ <summary>Click to show the Duplex Chat code.</summary>
582
+
583
+ ```python
584
+ import librosa
585
+ import numpy as np
586
+ import soundfile as sf
587
+
588
+ duplex = model.as_duplex(generate_audio=True) # full-duplex with speech output
589
+ duplex.prepare(prompt_wav_path="Realtime-Venus-Audio/assets/HT_ref_audio.wav")
590
+
591
+ audio, _ = librosa.load(
592
+ "Realtime-Venus-Audio/assets/case_duplex.wav", sr=16000, mono=True
593
+ )
594
+ audio = np.concatenate([audio, np.zeros(10 * 16000, dtype=np.float32)])
595
+
596
+ chunk_samples = int(duplex.CHUNK_MS * duplex.SAMPLE_RATE / 1000)
597
+ total_chunks = max(1, (len(audio) + chunk_samples - 1) // chunk_samples)
598
+ timed_audio = []
599
+ for chunk_index in range(total_chunks):
600
+ chunk = audio[chunk_index * chunk_samples:(chunk_index + 1) * chunk_samples]
601
+ if len(chunk) < chunk_samples:
602
+ chunk = np.pad(chunk, (0, chunk_samples - len(chunk)))
603
+ duplex.streaming_prefill(audio_waveform=chunk)
604
+ result = duplex.streaming_generate(
605
+ max_new_speak_tokens_per_chunk=20,
606
+ decode_mode="sampling",
607
+ temperature=0.7,
608
+ top_k=20,
609
+ top_p=0.8,
610
+ listen_prob_scale=1.0,
611
+ )
612
+ state = "listen" if result["is_listen"] else f"speak> {result['text']}"
613
+ print(f"[{chunk_index + 1}/{total_chunks}] {state}", flush=True)
614
+ if result["audio_waveform"] is not None and not result["is_listen"]:
615
+ timed_audio.append((chunk_index, result["audio_waveform"]))
616
+
617
+ # stitch the generated speech on its original timeline (24 kHz)
618
+ sample_rate = 24000
619
+ total_samples = max(
620
+ t * sample_rate + len(np.asarray(w, dtype=np.float32).squeeze())
621
+ for t, w in timed_audio
622
+ )
623
+ output = np.zeros(total_samples, dtype=np.float32)
624
+ for t, waveform in timed_audio:
625
+ w = np.asarray(waveform, dtype=np.float32).squeeze()
626
+ output[t * sample_rate: t * sample_rate + len(w)] += w
627
+ sf.write("output/audio_full_duplex.wav", np.clip(output, -1.0, 1.0), sample_rate)
628
+ print("Saved generated speech to output/audio_full_duplex.wav")
629
+ ```
630
+
631
+ </details>
632
+
633
+ ## 9. 📝 Citation
634
+
635
+ If you find Realtime-Venus useful, please cite the technical report:
636
+
637
+ ```bibtex
638
+ @article{zhao2026realtime,
639
+ title={{Realtime-Venus}: A full-duplex interaction system with asynchronous delegation},
640
+ author={{Venus Team,Ant Group;Tsinghua University}},
641
+ journal={arXiv preprint arXiv:2609.13814},
642
+ year={2026}
643
+ }
644
+ ```
645
+
646
+ ## 10. 📄 License
647
+
648
+ This repository includes an [Apache License 2.0](./LICENSE). Please also review
649
+ the licenses and acceptable-use terms of the upstream model, third-party
650
+ libraries, and any data used with this checkpoint.
README_zh.md ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - zh
5
+ - en
6
+ library_name: transformers
7
+ pipeline_tag: any-to-any
8
+ tags:
9
+ - multimodal
10
+ - audio
11
+ - video
12
+ - speech
13
+ - streaming
14
+ - full-duplex
15
+ - long-video
16
+ - custom-code
17
+ ---
18
+
19
+ <div align="center">
20
+ <img src="./assets/venus-logo-white.gif" alt="Realtime-Venus logo" width="180">
21
+ <br>
22
+
23
+ # Realtime-Venus
24
+
25
+ **支持异步委派的全双工交互系统**
26
+
27
+ [English](./README.md) | **简体中文**
28
+
29
+ <br>
30
+
31
+ <a href="https://realtime-venus.github.io/"><img src="https://img.shields.io/badge/Project_Page-4c9aff.svg?logo=googlechrome&logoColor=white" alt="项目主页"></a>
32
+ <a href="https://huggingface.co/inclusionAI/Realtime-Venus"><img src="https://img.shields.io/badge/Hugging_Face-Realtime--Venus-FFD21E.svg?logo=huggingface&logoColor=000" alt="Hugging Face 上的 Realtime-Venus"></a>
33
+ <a href="https://www.modelscope.cn/models/inclusionAI/Realtime-Venus"><img src="https://img.shields.io/badge/ModelScope-Realtime--Venus-624AFF.svg?logo=modelscope&logoColor=white" alt="ModelScope 上的 Realtime-Venus"></a>
34
+ <a href="https://arxiv.org/abs/2609.13814"><img src="https://img.shields.io/badge/arXiv-2609.13814-b31b1b.svg?logo=arxiv&logoColor=white" alt="arXiv"></a>
35
+ <a href="https://github.com/inclusionAI/Realtime-Venus"><img src="https://img.shields.io/badge/GitHub-Realtime--Venus-181717.svg?logo=github&logoColor=white" alt="GitHub"></a>
36
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-0b7285.svg?logo=apache&logoColor=white" alt="Apache License 2.0"></a>
37
+
38
+ </div>
39
+
40
+ <p align="center">
41
+ <a href="https://arxiv.org/abs/2609.13814">
42
+ <img src="https://arxiv.org/html/2609.13814v1/case.png" alt="Realtime-Venus 的主动交互、异步委派与全双工交互示例" width="100%">
43
+ </a>
44
+ </p>
45
+ <p align="center"><em>Realtime-Venus 支持主动式音视频交互、异步任务委派和可感知用户打断的全双工对话。</em></p>
46
+
47
+ ## 1. 🧭 概览
48
+
49
+ 本仓库包含 [Realtime-Venus](https://realtime-venus.github.io/) 系统的两个模型:
50
+
51
+ - **Realtime-Venus-Omni**(`Realtime-Venus-Omni/`):9B 音视频交互模型。模型能够持续
52
+ 观看和聆听,在共享的因果时间线上决定是否、何时响应,并生成文本与语音。模型基于
53
+ MiniCPM-o 4.5 改造,支持主动式交互、语义级打断处理,以及免训练的长视频 Memory。
54
+ - **Realtime-Venus-Audio**(`Realtime-Venus-Audio/`):基于同一流式骨干的音频模型,
55
+ 用于不包含视觉输入的音频理解与音频对话,输出文本或语音。
56
+
57
+ 两个目录都包含模型权重与自定义 Hugging Face Transformers 代码。异步
58
+ Realtime-Venus-Harness 及其外部工具集成见
59
+ [GitHub 仓库](https://github.com/inclusionAI/Realtime-Venus)。
60
+
61
+ ## 2. ✨ 核心亮点
62
+
63
+ - **原生全双工对话:** 在说话的同时继续感知,并区分附和、打断、纠正和话题重定向。
64
+ - **Omni-Proactive 主动交互:** 持续处理时间对齐的视频和音频,在新事件值得响应时
65
+ 主动发言,无需等待用户提问。
66
+ - **Delegate 委派能力:** 在共享因果时间线上发出流内 `<delegate>` 请求,并以同样方式
67
+ 接收异步返回的后端结果,外部任务不会阻塞当前对话。(执行请求需要
68
+ Realtime-Venus-Harness 运行时,见
69
+ [GitHub 仓库](https://github.com/inclusionAI/Realtime-Venus)。)
70
+ - **免训练长视频 Memory:** 归档视觉信息丰富的时刻,检索与问题相关且不冗余的证据,
71
+ 并重新组装对应的音视频上下文,无需额外训练模型。
72
+ - **文本与语音输出:** 通过随仓库提供的 Token2wav 资源和参考音色,同时生成回复文本
73
+ 与原生语音。
74
+
75
+ ## 3. 📋 模型信息
76
+
77
+ | 项目 | Realtime-Venus-Omni | Realtime-Venus-Audio |
78
+ | --- | --- | --- |
79
+ | 参数量 | 9B | 9B |
80
+ | 基础架构 | MiniCPM-o 4.5 / Omni-Flow | MiniCPM-o 4.5 / Omni-Flow |
81
+ | 视觉编码器 | SigLIP2 | 推理时不使用 |
82
+ | 音频编码器 | Whisper-Medium | Whisper-Medium |
83
+ | 语言模型骨干 | Qwen3-8B | Qwen3-8B |
84
+ | 语音生成 | 离散 S3 语音 token 与流式 flow-matching 解码器 | 同一解码器,在全双工模式下启用 |
85
+ | 输入 | 视频/图像、音频和文本 | 音频和文本 |
86
+ | 输出 | 文本及可选语音波形 | 文本和语音波形 |
87
+ | 上下文长度 | 40,960 tokens | 40,960 tokens |
88
+ | 权重精度 | BF16 | BF16 |
89
+
90
+ ## 4. 📊 评测
91
+
92
+ 下列结果均来自 [Realtime-Venus 技术报告](https://arxiv.org/abs/2609.13814)。
93
+
94
+ <p align="center"><img src="assets/paper-understanding.svg" width="100%" alt="Omni 视频理解与 Audio 音频理解的雷达图对比" /><br /><sub>图 1. 论文中的视频与音频理解结果(<a href="https://arxiv.org/html/2609.13814v1#S0.F1">链接</a>)。</sub></p>
95
+
96
+ <p align="center"><img src="assets/paper-duplex.svg" width="100%" alt="不同重叠语音场景下打断处理与续说的全双工基准对比" /><br /><sub>图 2. 论文中的全双工交互结果(<a href="https://arxiv.org/html/2609.13814v1#S0.F2">链接</a>)。</sub></p>
97
+
98
+ ## 5. 🗂️ 仓库结构
99
+
100
+ ```text
101
+ .
102
+ ├── Realtime-Venus-Omni/ # 音视频全双工模型
103
+ │ ├── model-*.safetensors # 分片模型权重
104
+ │ ├── config.json, *.py # 模型配置与自定义 Transformers 代码
105
+ │ ├── realtime_venus_omni_memory.py # Memory 公开入口
106
+ │ ├── memory_adapter/ # Chat 与 Duplex Memory 运行时
107
+ │ ├── assets/ # 参考音色、Token2wav、演示视频
108
+ │ └── requirements.txt
109
+ ├── Realtime-Venus-Audio/ # 音频模型
110
+ │ ├── model-*.safetensors # 分片模型权重
111
+ │ ├── config.json, *.py # 模型配置与自定义 Transformers 代码
112
+ │ └── assets/ # 参考音色、Token2wav、演示音频
113
+ ├── assets/ # Brand resources (logo)
114
+ ├── README.md
115
+ ├── README_zh.md
116
+ └── LICENSE
117
+ ```
118
+
119
+ 以下示例会把生成的媒体写入 `output/`。重复实验时请使用新的文件名或输出目录。
120
+
121
+ ## 6. 🛠️ 安装
122
+
123
+ 需要 Python 3.10、CUDA 和 FFmpeg。先下载仓库(两个模型分别位于其子目录中),
124
+ 再安装 Python 依赖:
125
+
126
+ ```bash
127
+ huggingface-cli download inclusionAI/Realtime-Venus --local-dir .
128
+ # 或:modelscope download --model inclusionAI/Realtime-Venus --local_dir .
129
+
130
+ python -m pip install -r Realtime-Venus-Omni/requirements.txt
131
+ ```
132
+
133
+ 下文所有示例路径均相对于本仓库根目录。`from_pretrained` 无法直接指定 Hub
134
+ 仓库的子目录,因此示例在下载完成后指向本地的 `Realtime-Venus-Omni/` 与
135
+ `Realtime-Venus-Audio/` 路径。
136
+
137
+ ## 7. 🎙️ Realtime-Venus-Omni 使用方法
138
+
139
+ 这些示例的可独立运行脚本见 GitHub 上的
140
+ [Omni cookbook](https://github.com/inclusionAI/Realtime-Venus/tree/main/frontend/Realtime-Venus-Omni)。
141
+
142
+ ### 7.1 🧱 模型初始化
143
+
144
+ 以下示例共用下方的模型初始化代码;请在新的 Python 进程中分别运行每个示例。Chat 与 Duplex 会自动加载默认参考音色。
145
+
146
+ <details>
147
+ <summary>点击展开 Omni 模型加载代码。</summary>
148
+
149
+ ```python
150
+ from pathlib import Path
151
+
152
+ import torch
153
+ from transformers import AutoModel, set_seed
154
+
155
+ Path("output").mkdir(exist_ok=True)
156
+ set_seed(42)
157
+ print("Loading model ...")
158
+ model = AutoModel.from_pretrained(
159
+ "./Realtime-Venus-Omni", # 也可以使用子目录的绝对路径
160
+ trust_remote_code=True,
161
+ local_files_only=True,
162
+ attn_implementation="sdpa",
163
+ torch_dtype=torch.bfloat16,
164
+ )
165
+ model.eval().cuda()
166
+ print("Model loaded.")
167
+ ```
168
+
169
+ </details>
170
+
171
+ ### 7.2 🔊 全双工 Omni 模式
172
+
173
+ `model = model.as_duplex()` 将模型切换为全双工流式模式;`prepare()` 初始化会话;
174
+ 随后每秒输入由一次 `streaming_prefill()` 与 `streaming_generate()` 处理;
175
+ `as_simplex()` 切换回离线模式。必须在导入 `minicpmo.utils` 前设置
176
+ `MAX_NUM_FRAMES`,否则超过 64 秒的视频会被默认帧数上限截断。
177
+
178
+ 字幕字体说明:Duplex 示例会通过 FFmpeg/libass 将回复文本烧录到输出视频中,字体由
179
+ fontconfig 解析。若系统没有支持中日韩字符的字体,中文等非拉丁文字会显示为空白方框。
180
+ 在任意 Linux 发行版中均可无需 root 权限安装字体并刷新缓存:
181
+
182
+ ```bash
183
+ mkdir -p ~/.local/share/fonts
184
+ curl --fail --location --retry 3 \
185
+ --output ~/.local/share/fonts/NotoSansCJKsc-Regular.otf \
186
+ https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf
187
+ fc-cache -f
188
+ ```
189
+
190
+ 也可以通过包管理器安装:Debian/Ubuntu 使用 `apt install -y fonts-noto-cjk`;
191
+ RHEL/Alibaba Cloud Linux 使用
192
+ `yum install -y cjkuni-ukai-fonts cjkuni-uming-fonts`。无需修改模型代码。
193
+
194
+ #### 7.2.1 全双工 Chat
195
+
196
+ 逐秒流式输入演示视频,并在 `question_times` 指定的秒数注入与 `questions` 一一对应的
197
+ 文本问题。模型会持续聆听,并在回答时生成语音。
198
+
199
+ <details>
200
+ <summary>点击展开全双工 Chat 推理代码。</summary>
201
+
202
+ ```python
203
+ import os
204
+
205
+ os.environ["MAX_NUM_FRAMES"] = "100000"
206
+
207
+ from minicpmo.utils import get_video_frame_audio_segments, generate_duplex_video
208
+
209
+ model = model.as_duplex() # 切换为全双工流式模式
210
+ model.prepare()
211
+
212
+ video_path = "Realtime-Venus-Omni/assets/sample_1_real.mp4"
213
+ # 每个问题在对应的秒数注入
214
+ question_times = [60, 128]
215
+ questions = [
216
+ "What do you see in the video so far?",
217
+ "What is the color of the cooler labeled PRIME near the team bench?",
218
+ ]
219
+ question_plan = dict(zip(question_times, questions))
220
+ print(f"Extracting per-second audio and frames from {video_path} ...")
221
+ frames, audios, _ = get_video_frame_audio_segments(
222
+ video_path, stack_frames=1, use_ffmpeg=True, adjust_audio_length=True
223
+ )
224
+ print(f"Streaming {len(audios)} seconds; questions are injected at {question_times}.")
225
+ results, output_audio = [], []
226
+ for second, (frame, audio) in enumerate(zip(frames, audios), start=1):
227
+ model.streaming_prefill(
228
+ audio_waveform=audio,
229
+ frame_list=[frame] if frame is not None else None,
230
+ text_list=[question_plan[second]] if second in question_plan else None,
231
+ )
232
+ result = model.streaming_generate()
233
+ print(
234
+ f"[{second}/{len(audios)}]",
235
+ "listen..." if result["is_listen"] else f"speak> {result['text']}",
236
+ flush=True,
237
+ )
238
+ results.append({"chunk_idx": second - 1, **result})
239
+ if result["audio_waveform"] is not None:
240
+ output_audio.append((second - 1, result["audio_waveform"]))
241
+
242
+ model = model.as_simplex()
243
+ print("Muxing the spoken responses into the output video ...")
244
+ generate_duplex_video(
245
+ video_path=video_path,
246
+ output_video_path="output/duplex_chat.mp4",
247
+ results_log=results,
248
+ timed_output_audio=output_audio,
249
+ )
250
+ ```
251
+
252
+ </details>
253
+
254
+ #### 7.2.2 语音输入全双工 Chat
255
+
256
+ 处理流程与上例相同,但问题已作为语音混入视频音轨(约第 3 秒,请求水烧开时提醒),
257
+ 因此不注入文本,模型必须直接听取问题。
258
+
259
+ <details>
260
+ <summary>点击展开语音输入全双工 Chat 推理代码。</summary>
261
+
262
+ ```python
263
+ import os
264
+
265
+ os.environ["MAX_NUM_FRAMES"] = "100000"
266
+
267
+ from minicpmo.utils import get_video_frame_audio_segments, generate_duplex_video
268
+
269
+ model = model.as_duplex() # 切换为全双工流式模式
270
+ model.prepare()
271
+
272
+ video_path = "Realtime-Venus-Omni/assets/speech_in.mp4"
273
+ print(f"Extracting per-second audio and frames from {video_path} ...")
274
+ frames, audios, _ = get_video_frame_audio_segments(
275
+ video_path, stack_frames=1, use_ffmpeg=True, adjust_audio_length=True
276
+ )
277
+ print(f"Streaming {len(audios)} seconds; the spoken question is already in the audio track.")
278
+ results, output_audio = [], []
279
+ for second, (frame, audio) in enumerate(zip(frames, audios), start=1):
280
+ model.streaming_prefill(
281
+ audio_waveform=audio,
282
+ frame_list=[frame] if frame is not None else None,
283
+ )
284
+ result = model.streaming_generate()
285
+ print(
286
+ f"[{second}/{len(audios)}]",
287
+ "listen..." if result["is_listen"] else result["text"],
288
+ flush=True,
289
+ )
290
+ results.append({"chunk_idx": second - 1, **result})
291
+ if result["audio_waveform"] is not None:
292
+ output_audio.append((second - 1, result["audio_waveform"]))
293
+
294
+ model = model.as_simplex()
295
+ print("Muxing the spoken responses into the output video ...")
296
+ generate_duplex_video(
297
+ video_path=video_path,
298
+ output_video_path="output/duplex_speech_in_chat.mp4",
299
+ results_log=results,
300
+ timed_output_audio=output_audio,
301
+ )
302
+ ```
303
+
304
+ </details>
305
+
306
+ #### 7.2.3 Memory 全双工 Chat
307
+
308
+ 在进入 Duplex 模式前调用 `model.use_memory(memory_minutes=40)`,即可启用长视频 Memory。
309
+
310
+ <details>
311
+ <summary>点击展开 Memory 全双工 Chat 推理代码。</summary>
312
+
313
+ ```python
314
+ import os
315
+
316
+ os.environ["MAX_NUM_FRAMES"] = "100000"
317
+
318
+ from minicpmo.utils import get_video_frame_audio_segments, generate_duplex_video
319
+
320
+ model.use_memory(memory_minutes=40) # 启用长视频记忆
321
+ model = model.as_duplex() # 切换为全双工流式模式
322
+ model.prepare()
323
+
324
+ video_path = "Realtime-Venus-Omni/assets/sample_1_real.mp4"
325
+ question = "What is the color of the cooler labeled PRIME near the team bench?"
326
+ print(f"Extracting per-second audio and frames from {video_path} ...")
327
+ frames, audios, _ = get_video_frame_audio_segments(
328
+ video_path, stack_frames=1, use_ffmpeg=True, adjust_audio_length=True
329
+ )
330
+ print(f"Streaming {len(audios)} seconds; the text question is injected at second 128.")
331
+ results, output_audio = [], []
332
+ for second, (frame, audio) in enumerate(zip(frames, audios), start=1):
333
+ model.streaming_prefill(
334
+ audio_waveform=audio,
335
+ frame_list=[frame] if frame is not None else None,
336
+ text_list=[question] if second == 128 else None,
337
+ )
338
+ result = model.streaming_generate()
339
+ print(
340
+ f"[{second}/{len(audios)}]",
341
+ "listen..." if result["is_listen"] else f"speak> {result['text']}",
342
+ flush=True,
343
+ )
344
+ results.append({"chunk_idx": second - 1, **result})
345
+ if result["audio_waveform"] is not None:
346
+ output_audio.append((second - 1, result["audio_waveform"]))
347
+
348
+ model = model.as_simplex()
349
+ print("Muxing the spoken responses into the output video ...")
350
+ generate_duplex_video(
351
+ video_path=video_path,
352
+ output_video_path="output/duplex_memory_chat.mp4",
353
+ results_log=results,
354
+ timed_output_audio=output_audio,
355
+ )
356
+ ```
357
+
358
+ </details>
359
+
360
+ ### 7.3 💬 半双工 Omni 模式
361
+
362
+ `model.chat(...)` 以完整视频为输入,逐轮回答问题;`model.init_tts()` 用于启用语音输出。
363
+
364
+ #### 7.3.1 离线 Chat
365
+
366
+ 采样后的视频帧、逐秒音频和问题会一起传入一次 `chat()` 调用。128 帧上限
367
+ (`MAX_NUM_FRAMES`)用于控制视觉输入规模,`max_inp_length=32768` 用于设置输入
368
+ token 预算。完整音频仍会保留,因此即使进行视频帧采样,超长视频仍可能超过该预算。
369
+
370
+ <details>
371
+ <summary>点击展开离线 Chat 推理代码。</summary>
372
+
373
+ ```python
374
+ import os
375
+
376
+ os.environ.setdefault("MAX_NUM_FRAMES", "128")
377
+
378
+ from minicpmo.utils import get_video_frame_audio_segments
379
+
380
+ model.init_tts() # 启用语音输出
381
+
382
+ video_path = "Realtime-Venus-Omni/assets/sample_1_real.mp4"
383
+ question = "What is the color of the cooler labeled PRIME near the team bench?"
384
+ print(f"Extracting audio and frames from {video_path} ...")
385
+ frames, audios, _ = get_video_frame_audio_segments(
386
+ video_path, stack_frames=1
387
+ )
388
+ content = []
389
+ for frame, audio in zip(frames, audios):
390
+ if frame is not None:
391
+ content.append(frame)
392
+ content.append(audio)
393
+ content.append(question)
394
+
395
+ print("Running chat inference ...")
396
+ response = model.chat(
397
+ msgs=[{"role": "user", "content": content}],
398
+ max_new_tokens=4096,
399
+ max_inp_length=32768,
400
+ do_sample=True,
401
+ temperature=0.7,
402
+ use_image_id=False,
403
+ max_slice_nums=1,
404
+ use_tts_template=True,
405
+ enable_thinking=False,
406
+ omni_mode=True,
407
+ generate_audio=True,
408
+ output_audio_path="output/offline_chat.wav",
409
+ )
410
+ print(response)
411
+ ```
412
+
413
+ </details>
414
+
415
+ #### 7.3.2 Memory 离线 Chat
416
+
417
+ 在调用 Chat 前通过 `model.use_memory()` 启用 Memory;检索最多选择 96 个历史帧和
418
+ 4 个近期帧,并为每个选中帧保留前后各 1 秒的音频。
419
+
420
+ <details>
421
+ <summary>点击展开 Memory 离线 Chat 推理代码。</summary>
422
+
423
+ ```python
424
+ import os
425
+
426
+ os.environ["MAX_NUM_FRAMES"] = "100000"
427
+
428
+ from minicpmo.utils import get_video_frame_audio_segments
429
+
430
+ model.use_memory() # 启用长视频记忆
431
+ model.init_tts() # 启用语音输出
432
+
433
+ video_path = "Realtime-Venus-Omni/assets/sample_1_real.mp4"
434
+ question = "What is the color of the cooler labeled PRIME near the team bench?"
435
+ print(f"Extracting audio and frames from {video_path} ...")
436
+ frames, audios, _ = get_video_frame_audio_segments(
437
+ video_path, stack_frames=1, use_ffmpeg=True, adjust_audio_length=True
438
+ )
439
+ content = []
440
+ for frame, audio in zip(frames, audios):
441
+ if frame is not None:
442
+ content.append(frame)
443
+ content.append(audio)
444
+ content.append(question)
445
+
446
+ print("Running chat inference ...")
447
+ response = model.chat(
448
+ msgs=[{"role": "user", "content": content}],
449
+ max_new_tokens=4096,
450
+ max_inp_length=32768,
451
+ do_sample=True,
452
+ temperature=0.7,
453
+ use_image_id=False,
454
+ max_slice_nums=1,
455
+ use_tts_template=True,
456
+ enable_thinking=False,
457
+ omni_mode=True,
458
+ generate_audio=True,
459
+ output_audio_path="output/offline_memory_chat.wav",
460
+ )
461
+ print(response)
462
+ ```
463
+
464
+ </details>
465
+
466
+ ## 8. 🎧 Realtime-Venus-Audio 使用方法
467
+
468
+ 这些示例的可独立运行脚本见 GitHub 上的
469
+ [Audio cookbook](https://github.com/inclusionAI/Realtime-Venus/tree/main/frontend/Realtime-Venus-Audio)。
470
+
471
+ Audio 模型以两种方式运行纯音频推理:轮次式 `model.chat`(文本回复)与
472
+ 全双工流式 API(语音回复)。输入统一解码为 16 kHz 单声道音频,可来自任意
473
+ 音频或视频文件。
474
+
475
+ ### 8.1 🧱 模型初始化
476
+
477
+ 下方代码以 `init_tts=True` 启用语音输出,同一个 `model` 可支撑后文两个示例;
478
+ 纯文本对话可改用 `init_tts=False`,加载更快。
479
+
480
+ <details>
481
+ <summary>点击展开 Audio 模型加载代码。</summary>
482
+
483
+ ```python
484
+ from pathlib import Path
485
+
486
+ import torch
487
+ from transformers import AutoModel, AutoTokenizer, set_seed
488
+
489
+ Path("output").mkdir(exist_ok=True)
490
+ set_seed(42)
491
+ print("Loading model ...")
492
+ tokenizer = AutoTokenizer.from_pretrained(
493
+ "./Realtime-Venus-Audio", trust_remote_code=True, local_files_only=True,
494
+ fix_mistral_regex=True,
495
+ )
496
+ model = AutoModel.from_pretrained(
497
+ "./Realtime-Venus-Audio",
498
+ trust_remote_code=True,
499
+ local_files_only=True,
500
+ attn_implementation="sdpa",
501
+ torch_dtype=torch.bfloat16,
502
+ init_vision=False, # 纯音频用法
503
+ init_audio=True,
504
+ init_tts=True, # 语音输出;纯文本对话可设为 False
505
+ ).eval().cuda()
506
+ print("Model loaded.")
507
+ ```
508
+
509
+ </details>
510
+
511
+ ### 8.2 💭 离线 Chat
512
+
513
+ 对完整音频输入进行一轮确定性回答:音频(可加一条文本指令)随一次
514
+ `model.chat()` 调用传入。
515
+
516
+ <details>
517
+ <summary>点击展开离线 Chat 推理代码。</summary>
518
+
519
+ ```python
520
+ import librosa
521
+
522
+ print("Loading audio ...")
523
+ audio, _ = librosa.load(
524
+ "Realtime-Venus-Audio/assets/case_offline.wav", sr=16000, mono=True
525
+ )
526
+ msgs = [
527
+ {"role": "system", "content": "You are a helpful assistant."},
528
+ {"role": "user", "content": [audio, "What is the speaker asking about?"]},
529
+ ]
530
+
531
+ print("Running chat inference ...")
532
+ answer = model.chat(
533
+ msgs=msgs,
534
+ tokenizer=tokenizer,
535
+ do_sample=False,
536
+ max_new_tokens=2048,
537
+ enable_thinking=False,
538
+ use_tts_template=True,
539
+ generate_audio=False,
540
+ )
541
+ print(answer)
542
+ ```
543
+
544
+ </details>
545
+
546
+ ### 8.3 🎙️ 全双工 Chat
547
+
548
+ `model.as_duplex(generate_audio=True)` 切换为全双工流式模式:音频逐秒输入,
549
+ 模型持续聆听、在回答时开口说话。示例在输入末尾追加 10 秒静音,让模型在
550
+ 输入结束后说完回复,生成的语音写入 `output/audio_full_duplex.wav`。
551
+
552
+ <details>
553
+ <summary>点击展开全双工 Chat 推理代码。</summary>
554
+
555
+ ```python
556
+ import librosa
557
+ import numpy as np
558
+ import soundfile as sf
559
+
560
+ duplex = model.as_duplex(generate_audio=True) # 全双工,带语音输出
561
+ duplex.prepare(prompt_wav_path="Realtime-Venus-Audio/assets/HT_ref_audio.wav")
562
+
563
+ audio, _ = librosa.load(
564
+ "Realtime-Venus-Audio/assets/case_duplex.wav", sr=16000, mono=True
565
+ )
566
+ audio = np.concatenate([audio, np.zeros(10 * 16000, dtype=np.float32)])
567
+
568
+ chunk_samples = int(duplex.CHUNK_MS * duplex.SAMPLE_RATE / 1000)
569
+ total_chunks = max(1, (len(audio) + chunk_samples - 1) // chunk_samples)
570
+ timed_audio = []
571
+ for chunk_index in range(total_chunks):
572
+ chunk = audio[chunk_index * chunk_samples:(chunk_index + 1) * chunk_samples]
573
+ if len(chunk) < chunk_samples:
574
+ chunk = np.pad(chunk, (0, chunk_samples - len(chunk)))
575
+ duplex.streaming_prefill(audio_waveform=chunk)
576
+ result = duplex.streaming_generate(
577
+ max_new_speak_tokens_per_chunk=20,
578
+ decode_mode="sampling",
579
+ temperature=0.7,
580
+ top_k=20,
581
+ top_p=0.8,
582
+ listen_prob_scale=1.0,
583
+ )
584
+ state = "listen" if result["is_listen"] else f"speak> {result['text']}"
585
+ print(f"[{chunk_index + 1}/{total_chunks}] {state}", flush=True)
586
+ if result["audio_waveform"] is not None and not result["is_listen"]:
587
+ timed_audio.append((chunk_index, result["audio_waveform"]))
588
+
589
+ # 按原始时间线拼接生成的语音(24 kHz)
590
+ sample_rate = 24000
591
+ total_samples = max(
592
+ t * sample_rate + len(np.asarray(w, dtype=np.float32).squeeze())
593
+ for t, w in timed_audio
594
+ )
595
+ output = np.zeros(total_samples, dtype=np.float32)
596
+ for t, waveform in timed_audio:
597
+ w = np.asarray(waveform, dtype=np.float32).squeeze()
598
+ output[t * sample_rate: t * sample_rate + len(w)] += w
599
+ sf.write("output/audio_full_duplex.wav", np.clip(output, -1.0, 1.0), sample_rate)
600
+ print("Saved generated speech to output/audio_full_duplex.wav")
601
+ ```
602
+
603
+ </details>
604
+
605
+ ## 9. 📝 引用
606
+
607
+ 如果 Realtime-Venus 对你的工作有帮助,请引用技术报告:
608
+
609
+ ```bibtex
610
+ @article{zhao2026realtime,
611
+ title={{Realtime-Venus}: A full-duplex interaction system with asynchronous delegation},
612
+ author={{Venus Team,Ant Group;Tsinghua University}},
613
+ journal={arXiv preprint arXiv:2609.13814},
614
+ year={2026}
615
+ }
616
+ ```
617
+
618
+ ## 10. 📄 许可证
619
+
620
+ 本仓库采用 [Apache License 2.0](./LICENSE)。同时请查阅上游模型、第三方库以及使用本模型时
621
+ 所涉及数据的许可证与可接受使用条款。
Realtime-Venus-Audio/NOTICE ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ Realtime-Venus-Audio
2
+ Copyright 2026 The Realtime-Venus Team. All rights reserved.
3
+
4
+ This product includes software derived from MiniCPM-o 4.5
5
+ (https://github.com/OpenBMB/MiniCPM-o), Copyright The OpenBMB Team,
6
+ licensed under the Apache License, Version 2.0.
7
+
8
+ Modifications focus on audio-only understanding and full-duplex speech
9
+ conversation without visual input.
Realtime-Venus-Audio/added_tokens.json ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "</answer>": 151686,
3
+ "</backend>": 151751,
4
+ "</box>": 151674,
5
+ "</delegate>": 151749,
6
+ "</focus>": 151688,
7
+ "</image>": 151670,
8
+ "</image_id>": 151682,
9
+ "</image_save_to>": 151696,
10
+ "</line>": 151690,
11
+ "</perception>": 151692,
12
+ "</point>": 151678,
13
+ "</quad>": 151676,
14
+ "</ref>": 151672,
15
+ "</slice>": 151680,
16
+ "</source_image>": 151694,
17
+ "</think>": 151668,
18
+ "</tool_call>": 151658,
19
+ "</tool_response>": 151666,
20
+ "</unit>": 151684,
21
+ "<answer>": 151685,
22
+ "<backend>": 151750,
23
+ "<box>": 151673,
24
+ "<delegate>": 151748,
25
+ "<focus>": 151687,
26
+ "<image>": 151669,
27
+ "<image_id>": 151681,
28
+ "<image_save_to>": 151695,
29
+ "<line>": 151689,
30
+ "<perception>": 151691,
31
+ "<point>": 151677,
32
+ "<quad>": 151675,
33
+ "<ref>": 151671,
34
+ "<slice>": 151679,
35
+ "<source_image>": 151693,
36
+ "<think>": 151667,
37
+ "<tool_call>": 151657,
38
+ "<tool_response>": 151665,
39
+ "<unit>": 151683,
40
+ "<|audio_end|>": 151699,
41
+ "<|audio_start|>": 151697,
42
+ "<|audio|>": 151698,
43
+ "<|box_end|>": 151649,
44
+ "<|box_start|>": 151648,
45
+ "<|chunk_bos|>": 151719,
46
+ "<|chunk_eos|>": 151718,
47
+ "<|chunk_tts_bos|>": 151720,
48
+ "<|chunk_tts_eos|>": 151721,
49
+ "<|emotion_end|>": 151711,
50
+ "<|emotion_start|>": 151710,
51
+ "<|endoftext|>": 151643,
52
+ "<|file_sep|>": 151664,
53
+ "<|fim_middle|>": 151660,
54
+ "<|fim_pad|>": 151662,
55
+ "<|fim_prefix|>": 151659,
56
+ "<|fim_suffix|>": 151661,
57
+ "<|im_end|>": 151645,
58
+ "<|im_start|>": 151644,
59
+ "<|image_pad|>": 151655,
60
+ "<|interrupt|>": 151707,
61
+ "<|listen|>": 151705,
62
+ "<|object_ref_end|>": 151647,
63
+ "<|object_ref_start|>": 151646,
64
+ "<|pitch_end|>": 151715,
65
+ "<|pitch_start|>": 151714,
66
+ "<|quad_end|>": 151651,
67
+ "<|quad_start|>": 151650,
68
+ "<|repo_name|>": 151663,
69
+ "<|speak|>": 151706,
70
+ "<|speed_end|>": 151713,
71
+ "<|speed_start|>": 151712,
72
+ "<|spk_bos|>": 151700,
73
+ "<|spk_eos|>": 151702,
74
+ "<|spk|>": 151701,
75
+ "<|timbre_10|>": 151726,
76
+ "<|timbre_11|>": 151727,
77
+ "<|timbre_12|>": 151728,
78
+ "<|timbre_13|>": 151729,
79
+ "<|timbre_14|>": 151730,
80
+ "<|timbre_15|>": 151731,
81
+ "<|timbre_16|>": 151732,
82
+ "<|timbre_17|>": 151733,
83
+ "<|timbre_18|>": 151734,
84
+ "<|timbre_19|>": 151735,
85
+ "<|timbre_20|>": 151736,
86
+ "<|timbre_21|>": 151737,
87
+ "<|timbre_22|>": 151738,
88
+ "<|timbre_23|>": 151739,
89
+ "<|timbre_24|>": 151740,
90
+ "<|timbre_25|>": 151741,
91
+ "<|timbre_26|>": 151742,
92
+ "<|timbre_27|>": 151743,
93
+ "<|timbre_28|>": 151744,
94
+ "<|timbre_29|>": 151745,
95
+ "<|timbre_30|>": 151746,
96
+ "<|timbre_31|>": 151747,
97
+ "<|timbre_7|>": 151723,
98
+ "<|timbre_8|>": 151724,
99
+ "<|timbre_9|>": 151725,
100
+ "<|tts_bos|>": 151703,
101
+ "<|tts_eos|>": 151704,
102
+ "<|tts_pad|>": 151722,
103
+ "<|turn_bos|>": 151716,
104
+ "<|turn_eos|>": 151717,
105
+ "<|vad_end|>": 151709,
106
+ "<|vad_start|>": 151708,
107
+ "<|video_pad|>": 151656,
108
+ "<|vision_end|>": 151653,
109
+ "<|vision_pad|>": 151654,
110
+ "<|vision_start|>": 151652
111
+ }
Realtime-Venus-Audio/assets/HT_ref_audio.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cb8f06ba5080cdf548969138881fb8ad8b04e2516108f4e08ba0363b68b613ea
3
+ size 192590
Realtime-Venus-Audio/assets/case_duplex.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b5adc85ed4ee731615b3fc0adb328ffcbd4ba4841edad02cc8329c04f78e495f
3
+ size 1771710
Realtime-Venus-Audio/assets/case_offline.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cab2cfdc338706c26ea43c7de898ec67182456030f4680519d1e62c294723d57
3
+ size 245804
Realtime-Venus-Audio/assets/token2wav/campplus.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a6ac6a63997761ae2997373e2ee1c47040854b4b759ea41ec48e4e42df0f4d73
3
+ size 28303423
Realtime-Venus-Audio/assets/token2wav/flow.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:15ccff24256ff61537c7f8b51e025116b83405f3fb017b54b008fc97da115446
3
+ size 623466603
Realtime-Venus-Audio/assets/token2wav/flow.yaml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flow: !new:cosyvoice2.flow.flow.CausalMaskedDiffWithXvec
2
+ input_size: 512
3
+ output_size: 80
4
+ spk_embed_dim: 192
5
+ output_type: 'mel'
6
+ vocab_size: 6561
7
+ encoder: !new:cosyvoice2.transformer.upsample_encoder_v2.UpsampleConformerEncoderV2
8
+ input_size: 512
9
+ output_size: 512
10
+ input_layer: 'linear'
11
+ pre_lookahead_len: 3
12
+ num_blocks: 6
13
+ num_up_blocks: 4
14
+ up_stride: 2
15
+ up_scale_factor: 2
16
+ attention_heads: 8
17
+ pos_enc_layer_type: 'rel_pos_espnet'
18
+ selfattention_layer_type: 'rel_selfattn'
19
+ key_bias: true
20
+ linear_units: 2048
21
+ dropout_rate: 0.1
22
+ positional_dropout_rate: 0.1
23
+ attention_dropout_rate: 0.1
24
+ normalize_before: True
25
+ decoder: !new:cosyvoice2.flow.flow_matching.CausalConditionalCFM
26
+ inference_cfg_rate: 0.7
27
+ estimator: !new:cosyvoice2.flow.decoder_dit.DiT
28
+ in_channels: 320
29
+ out_channels: 80
30
+ mlp_ratio: 4.0
31
+ depth: 16
32
+ num_heads: 8
33
+ head_dim: 64
34
+ hidden_size: 512
Realtime-Venus-Audio/assets/token2wav/hift.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3386cc880324d4e98e05987b99107f49e40ed925b8ecc87c1f4939432d429879
3
+ size 83390254
Realtime-Venus-Audio/assets/token2wav/speech_tokenizer_v2_25hz.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d43342aa12163a80bf07bffb94c9de2e120a8df2f9917cd2f642e7f4219c6f71
3
+ size 496082973
Realtime-Venus-Audio/chat_template.jinja ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if tools %}
2
+ {{- '<|im_start|>system\n' }}
3
+ {%- if messages[0].role == 'system' %}
4
+ {{- messages[0].content + '\n\n' }}
5
+ {%- endif %}
6
+ {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
7
+ {%- for tool in tools %}
8
+ {{- "\n" }}
9
+ {{- tool | tojson }}
10
+ {%- endfor %}
11
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
12
+ {%- else %}
13
+ {%- if messages[0].role == 'system' %}
14
+ {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
15
+ {%- endif %}
16
+ {%- endif %}
17
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
18
+ {%- for message in messages[::-1] %}
19
+ {%- set index = (messages|length - 1) - loop.index0 %}
20
+ {%- if ns.multi_step_tool and message.role == "user" and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}
21
+ {%- set ns.multi_step_tool = false %}
22
+ {%- set ns.last_query_index = index %}
23
+ {%- endif %}
24
+ {%- endfor %}
25
+ {%- for message in messages %}
26
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
27
+ {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
28
+ {%- elif message.role == "assistant" %}
29
+ {%- set content = message.content %}
30
+ {%- set reasoning_content = '' %}
31
+ {%- if message.reasoning_content is defined and message.reasoning_content is not none %}
32
+ {%- set reasoning_content = message.reasoning_content %}
33
+ {%- else %}
34
+ {%- if '</think>' in message.content %}
35
+ {%- set content = message.content.split('</think>')[-1].lstrip('\n') %}
36
+ {%- set reasoning_content = message.content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
37
+ {%- endif %}
38
+ {%- endif %}
39
+ {%- if loop.index0 > ns.last_query_index %}
40
+ {%- if loop.last or (not loop.last and reasoning_content) %}
41
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
42
+ {%- else %}
43
+ {{- '<|im_start|>' + message.role + '\n' + content }}
44
+ {%- endif %}
45
+ {%- else %}
46
+ {{- '<|im_start|>' + message.role + '\n' + content }}
47
+ {%- endif %}
48
+ {%- if message.tool_calls %}
49
+ {%- for tool_call in message.tool_calls %}
50
+ {%- if (loop.first and content) or (not loop.first) %}
51
+ {{- '\n' }}
52
+ {%- endif %}
53
+ {%- if tool_call.function %}
54
+ {%- set tool_call = tool_call.function %}
55
+ {%- endif %}
56
+ {{- '<tool_call>\n{"name": "' }}
57
+ {{- tool_call.name }}
58
+ {{- '", "arguments": ' }}
59
+ {%- if tool_call.arguments is string %}
60
+ {{- tool_call.arguments }}
61
+ {%- else %}
62
+ {{- tool_call.arguments | tojson }}
63
+ {%- endif %}
64
+ {{- '}\n</tool_call>' }}
65
+ {%- endfor %}
66
+ {%- endif %}
67
+ {{- '<|im_end|>\n' }}
68
+ {%- elif message.role == "tool" %}
69
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
70
+ {{- '<|im_start|>user' }}
71
+ {%- endif %}
72
+ {{- '\n<tool_response>\n' }}
73
+ {{- message.content }}
74
+ {{- '\n</tool_response>' }}
75
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
76
+ {{- '<|im_end|>\n' }}
77
+ {%- endif %}
78
+ {%- endif %}
79
+ {%- endfor %}
80
+ {%- if add_generation_prompt %}
81
+ {{- '<|im_start|>assistant\n' }}
82
+ {%- if enable_thinking is defined and enable_thinking is false %}
83
+ {{- '<think>\n\n</think>\n\n' }}
84
+ {%- endif %}
85
+ {%- if use_tts_template is defined and use_tts_template is true %}
86
+ {{- '<|tts_bos|>' }}
87
+ {%- endif %}
88
+ {%- endif %}
Realtime-Venus-Audio/config.json ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "MiniCPMO"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "audio_chunk_length": 1.0,
8
+ "audio_config": {
9
+ "_attn_implementation_autoset": true,
10
+ "_name_or_path": "openai/whisper-medium",
11
+ "activation_dropout": 0.0,
12
+ "activation_function": "gelu",
13
+ "apply_spec_augment": false,
14
+ "architectures": [
15
+ "MiniCPMWhisperEncoder"
16
+ ],
17
+ "attention_dropout": 0.0,
18
+ "begin_suppress_tokens": [
19
+ 220,
20
+ 50257
21
+ ],
22
+ "bos_token_id": 50257,
23
+ "classifier_proj_size": 256,
24
+ "d_model": 1024,
25
+ "decoder_attention_heads": 16,
26
+ "decoder_ffn_dim": 4096,
27
+ "decoder_layerdrop": 0.0,
28
+ "decoder_layers": 24,
29
+ "decoder_start_token_id": 50258,
30
+ "dropout": 0.0,
31
+ "dtype": "float32",
32
+ "encoder_attention_heads": 16,
33
+ "encoder_ffn_dim": 4096,
34
+ "encoder_layerdrop": 0.0,
35
+ "encoder_layers": 24,
36
+ "eos_token_id": 50257,
37
+ "forced_decoder_ids": [
38
+ [
39
+ 1,
40
+ 50259
41
+ ],
42
+ [
43
+ 2,
44
+ 50359
45
+ ],
46
+ [
47
+ 3,
48
+ 50363
49
+ ]
50
+ ],
51
+ "init_std": 0.02,
52
+ "mask_feature_length": 10,
53
+ "mask_feature_min_masks": 0,
54
+ "mask_feature_prob": 0.0,
55
+ "mask_time_length": 10,
56
+ "mask_time_min_masks": 2,
57
+ "mask_time_prob": 0.05,
58
+ "max_length": 448,
59
+ "max_source_positions": 1500,
60
+ "max_target_positions": 448,
61
+ "median_filter_width": 7,
62
+ "model_type": "whisper",
63
+ "num_hidden_layers": 24,
64
+ "num_mel_bins": 80,
65
+ "pad_token_id": 50257,
66
+ "scale_embedding": false,
67
+ "suppress_tokens": [
68
+ 1,
69
+ 2,
70
+ 7,
71
+ 8,
72
+ 9,
73
+ 10,
74
+ 14,
75
+ 25,
76
+ 26,
77
+ 27,
78
+ 28,
79
+ 29,
80
+ 31,
81
+ 58,
82
+ 59,
83
+ 60,
84
+ 61,
85
+ 62,
86
+ 63,
87
+ 90,
88
+ 91,
89
+ 92,
90
+ 93,
91
+ 359,
92
+ 503,
93
+ 522,
94
+ 542,
95
+ 873,
96
+ 893,
97
+ 902,
98
+ 918,
99
+ 922,
100
+ 931,
101
+ 1350,
102
+ 1853,
103
+ 1982,
104
+ 2460,
105
+ 2627,
106
+ 3246,
107
+ 3253,
108
+ 3268,
109
+ 3536,
110
+ 3846,
111
+ 3961,
112
+ 4183,
113
+ 4667,
114
+ 6585,
115
+ 6647,
116
+ 7273,
117
+ 9061,
118
+ 9383,
119
+ 10428,
120
+ 10929,
121
+ 11938,
122
+ 12033,
123
+ 12331,
124
+ 12562,
125
+ 13793,
126
+ 14157,
127
+ 14635,
128
+ 15265,
129
+ 15618,
130
+ 16553,
131
+ 16604,
132
+ 18362,
133
+ 18956,
134
+ 20075,
135
+ 21675,
136
+ 22520,
137
+ 26130,
138
+ 26161,
139
+ 26435,
140
+ 28279,
141
+ 29464,
142
+ 31650,
143
+ 32302,
144
+ 32470,
145
+ 36865,
146
+ 42863,
147
+ 47425,
148
+ 49870,
149
+ 50254,
150
+ 50258,
151
+ 50358,
152
+ 50359,
153
+ 50360,
154
+ 50361,
155
+ 50362
156
+ ],
157
+ "use_cache": true,
158
+ "use_weighted_layer_sum": false,
159
+ "vocab_size": 51865
160
+ },
161
+ "audio_pool_step": 5,
162
+ "auto_map": {
163
+ "AutoConfig": "configuration_minicpmo.MiniCPMOConfig",
164
+ "AutoModel": "modeling_minicpmo.MiniCPMO",
165
+ "AutoModelForCausalLM": "modeling_minicpmo.MiniCPMO"
166
+ },
167
+ "batch_vision_input": true,
168
+ "bos_token_id": 151643,
169
+ "drop_vision_last_layer": false,
170
+ "dtype": "bfloat16",
171
+ "eos_token_id": 151645,
172
+ "head_dim": 128,
173
+ "hidden_act": "silu",
174
+ "hidden_size": 4096,
175
+ "image_size": 448,
176
+ "init_audio": true,
177
+ "init_tts": true,
178
+ "init_vision": true,
179
+ "initializer_range": 0.02,
180
+ "intermediate_size": 12288,
181
+ "layer_types": [
182
+ "full_attention",
183
+ "full_attention",
184
+ "full_attention",
185
+ "full_attention",
186
+ "full_attention",
187
+ "full_attention",
188
+ "full_attention",
189
+ "full_attention",
190
+ "full_attention",
191
+ "full_attention",
192
+ "full_attention",
193
+ "full_attention",
194
+ "full_attention",
195
+ "full_attention",
196
+ "full_attention",
197
+ "full_attention",
198
+ "full_attention",
199
+ "full_attention",
200
+ "full_attention",
201
+ "full_attention",
202
+ "full_attention",
203
+ "full_attention",
204
+ "full_attention",
205
+ "full_attention",
206
+ "full_attention",
207
+ "full_attention",
208
+ "full_attention",
209
+ "full_attention",
210
+ "full_attention",
211
+ "full_attention",
212
+ "full_attention",
213
+ "full_attention",
214
+ "full_attention",
215
+ "full_attention",
216
+ "full_attention",
217
+ "full_attention"
218
+ ],
219
+ "listen_speak_type": "asr",
220
+ "max_position_embeddings": 40960,
221
+ "max_window_layers": 36,
222
+ "model_type": "minicpmo",
223
+ "num_attention_heads": 32,
224
+ "num_hidden_layers": 36,
225
+ "num_key_value_heads": 8,
226
+ "patch_size": 14,
227
+ "query_num": 64,
228
+ "rms_norm_eps": 1e-06,
229
+ "rope_scaling": null,
230
+ "rope_theta": 1000000,
231
+ "slice_config": {
232
+ "max_slice_nums": 1,
233
+ "model_type": "minicpmv",
234
+ "patch_size": 14,
235
+ "scale_resolution": 448
236
+ },
237
+ "slice_mode": true,
238
+ "sliding_window": null,
239
+ "stream_input": true,
240
+ "tie_word_embeddings": false,
241
+ "transformers_version": "4.57.6",
242
+ "tts_config": {
243
+ "_attn_implementation_autoset": true,
244
+ "attention_type": "full_attention",
245
+ "attn_implementation": "eager",
246
+ "audio_bos_token_id": 151687,
247
+ "audio_tokenizer_sample_rate": 16000,
248
+ "audio_tokenizer_type": "s3tokenizer",
249
+ "aug_layer_loss_weight": false,
250
+ "aug_loss_weight": false,
251
+ "backbone_model": "llama",
252
+ "condition_type": "hidden_text_merge",
253
+ "cosyvoice_config_path": null,
254
+ "cosyvoice_model_dir": null,
255
+ "filter_tts_loss": false,
256
+ "hidden_act": "silu",
257
+ "hidden_size": 768,
258
+ "interleaved": false,
259
+ "intermediate_size": 3072,
260
+ "llm_dim": 4096,
261
+ "llm_dim_model_base": 256,
262
+ "llm_down_scale": false,
263
+ "llm_hidden_size": 4096,
264
+ "llm_intermediate_size": 768,
265
+ "long_weight": 0.1,
266
+ "max_position_embeddings": 4096,
267
+ "model_type": "minicpmtts",
268
+ "normalize_projected_hidden": true,
269
+ "num_attention_heads": 12,
270
+ "num_audio_tokens": 6562,
271
+ "num_hidden_layers": 20,
272
+ "num_key_value_heads": 12,
273
+ "num_mel_bins": 100,
274
+ "num_text_tokens": 152064,
275
+ "num_vq": 1,
276
+ "projector_type": "mlp",
277
+ "recomputed_chunks": 1,
278
+ "s3_stream_chunk_size": 25,
279
+ "s3_stream_generate": false,
280
+ "s3_stream_n_timesteps": 10,
281
+ "s3_stream_prelook_size": 3,
282
+ "short_weight": 0.1,
283
+ "streaming": false,
284
+ "streaming_audio_chunk_size": 50,
285
+ "streaming_sliding_window": false,
286
+ "streaming_sliding_window_audio_frame_rate": 50,
287
+ "streaming_sliding_window_audio_init_text_length": 10,
288
+ "streaming_sliding_window_audio_window_size": 300,
289
+ "streaming_sliding_window_average_speed": 5,
290
+ "streaming_sliding_window_fast_speed": 7,
291
+ "streaming_sliding_window_max_text_len": 500,
292
+ "streaming_sliding_window_slow_speed": 3,
293
+ "streaming_sliding_window_text_window_size": 50,
294
+ "streaming_text_chunk_max": 7,
295
+ "streaming_text_chunk_min": 3,
296
+ "streaming_text_reserved_len": 300,
297
+ "text_eos_token_id": 151692,
298
+ "tts_filter_loss_fix": false,
299
+ "use_llm_hidden_state": false,
300
+ "use_text": true,
301
+ "window_size": 2
302
+ },
303
+ "use_cache": true,
304
+ "use_image_id": true,
305
+ "use_sliding_window": false,
306
+ "version": "4.5",
307
+ "vision_batch_size": 16,
308
+ "vision_config": {
309
+ "_attn_implementation_autoset": true,
310
+ "attention_dropout": 0.0,
311
+ "hidden_act": "gelu_pytorch_tanh",
312
+ "hidden_size": 1152,
313
+ "image_size": 980,
314
+ "intermediate_size": 4304,
315
+ "layer_norm_eps": 1e-06,
316
+ "model_type": "siglip_vision_model",
317
+ "num_attention_heads": 16,
318
+ "num_channels": 3,
319
+ "num_hidden_layers": 27,
320
+ "patch_size": 14
321
+ },
322
+ "vocab_size": 151752
323
+ }
Realtime-Venus-Audio/configuration_minicpmo.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Copyright 2026 The OpenBMB Team. All rights reserved.
4
+ # Modifications Copyright 2026 The Realtime-Venus Team.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ import os
19
+ from typing import Union
20
+
21
+ from transformers import PretrainedConfig
22
+ from transformers import Qwen3Config
23
+ from transformers import WhisperConfig
24
+ from transformers.utils import logging
25
+
26
+ from .modeling_navit_siglip import SiglipVisionConfig
27
+
28
+ logger = logging.get_logger(__name__)
29
+
30
+
31
+ class MiniCPMVSliceConfig(PretrainedConfig):
32
+ model_type = "minicpmv"
33
+
34
+ def __init__(
35
+ self,
36
+ patch_size=14,
37
+ max_slice_nums=9,
38
+ scale_resolution=448,
39
+ **kwargs,
40
+ ):
41
+ super().__init__(**kwargs)
42
+ self.patch_size = patch_size
43
+ self.max_slice_nums = max_slice_nums
44
+ self.scale_resolution = scale_resolution
45
+
46
+ @classmethod
47
+ def from_pretrained(cls, pretrained_model_name_or_path: Union[str, os.PathLike], **kwargs) -> "PretrainedConfig":
48
+ cls._set_token_in_kwargs(kwargs)
49
+
50
+ config_dict, kwargs = cls.get_config_dict(pretrained_model_name_or_path, **kwargs)
51
+
52
+ if config_dict.get("model_type") == "minicpmv":
53
+ config_dict = config_dict["slice_config"]
54
+
55
+ if "model_type" in config_dict and hasattr(cls, "model_type") and config_dict["model_type"] != cls.model_type:
56
+ logger.warning(
57
+ f"You are using a model of type {config_dict['model_type']} to instantiate a model of type "
58
+ f"{cls.model_type}. This is not supported for all configurations of models and can yield errors."
59
+ )
60
+
61
+ return cls.from_dict(config_dict, **kwargs)
62
+
63
+
64
+ class MiniCPMTTSConfig(PretrainedConfig):
65
+ model_type = "minicpmtts"
66
+
67
+ def __init__(
68
+ self,
69
+ llm_dim: int = 2560,
70
+ llm_intermediate_size: int = 768,
71
+ llm_down_scale: bool = False,
72
+ llm_dim_model_base: int = 256,
73
+ projector_type: str = "mlp",
74
+ hidden_act: str = "silu",
75
+ aug_loss_weight: bool = False,
76
+ aug_layer_loss_weight: bool = False,
77
+ filter_tts_loss: bool = False,
78
+ tts_filter_loss_fix: bool = False,
79
+ long_weight: float = 0.1,
80
+ short_weight: float = 0.1,
81
+ hidden_size: int = 768,
82
+ intermediate_size: int = 3072,
83
+ num_attention_heads: int = 12,
84
+ num_hidden_layers: int = 20,
85
+ num_key_value_heads: int = 12,
86
+ max_position_embeddings: int = 4096,
87
+ num_audio_tokens: int = 4097,
88
+ num_text_tokens: int = 21178,
89
+ num_mel_bins: int = 100,
90
+ num_vq: int = 1,
91
+ use_llm_hidden_state: bool = False,
92
+ audio_bos_token_id: int = 21132,
93
+ text_eos_token_id: int = 21133,
94
+ use_text: bool = True,
95
+ streaming: bool = False,
96
+ streaming_text_chunk_min: int = 3,
97
+ streaming_text_chunk_max: int = 7,
98
+ streaming_text_reserved_len: int = 300,
99
+ streaming_audio_chunk_size: int = 50,
100
+ attn_implementation: str = "sdpa",
101
+ condition_type: str = "llm_hidden",
102
+ backbone_model: str = "llama",
103
+ audio_tokenizer_type: str = "wavtokenizer",
104
+ audio_tokenizer_sample_rate: int = 24000,
105
+ streaming_sliding_window: bool = False,
106
+ streaming_sliding_window_max_text_len: int = 500,
107
+ streaming_sliding_window_average_speed: int = 5,
108
+ streaming_sliding_window_fast_speed: int = 7,
109
+ streaming_sliding_window_slow_speed: int = 3,
110
+ streaming_sliding_window_audio_frame_rate: int = 50,
111
+ streaming_sliding_window_audio_init_text_length: int = 10,
112
+ streaming_sliding_window_audio_window_size: int = 300,
113
+ normalize_projected_hidden: bool = False,
114
+ interleaved: bool = False,
115
+ attention_type: str = "sliding_recompute",
116
+ recomputed_chunks: int = 1,
117
+ window_size: int = 2,
118
+ **kwargs,
119
+ ):
120
+ super().__init__(**kwargs)
121
+
122
+ self.llm_dim = llm_dim
123
+ self.llm_hidden_size = llm_dim
124
+ self.llm_intermediate_size = llm_intermediate_size
125
+ self.llm_down_scale = llm_down_scale
126
+ self.llm_dim_model_base = llm_dim_model_base
127
+ self.projector_type = projector_type
128
+ self.aug_loss_weight = aug_loss_weight
129
+ self.aug_layer_loss_weight = aug_layer_loss_weight
130
+ self.tts_filter_loss_fix = tts_filter_loss_fix
131
+ self.filter_tts_loss = filter_tts_loss
132
+ self.long_weight = long_weight
133
+ self.short_weight = short_weight
134
+ self.hidden_act = hidden_act
135
+
136
+ self.hidden_size = hidden_size
137
+ self.intermediate_size = intermediate_size
138
+ self.num_attention_heads = num_attention_heads
139
+ self.num_hidden_layers = num_hidden_layers
140
+ self.num_key_value_heads = num_key_value_heads
141
+ self.max_position_embeddings = max_position_embeddings
142
+ self.num_audio_tokens = num_audio_tokens
143
+ self.num_text_tokens = num_text_tokens
144
+ self.num_mel_bins = num_mel_bins
145
+ self.num_vq = num_vq
146
+ self.use_llm_hidden_state = use_llm_hidden_state
147
+ self.audio_bos_token_id = audio_bos_token_id
148
+ self.text_eos_token_id = text_eos_token_id
149
+ self.use_text = use_text
150
+ self.streaming = streaming
151
+ self.streaming_text_chunk_min = streaming_text_chunk_min
152
+ self.streaming_text_chunk_max = streaming_text_chunk_max
153
+ self.streaming_text_reserved_len = streaming_text_reserved_len
154
+ self.streaming_audio_chunk_size = streaming_audio_chunk_size
155
+ self.attn_implementation = attn_implementation
156
+ self.condition_type = condition_type
157
+ self.backbone_model = backbone_model
158
+ self.audio_tokenizer_type = audio_tokenizer_type
159
+ self.audio_tokenizer_sample_rate = audio_tokenizer_sample_rate
160
+
161
+ self.streaming_sliding_window = streaming_sliding_window
162
+ self.streaming_sliding_window_max_text_len = streaming_sliding_window_max_text_len
163
+ self.streaming_sliding_window_average_speed = streaming_sliding_window_average_speed
164
+ self.streaming_sliding_window_fast_speed = streaming_sliding_window_fast_speed
165
+ self.streaming_sliding_window_slow_speed = streaming_sliding_window_slow_speed
166
+ self.streaming_sliding_window_audio_frame_rate = streaming_sliding_window_audio_frame_rate
167
+ self.streaming_sliding_window_audio_init_text_length = streaming_sliding_window_audio_init_text_length
168
+ self.streaming_sliding_window_audio_window_size = streaming_sliding_window_audio_window_size
169
+
170
+ self.normalize_projected_hidden = normalize_projected_hidden
171
+
172
+ self.interleaved = interleaved
173
+ self.attention_type = attention_type
174
+ self.recomputed_chunks = recomputed_chunks
175
+ self.window_size = window_size
176
+
177
+
178
+ class MiniCPMOConfig(Qwen3Config):
179
+ model_type = "minicpmo"
180
+ keys_to_ignore_at_inference = ["past_key_values"]
181
+
182
+ default_vision_config = {
183
+ "hidden_size": 1152,
184
+ "image_size": 980,
185
+ "intermediate_size": 4304,
186
+ "model_type": "siglip",
187
+ "num_attention_heads": 16,
188
+ "num_hidden_layers": 27,
189
+ "patch_size": 14,
190
+ }
191
+
192
+ def __init__(
193
+ self,
194
+ use_cache=True,
195
+ query_num=64,
196
+ image_size=448,
197
+ drop_vision_last_layer=True,
198
+ batch_vision_input=True,
199
+ slice_config=None,
200
+ vision_config=None,
201
+ audio_config=None,
202
+ tts_config=None,
203
+ use_image_id=True,
204
+ vision_batch_size=16,
205
+ audio_pool_step=5,
206
+ audio_chunk_length=1.0,
207
+ stream_input=False,
208
+ listen_speak_type="asr",
209
+ init_vision=True,
210
+ init_audio=True,
211
+ init_tts=True,
212
+ **kwargs,
213
+ ):
214
+ self.use_cache = use_cache
215
+ self.query_num = query_num
216
+ self.image_size = image_size
217
+ self.drop_vision_last_layer = drop_vision_last_layer
218
+ self.batch_vision_input = batch_vision_input
219
+ self.use_image_id = use_image_id
220
+ self.vision_batch_size = vision_batch_size
221
+ self.audio_pool_step = audio_pool_step
222
+ self.audio_chunk_length = audio_chunk_length
223
+ self.stream_input = stream_input
224
+ self.listen_speak_type = listen_speak_type
225
+
226
+ self.init_vision = init_vision
227
+ self.init_audio = init_audio
228
+ self.init_tts = init_tts
229
+
230
+ if slice_config is None:
231
+ self.slice_config = MiniCPMVSliceConfig(max_slice_nums=1)
232
+ else:
233
+ self.slice_config = MiniCPMVSliceConfig(**slice_config)
234
+ self.slice_mode = True
235
+
236
+ # same as HuggingFaceM4/siglip-so400m-14-980-flash-attn2-navit add tgt_sizes
237
+ if vision_config is None:
238
+ self.vision_config = SiglipVisionConfig(**self.default_vision_config)
239
+ logger.info("vision_config is None, using default vision config")
240
+ elif isinstance(vision_config, dict):
241
+ self.vision_config = SiglipVisionConfig(**vision_config)
242
+ elif isinstance(vision_config, SiglipVisionConfig):
243
+ self.vision_config = vision_config
244
+
245
+ if audio_config is None:
246
+ self.audio_config = WhisperConfig()
247
+ elif isinstance(audio_config, dict):
248
+ self.audio_config = WhisperConfig(**audio_config)
249
+ elif isinstance(audio_config, WhisperConfig):
250
+ self.audio_config = audio_config
251
+
252
+ if tts_config is None:
253
+ self.tts_config = MiniCPMTTSConfig()
254
+ elif isinstance(tts_config, dict):
255
+ self.tts_config = MiniCPMTTSConfig(**tts_config)
256
+ elif isinstance(tts_config, MiniCPMTTSConfig):
257
+ self.tts_config = tts_config
258
+
259
+ self.patch_size = self.vision_config.patch_size
260
+
261
+ super().__init__(**kwargs)
Realtime-Venus-Audio/generation_config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 151643,
3
+ "do_sample": true,
4
+ "eos_token_id": [
5
+ 151645,
6
+ 151643
7
+ ],
8
+ "pad_token_id": 151643,
9
+ "temperature": 0.6,
10
+ "top_k": 20,
11
+ "top_p": 0.95,
12
+ "transformers_version": "4.57.6"
13
+ }
Realtime-Venus-Audio/merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
Realtime-Venus-Audio/model-00001-of-00004.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dc11f3b0ed783e85efc3ac8acf43fefd7d86fd1e6837209ba2722cf6d3b6ccc7
3
+ size 4900750800
Realtime-Venus-Audio/model-00002-of-00004.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bb0d68f6a34d45256516730cc07a30a674948bc444481784bc2de89eca275766
3
+ size 4915960936
Realtime-Venus-Audio/model-00003-of-00004.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6377dca09d445c0343690dad23f86e1b2084ca010206d4c3fbb94e43711f9040
3
+ size 4983069056
Realtime-Venus-Audio/model-00004-of-00004.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:188d68d0d1771083cbf7478e82c4493eae3e4e0816759d6005d16e2a150acecf
3
+ size 3944022140
Realtime-Venus-Audio/model.safetensors.index.json ADDED
The diff for this file is too large to render. See raw diff
 
Realtime-Venus-Audio/modeling_minicpmo.py ADDED
The diff for this file is too large to render. See raw diff
 
Realtime-Venus-Audio/modeling_navit_siglip.py ADDED
@@ -0,0 +1,981 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2024 Google AI and The HuggingFace Team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """PyTorch Siglip model."""
16
+ # Copied from HuggingFaceM4/siglip-so400m-14-980-flash-attn2-navit and add tgt_sizes
17
+
18
+
19
+ import math
20
+ import os
21
+ import warnings
22
+ from dataclasses import dataclass
23
+ from typing import Optional
24
+ from typing import Tuple
25
+ from typing import Union
26
+
27
+ import numpy as np
28
+ import torch
29
+ import torch.nn.functional as F
30
+ import torch.utils.checkpoint
31
+ from torch import nn
32
+ from torch.nn.init import _calculate_fan_in_and_fan_out
33
+ from transformers.activations import ACT2FN
34
+ from transformers.configuration_utils import PretrainedConfig
35
+ from transformers.modeling_attn_mask_utils import _prepare_4d_attention_mask
36
+ from transformers.modeling_outputs import BaseModelOutput
37
+ from transformers.modeling_outputs import BaseModelOutputWithPooling
38
+ from transformers.modeling_utils import PreTrainedModel
39
+ from transformers.utils import add_start_docstrings
40
+ from transformers.utils import add_start_docstrings_to_model_forward
41
+ from transformers.utils import is_flash_attn_2_available
42
+ from transformers.utils import logging
43
+ from transformers.utils import ModelOutput
44
+ from transformers.utils import replace_return_docstrings
45
+
46
+ logger = logging.get_logger(__name__)
47
+
48
+
49
+ class SiglipVisionConfig(PretrainedConfig):
50
+ r"""
51
+ This is the configuration class to store the configuration of a [`SiglipVisionModel`]. It is used to instantiate a
52
+ Siglip vision encoder according to the specified arguments, defining the model architecture. Instantiating a
53
+ configuration with the defaults will yield a similar configuration to that of the vision encoder of the Siglip
54
+ [google/siglip-base-patch16-224](https://huggingface.co/google/siglip-base-patch16-224) architecture.
55
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
56
+ documentation from [`PretrainedConfig`] for more information.
57
+ Args:
58
+ hidden_size (`int`, *optional*, defaults to 768):
59
+ Dimensionality of the encoder layers and the pooler layer.
60
+ intermediate_size (`int`, *optional*, defaults to 3072):
61
+ Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
62
+ num_hidden_layers (`int`, *optional*, defaults to 12):
63
+ Number of hidden layers in the Transformer encoder.
64
+ num_attention_heads (`int`, *optional*, defaults to 12):
65
+ Number of attention heads for each attention layer in the Transformer encoder.
66
+ num_channels (`int`, *optional*, defaults to 3):
67
+ Number of channels in the input images.
68
+ image_size (`int`, *optional*, defaults to 224):
69
+ The size (resolution) of each image.
70
+ patch_size (`int`, *optional*, defaults to 16):
71
+ The size (resolution) of each patch.
72
+ hidden_act (`str` or `function`, *optional*, defaults to `"gelu_pytorch_tanh"`):
73
+ The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
74
+ `"relu"`, `"selu"` and `"gelu_new"` ``"quick_gelu"` are supported.
75
+ layer_norm_eps (`float`, *optional*, defaults to 1e-06):
76
+ The epsilon used by the layer normalization layers.
77
+ attention_dropout (`float`, *optional*, defaults to 0.0):
78
+ The dropout ratio for the attention probabilities.
79
+ Example:
80
+ ```python
81
+ >>> from transformers import SiglipVisionConfig, SiglipVisionModel
82
+ >>> # Initializing a SiglipVisionConfig with google/siglip-base-patch16-224 style configuration
83
+ >>> configuration = SiglipVisionConfig()
84
+ >>> # Initializing a SiglipVisionModel (with random weights) from the google/siglip-base-patch16-224 style configuration
85
+ >>> model = SiglipVisionModel(configuration)
86
+ >>> # Accessing the model configuration
87
+ >>> configuration = model.config
88
+ ```"""
89
+
90
+ model_type = "siglip_vision_model"
91
+
92
+ def __init__(
93
+ self,
94
+ hidden_size=768,
95
+ intermediate_size=3072,
96
+ num_hidden_layers=12,
97
+ num_attention_heads=12,
98
+ num_channels=3,
99
+ image_size=224,
100
+ patch_size=16,
101
+ hidden_act="gelu_pytorch_tanh",
102
+ layer_norm_eps=1e-6,
103
+ attention_dropout=0.0,
104
+ **kwargs,
105
+ ):
106
+ super().__init__(**kwargs)
107
+
108
+ self.hidden_size = hidden_size
109
+ self.intermediate_size = intermediate_size
110
+ self.num_hidden_layers = num_hidden_layers
111
+ self.num_attention_heads = num_attention_heads
112
+ self.num_channels = num_channels
113
+ self.patch_size = patch_size
114
+ self.image_size = image_size
115
+ self.attention_dropout = attention_dropout
116
+ self.layer_norm_eps = layer_norm_eps
117
+ self.hidden_act = hidden_act
118
+
119
+ @classmethod
120
+ def from_pretrained(cls, pretrained_model_name_or_path: Union[str, os.PathLike], **kwargs) -> "PretrainedConfig":
121
+ cls._set_token_in_kwargs(kwargs)
122
+
123
+ config_dict, kwargs = cls.get_config_dict(pretrained_model_name_or_path, **kwargs)
124
+
125
+ # get the vision config dict if we are loading from SiglipConfig
126
+ if config_dict.get("model_type") == "siglip":
127
+ config_dict = config_dict["vision_config"]
128
+
129
+ if "model_type" in config_dict and hasattr(cls, "model_type") and config_dict["model_type"] != cls.model_type:
130
+ logger.warning(
131
+ f"You are using a model of type {config_dict['model_type']} to instantiate a model of type "
132
+ f"{cls.model_type}. This is not supported for all configurations of models and can yield errors."
133
+ )
134
+
135
+ return cls.from_dict(config_dict, **kwargs)
136
+
137
+
138
+ _CHECKPOINT_FOR_DOC = "google/siglip-base-patch16-224"
139
+
140
+ SIGLIP_PRETRAINED_MODEL_ARCHIVE_LIST = [
141
+ "google/siglip-base-patch16-224",
142
+ # See all SigLIP models at https://huggingface.co/models?filter=siglip
143
+ ]
144
+
145
+ if is_flash_attn_2_available():
146
+ from flash_attn import flash_attn_func
147
+ from flash_attn import flash_attn_varlen_func
148
+ from flash_attn.bert_padding import index_first_axis # noqa
149
+ from flash_attn.bert_padding import pad_input
150
+ from flash_attn.bert_padding import unpad_input
151
+
152
+
153
+ # Copied from transformers.models.llama.modeling_llama._get_unpad_data
154
+ def _get_unpad_data(attention_mask):
155
+ seqlens_in_batch = attention_mask.sum(dim=-1, dtype=torch.int32)
156
+ indices = torch.nonzero(attention_mask.flatten(), as_tuple=False).flatten()
157
+ max_seqlen_in_batch = seqlens_in_batch.max().item()
158
+ cu_seqlens = F.pad(torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.torch.int32), (1, 0))
159
+ return (
160
+ indices,
161
+ cu_seqlens,
162
+ max_seqlen_in_batch,
163
+ )
164
+
165
+
166
+ def _trunc_normal_(tensor, mean, std, a, b):
167
+ # Cut & paste from PyTorch official master until it's in a few official releases - RW
168
+ # Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf
169
+ def norm_cdf(x):
170
+ # Computes standard normal cumulative distribution function
171
+ return (1.0 + math.erf(x / math.sqrt(2.0))) / 2.0
172
+
173
+ if (mean < a - 2 * std) or (mean > b + 2 * std):
174
+ warnings.warn(
175
+ "mean is more than 2 std from [a, b] in nn.init.trunc_normal_. "
176
+ "The distribution of values may be incorrect.",
177
+ stacklevel=2,
178
+ )
179
+
180
+ # Values are generated by using a truncated uniform distribution and
181
+ # then using the inverse CDF for the normal distribution.
182
+ # Get upper and lower cdf values
183
+ l = norm_cdf((a - mean) / std)
184
+ u = norm_cdf((b - mean) / std)
185
+
186
+ # Uniformly fill tensor with values from [l, u], then translate to
187
+ # [2l-1, 2u-1].
188
+ tensor.uniform_(2 * l - 1, 2 * u - 1)
189
+
190
+ # Use inverse cdf transform for normal distribution to get truncated
191
+ # standard normal
192
+ if tensor.dtype in [torch.float16, torch.bfloat16]:
193
+ # The `erfinv_` op is not (yet?) defined in float16+cpu, bfloat16+gpu
194
+ og_dtype = tensor.dtype
195
+ tensor = tensor.to(torch.float32)
196
+ tensor.erfinv_()
197
+ tensor = tensor.to(og_dtype)
198
+ else:
199
+ tensor.erfinv_()
200
+
201
+ # Transform to proper mean, std
202
+ tensor.mul_(std * math.sqrt(2.0))
203
+ tensor.add_(mean)
204
+
205
+ # Clamp to ensure it's in the proper range
206
+ if tensor.dtype == torch.float16:
207
+ # The `clamp_` op is not (yet?) defined in float16+cpu
208
+ tensor = tensor.to(torch.float32)
209
+ tensor.clamp_(min=a, max=b)
210
+ tensor = tensor.to(torch.float16)
211
+ else:
212
+ tensor.clamp_(min=a, max=b)
213
+
214
+
215
+ def trunc_normal_tf_(
216
+ tensor: torch.Tensor,
217
+ mean: float = 0.0,
218
+ std: float = 1.0,
219
+ a: float = -2.0,
220
+ b: float = 2.0,
221
+ ) -> torch.Tensor:
222
+ """Fills the input Tensor with values drawn from a truncated
223
+ normal distribution. The values are effectively drawn from the
224
+ normal distribution :math:`\\mathcal{N}(\text{mean}, \text{std}^2)`
225
+ with values outside :math:`[a, b]` redrawn until they are within
226
+ the bounds. The method used for generating the random values works
227
+ best when :math:`a \\leq \text{mean} \\leq b`.
228
+ NOTE: this 'tf' variant behaves closer to Tensorflow / JAX impl where the
229
+ bounds [a, b] are applied when sampling the normal distribution with mean=0, std=1.0
230
+ and the result is subsquently scaled and shifted by the mean and std args.
231
+ Args:
232
+ tensor: an n-dimensional `torch.Tensor`
233
+ mean: the mean of the normal distribution
234
+ std: the standard deviation of the normal distribution
235
+ a: the minimum cutoff value
236
+ b: the maximum cutoff value
237
+ """
238
+ with torch.no_grad():
239
+ _trunc_normal_(tensor, 0, 1.0, a, b)
240
+ tensor.mul_(std).add_(mean)
241
+
242
+
243
+ def variance_scaling_(tensor, scale=1.0, mode="fan_in", distribution="normal"):
244
+ fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor)
245
+ if mode == "fan_in":
246
+ denom = fan_in
247
+ elif mode == "fan_out":
248
+ denom = fan_out
249
+ elif mode == "fan_avg":
250
+ denom = (fan_in + fan_out) / 2
251
+
252
+ variance = scale / denom
253
+
254
+ if distribution == "truncated_normal":
255
+ # constant is stddev of standard normal truncated to (-2, 2)
256
+ trunc_normal_tf_(tensor, std=math.sqrt(variance) / 0.87962566103423978)
257
+ elif distribution == "normal":
258
+ with torch.no_grad():
259
+ tensor.normal_(std=math.sqrt(variance))
260
+ elif distribution == "uniform":
261
+ bound = math.sqrt(3 * variance)
262
+ with torch.no_grad():
263
+ tensor.uniform_(-bound, bound)
264
+ else:
265
+ raise ValueError(f"invalid distribution {distribution}")
266
+
267
+
268
+ def lecun_normal_(tensor):
269
+ variance_scaling_(tensor, mode="fan_in", distribution="truncated_normal")
270
+
271
+
272
+ def default_flax_embed_init(tensor):
273
+ variance_scaling_(tensor, mode="fan_in", distribution="normal")
274
+
275
+
276
+ @dataclass
277
+ # Copied from transformers.models.clip.modeling_clip.CLIPVisionModelOutput with CLIP->Siglip
278
+ class SiglipVisionModelOutput(ModelOutput):
279
+ """
280
+ Base class for vision model's outputs that also contains image embeddings of the pooling of the last hidden states.
281
+ Args:
282
+ image_embeds (`torch.FloatTensor` of shape `(batch_size, output_dim)` *optional* returned when model is initialized with `with_projection=True`):
283
+ The image embeddings obtained by applying the projection layer to the pooler_output.
284
+ last_hidden_state (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`):
285
+ Sequence of hidden-states at the output of the last layer of the model.
286
+ hidden_states (`tuple(torch.FloatTensor)`, *optional*, returned when `output_hidden_states=True` is passed or when `config.output_hidden_states=True`):
287
+ Tuple of `torch.FloatTensor` (one for the output of the embeddings, if the model has an embedding layer, +
288
+ one for the output of each layer) of shape `(batch_size, sequence_length, hidden_size)`.
289
+ Hidden-states of the model at the output of each layer plus the optional initial embedding outputs.
290
+ attentions (`tuple(torch.FloatTensor)`, *optional*, returned when `output_attentions=True` is passed or when `config.output_attentions=True`):
291
+ Tuple of `torch.FloatTensor` (one for each layer) of shape `(batch_size, num_heads, sequence_length,
292
+ sequence_length)`.
293
+ Attentions weights after the attention softmax, used to compute the weighted average in the self-attention
294
+ heads.
295
+ """
296
+
297
+ image_embeds: Optional[torch.FloatTensor] = None
298
+ last_hidden_state: torch.FloatTensor = None
299
+ hidden_states: Optional[Tuple[torch.FloatTensor]] = None
300
+ attentions: Optional[Tuple[torch.FloatTensor]] = None
301
+
302
+
303
+ class SiglipVisionEmbeddings(nn.Module):
304
+ def __init__(self, config: SiglipVisionConfig):
305
+ super().__init__()
306
+ self.config = config
307
+ self.embed_dim = config.hidden_size
308
+ self.image_size = config.image_size
309
+ self.patch_size = config.patch_size
310
+
311
+ self.patch_embedding = nn.Conv2d(
312
+ in_channels=config.num_channels,
313
+ out_channels=self.embed_dim,
314
+ kernel_size=self.patch_size,
315
+ stride=self.patch_size,
316
+ padding="valid",
317
+ )
318
+
319
+ self.num_patches_per_side = self.image_size // self.patch_size
320
+ self.num_patches = self.num_patches_per_side**2
321
+ self.num_positions = self.num_patches
322
+ self.position_embedding = nn.Embedding(self.num_positions, self.embed_dim)
323
+
324
+ def forward(
325
+ self,
326
+ pixel_values: torch.FloatTensor,
327
+ patch_attention_mask: torch.BoolTensor,
328
+ tgt_sizes: Optional[torch.IntTensor] = None,
329
+ ) -> torch.Tensor:
330
+ batch_size = pixel_values.size(0)
331
+
332
+ patch_embeds = self.patch_embedding(pixel_values)
333
+ embeddings = patch_embeds.flatten(2).transpose(1, 2)
334
+
335
+ max_im_h, max_im_w = pixel_values.size(2), pixel_values.size(3)
336
+ max_nb_patches_h, max_nb_patches_w = (
337
+ max_im_h // self.patch_size,
338
+ max_im_w // self.patch_size,
339
+ )
340
+ boundaries = torch.arange(1 / self.num_patches_per_side, 1.0, 1 / self.num_patches_per_side)
341
+ position_ids = torch.full(
342
+ size=(
343
+ batch_size,
344
+ max_nb_patches_h * max_nb_patches_w,
345
+ ),
346
+ fill_value=0,
347
+ )
348
+
349
+ for batch_idx, p_attn_mask in enumerate(patch_attention_mask):
350
+ if tgt_sizes is not None:
351
+ nb_patches_h = tgt_sizes[batch_idx][0]
352
+ nb_patches_w = tgt_sizes[batch_idx][1]
353
+ else:
354
+ nb_patches_h = p_attn_mask[:, 0].sum()
355
+ nb_patches_w = p_attn_mask[0].sum()
356
+
357
+ fractional_coords_h = torch.arange(0, 1 - 1e-6, 1 / nb_patches_h)
358
+ fractional_coords_w = torch.arange(0, 1 - 1e-6, 1 / nb_patches_w)
359
+
360
+ bucket_coords_h = torch.bucketize(fractional_coords_h, boundaries, right=True)
361
+ bucket_coords_w = torch.bucketize(fractional_coords_w, boundaries, right=True)
362
+
363
+ pos_ids = (bucket_coords_h[:, None] * self.num_patches_per_side + bucket_coords_w).flatten()
364
+ position_ids[batch_idx][p_attn_mask.view(-1).cpu()] = pos_ids
365
+
366
+ position_ids = position_ids.to(self.position_embedding.weight.device)
367
+
368
+ embeddings = embeddings + self.position_embedding(position_ids)
369
+ return embeddings
370
+
371
+
372
+ class SiglipAttention(nn.Module):
373
+ """Multi-headed attention from 'Attention Is All You Need' paper"""
374
+
375
+ # Copied from transformers.models.clip.modeling_clip.CLIPAttention.__init__
376
+ def __init__(self, config):
377
+ super().__init__()
378
+ self.config = config
379
+ self.embed_dim = config.hidden_size
380
+ self.num_heads = config.num_attention_heads
381
+ self.head_dim = self.embed_dim // self.num_heads
382
+ if self.head_dim * self.num_heads != self.embed_dim:
383
+ raise ValueError(
384
+ f"embed_dim must be divisible by num_heads (got `embed_dim`: {self.embed_dim} and `num_heads`:"
385
+ f" {self.num_heads})."
386
+ )
387
+ self.scale = self.head_dim**-0.5
388
+ self.dropout = config.attention_dropout
389
+
390
+ self.k_proj = nn.Linear(self.embed_dim, self.embed_dim)
391
+ self.v_proj = nn.Linear(self.embed_dim, self.embed_dim)
392
+ self.q_proj = nn.Linear(self.embed_dim, self.embed_dim)
393
+ self.out_proj = nn.Linear(self.embed_dim, self.embed_dim)
394
+
395
+ def forward(
396
+ self,
397
+ hidden_states: torch.Tensor,
398
+ attention_mask: Optional[torch.Tensor] = None,
399
+ output_attentions: Optional[bool] = False,
400
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:
401
+ """Input shape: Batch x Time x Channel"""
402
+
403
+ batch_size, q_len, _ = hidden_states.size()
404
+
405
+ query_states = self.q_proj(hidden_states)
406
+ key_states = self.k_proj(hidden_states)
407
+ value_states = self.v_proj(hidden_states)
408
+
409
+ query_states = query_states.view(batch_size, q_len, self.num_heads, self.head_dim).transpose(1, 2)
410
+ key_states = key_states.view(batch_size, q_len, self.num_heads, self.head_dim).transpose(1, 2)
411
+ value_states = value_states.view(batch_size, q_len, self.num_heads, self.head_dim).transpose(1, 2)
412
+
413
+ k_v_seq_len = key_states.shape[-2]
414
+ attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) * self.scale
415
+
416
+ if attn_weights.size() != (batch_size, self.num_heads, q_len, k_v_seq_len):
417
+ raise ValueError(
418
+ f"Attention weights should be of size {(batch_size, self.num_heads, q_len, k_v_seq_len)}, but is"
419
+ f" {attn_weights.size()}"
420
+ )
421
+
422
+ if attention_mask is not None:
423
+ if attention_mask.size() != (batch_size, 1, q_len, k_v_seq_len):
424
+ raise ValueError(
425
+ f"Attention mask should be of size {(batch_size, 1, q_len, k_v_seq_len)}, but is {attention_mask.size()}"
426
+ )
427
+ attn_weights = attn_weights + attention_mask
428
+
429
+ # upcast attention to fp32
430
+ attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)
431
+ attn_weights = nn.functional.dropout(attn_weights, p=self.dropout, training=self.training)
432
+ attn_output = torch.matmul(attn_weights, value_states)
433
+
434
+ if attn_output.size() != (batch_size, self.num_heads, q_len, self.head_dim):
435
+ raise ValueError(
436
+ f"`attn_output` should be of size {(batch_size, self.num_heads, q_len, self.head_dim)}, but is"
437
+ f" {attn_output.size()}"
438
+ )
439
+
440
+ attn_output = attn_output.transpose(1, 2).contiguous()
441
+ attn_output = attn_output.reshape(batch_size, q_len, self.embed_dim)
442
+
443
+ attn_output = self.out_proj(attn_output)
444
+
445
+ return attn_output, attn_weights
446
+
447
+
448
+ class SiglipFlashAttention2(SiglipAttention):
449
+ """
450
+ Llama flash attention module. This module inherits from `LlamaAttention` as the weights of the module stays
451
+ untouched. The only required change would be on the forward pass where it needs to correctly call the public API of
452
+ flash attention and deal with padding tokens in case the input contains any of them.
453
+ """
454
+
455
+ def __init__(self, *args, **kwargs):
456
+ super().__init__(*args, **kwargs)
457
+ self.is_causal = False # Hack to make sure we don't use a causal mask
458
+
459
+ def forward(
460
+ self,
461
+ hidden_states: torch.Tensor,
462
+ attention_mask: Optional[torch.LongTensor] = None,
463
+ position_ids: Optional[torch.LongTensor] = None,
464
+ past_key_value: Optional[Tuple[torch.Tensor]] = None,
465
+ output_attentions: bool = False,
466
+ use_cache: bool = False,
467
+ **kwargs,
468
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:
469
+ output_attentions = False
470
+
471
+ bsz, q_len, _ = hidden_states.size()
472
+
473
+ query_states = self.q_proj(hidden_states)
474
+ key_states = self.k_proj(hidden_states)
475
+ value_states = self.v_proj(hidden_states)
476
+
477
+ # Flash attention requires the input to have the shape
478
+ # batch_size x seq_length x head_dim x hidden_dim
479
+ # therefore we just need to keep the original shape
480
+ query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
481
+ key_states = key_states.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
482
+ value_states = value_states.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
483
+
484
+ kv_seq_len = key_states.shape[-2]
485
+ if past_key_value is not None:
486
+ kv_seq_len += past_key_value.get_usable_length(kv_seq_len, self.layer_idx)
487
+ # cos, sin = self.rotary_emb(value_states, seq_len=kv_seq_len)
488
+ # query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin, position_ids)
489
+
490
+ # if past_key_value is not None:
491
+ # cache_kwargs = {"sin": sin, "cos": cos} # Specific to RoPE models
492
+ # key_states, value_states = past_key_value.update(key_states, value_states, self.layer_idx, cache_kwargs)
493
+
494
+ # TODO: These transpose are quite inefficient but Flash Attention requires the layout [batch_size, sequence_length, num_heads, head_dim]. We would need to refactor the KV cache
495
+ # to be able to avoid many of these transpose/reshape/view.
496
+ query_states = query_states.transpose(1, 2)
497
+ key_states = key_states.transpose(1, 2)
498
+ value_states = value_states.transpose(1, 2)
499
+
500
+ dropout_rate = self.dropout if self.training else 0.0
501
+
502
+ # In PEFT, usually we cast the layer norms in float32 for training stability reasons
503
+ # therefore the input hidden states gets silently casted in float32. Hence, we need
504
+ # cast them back in the correct dtype just to be sure everything works as expected.
505
+ # This might slowdown training & inference so it is recommended to not cast the LayerNorms
506
+ # in fp32. (LlamaRMSNorm handles it correctly)
507
+
508
+ input_dtype = query_states.dtype
509
+ if input_dtype == torch.float32:
510
+ if torch.is_autocast_enabled():
511
+ target_dtype = torch.get_autocast_gpu_dtype()
512
+ # Handle the case where the model is quantized
513
+ elif hasattr(self.config, "_pre_quantization_dtype"):
514
+ target_dtype = self.config._pre_quantization_dtype
515
+ else:
516
+ target_dtype = self.q_proj.weight.dtype
517
+
518
+ logger.warning_once(
519
+ "The input hidden states seems to be silently casted in float32, this might be related to the fact"
520
+ " you have upcasted embedding or layer norm layers in float32. We will cast back the input in"
521
+ f" {target_dtype}."
522
+ )
523
+
524
+ query_states = query_states.to(target_dtype)
525
+ key_states = key_states.to(target_dtype)
526
+ value_states = value_states.to(target_dtype)
527
+
528
+ attn_output = self._flash_attention_forward(
529
+ query_states,
530
+ key_states,
531
+ value_states,
532
+ attention_mask,
533
+ q_len,
534
+ dropout=dropout_rate,
535
+ )
536
+
537
+ attn_output = attn_output.reshape(bsz, q_len, self.embed_dim).contiguous()
538
+ attn_output = self.out_proj(attn_output)
539
+
540
+ if not output_attentions:
541
+ attn_weights = None
542
+
543
+ return attn_output, attn_weights
544
+
545
+ def _flash_attention_forward(
546
+ self,
547
+ query_states,
548
+ key_states,
549
+ value_states,
550
+ attention_mask,
551
+ query_length,
552
+ dropout=0.0,
553
+ softmax_scale=None,
554
+ ):
555
+ """
556
+ Calls the forward method of Flash Attention - if the input hidden states contain at least one padding token
557
+ first unpad the input, then computes the attention scores and pad the final attention scores.
558
+ Args:
559
+ query_states (`torch.Tensor`):
560
+ Input query states to be passed to Flash Attention API
561
+ key_states (`torch.Tensor`):
562
+ Input key states to be passed to Flash Attention API
563
+ value_states (`torch.Tensor`):
564
+ Input value states to be passed to Flash Attention API
565
+ attention_mask (`torch.Tensor`):
566
+ The padding mask - corresponds to a tensor of size `(batch_size, seq_len)` where 0 stands for the
567
+ position of padding tokens and 1 for the position of non-padding tokens.
568
+ dropout (`int`, *optional*):
569
+ Attention dropout
570
+ softmax_scale (`float`, *optional*):
571
+ The scaling of QK^T before applying softmax. Default to 1 / sqrt(head_dim)
572
+ """
573
+
574
+ # TODO: Remove the `query_length != 1` check once Flash Attention for RoCm is bumped to 2.1. For details, please see the comment in LlamaFlashAttention2 __init__.
575
+ causal = self.is_causal and query_length != 1
576
+
577
+ # Contains at least one padding token in the sequence
578
+ if attention_mask is not None:
579
+ batch_size = query_states.shape[0]
580
+ (
581
+ query_states,
582
+ key_states,
583
+ value_states,
584
+ indices_q,
585
+ cu_seq_lens,
586
+ max_seq_lens,
587
+ ) = self._upad_input(query_states, key_states, value_states, attention_mask, query_length)
588
+
589
+ cu_seqlens_q, cu_seqlens_k = cu_seq_lens
590
+ max_seqlen_in_batch_q, max_seqlen_in_batch_k = max_seq_lens
591
+
592
+ attn_output_unpad = flash_attn_varlen_func(
593
+ query_states,
594
+ key_states,
595
+ value_states,
596
+ cu_seqlens_q=cu_seqlens_q,
597
+ cu_seqlens_k=cu_seqlens_k,
598
+ max_seqlen_q=max_seqlen_in_batch_q,
599
+ max_seqlen_k=max_seqlen_in_batch_k,
600
+ dropout_p=dropout,
601
+ softmax_scale=softmax_scale,
602
+ causal=causal,
603
+ )
604
+
605
+ attn_output = pad_input(attn_output_unpad, indices_q, batch_size, query_length)
606
+ else:
607
+ attn_output = flash_attn_func(
608
+ query_states,
609
+ key_states,
610
+ value_states,
611
+ dropout,
612
+ softmax_scale=softmax_scale,
613
+ causal=causal,
614
+ )
615
+
616
+ return attn_output
617
+
618
+ def _upad_input(self, query_layer, key_layer, value_layer, attention_mask, query_length):
619
+ indices_k, cu_seqlens_k, max_seqlen_in_batch_k = _get_unpad_data(attention_mask)
620
+ batch_size, kv_seq_len, num_key_value_heads, head_dim = key_layer.shape
621
+
622
+ key_layer = index_first_axis(
623
+ key_layer.reshape(batch_size * kv_seq_len, num_key_value_heads, head_dim),
624
+ indices_k,
625
+ )
626
+ value_layer = index_first_axis(
627
+ value_layer.reshape(batch_size * kv_seq_len, num_key_value_heads, head_dim),
628
+ indices_k,
629
+ )
630
+ if query_length == kv_seq_len:
631
+ query_layer = index_first_axis(
632
+ query_layer.reshape(batch_size * kv_seq_len, self.num_heads, head_dim),
633
+ indices_k,
634
+ )
635
+ cu_seqlens_q = cu_seqlens_k
636
+ max_seqlen_in_batch_q = max_seqlen_in_batch_k
637
+ indices_q = indices_k
638
+ elif query_length == 1:
639
+ max_seqlen_in_batch_q = 1
640
+ cu_seqlens_q = torch.arange(
641
+ batch_size + 1, dtype=torch.int32, device=query_layer.device
642
+ ) # There is a memcpy here, that is very bad.
643
+ indices_q = cu_seqlens_q[:-1]
644
+ query_layer = query_layer.squeeze(1)
645
+ else:
646
+ # The -q_len: slice assumes left padding.
647
+ attention_mask = attention_mask[:, -query_length:]
648
+ query_layer, indices_q, cu_seqlens_q, max_seqlen_in_batch_q = unpad_input(query_layer, attention_mask)
649
+
650
+ return (
651
+ query_layer,
652
+ key_layer,
653
+ value_layer,
654
+ indices_q,
655
+ (cu_seqlens_q, cu_seqlens_k),
656
+ (max_seqlen_in_batch_q, max_seqlen_in_batch_k),
657
+ )
658
+
659
+
660
+ # Copied from transformers.models.clip.modeling_clip.CLIPMLP with CLIP->Siglip
661
+ class SiglipMLP(nn.Module):
662
+ def __init__(self, config):
663
+ super().__init__()
664
+ self.config = config
665
+ self.activation_fn = ACT2FN[config.hidden_act]
666
+ self.fc1 = nn.Linear(config.hidden_size, config.intermediate_size)
667
+ self.fc2 = nn.Linear(config.intermediate_size, config.hidden_size)
668
+
669
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
670
+ hidden_states = self.fc1(hidden_states)
671
+ hidden_states = self.activation_fn(hidden_states)
672
+ hidden_states = self.fc2(hidden_states)
673
+ return hidden_states
674
+
675
+
676
+ # Copied from transformers.models.clip.modeling_clip.CLIPEncoderLayer with CLIP->Siglip
677
+ class SiglipEncoderLayer(nn.Module):
678
+ def __init__(self, config: SiglipVisionConfig):
679
+ super().__init__()
680
+ self.embed_dim = config.hidden_size
681
+ self._use_flash_attention_2 = config._attn_implementation == "flash_attention_2"
682
+ self.self_attn = SiglipAttention(config) if not self._use_flash_attention_2 else SiglipFlashAttention2(config)
683
+ self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
684
+ self.mlp = SiglipMLP(config)
685
+ self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
686
+
687
+ def forward(
688
+ self,
689
+ hidden_states: torch.Tensor,
690
+ attention_mask: torch.Tensor,
691
+ output_attentions: Optional[bool] = False,
692
+ ) -> Tuple[torch.FloatTensor]:
693
+ """
694
+ Args:
695
+ hidden_states (`torch.FloatTensor`):
696
+ Input to the layer of shape `(batch, seq_len, embed_dim)`.
697
+ attention_mask (`torch.FloatTensor`):
698
+ Attention mask of shape `(batch, 1, q_len, k_v_seq_len)` where padding elements are indicated by very large negative values.
699
+ output_attentions (`bool`, *optional*, defaults to `False`):
700
+ Whether or not to return the attentions tensors of all attention layers. See `attentions` under
701
+ returned tensors for more detail.
702
+ """
703
+ residual = hidden_states
704
+
705
+ hidden_states = self.layer_norm1(hidden_states)
706
+ hidden_states, attn_weights = self.self_attn(
707
+ hidden_states=hidden_states,
708
+ attention_mask=attention_mask,
709
+ output_attentions=output_attentions,
710
+ )
711
+ hidden_states = residual + hidden_states
712
+
713
+ residual = hidden_states
714
+ hidden_states = self.layer_norm2(hidden_states)
715
+ hidden_states = self.mlp(hidden_states)
716
+ hidden_states = residual + hidden_states
717
+
718
+ outputs = (hidden_states,)
719
+
720
+ if output_attentions:
721
+ outputs += (attn_weights,)
722
+
723
+ return outputs
724
+
725
+
726
+ class SiglipPreTrainedModel(PreTrainedModel):
727
+ """
728
+ An abstract class to handle weights initialization and a simple interface for downloading and loading pretrained
729
+ models.
730
+ """
731
+
732
+ config_class = SiglipVisionConfig
733
+ base_model_prefix = "siglip"
734
+ supports_gradient_checkpointing = True
735
+
736
+ def _init_weights(self, module):
737
+ """Initialize the weights"""
738
+
739
+ if isinstance(module, SiglipVisionEmbeddings):
740
+ width = self.config.hidden_size
741
+ nn.init.normal_(module.position_embedding.weight, std=1 / np.sqrt(width))
742
+ elif isinstance(module, nn.Embedding):
743
+ default_flax_embed_init(module.weight)
744
+ elif isinstance(module, SiglipAttention):
745
+ nn.init.normal_(module.q_proj.weight)
746
+ nn.init.normal_(module.k_proj.weight)
747
+ nn.init.normal_(module.v_proj.weight)
748
+ nn.init.normal_(module.out_proj.weight)
749
+ nn.init.zeros_(module.q_proj.bias)
750
+ nn.init.zeros_(module.k_proj.bias)
751
+ nn.init.zeros_(module.v_proj.bias)
752
+ nn.init.zeros_(module.out_proj.bias)
753
+ elif isinstance(module, SiglipMLP):
754
+ nn.init.normal_(module.fc1.weight)
755
+ nn.init.normal_(module.fc2.weight)
756
+ nn.init.normal_(module.fc1.bias, std=1e-6)
757
+ nn.init.normal_(module.fc2.bias, std=1e-6)
758
+ elif isinstance(module, (nn.Linear, nn.Conv2d)):
759
+ lecun_normal_(module.weight)
760
+ if module.bias is not None:
761
+ nn.init.zeros_(module.bias)
762
+ elif isinstance(module, nn.LayerNorm):
763
+ module.bias.data.zero_()
764
+ module.weight.data.fill_(1.0)
765
+
766
+
767
+ SIGLIP_START_DOCSTRING = r"""
768
+ This model inherits from [`PreTrainedModel`]. Check the superclass documentation for the generic methods the
769
+ library implements for all its model (such as downloading or saving, resizing the input embeddings, pruning heads
770
+ etc.)
771
+ This model is also a PyTorch [torch.nn.Module](https://pytorch.org/docs/stable/nn.html#torch.nn.Module) subclass.
772
+ Use it as a regular PyTorch Module and refer to the PyTorch documentation for all matter related to general usage
773
+ and behavior.
774
+ Parameters:
775
+ config ([`SiglipVisionConfig`]): Model configuration class with all the parameters of the model.
776
+ Initializing with a config file does not load the weights associated with the model, only the
777
+ configuration. Check out the [`~PreTrainedModel.from_pretrained`] method to load the model weights.
778
+ """
779
+
780
+
781
+ SIGLIP_VISION_INPUTS_DOCSTRING = r"""
782
+ Args:
783
+ pixel_values (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`):
784
+ Pixel values. Padding will be ignored by default should you provide it. Pixel values can be obtained using
785
+ [`AutoImageProcessor`]. See [`CLIPImageProcessor.__call__`] for details.
786
+ output_attentions (`bool`, *optional*):
787
+ Whether or not to return the attentions tensors of all attention layers. See `attentions` under returned
788
+ tensors for more detail.
789
+ output_hidden_states (`bool`, *optional*):
790
+ Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors for
791
+ more detail.
792
+ return_dict (`bool`, *optional*):
793
+ Whether or not to return a [`~utils.ModelOutput`] instead of a plain tuple.
794
+ """
795
+
796
+
797
+ # Copied from transformers.models.clip.modeling_clip.CLIPEncoder with CLIP->Siglip
798
+ class SiglipEncoder(nn.Module):
799
+ """
800
+ Transformer encoder consisting of `config.num_hidden_layers` self attention layers. Each layer is a
801
+ [`SiglipEncoderLayer`].
802
+ Args:
803
+ config: SiglipConfig
804
+ """
805
+
806
+ def __init__(self, config: SiglipVisionConfig):
807
+ super().__init__()
808
+ self.config = config
809
+ self.layers = nn.ModuleList([SiglipEncoderLayer(config) for _ in range(config.num_hidden_layers)])
810
+ self.gradient_checkpointing = False
811
+
812
+ # Ignore copy
813
+ def forward(
814
+ self,
815
+ inputs_embeds,
816
+ attention_mask: Optional[torch.Tensor] = None,
817
+ output_attentions: Optional[bool] = None,
818
+ output_hidden_states: Optional[bool] = None,
819
+ return_dict: Optional[bool] = None,
820
+ ) -> Union[Tuple, BaseModelOutput]:
821
+ r"""
822
+ Args:
823
+ inputs_embeds (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`):
824
+ Optionally, instead of passing `input_ids` you can choose to directly pass an embedded representation.
825
+ This is useful if you want more control over how to convert `input_ids` indices into associated vectors
826
+ than the model's internal embedding lookup matrix.
827
+ attention_mask (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*):
828
+ Mask to avoid performing attention on padding token indices. Mask values selected in `[0, 1]`:
829
+ - 1 for tokens that are **not masked**,
830
+ - 0 for tokens that are **masked**.
831
+ [What are attention masks?](../glossary#attention-mask)
832
+ output_attentions (`bool`, *optional*):
833
+ Whether or not to return the attentions tensors of all attention layers. See `attentions` under
834
+ returned tensors for more detail.
835
+ output_hidden_states (`bool`, *optional*):
836
+ Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors
837
+ for more detail.
838
+ return_dict (`bool`, *optional*):
839
+ Whether or not to return a [`~utils.ModelOutput`] instead of a plain tuple.
840
+ """
841
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
842
+ output_hidden_states = (
843
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
844
+ )
845
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
846
+
847
+ encoder_states = () if output_hidden_states else None
848
+ all_attentions = () if output_attentions else None
849
+
850
+ hidden_states = inputs_embeds
851
+ for encoder_layer in self.layers:
852
+ if output_hidden_states:
853
+ encoder_states = encoder_states + (hidden_states,)
854
+ if self.gradient_checkpointing and self.training:
855
+ layer_outputs = self._gradient_checkpointing_func(
856
+ encoder_layer.__call__,
857
+ hidden_states,
858
+ attention_mask,
859
+ output_attentions,
860
+ )
861
+ else:
862
+ layer_outputs = encoder_layer(
863
+ hidden_states,
864
+ attention_mask,
865
+ output_attentions=output_attentions,
866
+ )
867
+
868
+ hidden_states = layer_outputs[0]
869
+
870
+ if output_attentions:
871
+ all_attentions = all_attentions + (layer_outputs[1],)
872
+
873
+ if output_hidden_states:
874
+ encoder_states = encoder_states + (hidden_states,)
875
+
876
+ if not return_dict:
877
+ return tuple(v for v in [hidden_states, encoder_states, all_attentions] if v is not None)
878
+ return BaseModelOutput(
879
+ last_hidden_state=hidden_states,
880
+ hidden_states=encoder_states,
881
+ attentions=all_attentions,
882
+ )
883
+
884
+
885
+ @add_start_docstrings(
886
+ """The vision model from SigLIP without any head or projection on top.""",
887
+ SIGLIP_START_DOCSTRING,
888
+ )
889
+ class SiglipVisionTransformer(SiglipPreTrainedModel):
890
+ config_class = SiglipVisionConfig
891
+ main_input_name = "pixel_values"
892
+ _supports_flash_attn_2 = True
893
+ _no_split_modules = []
894
+
895
+ def __init__(self, config: SiglipVisionConfig):
896
+ super().__init__(config)
897
+ self.config = config
898
+ embed_dim = config.hidden_size
899
+
900
+ self.embeddings = SiglipVisionEmbeddings(config)
901
+ self.encoder = SiglipEncoder(config)
902
+ self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
903
+ self._use_flash_attention_2 = config._attn_implementation == "flash_attention_2"
904
+
905
+ # Initialize weights and apply final processing
906
+ self.post_init()
907
+
908
+ def get_input_embeddings(self) -> nn.Module:
909
+ return self.embeddings.patch_embedding
910
+
911
+ @add_start_docstrings_to_model_forward(SIGLIP_VISION_INPUTS_DOCSTRING)
912
+ @replace_return_docstrings(output_type=BaseModelOutputWithPooling, config_class=SiglipVisionConfig)
913
+ def forward(
914
+ self,
915
+ pixel_values,
916
+ patch_attention_mask: Optional[torch.BoolTensor] = None,
917
+ tgt_sizes: Optional[torch.IntTensor] = None,
918
+ output_attentions: Optional[bool] = None,
919
+ output_hidden_states: Optional[bool] = None,
920
+ return_dict: Optional[bool] = None,
921
+ ) -> Union[Tuple, BaseModelOutputWithPooling]:
922
+ r"""
923
+ Returns:
924
+ """
925
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
926
+ output_hidden_states = (
927
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
928
+ )
929
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
930
+
931
+ batch_size = pixel_values.size(0)
932
+ if patch_attention_mask is None:
933
+ patch_attention_mask = torch.ones(
934
+ size=(
935
+ batch_size,
936
+ pixel_values.size(2) // self.config.patch_size,
937
+ pixel_values.size(3) // self.config.patch_size,
938
+ ),
939
+ dtype=torch.bool,
940
+ device=pixel_values.device,
941
+ )
942
+
943
+ hidden_states = self.embeddings(
944
+ pixel_values=pixel_values,
945
+ patch_attention_mask=patch_attention_mask,
946
+ tgt_sizes=tgt_sizes,
947
+ )
948
+
949
+ patch_attention_mask = patch_attention_mask.view(batch_size, -1)
950
+ # The call to `_upad_input` in `_flash_attention_forward` is expensive
951
+ # So when the `patch_attention_mask` is full of 1s (i.e. attending to the whole sequence),
952
+ # avoiding passing the attention_mask, which is equivalent to attending to the full sequence
953
+ if not torch.any(~patch_attention_mask):
954
+ attention_mask = None
955
+ else:
956
+ attention_mask = (
957
+ _prepare_4d_attention_mask(patch_attention_mask, hidden_states.dtype)
958
+ if not self._use_flash_attention_2
959
+ else patch_attention_mask
960
+ )
961
+
962
+ encoder_outputs = self.encoder(
963
+ inputs_embeds=hidden_states,
964
+ attention_mask=attention_mask,
965
+ output_attentions=output_attentions,
966
+ output_hidden_states=output_hidden_states,
967
+ return_dict=return_dict,
968
+ )
969
+
970
+ last_hidden_state = encoder_outputs[0]
971
+ last_hidden_state = self.post_layernorm(last_hidden_state)
972
+
973
+ if not return_dict:
974
+ return (last_hidden_state, None) + encoder_outputs[1:]
975
+
976
+ return BaseModelOutputWithPooling(
977
+ last_hidden_state=last_hidden_state,
978
+ pooler_output=None,
979
+ hidden_states=encoder_outputs.hidden_states,
980
+ attentions=encoder_outputs.attentions,
981
+ )
Realtime-Venus-Audio/preprocessor_config.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_pool_step": 5,
3
+ "auto_map": {
4
+ "AutoFeatureExtractor": "processing_minicpmo.MiniCPMAAudioProcessor",
5
+ "AutoImageProcessor": "processing_minicpmo.MiniCPMVImageProcessor",
6
+ "AutoProcessor": "processing_minicpmo.MiniCPMOProcessor"
7
+ },
8
+ "chunk_length": 30,
9
+ "dither": 0.0,
10
+ "dynamic_log_norm": true,
11
+ "dynamic_range_db": 8.0,
12
+ "feature_extractor_type": "MiniCPMAAudioProcessor",
13
+ "feature_size": 80,
14
+ "hop_length": 160,
15
+ "im_end": "</image>",
16
+ "im_id_end": "</image_id>",
17
+ "im_id_start": "<image_id>",
18
+ "im_start": "<image>",
19
+ "image_feature_size": 64,
20
+ "image_processor_type": "MiniCPMVImageProcessor",
21
+ "log_floor_db": -10.0,
22
+ "max_slice_nums": 9,
23
+ "n_fft": 400,
24
+ "n_samples": 480000,
25
+ "nb_max_frames": 3000,
26
+ "norm_mean": [
27
+ 0.5,
28
+ 0.5,
29
+ 0.5
30
+ ],
31
+ "norm_std": [
32
+ 0.5,
33
+ 0.5,
34
+ 0.5
35
+ ],
36
+ "padding_side": "right",
37
+ "padding_value": 0.0,
38
+ "patch_size": 14,
39
+ "processor_class": "MiniCPMOProcessor",
40
+ "return_attention_mask": false,
41
+ "sampling_rate": 16000,
42
+ "scale_resolution": 448,
43
+ "slice_end": "</slice>",
44
+ "slice_mode": true,
45
+ "slice_start": "<slice>",
46
+ "unk": "<unk>",
47
+ "use_image_id": true,
48
+ "version": 4.5
49
+ }
Realtime-Venus-Audio/processing_minicpmo.py ADDED
@@ -0,0 +1,1666 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Copyright 2026 The OpenBMB Team. All rights reserved.
4
+ # Modifications Copyright 2026 The Realtime-Venus Team.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ import copy
19
+ import math
20
+ import re
21
+ from typing import Any
22
+ from typing import Dict
23
+ from typing import List
24
+ from typing import Optional
25
+ from typing import Tuple
26
+ from typing import Union
27
+
28
+ import numpy as np
29
+ import torch
30
+ from PIL import Image
31
+ from transformers import AutoImageProcessor
32
+ from transformers.audio_utils import spectrogram
33
+ from transformers.audio_utils import window_function
34
+ from transformers.image_processing_utils import BaseImageProcessor
35
+ from transformers.image_processing_utils import BatchFeature
36
+ from transformers.image_transforms import to_channel_dimension_format
37
+ from transformers.image_utils import ChannelDimension
38
+ from transformers.image_utils import ImageInput
39
+ from transformers.image_utils import infer_channel_dimension_format
40
+ from transformers.image_utils import is_torch_tensor
41
+ from transformers.image_utils import to_numpy_array
42
+ from transformers.image_utils import valid_images
43
+ from transformers.models.whisper.feature_extraction_whisper import WhisperFeatureExtractor
44
+ from transformers.processing_utils import ProcessorMixin
45
+ from transformers.tokenization_utils_base import PreTokenizedInput
46
+ from transformers.tokenization_utils_base import TextInput
47
+ from transformers.utils import is_torch_device
48
+ from transformers.utils import is_torch_dtype
49
+ from transformers.utils import requires_backends
50
+ from transformers.utils import TensorType
51
+
52
+
53
+ def recursive_converter(converter, value):
54
+ if isinstance(value, list):
55
+ new_value = []
56
+ for v in value:
57
+ new_value += [recursive_converter(converter, v)]
58
+ return new_value
59
+ else:
60
+ return converter(value)
61
+
62
+
63
+ class MiniCPMOBatchFeature(BatchFeature):
64
+ """Extend from BatchFeature for supporting various image size"""
65
+
66
+ def __init__(self, data: Optional[Dict[str, Any]] = None, tensor_type: Union[None, str, TensorType] = None):
67
+ super().__init__(data)
68
+ self.convert_to_tensors(tensor_type=tensor_type)
69
+
70
+ def convert_to_tensors(self, tensor_type: Optional[Union[str, TensorType]] = None):
71
+ if tensor_type is None:
72
+ return self
73
+
74
+ is_tensor, as_tensor = self._get_is_as_tensor_fns(tensor_type)
75
+
76
+ def converter(value):
77
+ try:
78
+ if not is_tensor(value):
79
+ tensor = as_tensor(value)
80
+ return tensor
81
+ except: # noqa E722
82
+ if key == "overflowing_values":
83
+ raise ValueError("Unable to create tensor returning overflowing values of different lengths. ")
84
+ raise ValueError(
85
+ "Unable to create tensor, you should probably activate padding "
86
+ "with 'padding=True' to have batched tensors with the same length."
87
+ )
88
+
89
+ for key, value in self.items():
90
+ self[key] = recursive_converter(converter, value)
91
+ return self
92
+
93
+ def to(self, *args, **kwargs) -> "MiniCPMOBatchFeature":
94
+ requires_backends(self, ["torch"])
95
+ import torch
96
+
97
+ def cast_tensor(v):
98
+ if not torch.is_tensor(v):
99
+ return v
100
+
101
+ if torch.is_floating_point(v):
102
+ return v.to(*args, **kwargs)
103
+ elif device is not None:
104
+ return v.to(device=device)
105
+ else:
106
+ return v
107
+
108
+ new_data = {}
109
+ device = kwargs.get("device")
110
+ if device is None and len(args) > 0:
111
+ arg = args[0]
112
+ if is_torch_dtype(arg):
113
+ pass
114
+ elif isinstance(arg, str) or is_torch_device(arg) or isinstance(arg, int):
115
+ device = arg
116
+ else:
117
+ raise ValueError(f"Attempting to cast a BatchFeature to type {str(arg)}. This is not supported.")
118
+
119
+ # We cast only floating point tensors to avoid issues with tokenizers casting `LongTensor` to `FloatTensor`
120
+ for k, v in self.items():
121
+ new_data[k] = recursive_converter(cast_tensor, v)
122
+ self.data = new_data
123
+ return self
124
+
125
+
126
+ class MiniCPMVImageProcessor(BaseImageProcessor):
127
+ model_input_names = ["pixel_values"]
128
+
129
+ def __init__(self, max_slice_nums=9, scale_resolution=448, patch_size=14, **kwargs):
130
+ super().__init__(**kwargs)
131
+ self.max_slice_nums = max_slice_nums
132
+ self.scale_resolution = scale_resolution
133
+ self.patch_size = patch_size
134
+ self.use_image_id = kwargs.pop("use_image_id", False)
135
+ self.image_feature_size = kwargs.pop("image_feature_size", 64)
136
+ self.im_start_token = kwargs.pop("im_start", "<image>")
137
+ self.im_end_token = kwargs.pop("im_end", "</image>")
138
+ self.slice_start_token = kwargs.pop("slice_start", "<slice>")
139
+ self.slice_end_token = kwargs.pop("slice_end", "</slice>")
140
+ self.unk_token = kwargs.pop("unk", "<unk>")
141
+ self.im_id_start = kwargs.pop("im_id_start", "<image_id>")
142
+ self.im_id_end = kwargs.pop("im_id_end", "</image_id>")
143
+ self.slice_mode = kwargs.pop("slice_mode", True)
144
+
145
+ self.mean = np.array(kwargs.pop("norm_mean", [0.5, 0.5, 0.5]))
146
+ self.std = np.array(kwargs.pop("norm_std", [0.5, 0.5, 0.5]))
147
+ self.version = kwargs.pop("version", 2.0)
148
+
149
+ @staticmethod
150
+ def ensure_divide(length, patch_size):
151
+ return max(round(length / patch_size) * patch_size, patch_size)
152
+
153
+ def find_best_resize(self, original_size, scale_resolution, patch_size, allow_upscale=False):
154
+ width, height = original_size
155
+ if (width * height > scale_resolution * scale_resolution) or allow_upscale:
156
+ r = width / height
157
+ height = int(scale_resolution / math.sqrt(r))
158
+ width = int(height * r)
159
+ best_width = self.ensure_divide(width, patch_size)
160
+ best_height = self.ensure_divide(height, patch_size)
161
+ return best_width, best_height
162
+
163
+ def get_refine_size(self, original_size, grid, scale_resolution, patch_size, allow_upscale=False):
164
+ width, height = original_size
165
+ grid_x, grid_y = grid
166
+
167
+ refine_width = self.ensure_divide(width, grid_x)
168
+ refine_height = self.ensure_divide(height, grid_y)
169
+
170
+ grid_width = refine_width / grid_x
171
+ grid_height = refine_height / grid_y
172
+
173
+ best_grid_size = self.find_best_resize(
174
+ (grid_width, grid_height), scale_resolution, patch_size, allow_upscale=allow_upscale
175
+ )
176
+ refine_size = (best_grid_size[0] * grid_x, best_grid_size[1] * grid_y)
177
+ return refine_size
178
+
179
+ @staticmethod
180
+ def split_to_patches(image, grid):
181
+ patches = []
182
+ width, height = image.size
183
+ grid_x = int(width / grid[0])
184
+ grid_y = int(height / grid[1])
185
+ for i in range(0, height, grid_y):
186
+ images = []
187
+ for j in range(0, width, grid_x):
188
+ box = (j, i, j + grid_x, i + grid_y)
189
+ patch = image.crop(box)
190
+ images.append(patch)
191
+ patches.append(images)
192
+ return patches
193
+
194
+ def slice_image(self, image, max_slice_nums=9, scale_resolution=448, patch_size=14, never_split=False):
195
+ original_size = image.size
196
+ source_image = None
197
+ best_grid = self.get_sliced_grid(original_size, max_slice_nums, never_split)
198
+ patches = []
199
+
200
+ if best_grid is None:
201
+ # dont need to slice, upsample
202
+ best_size = self.find_best_resize(original_size, scale_resolution, patch_size, allow_upscale=True)
203
+ source_image = image.resize(best_size, resample=Image.Resampling.BICUBIC)
204
+ else:
205
+ # source image, down-sampling and ensure divided by patch_size
206
+ best_resize = self.find_best_resize(original_size, scale_resolution, patch_size)
207
+ source_image = image.copy().resize(best_resize, resample=Image.Resampling.BICUBIC)
208
+ refine_size = self.get_refine_size(
209
+ original_size, best_grid, scale_resolution, patch_size, allow_upscale=True
210
+ )
211
+ refine_image = image.resize(refine_size, resample=Image.Resampling.BICUBIC)
212
+ patches = self.split_to_patches(refine_image, best_grid)
213
+
214
+ return source_image, patches, best_grid
215
+
216
+ def get_grid_placeholder(self, grid):
217
+ if grid is None:
218
+ return ""
219
+ slice_image_placeholder = (
220
+ self.slice_start_token + self.unk_token * self.image_feature_size + self.slice_end_token
221
+ )
222
+
223
+ cols = grid[0]
224
+ rows = grid[1]
225
+ slices = []
226
+ for i in range(rows):
227
+ lines = []
228
+ for j in range(cols):
229
+ lines.append(slice_image_placeholder)
230
+ slices.append("".join(lines))
231
+
232
+ slice_placeholder = "\n".join(slices)
233
+ return slice_placeholder
234
+
235
+ def get_image_id_placeholder(self, idx=0):
236
+ return f"{self.im_id_start}{idx}{self.im_id_end}"
237
+
238
+ def get_sliced_images(self, image, max_slice_nums=None):
239
+ slice_images = []
240
+
241
+ if not self.slice_mode:
242
+ return [image]
243
+
244
+ max_slice_nums = self.max_slice_nums if max_slice_nums is None else int(max_slice_nums)
245
+ assert max_slice_nums > 0
246
+ source_image, patches, sliced_grid = self.slice_image(
247
+ image, max_slice_nums, self.scale_resolution, self.patch_size # default: 9 # default: 448 # default: 14
248
+ )
249
+
250
+ slice_images.append(source_image)
251
+ if len(patches) > 0:
252
+ for i in range(len(patches)):
253
+ for j in range(len(patches[0])):
254
+ slice_images.append(patches[i][j])
255
+ return slice_images
256
+
257
+ def get_sliced_grid(self, image_size, max_slice_nums, nerver_split=False):
258
+ original_width, original_height = image_size
259
+ log_ratio = math.log(original_width / original_height)
260
+ ratio = original_width * original_height / (self.scale_resolution * self.scale_resolution)
261
+ multiple = min(math.ceil(ratio), max_slice_nums)
262
+ if multiple <= 1 or nerver_split:
263
+ return None
264
+ candidate_split_grids_nums = []
265
+ for i in [multiple - 1, multiple, multiple + 1]:
266
+ if i == 1 or i > max_slice_nums:
267
+ continue
268
+ candidate_split_grids_nums.append(i)
269
+
270
+ candidate_grids = []
271
+ for split_grids_nums in candidate_split_grids_nums:
272
+ m = 1
273
+ while m <= split_grids_nums:
274
+ if split_grids_nums % m == 0:
275
+ candidate_grids.append([m, split_grids_nums // m])
276
+ m += 1
277
+
278
+ best_grid = [1, 1]
279
+ min_error = float("inf")
280
+ for grid in candidate_grids:
281
+ error = abs(log_ratio - math.log(grid[0] / grid[1]))
282
+ if error < min_error:
283
+ best_grid = grid
284
+ min_error = error
285
+
286
+ return best_grid
287
+
288
+ def get_slice_image_placeholder(self, image_size, image_idx=0, max_slice_nums=None, use_image_id=None):
289
+ max_slice_nums = self.max_slice_nums if max_slice_nums is None else int(max_slice_nums)
290
+ assert max_slice_nums > 0
291
+ grid = self.get_sliced_grid(image_size=image_size, max_slice_nums=max_slice_nums)
292
+
293
+ image_placeholder = self.im_start_token + self.unk_token * self.image_feature_size + self.im_end_token
294
+ use_image_id = self.use_image_id if use_image_id is None else bool(use_image_id)
295
+ if use_image_id:
296
+ final_placeholder = self.get_image_id_placeholder(image_idx) + image_placeholder
297
+ else:
298
+ final_placeholder = image_placeholder
299
+
300
+ if self.slice_mode:
301
+ final_placeholder = final_placeholder + self.get_grid_placeholder(grid=grid)
302
+ return final_placeholder
303
+
304
+ @staticmethod
305
+ def to_pil_image(image, rescale=None) -> Image.Image:
306
+ """Converts `image` to a PIL Image. Optionally rescales it and puts the channel dimension back
307
+ as the last axis if needed.
308
+
309
+ Args:
310
+ image (`Image.Image` or `numpy.ndarray` or `torch.Tensor`):
311
+ The image to convert to the PIL Image format.
312
+ rescale (`bool`, *optional*):
313
+ whether to apply the scaling factor (to make pixel values integers between 0 and 255). Will
314
+ default to `True` if the image type is a floating type, `False` otherwise.
315
+ """
316
+ if isinstance(image, Image.Image):
317
+ return image
318
+ if is_torch_tensor(image):
319
+ image = image.numpy()
320
+
321
+ if isinstance(image, np.ndarray):
322
+ if rescale is None:
323
+ # rescale default to the array being of floating type.
324
+ rescale = isinstance(image.flat[0], np.floating)
325
+ # If the channel as been moved to first dim, we put it back at the end.
326
+ if image.ndim == 3 and image.shape[0] in [1, 3]:
327
+ image = image.transpose(1, 2, 0)
328
+ if rescale:
329
+ image = image * 255
330
+ image = image.astype(np.uint8)
331
+ return Image.fromarray(image)
332
+ return image
333
+
334
+ def reshape_by_patch(self, image):
335
+ image = torch.from_numpy(image)
336
+ patch_size = self.patch_size
337
+ patches = torch.nn.functional.unfold(image, (patch_size, patch_size), stride=(patch_size, patch_size))
338
+
339
+ patches = patches.reshape(image.size(0), patch_size, patch_size, -1)
340
+ patches = patches.permute(0, 1, 3, 2).reshape(image.size(0), patch_size, -1)
341
+ return patches.numpy()
342
+
343
+ def preprocess(
344
+ self,
345
+ images: Union[Image.Image, List[Image.Image], List[List[Image.Image]]],
346
+ do_pad: Optional[bool] = True,
347
+ max_slice_nums: int = None,
348
+ return_tensors: Optional[Union[str, TensorType]] = None,
349
+ **kwargs,
350
+ ) -> MiniCPMOBatchFeature:
351
+ if isinstance(images, Image.Image):
352
+ images_list = [[images]]
353
+ elif isinstance(images[0], Image.Image):
354
+ images_list = [images]
355
+ else:
356
+ images_list = images
357
+
358
+ new_images_list = []
359
+ image_sizes_list = []
360
+ tgt_sizes_list = []
361
+
362
+ for _images in images_list:
363
+ if _images is None or len(_images) == 0:
364
+ new_images_list.append([])
365
+ image_sizes_list.append([])
366
+ tgt_sizes_list.append([])
367
+ continue
368
+ if not valid_images(_images):
369
+ raise ValueError(
370
+ "Invalid image type. Must be of type PIL.Image.Image, numpy.ndarray, "
371
+ "torch.Tensor, tf.Tensor or jax.ndarray."
372
+ )
373
+
374
+ _images = [self.to_pil_image(image).convert("RGB") for image in _images]
375
+ input_data_format = infer_channel_dimension_format(np.array(_images[0]))
376
+
377
+ new_images = []
378
+ image_sizes = [image.size for image in _images]
379
+ tgt_sizes = []
380
+ for image in _images:
381
+ image_patches = self.get_sliced_images(image, max_slice_nums)
382
+ image_patches = [to_numpy_array(image).astype(np.float32) / 255 for image in image_patches]
383
+ image_patches = [
384
+ self.normalize(image=image, mean=self.mean, std=self.std, input_data_format=input_data_format)
385
+ for image in image_patches
386
+ ]
387
+ image_patches = [
388
+ to_channel_dimension_format(image, ChannelDimension.FIRST, input_channel_dim=input_data_format)
389
+ for image in image_patches
390
+ ]
391
+ for slice_image in image_patches:
392
+ new_images.append(self.reshape_by_patch(slice_image))
393
+ tgt_sizes.append(
394
+ np.array((slice_image.shape[1] // self.patch_size, slice_image.shape[2] // self.patch_size))
395
+ )
396
+
397
+ if tgt_sizes:
398
+ tgt_sizes = np.vstack(tgt_sizes)
399
+
400
+ new_images_list.append(new_images)
401
+ image_sizes_list.append(image_sizes)
402
+ tgt_sizes_list.append(tgt_sizes)
403
+ return MiniCPMOBatchFeature(
404
+ data={"pixel_values": new_images_list, "image_sizes": image_sizes_list, "tgt_sizes": tgt_sizes_list},
405
+ tensor_type=return_tensors,
406
+ )
407
+
408
+
409
+ AutoImageProcessor.register("MiniCPMVImageProcessor", MiniCPMVImageProcessor)
410
+
411
+
412
+ def chunk_audio(audio: np.ndarray, max_duration_seconds: int = 30, sample_rate: int = 16000) -> List[np.ndarray]:
413
+ """split long audio into chunks
414
+
415
+ Args:
416
+ audio:
417
+ max_duration_seconds:
418
+ sample_rate:
419
+
420
+ Returns:
421
+ chunks
422
+ """
423
+ max_len = int(max_duration_seconds * sample_rate)
424
+
425
+ if len(audio) <= max_len:
426
+ return [audio]
427
+
428
+ chunks = []
429
+ for i in range(0, len(audio), max_len):
430
+ chunk = audio[i : i + max_len]
431
+ chunks.append(chunk)
432
+
433
+ return chunks
434
+
435
+
436
+ def process_audio_batch(
437
+ audios: Union[np.ndarray, List[np.ndarray], List[List[np.ndarray]]],
438
+ feature_extractor,
439
+ sampling_rate: int = 16000,
440
+ max_duration_seconds: int = 30,
441
+ return_attention_mask: bool = True,
442
+ ) -> Tuple[torch.Tensor, List[torch.Tensor]]:
443
+ """extract audio mel features
444
+
445
+ Args:
446
+ audios:
447
+ feature_extractor: WhisperFeatureExtractor
448
+ sampling_rate:
449
+ max_duration_seconds:
450
+ return_attention_mask:
451
+
452
+ Returns:
453
+ (audio_features, audio_feature_lens)
454
+ audio_features: [batch_size, n_mels, max_frames]
455
+ audio_feature_lens:
456
+ """
457
+ if isinstance(audios, np.ndarray):
458
+ audios_list = [[audios]]
459
+ elif len(audios) > 0 and isinstance(audios[0], np.ndarray):
460
+ audios_list = [audios]
461
+ else:
462
+ audios_list = audios
463
+
464
+ audio_features_all = []
465
+ audio_feature_lens_list = []
466
+
467
+ for batch_audios in audios_list:
468
+ batch_lens = []
469
+
470
+ for audio in batch_audios:
471
+ chunks = chunk_audio(audio, max_duration_seconds, sampling_rate)
472
+
473
+ for chunk in chunks:
474
+ audio_input = feature_extractor(
475
+ chunk,
476
+ sampling_rate=sampling_rate,
477
+ return_tensors="pt",
478
+ padding="max_length",
479
+ return_attention_mask=return_attention_mask,
480
+ )
481
+
482
+ audio_feature = audio_input["input_features"] # [1, 80, frames]
483
+
484
+ if return_attention_mask:
485
+ actual_len = audio_input["attention_mask"].sum(dim=1) # Tensor([frames])
486
+ audio_feature = audio_feature[:, :, : actual_len[0]]
487
+ batch_lens.append(actual_len[0])
488
+ else:
489
+ batch_lens.append(torch.tensor(audio_feature.shape[2]))
490
+
491
+ audio_features_all.append(audio_feature.squeeze(0)) # [80, frames]
492
+
493
+ if len(batch_lens) > 0:
494
+ audio_feature_lens_list.append(torch.hstack(batch_lens))
495
+ else:
496
+ audio_feature_lens_list.append(torch.tensor([]))
497
+
498
+ # pad to same length
499
+ if audio_features_all:
500
+ audio_features = torch.nn.utils.rnn.pad_sequence(
501
+ [feat.transpose(0, 1) for feat in audio_features_all], batch_first=True, padding_value=0.0
502
+ ).transpose(
503
+ 1, 2
504
+ ) # [batch, 80, max_frames]
505
+ else:
506
+ audio_features = torch.tensor([])
507
+
508
+ return audio_features, audio_feature_lens_list
509
+
510
+
511
+ def regroup_audio_features(
512
+ audio_features: torch.Tensor, audio_feature_lens: List[torch.Tensor], regroup_seconds: int, fps: int = 100
513
+ ) -> Tuple[torch.Tensor, List[torch.Tensor]]:
514
+ """regroup audio features to fixed duration
515
+
516
+ Args:
517
+ audio_features: [batch, n_mels, frames]
518
+ audio_feature_lens: each batch's actual length
519
+ regroup_seconds: regroup duration (seconds)
520
+ fps: frames per second
521
+
522
+ Returns:
523
+ (regrouped_features, regrouped_lens)
524
+ """
525
+ # flatten to continuous frames sequence
526
+ all_lens = []
527
+ for lens in audio_feature_lens:
528
+ if isinstance(lens, torch.Tensor):
529
+ all_lens.extend(lens.tolist())
530
+ elif isinstance(lens, list):
531
+ all_lens.extend([int(x) for x in lens])
532
+
533
+ if len(all_lens) == 0:
534
+ return torch.tensor([]), []
535
+
536
+ # concatenate all valid features
537
+ flat_slices = [audio_features[i, :, :L] for i, L in enumerate(all_lens)] # [n_mels, L]
538
+
539
+ if len(flat_slices) == 1:
540
+ full_feat = flat_slices[0]
541
+ else:
542
+ full_feat = torch.cat(flat_slices, dim=1) # [n_mels, total_frames]
543
+
544
+ # split to fixed frames
545
+ frames_per_seg = int(regroup_seconds * fps)
546
+ segments = []
547
+
548
+ for start in range(0, full_feat.size(1), frames_per_seg):
549
+ seg = full_feat[:, start : start + frames_per_seg]
550
+ if seg.size(1) > 0:
551
+ segments.append(seg)
552
+
553
+ if len(segments) == 0:
554
+ return torch.tensor([]), []
555
+
556
+ # pad and convert to batch
557
+ seg_lens = [s.size(1) for s in segments]
558
+ segs_transposed = [s.transpose(0, 1) for s in segments]
559
+
560
+ padded = torch.nn.utils.rnn.pad_sequence(segs_transposed, batch_first=True, padding_value=0.0) # [N, max_T, n_mels]
561
+
562
+ padded = padded.transpose(1, 2) # [N, n_mels, max_T]
563
+ lens_tensor = torch.tensor(seg_lens, dtype=torch.int32, device=padded.device)
564
+
565
+ return padded, [lens_tensor]
566
+
567
+
568
+ class MiniCPMAAudioProcessor(WhisperFeatureExtractor):
569
+ """
570
+ On top of WhisperFeatureExtractor:
571
+ - support dynamic_log_norm (original max-8dB, adjustable dynamic_range_db)
572
+ - or fixed log_floor_db (e.g. -10dB)
573
+ - this is because we need to do streaming scheme, in which we can't do dynamic setting
574
+ - this can be modified in the middle, through set_dynamic_log_norm
575
+ Two paths (torch / numpy) keep consistent clipping and scaling order:
576
+ log10 -> (dynamic/fixed lower limit clipping) -> (+4)/4
577
+ """
578
+
579
+ def __init__(
580
+ self,
581
+ *args,
582
+ dynamic_log_norm: bool = True,
583
+ dynamic_range_db: float = 8.0,
584
+ log_floor_db: float = -10.0,
585
+ **kwargs,
586
+ ):
587
+ super().__init__(*args, **kwargs)
588
+ self.dynamic_log_norm = bool(dynamic_log_norm)
589
+ self.dynamic_range_db = float(dynamic_range_db)
590
+ self.log_floor_db = float(log_floor_db)
591
+
592
+ def set_spac_log_norm(
593
+ self,
594
+ dynamic_range_db: Optional[float] = None,
595
+ log_floor_db: Optional[float] = None,
596
+ *,
597
+ inplace: bool = True,
598
+ ) -> "MiniCPMAAudioProcessor":
599
+ """Hot update dynamic/fixed lower limit strategy.
600
+
601
+ Args:
602
+ enabled: True=use dynamic threshold (max - dynamic_range_db), False=use fixed lower limit log_floor_db.
603
+ None means keep unchanged.
604
+ dynamic_range_db: dynamic range (dB), only effective when enabled=True. None means keep unchanged.
605
+ log_floor_db: fixed log floor (dB, usually <= 0), only effective when enabled=False. None means keep unchanged.
606
+ inplace: True directly modify current instance; False return a shallow copy and modify on it.
607
+
608
+ Returns:
609
+ self or new instance (when inplace=False).
610
+ """
611
+
612
+ target = self if inplace else copy.copy(self)
613
+
614
+ if dynamic_range_db is not None:
615
+ val = float(dynamic_range_db)
616
+ if val < 0:
617
+ raise ValueError("dynamic_range_db must be >= 0.")
618
+ target.dynamic_log_norm = True # explicitly set the value to dynamic mode
619
+ target.dynamic_range_db = val
620
+
621
+ if log_floor_db is not None:
622
+ val = float(log_floor_db)
623
+ # usually log10(mel) maximum is not more than ~0dB, floor should be <= 0; here do loose validation
624
+ if val > 0:
625
+ raise ValueError("log_floor_db should be <= 0 (log10 scale).")
626
+ target.dynamic_log_norm = False # explicitly set the value to fixed lower limit mode
627
+ target.log_floor_db = val
628
+
629
+ return target
630
+
631
+ def _np_extract_fbank_features(self, waveform_batch: np.ndarray, device: str) -> np.ndarray:
632
+ """NumPy version consistent with upstream, but replace max-8dB with configurable dynamic/fixed lower limit clipping."""
633
+ if device != "cpu":
634
+ raise ValueError(
635
+ f"Got device `{device}` for feature extraction, but feature extraction on CUDA accelerator "
636
+ "devices requires torch. Set device='cpu' or install torch."
637
+ )
638
+
639
+ log_spec_batch: List[np.ndarray] = []
640
+ for waveform in waveform_batch:
641
+ # generate log10 Mel
642
+ log_spec = spectrogram(
643
+ waveform,
644
+ window_function(self.n_fft, "hann"),
645
+ frame_length=self.n_fft,
646
+ hop_length=self.hop_length,
647
+ power=2.0,
648
+ dither=self.dither,
649
+ mel_filters=self.mel_filters,
650
+ log_mel="log10",
651
+ )
652
+ # consistent with upstream: remove the last frame
653
+ log_spec = log_spec[:, :-1]
654
+
655
+ # dynamic/fixed clipping
656
+ if self.dynamic_log_norm:
657
+ threshold = log_spec.max() - self.dynamic_range_db
658
+ log_spec = np.maximum(log_spec, threshold)
659
+ else:
660
+ log_spec = np.maximum(log_spec, self.log_floor_db)
661
+
662
+ # consistent with Whisper linear scaling
663
+ log_spec = (log_spec + 4.0) / 4.0
664
+
665
+ log_spec_batch.append(log_spec)
666
+
667
+ return np.array(log_spec_batch)
668
+
669
+ def _torch_extract_fbank_features(self, waveform: np.ndarray, device: str = "cpu") -> np.ndarray:
670
+ if torch is None:
671
+ raise RuntimeError("PyTorch is not installed, cannot compute STFT on GPU.")
672
+
673
+ waveform = torch.from_numpy(waveform).to(device, torch.float32)
674
+ window = torch.hann_window(self.n_fft, device=device)
675
+
676
+ if self.dither != 0.0:
677
+ waveform = waveform + self.dither * torch.randn_like(waveform)
678
+
679
+ stft = torch.stft(waveform, n_fft=self.n_fft, hop_length=self.hop_length, window=window, return_complex=True)
680
+ magnitudes = stft[..., :-1].abs() ** 2
681
+
682
+ mel_filters = torch.from_numpy(self.mel_filters).to(device, torch.float32) # [n_mels, 1+n_fft//2]
683
+ mel_spec = mel_filters.T @ magnitudes # [..., n_mels, T]
684
+
685
+ log_spec = torch.clamp(mel_spec, min=1e-10).log10() # <= 0
686
+
687
+ if self.dynamic_log_norm:
688
+ if waveform.dim() == 2:
689
+ max_val_t = log_spec.max(dim=2, keepdim=True)[0] # over T
690
+ max_val_bt = max_val_t.max(dim=1, keepdim=True)[0] # over mel
691
+ threshold = max_val_bt - self.dynamic_range_db
692
+ log_spec = torch.maximum(log_spec, threshold)
693
+ else:
694
+ threshold = log_spec.max() - self.dynamic_range_db
695
+ log_spec = torch.maximum(log_spec, threshold)
696
+ else:
697
+ floor_tensor = torch.tensor(self.log_floor_db, dtype=log_spec.dtype, device=log_spec.device)
698
+ log_spec = torch.maximum(log_spec, floor_tensor)
699
+
700
+ log_spec = (log_spec + 4.0) / 4.0
701
+
702
+ if device != "cpu":
703
+ log_spec = log_spec.detach().cpu()
704
+ return log_spec.numpy()
705
+
706
+ def process(self, *args, **kwargs):
707
+ """Alias of __call__ for convenience."""
708
+ return self.__call__(*args, **kwargs)
709
+
710
+
711
+ class StreamingMelProcessorExact:
712
+ """Strictly offline equivalent streaming Mel processor.
713
+
714
+ - accumulate all historical audio into buffer; use the same feature_extractor to calculate the entire mel after each addition.
715
+ - only output "stable" frames: the frame center does not depend on future (right) context, i.e. center + n_fft//2 <= current buffer length.
716
+ - output the last batch of frames at the end (flush), ensuring complete consistency with offline full-calculation.
717
+
718
+ Cost: Each call performs feature extraction on the accumulated buffer (can be optimized to incremental if needed).
719
+ """
720
+
721
+ def __init__(
722
+ self,
723
+ feature_extractor: MiniCPMAAudioProcessor,
724
+ chunk_ms: int = 100,
725
+ first_chunk_ms: Optional[int] = None,
726
+ sample_rate: int = 16000,
727
+ n_fft: int = 400,
728
+ hop_length: int = 160,
729
+ n_mels: int = 80,
730
+ cnn_redundancy_ms: int = 10, # (given in ms, usually 10ms=1 frame)
731
+ # sliding window parameters
732
+ enable_sliding_window: bool = False, # whether to enable sliding window
733
+ slide_trigger_seconds: float = 30.0, # trigger threshold for sliding window in seconds
734
+ slide_stride_seconds: float = 10.0, # stride for sliding window in seconds
735
+ ):
736
+ self.feature_extractor = feature_extractor
737
+ self.chunk_ms = chunk_ms
738
+ self.first_chunk_ms = first_chunk_ms if first_chunk_ms is not None else chunk_ms
739
+ self.sample_rate = sample_rate
740
+ self.n_fft = n_fft
741
+ self.hop_length = hop_length
742
+ self.n_mels = n_mels
743
+
744
+ self.chunk_samples = int(round(chunk_ms * sample_rate / 1000))
745
+ self.chunk_frames = self.chunk_samples // hop_length
746
+ # align to hop_length to avoid frame boundary issues
747
+ hop = self.hop_length
748
+ raw_first_samples = int(round(self.first_chunk_ms * sample_rate / 1000))
749
+ aligned_first = max(hop, (raw_first_samples // hop) * hop)
750
+ self.first_chunk_samples = aligned_first
751
+ self.half_window = n_fft // 2 # required right context
752
+
753
+ # redundancy frames (in frames), <=1 frame: 10ms → 1 frame
754
+ self.cnn_redundancy_ms = cnn_redundancy_ms
755
+ self.cnn_redundancy_samples = int(cnn_redundancy_ms * sample_rate / 1000)
756
+ self.cnn_redundancy_frames = max(0, self.cnn_redundancy_samples // hop_length)
757
+
758
+ # sliding window configuration (Trigger mode)
759
+ self.enable_sliding_window = enable_sliding_window
760
+ self.trigger_seconds = slide_trigger_seconds
761
+ self.slide_seconds = slide_stride_seconds
762
+
763
+ # shift/base (global frame coordinates)
764
+ self.left_samples_dropped = 0 # samples dropped from the left
765
+ self.base_T = 0 # index of the "global frame" corresponding to mel_full[:, :, 0]
766
+
767
+ self.reset()
768
+
769
+ def reset(self):
770
+ self.buffer = np.zeros(0, dtype=np.float32)
771
+ self.last_emitted_T = 0
772
+ self.total_samples_processed = 0
773
+ self.chunk_count = 0
774
+ self.is_first = True
775
+ self.left_samples_dropped = 0
776
+ self.base_T = 0
777
+
778
+ def get_chunk_size(self) -> int:
779
+ return self.first_chunk_samples if self.is_first else self.chunk_samples
780
+
781
+ def get_expected_output_frames(self) -> int:
782
+ raise NotImplementedError("get_expected_output_frames is not implemented")
783
+
784
+ def _extract_full(self) -> torch.Tensor:
785
+ # when buffer length is less than n_fft, Whisper's internal STFT will raise an error in center=True and pad mode
786
+ # (pad is greater than input length). At this time, there is no stable frame to output, so return empty features directly.
787
+ if len(self.buffer) < self.n_fft:
788
+ raise ValueError(f"buffer length is shorter than n_fft {len(self.buffer)} < {self.n_fft}")
789
+ # if buffer length is less than 5s, use set_spac_log_norm(log_floor_db=-10) or the last cached result
790
+ if len(self.buffer) < 5 * self.sample_rate:
791
+ # TODO: here the best is to do some experiments to choose the best one, now this is selected through experience, can see MiniCPMAAudioProcessor's main implementation
792
+ self.feature_extractor.set_spac_log_norm(log_floor_db=-10)
793
+ # if buffer length is greater than 5s, use set_spac_log_norm(dynamic_range_db=8)
794
+ else:
795
+ self.feature_extractor.set_spac_log_norm(dynamic_range_db=8)
796
+ feats = self.feature_extractor(
797
+ self.buffer,
798
+ sampling_rate=self.sample_rate,
799
+ return_tensors="pt",
800
+ padding=False,
801
+ )
802
+ return feats.input_features # [1, 80, T]
803
+
804
+ def _stable_frames_count(self) -> int:
805
+ # number of stable frames = floor((len(buffer) - half_window) / hop) + 1, minimum is 0
806
+ L = int(self.buffer.shape[0])
807
+ if L <= 0:
808
+ return 0
809
+ if L < self.half_window:
810
+ return 0
811
+ return max(0, (L - self.half_window) // self.hop_length + 1)
812
+
813
+ def _maybe_slide_buffer(self):
814
+ """Trigger mode sliding window: when the buffer reaches the trigger threshold, slide a fixed length window."""
815
+ if not self.enable_sliding_window:
816
+ return
817
+
818
+ sr = self.sample_rate
819
+ hop = self.hop_length
820
+ L = len(self.buffer)
821
+
822
+ # convert seconds to samples
823
+ trigger_samples = int(self.trigger_seconds * sr)
824
+ stride_samples = int(self.slide_seconds * sr)
825
+
826
+ # check if the trigger threshold is reached
827
+ if L < trigger_samples:
828
+ return
829
+
830
+ # calculate the number of samples to drop (fixed sliding stride_samples)
831
+ drop = stride_samples
832
+
833
+ # cannot drop the left context that is still needed for subsequent emission
834
+ # in trigger mode, we only need to protect the minimum necessary data
835
+ # i.e. ensure that we do not discard frames that may be needed in the future
836
+ last_emitted_local = self.last_emitted_T - self.base_T
837
+
838
+ # only protect necessary context (e.g. the most recent 1 second data)
839
+ min_keep_seconds = 1.0 # keep at least 1 second of data to ensure continuity
840
+ min_keep_samples = int(min_keep_seconds * sr)
841
+
842
+ # guard_samples are the minimum samples we must keep
843
+ guard_samples = min(min_keep_samples, L - drop)
844
+
845
+ # limit: do not exceed the safe boundary; and align hop
846
+ max_allowed_drop = max(0, L - guard_samples)
847
+ drop = min(drop, max_allowed_drop)
848
+ drop = (drop // hop) * hop
849
+
850
+ if drop <= 0:
851
+ return
852
+
853
+ # truly drop & update base
854
+ self.buffer = self.buffer[drop:]
855
+ self.left_samples_dropped += drop
856
+ self.base_T += drop // hop
857
+
858
+ def process(self, audio_chunk: np.ndarray, is_last_chunk: bool = False) -> Tuple[torch.Tensor, Dict]:
859
+ self.chunk_count += 1
860
+ # append to buffer
861
+ if len(self.buffer) == 0:
862
+ self.buffer = audio_chunk.astype(np.float32, copy=True)
863
+ else:
864
+ self.buffer = np.concatenate([self.buffer, audio_chunk.astype(np.float32, copy=True)])
865
+
866
+ # sliding window processing
867
+ self._maybe_slide_buffer()
868
+
869
+ # full extraction (for the current window)
870
+ mel_full = self._extract_full()
871
+ T_full = mel_full.shape[-1] # local frames in the current window
872
+ stable_T = min(T_full, self._stable_frames_count()) # local stable frames
873
+ stable_T_global = self.base_T + stable_T # map to global frame coordinates
874
+
875
+ # plan the core frames for the current emission (global coordinates)
876
+ core_start_g = self.last_emitted_T
877
+ core_end_g = core_start_g + self.chunk_frames
878
+ required_stable_g = core_end_g + self.cnn_redundancy_frames
879
+
880
+ if stable_T_global >= required_stable_g or is_last_chunk:
881
+ emit_start_g = max(0, core_start_g - self.cnn_redundancy_frames)
882
+ emit_end_g = core_end_g + self.cnn_redundancy_frames
883
+
884
+ # global -> local index
885
+ emit_start = max(0, emit_start_g - self.base_T)
886
+ emit_end = emit_end_g - self.base_T
887
+ emit_start = max(0, min(emit_start, T_full))
888
+ emit_end = max(emit_start, min(emit_end, T_full))
889
+
890
+ mel_output = mel_full[:, :, emit_start:emit_end]
891
+ self.last_emitted_T = core_end_g # only advance the core frame pointer (global)
892
+ else:
893
+ mel_output = mel_full[:, :, 0:0]
894
+
895
+ self.total_samples_processed += len(audio_chunk)
896
+ self.is_first = False
897
+
898
+ info = {
899
+ "type": "exact_chunk",
900
+ "chunk_number": self.chunk_count,
901
+ "emitted_frames": mel_output.shape[-1],
902
+ "stable_T": stable_T,
903
+ "T_full": T_full,
904
+ "base_T": self.base_T,
905
+ "stable_T_global": stable_T_global,
906
+ "buffer_len_samples": int(self.buffer.shape[0]),
907
+ "left_samples_dropped": self.left_samples_dropped,
908
+ "core_start": core_start_g, # if keep the original field name, use the global value here
909
+ "core_end": core_end_g, # same as above
910
+ }
911
+ return mel_output, info
912
+
913
+ def flush(self) -> torch.Tensor:
914
+ """Called when the stream ends, output the remaining unemitted frames, ensuring consistency with offline (calculated by global coordinates)."""
915
+ if len(self.buffer) == 0:
916
+ return torch.zeros(1, 80, 0)
917
+
918
+ mel_full = self._extract_full()
919
+ T_local = mel_full.shape[-1]
920
+ T_global = self.base_T + T_local
921
+
922
+ if self.last_emitted_T < T_global:
923
+ start_l = max(0, self.last_emitted_T - self.base_T)
924
+ tail = mel_full[:, :, start_l:]
925
+ self.last_emitted_T = T_global
926
+ return tail
927
+ return mel_full[:, :, 0:0]
928
+
929
+ def get_config(self) -> Dict:
930
+ return {
931
+ "chunk_ms": self.chunk_ms,
932
+ "first_chunk_ms": self.first_chunk_ms,
933
+ "effective_first_chunk_ms": self.first_chunk_samples / self.sample_rate * 1000.0,
934
+ "sample_rate": self.sample_rate,
935
+ "n_fft": self.n_fft,
936
+ "hop_length": self.hop_length,
937
+ "cnn_redundancy_ms": self.cnn_redundancy_ms,
938
+ "cnn_redundancy_frames": self.cnn_redundancy_frames,
939
+ "enable_sliding_window": self.enable_sliding_window,
940
+ "trigger_seconds": self.trigger_seconds,
941
+ "slide_seconds": self.slide_seconds,
942
+ }
943
+
944
+ def get_state(self) -> Dict:
945
+ return {
946
+ "chunk_count": self.chunk_count,
947
+ "last_emitted_T": self.last_emitted_T,
948
+ "total_samples_processed": self.total_samples_processed,
949
+ "buffer_len": int(self.buffer.shape[0]),
950
+ "base_T": self.base_T,
951
+ "left_samples_dropped": self.left_samples_dropped,
952
+ }
953
+
954
+ def get_snapshot(self) -> Dict:
955
+ """Get a complete state snapshot (including buffer), used for recovery from a fast start.
956
+
957
+ Returns:
958
+ A dictionary containing the complete state, which can be used to restore the snapshot
959
+ """
960
+ buffer_copy = self.buffer.copy()
961
+ snapshot = {
962
+ "chunk_count": self.chunk_count,
963
+ "last_emitted_T": self.last_emitted_T,
964
+ "total_samples_processed": self.total_samples_processed,
965
+ "buffer": buffer_copy,
966
+ "base_T": self.base_T,
967
+ "left_samples_dropped": self.left_samples_dropped,
968
+ "is_first": self.is_first,
969
+ # save the state of the feature_extractor (key: ensure determinism of mel feature extraction)
970
+ "fe_dynamic_log_norm": getattr(self.feature_extractor, "dynamic_log_norm", None),
971
+ "fe_dynamic_range_db": getattr(self.feature_extractor, "dynamic_range_db", None),
972
+ "fe_log_floor_db": getattr(self.feature_extractor, "log_floor_db", None),
973
+ }
974
+
975
+ return snapshot
976
+
977
+ def restore_snapshot(self, snapshot: Dict) -> None:
978
+ """Restore state from a snapshot
979
+
980
+ Args:
981
+ snapshot: the snapshot dictionary returned by get_snapshot
982
+ """
983
+ # record the state before restoration
984
+ prev_state = {
985
+ "chunk_count": self.chunk_count,
986
+ "last_emitted_T": self.last_emitted_T,
987
+ "buffer_len": len(self.buffer),
988
+ }
989
+
990
+ # restore state
991
+ self.chunk_count = snapshot["chunk_count"]
992
+ self.last_emitted_T = snapshot["last_emitted_T"]
993
+ self.total_samples_processed = snapshot["total_samples_processed"]
994
+ self.buffer = snapshot["buffer"].copy() # copy buffer
995
+ self.base_T = snapshot["base_T"]
996
+ self.left_samples_dropped = snapshot["left_samples_dropped"]
997
+ self.is_first = snapshot["is_first"]
998
+
999
+ # restore the state of the feature_extractor (key: ensure determinism of mel feature extraction)
1000
+ if snapshot.get("fe_dynamic_log_norm") is not None:
1001
+ self.feature_extractor.dynamic_log_norm = snapshot["fe_dynamic_log_norm"]
1002
+ if snapshot.get("fe_dynamic_range_db") is not None:
1003
+ self.feature_extractor.dynamic_range_db = snapshot["fe_dynamic_range_db"]
1004
+ if snapshot.get("fe_log_floor_db") is not None:
1005
+ self.feature_extractor.log_floor_db = snapshot["fe_log_floor_db"]
1006
+
1007
+
1008
+ class MiniCPMOProcessor(ProcessorMixin):
1009
+ attributes = ["image_processor", "audio_processor", "tokenizer"]
1010
+ audio_processor_class = "AutoFeatureExtractor"
1011
+ image_processor_class = "AutoImageProcessor"
1012
+ tokenizer_class = "AutoTokenizer"
1013
+
1014
+ def __init__(self, image_processor=None, audio_processor=None, tokenizer=None, **kwargs):
1015
+ super().__init__(image_processor, audio_processor, tokenizer)
1016
+
1017
+ self.version = image_processor.version if image_processor else None
1018
+ # audio feature pooling step, needs to be consistent with config.audio_pool_step
1019
+ self.pool_step = kwargs.get("audio_pool_step", 5)
1020
+
1021
+ # initialize the streaming audio processor
1022
+ self._streaming_mel_processor = None
1023
+ if audio_processor is not None:
1024
+ self._init_streaming_processor()
1025
+
1026
+ def get_audio_placeholder(
1027
+ self,
1028
+ audio_lens: int,
1029
+ chunk_input: bool = True,
1030
+ chunk_length: int = 1,
1031
+ ) -> str:
1032
+ """
1033
+ Public method to get audio placeholder string for vLLM integration.
1034
+
1035
+ Args:
1036
+ audio_lens: Length of audio in samples
1037
+ chunk_input: Whether to use chunked processing
1038
+ chunk_length: Chunk length in seconds
1039
+
1040
+ Returns:
1041
+ Audio placeholder string
1042
+ """
1043
+ pool_step = self.pool_step
1044
+ feature_lens = math.ceil(audio_lens / self.audio_processor.hop_length)
1045
+
1046
+ feature_lens = (feature_lens - 1) // 2 + 1
1047
+ output_lens = (feature_lens - pool_step) // pool_step + 1
1048
+
1049
+ if chunk_input:
1050
+ fbank_feat_in_chunk = int(chunk_length * 100)
1051
+ cnn_feat_in_chunk = (fbank_feat_in_chunk - 1) // 2 + 1
1052
+ audio_embeds_in_chunk = (cnn_feat_in_chunk - pool_step) // pool_step + 1
1053
+ num_audio_chunks = (output_lens + audio_embeds_in_chunk - 1) // audio_embeds_in_chunk
1054
+
1055
+ place_holders = ""
1056
+ total_unk_len = 0
1057
+ for _ in range(num_audio_chunks):
1058
+ unk_len = min(audio_embeds_in_chunk, output_lens - total_unk_len)
1059
+ place_holders += self.tokenizer.audio_start + "<unk>" * unk_len + self.tokenizer.audio_end
1060
+ total_unk_len += unk_len
1061
+ audio_placeholder = place_holders
1062
+ else:
1063
+ audio_placeholder = self.tokenizer.audio_start + "<unk>" * output_lens + self.tokenizer.audio_end
1064
+
1065
+ return audio_placeholder
1066
+
1067
+ def _init_streaming_processor(
1068
+ self,
1069
+ chunk_ms: int = 100,
1070
+ cnn_redundancy_ms: int = 0,
1071
+ *,
1072
+ mode: str = "exact",
1073
+ first_chunk_ms: Optional[int] = None,
1074
+ enable_sliding_window: bool = False,
1075
+ slide_trigger_seconds: float = 30.0,
1076
+ slide_stride_seconds: float = 10.0,
1077
+ ):
1078
+ """Initialize the streaming processor
1079
+
1080
+ Args:
1081
+ chunk_ms: Chunk size in milliseconds, also the sliding step.
1082
+ cnn_redundancy_ms: CNN boundary redundancy in milliseconds (before and after), 0 means standard mode.
1083
+ mode: streaming processing mode, currently only supports "exact"
1084
+ first_chunk_ms: the size of the first chunk (milliseconds), if not specified, it is the same as chunk_ms
1085
+ enable_sliding_window: whether to enable sliding window (trigger mode)
1086
+ slide_trigger_seconds: trigger threshold for sliding window in seconds
1087
+ slide_stride_seconds: stride for sliding window in seconds
1088
+ """
1089
+ if mode == "exact":
1090
+ self._streaming_mel_processor = StreamingMelProcessorExact(
1091
+ feature_extractor=self.audio_processor,
1092
+ chunk_ms=chunk_ms,
1093
+ first_chunk_ms=first_chunk_ms,
1094
+ sample_rate=16000,
1095
+ cnn_redundancy_ms=cnn_redundancy_ms,
1096
+ enable_sliding_window=enable_sliding_window,
1097
+ slide_trigger_seconds=slide_trigger_seconds,
1098
+ slide_stride_seconds=slide_stride_seconds,
1099
+ )
1100
+ else:
1101
+ raise ValueError(f"Unsupported mode: {mode}, only 'exact' is supported")
1102
+ self._streaming_mode = mode if mode in ["exact"] else ("exact")
1103
+
1104
+ def set_streaming_mode(
1105
+ self,
1106
+ mode: str = "exact",
1107
+ chunk_ms: int = 100,
1108
+ cnn_redundancy_ms: int = 0,
1109
+ *,
1110
+ first_chunk_ms: Optional[int] = None,
1111
+ enable_sliding_window: bool = False,
1112
+ slide_trigger_seconds: float = 30.0,
1113
+ slide_stride_seconds: float = 10.0,
1114
+ ):
1115
+ """Set streaming processing mode
1116
+
1117
+ Args:
1118
+ mode: streaming processing mode, currently only supports "exact"
1119
+ chunk_ms: chunk size in milliseconds, also the sliding step.
1120
+ cnn_redundancy_ms: CNN boundary redundancy in milliseconds (before and after), 0 means standard mode.
1121
+ first_chunk_ms: the size of the first chunk (milliseconds), if not specified, it is the same as chunk_ms
1122
+ enable_sliding_window: whether to enable sliding window (trigger mode)
1123
+ slide_trigger_seconds: trigger threshold for sliding window in seconds
1124
+ slide_stride_seconds: stride for sliding window in seconds
1125
+ """
1126
+ if self.audio_processor is None:
1127
+ raise ValueError("audio_processor is not set, cannot initialize the streaming processor")
1128
+ self._init_streaming_processor(
1129
+ chunk_ms=chunk_ms,
1130
+ cnn_redundancy_ms=cnn_redundancy_ms,
1131
+ mode=mode,
1132
+ first_chunk_ms=first_chunk_ms,
1133
+ enable_sliding_window=enable_sliding_window,
1134
+ slide_trigger_seconds=slide_trigger_seconds,
1135
+ slide_stride_seconds=slide_stride_seconds,
1136
+ )
1137
+
1138
+ def process_image(
1139
+ self,
1140
+ images: Optional[ImageInput] = None,
1141
+ do_pad: bool = True,
1142
+ max_slice_nums: int = 1,
1143
+ return_tensors: str = "pt",
1144
+ ) -> MiniCPMOBatchFeature:
1145
+ """Process image data
1146
+
1147
+ Args:
1148
+ images: input images
1149
+ do_pad: whether to pad
1150
+ max_slice_nums: maximum number of slices
1151
+ return_tensors: return tensor type
1152
+ Returns:
1153
+ MiniCPMOBatchFeature object
1154
+ """
1155
+ if images is None:
1156
+ return MiniCPMOBatchFeature(data={"pixel_values": [[]], "image_sizes": [[]], "tgt_sizes": [[]]})
1157
+
1158
+ result = self.image_processor(
1159
+ images, do_pad=do_pad, max_slice_nums=max_slice_nums, return_tensors=return_tensors
1160
+ )
1161
+
1162
+ model_inputs = {
1163
+ "pixel_values": result.get("pixel_values", [[]]),
1164
+ "image_sizes": result.get("image_sizes", [[]]),
1165
+ "tgt_sizes": result.get("tgt_sizes", [[]]),
1166
+ }
1167
+
1168
+ return MiniCPMOBatchFeature(data=model_inputs)
1169
+
1170
+ def process_audio(
1171
+ self,
1172
+ audios: Optional[Union[np.ndarray, List[np.ndarray]]] = None,
1173
+ sampling_rate: int = 16000,
1174
+ regroup_to_seconds: Optional[int] = None,
1175
+ fps: int = 100,
1176
+ ) -> MiniCPMOBatchFeature:
1177
+ """Process audio data in batch
1178
+
1179
+ Args:
1180
+ audios: audio data
1181
+ sampling_rate: sampling rate
1182
+ regroup_to_seconds: regroup duration in seconds
1183
+ fps: frames per second
1184
+ Returns:
1185
+ MiniCPMOBatchFeature object
1186
+ """
1187
+ if audios is None:
1188
+ return MiniCPMOBatchFeature(data={"audio_features": [], "audio_feature_lens": []})
1189
+
1190
+ audio_features, audio_feature_lens = process_audio_batch(
1191
+ audios=audios,
1192
+ feature_extractor=self.audio_processor,
1193
+ sampling_rate=sampling_rate,
1194
+ max_duration_seconds=30,
1195
+ return_attention_mask=True,
1196
+ )
1197
+
1198
+ if regroup_to_seconds is not None and len(audio_features) > 0:
1199
+ audio_features, audio_feature_lens = regroup_audio_features(
1200
+ audio_features=audio_features,
1201
+ audio_feature_lens=audio_feature_lens,
1202
+ regroup_seconds=regroup_to_seconds,
1203
+ fps=fps,
1204
+ )
1205
+
1206
+ model_inputs = {"audio_features": audio_features, "audio_feature_lens": audio_feature_lens}
1207
+
1208
+ return MiniCPMOBatchFeature(data=model_inputs)
1209
+
1210
+ def process_audio_streaming(
1211
+ self,
1212
+ audio_chunk: np.ndarray,
1213
+ reset: bool = False,
1214
+ return_batch_feature: bool = False,
1215
+ is_last_chunk: bool = False,
1216
+ ) -> Union[Tuple[torch.Tensor, dict], MiniCPMOBatchFeature]:
1217
+ """Process audio chunk in streaming
1218
+
1219
+ Args:
1220
+ audio_chunk: audio data chunk (any audio, e.g. first process 125ms, then process 100ms)
1221
+ reset: whether to reset the processor state
1222
+ return_batch_feature: whether to return MiniCPMOBatchFeature format (consistent with process_audio)
1223
+ Returns:
1224
+ If return_batch_feature=False:
1225
+ (audio_features, info)
1226
+ - audio_features: [1, 80, n_frames] mel features
1227
+ - info: processing information dictionary
1228
+ If return_batch_feature=True:
1229
+ MiniCPMOBatchFeature object, containing:
1230
+ - audio_features: [1, 80, n_frames] mel features
1231
+ - audio_feature_lens: [tensor([n_frames])]
1232
+ - info: processing information (as an extra attribute)
1233
+ """
1234
+ if self._streaming_mel_processor is None:
1235
+ raise ValueError("Streaming processor not initialized, please ensure audio_processor is set")
1236
+
1237
+ if reset:
1238
+ self._streaming_mel_processor.reset()
1239
+
1240
+ # process chunk
1241
+ mel_features, info = self._streaming_mel_processor.process(audio_chunk, is_last_chunk=is_last_chunk)
1242
+
1243
+ # determine the return format based on the parameters
1244
+ if return_batch_feature:
1245
+ # return the format consistent with process_audio
1246
+ # note: info returns emitted_frames, which represents the actual output frames
1247
+ n_frames = info.get("emitted_frames", mel_features.shape[-1])
1248
+ model_inputs = {
1249
+ "audio_features": mel_features,
1250
+ "audio_feature_lens": [torch.tensor([n_frames])],
1251
+ "streaming_info": info, # add streaming processing information
1252
+ }
1253
+ return MiniCPMOBatchFeature(data=model_inputs)
1254
+ else:
1255
+ return mel_features, info
1256
+
1257
+ def reset_streaming(self):
1258
+ if self._streaming_mel_processor is not None:
1259
+ self._streaming_mel_processor.reset()
1260
+
1261
+ def get_streaming_chunk_size(self) -> int:
1262
+ if self._streaming_mel_processor is None:
1263
+ raise ValueError("Streaming processor not initialized")
1264
+ return self._streaming_mel_processor.get_chunk_size()
1265
+
1266
+ def configure_streaming(
1267
+ self,
1268
+ chunk_ms: int = 100,
1269
+ enable_sliding_window: bool = False,
1270
+ slide_trigger_seconds: float = 30.0,
1271
+ slide_stride_seconds: float = 10.0,
1272
+ ):
1273
+ """Configure streaming processor parameters
1274
+
1275
+ Args:
1276
+ chunk_ms: chunk size in milliseconds
1277
+ enable_sliding_window: whether to enable sliding window (trigger mode)
1278
+ slide_trigger_seconds: trigger threshold for sliding window in seconds
1279
+ slide_stride_seconds: stride for sliding window in seconds
1280
+ """
1281
+ if self.audio_processor is None:
1282
+ raise ValueError("audio_processor is not set")
1283
+
1284
+ self._init_streaming_processor(
1285
+ chunk_ms=chunk_ms,
1286
+ enable_sliding_window=enable_sliding_window,
1287
+ slide_trigger_seconds=slide_trigger_seconds,
1288
+ slide_stride_seconds=slide_stride_seconds,
1289
+ )
1290
+
1291
+ def get_streaming_config(self) -> dict:
1292
+ if self._streaming_mel_processor is None:
1293
+ return {}
1294
+ return self._streaming_mel_processor.get_config()
1295
+
1296
+ def get_streaming_state(self) -> dict:
1297
+ if self._streaming_mel_processor is None:
1298
+ return {}
1299
+ return self._streaming_mel_processor.get_state()
1300
+
1301
+ def get_streaming_snapshot(self) -> dict:
1302
+ if self._streaming_mel_processor is None:
1303
+ return {}
1304
+ return self._streaming_mel_processor.get_snapshot()
1305
+
1306
+ def restore_streaming_snapshot(self, snapshot: dict) -> None:
1307
+ if self._streaming_mel_processor is None:
1308
+ return
1309
+ if not snapshot:
1310
+ return
1311
+ self._streaming_mel_processor.restore_snapshot(snapshot)
1312
+
1313
+ def __call__(
1314
+ self,
1315
+ text: Union[TextInput, PreTokenizedInput, List[TextInput], List[PreTokenizedInput]],
1316
+ images: ImageInput = None,
1317
+ audios: Union[np.ndarray, List[np.ndarray], List[List[np.ndarray]]] = None,
1318
+ audio_parts: Optional[list] = None,
1319
+ max_length: Optional[int] = None,
1320
+ do_pad: Optional[bool] = True,
1321
+ max_slice_nums: int = None,
1322
+ use_image_id: bool = True,
1323
+ stream_input: bool = False,
1324
+ return_tensors: Optional[Union[str, TensorType]] = TensorType.PYTORCH,
1325
+ sampling_rate: Optional[int] = 16000,
1326
+ online_streaming: bool = False,
1327
+ audio_chunk_idx: int = 0,
1328
+ is_last_chunk: bool = False,
1329
+ **kwargs,
1330
+ ) -> MiniCPMOBatchFeature:
1331
+ if images is not None:
1332
+ image_inputs = self.process_image(
1333
+ images=images, do_pad=do_pad, max_slice_nums=max_slice_nums, return_tensors=return_tensors
1334
+ )
1335
+ else:
1336
+ image_inputs = None
1337
+
1338
+ audio_features, audio_feature_lens, audio_phs = self.audio_feature_extract(
1339
+ audios,
1340
+ audio_parts,
1341
+ stream_input,
1342
+ sampling_rate,
1343
+ online_streaming=online_streaming,
1344
+ is_last_chunk=is_last_chunk,
1345
+ )
1346
+
1347
+ model_inputs = self._convert_omni_to_inputs(
1348
+ image_inputs,
1349
+ audio_phs,
1350
+ text,
1351
+ max_slice_nums=max_slice_nums,
1352
+ use_image_id=use_image_id,
1353
+ max_length=max_length,
1354
+ **kwargs,
1355
+ )
1356
+
1357
+ model_inputs["audio_features"] = audio_features
1358
+ model_inputs["audio_feature_lens"] = audio_feature_lens
1359
+
1360
+ result = MiniCPMOBatchFeature(data={**model_inputs})
1361
+
1362
+ if online_streaming:
1363
+ result.use_extra_context = True
1364
+ result.prefix_extra_frames = 0 if audio_chunk_idx == 0 else 2
1365
+ result.suffix_extra_frames = 2
1366
+ result.chunk_idx = audio_chunk_idx
1367
+
1368
+ return result
1369
+
1370
+ def audio_feature_extract(
1371
+ self,
1372
+ audios: Union[np.ndarray, List[np.ndarray], List[List[np.ndarray]], None] = None,
1373
+ audio_parts: Optional[list] = None,
1374
+ stream_input: Optional[bool] = False,
1375
+ sampling_rate: Optional[int] = None,
1376
+ chunk_length: Optional[int] = 1,
1377
+ online_streaming: bool = False,
1378
+ is_last_chunk: bool = False,
1379
+ **kwargs,
1380
+ ):
1381
+ if audios is None:
1382
+ return [], [], []
1383
+
1384
+ if isinstance(audios, np.ndarray):
1385
+ audios_list = [[audios]]
1386
+ elif isinstance(audios[0], np.ndarray):
1387
+ audios_list = [audios]
1388
+ else:
1389
+ audios_list = audios
1390
+
1391
+ if audio_parts is not None:
1392
+ assert len(audio_parts) == len(audios_list)
1393
+ for parts, audios in zip(audio_parts, audios_list):
1394
+ assert len(parts) == len(audios)
1395
+
1396
+ audio_feature_lens_list = []
1397
+ audio_ph_list = []
1398
+ audio_features_all = []
1399
+
1400
+ # audio placeholder not dependent on audio_parts
1401
+ for audios in audios_list:
1402
+ if audios:
1403
+ audio_ph_list.append(
1404
+ [
1405
+ self.get_audio_placeholder(len(a), chunk_input=stream_input, chunk_length=chunk_length)
1406
+ for a in audios
1407
+ ]
1408
+ )
1409
+ else:
1410
+ audio_ph_list.append([])
1411
+
1412
+ for idx, audios in enumerate(audios_list):
1413
+ if audio_parts is not None:
1414
+ # same audio part merge
1415
+ audio_part = audio_parts[idx]
1416
+ merge_audio = []
1417
+ cur_audio = []
1418
+ for aid, (part, audio) in enumerate(zip(audio_part, audios)):
1419
+ if aid == 0 or audio_part[aid] == audio_part[aid - 1]:
1420
+ cur_audio.append(audio)
1421
+ else:
1422
+ merge_audio.append(np.hstack(cur_audio))
1423
+ cur_audio = [audio]
1424
+ if cur_audio:
1425
+ merge_audio.append(np.hstack(cur_audio))
1426
+ else:
1427
+ merge_audio = audios
1428
+
1429
+ # If the audio exceeds 30 seconds, split it into chunks every 30 seconds.
1430
+ final_merge_audio = []
1431
+ max_audio_inp_len = 30 * sampling_rate
1432
+ for audio in merge_audio:
1433
+ if len(audio) <= max_audio_inp_len:
1434
+ final_merge_audio.append(audio)
1435
+ else:
1436
+ for i in range(math.ceil(len(audio) / max_audio_inp_len)):
1437
+ final_merge_audio.append(audio[i * max_audio_inp_len : (i + 1) * max_audio_inp_len])
1438
+
1439
+ audio_feature_lens = []
1440
+
1441
+ if audios:
1442
+ if online_streaming:
1443
+ # online streaming: only support single audio, directly use process_audio_streaming return format
1444
+ assert (
1445
+ len(final_merge_audio) == 1
1446
+ ), f"online streaming mode only supports single audio, currently there are {len(final_merge_audio)}"
1447
+ audio = final_merge_audio[0]
1448
+ result = self.process_audio_streaming(
1449
+ audio, reset=False, return_batch_feature=True, is_last_chunk=is_last_chunk
1450
+ )
1451
+ audio_features_all.append(
1452
+ result["audio_features"].squeeze(0)
1453
+ ) # [1, 80, T] -> [80, T], keep consistent with batch processing
1454
+ audio_feature_lens_list.append(result["audio_feature_lens"][0])
1455
+ else:
1456
+ # batch processing
1457
+ audio_inputs = self.audio_processor(
1458
+ final_merge_audio,
1459
+ sampling_rate=sampling_rate,
1460
+ return_attention_mask=True,
1461
+ padding="max_length",
1462
+ return_tensors="pt",
1463
+ **kwargs,
1464
+ )
1465
+ audio_feature = audio_inputs["input_features"]
1466
+ actual_lens = audio_inputs["attention_mask"].sum(dim=1)
1467
+
1468
+ for feat, lens in zip(audio_feature, actual_lens):
1469
+ audio_features_all.append(feat[:, :lens])
1470
+ audio_feature_lens.append(lens)
1471
+
1472
+ audio_feature_lens = torch.hstack(audio_feature_lens)
1473
+ audio_feature_lens_list.append(audio_feature_lens)
1474
+ else:
1475
+ audio_feature_lens_list.append([])
1476
+
1477
+ if audio_features_all:
1478
+ audio_features = [i.permute(1, 0) for i in audio_features_all]
1479
+ audio_features = torch.nn.utils.rnn.pad_sequence(
1480
+ audio_features, batch_first=True, padding_value=0.0
1481
+ ).permute(0, 2, 1)
1482
+ else:
1483
+ audio_features = []
1484
+
1485
+ return audio_features, audio_feature_lens_list, audio_ph_list
1486
+
1487
+ def _convert(self, input_str, max_inp_length: Optional[int] = None):
1488
+ old_input_ids = self.tokenizer.encode(input_str)
1489
+
1490
+ listen_token_id = self.tokenizer.convert_tokens_to_ids("<|listen|>")
1491
+ input_ids = []
1492
+ for token in old_input_ids:
1493
+ if token != listen_token_id:
1494
+ input_ids.append(token)
1495
+
1496
+ if max_inp_length is not None:
1497
+ input_ids = input_ids[:max_inp_length]
1498
+ input_ids = torch.tensor(input_ids, dtype=torch.int32)
1499
+
1500
+ ## image bound
1501
+ start_cond = (input_ids == self.tokenizer.im_start_id) | (input_ids == self.tokenizer.slice_start_id)
1502
+ end_cond = (input_ids == self.tokenizer.im_end_id) | (input_ids == self.tokenizer.slice_end_id)
1503
+
1504
+ image_start_idx = torch.where(start_cond)[0]
1505
+ image_start_idx += 1
1506
+ image_end_idx = torch.where(end_cond)[0]
1507
+
1508
+ valid_image_nums = max(len(image_start_idx), len(image_end_idx))
1509
+
1510
+ image_bounds = torch.hstack(
1511
+ [
1512
+ image_start_idx[:valid_image_nums].unsqueeze(-1),
1513
+ image_end_idx[:valid_image_nums].unsqueeze(-1),
1514
+ ]
1515
+ )
1516
+
1517
+ ## audio bound
1518
+ audio_start_idx = torch.where(input_ids == self.tokenizer.audio_start_id)[0]
1519
+ audio_end_idx = torch.where(input_ids == self.tokenizer.audio_end_id)[0]
1520
+ assert len(audio_start_idx) == len(audio_end_idx)
1521
+ audio_bounds = torch.hstack([(audio_start_idx + 1).unsqueeze(-1), audio_end_idx.unsqueeze(-1)])
1522
+
1523
+ spk_start_idx = torch.where(input_ids == self.tokenizer.spk_start_id)[0]
1524
+ spk_end_idx = torch.where(input_ids == self.tokenizer.spk_end_id)[0]
1525
+ assert len(spk_start_idx) == len(spk_end_idx)
1526
+ spk_bounds = torch.hstack([(spk_start_idx + 1).unsqueeze(-1), spk_end_idx.unsqueeze(-1)])
1527
+
1528
+ return input_ids, image_bounds, audio_bounds, spk_bounds
1529
+
1530
+ def _convert_omni_to_inputs(
1531
+ self,
1532
+ images,
1533
+ audio_phs,
1534
+ texts: Union[str, List[str]],
1535
+ truncation=None,
1536
+ max_length=None,
1537
+ max_slice_nums=None,
1538
+ use_image_id=None,
1539
+ return_tensors=None,
1540
+ **kwargs,
1541
+ ):
1542
+ if images is None and audio_phs is None:
1543
+ model_inputs = self.tokenizer(
1544
+ texts, return_tensors=return_tensors, truncation=truncation, max_length=max_length, **kwargs
1545
+ )
1546
+ return MiniCPMOBatchFeature(data={**model_inputs})
1547
+
1548
+ image_pattern = "<image>./</image>"
1549
+ audio_pattern = "<audio>./</audio>"
1550
+ split_pattern = f"({image_pattern}|{audio_pattern})"
1551
+
1552
+ if isinstance(texts, str):
1553
+ texts = [texts]
1554
+
1555
+ bs = len(texts)
1556
+ if images is not None:
1557
+ images, image_sizes, tgt_sizes = images["pixel_values"], images["image_sizes"], images["tgt_sizes"]
1558
+ else:
1559
+ images, image_sizes, tgt_sizes = [[]] * bs, [[]] * bs, [[]] * bs
1560
+
1561
+ input_ids_list = []
1562
+ image_bounds_list = []
1563
+ audio_bounds_list = []
1564
+ spk_bounds_list = []
1565
+
1566
+ for index, text in enumerate(texts):
1567
+ text_chunks = re.split(split_pattern, text)
1568
+
1569
+ image_tags = re.findall(image_pattern, text)
1570
+ audio_tags = re.findall(audio_pattern, text)
1571
+
1572
+ if image_tags:
1573
+ assert images is not None
1574
+ assert len(image_tags) == len(image_sizes[index])
1575
+ if audio_tags:
1576
+ assert audio_phs is not None
1577
+ assert len(audio_tags) == len(audio_phs[index])
1578
+
1579
+ image_id = 0
1580
+ audio_id = 0
1581
+ for i, chunk in enumerate(text_chunks):
1582
+ if chunk == image_pattern:
1583
+ image_placeholder = self.image_processor.get_slice_image_placeholder(
1584
+ image_sizes[index][image_id], image_id, max_slice_nums, use_image_id
1585
+ )
1586
+ image_id += 1
1587
+ text_chunks[i] = image_placeholder
1588
+ elif chunk == audio_pattern:
1589
+ audio_placeholder = audio_phs[index][audio_id]
1590
+ audio_id += 1
1591
+ text_chunks[i] = audio_placeholder
1592
+
1593
+ final_text = "".join(text_chunks)
1594
+ input_ids, image_bounds, audio_bounds, spk_bounds = self._convert(final_text, max_length)
1595
+
1596
+ input_ids_list.append(input_ids)
1597
+ image_bounds_list.append(image_bounds)
1598
+ audio_bounds_list.append(audio_bounds)
1599
+ spk_bounds_list.append(spk_bounds)
1600
+
1601
+ padded_input_ids, padding_lengths = self.pad(input_ids_list, padding_side="left")
1602
+ attention_mask = torch.ones_like(padded_input_ids, dtype=torch.bool)
1603
+ for i, length in enumerate(padding_lengths):
1604
+ image_bounds_list[i] = image_bounds_list[i] + length
1605
+ audio_bounds_list[i] = audio_bounds_list[i] + length
1606
+ spk_bounds_list[i] = spk_bounds_list[i] + length
1607
+ attention_mask[i, :length] = False
1608
+
1609
+ data = {
1610
+ "input_ids": padded_input_ids,
1611
+ "attention_mask": attention_mask,
1612
+ "pixel_values": images,
1613
+ "image_sizes": image_sizes,
1614
+ "image_bound": image_bounds_list,
1615
+ "tgt_sizes": tgt_sizes,
1616
+ "audio_bounds": audio_bounds_list,
1617
+ "spk_bounds": spk_bounds_list,
1618
+ }
1619
+
1620
+ return data
1621
+
1622
+ def pad(self, inputs, max_length=None, padding_value=0, padding_side="left"):
1623
+ items = []
1624
+ if isinstance(inputs[0], list):
1625
+ assert isinstance(inputs[0][0], torch.Tensor)
1626
+ for it in inputs:
1627
+ for tr in it:
1628
+ items.append(tr)
1629
+ else:
1630
+ assert isinstance(inputs[0], torch.Tensor)
1631
+ items = inputs
1632
+
1633
+ batch_size = len(items)
1634
+ shape = items[0].shape
1635
+ dim = len(shape)
1636
+ assert dim <= 2
1637
+ if max_length is None:
1638
+ max_length = 0
1639
+ max_length = max(max_length, max(item.shape[-1] for item in items))
1640
+ min_length = min(item.shape[-1] for item in items)
1641
+ dtype = items[0].dtype
1642
+
1643
+ if dim == 0:
1644
+ return torch.stack([item for item in items], dim=0), [0]
1645
+ elif dim == 1:
1646
+ if max_length == min_length:
1647
+ return torch.stack([item for item in items], dim=0), [0] * batch_size
1648
+ tensor = torch.zeros((batch_size, max_length), dtype=dtype) + padding_value
1649
+ else:
1650
+ tensor = torch.zeros((batch_size, max_length, shape[-1]), dtype=dtype) + padding_value
1651
+
1652
+ padding_length = []
1653
+ for i, item in enumerate(items):
1654
+ if dim == 1:
1655
+ if padding_side == "left":
1656
+ tensor[i, -len(item) :] = item.clone()
1657
+ else:
1658
+ tensor[i, : len(item)] = item.clone()
1659
+ elif dim == 2:
1660
+ if padding_side == "left":
1661
+ tensor[i, -len(item) :, :] = item.clone()
1662
+ else:
1663
+ tensor[i, : len(item), :] = item.clone()
1664
+ padding_length.append(tensor.shape[-1] - len(item))
1665
+
1666
+ return tensor, padding_length
Realtime-Venus-Audio/processor_config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoProcessor": "processing_minicpmo.MiniCPMOProcessor"
4
+ },
5
+ "processor_class": "MiniCPMOProcessor"
6
+ }
Realtime-Venus-Audio/special_tokens_map.json ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<unk>",
4
+ "<image>",
5
+ "</image>",
6
+ "<ref>",
7
+ "</ref>",
8
+ "<box>",
9
+ "</box>",
10
+ "<quad>",
11
+ "</quad>",
12
+ "<point>",
13
+ "</point>",
14
+ "<slice>",
15
+ "</slice>",
16
+ "<image_id>",
17
+ "</image_id>",
18
+ "<unit>",
19
+ "</unit>",
20
+ "<answer>",
21
+ "</answer>",
22
+ "<focus>",
23
+ "</focus>",
24
+ "<line>",
25
+ "</line>",
26
+ "<perception>",
27
+ "</perception>",
28
+ "<source_image>",
29
+ "</source_image>",
30
+ "<image_save_to>",
31
+ "</image_save_to>",
32
+ "<|audio_start|>",
33
+ "<|audio|>",
34
+ "<|audio_end|>",
35
+ "<|spk_bos|>",
36
+ "<|spk|>",
37
+ "<|spk_eos|>",
38
+ "<|tts_bos|>",
39
+ "<|tts_eos|>",
40
+ "<|listen|>",
41
+ "<|speak|>",
42
+ "<|interrupt|>",
43
+ "<|vad_start|>",
44
+ "<|vad_end|>",
45
+ "<|emotion_start|>",
46
+ "<|emotion_end|>",
47
+ "<|speed_start|>",
48
+ "<|speed_end|>",
49
+ "<|pitch_start|>",
50
+ "<|pitch_end|>",
51
+ "<|chunk_eos|>",
52
+ "<|chunk_bos|>",
53
+ "<|chunk_tts_bos|>",
54
+ "<|chunk_tts_eos|>",
55
+ "<|tts_pad|>",
56
+ "<|timbre_7|>",
57
+ "<|timbre_8|>",
58
+ "<|timbre_9|>",
59
+ "<|timbre_10|>",
60
+ "<|timbre_11|>",
61
+ "<|timbre_12|>",
62
+ "<|timbre_13|>",
63
+ "<|timbre_14|>",
64
+ "<|timbre_15|>",
65
+ "<|timbre_16|>",
66
+ "<|timbre_17|>",
67
+ "<|timbre_18|>",
68
+ "<|timbre_19|>",
69
+ "<|timbre_20|>",
70
+ "<|timbre_21|>",
71
+ "<|timbre_22|>",
72
+ "<|timbre_23|>",
73
+ "<|timbre_24|>",
74
+ "<|timbre_25|>",
75
+ "<|timbre_26|>",
76
+ "<|timbre_27|>",
77
+ "<|timbre_28|>",
78
+ "<|timbre_29|>",
79
+ "<|timbre_30|>",
80
+ "<|timbre_31|>",
81
+ "<delegate>",
82
+ "</delegate>",
83
+ "<backend>",
84
+ "</backend>",
85
+ {
86
+ "content": "<|im_end|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false
91
+ }
92
+ ],
93
+ "bos_token": {
94
+ "content": "<|im_start|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false
99
+ },
100
+ "eos_token": {
101
+ "content": "<|im_end|>",
102
+ "lstrip": false,
103
+ "normalized": false,
104
+ "rstrip": false,
105
+ "single_word": false
106
+ },
107
+ "pad_token": {
108
+ "content": "<|endoftext|>",
109
+ "lstrip": false,
110
+ "normalized": false,
111
+ "rstrip": false,
112
+ "single_word": false
113
+ },
114
+ "unk_token": {
115
+ "content": "<unk>",
116
+ "lstrip": false,
117
+ "normalized": false,
118
+ "rstrip": false,
119
+ "single_word": false
120
+ }
121
+ }
Realtime-Venus-Audio/tokenization_minicpmo_fast.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Copyright 2026 The OpenBMB Team. All rights reserved.
4
+ # Modifications Copyright 2026 The Realtime-Venus Team.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ from typing import List
19
+
20
+ from transformers import Qwen2TokenizerFast
21
+
22
+
23
+ class MiniCPMOTokenizerFast(Qwen2TokenizerFast):
24
+ def __init__(self, **kwargs):
25
+ self._bad_token_ids = kwargs.pop("bad_token_ids", [])
26
+
27
+ super().__init__(**kwargs)
28
+
29
+ # image
30
+ self.im_start = "<image>"
31
+ self.im_end = "</image>"
32
+ self.ref_start = "<ref>"
33
+ self.ref_end = "</ref>"
34
+ self.box_start = "<box>"
35
+ self.box_end = "</box>"
36
+ self.quad_start = "<quad>"
37
+ self.quad_end = "</quad>"
38
+ self.slice_start = "<slice>"
39
+ self.slice_end = "</slice>"
40
+ self.im_id_start = "<image_id>"
41
+ self.im_id_end = "</image_id>"
42
+
43
+ # audio
44
+ self.audio_start = "<|audio_start|>"
45
+ self.audio_end = "<|audio_end|>"
46
+ self.spk_start = "<|spk_bos|>"
47
+ self.spk_end = "<|spk_eos|>"
48
+ self.tts_start = "<|tts_bos|>"
49
+ self.tts_end = "<|tts_eos|>"
50
+
51
+ @property
52
+ def eos_id(self):
53
+ return self.eos_token_id
54
+
55
+ @property
56
+ def bos_id(self):
57
+ return self.bos_token_id
58
+
59
+ @property
60
+ def unk_id(self):
61
+ return self.unk_token_id
62
+
63
+ @property
64
+ def im_start_id(self):
65
+ return self.convert_tokens_to_ids(self.im_start)
66
+
67
+ @property
68
+ def im_end_id(self):
69
+ return self.convert_tokens_to_ids(self.im_end)
70
+
71
+ @property
72
+ def slice_start_id(self):
73
+ return self.convert_tokens_to_ids(self.slice_start)
74
+
75
+ @property
76
+ def slice_end_id(self):
77
+ return self.convert_tokens_to_ids(self.slice_end)
78
+
79
+ @property
80
+ def im_id_start_id(self):
81
+ return self.convert_tokens_to_ids(self.im_id_start)
82
+
83
+ @property
84
+ def im_id_end_id(self):
85
+ return self.convert_tokens_to_ids(self.im_id_end)
86
+
87
+ @property
88
+ def audio_start_id(self):
89
+ return self.convert_tokens_to_ids(self.audio_start)
90
+
91
+ @property
92
+ def audio_end_id(self):
93
+ return self.convert_tokens_to_ids(self.audio_end)
94
+
95
+ @property
96
+ def spk_start_id(self):
97
+ return self.convert_tokens_to_ids(self.spk_start)
98
+
99
+ @property
100
+ def spk_end_id(self):
101
+ return self.convert_tokens_to_ids(self.spk_end)
102
+
103
+ @property
104
+ def tts_start_id(self):
105
+ return self.convert_tokens_to_ids(self.tts_start)
106
+
107
+ @property
108
+ def tts_end_id(self):
109
+ return self.convert_tokens_to_ids(self.tts_end)
110
+
111
+ @staticmethod
112
+ def escape(text: str) -> str:
113
+ return text
114
+
115
+ @staticmethod
116
+ def unescape(text: str) -> str:
117
+ return text
118
+
119
+ @property
120
+ def bad_token_ids(self) -> List[int]:
121
+ return self._bad_token_ids
Realtime-Venus-Audio/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6d694bbb227bd59984fcad14c7d807ad74507019c9a1a8013d325c9d87977432
3
+ size 11438460
Realtime-Venus-Audio/tokenizer_config.json ADDED
@@ -0,0 +1,990 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_prefix_space": false,
4
+ "added_tokens_decoder": {
5
+ "128244": {
6
+ "content": "<unk>",
7
+ "lstrip": false,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false,
11
+ "special": true
12
+ },
13
+ "151643": {
14
+ "content": "<|endoftext|>",
15
+ "lstrip": false,
16
+ "normalized": false,
17
+ "rstrip": false,
18
+ "single_word": false,
19
+ "special": true
20
+ },
21
+ "151644": {
22
+ "content": "<|im_start|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false,
27
+ "special": true
28
+ },
29
+ "151645": {
30
+ "content": "<|im_end|>",
31
+ "lstrip": false,
32
+ "normalized": false,
33
+ "rstrip": false,
34
+ "single_word": false,
35
+ "special": true
36
+ },
37
+ "151646": {
38
+ "content": "<|object_ref_start|>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false,
43
+ "special": true
44
+ },
45
+ "151647": {
46
+ "content": "<|object_ref_end|>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false,
51
+ "special": true
52
+ },
53
+ "151648": {
54
+ "content": "<|box_start|>",
55
+ "lstrip": false,
56
+ "normalized": false,
57
+ "rstrip": false,
58
+ "single_word": false,
59
+ "special": true
60
+ },
61
+ "151649": {
62
+ "content": "<|box_end|>",
63
+ "lstrip": false,
64
+ "normalized": false,
65
+ "rstrip": false,
66
+ "single_word": false,
67
+ "special": true
68
+ },
69
+ "151650": {
70
+ "content": "<|quad_start|>",
71
+ "lstrip": false,
72
+ "normalized": false,
73
+ "rstrip": false,
74
+ "single_word": false,
75
+ "special": true
76
+ },
77
+ "151651": {
78
+ "content": "<|quad_end|>",
79
+ "lstrip": false,
80
+ "normalized": false,
81
+ "rstrip": false,
82
+ "single_word": false,
83
+ "special": true
84
+ },
85
+ "151652": {
86
+ "content": "<|vision_start|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false,
91
+ "special": true
92
+ },
93
+ "151653": {
94
+ "content": "<|vision_end|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false,
99
+ "special": true
100
+ },
101
+ "151654": {
102
+ "content": "<|vision_pad|>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false,
107
+ "special": true
108
+ },
109
+ "151655": {
110
+ "content": "<|image_pad|>",
111
+ "lstrip": false,
112
+ "normalized": false,
113
+ "rstrip": false,
114
+ "single_word": false,
115
+ "special": true
116
+ },
117
+ "151656": {
118
+ "content": "<|video_pad|>",
119
+ "lstrip": false,
120
+ "normalized": false,
121
+ "rstrip": false,
122
+ "single_word": false,
123
+ "special": true
124
+ },
125
+ "151657": {
126
+ "content": "<tool_call>",
127
+ "lstrip": false,
128
+ "normalized": false,
129
+ "rstrip": false,
130
+ "single_word": false,
131
+ "special": false
132
+ },
133
+ "151658": {
134
+ "content": "</tool_call>",
135
+ "lstrip": false,
136
+ "normalized": false,
137
+ "rstrip": false,
138
+ "single_word": false,
139
+ "special": false
140
+ },
141
+ "151659": {
142
+ "content": "<|fim_prefix|>",
143
+ "lstrip": false,
144
+ "normalized": false,
145
+ "rstrip": false,
146
+ "single_word": false,
147
+ "special": false
148
+ },
149
+ "151660": {
150
+ "content": "<|fim_middle|>",
151
+ "lstrip": false,
152
+ "normalized": false,
153
+ "rstrip": false,
154
+ "single_word": false,
155
+ "special": false
156
+ },
157
+ "151661": {
158
+ "content": "<|fim_suffix|>",
159
+ "lstrip": false,
160
+ "normalized": false,
161
+ "rstrip": false,
162
+ "single_word": false,
163
+ "special": false
164
+ },
165
+ "151662": {
166
+ "content": "<|fim_pad|>",
167
+ "lstrip": false,
168
+ "normalized": false,
169
+ "rstrip": false,
170
+ "single_word": false,
171
+ "special": false
172
+ },
173
+ "151663": {
174
+ "content": "<|repo_name|>",
175
+ "lstrip": false,
176
+ "normalized": false,
177
+ "rstrip": false,
178
+ "single_word": false,
179
+ "special": false
180
+ },
181
+ "151664": {
182
+ "content": "<|file_sep|>",
183
+ "lstrip": false,
184
+ "normalized": false,
185
+ "rstrip": false,
186
+ "single_word": false,
187
+ "special": false
188
+ },
189
+ "151665": {
190
+ "content": "<tool_response>",
191
+ "lstrip": false,
192
+ "normalized": false,
193
+ "rstrip": false,
194
+ "single_word": false,
195
+ "special": false
196
+ },
197
+ "151666": {
198
+ "content": "</tool_response>",
199
+ "lstrip": false,
200
+ "normalized": false,
201
+ "rstrip": false,
202
+ "single_word": false,
203
+ "special": false
204
+ },
205
+ "151667": {
206
+ "content": "<think>",
207
+ "lstrip": false,
208
+ "normalized": false,
209
+ "rstrip": false,
210
+ "single_word": false,
211
+ "special": false
212
+ },
213
+ "151668": {
214
+ "content": "</think>",
215
+ "lstrip": false,
216
+ "normalized": false,
217
+ "rstrip": false,
218
+ "single_word": false,
219
+ "special": false
220
+ },
221
+ "151669": {
222
+ "content": "<image>",
223
+ "lstrip": false,
224
+ "normalized": false,
225
+ "rstrip": false,
226
+ "single_word": false,
227
+ "special": true
228
+ },
229
+ "151670": {
230
+ "content": "</image>",
231
+ "lstrip": false,
232
+ "normalized": false,
233
+ "rstrip": false,
234
+ "single_word": false,
235
+ "special": true
236
+ },
237
+ "151671": {
238
+ "content": "<ref>",
239
+ "lstrip": false,
240
+ "normalized": false,
241
+ "rstrip": false,
242
+ "single_word": false,
243
+ "special": true
244
+ },
245
+ "151672": {
246
+ "content": "</ref>",
247
+ "lstrip": false,
248
+ "normalized": false,
249
+ "rstrip": false,
250
+ "single_word": false,
251
+ "special": true
252
+ },
253
+ "151673": {
254
+ "content": "<box>",
255
+ "lstrip": false,
256
+ "normalized": false,
257
+ "rstrip": false,
258
+ "single_word": false,
259
+ "special": true
260
+ },
261
+ "151674": {
262
+ "content": "</box>",
263
+ "lstrip": false,
264
+ "normalized": false,
265
+ "rstrip": false,
266
+ "single_word": false,
267
+ "special": true
268
+ },
269
+ "151675": {
270
+ "content": "<quad>",
271
+ "lstrip": false,
272
+ "normalized": false,
273
+ "rstrip": false,
274
+ "single_word": false,
275
+ "special": true
276
+ },
277
+ "151676": {
278
+ "content": "</quad>",
279
+ "lstrip": false,
280
+ "normalized": false,
281
+ "rstrip": false,
282
+ "single_word": false,
283
+ "special": true
284
+ },
285
+ "151677": {
286
+ "content": "<point>",
287
+ "lstrip": false,
288
+ "normalized": false,
289
+ "rstrip": false,
290
+ "single_word": false,
291
+ "special": true
292
+ },
293
+ "151678": {
294
+ "content": "</point>",
295
+ "lstrip": false,
296
+ "normalized": false,
297
+ "rstrip": false,
298
+ "single_word": false,
299
+ "special": true
300
+ },
301
+ "151679": {
302
+ "content": "<slice>",
303
+ "lstrip": false,
304
+ "normalized": false,
305
+ "rstrip": false,
306
+ "single_word": false,
307
+ "special": true
308
+ },
309
+ "151680": {
310
+ "content": "</slice>",
311
+ "lstrip": false,
312
+ "normalized": false,
313
+ "rstrip": false,
314
+ "single_word": false,
315
+ "special": true
316
+ },
317
+ "151681": {
318
+ "content": "<image_id>",
319
+ "lstrip": false,
320
+ "normalized": false,
321
+ "rstrip": false,
322
+ "single_word": false,
323
+ "special": true
324
+ },
325
+ "151682": {
326
+ "content": "</image_id>",
327
+ "lstrip": false,
328
+ "normalized": false,
329
+ "rstrip": false,
330
+ "single_word": false,
331
+ "special": true
332
+ },
333
+ "151683": {
334
+ "content": "<unit>",
335
+ "lstrip": false,
336
+ "normalized": false,
337
+ "rstrip": false,
338
+ "single_word": false,
339
+ "special": true
340
+ },
341
+ "151684": {
342
+ "content": "</unit>",
343
+ "lstrip": false,
344
+ "normalized": false,
345
+ "rstrip": false,
346
+ "single_word": false,
347
+ "special": true
348
+ },
349
+ "151685": {
350
+ "content": "<answer>",
351
+ "lstrip": false,
352
+ "normalized": false,
353
+ "rstrip": false,
354
+ "single_word": false,
355
+ "special": true
356
+ },
357
+ "151686": {
358
+ "content": "</answer>",
359
+ "lstrip": false,
360
+ "normalized": false,
361
+ "rstrip": false,
362
+ "single_word": false,
363
+ "special": true
364
+ },
365
+ "151687": {
366
+ "content": "<focus>",
367
+ "lstrip": false,
368
+ "normalized": false,
369
+ "rstrip": false,
370
+ "single_word": false,
371
+ "special": true
372
+ },
373
+ "151688": {
374
+ "content": "</focus>",
375
+ "lstrip": false,
376
+ "normalized": false,
377
+ "rstrip": false,
378
+ "single_word": false,
379
+ "special": true
380
+ },
381
+ "151689": {
382
+ "content": "<line>",
383
+ "lstrip": false,
384
+ "normalized": false,
385
+ "rstrip": false,
386
+ "single_word": false,
387
+ "special": true
388
+ },
389
+ "151690": {
390
+ "content": "</line>",
391
+ "lstrip": false,
392
+ "normalized": false,
393
+ "rstrip": false,
394
+ "single_word": false,
395
+ "special": true
396
+ },
397
+ "151691": {
398
+ "content": "<perception>",
399
+ "lstrip": false,
400
+ "normalized": false,
401
+ "rstrip": false,
402
+ "single_word": false,
403
+ "special": true
404
+ },
405
+ "151692": {
406
+ "content": "</perception>",
407
+ "lstrip": false,
408
+ "normalized": false,
409
+ "rstrip": false,
410
+ "single_word": false,
411
+ "special": true
412
+ },
413
+ "151693": {
414
+ "content": "<source_image>",
415
+ "lstrip": false,
416
+ "normalized": false,
417
+ "rstrip": false,
418
+ "single_word": false,
419
+ "special": true
420
+ },
421
+ "151694": {
422
+ "content": "</source_image>",
423
+ "lstrip": false,
424
+ "normalized": false,
425
+ "rstrip": false,
426
+ "single_word": false,
427
+ "special": true
428
+ },
429
+ "151695": {
430
+ "content": "<image_save_to>",
431
+ "lstrip": false,
432
+ "normalized": false,
433
+ "rstrip": false,
434
+ "single_word": false,
435
+ "special": true
436
+ },
437
+ "151696": {
438
+ "content": "</image_save_to>",
439
+ "lstrip": false,
440
+ "normalized": false,
441
+ "rstrip": false,
442
+ "single_word": false,
443
+ "special": true
444
+ },
445
+ "151697": {
446
+ "content": "<|audio_start|>",
447
+ "lstrip": false,
448
+ "normalized": false,
449
+ "rstrip": false,
450
+ "single_word": false,
451
+ "special": true
452
+ },
453
+ "151698": {
454
+ "content": "<|audio|>",
455
+ "lstrip": false,
456
+ "normalized": false,
457
+ "rstrip": false,
458
+ "single_word": false,
459
+ "special": true
460
+ },
461
+ "151699": {
462
+ "content": "<|audio_end|>",
463
+ "lstrip": false,
464
+ "normalized": false,
465
+ "rstrip": false,
466
+ "single_word": false,
467
+ "special": true
468
+ },
469
+ "151700": {
470
+ "content": "<|spk_bos|>",
471
+ "lstrip": false,
472
+ "normalized": false,
473
+ "rstrip": false,
474
+ "single_word": false,
475
+ "special": true
476
+ },
477
+ "151701": {
478
+ "content": "<|spk|>",
479
+ "lstrip": false,
480
+ "normalized": false,
481
+ "rstrip": false,
482
+ "single_word": false,
483
+ "special": true
484
+ },
485
+ "151702": {
486
+ "content": "<|spk_eos|>",
487
+ "lstrip": false,
488
+ "normalized": false,
489
+ "rstrip": false,
490
+ "single_word": false,
491
+ "special": true
492
+ },
493
+ "151703": {
494
+ "content": "<|tts_bos|>",
495
+ "lstrip": false,
496
+ "normalized": false,
497
+ "rstrip": false,
498
+ "single_word": false,
499
+ "special": true
500
+ },
501
+ "151704": {
502
+ "content": "<|tts_eos|>",
503
+ "lstrip": false,
504
+ "normalized": false,
505
+ "rstrip": false,
506
+ "single_word": false,
507
+ "special": true
508
+ },
509
+ "151705": {
510
+ "content": "<|listen|>",
511
+ "lstrip": false,
512
+ "normalized": false,
513
+ "rstrip": false,
514
+ "single_word": false,
515
+ "special": true
516
+ },
517
+ "151706": {
518
+ "content": "<|speak|>",
519
+ "lstrip": false,
520
+ "normalized": false,
521
+ "rstrip": false,
522
+ "single_word": false,
523
+ "special": true
524
+ },
525
+ "151707": {
526
+ "content": "<|interrupt|>",
527
+ "lstrip": false,
528
+ "normalized": false,
529
+ "rstrip": false,
530
+ "single_word": false,
531
+ "special": true
532
+ },
533
+ "151708": {
534
+ "content": "<|vad_start|>",
535
+ "lstrip": false,
536
+ "normalized": false,
537
+ "rstrip": false,
538
+ "single_word": false,
539
+ "special": true
540
+ },
541
+ "151709": {
542
+ "content": "<|vad_end|>",
543
+ "lstrip": false,
544
+ "normalized": false,
545
+ "rstrip": false,
546
+ "single_word": false,
547
+ "special": true
548
+ },
549
+ "151710": {
550
+ "content": "<|emotion_start|>",
551
+ "lstrip": false,
552
+ "normalized": false,
553
+ "rstrip": false,
554
+ "single_word": false,
555
+ "special": true
556
+ },
557
+ "151711": {
558
+ "content": "<|emotion_end|>",
559
+ "lstrip": false,
560
+ "normalized": false,
561
+ "rstrip": false,
562
+ "single_word": false,
563
+ "special": true
564
+ },
565
+ "151712": {
566
+ "content": "<|speed_start|>",
567
+ "lstrip": false,
568
+ "normalized": false,
569
+ "rstrip": false,
570
+ "single_word": false,
571
+ "special": true
572
+ },
573
+ "151713": {
574
+ "content": "<|speed_end|>",
575
+ "lstrip": false,
576
+ "normalized": false,
577
+ "rstrip": false,
578
+ "single_word": false,
579
+ "special": true
580
+ },
581
+ "151714": {
582
+ "content": "<|pitch_start|>",
583
+ "lstrip": false,
584
+ "normalized": false,
585
+ "rstrip": false,
586
+ "single_word": false,
587
+ "special": true
588
+ },
589
+ "151715": {
590
+ "content": "<|pitch_end|>",
591
+ "lstrip": false,
592
+ "normalized": false,
593
+ "rstrip": false,
594
+ "single_word": false,
595
+ "special": true
596
+ },
597
+ "151716": {
598
+ "content": "<|turn_bos|>",
599
+ "lstrip": false,
600
+ "normalized": false,
601
+ "rstrip": false,
602
+ "single_word": false,
603
+ "special": true
604
+ },
605
+ "151717": {
606
+ "content": "<|turn_eos|>",
607
+ "lstrip": false,
608
+ "normalized": false,
609
+ "rstrip": false,
610
+ "single_word": false,
611
+ "special": true
612
+ },
613
+ "151718": {
614
+ "content": "<|chunk_eos|>",
615
+ "lstrip": false,
616
+ "normalized": false,
617
+ "rstrip": false,
618
+ "single_word": false,
619
+ "special": true
620
+ },
621
+ "151719": {
622
+ "content": "<|chunk_bos|>",
623
+ "lstrip": false,
624
+ "normalized": false,
625
+ "rstrip": false,
626
+ "single_word": false,
627
+ "special": true
628
+ },
629
+ "151720": {
630
+ "content": "<|chunk_tts_bos|>",
631
+ "lstrip": false,
632
+ "normalized": false,
633
+ "rstrip": false,
634
+ "single_word": false,
635
+ "special": true
636
+ },
637
+ "151721": {
638
+ "content": "<|chunk_tts_eos|>",
639
+ "lstrip": false,
640
+ "normalized": false,
641
+ "rstrip": false,
642
+ "single_word": false,
643
+ "special": true
644
+ },
645
+ "151722": {
646
+ "content": "<|tts_pad|>",
647
+ "lstrip": false,
648
+ "normalized": false,
649
+ "rstrip": false,
650
+ "single_word": false,
651
+ "special": true
652
+ },
653
+ "151723": {
654
+ "content": "<|timbre_7|>",
655
+ "lstrip": false,
656
+ "normalized": false,
657
+ "rstrip": false,
658
+ "single_word": false,
659
+ "special": true
660
+ },
661
+ "151724": {
662
+ "content": "<|timbre_8|>",
663
+ "lstrip": false,
664
+ "normalized": false,
665
+ "rstrip": false,
666
+ "single_word": false,
667
+ "special": true
668
+ },
669
+ "151725": {
670
+ "content": "<|timbre_9|>",
671
+ "lstrip": false,
672
+ "normalized": false,
673
+ "rstrip": false,
674
+ "single_word": false,
675
+ "special": true
676
+ },
677
+ "151726": {
678
+ "content": "<|timbre_10|>",
679
+ "lstrip": false,
680
+ "normalized": false,
681
+ "rstrip": false,
682
+ "single_word": false,
683
+ "special": true
684
+ },
685
+ "151727": {
686
+ "content": "<|timbre_11|>",
687
+ "lstrip": false,
688
+ "normalized": false,
689
+ "rstrip": false,
690
+ "single_word": false,
691
+ "special": true
692
+ },
693
+ "151728": {
694
+ "content": "<|timbre_12|>",
695
+ "lstrip": false,
696
+ "normalized": false,
697
+ "rstrip": false,
698
+ "single_word": false,
699
+ "special": true
700
+ },
701
+ "151729": {
702
+ "content": "<|timbre_13|>",
703
+ "lstrip": false,
704
+ "normalized": false,
705
+ "rstrip": false,
706
+ "single_word": false,
707
+ "special": true
708
+ },
709
+ "151730": {
710
+ "content": "<|timbre_14|>",
711
+ "lstrip": false,
712
+ "normalized": false,
713
+ "rstrip": false,
714
+ "single_word": false,
715
+ "special": true
716
+ },
717
+ "151731": {
718
+ "content": "<|timbre_15|>",
719
+ "lstrip": false,
720
+ "normalized": false,
721
+ "rstrip": false,
722
+ "single_word": false,
723
+ "special": true
724
+ },
725
+ "151732": {
726
+ "content": "<|timbre_16|>",
727
+ "lstrip": false,
728
+ "normalized": false,
729
+ "rstrip": false,
730
+ "single_word": false,
731
+ "special": true
732
+ },
733
+ "151733": {
734
+ "content": "<|timbre_17|>",
735
+ "lstrip": false,
736
+ "normalized": false,
737
+ "rstrip": false,
738
+ "single_word": false,
739
+ "special": true
740
+ },
741
+ "151734": {
742
+ "content": "<|timbre_18|>",
743
+ "lstrip": false,
744
+ "normalized": false,
745
+ "rstrip": false,
746
+ "single_word": false,
747
+ "special": true
748
+ },
749
+ "151735": {
750
+ "content": "<|timbre_19|>",
751
+ "lstrip": false,
752
+ "normalized": false,
753
+ "rstrip": false,
754
+ "single_word": false,
755
+ "special": true
756
+ },
757
+ "151736": {
758
+ "content": "<|timbre_20|>",
759
+ "lstrip": false,
760
+ "normalized": false,
761
+ "rstrip": false,
762
+ "single_word": false,
763
+ "special": true
764
+ },
765
+ "151737": {
766
+ "content": "<|timbre_21|>",
767
+ "lstrip": false,
768
+ "normalized": false,
769
+ "rstrip": false,
770
+ "single_word": false,
771
+ "special": true
772
+ },
773
+ "151738": {
774
+ "content": "<|timbre_22|>",
775
+ "lstrip": false,
776
+ "normalized": false,
777
+ "rstrip": false,
778
+ "single_word": false,
779
+ "special": true
780
+ },
781
+ "151739": {
782
+ "content": "<|timbre_23|>",
783
+ "lstrip": false,
784
+ "normalized": false,
785
+ "rstrip": false,
786
+ "single_word": false,
787
+ "special": true
788
+ },
789
+ "151740": {
790
+ "content": "<|timbre_24|>",
791
+ "lstrip": false,
792
+ "normalized": false,
793
+ "rstrip": false,
794
+ "single_word": false,
795
+ "special": true
796
+ },
797
+ "151741": {
798
+ "content": "<|timbre_25|>",
799
+ "lstrip": false,
800
+ "normalized": false,
801
+ "rstrip": false,
802
+ "single_word": false,
803
+ "special": true
804
+ },
805
+ "151742": {
806
+ "content": "<|timbre_26|>",
807
+ "lstrip": false,
808
+ "normalized": false,
809
+ "rstrip": false,
810
+ "single_word": false,
811
+ "special": true
812
+ },
813
+ "151743": {
814
+ "content": "<|timbre_27|>",
815
+ "lstrip": false,
816
+ "normalized": false,
817
+ "rstrip": false,
818
+ "single_word": false,
819
+ "special": true
820
+ },
821
+ "151744": {
822
+ "content": "<|timbre_28|>",
823
+ "lstrip": false,
824
+ "normalized": false,
825
+ "rstrip": false,
826
+ "single_word": false,
827
+ "special": true
828
+ },
829
+ "151745": {
830
+ "content": "<|timbre_29|>",
831
+ "lstrip": false,
832
+ "normalized": false,
833
+ "rstrip": false,
834
+ "single_word": false,
835
+ "special": true
836
+ },
837
+ "151746": {
838
+ "content": "<|timbre_30|>",
839
+ "lstrip": false,
840
+ "normalized": false,
841
+ "rstrip": false,
842
+ "single_word": false,
843
+ "special": true
844
+ },
845
+ "151747": {
846
+ "content": "<|timbre_31|>",
847
+ "lstrip": false,
848
+ "normalized": false,
849
+ "rstrip": false,
850
+ "single_word": false,
851
+ "special": true
852
+ },
853
+ "151748": {
854
+ "content": "<delegate>",
855
+ "lstrip": false,
856
+ "normalized": false,
857
+ "rstrip": false,
858
+ "single_word": false,
859
+ "special": true
860
+ },
861
+ "151749": {
862
+ "content": "</delegate>",
863
+ "lstrip": false,
864
+ "normalized": false,
865
+ "rstrip": false,
866
+ "single_word": false,
867
+ "special": true
868
+ },
869
+ "151750": {
870
+ "content": "<backend>",
871
+ "lstrip": false,
872
+ "normalized": false,
873
+ "rstrip": false,
874
+ "single_word": false,
875
+ "special": true
876
+ },
877
+ "151751": {
878
+ "content": "</backend>",
879
+ "lstrip": false,
880
+ "normalized": false,
881
+ "rstrip": false,
882
+ "single_word": false,
883
+ "special": true
884
+ }
885
+ },
886
+ "additional_special_tokens": [
887
+ "<unk>",
888
+ "<image>",
889
+ "</image>",
890
+ "<ref>",
891
+ "</ref>",
892
+ "<box>",
893
+ "</box>",
894
+ "<quad>",
895
+ "</quad>",
896
+ "<point>",
897
+ "</point>",
898
+ "<slice>",
899
+ "</slice>",
900
+ "<image_id>",
901
+ "</image_id>",
902
+ "<unit>",
903
+ "</unit>",
904
+ "<answer>",
905
+ "</answer>",
906
+ "<focus>",
907
+ "</focus>",
908
+ "<line>",
909
+ "</line>",
910
+ "<perception>",
911
+ "</perception>",
912
+ "<source_image>",
913
+ "</source_image>",
914
+ "<image_save_to>",
915
+ "</image_save_to>",
916
+ "<|audio_start|>",
917
+ "<|audio|>",
918
+ "<|audio_end|>",
919
+ "<|spk_bos|>",
920
+ "<|spk|>",
921
+ "<|spk_eos|>",
922
+ "<|tts_bos|>",
923
+ "<|tts_eos|>",
924
+ "<|listen|>",
925
+ "<|speak|>",
926
+ "<|interrupt|>",
927
+ "<|vad_start|>",
928
+ "<|vad_end|>",
929
+ "<|emotion_start|>",
930
+ "<|emotion_end|>",
931
+ "<|speed_start|>",
932
+ "<|speed_end|>",
933
+ "<|pitch_start|>",
934
+ "<|pitch_end|>",
935
+ "<|chunk_eos|>",
936
+ "<|chunk_bos|>",
937
+ "<|chunk_tts_bos|>",
938
+ "<|chunk_tts_eos|>",
939
+ "<|tts_pad|>",
940
+ "<|timbre_7|>",
941
+ "<|timbre_8|>",
942
+ "<|timbre_9|>",
943
+ "<|timbre_10|>",
944
+ "<|timbre_11|>",
945
+ "<|timbre_12|>",
946
+ "<|timbre_13|>",
947
+ "<|timbre_14|>",
948
+ "<|timbre_15|>",
949
+ "<|timbre_16|>",
950
+ "<|timbre_17|>",
951
+ "<|timbre_18|>",
952
+ "<|timbre_19|>",
953
+ "<|timbre_20|>",
954
+ "<|timbre_21|>",
955
+ "<|timbre_22|>",
956
+ "<|timbre_23|>",
957
+ "<|timbre_24|>",
958
+ "<|timbre_25|>",
959
+ "<|timbre_26|>",
960
+ "<|timbre_27|>",
961
+ "<|timbre_28|>",
962
+ "<|timbre_29|>",
963
+ "<|timbre_30|>",
964
+ "<|timbre_31|>",
965
+ "<delegate>",
966
+ "</delegate>",
967
+ "<backend>",
968
+ "</backend>",
969
+ "<|im_end|>"
970
+ ],
971
+ "auto_map": {
972
+ "AutoProcessor": "processing_minicpmo.MiniCPMOProcessor",
973
+ "AutoTokenizer": [
974
+ "tokenization_qwen2.Qwen2Tokenizer",
975
+ "tokenization_minicpmo_fast.MiniCPMOTokenizerFast"
976
+ ]
977
+ },
978
+ "bos_token": "<|im_start|>",
979
+ "clean_up_tokenization_spaces": false,
980
+ "eos_token": "<|im_end|>",
981
+ "errors": "replace",
982
+ "extra_special_tokens": {},
983
+ "model_max_length": 131072,
984
+ "pad_token": "<|endoftext|>",
985
+ "padding_side": "left",
986
+ "processor_class": "MiniCPMOProcessor",
987
+ "split_special_tokens": false,
988
+ "tokenizer_class": "MiniCPMOTokenizer",
989
+ "unk_token": "<unk>"
990
+ }
Realtime-Venus-Audio/utils.py ADDED
@@ -0,0 +1,2418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Copyright 2026 The OpenBMB Team. All rights reserved.
4
+ # Modifications Copyright 2026 The Realtime-Venus Team.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ import logging
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+ from typing import Dict
22
+ from typing import List
23
+ from typing import Literal
24
+ from typing import Optional
25
+ from typing import Tuple
26
+ from typing import Union
27
+
28
+ import torch
29
+ import torch.nn.functional as F
30
+ import torch.nn.utils.parametrize as P
31
+ from transformers.cache_utils import DynamicCache
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ # text
37
+ @dataclass
38
+ class GenerateChunkOutput:
39
+ chunk_token_ids: torch.Tensor
40
+ current_inputs_embeds: torch.Tensor
41
+ input_last_hidden_states: Optional[torch.Tensor] # for tts use_speaker_embedding
42
+ last_hidden_states: Optional[torch.Tensor] # for tts input feature (projector_semantic)
43
+ past_key_values: Optional[torch.Tensor]
44
+ finished: bool
45
+
46
+
47
+ class ChunkPrefillChunkGenerate:
48
+ def __init__(self, model, tokenizer, terminators):
49
+ self.tokenizer = tokenizer
50
+ self.model = model
51
+ self.terminators = terminators
52
+ self.terminators_ids = [tokenizer.convert_tokens_to_ids(i) for i in self.terminators]
53
+ self.embedding_layer = self.model.get_input_embeddings()
54
+
55
+ self.forbidden_tokens = [
56
+ ":",
57
+ ":",
58
+ ";",
59
+ "#",
60
+ "“",
61
+ "”",
62
+ "‘",
63
+ "’",
64
+ "@",
65
+ "*",
66
+ "【",
67
+ "】",
68
+ "「",
69
+ "」",
70
+ "(",
71
+ ")",
72
+ "(",
73
+ ")",
74
+ "[",
75
+ "]",
76
+ "&",
77
+ "/",
78
+ "$",
79
+ ]
80
+
81
+ self.forbidden_token_ids = [tokenizer.convert_tokens_to_ids(i) for i in self.forbidden_tokens]
82
+ bad_token_ids = getattr(tokenizer, "bad_token_ids", [])
83
+ if bad_token_ids:
84
+ self.forbidden_token_ids.extend(bad_token_ids)
85
+
86
+ @staticmethod
87
+ def prepare_generation_config(do_sample, max_new_tokens=50, min_new_tokens=0, **kwargs):
88
+ num_beams = kwargs.get("num_beams", 3)
89
+ generation_config = {
90
+ "num_beams": num_beams,
91
+ "top_p": 0.8,
92
+ "top_k": 100,
93
+ "temperature": 0.7,
94
+ "do_sample": True,
95
+ "repetition_penalty": 1.05,
96
+ }
97
+
98
+ if do_sample:
99
+ generation_config.update(
100
+ {
101
+ "top_p": 0.8,
102
+ "top_k": 100,
103
+ "temperature": 0.7,
104
+ "do_sample": True,
105
+ "repetition_penalty": 1.05,
106
+ }
107
+ )
108
+ elif num_beams > 1:
109
+ generation_config.update({"num_beams": num_beams, "repetition_penalty": 1.2, "do_sample": False})
110
+ else:
111
+ generation_config.update({"do_sample": False, "repetition_penalty": 1.05})
112
+
113
+ generation_config.update((k, kwargs[k]) for k in generation_config.keys() & kwargs.keys())
114
+ generation_config["min_new_tokens"] = min_new_tokens
115
+ generation_config["max_new_tokens"] = max_new_tokens
116
+
117
+ return generation_config
118
+
119
+ def chunk_generate(
120
+ self,
121
+ inputs_embeds: torch.Tensor,
122
+ past_key_values,
123
+ is_first_generate_chunk: bool,
124
+ chunk_size: int,
125
+ return_hidden_states: bool,
126
+ do_sample: bool,
127
+ temperature: float,
128
+ top_p: float,
129
+ top_k: int,
130
+ repetition_penalty: float = 1.05,
131
+ length_penalty: float = 1.0,
132
+ all_input_ids: Optional[torch.Tensor] = None,
133
+ ) -> GenerateChunkOutput:
134
+ """
135
+ Args:
136
+ inputs_embeds: [1, seq_len, hidden_dim], Input embeddings of current chunk.
137
+ past_key_values: [num_layers, 2, batch_size, num_heads, seq_len, head_dim], Past key values for llm.
138
+ is_first_generate_chunk: bool, Whether this is the first generate chunk.
139
+ chunk_size: int, The size of the current chunk, default is 10, and it is fixed during training.
140
+ return_hidden_states: bool Whether to return the hidden states, default is True.
141
+ do_sample: bool Whether to sample from the model, default is True.
142
+ temperature: float The temperature for the model, default is 0.7.
143
+ top_p: float The top-p for the model, default is 0.8.
144
+ top_k: int The top-k for the model, default is 100.
145
+ repetition_penalty: float, The repetition penalty for the model, default is 1.05.
146
+ length_penalty: float, The length penalty for the model, default is 1.0. Higher value means more detailed generation.
147
+ all_input_ids: Optional[torch.Tensor], The input ids for the current chunk.
148
+ """
149
+
150
+ finished = False
151
+ current_inputs_embeds = inputs_embeds.clone()
152
+ input_last_hidden_states = []
153
+ last_hidden_states = []
154
+ generated_tokens = []
155
+
156
+ for token_idx in range(chunk_size):
157
+ if is_first_generate_chunk and token_idx == 0:
158
+ # first generate chunk, prefill inputs_embeds
159
+ model_inputs = {
160
+ "inputs_embeds": current_inputs_embeds,
161
+ "past_key_values": past_key_values,
162
+ "use_cache": True,
163
+ "output_hidden_states": return_hidden_states,
164
+ }
165
+ else: # for all other cases: prefill the latest generated token
166
+ model_inputs = {
167
+ "inputs_embeds": current_inputs_embeds[:, -1:, :],
168
+ "past_key_values": past_key_values,
169
+ "use_cache": True,
170
+ "output_hidden_states": return_hidden_states,
171
+ }
172
+
173
+ with torch.no_grad():
174
+ outputs = self.model(**model_inputs)
175
+
176
+ # last token's logits
177
+ logits = outputs.logits[:, -1, :].to(copy=True, dtype=torch.float32, device=inputs_embeds.device)
178
+
179
+ # forbid specific tokens decoding = model.generate@suppress_tokens
180
+ if self.forbidden_token_ids:
181
+ logits[:, self.forbidden_token_ids] = float("-inf")
182
+
183
+ past_key_values = outputs.past_key_values
184
+
185
+ PENALTY_WINDOW_SIZE = 128
186
+
187
+ # apply repetition penalty
188
+ if repetition_penalty != 1.0:
189
+ # get token ids for repetition penalty
190
+ if all_input_ids is not None:
191
+ # use global input ids (including original input and generated part)
192
+ if len(generated_tokens) > 0:
193
+ generated_token_ids = torch.cat(generated_tokens, dim=1)
194
+ current_sequence = torch.cat(
195
+ [
196
+ all_input_ids[:, -PENALTY_WINDOW_SIZE:],
197
+ generated_token_ids,
198
+ ],
199
+ dim=1,
200
+ )
201
+ else:
202
+ current_sequence = all_input_ids[:, -PENALTY_WINDOW_SIZE:]
203
+ unique_token_ids = torch.unique(current_sequence.squeeze(0))
204
+ elif len(generated_tokens) > 0:
205
+ # revert to original logic: only use generated tokens
206
+ generated_token_ids = torch.cat(generated_tokens, dim=1).squeeze(0)
207
+ unique_token_ids = torch.unique(generated_token_ids)
208
+ else:
209
+ unique_token_ids = torch.tensor([], dtype=torch.long, device=logits.device)
210
+
211
+ # apply repetition penalty
212
+ for token_id in unique_token_ids:
213
+ if logits[0, token_id] > 0:
214
+ logits[0, token_id] = logits[0, token_id] / repetition_penalty
215
+ else:
216
+ logits[0, token_id] = logits[0, token_id] * repetition_penalty
217
+
218
+ # apply length penalty, higher value means more detailed generation
219
+ if length_penalty != 1.0:
220
+ for eos_token_id in self.terminators_ids:
221
+ if logits[0, eos_token_id] > 0:
222
+ logits[0, eos_token_id] = logits[0, eos_token_id] / length_penalty
223
+ else:
224
+ logits[0, eos_token_id] = logits[0, eos_token_id] * length_penalty
225
+
226
+ # apply temperature
227
+ if temperature != 1.0:
228
+ logits = logits / temperature
229
+
230
+ if do_sample:
231
+ # Top-k filtering
232
+ if top_k > 0:
233
+ top_k_logits, top_k_indices = torch.topk(logits, min(top_k, logits.size(-1)))
234
+ logits_filtered = torch.full_like(logits, float("-inf"))
235
+ logits_filtered.scatter_(1, top_k_indices, top_k_logits)
236
+ logits = logits_filtered
237
+
238
+ # Top-p filtering
239
+ if top_p < 1.0:
240
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
241
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
242
+
243
+ # remove tokens with cumulative probability greater than top_p
244
+ sorted_indices_to_remove = cumulative_probs > top_p
245
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
246
+ sorted_indices_to_remove[..., 0] = 0
247
+
248
+ indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)
249
+ logits[indices_to_remove] = float("-inf")
250
+
251
+ # sampling
252
+ probs = F.softmax(logits, dim=-1)
253
+ next_token = torch.multinomial(probs, num_samples=1)
254
+ else:
255
+ next_token = torch.argmax(logits, dim=-1, keepdim=True)
256
+
257
+ if return_hidden_states:
258
+ if is_first_generate_chunk and token_idx == 0:
259
+ input_last_hidden_states.append(outputs.hidden_states[-1])
260
+ else:
261
+ last_hidden_states.append(outputs.hidden_states[-1])
262
+
263
+ # if terminator token, stop generating
264
+ if next_token.item() in self.terminators_ids:
265
+ finished = True
266
+ break
267
+
268
+ generated_tokens.append(next_token)
269
+
270
+ # convert new token to embeddings and concatenate
271
+ next_token_embed = self.embedding_layer(next_token)
272
+
273
+ # update inputs_embeds, add one
274
+ current_inputs_embeds = torch.cat([current_inputs_embeds, next_token_embed], dim=1)
275
+
276
+ if len(generated_tokens) > 0:
277
+ chunk_token_ids = torch.cat(generated_tokens, dim=1)
278
+ else:
279
+ # special case: if last chunk and first predict is eos token, return last token of previous chunk. return a tensor with shape (1, 0)
280
+ if finished:
281
+ chunk_token_ids = torch.zeros((1, 0), dtype=torch.long, device=current_inputs_embeds.device)
282
+ else:
283
+ raise Exception("this should not happen")
284
+
285
+ if len(last_hidden_states) > 0:
286
+ last_hidden_states = torch.cat(last_hidden_states, dim=1)
287
+ else:
288
+ # special case: if last chunk, return last token of previous chunk.
289
+ if finished:
290
+ last_hidden_states = torch.cat(last_hidden_states, dim=1)
291
+ else:
292
+ raise Exception("this should not happen")
293
+
294
+ if len(input_last_hidden_states) > 0:
295
+ input_last_hidden_states = torch.cat(input_last_hidden_states, dim=1)
296
+ else:
297
+ input_last_hidden_states = None
298
+
299
+ return GenerateChunkOutput(
300
+ chunk_token_ids=chunk_token_ids,
301
+ current_inputs_embeds=current_inputs_embeds,
302
+ input_last_hidden_states=input_last_hidden_states,
303
+ last_hidden_states=last_hidden_states,
304
+ past_key_values=past_key_values,
305
+ finished=finished,
306
+ )
307
+
308
+
309
+ def streaming_token_decoder(token_iterator, tokenizer, skip_special_tokens=False):
310
+ """
311
+ Incrementally decode tokens from an iterator, handling partial multi-byte characters.
312
+
313
+ When streaming tokens, multi-byte characters (like Chinese) may be split across multiple
314
+ tokens. Decoding partial tokens results in replacement characters (U+FFFD). This function
315
+ buffers tokens and only yields complete characters.
316
+
317
+ Args:
318
+ token_iterator: An iterator yielding (token_ids, is_finished) tuples.
319
+ token_ids can be torch.Tensor or any iterable of integers.
320
+ tokenizer: The tokenizer to use for decoding.
321
+ skip_special_tokens: Whether to skip special tokens during decoding.
322
+
323
+ Yields:
324
+ (decoded_text, is_finished) tuples where decoded_text is the new text since last yield.
325
+ """
326
+ accumulated_token_ids = []
327
+ yielded_text_len = 0
328
+
329
+ for token_ids, is_finished in token_iterator:
330
+ # Accumulate token IDs
331
+ if torch.is_tensor(token_ids):
332
+ accumulated_token_ids.extend(token_ids.reshape(-1).tolist())
333
+ else:
334
+ accumulated_token_ids.extend(list(token_ids) if hasattr(token_ids, "__iter__") else [token_ids])
335
+
336
+ # Decode all accumulated tokens
337
+ full_decoded = tokenizer.decode(accumulated_token_ids, skip_special_tokens=skip_special_tokens)
338
+
339
+ if is_finished:
340
+ # Final chunk - yield all remaining text
341
+ new_text = full_decoded[yielded_text_len:]
342
+ yield new_text, is_finished
343
+ else:
344
+ # Find safe prefix without incomplete multi-byte characters
345
+ # The replacement character '�' (U+FFFD) indicates incomplete decoding
346
+ new_text = full_decoded[yielded_text_len:]
347
+
348
+ # Hold back text ending with replacement character (incomplete UTF-8 sequence)
349
+ safe_end = len(new_text)
350
+ while safe_end > 0 and new_text[safe_end - 1] == "\ufffd":
351
+ safe_end -= 1
352
+
353
+ safe_text = new_text[:safe_end] if safe_end > 0 else ""
354
+ yielded_text_len += len(safe_text)
355
+ yield safe_text, is_finished
356
+
357
+
358
+ def torch_clone_recursive(obj):
359
+ """Recursively clone nested containers of torch.Tensors.
360
+
361
+ Supported container types: dict, list, tuple. Non-container non-Tensor
362
+ objects are returned as-is.
363
+ """
364
+ if torch.is_tensor(obj):
365
+ return obj.clone()
366
+ elif isinstance(obj, dict):
367
+ return {k: torch_clone_recursive(v) for k, v in obj.items()}
368
+ elif isinstance(obj, list):
369
+ return [torch_clone_recursive(v) for v in obj]
370
+ elif isinstance(obj, tuple):
371
+ return tuple(torch_clone_recursive(v) for v in obj)
372
+ else:
373
+ raise ValueError(f"Unsupported type: {type(obj)}")
374
+
375
+
376
+ def rotate_half(x: torch.Tensor) -> torch.Tensor:
377
+ """Rotate half the hidden dims of the input for RoPE."""
378
+ dim = x.shape[-1]
379
+ x1 = x[..., : dim // 2]
380
+ x2 = x[..., dim // 2 :]
381
+ return torch.cat((-x2, x1), dim=-1)
382
+
383
+
384
+ @dataclass
385
+ class SpeculativeSnapshot:
386
+ """Speculative snapshot for VAD speculative rollback.
387
+
388
+ Used in VAD speculative execution: creates a snapshot after streaming_prefill
389
+ and before streaming_generate. If speculation fails (user continues speaking),
390
+ the state can be restored to continue streaming_prefill.
391
+
392
+ Implementation:
393
+ - LLM KV Cache: only record length, restore by truncation (zero extra VRAM)
394
+ - Audio KV Cache: requires cloning, as generate sets it to None
395
+ - Mel processor: save full state snapshot (including buffer)
396
+ """
397
+
398
+ # KV Cache length (for truncation recovery)
399
+ llm_cache_length: int
400
+ audio_cache_length: int
401
+
402
+ # session state
403
+ new_user_msg: bool
404
+ llm_generated: bool
405
+ llm_generate_completed: bool
406
+
407
+ # Round management
408
+ next_round_id: int
409
+ pending_round_id: Optional[int]
410
+ omni_chunk_history_length: int
411
+
412
+ # TTS state (requires cloning, but usually small)
413
+ tts_last_turn_tokens: Optional[torch.Tensor]
414
+
415
+ # Streaming processor state
416
+ audio_chunk_idx: int
417
+
418
+ # Mel processor state snapshot (including buffer)
419
+ mel_processor_snapshot: Optional[dict] = None
420
+
421
+ # Audio encoder KV cache (requires cloning to ensure determinism after recovery)
422
+ audio_past_key_values: Optional[tuple] = None
423
+
424
+ # timestamp (for debugging)
425
+ timestamp: float = 0.0
426
+
427
+ # debug field: for verifying correctness of recovery
428
+ llm_cache_checksum: Optional[float] = None # LLM KV Cache first layer K sum
429
+ audio_cache_checksum: Optional[float] = None # Audio KV Cache first layer K sum
430
+ mel_buffer_checksum: Optional[float] = None # Mel buffer sum
431
+
432
+ # RNG state (key: for ensuring determinism of dithering etc. after recovery)
433
+ rng_state_cpu: Optional[torch.Tensor] = None # torch CPU RNG state
434
+ rng_state_cuda: Optional[torch.Tensor] = None # torch CUDA RNG state (if on GPU)
435
+
436
+ def summary(self) -> str:
437
+ mel_buf_len = 0
438
+ if self.mel_processor_snapshot:
439
+ buf = self.mel_processor_snapshot.get("buffer")
440
+ if buf is not None:
441
+ mel_buf_len = len(buf)
442
+ return (
443
+ f"llm_cache={self.llm_cache_length}, "
444
+ f"audio_cache={self.audio_cache_length}, "
445
+ f"audio_chunk_idx={self.audio_chunk_idx}, "
446
+ f"mel_buffer={mel_buf_len}, "
447
+ f"history_len={self.omni_chunk_history_length}, "
448
+ f"new_user_msg={self.new_user_msg}, "
449
+ f"llm_generated={self.llm_generated}"
450
+ )
451
+
452
+
453
+ # tts
454
+ @dataclass
455
+ class TTSSamplingParams:
456
+ top_p: float = 0.85
457
+ min_p: float = 0.01
458
+ top_k: int = 25
459
+ repetition_penalty: float = 1.05
460
+ temperature: float = 0.8
461
+ win_size: int = 16
462
+ tau_r: float = 0.1
463
+
464
+
465
+ class TTSStreamingGenerator:
466
+ """
467
+ Streaming generator for TTS that processes chunks and yields audio tokens in real-time.
468
+
469
+ Supported attention types:
470
+ - full_attention: Full attention, all tokens can attend to each other
471
+ - sliding_window: Sliding window attention, KV cache is truncated to fixed size (token_window_size)
472
+ - sliding_recompute: Sliding recompute, only keep previous chunk and recompute with current chunk
473
+ - reindex: Keep first chunk as sink, reindex sliding window positions via RoPE rotation
474
+ """
475
+
476
+ def __init__(
477
+ self,
478
+ model,
479
+ temperature: float,
480
+ eos_token: Union[int, torch.Tensor],
481
+ chunk_size: int = 25, # s3tokenizer 1s = 25token
482
+ tts_last_turn_tokens: torch.Tensor = None,
483
+ logits_processors=None,
484
+ logits_warpers=None,
485
+ ):
486
+ self.tts = model
487
+ self.device = model.device
488
+ self.temperature = torch.tensor([temperature], dtype=torch.float, device=self.device)
489
+ self.eos_token = (
490
+ torch.tensor(eos_token, device=self.device) if isinstance(eos_token, int) else eos_token.to(self.device)
491
+ )
492
+
493
+ self.num_vq = model.num_vq
494
+ self.num_audio_tokens = model.num_audio_tokens
495
+ self.recomputed_chunks = model.recomputed_chunks
496
+ self.emb_code = model.emb_code
497
+ self.head_code = model.head_code
498
+
499
+ # Attention type and window sizes
500
+ self.attention_type = model.attention_type # "full_attention", "sliding_window", "sliding_recompute", "reindex"
501
+ self.chunk_window_size = model.chunk_window_size # chunk-level window for sliding_recompute (default 2)
502
+ self.token_window_size = model.token_window_size # token-level window for sliding_window/reindex (default 300)
503
+
504
+ # RoPE config (for reindex mode)
505
+ self.rope_theta = model.model.config.rope_theta
506
+ self.head_dim = model.model.config.hidden_size // model.model.config.num_attention_heads
507
+
508
+ # Logits processors
509
+ self.logits_processors = logits_processors if logits_processors is not None else []
510
+ # Logits warpers (like TopP/TopK), separate from processors
511
+ self.logits_warpers = logits_warpers if logits_warpers is not None else []
512
+
513
+ # initialize state
514
+ self.past_key_values = None
515
+ self.text_start_pos = 0
516
+ self.idx = -1 # start from -1, become 0 when first called
517
+ self.all_conditions = []
518
+ self.all_generated_tokens = []
519
+ self.tts_last_turn_tokens = tts_last_turn_tokens
520
+ self.spk_emb = None
521
+
522
+ audio_bos = [self.tts.audio_bos_token_id]
523
+ audio_bos = torch.Tensor(audio_bos).to(self.tts.emb_text.weight.device, dtype=torch.long)
524
+
525
+ self.audio_bos_embeds = self.tts.emb_text(audio_bos).unsqueeze(0)
526
+ self.text_eos_embed = self.tts.emb_text(
527
+ torch.tensor(
528
+ [self.tts.config.text_eos_token_id],
529
+ device=self.tts.emb_text.weight.device,
530
+ dtype=torch.long,
531
+ )
532
+ ).unsqueeze(0)
533
+
534
+ # buffer related, used to fill up chunk_size and yield to outside
535
+ self.chunk_size = chunk_size
536
+ self._token_buffer: List[torch.Tensor] = []
537
+
538
+ # Chunk info tracking for sliding_recompute and reindex
539
+ self._chunk_info: List[dict] = []
540
+ self._total_seq_len = 0
541
+
542
+ # Reindex mode: track sink (first chunk) length
543
+ self._sink_kv_len = 0
544
+
545
+ def _build_recompute_inputs(self, current_condition: torch.Tensor) -> torch.Tensor:
546
+ """Build recompute inputs for sliding_recompute mode."""
547
+ if len(self._chunk_info) == 0:
548
+ return current_condition
549
+
550
+ prev_chunk = self._chunk_info[-1]
551
+ prev_condition = prev_chunk["condition"]
552
+ prev_audio_tokens = prev_chunk["audio_tokens"]
553
+
554
+ recompute_list = [prev_condition]
555
+ if len(prev_audio_tokens) > 0:
556
+ prev_audio_embeds = torch.cat([self.emb_code[0](tok) for tok in prev_audio_tokens], dim=1)
557
+ recompute_list.append(prev_audio_embeds)
558
+
559
+ recompute_list.append(current_condition)
560
+ return torch.cat(recompute_list, dim=1)
561
+
562
+ def _truncate_kv_cache_sliding_window(self):
563
+ """Truncate KV cache for sliding_window mode."""
564
+ if self.past_key_values is None:
565
+ return
566
+
567
+ if hasattr(self.past_key_values, "get_seq_length"):
568
+ current_kv_len = self.past_key_values.get_seq_length()
569
+ else:
570
+ current_kv_len = self.past_key_values[0][0].shape[2]
571
+
572
+ if current_kv_len <= self.token_window_size:
573
+ return
574
+
575
+ new_cache = DynamicCache()
576
+ num_layers = (
577
+ len(self.past_key_values.key_cache)
578
+ if hasattr(self.past_key_values, "key_cache")
579
+ else len(self.past_key_values)
580
+ )
581
+
582
+ for layer_idx in range(num_layers):
583
+ if hasattr(self.past_key_values, "key_cache"):
584
+ key = self.past_key_values.key_cache[layer_idx][:, :, -self.token_window_size :, :]
585
+ value = self.past_key_values.value_cache[layer_idx][:, :, -self.token_window_size :, :]
586
+ else:
587
+ key = self.past_key_values[layer_idx][0][:, :, -self.token_window_size :, :]
588
+ value = self.past_key_values[layer_idx][1][:, :, -self.token_window_size :, :]
589
+ new_cache.update(key, value, layer_idx)
590
+
591
+ self.past_key_values = new_cache
592
+
593
+ @staticmethod
594
+ def _apply_rope_rotation(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
595
+ """Apply RoPE rotation to tensor."""
596
+ return x * cos + rotate_half(x) * sin
597
+
598
+ def _compute_rope_cos_sin(self, positions: torch.Tensor, device: torch.device, dtype: torch.dtype):
599
+ """Compute RoPE cos and sin for given positions."""
600
+ dim_half = self.head_dim // 2
601
+ freq_seq = torch.arange(0, dim_half, dtype=torch.float32, device=device)
602
+ inv_freq = 1.0 / (self.rope_theta ** (freq_seq / dim_half))
603
+
604
+ # positions: [seq_len]
605
+ angles = positions.float().unsqueeze(-1) * inv_freq.unsqueeze(0) # [seq_len, dim_half]
606
+ angles = torch.cat([angles, angles], dim=-1) # [seq_len, head_dim]
607
+
608
+ cos = angles.cos().to(dtype)
609
+ sin = angles.sin().to(dtype)
610
+ return cos, sin
611
+
612
+ def _reindex_kv_cache(self):
613
+ """
614
+ Reindex KV cache for reindex mode:
615
+ 1. Keep first chunk as attention sink
616
+ 2. Keep last chunk
617
+ 3. Discard middle chunks
618
+ 4. Reindex the last chunk's key positions to be right after sink via RoPE rotation
619
+ """
620
+ if self.past_key_values is None or len(self._chunk_info) < 2:
621
+ return
622
+
623
+ # Get current KV cache length
624
+ if hasattr(self.past_key_values, "get_seq_length"):
625
+ current_kv_len = self.past_key_values.get_seq_length()
626
+ else:
627
+ current_kv_len = self.past_key_values[0][0].shape[2]
628
+
629
+ # Calculate sink length (first chunk)
630
+ sink_len = self._chunk_info[0]["condition_len"] + self._chunk_info[0]["audio_token_count"]
631
+
632
+ # Last chunk length
633
+ last_chunk = self._chunk_info[-1]
634
+ last_chunk_len = last_chunk["condition_len"] + last_chunk["audio_token_count"]
635
+
636
+ keep_len = sink_len + last_chunk_len
637
+
638
+ # Get device and dtype
639
+ device = self.past_key_values.key_cache[0].device
640
+ dtype = self.past_key_values.key_cache[0].dtype
641
+
642
+ if current_kv_len <= keep_len:
643
+ last_chunk_kv_len = current_kv_len - sink_len
644
+ if last_chunk_kv_len <= 0:
645
+ return
646
+ self.text_start_pos = current_kv_len
647
+ return
648
+
649
+ # Step 1: Truncate KV cache - keep sink and last chunk
650
+ new_cache = DynamicCache()
651
+ num_layers = len(self.past_key_values.key_cache)
652
+
653
+ original_start_pos = current_kv_len - last_chunk_len
654
+ new_start_pos = sink_len
655
+ delta = new_start_pos - original_start_pos # This is a scalar constant
656
+ delta_positions = torch.full((last_chunk_len,), delta, dtype=torch.float32, device=device)
657
+
658
+ # Compute rotation cos/sin
659
+ cos, sin = self._compute_rope_cos_sin(delta_positions, device, dtype)
660
+ cos = cos.unsqueeze(0).unsqueeze(0) # [1, 1, seq_len, head_dim]
661
+ sin = sin.unsqueeze(0).unsqueeze(0)
662
+
663
+ for layer_idx in range(num_layers):
664
+ key_full = self.past_key_values.key_cache[layer_idx]
665
+ value_full = self.past_key_values.value_cache[layer_idx]
666
+
667
+ # Extract sink and last chunk
668
+ key_sink = key_full[:, :, :sink_len, :]
669
+ value_sink = value_full[:, :, :sink_len, :]
670
+ key_last = key_full[:, :, -last_chunk_len:, :]
671
+ value_last = value_full[:, :, -last_chunk_len:, :]
672
+
673
+ # Apply RoPE rotation to reindex key positions
674
+ key_last_reindexed = self._apply_rope_rotation(key_last, cos, sin)
675
+
676
+ # Concatenate sink and reindexed last chunk
677
+ key = torch.cat([key_sink, key_last_reindexed], dim=2)
678
+ value = torch.cat([value_sink, value_last], dim=2)
679
+
680
+ new_cache.update(key, value, layer_idx)
681
+
682
+ self.past_key_values = new_cache
683
+
684
+ # Update text_start_pos to reflect new positions
685
+ self.text_start_pos = sink_len + last_chunk_len
686
+
687
+ @torch.inference_mode()
688
+ def generate_with_buffer(
689
+ self,
690
+ condition: torch.Tensor,
691
+ text_finished: bool = False,
692
+ max_new_token: int = 500,
693
+ ):
694
+ """input a condition embedding chunk, generate audio token each time,
695
+ and accumulate to buffer, only yield when buffer satisfies chunk_size.
696
+
697
+ Yields:
698
+ torch.Tensor of shape [chunk_size] (2D: [1, chunk_size])
699
+ """
700
+ self.idx += 1
701
+ self.device = self.tts.device
702
+
703
+ # if text finished, first concatenate Text EOS
704
+ if text_finished:
705
+ condition = torch.cat([condition, self.text_eos_embed], dim=1)
706
+
707
+ # always concatenate Audio BOS
708
+ condition = torch.cat([condition, self.audio_bos_embeds], dim=1).to(self.device)
709
+
710
+ self.all_conditions.append(condition)
711
+
712
+ # Initialize current chunk info
713
+ current_chunk_info = {
714
+ "condition_len": condition.shape[1],
715
+ "audio_token_count": 0,
716
+ "condition": condition.clone(),
717
+ "audio_tokens": [],
718
+ }
719
+
720
+ # Handle different attention types
721
+ if self.attention_type == "sliding_recompute" and self.idx >= 1:
722
+ # sliding_recompute: discard KV cache, recompute with previous + current chunk
723
+ self.past_key_values = None
724
+ current_condition = self._build_recompute_inputs(condition)
725
+ self.text_start_pos = 0
726
+ elif self.attention_type == "reindex" and self.idx >= 1:
727
+ # reindex: truncate KV cache keeping sink + last chunk, reindex positions via RoPE
728
+ self._reindex_kv_cache()
729
+ current_condition = condition
730
+ # Always update text_start_pos based on actual KV cache length (like reference code)
731
+ if self.past_key_values is not None:
732
+ if hasattr(self.past_key_values, "get_seq_length"):
733
+ kv_len = self.past_key_values.get_seq_length()
734
+ else:
735
+ kv_len = self.past_key_values[0][0].shape[2]
736
+ self.text_start_pos = kv_len
737
+ else:
738
+ current_condition = condition
739
+
740
+ condition_length = current_condition.shape[1]
741
+ prefill_len = condition_length
742
+ finished = torch.zeros(1, dtype=torch.bool, device=self.device)
743
+ chunk_generated_tokens = []
744
+
745
+ for t in range(max_new_token):
746
+ if t == 0:
747
+ inputs_embeds = current_condition
748
+ pos_ids = torch.arange(
749
+ self.text_start_pos,
750
+ self.text_start_pos + condition_length,
751
+ dtype=torch.long,
752
+ device=self.device,
753
+ ).unsqueeze(0)
754
+ else:
755
+ last = self.all_generated_tokens[-1]
756
+ # last: [1,1], directly as code id
757
+ inputs_embeds = self.emb_code[0](last)
758
+ pos_ids = torch.tensor(
759
+ [self.text_start_pos + prefill_len + t - 1],
760
+ dtype=torch.long,
761
+ device=self.device,
762
+ ).unsqueeze(0)
763
+
764
+ outputs = self.tts.model(
765
+ position_ids=pos_ids,
766
+ past_key_values=self.past_key_values,
767
+ inputs_embeds=inputs_embeds,
768
+ use_cache=True,
769
+ )
770
+ hidden_states = outputs.last_hidden_state
771
+
772
+ # Handle KV cache based on attention type
773
+ if self.attention_type == "sliding_window":
774
+ self.past_key_values = outputs.past_key_values
775
+ self._truncate_kv_cache_sliding_window()
776
+ else:
777
+ self.past_key_values = outputs.past_key_values
778
+
779
+ with P.cached():
780
+ logits = torch.empty(
781
+ hidden_states.size(0),
782
+ hidden_states.size(1),
783
+ self.num_audio_tokens,
784
+ self.num_vq,
785
+ dtype=torch.float,
786
+ device=self.device,
787
+ )
788
+ for num_vq_iter in range(self.num_vq):
789
+ x: torch.Tensor = self.head_code[num_vq_iter](hidden_states)
790
+ logits[..., num_vq_iter] = x
791
+ del x
792
+
793
+ del hidden_states
794
+
795
+ logits = logits[:, -1].float()
796
+
797
+ logits = logits.permute(0, 2, 1)
798
+ logits = logits.reshape(-1, logits.size(2))
799
+
800
+ logits /= self.temperature
801
+
802
+ audio_bos = len(self.all_generated_tokens) == 0 and t == 0
803
+
804
+ if not audio_bos:
805
+ # use generated tokens (current chunk) as input for processor/warper (align with modeling_minicpmo)
806
+ all_generated_tokens = torch.cat(self.all_generated_tokens, dim=1).to(self.device) # [1, T]
807
+ for processor in self.logits_processors:
808
+ logits = processor(all_generated_tokens, logits)
809
+
810
+ for warper in self.logits_warpers:
811
+ logits = warper(all_generated_tokens, logits)
812
+ del all_generated_tokens
813
+
814
+ # sample next token (only use first codebook, same as generate)
815
+ scores = F.softmax(logits, dim=-1)
816
+ idx_next = torch.multinomial(scores, num_samples=1) # [(B*num_vq), 1]
817
+ next_id = idx_next.view(-1, self.num_vq)[:, 0:1] # only take first codebook → [B, 1]
818
+ del scores
819
+
820
+ if next_id.eq(
821
+ self.eos_token
822
+ ).any(): # generated audio eos token, means this chunk is finished, no longer generate new tokens
823
+ finished[:] = True
824
+ else: # eos token cannot be added to buffer, he does not speak.
825
+ # convert next_id to correct shape [1, 1], no num_vq dimension
826
+ if next_id.dim() == 0: # if scalar
827
+ next_tok = next_id.unsqueeze(0).unsqueeze(0) # [1, 1]
828
+ elif next_id.dim() == 1: # if 1D [1]
829
+ next_tok = next_id.unsqueeze(0) # [1, 1]
830
+ else:
831
+ next_tok = next_id
832
+
833
+ self.all_generated_tokens.append(next_tok)
834
+ chunk_generated_tokens.append(next_tok)
835
+
836
+ # Update chunk info for sliding_recompute
837
+ current_chunk_info["audio_tokens"].append(next_tok.clone())
838
+ current_chunk_info["audio_token_count"] += 1
839
+
840
+ self._token_buffer.append(next_tok)
841
+
842
+ if len(self._token_buffer) == 0:
843
+ # case 1: if last text chunk, yield None
844
+ if text_finished:
845
+ yield torch.empty(1, 0, dtype=torch.long, device=self.device), True
846
+ break
847
+ # case 2: if not last text chunk, break directly
848
+ else:
849
+ break
850
+ else: # buffer has something
851
+ # case 1: if buffer is larger/equal to chunk_size, yield out
852
+ if len(self._token_buffer) >= self.chunk_size:
853
+ batch = torch.cat(self._token_buffer[: self.chunk_size], dim=1) # [1, chunk_size]
854
+ yield batch, False # → [1, chunk_size]
855
+ # discard yielded part
856
+ self._token_buffer = self._token_buffer[self.chunk_size :]
857
+
858
+ # case 2: if buffer is smaller than chunk_size
859
+ else:
860
+ # if generation finished, and is the last text chunk, yield all remaining tokens, then break
861
+ if finished.all():
862
+ if text_finished:
863
+ batch = torch.cat(self._token_buffer, dim=1) # [1, chunk_size]
864
+ yield batch, True # → [1, chunk_size]
865
+ self._token_buffer = []
866
+ break
867
+ else:
868
+ # not the last text chunk, need to wait for next text chunk to fill up buffer, then this call ends
869
+ break
870
+ else: # generation of this audio chunk is not finished, continue generating
871
+ continue
872
+
873
+ # Save current chunk info for sliding_recompute and reindex
874
+ self._chunk_info.append(current_chunk_info)
875
+ self._total_seq_len += condition.shape[1] + len(chunk_generated_tokens)
876
+
877
+ # Update text_start_pos based on attention type
878
+ if self.attention_type == "sliding_recompute":
879
+ # sliding_recompute: will be reset at next chunk start, update normally here
880
+ self.text_start_pos += prefill_len + len(chunk_generated_tokens)
881
+ elif self.attention_type == "reindex":
882
+ # reindex: position based on actual KV cache length (positions have been reindexed to be continuous)
883
+ if self.past_key_values is not None:
884
+ if hasattr(self.past_key_values, "get_seq_length"):
885
+ self.text_start_pos = self.past_key_values.get_seq_length()
886
+ else:
887
+ self.text_start_pos = self.past_key_values[0][0].shape[2]
888
+ else:
889
+ self.text_start_pos += condition.shape[1] + len(chunk_generated_tokens)
890
+ else:
891
+ self.text_start_pos += condition.shape[1] + len(chunk_generated_tokens)
892
+ # note: remaining tokens in buffer will be kept, and accumulated next time
893
+
894
+
895
+ # sliding window
896
+ @dataclass
897
+ class StreamingWindowConfig:
898
+ text_window_high_tokens: int = 8000
899
+ text_window_low_tokens: int = 6000
900
+
901
+
902
+ @dataclass
903
+ class DuplexWindowConfig:
904
+ """duplex sliding window configuration
905
+
906
+ sliding window mode:
907
+ - "off": disable sliding window
908
+ - "basic": basic sliding window (trigger by cache length)
909
+ - "context": sliding window with context (trigger by unit number, preserve generated text to previous)
910
+ """
911
+
912
+ # sliding window mode
913
+ sliding_window_mode: str = "off" # "off" / "basic" / "context"
914
+
915
+ # basic sliding window parameters
916
+ basic_window_high_tokens: int = 8000 # high watermark: trigger sliding window when exceeded
917
+ basic_window_low_tokens: int = 6000 # low watermark: keep to this value after sliding window
918
+
919
+ # context sliding window parameters
920
+ context_previous_max_tokens: int = 500 # previous maximum token number
921
+ context_max_units: int = 24 # maximum unit number (trigger sliding window when exceeded)
922
+
923
+ # verification mode (for comparison test)
924
+ verify_mode: bool = False # whether to enable verification log
925
+
926
+
927
+ def as_dynamic_cache(past_key_values):
928
+ """Convert legacy tuple cache to DynamicCache if needed."""
929
+ if isinstance(past_key_values, DynamicCache):
930
+ return past_key_values
931
+
932
+ if isinstance(past_key_values, tuple):
933
+ return DynamicCache.from_legacy_cache(past_key_values)
934
+
935
+ return past_key_values
936
+
937
+
938
+ def get_kv_cache_length(cache) -> int:
939
+ """Get the sequence length of a KV cache.
940
+
941
+ Args:
942
+ cache: DynamicCache or tuple-based cache
943
+
944
+ Returns:
945
+ The number of tokens in the cache
946
+ """
947
+ if cache is None:
948
+ return 0
949
+
950
+ if isinstance(cache, DynamicCache):
951
+ if not cache.key_cache or not cache.key_cache[0].numel():
952
+ return 0
953
+ return cache.key_cache[0].shape[-2]
954
+
955
+ if isinstance(cache, tuple):
956
+ return cache[0][0].shape[2]
957
+
958
+ return 0
959
+
960
+
961
+ def get_rotary_cos_sin(
962
+ head_dim: int,
963
+ positions: torch.Tensor,
964
+ device: torch.device,
965
+ dtype: torch.dtype,
966
+ rope_theta: float = 10000.0,
967
+ inv_freq_cache: Optional[Dict[Tuple, torch.Tensor]] = None,
968
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
969
+ """Compute RoPE cos and sin components for given positions.
970
+
971
+ Args:
972
+ head_dim: Dimension of each attention head
973
+ positions: Position indices tensor
974
+ device: Target device
975
+ dtype: Target dtype
976
+ rope_theta: RoPE base frequency (default 10000.0)
977
+ inv_freq_cache: Optional cache dict for inverse frequencies
978
+
979
+ Returns:
980
+ Tuple of (cos, sin) tensors with shape [1, 1, seq_len, head_dim]
981
+ """
982
+ cache_key = (head_dim, device)
983
+
984
+ inv_freq = inv_freq_cache.get(cache_key) if inv_freq_cache is not None else None
985
+ if inv_freq is None or inv_freq.device != device or inv_freq.shape[0] != head_dim // 2:
986
+ exponent = torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim
987
+ inv_freq = 1.0 / (rope_theta**exponent)
988
+ if inv_freq_cache is not None:
989
+ inv_freq_cache[cache_key] = inv_freq
990
+
991
+ positions = positions.to(device=device, dtype=torch.float32)
992
+ angles = torch.einsum("i,j->ij", positions, inv_freq)
993
+ cos = torch.cos(angles)
994
+ sin = torch.sin(angles)
995
+
996
+ # Use cat instead of repeat_interleave, consistent with model's original RotaryEmbedding
997
+ # Original: emb = torch.cat((freqs, freqs), dim=-1) -> [f0, f1, ..., f_{d/2}, f0, f1, ..., f_{d/2}]
998
+ cos_full = torch.cat([cos, cos], dim=-1).to(dtype=dtype)
999
+ sin_full = torch.cat([sin, sin], dim=-1).to(dtype=dtype)
1000
+ cos_full = cos_full.unsqueeze(0).unsqueeze(0)
1001
+ sin_full = sin_full.unsqueeze(0).unsqueeze(0)
1002
+ return cos_full, sin_full
1003
+
1004
+
1005
+ def realign_rotary_suffix(
1006
+ suffix_keys: torch.Tensor,
1007
+ old_positions: torch.Tensor,
1008
+ new_positions: torch.Tensor,
1009
+ rope_theta: float = 10000.0,
1010
+ inv_freq_cache: Optional[Dict[Tuple, torch.Tensor]] = None,
1011
+ ) -> torch.Tensor:
1012
+ """Realign RoPE position encoding after cache eviction.
1013
+
1014
+ When tokens are dropped from the middle of a cache, the suffix tokens
1015
+ need their RoPE embeddings recalculated with new position indices.
1016
+
1017
+ Args:
1018
+ suffix_keys: Key tensor to realign, shape [batch, heads, seq_len, head_dim]
1019
+ old_positions: Original position indices
1020
+ new_positions: New position indices after eviction
1021
+ rope_theta: RoPE base frequency
1022
+ inv_freq_cache: Optional cache dict for inverse frequencies
1023
+
1024
+ Returns:
1025
+ Realigned key tensor with same shape as input
1026
+ """
1027
+ if suffix_keys.numel() == 0:
1028
+ return suffix_keys
1029
+
1030
+ head_dim = suffix_keys.shape[-1]
1031
+ device = suffix_keys.device
1032
+ dtype = suffix_keys.dtype
1033
+
1034
+ # Compute old position cos/sin
1035
+ cos_old, sin_old = get_rotary_cos_sin(head_dim, old_positions, device, dtype, rope_theta, inv_freq_cache)
1036
+
1037
+ # Inverse transform: recover original key
1038
+ base = cos_old * suffix_keys - sin_old * rotate_half(suffix_keys)
1039
+
1040
+ # Compute new position cos/sin
1041
+ cos_new, sin_new = get_rotary_cos_sin(head_dim, new_positions, device, dtype, rope_theta, inv_freq_cache)
1042
+
1043
+ # Forward transform: re-encode with new positions
1044
+ return cos_new * base + sin_new * rotate_half(base)
1045
+
1046
+
1047
+ def drop_tokens_from_cache(
1048
+ cache: Optional[DynamicCache | Tuple],
1049
+ length: int,
1050
+ preserve: int,
1051
+ position_offset: int,
1052
+ rope_theta: float = 10000.0,
1053
+ inv_freq_cache: Optional[Dict[Tuple, torch.Tensor]] = None,
1054
+ ) -> Tuple[Optional[DynamicCache], int, bool]:
1055
+ """Drop tokens from a KV cache while preserving system prompt.
1056
+
1057
+ Removes tokens in the range [preserve, preserve + length) from the cache,
1058
+ realigning RoPE embeddings for the suffix.
1059
+
1060
+ Args:
1061
+ cache: DynamicCache or tuple-based cache (will be converted to DynamicCache)
1062
+ length: Number of tokens to drop
1063
+ preserve: Number of tokens to preserve at the start (system prompt)
1064
+ position_offset: Current position offset for RoPE calculation
1065
+ rope_theta: RoPE base frequency
1066
+ inv_freq_cache: Optional cache dict for inverse frequencies
1067
+
1068
+ Returns:
1069
+ Tuple of (cache, new_position_offset, success)
1070
+ Note: Tuple cache will be converted to DynamicCache. Modification is in-place.
1071
+ """
1072
+ if cache is None or length <= 0:
1073
+ return cache, position_offset, False
1074
+
1075
+ cache = as_dynamic_cache(cache)
1076
+
1077
+ total_len = get_kv_cache_length(cache)
1078
+ if total_len <= 0:
1079
+ return cache, position_offset, False
1080
+
1081
+ preserve = min(preserve, total_len)
1082
+ available = total_len - preserve
1083
+
1084
+ if available < length:
1085
+ logger.warning(
1086
+ "Cannot drop %d tokens: only %d available (total=%d, preserve=%d)",
1087
+ length,
1088
+ available,
1089
+ total_len,
1090
+ preserve,
1091
+ )
1092
+ return cache, position_offset, False
1093
+
1094
+ suffix_len = total_len - preserve - length
1095
+ # note: after RoPE reindex, the position of cache has been compressed (from preserve start)
1096
+ # so here should not add position_offset, but use the actual layout of current cache
1097
+ suffix_offset = preserve + length # suffix current position in cache
1098
+ prefix_offset = preserve # suffix new position (follow preserve)
1099
+
1100
+ # Prepare position tensors for RoPE realignment
1101
+ old_positions = None
1102
+ new_positions = None
1103
+ if suffix_len > 0:
1104
+ device = cache.key_cache[0].device
1105
+ old_positions = torch.arange(
1106
+ suffix_offset,
1107
+ suffix_offset + suffix_len,
1108
+ device=device,
1109
+ dtype=torch.long,
1110
+ )
1111
+ new_positions = torch.arange(
1112
+ prefix_offset,
1113
+ prefix_offset + suffix_len,
1114
+ device=device,
1115
+ dtype=torch.long,
1116
+ )
1117
+
1118
+ keep_len = total_len - length
1119
+
1120
+ # Process each layer (in-place modification)
1121
+ for layer_idx in range(len(cache.key_cache)):
1122
+ key_tensor = cache.key_cache[layer_idx]
1123
+ value_tensor = cache.value_cache[layer_idx]
1124
+
1125
+ if not key_tensor.numel():
1126
+ continue
1127
+
1128
+ # Preserve prefix (system prompt)
1129
+ prefix_keys = key_tensor[:, :, :preserve, :]
1130
+ prefix_values = value_tensor[:, :, :preserve, :]
1131
+
1132
+ if suffix_len > 0:
1133
+ # Keep and realign suffix
1134
+ suffix_keys = key_tensor[:, :, preserve + length :, :]
1135
+ suffix_values = value_tensor[:, :, preserve + length :, :]
1136
+
1137
+ if old_positions is not None and new_positions is not None and suffix_keys.numel():
1138
+ suffix_keys = realign_rotary_suffix(
1139
+ suffix_keys,
1140
+ old_positions,
1141
+ new_positions,
1142
+ rope_theta,
1143
+ inv_freq_cache,
1144
+ )
1145
+
1146
+ cache.key_cache[layer_idx] = torch.cat([prefix_keys, suffix_keys], dim=-2).contiguous()
1147
+ cache.value_cache[layer_idx] = torch.cat([prefix_values, suffix_values], dim=-2).contiguous()
1148
+ else:
1149
+ cache.key_cache[layer_idx] = prefix_keys.contiguous()
1150
+ cache.value_cache[layer_idx] = prefix_values.contiguous()
1151
+
1152
+ cache.crop(keep_len)
1153
+ cache._seen_tokens = max(keep_len, 0)
1154
+
1155
+ new_offset = position_offset + length
1156
+ logger.debug("Dropped %d tokens from cache, new length=%d", length, keep_len)
1157
+
1158
+ return cache, new_offset, True
1159
+
1160
+
1161
+ # stream decoder
1162
+ def top_k_top_p_filtering(logits, top_k=0, top_p=0.0, filter_value=-float("inf")):
1163
+ logits = logits.clone()
1164
+
1165
+ # Top-k filtering
1166
+ if top_k > 0:
1167
+ top_k = min(top_k, logits.size(-1))
1168
+ indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
1169
+ logits[indices_to_remove] = filter_value
1170
+
1171
+ # Top-p (nucleus) filtering
1172
+ if top_p > 0.0:
1173
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
1174
+ probs = F.softmax(sorted_logits, dim=-1)
1175
+ cumulative_probs = torch.cumsum(probs, dim=-1)
1176
+
1177
+ sorted_indices_to_remove = cumulative_probs > top_p
1178
+ # keep the first token that exceeds top_p
1179
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
1180
+ sorted_indices_to_remove[..., 0] = 0
1181
+
1182
+ indices_to_remove = sorted_indices[sorted_indices_to_remove]
1183
+ logits[0, indices_to_remove] = filter_value
1184
+
1185
+ return logits
1186
+
1187
+
1188
+ class StreamDecoder:
1189
+ def __init__(self, llm, tokenizer, special_token_ids=None, forbidden_token_ids=None):
1190
+ self.m = llm
1191
+ self.tokenizer = tokenizer
1192
+ self.listen_id = self.tokenizer.eos_token_id
1193
+
1194
+ self.chunk_eos_id = self.tokenizer.convert_tokens_to_ids("<|chunk_eos|>")
1195
+ self.chunk_tts_eos_id = self.tokenizer.convert_tokens_to_ids("<|chunk_tts_eos|>")
1196
+ self.turn_eos_id = self.tokenizer.convert_tokens_to_ids("<|turn_eos|>")
1197
+ self.speak_id = self.tokenizer.convert_tokens_to_ids("<|speak|>")
1198
+
1199
+ self.special_token_ids = special_token_ids if special_token_ids is not None else []
1200
+
1201
+ # cache special tokens (used for context sliding window filtering)
1202
+ self._all_special_ids = set()
1203
+ self._all_special_tokens_text = set()
1204
+ if self.tokenizer:
1205
+ if hasattr(self.tokenizer, "all_special_ids"):
1206
+ self._all_special_ids = set(self.tokenizer.all_special_ids)
1207
+ if hasattr(self.tokenizer, "all_special_tokens"):
1208
+ self._all_special_tokens_text = set(self.tokenizer.all_special_tokens)
1209
+
1210
+ custom_special_tokens = [
1211
+ "<unit>",
1212
+ "</unit>",
1213
+ "<image>",
1214
+ "</image>",
1215
+ "<slice>",
1216
+ "</slice>",
1217
+ "<|listen|>",
1218
+ "<|speak|>",
1219
+ "<|tts_bos|>",
1220
+ "<|tts_eos|>",
1221
+ "<|audio_start|>",
1222
+ "<|audio_end|>",
1223
+ "<|chunk_eos|>",
1224
+ "<|chunk_tts_eos|>",
1225
+ "<|turn_eos|>",
1226
+ "<|audio_start|>",
1227
+ "<|audio_end|>",
1228
+ ]
1229
+ self._all_special_tokens_text.update(custom_special_tokens)
1230
+ for token in custom_special_tokens:
1231
+ token_id = self.tokenizer.convert_tokens_to_ids(token)
1232
+ if token_id is not None and token_id != self.tokenizer.unk_token_id:
1233
+ self._all_special_ids.add(token_id)
1234
+
1235
+ if forbidden_token_ids is None:
1236
+ self.forbidden_token_ids = []
1237
+ elif isinstance(forbidden_token_ids, int):
1238
+ self.forbidden_token_ids = [self.forbidden_token_ids]
1239
+ else:
1240
+ self.forbidden_token_ids = forbidden_token_ids
1241
+ self.forbidden_token_ids.append(self.chunk_eos_id)
1242
+
1243
+ assert isinstance(self.forbidden_token_ids, list)
1244
+
1245
+ self.cache = None
1246
+ self.context = ""
1247
+ self.generated_tokens = [] # track generated tokens
1248
+ self.generated_special_tokens = [] # track generated special tokens
1249
+ self.reset()
1250
+ self.embeds = None
1251
+ self.system_embeds = None
1252
+
1253
+ # sliding window related states
1254
+ self._unit_history: List[Dict[str, Any]] = []
1255
+ self._next_unit_id: int = 0
1256
+ self._pending_unit_id: Optional[int] = None
1257
+ self._pending_unit_start_cache_len: int = 0
1258
+ self._system_preserve_length: int = 0
1259
+ self._position_offset: int = 0
1260
+ self._window_config = DuplexWindowConfig()
1261
+ self._window_enabled: bool = True
1262
+ self._rope_inv_freq_cache: Dict[Tuple, torch.Tensor] = {}
1263
+
1264
+ # context preserving sliding window states
1265
+ # initial cache layout: [prefix] [suffix] [units...]
1266
+ # after first sliding window: [prefix] [previous_marker + content] [suffix] [units...]
1267
+ # fixed dynamic sliding region fixed
1268
+ self._preserve_prefix_length: int = 0 # original prefix length (fixed)
1269
+ self._previous_content_length: int = 0 # previous content length (dynamic, including marker)
1270
+ self._suffix_token_ids: List[int] = [] # suffix token ids (e.g. <|im_end|>)
1271
+
1272
+ # previous marker (added dynamically after first sliding window)
1273
+ self._previous_marker: str = "\n\nprevious: " # fixed prefix marker
1274
+ self._previous_marker_token_ids: List[int] = [] # marker token ids (initialized)
1275
+ self._has_previous: bool = False # whether previous marker has been added
1276
+
1277
+ # previous content
1278
+ self._previous_text: str = "" # accumulated generated text (without marker)
1279
+ self._previous_token_ids: List[int] = [] # previous full token ids (including marker)
1280
+
1281
+ # validation statistics
1282
+ self._sliding_event_count: int = 0 # sliding window trigger count
1283
+ self._total_dropped_tokens: int = 0 # total dropped token count
1284
+ self._total_dropped_units: int = 0 # total dropped unit count
1285
+
1286
+ def sliding_embeds(self):
1287
+ # tmp = system_embeds
1288
+ # tmp +-》 embeds after 5s
1289
+ # reset
1290
+ # feed
1291
+ pass
1292
+
1293
+ def reset(self):
1294
+ self.context = ""
1295
+ self.cache = None
1296
+ self.generated_tokens = []
1297
+ self.generated_special_tokens = []
1298
+ self.embeds = None
1299
+ self.system_embeds = None
1300
+
1301
+ # sliding window state reset
1302
+ old_unit_count = len(self._unit_history) if hasattr(self, "_unit_history") else 0
1303
+ self._unit_history = []
1304
+ self._next_unit_id = 0
1305
+ self._pending_unit_id = None
1306
+ self._pending_unit_start_cache_len = 0
1307
+ self._system_preserve_length = 0
1308
+ self._position_offset = 0
1309
+ self._rope_inv_freq_cache = {}
1310
+
1311
+ # context preserving sliding window state reset
1312
+ self._preserve_prefix_length = 0
1313
+ self._previous_content_length = 0
1314
+ self._suffix_token_ids = []
1315
+ self._previous_marker = "\n\nprevious: "
1316
+ self._previous_marker_token_ids = []
1317
+ self._has_previous = False
1318
+ self._previous_text = ""
1319
+ self._previous_token_ids = []
1320
+
1321
+ # validation statistics
1322
+ self._sliding_event_count = 0 # sliding window trigger count
1323
+ self._total_dropped_tokens = 0 # total dropped token count
1324
+ self._total_dropped_units = 0 # total dropped unit count
1325
+
1326
+ def get_cache_length(self) -> int:
1327
+ if self.cache is None:
1328
+ return 0
1329
+ if isinstance(self.cache, DynamicCache):
1330
+ if len(self.cache.key_cache) > 0 and self.cache.key_cache[0].numel() > 0:
1331
+ return self.cache.key_cache[0].shape[2]
1332
+ return 0
1333
+ # Tuple cache format
1334
+ return self.cache[0][0].shape[2]
1335
+
1336
+ def get_total_generated_tokens(self) -> int:
1337
+ return sum(len(u.get("generated_tokens", [])) for u in self._unit_history)
1338
+
1339
+ def register_unit_start(self) -> int:
1340
+ self._pending_unit_id = self._next_unit_id
1341
+ self._pending_unit_start_cache_len = self.get_cache_length()
1342
+ return self._pending_unit_id
1343
+
1344
+ def register_unit_end(
1345
+ self,
1346
+ input_type: str,
1347
+ generated_tokens: Optional[List[int]] = None,
1348
+ is_listen: bool = False,
1349
+ generated_text: Optional[str] = None,
1350
+ ):
1351
+ """Call when unit ends, record unit information
1352
+
1353
+ Should be called after feeding </unit> token
1354
+
1355
+ Args:
1356
+ input_type: "audio" / "video" / "omni" / "system"
1357
+ generated_tokens: tokens generated by the unit (token ids)
1358
+ is_listen: whether the unit is in listen state
1359
+ generated_text: text generated by the unit (used for context preserving mode)
1360
+ """
1361
+ if self._pending_unit_id is None:
1362
+ logger.warning("register_unit_end called without register_unit_start")
1363
+ return
1364
+
1365
+ # calculate the length of the unit
1366
+ current_cache_len = self.get_cache_length()
1367
+ unit_len = current_cache_len - self._pending_unit_start_cache_len
1368
+
1369
+ if unit_len > 0:
1370
+ entry = {
1371
+ "unit_id": self._pending_unit_id,
1372
+ "length": unit_len,
1373
+ "type": input_type,
1374
+ "generated_tokens": generated_tokens or [],
1375
+ "generated_text": generated_text or "", # used for context preserving mode
1376
+ "is_listen": is_listen,
1377
+ }
1378
+ self._unit_history.append(entry)
1379
+
1380
+ self._pending_unit_id = None
1381
+ self._pending_unit_start_cache_len = 0
1382
+ self._next_unit_id += 1
1383
+
1384
+ def register_system_prompt(self):
1385
+ """Call after system prompt prefill, record preserve length"""
1386
+ self._system_preserve_length = self.get_cache_length()
1387
+
1388
+ # sliding window core methods
1389
+
1390
+ def _get_rope_theta(self) -> float:
1391
+ """get model rope_theta configuration"""
1392
+ return float(getattr(self.m.config, "rope_theta", 10000.0))
1393
+
1394
+ def _drop_tokens_from_cache(self, length: int) -> bool:
1395
+ """remove specified number of tokens from cache (protect system prompt)
1396
+
1397
+ remove tokens in the range [preserve, preserve + length)
1398
+ supports DynamicCache and tuple cache formats
1399
+ """
1400
+ if self.cache is None or length <= 0:
1401
+ return False
1402
+
1403
+ cache_type = "DynamicCache" if isinstance(self.cache, DynamicCache) else "TupleCache"
1404
+ cache_len_before = self.get_cache_length()
1405
+ offset_before = self._position_offset
1406
+
1407
+ new_cache, new_offset, success = drop_tokens_from_cache(
1408
+ cache=self.cache,
1409
+ length=length,
1410
+ preserve=self._system_preserve_length,
1411
+ position_offset=self._position_offset,
1412
+ rope_theta=self._get_rope_theta(),
1413
+ inv_freq_cache=self._rope_inv_freq_cache,
1414
+ )
1415
+ if success:
1416
+ self.cache = new_cache # For DynamicCache this is the same object (in-place)
1417
+ self._position_offset = new_offset
1418
+
1419
+ return success
1420
+
1421
+ def _drop_unit(self, unit_id: int) -> bool:
1422
+ """remove specified unit"""
1423
+ entries = [u for u in self._unit_history if u["unit_id"] == unit_id]
1424
+ if not entries:
1425
+ return False
1426
+
1427
+ total_len = sum(e["length"] for e in entries)
1428
+ if total_len <= 0:
1429
+ for e in entries:
1430
+ self._unit_history.remove(e)
1431
+ return False
1432
+
1433
+ if not self._drop_tokens_from_cache(total_len):
1434
+ return False
1435
+
1436
+ for e in entries:
1437
+ self._unit_history.remove(e)
1438
+
1439
+ return True
1440
+
1441
+ def _drop_next_unit(self) -> bool:
1442
+ """remove the earliest non-system unit"""
1443
+ for entry in self._unit_history:
1444
+ unit_id = entry.get("unit_id")
1445
+ if unit_id is None:
1446
+ continue
1447
+ # skip system type
1448
+ if entry.get("type") == "system":
1449
+ continue
1450
+ if self._drop_unit(unit_id):
1451
+ return True
1452
+ return False
1453
+
1454
+ def enforce_window(self) -> bool:
1455
+ """enforce sliding window strategy (same as single-mode, only look at cache length)
1456
+
1457
+ when cache length exceeds high water line, loop to remove the earliest unit,
1458
+ until cache length drops below the low water line.
1459
+ """
1460
+ if not self._window_enabled:
1461
+ return False
1462
+
1463
+ cfg = self._window_config
1464
+ cache_len_before = self.get_cache_length()
1465
+
1466
+ if cache_len_before <= cfg.basic_window_high_tokens:
1467
+ return False # not above high water line, no trigger
1468
+
1469
+ dropped_count = 0
1470
+ cache_len = cache_len_before
1471
+ while cache_len > cfg.basic_window_low_tokens:
1472
+ if not self._drop_next_unit():
1473
+ break
1474
+ dropped_count += 1
1475
+ cache_len = self.get_cache_length()
1476
+
1477
+ if dropped_count > 0:
1478
+ # update statistics counters
1479
+ self._sliding_event_count += 1
1480
+ self._total_dropped_tokens += cache_len_before - cache_len
1481
+ self._total_dropped_units += dropped_count
1482
+
1483
+ # consistency check
1484
+ expected = self._system_preserve_length + sum(u["length"] for u in self._unit_history)
1485
+ is_consistent = expected == cache_len
1486
+ if not is_consistent:
1487
+ logger.error(
1488
+ "CONSISTENCY ERROR! preserve=%d + sum(units)=%d != cache=%d, offset=%d",
1489
+ self._system_preserve_length,
1490
+ sum(u["length"] for u in self._unit_history),
1491
+ cache_len,
1492
+ self._position_offset,
1493
+ )
1494
+
1495
+ return dropped_count > 0
1496
+
1497
+ # context preserving sliding window methods
1498
+
1499
+ def register_system_prompt_with_context(
1500
+ self,
1501
+ suffix_token_ids: Optional[List[int]] = None,
1502
+ context_previous_marker: str = "\n\nprevious: ",
1503
+ ):
1504
+ """register system prompt (with context preserving mode)
1505
+
1506
+ initial cache layout: [prefix] [suffix] [units...]
1507
+ after first sliding window: [prefix] [context_previous_marker + content] [suffix] [units...]
1508
+
1509
+ when calling this method, cache should only have prefix (without previous marker)
1510
+ suffix will be fed in later
1511
+
1512
+ Args:
1513
+ suffix_token_ids: suffix token ids (e.g. id of <|im_end|>)
1514
+ context_previous_marker: previous marker prefix, e.g. "\\n\\nprevious: "
1515
+ """
1516
+ # prefix = current cache content (fixed, without previous marker)
1517
+ self._preserve_prefix_length = self.get_cache_length()
1518
+ self._previous_content_length = 0 # initially no previous content
1519
+ self._suffix_token_ids = suffix_token_ids or []
1520
+ # total preserve length = prefix + suffix (initially no previous)
1521
+ self._system_preserve_length = self._preserve_prefix_length + len(self._suffix_token_ids)
1522
+
1523
+ # initialize previous related states
1524
+ self._previous_marker = context_previous_marker
1525
+ self._previous_marker_token_ids = (
1526
+ self.tokenizer.encode(context_previous_marker, add_special_tokens=False) if self.tokenizer else []
1527
+ )
1528
+ self._has_previous = False
1529
+ self._previous_text = ""
1530
+ self._previous_token_ids = []
1531
+
1532
+ def _extract_generated_text(self, units: List[Dict[str, Any]]) -> Tuple[str, List[int]]:
1533
+ """extract generated text and token ids from units
1534
+
1535
+ Args:
1536
+ units: list of units to extract
1537
+
1538
+ Returns:
1539
+ (text, token_ids): concatenated text and token ids (filtered out special tokens)
1540
+ """
1541
+ text_parts = []
1542
+ token_ids = []
1543
+
1544
+ for u in units:
1545
+ # only keep generated content of non-listen units
1546
+ if u.get("is_listen", False):
1547
+ continue
1548
+ gen_text = u.get("generated_text", "")
1549
+ gen_tokens = u.get("generated_tokens", [])
1550
+
1551
+ # filter out special tokens from text
1552
+ if gen_text:
1553
+ clean_text = gen_text
1554
+ for st in self._all_special_tokens_text:
1555
+ clean_text = clean_text.replace(st, "")
1556
+ if clean_text.strip():
1557
+ text_parts.append(clean_text)
1558
+
1559
+ # filter out special tokens
1560
+ if gen_tokens:
1561
+ filtered_tokens = [t for t in gen_tokens if t not in self._all_special_ids]
1562
+ token_ids.extend(filtered_tokens)
1563
+
1564
+ return "".join(text_parts), token_ids
1565
+
1566
+ def _rebuild_cache_with_previous(
1567
+ self,
1568
+ new_previous_tokens: List[int],
1569
+ units_to_keep_len: Optional[int] = None,
1570
+ ) -> bool:
1571
+ """rebuild cache, insert new previous content between prefix and suffix
1572
+
1573
+ cache layout change:
1574
+ [prefix] [old_prev] [suffix] [old_units] → [prefix] [new_prev] [suffix] [remaining_units]
1575
+
1576
+ Args:
1577
+ new_previous_tokens: new previous token ids
1578
+ units_to_keep_len: length of units to keep (from cache end backwards)
1579
+ if None, calculate based on unit_history
1580
+
1581
+ Returns:
1582
+ whether successful rebuild
1583
+ """
1584
+ if self.cache is None:
1585
+ return False
1586
+
1587
+ old_previous_len = self._previous_content_length
1588
+ new_previous_len = len(new_previous_tokens)
1589
+ suffix_len = len(self._suffix_token_ids)
1590
+ total_cache_len = self.get_cache_length()
1591
+
1592
+ # calculate length of units to keep
1593
+ if units_to_keep_len is None:
1594
+ units_to_keep_len = sum(u["length"] for u in self._unit_history)
1595
+
1596
+ # special case: if previous is unchanged (new and old are empty), no need to rebuild prefix+suffix part of cache
1597
+ # but still need to reindex units RoPE (because a unit was deleted, position changed)
1598
+ if new_previous_len == 0 and old_previous_len == 0:
1599
+ # cache layout: [prefix(7)] [suffix(1)] [units...]
1600
+ # only keep prefix + suffix + remaining_units
1601
+ preserve_len = self._preserve_prefix_length + suffix_len
1602
+
1603
+ # simply slice cache: [prefix+suffix] + [remaining_units]
1604
+ # remaining_units in cache end
1605
+ if units_to_keep_len > 0:
1606
+ # [0:preserve_len] + [total-units_to_keep_len:total]
1607
+ prefix_suffix_cache = self._slice_cache(0, preserve_len)
1608
+ units_cache = self._slice_cache(total_cache_len - units_to_keep_len, None)
1609
+
1610
+ # calculate number of dropped tokens
1611
+ dropped_tokens = total_cache_len - preserve_len - units_to_keep_len
1612
+
1613
+ # reindex units RoPE: position from (preserve_len + dropped_tokens) to preserve_len
1614
+ # note: no position_offset, because cache position has been compressed (from 0 start)
1615
+ if dropped_tokens > 0:
1616
+ old_start = preserve_len + dropped_tokens
1617
+ new_start = preserve_len
1618
+ units_cache = self._reindex_rope_for_cache(units_cache, old_start, new_start, units_to_keep_len)
1619
+
1620
+ self.cache = self._concat_caches(prefix_suffix_cache, units_cache)
1621
+ else:
1622
+ self.cache = self._slice_cache(0, preserve_len)
1623
+
1624
+ return True
1625
+
1626
+ # 1. get prefix cache (fixed)
1627
+ prefix_end = self._preserve_prefix_length
1628
+ prefix_cache = self._slice_cache(0, prefix_end)
1629
+
1630
+ # 2. get units cache to keep (from end)
1631
+ units_start_in_old_cache = total_cache_len - units_to_keep_len
1632
+ units_cache = None
1633
+ if units_to_keep_len > 0:
1634
+ units_cache = self._slice_cache(units_start_in_old_cache, None)
1635
+
1636
+ # 3. calculate new previous + suffix cache (needs forward)
1637
+ # merge previous tokens and suffix tokens
1638
+ prev_suffix_tokens = new_previous_tokens + self._suffix_token_ids
1639
+ prev_suffix_len = len(prev_suffix_tokens)
1640
+
1641
+ new_prefix_prev_suffix_cache = prefix_cache
1642
+ if prev_suffix_len > 0:
1643
+ # Embed tokens
1644
+ prev_suffix_embeds = self.embed_tokens(prev_suffix_tokens)
1645
+ # calculate start position (after prefix)
1646
+ start_pos = self._preserve_prefix_length + self._position_offset
1647
+
1648
+ # forward calculate KV cache
1649
+ with torch.no_grad():
1650
+ device = prev_suffix_embeds.device
1651
+ position_ids = torch.arange(
1652
+ start_pos,
1653
+ start_pos + prev_suffix_len,
1654
+ device=device,
1655
+ ).unsqueeze(0)
1656
+
1657
+ # use prefix cache as past_key_values
1658
+ outputs = self.m(
1659
+ inputs_embeds=(
1660
+ prev_suffix_embeds.unsqueeze(0) if prev_suffix_embeds.dim() == 2 else prev_suffix_embeds
1661
+ ),
1662
+ position_ids=position_ids,
1663
+ past_key_values=prefix_cache,
1664
+ use_cache=True,
1665
+ return_dict=True,
1666
+ )
1667
+ # new cache contains prefix + new_previous + suffix
1668
+ new_prefix_prev_suffix_cache = outputs.past_key_values
1669
+
1670
+ # 4. adjust units cache RoPE
1671
+ # new layout: [prefix] [new_prev] [suffix] [units]
1672
+ # note: no position_offset, because cache position has been compressed (from 0 start)
1673
+ new_system_total = prefix_end + new_previous_len + suffix_len
1674
+ if units_cache is not None and self._get_cache_len(units_cache) > 0:
1675
+ old_start = units_start_in_old_cache
1676
+ new_start = new_system_total
1677
+
1678
+ if old_start != new_start:
1679
+ units_cache = self._reindex_rope_for_cache(units_cache, old_start, new_start, units_to_keep_len)
1680
+
1681
+ # 5. concatenate new cache
1682
+ if units_cache is not None and self._get_cache_len(units_cache) > 0:
1683
+ self.cache = self._concat_caches(new_prefix_prev_suffix_cache, units_cache)
1684
+ else:
1685
+ self.cache = new_prefix_prev_suffix_cache
1686
+
1687
+ # 6. update length
1688
+ self._previous_content_length = new_previous_len
1689
+ # total preserve length = prefix + previous + suffix
1690
+ self._system_preserve_length = prefix_end + new_previous_len + suffix_len
1691
+
1692
+ # print detailed cache layout information
1693
+ prev_text_preview = self._previous_text[:50] + "..." if len(self._previous_text) > 50 else self._previous_text
1694
+ suffix_preview = self.tokenizer.decode(self._suffix_token_ids) if self._suffix_token_ids else ""
1695
+ return True
1696
+
1697
+ def _slice_cache(self, start: int, end: Optional[int], clone: bool = True):
1698
+ """slice cache
1699
+
1700
+ Args:
1701
+ start: start position
1702
+ end: end position (None means to end)
1703
+ clone: whether to clone (default True, to prevent shared memory issues)
1704
+ """
1705
+ if self.cache is None:
1706
+ return None
1707
+ if isinstance(self.cache, DynamicCache):
1708
+ # DynamicCache
1709
+ new_key_cache = [
1710
+ k[:, :, start:end, :].clone() if clone else k[:, :, start:end, :] for k in self.cache.key_cache
1711
+ ]
1712
+ new_value_cache = [
1713
+ v[:, :, start:end, :].clone() if clone else v[:, :, start:end, :] for v in self.cache.value_cache
1714
+ ]
1715
+ new_cache = DynamicCache()
1716
+ new_cache.key_cache = new_key_cache
1717
+ new_cache.value_cache = new_value_cache
1718
+ return new_cache
1719
+ else:
1720
+ # Tuple cache
1721
+ if clone:
1722
+ return tuple(
1723
+ (layer[0][:, :, start:end, :].clone(), layer[1][:, :, start:end, :].clone()) for layer in self.cache
1724
+ )
1725
+ else:
1726
+ return tuple((layer[0][:, :, start:end, :], layer[1][:, :, start:end, :]) for layer in self.cache)
1727
+
1728
+ @staticmethod
1729
+ def _get_cache_len(cache) -> int:
1730
+ if cache is None:
1731
+ return 0
1732
+ if isinstance(cache, DynamicCache):
1733
+ if len(cache.key_cache) > 0 and cache.key_cache[0].numel() > 0:
1734
+ return cache.key_cache[0].shape[2]
1735
+ return 0
1736
+
1737
+ if cache and cache[0] and cache[0][0] is not None:
1738
+ return cache[0][0].shape[2]
1739
+ return 0
1740
+
1741
+ @staticmethod
1742
+ def _concat_caches(cache1, cache2):
1743
+ if cache1 is None:
1744
+ return cache2
1745
+ if cache2 is None:
1746
+ return cache1
1747
+
1748
+ if isinstance(cache1, DynamicCache):
1749
+ new_cache = DynamicCache()
1750
+ new_cache.key_cache = [torch.cat([k1, k2], dim=2) for k1, k2 in zip(cache1.key_cache, cache2.key_cache)]
1751
+ new_cache.value_cache = [
1752
+ torch.cat([v1, v2], dim=2) for v1, v2 in zip(cache1.value_cache, cache2.value_cache)
1753
+ ]
1754
+ return new_cache
1755
+ else:
1756
+ return tuple(
1757
+ (
1758
+ torch.cat([layer1[0], layer2[0]], dim=2),
1759
+ torch.cat([layer1[1], layer2[1]], dim=2),
1760
+ )
1761
+ for layer1, layer2 in zip(cache1, cache2)
1762
+ )
1763
+
1764
+ def _reindex_rope_for_cache(self, cache, old_start: int, new_start: int, length: int):
1765
+ """reindex RoPE position for cache"""
1766
+ if cache is None or length <= 0:
1767
+ return cache
1768
+
1769
+ if isinstance(cache, DynamicCache):
1770
+ device = cache.key_cache[0].device if cache.key_cache else None
1771
+ else:
1772
+ device = cache[0][0].device if cache and cache[0] else None
1773
+
1774
+ if device is None:
1775
+ return cache
1776
+
1777
+ old_positions = torch.arange(old_start, old_start + length, device=device, dtype=torch.long)
1778
+ new_positions = torch.arange(new_start, new_start + length, device=device, dtype=torch.long)
1779
+
1780
+ rope_theta = self._get_rope_theta()
1781
+
1782
+ if isinstance(cache, DynamicCache):
1783
+ new_key_cache = []
1784
+ for k in cache.key_cache:
1785
+ new_k = realign_rotary_suffix(k, old_positions, new_positions, rope_theta, self._rope_inv_freq_cache)
1786
+ new_key_cache.append(new_k)
1787
+ cache.key_cache = new_key_cache
1788
+ return cache
1789
+ else:
1790
+ new_cache = []
1791
+ for layer in cache:
1792
+ new_k = realign_rotary_suffix(
1793
+ layer[0], old_positions, new_positions, rope_theta, self._rope_inv_freq_cache
1794
+ )
1795
+ new_cache.append((new_k, layer[1]))
1796
+ return tuple(new_cache)
1797
+
1798
+ def _update_previous(
1799
+ self,
1800
+ new_text: str,
1801
+ new_tokens: List[int],
1802
+ max_tokens: int,
1803
+ ) -> None:
1804
+ """update previous context (also update cache)
1805
+
1806
+ when first sliding window, dynamically add marker + text, subsequent sliding window append text
1807
+ when content exceeds max_tokens, truncate content (keep marker)
1808
+ rebuild cache to maintain consistency
1809
+
1810
+ Args:
1811
+ new_text: new text
1812
+ new_tokens: new token ids
1813
+ max_tokens: previous content maximum token count (without marker)
1814
+ """
1815
+ marker_len = len(self._previous_marker_token_ids)
1816
+ tokens_to_drop = 0
1817
+
1818
+ # if no new content, do not add marker, but still need to rebuild cache
1819
+ if not new_tokens and not new_text:
1820
+ # still need to rebuild cache (because a unit was deleted)
1821
+ self._rebuild_cache_with_previous(self._previous_token_ids)
1822
+ return
1823
+
1824
+ if not self._has_previous:
1825
+ # when first has actual content: add marker + text
1826
+ self._previous_text = new_text
1827
+ self._previous_token_ids = self._previous_marker_token_ids.copy() + new_tokens
1828
+ self._has_previous = True
1829
+ else:
1830
+ # subsequent sliding window: append text to previous
1831
+ self._previous_text += new_text
1832
+ self._previous_token_ids.extend(new_tokens)
1833
+
1834
+ # calculate token count of content (without marker)
1835
+ content_token_count = len(self._previous_token_ids) - marker_len
1836
+
1837
+ # check if need to truncate content (keep marker)
1838
+ if content_token_count > max_tokens:
1839
+ # truncate left content, keep marker + latest max_tokens content
1840
+ tokens_to_drop = content_token_count - max_tokens
1841
+ old_text = self._previous_text
1842
+ # keep marker + truncated content
1843
+ content_tokens = self._previous_token_ids[marker_len + tokens_to_drop :]
1844
+ self._previous_token_ids = self._previous_marker_token_ids.copy() + content_tokens
1845
+ # redecode text (only decode content part)
1846
+ try:
1847
+ self._previous_text = self.tokenizer.decode(
1848
+ content_tokens,
1849
+ skip_special_tokens=True,
1850
+ )
1851
+ except Exception as e:
1852
+ logger.warning("_update_previous: decode failed: %s", e)
1853
+
1854
+ # rebuild cache
1855
+ self._rebuild_cache_with_previous(self._previous_token_ids)
1856
+
1857
+ def _drop_unit_with_context(
1858
+ self,
1859
+ unit_id: int,
1860
+ max_previous_tokens: int,
1861
+ ) -> Tuple[bool, str, List[int]]:
1862
+ """remove specified unit and return its generated content (for context preserving)
1863
+
1864
+ process:
1865
+ 1. extract generated content of unit
1866
+ 2. remove unit from cache (without prefix+previous)
1867
+ 3. append generated content to previous
1868
+ 4. rebuild cache (in _update_previous)
1869
+
1870
+ Args:
1871
+ unit_id: unit ID to remove
1872
+ max_previous_tokens: previous maximum token count
1873
+
1874
+ Returns:
1875
+ (success, extracted_text, extracted_tokens): whether successful, extracted text and tokens
1876
+ """
1877
+ entries = [u for u in self._unit_history if u["unit_id"] == unit_id]
1878
+ if not entries:
1879
+ return False, "", []
1880
+
1881
+ # extract generated content
1882
+ extracted_text, extracted_tokens = self._extract_generated_text(entries)
1883
+
1884
+ # calculate total length
1885
+ total_len = sum(e["length"] for e in entries)
1886
+ if total_len <= 0:
1887
+ for e in entries:
1888
+ self._unit_history.remove(e)
1889
+ return False, extracted_text, extracted_tokens
1890
+
1891
+ cache_before = self.get_cache_length()
1892
+
1893
+ # remove from unit_history (record for later processing)
1894
+ for e in entries:
1895
+ self._unit_history.remove(e)
1896
+
1897
+ # note: here no longer call _drop_tokens_from_cache
1898
+ # because _update_previous will rebuild the entire cache
1899
+
1900
+ # update previous (also rebuild cache)
1901
+ self._update_previous(extracted_text, extracted_tokens, max_previous_tokens)
1902
+
1903
+ return True, extracted_text, extracted_tokens
1904
+
1905
+ def _drop_next_unit_with_context(self, max_previous_tokens: int) -> bool:
1906
+ """remove the earliest non-system unit (with context preserving)"""
1907
+ for entry in self._unit_history:
1908
+ unit_id = entry.get("unit_id")
1909
+ if unit_id is None:
1910
+ continue
1911
+ if entry.get("type") == "system":
1912
+ continue
1913
+ success, _, _ = self._drop_unit_with_context(unit_id, max_previous_tokens)
1914
+ if success:
1915
+ return True
1916
+ return False
1917
+
1918
+ def enforce_window_with_context(self) -> bool:
1919
+ """context preserving sliding window execution
1920
+
1921
+ when unit count exceeds max_units, remove the earliest unit,
1922
+ and accumulate its generated content to previous.
1923
+ Cache will be automatically rebuilt in _update_previous.
1924
+
1925
+ Returns:
1926
+ whether sliding window is executed
1927
+ """
1928
+ if not self._window_enabled:
1929
+ return False
1930
+
1931
+ cfg = self._window_config
1932
+
1933
+ if cfg.sliding_window_mode != "context":
1934
+ # if not context mode, fallback to basic sliding window
1935
+ return self.enforce_window()
1936
+
1937
+ cache_len_before = self.get_cache_length()
1938
+ units_before = len(self._unit_history)
1939
+
1940
+ # context preserving mode: only check if unit count exceeds limit
1941
+ # (previous exceeds limit in _update_previous will automatically truncate left)
1942
+ if units_before <= cfg.context_max_units:
1943
+ return False
1944
+
1945
+ # sliding window loop: remove unit until count ≤ max_units
1946
+ dropped_count = 0
1947
+ while len(self._unit_history) > cfg.context_max_units:
1948
+ if not self._drop_next_unit_with_context(cfg.context_previous_max_tokens):
1949
+ break
1950
+
1951
+ dropped_count += 1
1952
+
1953
+ cache_len_after = self.get_cache_length()
1954
+
1955
+ if dropped_count > 0:
1956
+ # update statistics counter
1957
+ self._sliding_event_count += 1
1958
+ self._total_dropped_tokens += cache_len_before - cache_len_after
1959
+ self._total_dropped_units += dropped_count
1960
+
1961
+ # consistency check
1962
+ expected = self._system_preserve_length + sum(u["length"] for u in self._unit_history)
1963
+
1964
+ return dropped_count > 0
1965
+
1966
+ def get_previous_context(self) -> Tuple[str, List[int]]:
1967
+ """get current accumulated previous context
1968
+
1969
+ Returns:
1970
+ (previous_text, previous_token_ids): current accumulated text and token ids
1971
+ """
1972
+ return self._previous_text, self._previous_token_ids.copy()
1973
+
1974
+ def get_window_stats(self) -> Dict[str, Any]:
1975
+ """get sliding window statistics"""
1976
+ unit_lengths = [u["length"] for u in self._unit_history]
1977
+ return {
1978
+ "cache_length": self.get_cache_length(),
1979
+ "unit_count": len(self._unit_history),
1980
+ "unit_lengths": unit_lengths,
1981
+ "unit_total_length": sum(unit_lengths),
1982
+ "system_preserve_length": self._system_preserve_length,
1983
+ "position_offset": self._position_offset,
1984
+ "window_enabled": self._window_enabled,
1985
+ "total_generated_tokens": self.get_total_generated_tokens(),
1986
+ "pending_unit_id": self._pending_unit_id,
1987
+ "next_unit_id": self._next_unit_id,
1988
+ "config": {
1989
+ "sliding_window_mode": self._window_config.sliding_window_mode,
1990
+ "basic_window_high_tokens": self._window_config.basic_window_high_tokens,
1991
+ "basic_window_low_tokens": self._window_config.basic_window_low_tokens,
1992
+ "context_previous_max_tokens": self._window_config.context_previous_max_tokens,
1993
+ "context_max_units": self._window_config.context_max_units,
1994
+ },
1995
+ # context preserving related
1996
+ "preserve_prefix_length": self._preserve_prefix_length,
1997
+ "previous_content_length": self._previous_content_length,
1998
+ "suffix_token_count": len(self._suffix_token_ids),
1999
+ "previous_text_length": len(self._previous_text),
2000
+ "previous_token_count": len(self._previous_token_ids),
2001
+ "has_system_template": self._system_prompt_template is not None,
2002
+ }
2003
+
2004
+ def _verify_consistency(self) -> bool:
2005
+ """verify unit history and cache length consistency"""
2006
+ expected = self._system_preserve_length + sum(u["length"] for u in self._unit_history)
2007
+ actual = self.get_cache_length()
2008
+ return expected == actual
2009
+
2010
+ def print_verification_summary(self) -> Dict[str, Any]:
2011
+ """print verification summary (for comparing off/basic/context mode)
2012
+
2013
+ Returns:
2014
+ dictionary containing key verification data
2015
+ """
2016
+ cfg = self._window_config
2017
+
2018
+ # collect all generated text
2019
+ all_generated_text = []
2020
+ all_generated_tokens = []
2021
+ for u in self._unit_history:
2022
+ if not u.get("is_listen", False):
2023
+ gen_text = u.get("generated_text", "")
2024
+ gen_tokens = u.get("generated_tokens", [])
2025
+ if gen_text:
2026
+ all_generated_text.append(gen_text)
2027
+ if gen_tokens:
2028
+ all_generated_tokens.extend(gen_tokens)
2029
+
2030
+ combined_text = "".join(all_generated_text)
2031
+
2032
+ summary = {
2033
+ "mode": cfg.sliding_window_mode,
2034
+ "final_cache_length": self.get_cache_length(),
2035
+ "final_unit_count": len(self._unit_history),
2036
+ "sliding_event_count": self._sliding_event_count,
2037
+ "total_dropped_tokens": self._total_dropped_tokens,
2038
+ "total_dropped_units": self._total_dropped_units,
2039
+ "total_generated_tokens": len(all_generated_tokens),
2040
+ "generated_text": combined_text,
2041
+ "previous_text": self._previous_text,
2042
+ "previous_token_count": len(self._previous_token_ids),
2043
+ "position_offset": self._position_offset,
2044
+ "system_preserve_length": self._system_preserve_length,
2045
+ }
2046
+
2047
+ return summary
2048
+
2049
+ def set_window_config(self, config: DuplexWindowConfig) -> None:
2050
+ """set sliding window configuration"""
2051
+ self._window_config = config
2052
+
2053
+ def set_window_enabled(self, enabled: bool) -> None:
2054
+ """enable/disable sliding window"""
2055
+ old_enabled = self._window_enabled
2056
+ self._window_enabled = enabled
2057
+
2058
+ def get_context(self):
2059
+ return self.context
2060
+
2061
+ def embed_token(self, tid):
2062
+ if isinstance(tid, int):
2063
+ tid = torch.tensor([tid], device=self.m.device)
2064
+ return self.m.model.embed_tokens(tid)
2065
+
2066
+ def embed_tokens(self, token_ids: List[int]) -> torch.Tensor:
2067
+ """batch embed multiple tokens
2068
+
2069
+ Args:
2070
+ token_ids: list of token ids
2071
+
2072
+ Returns:
2073
+ embeddings tensor [L, H]
2074
+ """
2075
+ if not token_ids:
2076
+ return torch.empty(0, self.m.config.hidden_size, device=self.m.device)
2077
+ tids = torch.tensor(token_ids, device=self.m.device)
2078
+ return self.m.model.embed_tokens(tids)
2079
+
2080
+ @torch.no_grad()
2081
+ def feed(self, embeds: torch.Tensor, return_logits: bool = False):
2082
+ """
2083
+ embeds : [L, H] —— new embedding sequence fed into model at once
2084
+ """
2085
+ L = embeds.size(0)
2086
+ device = embeds.device
2087
+
2088
+ past_len = self.get_cache_length()
2089
+ pos_ids = torch.arange(past_len, past_len + L, device=device).unsqueeze(0) # [1, L]
2090
+
2091
+ out = self.m(
2092
+ inputs_embeds=embeds.unsqueeze(0), # [1, L, H]
2093
+ position_ids=pos_ids,
2094
+ past_key_values=self.cache,
2095
+ # use_cache = True,
2096
+ return_dict=True,
2097
+ output_hidden_states=True,
2098
+ # attention_mask=attention_mask
2099
+ )
2100
+ self.cache = out.past_key_values
2101
+
2102
+ if return_logits:
2103
+ logits = self.m.lm_head(out.hidden_states[-1])[:, -1] # [1, vocab]
2104
+ return logits, out.hidden_states[-1]
2105
+
2106
+ @torch.no_grad()
2107
+ def decode(
2108
+ self,
2109
+ logits,
2110
+ mode: Literal["sampling", "greedy"] = "sampling",
2111
+ temperature=0.7,
2112
+ top_k=20,
2113
+ top_p=0.8,
2114
+ listen_top_k=None,
2115
+ listen_prob_scale=1.0,
2116
+ text_repetition_penalty=1.05,
2117
+ text_repetition_window_size=512,
2118
+ ):
2119
+ """
2120
+ Args:
2121
+ logits:
2122
+ mode: sampling or greedy
2123
+ temperature:
2124
+ top_k:
2125
+ top_p:
2126
+ listen_top_k: force listen_id to be in top-k to keep
2127
+ listen_prob_scale: multiply listen_id probability by a weight (<1 means decrease, >1 means increase)
2128
+ text_repetition_penalty: repetition penalty coefficient, >1.0 means decrease repetition, <1.0 means increase repetition
2129
+ text_repetition_window_size: repetition penalty window size
2130
+
2131
+ Sampling strategy:
2132
+ 1. first sample all tokens with original logits (apply temperature)
2133
+ 2. if sampled chunk_eos, return directly (keep the original model's decision of when to stop)
2134
+ 3. if not sampled chunk_eos, mask it (set logit to -inf), continue sampling text tokens
2135
+ 4. apply repetition penalty, top-k, top-p, etc. to the text tokens for the final sampling
2136
+ """
2137
+
2138
+ logits = logits.clone()
2139
+
2140
+ # 0. independently check chunk_eos before sampling
2141
+ eos_id = self.chunk_eos_id
2142
+
2143
+ with torch.no_grad():
2144
+ if mode == "greedy":
2145
+ sampled_token = torch.argmax(logits[0]).item()
2146
+ else:
2147
+ original_probs = F.softmax(logits[0], dim=-1)
2148
+ sampled_token = torch.multinomial(original_probs, num_samples=1).item()
2149
+
2150
+ # if sampled chunk_eos, return directly
2151
+ if sampled_token == eos_id:
2152
+ next_token_id = torch.tensor([eos_id], device=logits.device)
2153
+ next_token_str = self.tokenizer.decode(next_token_id)
2154
+
2155
+ return next_token_id
2156
+
2157
+ # if not sampled chunk_eos, set its logit to -inf
2158
+ if self.forbidden_token_ids:
2159
+ logits[:, self.forbidden_token_ids] = float("-inf")
2160
+
2161
+ # 1. apply repetition penalty
2162
+ if text_repetition_penalty != 1.0 and len(self.generated_tokens) > 0:
2163
+ # get recent tokens (within window size) considering special tokens and normal tokens
2164
+ recent_tokens = self.generated_tokens[-text_repetition_window_size:]
2165
+
2166
+ # make it unique
2167
+ recent_tokens = list(set(recent_tokens))
2168
+
2169
+ # apply penalty to repeated tokens
2170
+ for token_id in recent_tokens:
2171
+ if token_id < logits.size(-1): # ensure token_id is in vocabulary range
2172
+ if text_repetition_penalty > 1.0:
2173
+ # penalize repetition: decrease logits
2174
+ logits[0, token_id] /= text_repetition_penalty
2175
+ else:
2176
+ # encourage repetition: increase logits
2177
+ logits[0, token_id] *= 1.0 / text_repetition_penalty
2178
+
2179
+ if listen_prob_scale != 1.0: # modify listen token logit separately
2180
+ logits[0, self.listen_id] *= listen_prob_scale
2181
+
2182
+ listen_rank = (logits[0] > logits[0, self.listen_id]).sum().item()
2183
+
2184
+ if listen_top_k is not None and listen_rank < listen_top_k: # listen_id is in top-k, return directly
2185
+ next_token_id = torch.tensor([self.listen_id], device=logits.device)
2186
+ next_token_str = self.tokenizer.decode(next_token_id)
2187
+
2188
+ if next_token_str == "<|listen|>":
2189
+ self.context += " "
2190
+ else:
2191
+ self.context += next_token_str
2192
+
2193
+ return next_token_id
2194
+
2195
+ if mode == "greedy":
2196
+ next_token_id = torch.argmax(logits, dim=-1)
2197
+ elif mode == "sampling":
2198
+ logits = logits / temperature
2199
+ logits = top_k_top_p_filtering(logits, top_k=top_k, top_p=top_p)
2200
+ probs = F.softmax(logits, dim=-1)
2201
+ next_token_id = torch.multinomial(probs, num_samples=1).squeeze(1)
2202
+ else:
2203
+ raise ValueError(f"Unsupported decode mode: {mode}")
2204
+
2205
+ if next_token_id.item() not in self.special_token_ids:
2206
+ self.generated_tokens.append(next_token_id.item())
2207
+ else:
2208
+ self.generated_special_tokens.append(next_token_id.item())
2209
+
2210
+ return next_token_id
2211
+
2212
+
2213
+ def _download_url_to_tempfile(url: str, suffix: str = "", timeout: int = 60) -> str:
2214
+ """
2215
+ Download a URL to a temporary file and return the path.
2216
+
2217
+ Args:
2218
+ url: HTTP/HTTPS URL to download
2219
+ suffix: File suffix (e.g., ".jpg", ".wav", ".mp4")
2220
+ timeout: Download timeout in seconds
2221
+
2222
+ Returns:
2223
+ Path to the downloaded temporary file
2224
+ """
2225
+ import tempfile
2226
+
2227
+ import requests
2228
+
2229
+ response = requests.get(url, timeout=timeout)
2230
+ response.raise_for_status()
2231
+
2232
+ with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
2233
+ f.write(response.content)
2234
+ return f.name
2235
+
2236
+
2237
+ def _is_url(path: str) -> bool:
2238
+ return path.startswith(("http://", "https://"))
2239
+
2240
+
2241
+ def normalize_content_item(item) -> Union[str, Any, List[Any]]:
2242
+ """Normalize structured content item to native format.
2243
+
2244
+ Supports:
2245
+ - Native format: str, PIL.Image, np.ndarray (pass through)
2246
+ - OpenAI structured format:
2247
+ - {"type": "text", "text": "..."} -> str
2248
+ - {"type": "image_url", "image_url": {"url": "..."}} -> PIL.Image
2249
+ - {"type": "audio_url", "audio_url": {"url": "..."}} -> np.ndarray
2250
+ - {"type": "video_url", "video_url": {"url": "...", ...}} -> List[Image, ndarray, ...]
2251
+
2252
+ URL formats supported:
2253
+ - Local file path: "/path/to/file.jpg"
2254
+ - HTTP/HTTPS URL: "https://example.com/image.jpg"
2255
+
2256
+ Args:
2257
+ item: Content item to normalize
2258
+
2259
+ Returns:
2260
+ Normalized item. For video_url, returns a tuple ("__video_contents__", list)
2261
+ that will be flattened by normalize_content().
2262
+
2263
+ Raises:
2264
+ ValueError: If content type is unknown or unsupported
2265
+ """
2266
+ import os
2267
+
2268
+ import numpy as np
2269
+ from PIL import Image
2270
+
2271
+ if isinstance(item, str):
2272
+ return item
2273
+ if isinstance(item, Image.Image):
2274
+ return item
2275
+ if isinstance(item, np.ndarray):
2276
+ return item
2277
+
2278
+ if isinstance(item, dict):
2279
+ item_type = item.get("type")
2280
+
2281
+ if item_type == "text":
2282
+ return item.get("text", "")
2283
+
2284
+ elif item_type == "image_url":
2285
+ image_url_obj = item.get("image_url", {})
2286
+ url = image_url_obj.get("url", "") if isinstance(image_url_obj, dict) else image_url_obj
2287
+
2288
+ if _is_url(url):
2289
+ # Download to temp file
2290
+ temp_path = _download_url_to_tempfile(url, suffix=".jpg", timeout=30)
2291
+ img = Image.open(temp_path)
2292
+ os.unlink(temp_path)
2293
+ return img
2294
+ else:
2295
+ return Image.open(url)
2296
+ elif item_type == "audio_url":
2297
+ import librosa
2298
+
2299
+ audio_url_obj = item.get("audio_url", {})
2300
+ url = audio_url_obj.get("url", "") if isinstance(audio_url_obj, dict) else audio_url_obj
2301
+
2302
+ if _is_url(url):
2303
+ # Download to temp file
2304
+ temp_path = _download_url_to_tempfile(url, suffix=".wav", timeout=60)
2305
+ audio_np, _ = librosa.load(temp_path, sr=16000, mono=True)
2306
+ os.unlink(temp_path)
2307
+ return audio_np
2308
+ else:
2309
+ audio_np, _ = librosa.load(url, sr=16000, mono=True)
2310
+ return audio_np
2311
+ elif item_type == "video_url":
2312
+ # Video processing - returns a LIST of items (frames + audio segments)
2313
+ # Note: Unlike image_url/audio_url which return single items,
2314
+ # video_url returns a list that will be flattened into the content
2315
+ from minicpmo.utils import get_video_frame_audio_segments
2316
+
2317
+ video_url_obj = item.get("video_url", {})
2318
+ if isinstance(video_url_obj, dict):
2319
+ video_url = video_url_obj.get("url", "")
2320
+ # Get optional parameters from video_url object (OpenAI style)
2321
+ stack_frames = video_url_obj.get("stack_frames", 1)
2322
+ use_ffmpeg = video_url_obj.get("use_ffmpeg", False)
2323
+ use_audio = video_url_obj.get("use_audio", True)
2324
+ else:
2325
+ video_url = video_url_obj
2326
+ stack_frames = 1
2327
+ use_ffmpeg = False
2328
+ use_audio = True
2329
+
2330
+ # Handle HTTP/HTTPS URL - download to temp file
2331
+ temp_video_path = None
2332
+ if _is_url(video_url):
2333
+ temp_video_path = _download_url_to_tempfile(video_url, suffix=".mp4", timeout=120)
2334
+ video_path = temp_video_path
2335
+ else:
2336
+ video_path = video_url
2337
+
2338
+ # Extract frames and audio segments
2339
+ video_frames, audio_segments, stacked_frames = get_video_frame_audio_segments(
2340
+ video_path,
2341
+ stack_frames=stack_frames,
2342
+ use_ffmpeg=use_ffmpeg,
2343
+ use_audio=use_audio
2344
+ )
2345
+
2346
+ # Clean up temp file if downloaded
2347
+ if temp_video_path is not None:
2348
+ os.unlink(temp_video_path)
2349
+
2350
+ # Build omni_contents (interleaved frames and audio, or frames only)
2351
+ omni_contents = []
2352
+ for i in range(len(video_frames)):
2353
+ omni_contents.append(video_frames[i])
2354
+ if use_audio and audio_segments is not None:
2355
+ omni_contents.append(audio_segments[i])
2356
+ if stacked_frames is not None and i < len(stacked_frames) and stacked_frames[i] is not None:
2357
+ omni_contents.append(stacked_frames[i])
2358
+
2359
+ # Return as a special marker to be flattened later
2360
+ return "__video_contents__", omni_contents
2361
+ else:
2362
+ raise ValueError(f"Unknown content type: {item_type}")
2363
+
2364
+ raise ValueError(f"Cannot normalize content item of type: {type(item)}")
2365
+
2366
+
2367
+ def normalize_content(content) -> list:
2368
+ """Normalize message content to list of native items.
2369
+
2370
+ Input formats:
2371
+ - str: "hello" -> ["hello"]
2372
+ - list of native items: [str, Image, np.ndarray] -> pass through with normalization
2373
+ - list of structured items: [{"type": "text", ...}] -> normalize each
2374
+ - video type: automatically expanded to omni_contents
2375
+ - mixed: works too
2376
+
2377
+ Args:
2378
+ content: Message content in any supported format
2379
+
2380
+ Returns:
2381
+ List of native items (str, PIL.Image, np.ndarray)
2382
+
2383
+ Examples:
2384
+ >>> normalize_content("hello")
2385
+ ["hello"]
2386
+
2387
+ >>> normalize_content([{"type": "text", "text": "hi"}])
2388
+ ["hi"]
2389
+
2390
+ >>> normalize_content([{"type": "video", "video": "/path/to/video.mp4"}])
2391
+ [<PIL.Image>, <np.ndarray>, <PIL.Image>, <np.ndarray>, ...]
2392
+ """
2393
+ import numpy as np
2394
+ from PIL import Image
2395
+
2396
+ if isinstance(content, str):
2397
+ return [content]
2398
+
2399
+ if isinstance(content, list):
2400
+ result = []
2401
+ for item in content:
2402
+ normalized = normalize_content_item(item)
2403
+ # Handle video content (returns tuple with marker)
2404
+ if isinstance(normalized, tuple) and len(normalized) == 2 and normalized[0] == "__video_contents__":
2405
+ # Flatten video contents into result
2406
+ result.extend(normalized[1])
2407
+ else:
2408
+ result.append(normalized)
2409
+ return result
2410
+
2411
+ # Single non-list item (Image or np.ndarray)
2412
+ if isinstance(content, (Image.Image, np.ndarray)):
2413
+ return [content]
2414
+
2415
+ normalized = normalize_content_item(content)
2416
+ if isinstance(normalized, tuple) and len(normalized) == 2 and normalized[0] == "__video_contents__":
2417
+ return normalized[1]
2418
+ return [normalized]
Realtime-Venus-Audio/vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
Realtime-Venus-Omni/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 OpenBMB
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.
Realtime-Venus-Omni/NOTICE ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ Realtime-Venus-Omni
2
+ Copyright 2026 The Realtime-Venus Team. All rights reserved.
3
+
4
+ This product includes software derived from MiniCPM-o 4.5
5
+ (https://github.com/OpenBMB/MiniCPM-o), Copyright The OpenBMB Team,
6
+ licensed under the Apache License, Version 2.0.
7
+
8
+ Modifications include the Realtime-Venus identity and system prompts,
9
+ full-duplex delegation output handling, the long-video Memory runtime
10
+ under memory_adapter/, and rebranded module, class, and configuration names.
Realtime-Venus-Omni/added_tokens.json ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "</answer>": 151686,
3
+ "</box>": 151674,
4
+ "</focus>": 151688,
5
+ "</image>": 151670,
6
+ "</image_id>": 151682,
7
+ "</image_save_to>": 151696,
8
+ "</line>": 151690,
9
+ "</perception>": 151692,
10
+ "</point>": 151678,
11
+ "</quad>": 151676,
12
+ "</ref>": 151672,
13
+ "</slice>": 151680,
14
+ "</source_image>": 151694,
15
+ "</think>": 151668,
16
+ "</tool_call>": 151658,
17
+ "</tool_response>": 151666,
18
+ "</unit>": 151684,
19
+ "<answer>": 151685,
20
+ "<box>": 151673,
21
+ "<focus>": 151687,
22
+ "<image>": 151669,
23
+ "<image_id>": 151681,
24
+ "<image_save_to>": 151695,
25
+ "<line>": 151689,
26
+ "<perception>": 151691,
27
+ "<point>": 151677,
28
+ "<quad>": 151675,
29
+ "<ref>": 151671,
30
+ "<slice>": 151679,
31
+ "<source_image>": 151693,
32
+ "<think>": 151667,
33
+ "<tool_call>": 151657,
34
+ "<tool_response>": 151665,
35
+ "<unit>": 151683,
36
+ "<|audio_end|>": 151699,
37
+ "<|audio_start|>": 151697,
38
+ "<|audio|>": 151698,
39
+ "<|box_end|>": 151649,
40
+ "<|box_start|>": 151648,
41
+ "<|emotion_end|>": 151711,
42
+ "<|emotion_start|>": 151710,
43
+ "<|endoftext|>": 151643,
44
+ "<|file_sep|>": 151664,
45
+ "<|fim_middle|>": 151660,
46
+ "<|fim_pad|>": 151662,
47
+ "<|fim_prefix|>": 151659,
48
+ "<|fim_suffix|>": 151661,
49
+ "<|im_end|>": 151645,
50
+ "<|im_start|>": 151644,
51
+ "<|image_pad|>": 151655,
52
+ "<|interrupt|>": 151707,
53
+ "<|listen|>": 151705,
54
+ "<|object_ref_end|>": 151647,
55
+ "<|object_ref_start|>": 151646,
56
+ "<|pitch_end|>": 151715,
57
+ "<|pitch_start|>": 151714,
58
+ "<|quad_end|>": 151651,
59
+ "<|quad_start|>": 151650,
60
+ "<|repo_name|>": 151663,
61
+ "<|speak|>": 151706,
62
+ "<|speed_end|>": 151713,
63
+ "<|speed_start|>": 151712,
64
+ "<|spk_bos|>": 151700,
65
+ "<|spk_eos|>": 151702,
66
+ "<|spk|>": 151701,
67
+ "<|turn_bos|>": 151716,
68
+ "<|timbre_10|>": 151726,
69
+ "<|timbre_11|>": 151727,
70
+ "<|timbre_12|>": 151728,
71
+ "<|timbre_13|>": 151729,
72
+ "<|timbre_14|>": 151730,
73
+ "<|timbre_15|>": 151731,
74
+ "<|timbre_16|>": 151732,
75
+ "<|timbre_17|>": 151733,
76
+ "<|timbre_18|>": 151734,
77
+ "<|timbre_19|>": 151735,
78
+ "<|turn_eos|>": 151717,
79
+ "<|timbre_20|>": 151736,
80
+ "<|timbre_21|>": 151737,
81
+ "<|timbre_22|>": 151738,
82
+ "<|timbre_23|>": 151739,
83
+ "<|timbre_24|>": 151740,
84
+ "<|timbre_25|>": 151741,
85
+ "<|timbre_26|>": 151742,
86
+ "<|timbre_27|>": 151743,
87
+ "<|timbre_28|>": 151744,
88
+ "<|timbre_29|>": 151745,
89
+ "<|chunk_eos|>": 151718,
90
+ "<|timbre_30|>": 151746,
91
+ "<|timbre_31|>": 151747,
92
+ "<|chunk_bos|>": 151719,
93
+ "<|chunk_tts_bos|>": 151720,
94
+ "<|chunk_tts_eos|>": 151721,
95
+ "<|tts_pad|>": 151722,
96
+ "<|timbre_7|>": 151723,
97
+ "<|timbre_8|>": 151724,
98
+ "<|timbre_9|>": 151725,
99
+ "<|tts_bos|>": 151703,
100
+ "<|tts_eos|>": 151704,
101
+ "<|vad_end|>": 151709,
102
+ "<|vad_start|>": 151708,
103
+ "<|video_pad|>": 151656,
104
+ "<|vision_end|>": 151653,
105
+ "<|vision_pad|>": 151654,
106
+ "<|vision_start|>": 151652,
107
+ "<delegate>": 151748,
108
+ "</delegate>": 151749,
109
+ "<backend>": 151750,
110
+ "</backend>": 151751
111
+ }
Realtime-Venus-Omni/assets/HT_ref_audio.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cb8f06ba5080cdf548969138881fb8ad8b04e2516108f4e08ba0363b68b613ea
3
+ size 192590
Realtime-Venus-Omni/assets/sample_1_real.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ba1aa1242318beef20e56dd3b19e5cf15503a121fe1a348a6366926fb86a4146
3
+ size 95813537
Realtime-Venus-Omni/assets/speech_in.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d44faf7742d6597440c1fbb94dc87e4db27086af8cf549f6fc28fdd2020b0155
3
+ size 1691744
Realtime-Venus-Omni/assets/token2wav/campplus.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a6ac6a63997761ae2997373e2ee1c47040854b4b759ea41ec48e4e42df0f4d73
3
+ size 28303423
Realtime-Venus-Omni/assets/token2wav/flow.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:15ccff24256ff61537c7f8b51e025116b83405f3fb017b54b008fc97da115446
3
+ size 623466603
Realtime-Venus-Omni/assets/token2wav/flow.yaml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flow: !new:cosyvoice2.flow.flow.CausalMaskedDiffWithXvec
2
+ input_size: 512
3
+ output_size: 80
4
+ spk_embed_dim: 192
5
+ output_type: 'mel'
6
+ vocab_size: 6561
7
+ encoder: !new:cosyvoice2.transformer.upsample_encoder_v2.UpsampleConformerEncoderV2
8
+ input_size: 512
9
+ output_size: 512
10
+ input_layer: 'linear'
11
+ pre_lookahead_len: 3
12
+ num_blocks: 6
13
+ num_up_blocks: 4
14
+ up_stride: 2
15
+ up_scale_factor: 2
16
+ attention_heads: 8
17
+ pos_enc_layer_type: 'rel_pos_espnet'
18
+ selfattention_layer_type: 'rel_selfattn'
19
+ key_bias: true
20
+ linear_units: 2048
21
+ dropout_rate: 0.1
22
+ positional_dropout_rate: 0.1
23
+ attention_dropout_rate: 0.1
24
+ normalize_before: True
25
+ decoder: !new:cosyvoice2.flow.flow_matching.CausalConditionalCFM
26
+ inference_cfg_rate: 0.7
27
+ estimator: !new:cosyvoice2.flow.decoder_dit.DiT
28
+ in_channels: 320
29
+ out_channels: 80
30
+ mlp_ratio: 4.0
31
+ depth: 16
32
+ num_heads: 8
33
+ head_dim: 64
34
+ hidden_size: 512
Realtime-Venus-Omni/assets/token2wav/hift.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3386cc880324d4e98e05987b99107f49e40ed925b8ecc87c1f4939432d429879
3
+ size 83390254
Realtime-Venus-Omni/assets/token2wav/speech_tokenizer_v2_25hz.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d43342aa12163a80bf07bffb94c9de2e120a8df2f9917cd2f642e7f4219c6f71
3
+ size 496082973
Realtime-Venus-Omni/config.json ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "RealtimeVenusOmni"
4
+ ],
5
+ "version": "4.5",
6
+ "attention_bias": false,
7
+ "attention_dropout": 0.0,
8
+ "audio_chunk_length": 1.0,
9
+ "audio_config": {
10
+ "_attn_implementation_autoset": true,
11
+ "_name_or_path": "openai/whisper-medium",
12
+ "activation_dropout": 0.0,
13
+ "activation_function": "gelu",
14
+ "apply_spec_augment": false,
15
+ "architectures": [
16
+ "RealtimeVenusOmniWhisperEncoder"
17
+ ],
18
+ "attention_dropout": 0.0,
19
+ "begin_suppress_tokens": [
20
+ 220,
21
+ 50257
22
+ ],
23
+ "bos_token_id": 50257,
24
+ "classifier_proj_size": 256,
25
+ "d_model": 1024,
26
+ "decoder_attention_heads": 16,
27
+ "decoder_ffn_dim": 4096,
28
+ "decoder_layerdrop": 0.0,
29
+ "decoder_layers": 24,
30
+ "decoder_start_token_id": 50258,
31
+ "dropout": 0.0,
32
+ "encoder_attention_heads": 16,
33
+ "encoder_ffn_dim": 4096,
34
+ "encoder_layerdrop": 0.0,
35
+ "encoder_layers": 24,
36
+ "eos_token_id": 50257,
37
+ "forced_decoder_ids": [
38
+ [
39
+ 1,
40
+ 50259
41
+ ],
42
+ [
43
+ 2,
44
+ 50359
45
+ ],
46
+ [
47
+ 3,
48
+ 50363
49
+ ]
50
+ ],
51
+ "init_std": 0.02,
52
+ "mask_feature_length": 10,
53
+ "mask_feature_min_masks": 0,
54
+ "mask_feature_prob": 0.0,
55
+ "mask_time_length": 10,
56
+ "mask_time_min_masks": 2,
57
+ "mask_time_prob": 0.05,
58
+ "max_length": 448,
59
+ "max_source_positions": 1500,
60
+ "max_target_positions": 448,
61
+ "median_filter_width": 7,
62
+ "model_type": "whisper",
63
+ "num_hidden_layers": 24,
64
+ "num_mel_bins": 80,
65
+ "pad_token_id": 50257,
66
+ "scale_embedding": false,
67
+ "suppress_tokens": [
68
+ 1,
69
+ 2,
70
+ 7,
71
+ 8,
72
+ 9,
73
+ 10,
74
+ 14,
75
+ 25,
76
+ 26,
77
+ 27,
78
+ 28,
79
+ 29,
80
+ 31,
81
+ 58,
82
+ 59,
83
+ 60,
84
+ 61,
85
+ 62,
86
+ 63,
87
+ 90,
88
+ 91,
89
+ 92,
90
+ 93,
91
+ 359,
92
+ 503,
93
+ 522,
94
+ 542,
95
+ 873,
96
+ 893,
97
+ 902,
98
+ 918,
99
+ 922,
100
+ 931,
101
+ 1350,
102
+ 1853,
103
+ 1982,
104
+ 2460,
105
+ 2627,
106
+ 3246,
107
+ 3253,
108
+ 3268,
109
+ 3536,
110
+ 3846,
111
+ 3961,
112
+ 4183,
113
+ 4667,
114
+ 6585,
115
+ 6647,
116
+ 7273,
117
+ 9061,
118
+ 9383,
119
+ 10428,
120
+ 10929,
121
+ 11938,
122
+ 12033,
123
+ 12331,
124
+ 12562,
125
+ 13793,
126
+ 14157,
127
+ 14635,
128
+ 15265,
129
+ 15618,
130
+ 16553,
131
+ 16604,
132
+ 18362,
133
+ 18956,
134
+ 20075,
135
+ 21675,
136
+ 22520,
137
+ 26130,
138
+ 26161,
139
+ 26435,
140
+ 28279,
141
+ 29464,
142
+ 31650,
143
+ 32302,
144
+ 32470,
145
+ 36865,
146
+ 42863,
147
+ 47425,
148
+ 49870,
149
+ 50254,
150
+ 50258,
151
+ 50358,
152
+ 50359,
153
+ 50360,
154
+ 50361,
155
+ 50362
156
+ ],
157
+ "torch_dtype": "float32",
158
+ "use_cache": true,
159
+ "use_weighted_layer_sum": false,
160
+ "vocab_size": 51865
161
+ },
162
+ "audio_pool_step": 5,
163
+ "auto_map": {
164
+ "AutoConfig": "configuration_realtime_venus_omni.RealtimeVenusOmniConfig",
165
+ "AutoModel": "modeling_realtime_venus_omni.RealtimeVenusOmni",
166
+ "AutoModelForCausalLM": "modeling_realtime_venus_omni.RealtimeVenusOmni"
167
+ },
168
+ "batch_vision_input": true,
169
+ "bos_token_id": 151643,
170
+ "drop_vision_last_layer": false,
171
+ "eos_token_id": 151645,
172
+ "head_dim": 128,
173
+ "hidden_act": "silu",
174
+ "hidden_size": 4096,
175
+ "image_size": 448,
176
+ "init_audio": true,
177
+ "init_tts": true,
178
+ "init_vision": true,
179
+ "initializer_range": 0.02,
180
+ "intermediate_size": 12288,
181
+ "listen_speak_type": "asr",
182
+ "max_position_embeddings": 40960,
183
+ "max_window_layers": 36,
184
+ "model_type": "realtime_venus_omni",
185
+ "num_attention_heads": 32,
186
+ "num_hidden_layers": 36,
187
+ "num_key_value_heads": 8,
188
+ "patch_size": 14,
189
+ "query_num": 64,
190
+ "rms_norm_eps": 1e-06,
191
+ "rope_scaling": null,
192
+ "rope_theta": 1000000,
193
+ "slice_config": {
194
+ "max_slice_nums": 1,
195
+ "model_type": "realtime_venus_omni_vision",
196
+ "patch_size": 14,
197
+ "scale_resolution": 448
198
+ },
199
+ "slice_mode": true,
200
+ "sliding_window": null,
201
+ "stream_input": true,
202
+ "tie_word_embeddings": false,
203
+ "torch_dtype": "bfloat16",
204
+ "transformers_version": "4.51.0",
205
+ "tts_config": {
206
+ "_attn_implementation_autoset": true,
207
+ "attention_type": "full_attention",
208
+ "attn_implementation": "sdpa",
209
+ "audio_bos_token_id": 151687,
210
+ "audio_tokenizer_sample_rate": 16000,
211
+ "audio_tokenizer_type": "s3tokenizer",
212
+ "aug_layer_loss_weight": false,
213
+ "aug_loss_weight": false,
214
+ "backbone_model": "llama",
215
+ "condition_type": "hidden_text_merge",
216
+ "cosyvoice_config_path": null,
217
+ "cosyvoice_model_dir": null,
218
+ "filter_tts_loss": false,
219
+ "hidden_act": "silu",
220
+ "hidden_size": 768,
221
+ "interleaved": false,
222
+ "intermediate_size": 3072,
223
+ "llm_dim": 4096,
224
+ "llm_dim_model_base": 256,
225
+ "llm_down_scale": false,
226
+ "llm_hidden_size": 4096,
227
+ "llm_intermediate_size": 768,
228
+ "long_weight": 0.1,
229
+ "max_position_embeddings": 4096,
230
+ "model_type": "realtime_venus_omni_tts",
231
+ "normalize_projected_hidden": true,
232
+ "num_attention_heads": 12,
233
+ "num_audio_tokens": 6562,
234
+ "num_hidden_layers": 20,
235
+ "num_key_value_heads": 12,
236
+ "num_mel_bins": 100,
237
+ "num_text_tokens": 152064,
238
+ "num_vq": 1,
239
+ "projector_type": "mlp",
240
+ "recomputed_chunks": 1,
241
+ "s3_stream_chunk_size": 25,
242
+ "s3_stream_generate": false,
243
+ "s3_stream_n_timesteps": 10,
244
+ "s3_stream_prelook_size": 3,
245
+ "short_weight": 0.1,
246
+ "streaming": false,
247
+ "streaming_audio_chunk_size": 50,
248
+ "streaming_sliding_window": false,
249
+ "streaming_sliding_window_audio_frame_rate": 50,
250
+ "streaming_sliding_window_audio_init_text_length": 10,
251
+ "streaming_sliding_window_audio_window_size": 300,
252
+ "streaming_sliding_window_average_speed": 5,
253
+ "streaming_sliding_window_fast_speed": 7,
254
+ "streaming_sliding_window_max_text_len": 500,
255
+ "streaming_sliding_window_slow_speed": 3,
256
+ "streaming_sliding_window_text_window_size": 50,
257
+ "streaming_text_chunk_max": 7,
258
+ "streaming_text_chunk_min": 3,
259
+ "streaming_text_reserved_len": 300,
260
+ "text_eos_token_id": 151692,
261
+ "tts_filter_loss_fix": false,
262
+ "use_llm_hidden_state": false,
263
+ "use_text": true,
264
+ "window_size": 2
265
+ },
266
+ "use_cache": true,
267
+ "use_image_id": true,
268
+ "use_sliding_window": false,
269
+ "vision_batch_size": 16,
270
+ "vision_config": {
271
+ "_attn_implementation_autoset": true,
272
+ "attention_dropout": 0.0,
273
+ "hidden_act": "gelu_pytorch_tanh",
274
+ "hidden_size": 1152,
275
+ "image_size": 980,
276
+ "intermediate_size": 4304,
277
+ "layer_norm_eps": 1e-06,
278
+ "model_type": "siglip_vision_model",
279
+ "num_attention_heads": 16,
280
+ "num_channels": 3,
281
+ "num_hidden_layers": 27,
282
+ "patch_size": 14
283
+ },
284
+ "vocab_size": 151752
285
+ }
Realtime-Venus-Omni/configuration_realtime_venus_omni.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Copyright 2026 The OpenBMB Team. All rights reserved.
4
+ # Modifications Copyright 2026 The Realtime-Venus Team.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ import os
19
+ from typing import Union
20
+
21
+ from transformers import PretrainedConfig
22
+ from transformers import Qwen3Config
23
+ from transformers import WhisperConfig
24
+ from transformers.utils import logging
25
+
26
+ from .modeling_navit_siglip import SiglipVisionConfig
27
+
28
+ logger = logging.get_logger(__name__)
29
+
30
+
31
+ class RealtimeVenusOmniSliceConfig(PretrainedConfig):
32
+ model_type = "realtime_venus_omni_vision"
33
+
34
+ def __init__(
35
+ self,
36
+ patch_size=14,
37
+ max_slice_nums=9,
38
+ scale_resolution=448,
39
+ **kwargs,
40
+ ):
41
+ super().__init__(**kwargs)
42
+ self.patch_size = patch_size
43
+ self.max_slice_nums = max_slice_nums
44
+ self.scale_resolution = scale_resolution
45
+
46
+ @classmethod
47
+ def from_pretrained(cls, pretrained_model_name_or_path: Union[str, os.PathLike], **kwargs) -> "PretrainedConfig":
48
+ cls._set_token_in_kwargs(kwargs)
49
+
50
+ config_dict, kwargs = cls.get_config_dict(pretrained_model_name_or_path, **kwargs)
51
+
52
+ if config_dict.get("model_type") == "realtime_venus_omni_vision":
53
+ config_dict = config_dict["slice_config"]
54
+
55
+ if "model_type" in config_dict and hasattr(cls, "model_type") and config_dict["model_type"] != cls.model_type:
56
+ logger.warning(
57
+ f"You are using a model of type {config_dict['model_type']} to instantiate a model of type "
58
+ f"{cls.model_type}. This is not supported for all configurations of models and can yield errors."
59
+ )
60
+
61
+ return cls.from_dict(config_dict, **kwargs)
62
+
63
+
64
+ class RealtimeVenusOmniTTSConfig(PretrainedConfig):
65
+ model_type = "realtime_venus_omni_tts"
66
+
67
+ def __init__(
68
+ self,
69
+ llm_dim: int = 2560,
70
+ llm_intermediate_size: int = 768,
71
+ llm_down_scale: bool = False,
72
+ llm_dim_model_base: int = 256,
73
+ projector_type: str = "mlp",
74
+ hidden_act: str = "silu",
75
+ aug_loss_weight: bool = False,
76
+ aug_layer_loss_weight: bool = False,
77
+ filter_tts_loss: bool = False,
78
+ tts_filter_loss_fix: bool = False,
79
+ long_weight: float = 0.1,
80
+ short_weight: float = 0.1,
81
+ hidden_size: int = 768,
82
+ intermediate_size: int = 3072,
83
+ num_attention_heads: int = 12,
84
+ num_hidden_layers: int = 20,
85
+ num_key_value_heads: int = 12,
86
+ max_position_embeddings: int = 4096,
87
+ num_audio_tokens: int = 4097,
88
+ num_text_tokens: int = 21178,
89
+ num_mel_bins: int = 100,
90
+ num_vq: int = 1,
91
+ use_llm_hidden_state: bool = False,
92
+ audio_bos_token_id: int = 21132,
93
+ text_eos_token_id: int = 21133,
94
+ use_text: bool = True,
95
+ streaming: bool = False,
96
+ streaming_text_chunk_min: int = 3,
97
+ streaming_text_chunk_max: int = 7,
98
+ streaming_text_reserved_len: int = 300,
99
+ streaming_audio_chunk_size: int = 50,
100
+ attn_implementation: str = "sdpa",
101
+ condition_type: str = "llm_hidden",
102
+ backbone_model: str = "llama",
103
+ audio_tokenizer_type: str = "wavtokenizer",
104
+ audio_tokenizer_sample_rate: int = 24000,
105
+ streaming_sliding_window: bool = False,
106
+ streaming_sliding_window_max_text_len: int = 500,
107
+ streaming_sliding_window_average_speed: int = 5,
108
+ streaming_sliding_window_fast_speed: int = 7,
109
+ streaming_sliding_window_slow_speed: int = 3,
110
+ streaming_sliding_window_audio_frame_rate: int = 50,
111
+ streaming_sliding_window_audio_init_text_length: int = 10,
112
+ streaming_sliding_window_audio_window_size: int = 300,
113
+ normalize_projected_hidden: bool = False,
114
+ interleaved: bool = False,
115
+ attention_type: str = "sliding_recompute",
116
+ recomputed_chunks: int = 1,
117
+ window_size: int = 2,
118
+ **kwargs,
119
+ ):
120
+ super().__init__(**kwargs)
121
+
122
+ self.llm_dim = llm_dim
123
+ self.llm_hidden_size = llm_dim
124
+ self.llm_intermediate_size = llm_intermediate_size
125
+ self.llm_down_scale = llm_down_scale
126
+ self.llm_dim_model_base = llm_dim_model_base
127
+ self.projector_type = projector_type
128
+ self.aug_loss_weight = aug_loss_weight
129
+ self.aug_layer_loss_weight = aug_layer_loss_weight
130
+ self.tts_filter_loss_fix = tts_filter_loss_fix
131
+ self.filter_tts_loss = filter_tts_loss
132
+ self.long_weight = long_weight
133
+ self.short_weight = short_weight
134
+ self.hidden_act = hidden_act
135
+
136
+ self.hidden_size = hidden_size
137
+ self.intermediate_size = intermediate_size
138
+ self.num_attention_heads = num_attention_heads
139
+ self.num_hidden_layers = num_hidden_layers
140
+ self.num_key_value_heads = num_key_value_heads
141
+ self.max_position_embeddings = max_position_embeddings
142
+ self.num_audio_tokens = num_audio_tokens
143
+ self.num_text_tokens = num_text_tokens
144
+ self.num_mel_bins = num_mel_bins
145
+ self.num_vq = num_vq
146
+ self.use_llm_hidden_state = use_llm_hidden_state
147
+ self.audio_bos_token_id = audio_bos_token_id
148
+ self.text_eos_token_id = text_eos_token_id
149
+ self.use_text = use_text
150
+ self.streaming = streaming
151
+ self.streaming_text_chunk_min = streaming_text_chunk_min
152
+ self.streaming_text_chunk_max = streaming_text_chunk_max
153
+ self.streaming_text_reserved_len = streaming_text_reserved_len
154
+ self.streaming_audio_chunk_size = streaming_audio_chunk_size
155
+ self.attn_implementation = attn_implementation
156
+ self.condition_type = condition_type
157
+ self.backbone_model = backbone_model
158
+ self.audio_tokenizer_type = audio_tokenizer_type
159
+ self.audio_tokenizer_sample_rate = audio_tokenizer_sample_rate
160
+
161
+ self.streaming_sliding_window = streaming_sliding_window
162
+ self.streaming_sliding_window_max_text_len = streaming_sliding_window_max_text_len
163
+ self.streaming_sliding_window_average_speed = streaming_sliding_window_average_speed
164
+ self.streaming_sliding_window_fast_speed = streaming_sliding_window_fast_speed
165
+ self.streaming_sliding_window_slow_speed = streaming_sliding_window_slow_speed
166
+ self.streaming_sliding_window_audio_frame_rate = streaming_sliding_window_audio_frame_rate
167
+ self.streaming_sliding_window_audio_init_text_length = streaming_sliding_window_audio_init_text_length
168
+ self.streaming_sliding_window_audio_window_size = streaming_sliding_window_audio_window_size
169
+
170
+ self.normalize_projected_hidden = normalize_projected_hidden
171
+
172
+ self.interleaved = interleaved
173
+ self.attention_type = attention_type
174
+ self.recomputed_chunks = recomputed_chunks
175
+ self.window_size = window_size
176
+
177
+
178
+ class RealtimeVenusOmniConfig(Qwen3Config):
179
+ model_type = "realtime_venus_omni"
180
+ keys_to_ignore_at_inference = ["past_key_values"]
181
+
182
+ default_vision_config = {
183
+ "hidden_size": 1152,
184
+ "image_size": 980,
185
+ "intermediate_size": 4304,
186
+ "model_type": "siglip",
187
+ "num_attention_heads": 16,
188
+ "num_hidden_layers": 27,
189
+ "patch_size": 14,
190
+ }
191
+
192
+ def __init__(
193
+ self,
194
+ use_cache=True,
195
+ query_num=64,
196
+ image_size=448,
197
+ drop_vision_last_layer=True,
198
+ batch_vision_input=True,
199
+ slice_config=None,
200
+ vision_config=None,
201
+ audio_config=None,
202
+ tts_config=None,
203
+ use_image_id=True,
204
+ vision_batch_size=16,
205
+ audio_pool_step=5,
206
+ audio_chunk_length=1.0,
207
+ stream_input=False,
208
+ listen_speak_type="asr",
209
+ init_vision=True,
210
+ init_audio=True,
211
+ init_tts=True,
212
+ **kwargs,
213
+ ):
214
+ self.use_cache = use_cache
215
+ self.query_num = query_num
216
+ self.image_size = image_size
217
+ self.drop_vision_last_layer = drop_vision_last_layer
218
+ self.batch_vision_input = batch_vision_input
219
+ self.use_image_id = use_image_id
220
+ self.vision_batch_size = vision_batch_size
221
+ self.audio_pool_step = audio_pool_step
222
+ self.audio_chunk_length = audio_chunk_length
223
+ self.stream_input = stream_input
224
+ self.listen_speak_type = listen_speak_type
225
+
226
+ self.init_vision = init_vision
227
+ self.init_audio = init_audio
228
+ self.init_tts = init_tts
229
+
230
+ if slice_config is None:
231
+ self.slice_config = RealtimeVenusOmniSliceConfig(max_slice_nums=1)
232
+ else:
233
+ self.slice_config = RealtimeVenusOmniSliceConfig(**slice_config)
234
+ self.slice_mode = True
235
+
236
+ # same as HuggingFaceM4/siglip-so400m-14-980-flash-attn2-navit add tgt_sizes
237
+ if vision_config is None:
238
+ self.vision_config = SiglipVisionConfig(**self.default_vision_config)
239
+ logger.info("vision_config is None, using default vision config")
240
+ elif isinstance(vision_config, dict):
241
+ self.vision_config = SiglipVisionConfig(**vision_config)
242
+ elif isinstance(vision_config, SiglipVisionConfig):
243
+ self.vision_config = vision_config
244
+
245
+ if audio_config is None:
246
+ self.audio_config = WhisperConfig()
247
+ elif isinstance(audio_config, dict):
248
+ self.audio_config = WhisperConfig(**audio_config)
249
+ elif isinstance(audio_config, WhisperConfig):
250
+ self.audio_config = audio_config
251
+
252
+ if tts_config is None:
253
+ self.tts_config = RealtimeVenusOmniTTSConfig()
254
+ elif isinstance(tts_config, dict):
255
+ self.tts_config = RealtimeVenusOmniTTSConfig(**tts_config)
256
+ elif isinstance(tts_config, RealtimeVenusOmniTTSConfig):
257
+ self.tts_config = tts_config
258
+
259
+ self.patch_size = self.vision_config.patch_size
260
+
261
+ super().__init__(**kwargs)
Realtime-Venus-Omni/generation_config.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 151643,
3
+ "do_sample": true,
4
+ "eos_token_id": [
5
+ 151645,
6
+ 151643
7
+ ],
8
+ "pad_token_id": 151643,
9
+ "temperature": 0.6,
10
+ "top_k": 20,
11
+ "top_p": 0.95
12
+ }