Compare commits

..

11 Commits

Author SHA1 Message Date
dengxuan 968d800110 fix(embed): to_dtype F32 before to_vec1以适应 F16 模型
ci / cargo fmt (push) Failing after 1m45s
ci / cargo clippy (push) Failing after 4m20s
ci / build and test (push) Successful in 4m33s
2026-07-25 14:12:07 +08:00
dengxuan 2df0929440 feat(embed): MiniCPM4 embed/embed_batch + MiniCPM4GenerateModel embed_text/embed_text_batch
ci / cargo fmt (push) Failing after 2m43s
ci / cargo clippy (push) Failing after 5m9s
ci / build and test (push) Successful in 8m32s
- MiniCPMModel::embed: run forward through layers + norm, mean pool, L2 normalize
- MiniCPMModel::embed_batch: batch version
- MiniCPM4GenerateModel::embed_text: tokenize + embed + return Vec<f32>
- MiniCPM4GenerateModel::embed_text_batch: batch version
2026-07-25 13:03:25 +08:00
jhqxxx e29ddc589d update doc 2026-06-07 13:27:22 +08:00
jhqxxx b164779af4 update doc 2026-06-06 18:45:40 +08:00
jhqxxx a2d0f64c5b add PaddleOCR-VL-1.6 2026-06-06 18:41:29 +08:00
jhqxxx cb2fe57359 update weixinqun qrcode 2026-05-31 19:10:34 +08:00
jhqxxx c1b84f880e update doc 2026-05-30 19:31:43 +08:00
jhqxxx 69af1ceba5 update doc 2026-05-30 13:19:58 +08:00
jhqxxx 791f3ea7e3 generate code refactored 2026-05-29 22:10:40 +08:00
jhqxxx 257057de07 generate code refactoring progress 1/3 2026-05-28 23:21:15 +08:00
jhqxxx 0ac15554a3 update fmt 2026-05-27 15:54:02 +08:00
47 changed files with 1142 additions and 1205 deletions
Generated
+1 -1
View File
@@ -21,7 +21,7 @@ dependencies = [
[[package]]
name = "aha"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"ahash",
"anyhow",
+2 -4
View File
@@ -1,10 +1,10 @@
[package]
name = "aha"
version = "0.2.5"
version = "0.2.6"
edition = "2024"
repository = "https://github.com/jhqxxx/aha"
license = "Apache-2.0"
description = "aha model inference library, now supports Qwen(2.5VL/3/3VL/3.5/ASR/3Embedding/3Reranker), MiniCPM4, VoxCPM(0.5B/1.5/2), DeepSeek-OCR/2, Hunyuan-OCR, PaddleOCR-VL/1.5, RMBG2.0, GLM(ASR-Nano-2512/OCR), Fun-ASR-Nano-2512, LFM(2/2.5/2VL/2.5VL)"
description = "aha model inference library, now supports Qwen(2.5VL/3/3VL/3.5/ASR/3Embedding/3Reranker), MiniCPM(4/5), VoxCPM(0.5B/1.5/2), DeepSeek-OCR/2, Hunyuan-OCR, PaddleOCR-VL/1.5, RMBG2.0, GLM(ASR-Nano-2512/OCR), Fun-ASR-Nano-2512, LFM(2/2.5/2VL/2.5VL)"
[dependencies]
candle-core = { version = "0.9.2" }
@@ -21,9 +21,7 @@ base64 = "0.22.1"
num = "0.4.3"
minijinja = "2.12.0"
tokenizers = "0.22.1"
# aha_openai_dive = { version = "1.4", features = ["stream"] }
uuid = { version = "1.18.1", features = ["v4"] }
# chrono = "0.4"
rocket = { version = "0.5.1", features = ["serde_json", "json"] }
tokio = "1.47.1"
hound = "3.5.1"
+14 -12
View File
@@ -18,6 +18,10 @@
<a href="README.zh-CN.md">简体中文</a> | <strong>English</strong>
</p>
<p align="center">
<strong>Official Website:</strong> <a href="https://s5dgj23f.pub.makeit.coderbox.cn/">https://s5dgj23f.pub.makeit.coderbox.cn/</a>
</p>
# aha
**Lightweight AI Inference Engine — All-in-one Solution for Text, Vision, Speech, and OCR**
@@ -31,7 +35,7 @@ aha is a high-performance, cross-platform AI inference engine built with Rust an
|----------|--------|
| **Text** | Qwen3, MiniCPM4, MiniCPM5, LFM2, LFM2.5 |
| **Vision** | Qwen2.5-VL, Qwen3-VL, Qwen3.5, <br> LFM2.5-VL, LFM2-VL |
| **OCR** | DeepSeek-OCR, DeepSeek-OCR-2 , PaddleOCR-VL <br> PaddleOCR-VL1.5, Hunyuan-OCR, GLM-OCR |
| **OCR** | DeepSeek-OCR, DeepSeek-OCR-2 , PaddleOCR-VL <br> PaddleOCR-VL1.5, PaddleOCR-VL1.6, Hunyuan-OCR <br> GLM-OCR |
| **ASR** | GLM-ASR-Nano, Fun-ASR-Nano, Qwen3-ASR |
| **TTS** | VoxCPM, VoxCPM1.5, VoxCPM2, Moss-TTS-Nano |
| **Image** | RMBG-2.0 (background removal) |
@@ -39,6 +43,15 @@ aha is a high-performance, cross-platform AI inference engine built with Rust an
| **Reranker** | Qwen3-Reranker |
## Changelog
### 2026-06-06
- add PaddleOCR-VL-1.6
### 2026-05-29
- generate code refactored
### 2026-05-28
- generate code refactoring progress 1/3
### 2026-05-27
- add MiniCPM5
@@ -48,17 +61,6 @@ aha is a high-performance, cross-platform AI inference engine built with Rust an
### 2026-05-11
- add Moss-TTS-Nanoits performance is worse than the original Python version
### 2026-05-09
- merge pr/eastgold15/46, add aha-ui
### 2026-04-25
- VoxCPM update stream
### 2026-04-17
- Qwen3ASR add vad data recognition
### 2026-04-16
- fix FireRedVAD fsmn cache bug
**[View full changelog](docs/changelog.md)** →
+14 -13
View File
@@ -18,6 +18,10 @@
<a href="README.md">English</a> | <strong>简体中文</strong>
</p>
<p align="center">
<strong>官方网站:</strong> <a href="https://s5dgj23f.pub.makeit.coderbox.cn/">https://s5dgj23f.pub.makeit.coderbox.cn/</a>
</p>
# aha
**轻量 AI 推理引擎 —— 文本、视觉、语音与 OCR 一站式解决方案**
@@ -30,7 +34,7 @@ aha 是一款基于 Rust 和 Candle 框架构建的高性能跨平台 AI 推理
|------|------|
| **文本** | Qwen3, MiniCPM4, MiniCPM5, LFM2, LFM2.5 |
| **视觉** | Qwen2.5-VL, Qwen3-VL, Qwen3.5 <br> LFM2.5-VL, LFM2-VL |
| **OCR** | DeepSeek-OCR, DeepSeek-OCR-2, PaddleOCR-VL, <br>PaddleOCR-VL1.5, Hunyuan-OCR, GLM-OCR |
| **OCR** | DeepSeek-OCR, DeepSeek-OCR-2, PaddleOCR-VL, <br>PaddleOCR-VL1.5, PaddleOCR-VL1.6, Hunyuan-OCR <br> GLM-OCR |
| **ASR** | GLM-ASR-Nano, Fun-ASR-Nano, Qwen3-ASR |
| **TTS** | VoxCPM, VoxCPM1.5, VoxCPM2, Moss-TTS-Nano |
| **图像** | RMBG-2.0 (背景移除) |
@@ -38,6 +42,15 @@ aha 是一款基于 Rust 和 Candle 框架构建的高性能跨平台 AI 推理
| **重排序** | Qwen3-Reranker |
## 更新日志
### 2026-06-06
- 新增 PaddleOCR-VL-1.6
### 2026-05-29
- generate代码重构完成
### 2026-05-28
- generate代码重构进度 1/3
### 2026-05-27
- 新增 MiniCPM5
@@ -47,18 +60,6 @@ aha 是一款基于 Rust 和 Candle 框架构建的高性能跨平台 AI 推理
### 2026-05-11
- 添加Moss-TTS-Nano,该模型比较小,计算误差影响较大,效果比python原版差
### 2026-05-09
- 合并 pr/eastgold15/46, 添加 aha-ui
### 2026-04-25
- VoxCPM 更新流式生成
### 2026-04-17
- Qwen3ASR 增加 vad 数据识别
### 2026-04-16
- 修复 FireRedVAD fsmn 缓存问题
**[查看完整更新日志](docs/changelog.zh-CN.md)** →
Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 153 KiB

+1 -2
View File
@@ -356,8 +356,7 @@ curl http://127.0.0.1:10100/audio/speech \
{
"role": "user",
"content": [
{"type": "text", "text": "Hello, this is AHA speaking."},
{"type": "audio", "audio_url": {"url": "https://package-release.coderbox.cn/aiway/test/other/%E5%93%AA%E5%90%92.wav"}}
{"type": "text", "text": "Hello, this is AHA speaking."}
]
}
]
+1 -2
View File
@@ -359,8 +359,7 @@ curl http://127.0.0.1:10100/audio/speech \
{
"role": "user",
"content": [
{"type": "text", "text": "你好,这是 AHA 在说话。"},
{"type": "audio", "audio_url": {"url": "https://package-release.coderbox.cn/aiway/test/other/%E5%93%AA%E5%90%92.wav"}}
{"type": "text", "text": "你好,这是 AHA 在说话。"}
]
}
]
+9
View File
@@ -5,6 +5,15 @@ All notable changes to aha will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### 2026-06-06
- add PaddleOCR-VL-1.6
### 2026-05-29
- generate code refactored
### 2026-05-28
- generate code refactoring progress 1/3
### 2026-05-27
- add MiniCPM5
+9
View File
@@ -5,6 +5,15 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。
### 2026-06-06
- 新增 PaddleOCR-VL-1.6
### 2026-05-29
- generate代码重构完成
### 2026-05-28
- generate代码重构进度 1/3
### 2026-05-27
- 新增 MiniCPM5
+1 -2
View File
@@ -179,8 +179,7 @@ curl http://127.0.0.1:10100/audio/speech \
{
"role": "user",
"content": [
{"type": "text", "text": "Hello, this is AHA speaking."},
{"type": "audio", "audio_url": {"url": "https://package-release.coderbox.cn/aiway/test/other/%E5%93%AA%E5%90%92.wav"}}
{"type": "text", "text": "Hello, this is AHA speaking."}
]
}
]
+1 -2
View File
@@ -180,8 +180,7 @@ curl http://127.0.0.1:10100/audio/speech \
{
"role": "user",
"content": [
{"type": "text", "text": "你好,这是 AHA 在说话。"},
{"type": "audio", "audio_url": {"url": "https://package-release.coderbox.cn/aiway/test/other/%E5%93%AA%E5%90%92.wav"}}
{"type": "text", "text": "你好,这是 AHA 在说话。"}
]
}
]
+238 -1
View File
@@ -3,7 +3,7 @@
#### Options prompt:
"OCR:" | "Table Recognition:" | "Formula Recognition:" | "Chart Recognition:"
## PaddleOCR-VL1.5
## PaddleOCR-VL1.5/1.6
#### Options prompt:
"OCR:" | "Table Recognition:" | "Formula Recognition:" | "Chart Recognition:" | "Spotting:" | "Seal Recognition:"
@@ -75,3 +75,240 @@ Please output the information in the image in the following JSON format:
| Parsing | 1. Identify the formula in the image and represent it using LaTeX format. <br> 2.Parse the table in the image into HTML. <br> 3. Parse the chart in the image; use Mermaid format for flowcharts and Markdown for other charts. <br> 4.Extract all information from the main body of the document image and represent it in markdown format, ignoring headers and footers. Tables should be expressed in HTML format, formulas in the document should be represented using LaTeX format, and the parsing should be organized according to the reading order. | 1. 识别图片中的公式,用 LaTeX 格式表示。 <br> 2. 把图中的表格解析为 HTML。 <br> 3. 解析图中的图表,对于流程图使用 Mermaid 格式表示,其他图表使用 Markdown 格式表示。 <br> 4. 提取文档图片中正文的所有信息用 markdown 格式表示,其中页眉、页脚部分忽略,表格用 html 格式表达,文档中公式用 latex 格式表示,按照阅读顺序组织进行解析。 |
| Information Extraction | 1. Output the value of Key. <br> 2. Extract the content of the fields: ['key1','key2', ...] from the image and return it in JSON format. <br> 3. Extract the subtitles from the image. | 1. 输出 Key 的值。 <br> 2. 提取图片中的: ['key1','key2', ...] 的字段内容,并按照 JSON 格式返回。 <br> 3. 提取图片中的字幕。 |
| Translation | First extract the text, then translate the text content into English. If it is a document, ignore the header and footer. Formulas should be represented in LaTeX format, and tables should be represented in HTML format. | 先提取文字,再将文字内容翻译为英文。若是文档,则其中页眉、页脚忽略。公式用latex格式表示,表格用html格式表示。 |
Here is the continuation for the English [model-card.md](file:///home/jhq/rust_code/aha/docs/model-card.md) file, translating and adapting the content from the Chinese version regarding VoxCPM models.
# TTS
## VoxCPM (0.5B / 1.5)
#### Mode Description
* **Zero-shot TTS (Default)**: Generates speech directly without reference audio.
* **Voice Cloning**: Requires reference audio (`audio_url`) and its corresponding transcript (`prompt_text`).
* **Note**: For versions 0.5B/1.5, if `audio_url` is provided, `prompt_text` **must** be provided in `metadata`. Conversely, if there is no `audio_url`, `prompt_text` should not be provided.
#### Metadata Parameters
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `prompt_text` | String | Conditional | **Required for Cloning**. The transcript of the reference audio. Used to extract speaker characteristics. |
| `min_len` | Integer | No | Minimum generation length (tokens/steps), default `2`. |
| `max_len` | Integer | No | Maximum generation length (tokens/steps), default `4096`. |
| `inference_timesteps` | Integer | No | Number of inference steps. Affects quality and speed. Default `10`. Higher values yield better quality but slower speed. |
| `cfg_value` | Float | No | Classifier-Free Guidance value. Controls adherence to the prompt. Default `2.0`. |
| `retry_badcase_ratio_threshold` | Float | No | Threshold for retrying bad cases. Default `6.0`. |
#### Example 1: Zero-shot TTS (No Reference)
```json
{
"model": "OpenBMB/VoxCPM-0.5B",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Hello, this is a test speech."
}
]
}
]
}
```
#### Example 2: Voice Cloning (With Reference)
```json
{
"model": "OpenBMB/VoxCPM-0.5B",
"messages": [
{
"role": "user",
"content": [
{
"type": "audio",
"audio_url": {
"url": "https://example.com/reference.wav"
}
},
{
"type": "text",
"text": "This is the target text I want to generate."
}
]
}
],
"metadata": {
"prompt_text": "This is the text content corresponding to the reference audio."
}
}
```
#### Response Example
The model returns a JSON object containing Base64-encoded audio data. The audio format is WAV.
```json
{
"id": "chatcmpl-uuid...",
"object": "chat.completion",
"created": 1710000000,
"model": "OpenBMB/VoxCPM-0.5B",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": [
{
"type": "audio",
"audio_url": {
"url": "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA..."
}
}
]
},
"finish_reason": "stop"
}
],
"usage": null
}
```
## VoxCPM2
VoxCPM2 supports three advanced voice generation modes. Switch between modes by combining content types in `messages` and parameters in `metadata`.
### 1. Voice Design
Generate a completely new voice from natural language descriptions alone (gender, age, tone, emotion, pace, etc.). **No reference audio required.**
* **Input**: Text message only.
* **Metadata**: Optional `control_instruction` for finer control (e.g., emotion, pace).
#### Example:
```json
{
"model": "OpenBMB/VoxCPM2",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Welcome to the future world."
}
]
}
],
"metadata": {
"control_instruction": "Young female, energetic"
}
}
```
### 2. Controllable Cloning
Clone a voice from a short audio clip, with optional style guidance to steer emotion, pace, and expression while preserving timbre.
* **Input**: Reference audio (`audio_url`) + Target text.
* **Metadata**: Optional `control_instruction` to adjust style (emotion, pace, etc.). **`prompt_text` is NOT required.**
#### Example:
```json
{
"model": "OpenBMB/VoxCPM2",
"messages": [
{
"role": "user",
"content": [
{
"type": "audio",
"audio_url": {
"url": "https://example.com/short_clip.wav"
}
},
{
"type": "text",
"text": "This news broadcast needs to be more serious and calm."
}
]
}
],
"metadata": {
"control_instruction": "serious, slow pace"
}
}
```
### 3. Ultimate Cloning
Provide reference audio and its verbatim transcript for audio-continuation cloning or high-fidelity cloning. Every vocal nuance is faithfully reproduced.
* **Input**: Reference audio (`audio_url`) + Target text.
* **Metadata**: **Must** provide `prompt_text` (accurate transcript of the reference audio).
#### Example:
```json
{
"model": "OpenBMB/VoxCPM2",
"messages": [
{
"role": "user",
"content": [
{
"type": "audio",
"audio_url": {
"url": "https://example.com/reference.wav"
}
},
{
"type": "text",
"text": "This is the new content to be spoken next."
}
]
}
],
"metadata": {
"prompt_text": "This is the actual text spoken in the reference audio."
}
}
```
#### General Metadata Parameters (Applicable to all VoxCPM2 modes)
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `prompt_text` | String | Ultimate Cloning Only | Verbatim transcript of the reference audio. Required only for Ultimate Cloning mode. |
| `control_instruction` | String | No | Natural language instruction to control style for Voice Design or Controllable Cloning (e.g., "angry", "whispering", "fast"). |
| `min_len` | Integer | No | Minimum generation length, default `2`. |
| `max_len` | Integer | No | Maximum generation length, default `4096`. |
| `inference_timesteps` | Integer | No | Inference steps, default `10`. |
| `cfg_value` | Float | No | CFG value, default `2.0`. |
| `retry_badcase_ratio_threshold` | Float | No | Bad case retry threshold, default `6.0`. |
#### Response Example
Same as VoxCPM 0.5B/1.5, returns a JSON object containing Base64-encoded WAV audio.
```json
{
"id": "chatcmpl-uuid...",
"object": "chat.completion",
"created": 1710000000,
"model": "OpenBMB/VoxCPM2",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": [
{
"type": "audio",
"audio_url": {
"url": "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA..."
}
}
]
},
"finish_reason": "stop"
}
],
"usage": null
}
```
+234 -2
View File
@@ -3,7 +3,7 @@
#### 可选提示词:
"OCR:" | "Table Recognition:" | "Formula Recognition:" | "Chart Recognition:"
## PaddleOCR-VL1.5
## PaddleOCR-VL1.5/1.6
#### 可选提示词:
"OCR:" | "Table Recognition:" | "Formula Recognition:" | "Chart Recognition:" | "Spotting:" | "Seal Recognition:"
@@ -75,5 +75,237 @@ extract structured information from documents. Prompts must follow a strict JSON
| 信息提取 | 1. Output the value of Key. <br> 2. Extract the content of the fields: ['key1','key2', ...] from the image and return it in JSON format. <br> 3. Extract the subtitles from the image. | 1. 输出 Key 的值。 <br> 2. 提取图片中的: ['key1','key2', ...] 的字段内容,并按照 JSON 格式返回。 <br> 3. 提取图片中的字幕。 |
| 翻译 | First extract the text, then translate the text content into English. If it is a document, ignore the header and footer. Formulas should be represented in LaTeX format, and tables should be represented in HTML format. | 先提取文字,再将文字内容翻译为英文。若是文档,则其中页眉、页脚忽略。公式用latex格式表示,表格用html格式表示。 |
# TTS
## VoxCPM
## VoxCPM (0.5B / 1.5)
### 模式说明
* **Zero-shot TTS (默认模式)**: 无需参考音频,直接生成语音。
* **Voice Cloning (克隆模式)**: 需要提供参考音频 (`audio_url`) 和对应的参考文本 (`prompt_text`)。
* **注意**: 对于 0.5B/1.5 版本,如果提供了 `audio_url`,则**必须**在 `metadata` 中提供 `prompt_text`;反之,如果没有 `audio_url`,则不应提供 `prompt_text`
### Metadata 参数
| 参数名 | 类型 | 必填 | 说明 |
| :--- | :--- | :--- | :--- |
| `prompt_text` | String | 条件必填 | **克隆模式必填**。参考音频对应的转录文本。用于提取说话人特征。 |
| `min_len` | Integer | 否 | 最小生成长度 (tokens/steps),默认 `2`。 |
| `max_len` | Integer | 否 | 最大生成长度 (tokens/steps),默认 `4096`。 |
| `inference_timesteps` | Integer | 否 | 推理步数,影响生成质量和速度,默认 `10`。值越大质量越高,速度越慢。 |
| `cfg_value` | Float | 否 | Classifier-Free Guidance 值,控制对提示词的遵循程度,默认 `2.0`。 |
| `retry_badcase_ratio_threshold` | Float | 否 | 坏案例重试阈值,默认 `6.0`。 |
#### 示例 1: Zero-shot TTS (无参考)
```json
{
"model": "OpenBMB/VoxCPM-0.5B",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "你好,这是一个测试语音。"
}
]
}
]
}
```
#### 示例 2: Voice Cloning (有参考)
```json
{
"model": "OpenBMB/VoxCPM-0.5B",
"messages": [
{
"role": "user",
"content": [
{
"type": "audio",
"audio_url": {
"url": "https://example.com/reference.wav"
}
},
{
"type": "text",
"text": "这是我要生成的目标文本。"
}
]
}
],
"metadata": {
"prompt_text": "这是参考音频对应的文字内容。"
}
}
```
#### 返回结果示例
模型将返回一个包含 Base64 编码音频数据的 JSON 对象。音频格式为 WAV。
```json
{
"id": "chatcmpl-uuid...",
"object": "chat.completion",
"created": 1710000000,
"model": "OpenBMB/VoxCPM-0.5B",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": [
{
"type": "audio",
"audio_url": {
"url": "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA..."
}
}
]
},
"finish_reason": "stop"
}
],
"usage": null
}
```
## VoxCPM2
VoxCPM2 支持三种高级语音生成模式。通过组合 `messages` 中的内容类型和 `metadata` 参数来切换模式。
### 1. Voice Design (声音设计)
仅通过自然语言描述生成全新声音(性别、年龄、语气、情感、语速等),**不需要**参考音频。
* **输入**: 纯文本消息。
* **Metadata**: 可选 `control_instruction` 用于更精细的控制(如情绪、语速)。
#### 示例:
```json
{
"model": "OpenBMB/VoxCPM2",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "欢迎来到未来世界。"
}
]
}
],
"metadata": {
"control_instruction": "年轻女性,活力"
}
}
```
### 2. Controllable Cloning (可控克隆)
从短音频片段中克隆声音,并可选地通过风格指导来控制情感、语速和表达,同时保留音色。
* **输入**: 参考音频 (`audio_url`) + 目标文本。
* **Metadata**: 可选 `control_instruction` 用于调整风格(情绪、语速等)。**不需要** `prompt_text`
#### 示例:
```json
{
"model": "OpenBMB/VoxCPM2",
"messages": [
{
"role": "user",
"content": [
{
"type": "audio",
"audio_url": {
"url": "https://example.com/short_clip.wav"
}
},
{
"type": "text",
"text": "这段新闻播报需要更加严肃和沉稳。"
}
]
}
],
"metadata": {
"control_instruction": "serious, slow pace"
}
}
```
### 3. Ultimate Cloning (极致克隆)
提供参考音频及其逐字稿,进行音频续写或高保真克隆。每个声音细节都被忠实还原。
* **输入**: 参考音频 (`audio_url`) + 目标文本。
* **Metadata**: **必须**提供 `prompt_text` (参考音频的准确转录)。
#### 示例:
```json
{
"model": "OpenBMB/VoxCPM2",
"messages": [
{
"role": "user",
"content": [
{
"type": "audio",
"audio_url": {
"url": "https://example.com/reference.wav"
}
},
{
"type": "text",
"text": "这是接下来要说的新内容。"
}
]
}
],
"metadata": {
"prompt_text": "这是参考音频中实际说的文字。"
}
}
```
#### 通用 Metadata 参数 (适用于所有 VoxCPM2 模式)
| 参数名 | 类型 | 必填 | 说明 |
| :--- | :--- | :--- | :--- |
| `prompt_text` | String | 仅 Ultimate Cloning | 参考音频的逐字稿。仅在极致克隆模式下需要。 |
| `control_instruction` | String | 否 | 自然语言指令,用于控制 Voice Design 或 Controllable Cloning 的风格(如 "angry", "whispering", "fast")。 |
| `min_len` | Integer | 否 | 最小生成长度,默认 `2`。 |
| `max_len` | Integer | 否 | 最大生成长度,默认 `4096`。 |
| `inference_timesteps` | Integer | 否 | 推理步数,默认 `10`。 |
| `cfg_value` | Float | 否 | CFG 值,默认 `2.0`。 |
| `retry_badcase_ratio_threshold` | Float | 否 | 坏案例重试阈值,默认 `6.0`。 |
#### 返回结果示例
与 VoxCPM 0.5B/1.5 相同,返回包含 Base64 编码 WAV 音频的 JSON 对象。
```json
{
"id": "chatcmpl-uuid...",
"object": "chat.completion",
"created": 1710000000,
"model": "OpenBMB/VoxCPM2",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": [
{
"type": "audio",
"audio_url": {
"url": "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA..."
}
}
]
},
"finish_reason": "stop"
}
],
"usage": null
}
```
+3 -2
View File
@@ -40,7 +40,8 @@ deepseek-ai/DeepSeek-OCR deepseek-ai ocr ✔
deepseek-ai/DeepSeek-OCR-2 deepseek-ai ocr
Tencent-Hunyuan/HunyuanOCR Tencent-Hunyuan ocr ✔
PaddlePaddle/PaddleOCR-VL PaddlePaddle ocr ✔
PaddlePaddle/PaddleOCR-VL-1.5 PaddlePaddle ocr
PaddlePaddle/PaddleOCR-VL-1.5 PaddlePaddle ocr
PaddlePaddle/PaddleOCR-VL-1.6 PaddlePaddle ocr ✔
AI-ModelScope/RMBG-2.0 AI-ModelScope image ✔
OpenBMB/VoxCPM-0.5B OpenBMB tts ✔
OpenBMB/VoxCPM1.5 OpenBMB tts ✔
@@ -88,7 +89,7 @@ ZhipuAI/GLM-OCR ZhipuAI ocr ✔
| Model | Languages | Model Id | License |
|-------|-----------|--------|---------|
| **PaddleOCR-VL** | Multi | PaddlePaddle/PaddleOCR-VL <br> PaddlePaddle/PaddleOCR-VL-1.5 | [Apache 2.0](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md) |
| **PaddleOCR-VL** | Multi | PaddlePaddle/PaddleOCR-VL <br> PaddlePaddle/PaddleOCR-VL-1.5 <br> PaddlePaddle/PaddleOCR-VL-1.6 | [Apache 2.0](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md) |
| **Hunyuan-OCR** | Chinese | Tencent-Hunyuan/HunyuanOCR | [Tencent Hunyuan Community License](https://huggingface.co/tencent/HunyuanOCR/blob/main/LICENSE) |
| **DeepSeek-OCR** | Multi | deepseek-ai/DeepSeek-OCR <br> deepseek-ai/DeepSeek-OCR-2 | [MIT](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/mit.md) |
| **GLM-OCR** | 8 | ZhipuAI/GLM-OCR | [MIT](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/mit.md) |
+3 -2
View File
@@ -40,7 +40,8 @@ deepseek-ai/DeepSeek-OCR deepseek-ai ocr ✔
deepseek-ai/DeepSeek-OCR-2 deepseek-ai ocr
Tencent-Hunyuan/HunyuanOCR Tencent-Hunyuan ocr ✔
PaddlePaddle/PaddleOCR-VL PaddlePaddle ocr ✔
PaddlePaddle/PaddleOCR-VL-1.5 PaddlePaddle ocr
PaddlePaddle/PaddleOCR-VL-1.5 PaddlePaddle ocr
PaddlePaddle/PaddleOCR-VL-1.6 PaddlePaddle ocr ✔
AI-ModelScope/RMBG-2.0 AI-ModelScope image ✔
OpenBMB/VoxCPM-0.5B OpenBMB tts ✔
OpenBMB/VoxCPM1.5 OpenBMB tts ✔
@@ -86,7 +87,7 @@ ZhipuAI/GLM-OCR ZhipuAI ocr ✔
| 模型 | 语言 | 模型id | 开源协议 |
|------|------|------|---------|
| **PaddleOCR-VL** | 多语言 | PaddlePaddle/PaddleOCR-VL <br> PaddlePaddle/PaddleOCR-VL-1.5 | [Apache 2.0](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md) |
| **PaddleOCR-VL** | 多语言 | PaddlePaddle/PaddleOCR-VL <br> PaddlePaddle/PaddleOCR-VL-1.5 <br> PaddlePaddle/PaddleOCR-VL-1.6 | [Apache 2.0](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md) |
| **Hunyuan-OCR** | 中文 | Tencent-Hunyuan/HunyuanOCR | [腾讯混元社区许可协议](https://huggingface.co/tencent/HunyuanOCR/blob/main/LICENSE) |
| **DeepSeek-OCR** | 多语言 | deepseek-ai/DeepSeek-OCR <br> deepseek-ai/DeepSeek-OCR-2 | [MIT](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/mit.md) |
| **GLM-OCR** | 8 | ZhipuAI/GLM-OCR | [MIT](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/mit.md) |
+1 -1
View File
@@ -298,7 +298,7 @@ pub(crate) fn run_run(args: RunArgs) -> anyhow::Result<()> {
WhichModel::HunyuanOCR => {
hunyuan_ocr::HunyuanORExec::run(&input, output.as_deref(), &weight_path)?;
}
WhichModel::PaddleOCRVL | WhichModel::PaddleOCRVL1_5 => {
WhichModel::PaddleOCRVL | WhichModel::PaddleOCRVL1_5 | WhichModel::PaddleOCRVL1_6 => {
paddleocr_vl::PaddleOVLExec::run(&input, output.as_deref(), &weight_path)?;
}
WhichModel::RMBG2_0 => {
+114 -1
View File
@@ -10,7 +10,7 @@ use crate::{
InferenceModel, MultiModalData,
sample::{get_logit_processor, use_repeat_penalty},
},
params::chat::{ChatCompletionChunkResponse, ChatCompletionResponse},
params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse},
tokenizer::TokenizerModel,
utils::response_utils::{
build_chunk_response_with_reasoning, build_chunk_response_with_usage,
@@ -366,3 +366,116 @@ pub fn generate_stream_generic<M: InferenceModel>(
};
Ok(stream)
}
pub struct PrepareData {
pub in_reasoning: bool,
pub input_ids: Tensor,
pub multi_model_data: MultiModalData,
}
pub trait GenerationDataProvider {
fn get_temperature(&self, req_temp: Option<f32>) -> Option<f32> {
req_temp
}
fn get_top_p(&self, req_top_p: Option<f32>) -> Option<f32> {
req_top_p
}
fn get_top_k(&self, top_k: Option<usize>) -> Option<usize> {
top_k
}
fn is_in_reasoning(&self, text: &str) -> bool {
text.ends_with("<think>\n")
}
fn get_multi_model_data(&self) -> MultiModalData {
MultiModalData::new(vec![])
}
fn get_data(&self, mes: &ChatCompletionParameters) -> Result<PrepareData>;
}
#[macro_export]
macro_rules! impl_generate_model {
($struct_name: ty) => {
impl<'a> $crate::models::GenerateModel for $struct_name {
fn generate(
&mut self,
mes: $crate::params::chat::ChatCompletionParameters,
) -> anyhow::Result<$crate::params::chat::ChatCompletionResponse> {
let seed = mes.seed.unwrap_or(299792458) as u64;
let sample_len = mes.max_tokens.unwrap_or(1024);
let temperature = self.get_temperature(mes.temperature);
let top_p = self.get_top_p(mes.top_p);
let top_k = self.get_top_k(mes.top_k);
let prepare_data = self.get_data(&mes)?;
let input_ids = prepare_data.input_ids;
let data = prepare_data.multi_model_data;
let mut ctx = $crate::models::common::generate::GenerationContext::new(
temperature,
top_p,
top_k,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
$crate::models::common::generate::generate_generic(
&mut self.model,
&self.tokenizer,
input_ids,
data,
&mut ctx,
&self.model_name,
)
}
fn generate_stream(
&mut self,
mes: $crate::params::chat::ChatCompletionParameters,
) -> anyhow::Result<
Box<
dyn rocket::futures::Stream<
Item = anyhow::Result<
$crate::params::chat::ChatCompletionChunkResponse,
>,
> + Send
+ Unpin
+ '_,
>,
> {
let seed = mes.seed.unwrap_or(299792458) as u64;
let prepare_data = self.get_data(&mes)?;
let input_ids = prepare_data.input_ids;
let data = prepare_data.multi_model_data;
let in_reasoning = prepare_data.in_reasoning;
let sample_len = mes.max_tokens.unwrap_or(1024);
let temperature = self.get_temperature(mes.temperature);
let top_p = self.get_top_p(mes.top_p);
let top_k = self.get_top_k(mes.top_k);
let stream = $crate::models::common::generate::generate_stream_generic(
&mut self.model,
&self.tokenizer,
input_ids,
data,
temperature,
top_p,
top_k,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
in_reasoning,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
}
}
};
}
+4 -1
View File
@@ -72,6 +72,8 @@ pub enum WhichModel {
PaddleOCRVL,
#[value(name = "PaddlePaddle/PaddleOCR-VL-1.5")]
PaddleOCRVL1_5,
#[value(name = "PaddlePaddle/PaddleOCR-VL-1.6")]
PaddleOCRVL1_6,
#[value(name = "AI-ModelScope/RMBG-2.0")]
RMBG2_0,
#[value(name = "OpenBMB/VoxCPM-0.5B")]
@@ -165,7 +167,8 @@ impl WhichModel {
| WhichModel::HunyuanOCR
| WhichModel::GlmOCR
| WhichModel::PaddleOCRVL
| WhichModel::PaddleOCRVL1_5 => "ocr",
| WhichModel::PaddleOCRVL1_5
| WhichModel::PaddleOCRVL1_6 => "ocr",
// ASR models
WhichModel::Qwen3ASR0_6B
| WhichModel::Qwen3ASR1_7B
+18 -104
View File
@@ -1,21 +1,14 @@
use crate::{
models::common::{
MultiModalData,
generate::{GenerationContext, generate_generic, generate_stream_generic},
},
params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse},
use crate::models::common::{
MultiModalData,
generate::{GenerationDataProvider, PrepareData},
};
use anyhow::Result;
use candle_core::{DType, Device};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::{
models::{
GenerateModel,
deepseek_ocr::{
config::DeepseekOCRConfig, model::DeepseekOCRModel, processor::DeepseekOCRProcessor,
},
models::deepseek_ocr::{
config::DeepseekOCRConfig, model::DeepseekOCRModel, processor::DeepseekOCRProcessor,
},
tokenizer::TokenizerModel,
utils::{extract_metadata_value, find_type_files, get_device, get_dtype},
@@ -24,9 +17,7 @@ use crate::{
pub struct DeepseekOCRGenerateModel {
tokenizer: TokenizerModel,
processor: DeepseekOCRProcessor,
deepseekocr_model: DeepseekOCRModel,
// bos_token_id: u32,
// eos_token_id: u32,
model: DeepseekOCRModel,
device: Device,
size: Vec<u32>,
model_name: String,
@@ -52,19 +43,15 @@ impl DeepseekOCRGenerateModel {
1usize
};
let processor = DeepseekOCRProcessor::new(device, dtype, version)?;
// let eos_token_id = cfg.eos_token_id;
// let bos_token_id = cfg.bos_token_id;
let model_list = find_type_files(path, "safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, device)? };
let deepseekocr_model = DeepseekOCRModel::new(vb, cfg, version)?;
let model = DeepseekOCRModel::new(vb, cfg, version)?;
let size = vec![512u32, 640, 1024, 1280];
Ok(Self {
tokenizer,
processor,
deepseekocr_model,
// bos_token_id,
// eos_token_id,
model,
device: device.clone(),
size,
model_name: model_name.to_string(),
@@ -73,8 +60,8 @@ impl DeepseekOCRGenerateModel {
}
}
impl GenerateModel for DeepseekOCRGenerateModel {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
impl GenerationDataProvider for DeepseekOCRGenerateModel {
fn get_data(&self, mes: &crate::params::chat::ChatCompletionParameters) -> Result<PrepareData> {
let base_size = extract_metadata_value::<u32>(&mes.metadata, "base_size").unwrap_or(640);
let base_size = if self.size.contains(&base_size) {
base_size
@@ -92,93 +79,20 @@ impl GenerateModel for DeepseekOCRGenerateModel {
let crop_mode = extract_metadata_value::<bool>(&mes.metadata, "crop_mode").unwrap_or(false);
let (input_ids, images_ori, image_crop, images_seq_mask, images_spatial_crop_t) = self
.processor
.process_info(&mes, &self.tokenizer, base_size, image_size, crop_mode)?;
let max_tokens = mes.max_tokens.unwrap_or(1024);
let mut ctx = GenerationContext::new(
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
mes.seed.unwrap_or(34562) as u64,
input_ids.dim(1)?,
max_tokens,
self.device.clone(),
);
.process_info(mes, &self.tokenizer, base_size, image_size, crop_mode)?;
let data_vec = vec![
Some(images_ori),
Some(image_crop),
Some(images_seq_mask),
Some(images_spatial_crop_t),
];
let data = MultiModalData::new(data_vec);
generate_generic(
&mut self.deepseekocr_model,
&self.tokenizer,
let multi_model_data = MultiModalData::new(data_vec);
Ok(PrepareData {
in_reasoning: false,
input_ids,
data,
&mut ctx,
&self.model_name,
)
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let base_size = extract_metadata_value::<u32>(&mes.metadata, "base_size").unwrap_or(640);
let base_size = if self.size.contains(&base_size) {
base_size
} else {
640
};
let image_size = extract_metadata_value::<u32>(&mes.metadata, "image_size").unwrap_or(640);
let image_size = if self.size.contains(&image_size) {
image_size
} else {
640
};
let base_size = if self.version == 2 { 1024 } else { base_size };
let image_size = if self.version == 2 { 768 } else { image_size };
let crop_mode = extract_metadata_value::<bool>(&mes.metadata, "crop_mode").unwrap_or(false);
let (input_ids, images_ori, image_crop, images_seq_mask, images_spatial_crop_t) = self
.processor
.process_info(&mes, &self.tokenizer, base_size, image_size, crop_mode)?;
let data_vec = vec![
images_ori.into(),
image_crop.into(),
images_seq_mask.into(),
images_spatial_crop_t.into(),
];
let data = MultiModalData::new(data_vec);
let temperature = mes.temperature;
let top_p = mes.top_p;
let seed = mes.seed.unwrap_or(34562) as u64;
let max_tokens = mes.max_tokens.unwrap_or(1024);
let stream = generate_stream_generic(
&mut self.deepseekocr_model,
&self.tokenizer,
input_ids,
data,
temperature,
top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
max_tokens,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
multi_model_data,
})
}
}
crate::impl_generate_model!(DeepseekOCRGenerateModel);
+25 -76
View File
@@ -3,18 +3,16 @@ use std::collections::HashMap;
use crate::{
models::common::{
MultiModalData,
generate::{GenerationContext, generate_generic, generate_stream_generic},
generate::{GenerationDataProvider, PrepareData},
},
params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse},
params::chat::ChatCompletionParameters,
};
use anyhow::{Result, anyhow};
use candle_core::{DType, Device, pickle::read_all_with_key};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::{
models::{
GenerateModel,
fun_asr_nano::{
config::FunASRNanoConfig, model::FunAsrNanoModel, processor::FunAsrNanoProcessor,
},
@@ -27,7 +25,7 @@ use crate::{
pub struct FunAsrNanoGenerateModel {
tokenizer: TokenizerModel,
processor: FunAsrNanoProcessor,
fun_asr_nano: FunAsrNanoModel,
model: FunAsrNanoModel,
device: Device,
dtype: DType,
generation_config: Qwen3GenerationConfig,
@@ -75,7 +73,7 @@ impl FunAsrNanoGenerateModel {
}
}
let vb = VarBuilder::from_tensors(dict_to_hashmap, dtype, &device);
let fun_asr_nano =
let model =
FunAsrNanoModel::new(vb, &cfg, &llm_cfg, generation_config.eos_token_id.clone())?;
let model_name = std::path::Path::new(path)
.file_name()
@@ -85,7 +83,7 @@ impl FunAsrNanoGenerateModel {
Ok(Self {
tokenizer,
processor,
fun_asr_nano,
model,
device,
dtype,
generation_config,
@@ -94,79 +92,30 @@ impl FunAsrNanoGenerateModel {
}
}
impl GenerateModel for FunAsrNanoGenerateModel {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let temperature = mes
.temperature
.unwrap_or(self.generation_config.temperature);
let top_p = mes.top_p.unwrap_or(self.generation_config.top_p);
let top_k = self.generation_config.top_k;
let seed = mes.seed.unwrap_or(34562) as u64;
let max_tokens = mes.max_tokens.unwrap_or(1024);
let (speech, fbank_mask, input_ids) = self.processor.process_info(&mes, &self.tokenizer)?;
let speech = speech.to_dtype(self.dtype)?;
let mut ctx = GenerationContext::new(
temperature.into(),
top_p.into(),
top_k.into(),
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
max_tokens,
self.device.clone(),
);
let data_vec = vec![speech.into(), fbank_mask.into()];
let data = MultiModalData::new(data_vec);
generate_generic(
&mut self.fun_asr_nano,
&self.tokenizer,
input_ids,
data,
&mut ctx,
&self.model_name,
)
impl GenerationDataProvider for FunAsrNanoGenerateModel {
fn get_temperature(&self, req_temp: Option<f32>) -> Option<f32> {
Some(req_temp.unwrap_or(self.generation_config.temperature))
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let temperature = mes
.temperature
.unwrap_or(self.generation_config.temperature);
let top_p = mes.top_p.unwrap_or(self.generation_config.top_p);
let top_k = self.generation_config.top_k;
let seed = mes.seed.unwrap_or(34562) as u64;
let max_tokens = mes.max_tokens.unwrap_or(1024);
let (speech, fbank_mask, input_ids) = self.processor.process_info(&mes, &self.tokenizer)?;
fn get_top_p(&self, req_top_p: Option<f32>) -> Option<f32> {
Some(req_top_p.unwrap_or(self.generation_config.top_p))
}
fn get_top_k(&self, top_k: Option<usize>) -> Option<usize> {
Some(top_k.unwrap_or(self.generation_config.top_k))
}
fn get_data(&self, mes: &ChatCompletionParameters) -> Result<PrepareData> {
let (speech, fbank_mask, input_ids) = self.processor.process_info(mes, &self.tokenizer)?;
let speech = speech.to_dtype(self.dtype)?;
let data_vec = vec![speech.into(), fbank_mask.into()];
let data = MultiModalData::new(data_vec);
let stream = generate_stream_generic(
&mut self.fun_asr_nano,
&self.tokenizer,
let multi_model_data = MultiModalData::new(data_vec);
Ok(PrepareData {
in_reasoning: false,
input_ids,
data,
temperature.into(),
top_p.into(),
top_k.into(),
mes.repeat_penalty,
mes.repeat_last_n,
seed,
max_tokens,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
multi_model_data,
})
}
}
crate::impl_generate_model!(FunAsrNanoGenerateModel);
+18 -77
View File
@@ -1,22 +1,18 @@
use crate::{
models::common::{
MultiModalData,
generate::{GenerationContext, generate_generic, generate_stream_generic},
generate::{GenerationDataProvider, PrepareData},
},
params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse},
params::chat::ChatCompletionParameters,
};
use anyhow::Result;
use candle_core::{DType, Device, Tensor};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::{
chat_template::ChatTemplate,
models::{
GenerateModel,
glm_asr_nano::{
config::GlmAsrNanoConfig, model::GlmAsrNanoModel, processor::GlmAsrNanoProcessor,
},
models::glm_asr_nano::{
config::GlmAsrNanoConfig, model::GlmAsrNanoModel, processor::GlmAsrNanoProcessor,
},
tokenizer::TokenizerModel,
utils::{find_type_files, get_device, get_dtype},
@@ -26,7 +22,7 @@ pub struct GlmAsrNanoGenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
processor: GlmAsrNanoProcessor,
glm_asr_nano: GlmAsrNanoModel,
model: GlmAsrNanoModel,
device: Device,
dtype: DType,
model_name: String,
@@ -45,7 +41,7 @@ impl<'a> GlmAsrNanoGenerateModel<'a> {
let model_list = find_type_files(path, "safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, &device)? };
let eos_ids = vec![59246u32, 59253, 59255];
let glm_asr_nano = GlmAsrNanoModel::new(vb, cfg, eos_ids)?;
let model = GlmAsrNanoModel::new(vb, cfg, eos_ids)?;
let model_name = std::path::Path::new(path)
.file_name()
.and_then(|s| s.to_str())
@@ -55,7 +51,7 @@ impl<'a> GlmAsrNanoGenerateModel<'a> {
chat_template,
tokenizer,
processor,
glm_asr_nano,
model,
device,
dtype,
model_name,
@@ -63,77 +59,22 @@ impl<'a> GlmAsrNanoGenerateModel<'a> {
}
}
impl<'a> GenerateModel for GlmAsrNanoGenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let seed = mes.seed.unwrap_or(34562) as u64;
let render_text: String = self.chat_template.apply_chat_template(&mes)?;
impl<'a> GenerationDataProvider for GlmAsrNanoGenerateModel<'a> {
fn get_data(&self, mes: &ChatCompletionParameters) -> Result<PrepareData> {
let render_text: String = self.chat_template.apply_chat_template(mes)?;
let (input_features, audio_token_lengths, replace_text) =
self.processor.process_info(&mes, &render_text)?;
self.processor.process_info(mes, &render_text)?;
let input_ids = self.tokenizer.text_encode(replace_text, &self.device)?;
let input_features = input_features.to_dtype(self.dtype)?;
let audio_token_lengths = Tensor::new(audio_token_lengths, &self.device)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let mut ctx = GenerationContext::new(
mes.temperature,
mes.top_p,
mes.top_k,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data_vec = vec![input_features.into(), audio_token_lengths.into()];
let data = MultiModalData::new(data_vec);
generate_generic(
&mut self.glm_asr_nano,
&self.tokenizer,
let multi_model_data = MultiModalData::new(data_vec);
Ok(PrepareData {
in_reasoning: false,
input_ids,
data,
&mut ctx,
&self.model_name,
)
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let seed = mes.seed.unwrap_or(34562) as u64;
let render_text = self.chat_template.apply_chat_template(&mes)?;
let (input_features, audio_token_lengths, replace_text) =
self.processor.process_info(&mes, &render_text)?;
let input_ids = self.tokenizer.text_encode(replace_text, &self.device)?;
let input_features = input_features.to_dtype(self.dtype)?;
let audio_token_lengths = Tensor::new(audio_token_lengths, &self.device)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let data_vec = vec![input_features.into(), audio_token_lengths.into()];
let data = MultiModalData::new(data_vec);
let stream = generate_stream_generic(
&mut self.glm_asr_nano,
&self.tokenizer,
input_ids,
data,
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
multi_model_data,
})
}
}
crate::impl_generate_model!(GlmAsrNanoGenerateModel<'a>);
+20 -108
View File
@@ -2,33 +2,26 @@
use crate::{
models::common::{
MultiModalData,
generate::{GenerationContext, generate_generic, generate_stream_generic},
generate::{GenerationDataProvider, PrepareData},
},
params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse},
params::chat::ChatCompletionParameters,
};
use anyhow::{Result, anyhow};
use candle_core::{DType, Device};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::{
// chat_template::ChatTemplate,
models::{
GenerateModel,
glm_ocr::{
config::{GlmOcrConfig, GlmOcrGenerationConfig},
model::GlmOcrModel,
processor::GlmOcrProcessor,
},
models::glm_ocr::{
config::{GlmOcrConfig, GlmOcrGenerationConfig},
model::GlmOcrModel,
processor::GlmOcrProcessor,
},
tokenizer::TokenizerModel,
utils::{
extract_user_text, find_type_files, get_device, get_dtype, img_utils::extract_image_url,
},
};
use anyhow::{Result, anyhow};
use candle_core::{DType, Device};
use candle_nn::VarBuilder;
pub struct GlmOcrGenerateModel {
// chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
processor: GlmOcrProcessor,
model: GlmOcrModel,
@@ -44,7 +37,6 @@ pub struct GlmOcrGenerateModel {
impl GlmOcrGenerateModel {
pub fn init(path: &str, device: Option<&Device>, dtype: Option<DType>) -> Result<Self> {
// let chat_template = ChatTemplate::init(path)?;
let tokenizer = TokenizerModel::init(path)?;
let config_path = path.to_string() + "/config.json";
let cfg: GlmOcrConfig = serde_json::from_slice(&std::fs::read(config_path)?)?;
@@ -64,7 +56,6 @@ impl GlmOcrGenerateModel {
.unwrap_or("ZhipuAI/GLM-OCR")
.to_string();
Ok(Self {
// chat_template,
tokenizer,
processor,
model,
@@ -80,17 +71,15 @@ impl GlmOcrGenerateModel {
}
}
impl GenerateModel for GlmOcrGenerateModel {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let seed = mes.seed.unwrap_or(34562) as u64;
// Extract image path and prompt from messages
let image_urls = extract_image_url(&mes);
impl GenerationDataProvider for GlmOcrGenerateModel {
fn get_data(&self, mes: &ChatCompletionParameters) -> Result<PrepareData> {
let image_urls = extract_image_url(mes);
let image_path = image_urls
.first()
.ok_or_else(|| anyhow!("No image provided"))?;
// Get prompt text from messages
let mut prompt = extract_user_text(&mes)?;
let mut prompt = extract_user_text(mes)?;
if prompt.chars().count() == 0 {
prompt = "Extract all text from this image.".to_string()
}
@@ -108,95 +97,18 @@ impl GenerateModel for GlmOcrGenerateModel {
)?;
let input_ids = processed.input_ids;
let sample_len = mes.max_tokens.unwrap_or(1024);
let mut ctx = GenerationContext::new(
mes.temperature,
mes.top_p,
mes.top_k,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data_vec = vec![
processed.pixel_values.into(),
processed.grid_thw.into(),
processed.image_mask.into(),
];
let data = MultiModalData::new(data_vec);
generate_generic(
&mut self.model,
&self.tokenizer,
let multi_model_data = MultiModalData::new(data_vec);
Ok(PrepareData {
in_reasoning: false,
input_ids,
data,
&mut ctx,
&self.model_name,
)
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let seed = mes.seed.unwrap_or(34562) as u64;
// Extract image path and prompt from messages
let image_urls = extract_image_url(&mes);
let image_path = image_urls
.first()
.ok_or_else(|| anyhow!("No image provided"))?;
// Get prompt text from messages
let mut prompt = extract_user_text(&mes)?;
if prompt.chars().count() == 0 {
prompt = "Extract all text from this image.".to_string()
}
let processed = self.processor.process_info(
image_path,
&prompt,
&self.tokenizer,
self.image_token_id,
self.image_start_token_id,
self.image_end_token_id,
self.patch_size,
self.temporal_patch_size,
self.spatial_merge_size,
)?;
let input_ids = processed.input_ids;
let sample_len = mes.max_tokens.unwrap_or(1024);
let data_vec = vec![
processed.pixel_values.into(),
processed.grid_thw.into(),
processed.image_mask.into(),
];
let data = MultiModalData::new(data_vec);
let stream = generate_stream_generic(
&mut self.model,
&self.tokenizer,
input_ids,
data,
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
multi_model_data,
})
}
}
crate::impl_generate_model!(GlmOcrGenerateModel);
+30 -93
View File
@@ -1,24 +1,20 @@
use crate::{
models::common::{
MultiModalData,
generate::{GenerationContext, generate_generic, generate_stream_generic},
generate::{GenerationDataProvider, PrepareData},
},
params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse},
params::chat::ChatCompletionParameters,
};
use anyhow::Result;
use candle_core::{DType, Device};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::{
chat_template::ChatTemplate,
models::{
GenerateModel,
hunyuan_ocr::{
config::{HunYuanVLConfig, HunyuanOCRGenerationConfig},
model::HunyuanVLModel,
processor::HunyuanVLProcessor,
},
models::hunyuan_ocr::{
config::{HunYuanVLConfig, HunyuanOCRGenerationConfig},
model::HunyuanVLModel,
processor::HunyuanVLProcessor,
},
tokenizer::TokenizerModel,
utils::{find_type_files, get_device, get_dtype},
@@ -28,7 +24,7 @@ pub struct HunyuanOCRGenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
pre_processor: HunyuanVLProcessor,
hunyuan_vl: HunyuanVLModel,
model: HunyuanVLModel,
device: Device,
generation_config: HunyuanOCRGenerationConfig,
model_name: String,
@@ -49,8 +45,7 @@ impl<'a> HunyuanOCRGenerateModel<'a> {
let generation_config_path = path.to_string() + "/generation_config.json";
let generation_config: HunyuanOCRGenerationConfig =
serde_json::from_slice(&std::fs::read(generation_config_path)?)?;
let hunyuan_vl =
HunyuanVLModel::new(vb, cfg.clone(), generation_config.eos_token_id.clone())?;
let model = HunyuanVLModel::new(vb, cfg.clone(), generation_config.eos_token_id.clone())?;
let model_name = std::path::Path::new(path)
.file_name()
@@ -61,7 +56,7 @@ impl<'a> HunyuanOCRGenerateModel<'a> {
chat_template,
tokenizer,
pre_processor,
hunyuan_vl,
model,
device,
generation_config,
model_name,
@@ -69,72 +64,24 @@ impl<'a> HunyuanOCRGenerateModel<'a> {
}
}
impl<'a> GenerateModel for HunyuanOCRGenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let temperature = mes
.temperature
.unwrap_or(self.generation_config.temperature);
let top_p = mes.top_p.unwrap_or(self.generation_config.top_p);
let top_k = self.generation_config.top_k;
let seed = mes.seed.unwrap_or(34562) as u64;
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let data = self
.pre_processor
.process_info(&mes, &self.tokenizer, &mes_render)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let input_ids = data.input_ids;
let mut ctx = GenerationContext::new(
temperature.into(),
top_p.into(),
top_k.into(),
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data_vec = vec![
data.pixel_values,
data.image_grid_thw,
data.image_mask.into(),
data.position_ids.into(),
];
let data = MultiModalData::new(data_vec);
generate_generic(
&mut self.hunyuan_vl,
&self.tokenizer,
input_ids,
data,
&mut ctx,
&self.model_name,
)
impl<'a> GenerationDataProvider for HunyuanOCRGenerateModel<'a> {
fn get_temperature(&self, req_temp: Option<f32>) -> Option<f32> {
Some(req_temp.unwrap_or(self.generation_config.temperature))
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let temperature = mes
.temperature
.unwrap_or(self.generation_config.temperature);
let top_p = mes.top_p.unwrap_or(self.generation_config.top_p);
let top_k = self.generation_config.top_k;
let seed = mes.seed.unwrap_or(34562) as u64;
let mes_render = self.chat_template.apply_chat_template(&mes)?;
fn get_top_p(&self, req_top_p: Option<f32>) -> Option<f32> {
Some(req_top_p.unwrap_or(self.generation_config.top_p))
}
fn get_top_k(&self, top_k: Option<usize>) -> Option<usize> {
Some(top_k.unwrap_or(self.generation_config.top_k))
}
fn get_data(&self, mes: &ChatCompletionParameters) -> Result<PrepareData> {
let mes_render = self.chat_template.apply_chat_template(mes)?;
let data = self
.pre_processor
.process_info(&mes, &self.tokenizer, &mes_render)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
.process_info(mes, &self.tokenizer, &mes_render)?;
let input_ids = data.input_ids;
let data_vec = vec![
data.pixel_values,
@@ -142,23 +89,13 @@ impl<'a> GenerateModel for HunyuanOCRGenerateModel<'a> {
data.image_mask.into(),
data.position_ids.into(),
];
let data = MultiModalData::new(data_vec);
let stream = generate_stream_generic(
&mut self.hunyuan_vl,
&self.tokenizer,
let multi_model_data = MultiModalData::new(data_vec);
Ok(PrepareData {
in_reasoning: false,
input_ids,
data,
temperature.into(),
top_p.into(),
top_k.into(),
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
multi_model_data,
})
}
}
crate::impl_generate_model!(HunyuanOCRGenerateModel<'a>);
+15 -72
View File
@@ -1,16 +1,9 @@
use crate::models::common::MultiModalData;
use crate::models::common::generate::{
GenerationContext, generate_generic, generate_stream_generic,
};
use crate::params::chat::{ChatCompletionParameters, ChatCompletionResponse};
use crate::models::common::generate::{GenerationDataProvider, PrepareData};
use crate::{
chat_template::ChatTemplate,
models::{
GenerateModel,
lfm2::{
config::{Lfm2Config, Lfm2GenerateConfig},
model::Lfm2Model,
},
models::lfm2::{
config::{Lfm2Config, Lfm2GenerateConfig},
model::Lfm2Model,
},
tokenizer::TokenizerModel,
utils::{find_type_files, get_device, get_dtype},
@@ -62,68 +55,18 @@ impl<'a> Lfm2GenerateModel<'a> {
}
}
impl<'a> GenerateModel for Lfm2GenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
impl<'a> GenerationDataProvider for Lfm2GenerateModel<'a> {
fn get_data(&self, mes: &crate::params::chat::ChatCompletionParameters) -> Result<PrepareData> {
let mes_render = self.chat_template.apply_chat_template(mes)?;
let in_reasoning = self.is_in_reasoning(&mes_render);
let input_ids = self.tokenizer.text_encode(mes_render, &self.device)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let seed = mes.seed.unwrap_or(34562) as u64;
let mut ctx = GenerationContext::new(
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data = MultiModalData::new(vec![]);
generate_generic(
&mut self.model,
&self.tokenizer,
let multi_model_data = self.get_multi_model_data();
Ok(PrepareData {
in_reasoning,
input_ids,
data,
&mut ctx,
&self.model_name,
)
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn rocket::futures::Stream<
Item = Result<crate::params::chat::ChatCompletionChunkResponse, anyhow::Error>,
> + Send
+ Unpin
+ '_,
>,
> {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let input_ids = self.tokenizer.text_encode(mes_render, &self.device)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let data = MultiModalData::new(vec![]);
let seed = mes.seed.unwrap_or(34562) as u64;
let stream = generate_stream_generic(
&mut self.model,
&self.tokenizer,
input_ids,
data,
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
multi_model_data,
})
}
}
crate::impl_generate_model!(Lfm2GenerateModel<'a>);
+13 -72
View File
@@ -1,9 +1,9 @@
use crate::{
models::common::{
MultiModalData,
generate::{GenerationContext, generate_generic, generate_stream_generic},
generate::{GenerationDataProvider, PrepareData},
},
params::chat::{ChatCompletionParameters, ChatCompletionResponse},
params::chat::ChatCompletionParameters,
};
use anyhow::Result;
use candle_core::{DType, Device};
@@ -12,7 +12,6 @@ use candle_nn::VarBuilder;
use crate::{
chat_template::ChatTemplate,
models::{
GenerateModel,
lfm2::config::Lfm2GenerateConfig,
lfm2vl::{config::Lfm2VLConfig, model::Lfm2VLModel, processor::Lfm2VLProcessor},
},
@@ -60,82 +59,24 @@ impl<'a> Lfm2VLGenerateModel<'a> {
}
}
impl<'a> GenerateModel for Lfm2VLGenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
impl<'a> GenerationDataProvider for Lfm2VLGenerateModel<'a> {
fn get_data(&self, mes: &ChatCompletionParameters) -> Result<PrepareData> {
let mes_render = self.chat_template.apply_chat_template(mes)?;
let (pixel_values, pixel_attention_mask, spatial_shapes, text) =
self.processor.process_info(&mes, &mes_render)?;
self.processor.process_info(mes, &mes_render)?;
let input_ids = self.tokenizer.text_encode(text, &self.device)?;
let seed = mes.seed.unwrap_or(34562) as u64;
let sample_len = mes.max_tokens.unwrap_or(1024);
let mut ctx = GenerationContext::new(
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data_vec = vec![
pixel_values.into(),
pixel_attention_mask.into(),
spatial_shapes.into(),
];
let data = MultiModalData::new(data_vec);
generate_generic(
&mut self.model,
&self.tokenizer,
let multi_model_data = MultiModalData::new(data_vec);
Ok(PrepareData {
in_reasoning: false,
input_ids,
data,
&mut ctx,
&self.model_name,
)
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn rocket::futures::Stream<
Item = Result<crate::params::chat::ChatCompletionChunkResponse, anyhow::Error>,
> + Send
+ Unpin
+ '_,
>,
> {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let (pixel_values, pixel_attention_mask, spatial_shapes, text) =
self.processor.process_info(&mes, &mes_render)?;
let input_ids = self.tokenizer.text_encode(text, &self.device)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let data_vec = vec![
pixel_values.into(),
pixel_attention_mask.into(),
spatial_shapes.into(),
];
let data = MultiModalData::new(data_vec);
let seed = mes.seed.unwrap_or(34562) as u64;
let stream = generate_stream_generic(
&mut self.model,
&self.tokenizer,
input_ids,
data,
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
multi_model_data,
})
}
}
crate::impl_generate_model!(Lfm2VLGenerateModel<'a>);
+38 -74
View File
@@ -1,25 +1,18 @@
use crate::models::common::MultiModalData;
use crate::models::common::generate::{
GenerationContext, generate_generic, generate_stream_generic,
};
use crate::params::chat::{
ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse,
};
use crate::models::common::generate::{GenerationDataProvider, PrepareData};
use anyhow::Result;
use candle_core::{DType, Device};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::models::minicpm4::config::MiniCPM4Config;
use crate::models::minicpm4::model::MiniCPMModel;
// use crate::models::GenerateStream;
use crate::utils::{find_type_files, get_device, get_dtype};
use crate::{chat_template::ChatTemplate, models::GenerateModel, tokenizer::TokenizerModel};
use crate::{chat_template::ChatTemplate, tokenizer::TokenizerModel};
pub struct MiniCPM4GenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
minicpm: MiniCPMModel,
model: MiniCPMModel,
device: Device,
model_name: String,
}
@@ -35,7 +28,7 @@ impl<'a> MiniCPM4GenerateModel<'a> {
let dtype = get_dtype(dtype, cfg_dtype);
let model_list = find_type_files(path, "safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, device)? };
let minicpm = MiniCPMModel::new(vb, cfg)?;
let model = MiniCPMModel::new(vb, cfg)?;
let model_name = std::path::Path::new(path)
.file_name()
.and_then(|s| s.to_str())
@@ -44,73 +37,44 @@ impl<'a> MiniCPM4GenerateModel<'a> {
Ok(MiniCPM4GenerateModel {
chat_template,
tokenizer,
minicpm,
model,
device: device.clone(),
model_name,
})
}
}
impl<'a> GenerateModel for MiniCPM4GenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let input_ids = self.tokenizer.text_encode(mes_render, &self.device)?;
let seed = mes.seed.unwrap_or(34562) as u64;
let sample_len = mes.max_tokens.unwrap_or(2048);
let mut ctx = GenerationContext::new(
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data = MultiModalData::new(vec![]);
generate_generic(
&mut self.minicpm,
&self.tokenizer,
input_ids,
data,
&mut ctx,
&self.model_name,
)
/// 文本向量嵌入。
///
/// 将输入文本编码为固定长度向量(mean pool + L2 normalize)。
pub fn embed_text(&mut self, text: &str) -> Result<Vec<f32>> {
let input_ids = self.tokenizer.text_encode(text.to_string(), &self.device)?;
let embedding = self.model.embed(&input_ids)?;
let vec: Vec<f32> = embedding.flatten_all()?.to_dtype(DType::F32)?.to_vec1()?;
Ok(vec)
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let seed = mes.seed.unwrap_or(34562) as u64;
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let input_ids = self.tokenizer.text_encode(mes_render, &self.device)?;
let data = MultiModalData::new(vec![]);
let sample_len = mes.max_tokens.unwrap_or(512);
let stream = generate_stream_generic(
&mut self.minicpm,
&self.tokenizer,
input_ids,
data,
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
/// 批量文本向量嵌入。
pub fn embed_text_batch(&mut self, texts: &[&str]) -> Result<Vec<Vec<f32>>> {
let mut results = Vec::with_capacity(texts.len());
for text in texts {
results.push(self.embed_text(text)?);
}
Ok(results)
}
}
impl<'a> GenerationDataProvider for MiniCPM4GenerateModel<'a> {
fn get_data(&self, mes: &crate::params::chat::ChatCompletionParameters) -> Result<PrepareData> {
let mes_render = self.chat_template.apply_chat_template(mes)?;
let in_reasoning = self.is_in_reasoning(&mes_render);
let input_ids = self.tokenizer.text_encode(mes_render, &self.device)?;
let multi_model_data = self.get_multi_model_data();
Ok(PrepareData {
in_reasoning,
input_ids,
multi_model_data,
})
}
}
crate::impl_generate_model!(MiniCPM4GenerateModel<'a>);
+54
View File
@@ -320,6 +320,60 @@ impl MiniCPMModel {
layer.clear_kv_cache()
}
}
/// 文本向量嵌入 — mean pool + L2 normalize。
///
/// 复用完整 forward passembed → layers → norm),
/// 在 lm_head 之前截取 hidden states,做均值池化后 L2 归一化。
pub fn embed(&mut self, input_ids: &Tensor) -> Result<Tensor> {
let (bs, seq_len) = input_ids.dims2()?;
let input_embeds = self
.embed_tokens
.forward(input_ids)?
.affine(self.cfg.scale_emb, 0.0)?;
let attention_mask: Option<Tensor> = {
if seq_len <= 1 {
None
} else {
Some(prepare_causal_attention_mask(
bs,
seq_len,
0,
input_ids.device(),
)?)
}
};
let (cos, sin) = self.rope_emb.forward(0, seq_len)?;
let mut hidden_states = input_embeds;
for layer in &self.layers {
hidden_states =
layer.forward(&hidden_states, &cos, &sin, attention_mask.as_ref())?;
}
hidden_states = self.norm.forward(&hidden_states)?;
// Mean pool across sequence dimension
let embedding = hidden_states.mean(1)?;
let embedding = embedding.affine(
1.0 / (self.cfg.hidden_size / self.cfg.dim_model_base) as f64,
0.0,
)?;
// L2 normalize
let norm = embedding.sqr()?.sum_keepdim(1)?.sqrt()?;
let embedding = embedding.broadcast_div(&norm)?;
Ok(embedding)
}
/// 批量文本向量嵌入。
pub fn embed_batch(&mut self, input_ids: &[&Tensor]) -> Result<Vec<Tensor>> {
let mut results = Vec::with_capacity(input_ids.len());
for ids in input_ids {
results.push(self.embed(ids)?);
}
Ok(results)
}
}
impl InferenceModel for MiniCPMModel {
+14 -68
View File
@@ -1,19 +1,13 @@
use crate::models::common::MultiModalData;
use crate::models::common::generate::{
GenerationContext, generate_generic, generate_stream_generic,
};
use crate::models::common::generate::{GenerationDataProvider, PrepareData};
use crate::models::llama::LlamaForCausalLM;
use crate::models::minicpm5::config::MiniCPM5Config;
use crate::params::chat::{
ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse,
};
use anyhow::Result;
use candle_core::{DType, Device};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::utils::{find_type_files, get_device, get_dtype};
use crate::{chat_template::ChatTemplate, models::GenerateModel, tokenizer::TokenizerModel};
use crate::{chat_template::ChatTemplate, tokenizer::TokenizerModel};
pub struct MiniCPM5GenerateModel<'a> {
chat_template: ChatTemplate<'a>,
@@ -70,66 +64,18 @@ impl<'a> MiniCPM5GenerateModel<'a> {
}
}
impl<'a> GenerateModel for MiniCPM5GenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
impl<'a> GenerationDataProvider for MiniCPM5GenerateModel<'a> {
fn get_data(&self, mes: &crate::params::chat::ChatCompletionParameters) -> Result<PrepareData> {
let mes_render = self.chat_template.apply_chat_template(mes)?;
let in_reasoning = self.is_in_reasoning(&mes_render);
let input_ids = self.tokenizer.text_encode(mes_render, &self.device)?;
let seed = mes.seed.unwrap_or(34562) as u64;
let sample_len = mes.max_tokens.unwrap_or(2048);
let mut ctx = GenerationContext::new(
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data = MultiModalData::new(vec![]);
generate_generic(
&mut self.model,
&self.tokenizer,
let multi_model_data = self.get_multi_model_data();
Ok(PrepareData {
in_reasoning,
input_ids,
data,
&mut ctx,
&self.model_name,
)
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let seed = mes.seed.unwrap_or(34562) as u64;
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let input_ids = self.tokenizer.text_encode(mes_render, &self.device)?;
let data = MultiModalData::new(vec![]);
let sample_len = mes.max_tokens.unwrap_or(512);
let stream = generate_stream_generic(
&mut self.model,
&self.tokenizer,
input_ids,
data,
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
multi_model_data,
})
}
}
crate::impl_generate_model!(MiniCPM5GenerateModel<'a>);
+1 -1
View File
@@ -284,7 +284,7 @@ pub fn load_model<'a>(
let model = HunyuanOCRGenerateModel::init(path, device, dtype)?;
ModelInstance::HunyuanOCR(model)
}
WhichModel::PaddleOCRVL | WhichModel::PaddleOCRVL1_5 => {
WhichModel::PaddleOCRVL | WhichModel::PaddleOCRVL1_5 | WhichModel::PaddleOCRVL1_6 => {
let model = PaddleOCRVLGenerateModel::init(path, device, dtype)?;
ModelInstance::PaddleOCRVL(Box::new(model))
}
+17 -87
View File
@@ -1,27 +1,22 @@
use crate::models::common::MultiModalData;
use crate::models::common::generate::{
GenerationContext, generate_generic, generate_stream_generic,
};
use crate::params::chat::{
ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse,
};
use crate::models::common::generate::{GenerationDataProvider, PrepareData};
use crate::params::chat::ChatCompletionParameters;
use anyhow::Result;
use candle_core::{D, DType, Device, IndexOp, Tensor};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::models::paddleocr_vl::config::{PaddleOCRVLConfig, PaddleOCRVLPreprocessorConfig};
use crate::models::paddleocr_vl::model::PaddleOCRVLModel;
use crate::models::paddleocr_vl::processor::PaddleOCRVLProcessor;
use crate::utils::tensor_utils::get_equal_mask;
use crate::utils::{find_type_files, get_device, get_dtype};
use crate::{chat_template::ChatTemplate, models::GenerateModel, tokenizer::TokenizerModel};
use crate::{chat_template::ChatTemplate, tokenizer::TokenizerModel};
pub struct PaddleOCRVLGenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
pre_processor: PaddleOCRVLProcessor,
paddleocr_vl: PaddleOCRVLModel,
model: PaddleOCRVLModel,
cfg: PaddleOCRVLConfig,
device: Device,
model_name: String,
@@ -42,7 +37,7 @@ impl<'a> PaddleOCRVLGenerateModel<'a> {
let pre_processor = PaddleOCRVLProcessor::new(processor_cfg, device, dtype)?;
let model_list = find_type_files(path, "safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, device)? };
let paddleocr_vl = PaddleOCRVLModel::new(cfg.clone(), vb, vec![2])?;
let model = PaddleOCRVLModel::new(cfg.clone(), vb, vec![2])?;
let model_name = std::path::Path::new(path)
.file_name()
.and_then(|s| s.to_str())
@@ -52,7 +47,7 @@ impl<'a> PaddleOCRVLGenerateModel<'a> {
chat_template,
tokenizer,
pre_processor,
paddleocr_vl,
model,
cfg,
device: device.clone(),
model_name,
@@ -60,11 +55,11 @@ impl<'a> PaddleOCRVLGenerateModel<'a> {
}
}
impl<'a> GenerateModel for PaddleOCRVLGenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
impl<'a> GenerationDataProvider for PaddleOCRVLGenerateModel<'a> {
fn get_data(&self, mes: &ChatCompletionParameters) -> Result<PrepareData> {
let mes_render = self.chat_template.apply_chat_template(mes)?;
let (replace_text, pixel_values, image_grid_thw) =
self.pre_processor.process_info(&mes, &mes_render)?;
self.pre_processor.process_info(mes, &mes_render)?;
let input_ids = self.tokenizer.text_encode(replace_text, &self.device)?;
let image_mask = get_equal_mask(&input_ids, self.cfg.image_token_id)?;
@@ -73,84 +68,19 @@ impl<'a> GenerateModel for PaddleOCRVLGenerateModel<'a> {
.cumsum(D::Minus1)?
.to_dtype(candle_core::DType::U32)?
.broadcast_sub(&Tensor::new(vec![1_u32], input_ids.device())?)?;
let seed = mes.seed.unwrap_or(34562) as u64;
let sample_len = mes.max_tokens.unwrap_or(1024);
let mut ctx = GenerationContext::new(
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data_vec = vec![
pixel_values,
image_grid_thw,
image_mask.into(),
cache_position.into(),
];
let data = MultiModalData::new(data_vec);
generate_generic(
&mut self.paddleocr_vl,
&self.tokenizer,
let multi_model_data = MultiModalData::new(data_vec);
Ok(PrepareData {
in_reasoning: false,
input_ids,
data,
&mut ctx,
&self.model_name,
)
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let (replace_text, pixel_values, image_grid_thw) =
self.pre_processor.process_info(&mes, &mes_render)?;
let input_ids = self.tokenizer.text_encode(replace_text, &self.device)?;
let image_mask = get_equal_mask(&input_ids, self.cfg.image_token_id)?;
let cache_position = Tensor::ones_like(&input_ids.i(0)?)?
.to_dtype(candle_core::DType::F64)?
.cumsum(D::Minus1)?
.to_dtype(candle_core::DType::U32)?
.broadcast_sub(&Tensor::new(vec![1_u32], input_ids.device())?)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let data_vec = vec![
pixel_values,
image_grid_thw,
image_mask.into(),
cache_position.into(),
];
let data = MultiModalData::new(data_vec);
let seed = mes.seed.unwrap_or(34562) as u64;
let stream = generate_stream_generic(
&mut self.paddleocr_vl,
&self.tokenizer,
input_ids,
data,
mes.temperature,
mes.top_p,
None,
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
false,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
multi_model_data,
})
}
}
crate::impl_generate_model!(PaddleOCRVLGenerateModel<'a>);
+7 -7
View File
@@ -30,7 +30,7 @@ pub struct Qwen2_5VLGenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
pre_processor: Qwen2_5VLProcessor,
qwen2_5_vl: Qwen2_5VLModel,
model: Qwen2_5VLModel,
device: Device,
endoftext_id: u32,
im_end_id: u32,
@@ -52,7 +52,7 @@ impl<'a> Qwen2_5VLGenerateModel<'a> {
// let model_list = find_safetensors_files(&path)?;
let model_list = find_type_files(path, "safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, device)? };
let qwen2_5_vl = Qwen2_5VLModel::new(cfg, vb)?;
let model = Qwen2_5VLModel::new(cfg, vb)?;
let model_name = std::path::Path::new(path)
.file_name()
.and_then(|s| s.to_str())
@@ -62,7 +62,7 @@ impl<'a> Qwen2_5VLGenerateModel<'a> {
chat_template,
tokenizer,
pre_processor,
qwen2_5_vl,
model,
device: device.clone(),
endoftext_id,
im_end_id,
@@ -102,7 +102,7 @@ impl<'a> GenerateModel for Qwen2_5VLGenerateModel<'a> {
let mut completion_secs = 0.0f64;
for _ in 0..sample_len {
let i_start = Instant::now();
let logits = self.qwen2_5_vl.forward(
let logits = self.model.forward(
&input_ids,
pixel_values,
image_grid_thw,
@@ -136,7 +136,7 @@ impl<'a> GenerateModel for Qwen2_5VLGenerateModel<'a> {
}
let num_token = generate.len() as u32;
let res = self.tokenizer.token_decode(generate)?;
self.qwen2_5_vl.clear_kv_cache();
self.model.clear_kv_cache();
let response = build_completion_response_with_time(
res,
&self.model_name,
@@ -190,7 +190,7 @@ impl<'a> GenerateModel for Qwen2_5VLGenerateModel<'a> {
let mut tool_call_content = String::new();
for _ in 0..sample_len {
let i_start = Instant::now();
let logits = self.qwen2_5_vl.forward(
let logits = self.model.forward(
&input_ids,
pixel_values,
image_grid_thw,
@@ -293,7 +293,7 @@ impl<'a> GenerateModel for Qwen2_5VLGenerateModel<'a> {
pixel_values = None;
pixel_values_video = None;
}
self.qwen2_5_vl.clear_kv_cache();
self.model.clear_kv_cache();
};
Ok(Box::new(Box::pin(stream)))
}
+28 -81
View File
@@ -1,24 +1,18 @@
use crate::models::common::MultiModalData;
use crate::models::common::generate::{
GenerationContext, generate_generic, generate_stream_generic,
};
use crate::params::chat::{
ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse,
};
use crate::models::common::generate::{GenerationDataProvider, PrepareData};
use anyhow::Result;
use candle_core::{DType, Device};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::models::qwen3::config::{Qwen3Config, Qwen3GenerationConfig};
use crate::models::qwen3::model::Qwen3Model;
use crate::utils::{find_type_files, get_device, get_dtype};
use crate::{chat_template::ChatTemplate, models::GenerateModel, tokenizer::TokenizerModel};
use crate::{chat_template::ChatTemplate, tokenizer::TokenizerModel};
pub struct Qwen3GenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
qwen3: Qwen3Model,
model: Qwen3Model,
device: Device,
generation_config: Qwen3GenerationConfig,
model_name: String,
@@ -38,7 +32,7 @@ impl<'a> Qwen3GenerateModel<'a> {
let generation_config_path = path.to_string() + "/generation_config.json";
let generation_config: Qwen3GenerationConfig =
serde_json::from_slice(&std::fs::read(generation_config_path)?)?;
let qwen3 = Qwen3Model::new(&cfg, vb, generation_config.eos_token_id.clone())?;
let model = Qwen3Model::new(&cfg, vb, generation_config.eos_token_id.clone())?;
let model_name = std::path::Path::new(path)
.file_name()
@@ -48,7 +42,7 @@ impl<'a> Qwen3GenerateModel<'a> {
Ok(Qwen3GenerateModel {
chat_template,
tokenizer,
qwen3,
model,
device: device.clone(),
generation_config,
model_name,
@@ -56,77 +50,30 @@ impl<'a> Qwen3GenerateModel<'a> {
}
}
impl<'a> GenerateModel for Qwen3GenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let temperature = mes
.temperature
.unwrap_or(self.generation_config.temperature);
let top_p = mes.top_p.unwrap_or(self.generation_config.top_p);
let top_k = self.generation_config.top_k;
let seed = mes.seed.unwrap_or(34562) as u64;
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let input_ids = self.tokenizer.text_encode(mes_render, &self.device)?;
let sample_len = mes.max_tokens.unwrap_or(2048);
let mut ctx = GenerationContext::new(
temperature.into(),
top_p.into(),
top_k.into(),
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data = MultiModalData::new(vec![]);
generate_generic(
&mut self.qwen3,
&self.tokenizer,
input_ids,
data,
&mut ctx,
&self.model_name,
)
impl<'a> GenerationDataProvider for Qwen3GenerateModel<'a> {
fn get_temperature(&self, req_temp: Option<f32>) -> Option<f32> {
Some(req_temp.unwrap_or(self.generation_config.temperature))
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let temperature = mes
.temperature
.unwrap_or(self.generation_config.temperature);
let top_p = mes.top_p.unwrap_or(self.generation_config.top_p);
let top_k = self.generation_config.top_k;
let seed = mes.seed.unwrap_or(34562) as u64;
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let in_reasoning = mes_render.ends_with("<think>\n");
fn get_top_p(&self, req_top_p: Option<f32>) -> Option<f32> {
Some(req_top_p.unwrap_or(self.generation_config.top_p))
}
fn get_top_k(&self, top_k: Option<usize>) -> Option<usize> {
Some(top_k.unwrap_or(self.generation_config.top_k))
}
fn get_data(&self, mes: &crate::params::chat::ChatCompletionParameters) -> Result<PrepareData> {
let mes_render = self.chat_template.apply_chat_template(mes)?;
let in_reasoning = self.is_in_reasoning(&mes_render);
let input_ids = self.tokenizer.text_encode(mes_render, &self.device)?;
let data = MultiModalData::new(vec![]);
let sample_len = mes.max_tokens.unwrap_or(512);
let stream = generate_stream_generic(
&mut self.qwen3,
&self.tokenizer,
input_ids,
data,
temperature.into(),
top_p.into(),
top_k.into(),
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
let multi_model_data = self.get_multi_model_data();
Ok(PrepareData {
in_reasoning,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
input_ids,
multi_model_data,
})
}
}
crate::impl_generate_model!(Qwen3GenerateModel<'a>);
+33 -98
View File
@@ -2,11 +2,11 @@ use crate::{
models::common::{
MultiModalData,
generate::{
GenerationContext, generate_generic, generate_generic_text, generate_stream_generic,
GenerationContext, GenerationDataProvider, PrepareData, generate_generic_text,
generate_stream_generic_text,
},
},
params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse},
params::chat::ChatCompletionParameters,
};
use anyhow::{Result, anyhow};
use candle_core::{DType, Device, quantized::gguf_file};
@@ -16,7 +16,6 @@ use rocket::futures::Stream;
use crate::{
chat_template::ChatTemplate,
models::{
GenerateModel,
common::gguf::Gguf,
qwen3_5::{config::Qwen3_5Config, model::Qwen3_5Model},
qwen3vl::processor::Qwen3VLProcessor,
@@ -29,7 +28,7 @@ pub struct Qwen3_5GenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
pre_processor: Option<Qwen3VLProcessor>,
qwen3_5: Qwen3_5Model,
model: Qwen3_5Model,
device: Device,
model_name: String,
repeat_penalty: f32,
@@ -53,13 +52,13 @@ impl<'a> Qwen3_5GenerateModel<'a> {
let model_list = find_type_files(path, "safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, &device)? };
let eos_ids = vec![cfg.text_config.eos_token_id];
let qwen3_5 = Qwen3_5Model::new_from_vb(vb, cfg, eos_ids)?;
let model = Qwen3_5Model::new_from_vb(vb, cfg, eos_ids)?;
Ok(Self {
chat_template,
tokenizer,
pre_processor: Some(pre_processor),
qwen3_5,
model,
device,
model_name: model_name.to_string(),
repeat_penalty: 1.0,
@@ -89,13 +88,13 @@ impl<'a> Qwen3_5GenerateModel<'a> {
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, &device)? };
let eos_ids = vec![cfg.text_config.eos_token_id];
// let qwen3_5 = Qwen3_5Model::new_from_vb(vb, cfg, eos_ids)?;
let qwen3_5 = Qwen3_5Model::new_from_vb_without_visual(vb, cfg, eos_ids)?;
let model = Qwen3_5Model::new_from_vb_without_visual(vb, cfg, eos_ids)?;
Ok(Self {
chat_template,
tokenizer,
pre_processor,
qwen3_5,
model,
device,
model_name: model_name.to_string(),
repeat_penalty: 1.0,
@@ -142,7 +141,7 @@ impl<'a> Qwen3_5GenerateModel<'a> {
.get_matedata("tokenizer.ggml.eos_token_id")?
.to_u32()?;
let eos_ids = vec![eos_token_id];
let qwen3_5 =
let model =
Qwen3_5Model::new_from_gguf(&mut model_gguf, mmproj_gguf.as_mut(), &device, eos_ids)?;
let stem = std::path::Path::new(model_file)
.file_stem() // 获取文件名主干(不含扩展名)
@@ -152,7 +151,7 @@ impl<'a> Qwen3_5GenerateModel<'a> {
chat_template,
tokenizer,
pre_processor,
qwen3_5,
model,
device,
model_name: stem.to_string(),
repeat_penalty: 1.2,
@@ -199,13 +198,7 @@ impl<'a> Qwen3_5GenerateModel<'a> {
];
let data = MultiModalData::new(data_vec);
generate_generic_text(
&mut self.qwen3_5,
&self.tokenizer,
input_ids,
data,
&mut ctx,
)
generate_generic_text(&mut self.model, &self.tokenizer, input_ids, data, &mut ctx)
}
pub fn generate_stream_text(
@@ -237,7 +230,7 @@ impl<'a> Qwen3_5GenerateModel<'a> {
let data = MultiModalData::new(data_vec);
let seed = mes.seed.unwrap_or(34562) as u64;
generate_stream_generic_text(
&mut self.qwen3_5,
&mut self.model,
&self.tokenizer,
input_ids,
data,
@@ -253,71 +246,25 @@ impl<'a> Qwen3_5GenerateModel<'a> {
}
}
impl<'a> GenerateModel for Qwen3_5GenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let seed = mes.seed.unwrap_or(32768) as u64;
let temperature = mes.temperature.unwrap_or(0.4);
let top_p = mes.top_p.unwrap_or(0.95);
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let (mes_text, pixel_values, image_grid_thw, pixel_values_video, video_grid_thw) =
if let Some(processor) = &self.pre_processor {
let input = processor.process_info(&mes, &mes_render)?;
(
input.replace_text,
input.pixel_values,
input.image_grid_thw,
input.pixel_values_video,
input.video_grid_thw,
)
} else {
(mes_render, None, None, None, None)
};
let input_ids = self.tokenizer.text_encode(mes_text, &self.device)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let mut ctx = GenerationContext::new(
temperature.into(),
top_p.into(),
Some(20),
self.repeat_penalty.into(),
self.repeat_last_n.into(),
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let data_vec = vec![
pixel_values,
image_grid_thw,
pixel_values_video,
video_grid_thw,
];
let data = MultiModalData::new(data_vec);
generate_generic(
&mut self.qwen3_5,
&self.tokenizer,
input_ids,
data,
&mut ctx,
&self.model_name,
)
impl<'a> GenerationDataProvider for Qwen3_5GenerateModel<'a> {
fn get_temperature(&self, req_temp: Option<f32>) -> Option<f32> {
Some(req_temp.unwrap_or(0.4))
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let in_reasoning = mes_render.ends_with("<think>\n");
fn get_top_p(&self, req_top_p: Option<f32>) -> Option<f32> {
Some(req_top_p.unwrap_or(0.95))
}
fn get_top_k(&self, top_k: Option<usize>) -> Option<usize> {
Some(top_k.unwrap_or(40))
}
fn get_data(&self, mes: &ChatCompletionParameters) -> Result<PrepareData> {
let mes_render = self.chat_template.apply_chat_template(mes)?;
let in_reasoning = self.is_in_reasoning(&mes_render);
let (mes_text, pixel_values, image_grid_thw, pixel_values_video, video_grid_thw) =
if let Some(processor) = &self.pre_processor {
let input = processor.process_info(&mes, &mes_render)?;
let input = processor.process_info(mes, &mes_render)?;
(
input.replace_text,
input.pixel_values,
@@ -329,31 +276,19 @@ impl<'a> GenerateModel for Qwen3_5GenerateModel<'a> {
(mes_render, None, None, None, None)
};
let input_ids = self.tokenizer.text_encode(mes_text, &self.device)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let data_vec = vec![
pixel_values,
image_grid_thw,
pixel_values_video,
video_grid_thw,
];
let data = MultiModalData::new(data_vec);
let seed = mes.seed.unwrap_or(34562) as u64;
let stream = generate_stream_generic(
&mut self.qwen3_5,
&self.tokenizer,
input_ids,
data,
mes.temperature,
mes.top_p,
None,
self.repeat_penalty.into(),
self.repeat_last_n.into(),
seed,
sample_len,
let multi_model_data = MultiModalData::new(data_vec);
Ok(PrepareData {
in_reasoning,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
input_ids,
multi_model_data,
})
}
}
crate::impl_generate_model!(Qwen3_5GenerateModel<'a>);
+9 -14
View File
@@ -37,7 +37,7 @@ pub struct Qwen3AsrGenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
processor: Qwen3AsrProcessor,
qwen3_asr: Qwen3ASRModel,
model: Qwen3ASRModel,
device: Device,
dtype: DType,
eos_token_id1: u32,
@@ -65,7 +65,7 @@ impl<'a> Qwen3AsrGenerateModel<'a> {
let dtype = get_dtype(dtype, cfg_dtype);
let model_list = find_type_files(path, "safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, &device)? };
let qwen3_asr = Qwen3ASRModel::new(vb, &cfg, generation_config.eos_token_id.clone())?;
let model = Qwen3ASRModel::new(vb, &cfg, generation_config.eos_token_id.clone())?;
let model_name = std::path::Path::new(path)
.file_name()
.and_then(|s| s.to_str())
@@ -75,7 +75,7 @@ impl<'a> Qwen3AsrGenerateModel<'a> {
chat_template,
tokenizer,
processor,
qwen3_asr,
model,
device,
dtype,
eos_token_id1: generation_config.eos_token_id[0] as u32,
@@ -116,13 +116,8 @@ impl<'a> Qwen3AsrGenerateModel<'a> {
);
let data_vec = vec![input_features];
let data = MultiModalData::new(data_vec);
let mut text = generate_generic_text(
&mut self.qwen3_asr,
&self.tokenizer,
input_ids,
data,
&mut ctx,
)?;
let mut text =
generate_generic_text(&mut self.model, &self.tokenizer, input_ids, data, &mut ctx)?;
if text.contains("<asr_text>") {
let mut split: Vec<&str> = text.split("<asr_text>").collect();
text = split.pop().unwrap_or(&text).to_string();
@@ -156,7 +151,7 @@ impl<'a> GenerateModel for Qwen3AsrGenerateModel<'a> {
for _ in 0..sample_len {
let i_start = Instant::now();
let logits =
self.qwen3_asr
self.model
.forward(&input_ids, seqlen_offset, input_features.as_ref())?;
let logits = logits.squeeze(0)?.squeeze(0)?.to_dtype(DType::F32)?;
let next_token = logit_processor.sample(&logits)?;
@@ -175,7 +170,7 @@ impl<'a> GenerateModel for Qwen3AsrGenerateModel<'a> {
input_ids = Tensor::from_vec(vec![next_token], (1, 1), &self.device)?;
input_features = None;
}
self.qwen3_asr.clear_kv_cache();
self.model.clear_kv_cache();
}
let num_token = generate.len() as u32;
let res = self.tokenizer.token_decode(generate)?;
@@ -226,7 +221,7 @@ impl<'a> GenerateModel for Qwen3AsrGenerateModel<'a> {
for _ in 0..sample_len {
let i_start = Instant::now();
let logits =
self.qwen3_asr
self.model
.forward(&input_ids, seqlen_offset, input_features.as_ref())?;
let logits = logits.squeeze(0)?.squeeze(0)?.to_dtype(DType::F32)?;
let next_token = logit_processor.sample(&logits)?;
@@ -266,7 +261,7 @@ impl<'a> GenerateModel for Qwen3AsrGenerateModel<'a> {
input_ids = Tensor::from_vec(vec![next_token], (1, 1), &self.device)?;
input_features = None;
}
self.qwen3_asr.clear_kv_cache();
self.model.clear_kv_cache();
}
};
Ok(Box::new(Box::pin(stream)))
+27 -88
View File
@@ -1,19 +1,17 @@
use crate::{
models::common::{
MultiModalData,
generate::{GenerationContext, generate_generic, generate_stream_generic},
generate::{GenerationDataProvider, PrepareData},
},
params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse},
params::chat::ChatCompletionParameters,
};
use anyhow::Result;
use candle_core::{DType, Device, Tensor};
use candle_nn::VarBuilder;
use rocket::futures::Stream;
use crate::{
chat_template::ChatTemplate,
models::{
GenerateModel,
qwen3::config::Qwen3GenerationConfig,
qwen3vl::{config::Qwen3VLConfig, model::Qwen3VLModel, processor::Qwen3VLProcessor},
},
@@ -25,7 +23,7 @@ pub struct Qwen3VLGenerateModel<'a> {
chat_template: ChatTemplate<'a>,
tokenizer: TokenizerModel,
pre_processor: Qwen3VLProcessor,
qwen3_vl: Qwen3VLModel,
model: Qwen3VLModel,
device: Device,
generation_config: Qwen3GenerationConfig,
model_name: String,
@@ -46,7 +44,7 @@ impl<'a> Qwen3VLGenerateModel<'a> {
let generation_config_path = path.to_string() + "/generation_config.json";
let generation_config: Qwen3GenerationConfig =
serde_json::from_slice(&std::fs::read(generation_config_path)?)?;
let qwen3_vl = Qwen3VLModel::new(cfg, vb, generation_config.eos_token_id.clone())?;
let model = Qwen3VLModel::new(cfg, vb, generation_config.eos_token_id.clone())?;
let model_name = std::path::Path::new(path)
.file_name()
@@ -57,7 +55,7 @@ impl<'a> Qwen3VLGenerateModel<'a> {
chat_template,
tokenizer,
pre_processor,
qwen3_vl,
model,
device,
generation_config,
model_name,
@@ -65,76 +63,28 @@ impl<'a> Qwen3VLGenerateModel<'a> {
}
}
impl<'a> GenerateModel for Qwen3VLGenerateModel<'a> {
fn generate(&mut self, mes: ChatCompletionParameters) -> Result<ChatCompletionResponse> {
let temperature = mes
.temperature
.unwrap_or(self.generation_config.temperature);
let top_p = mes.top_p.unwrap_or(self.generation_config.top_p);
let top_k = self.generation_config.top_k;
let seed = mes.seed.unwrap_or(34562) as u64;
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let input = self.pre_processor.process_info(&mes, &mes_render)?;
let input_ids = self
.tokenizer
.text_encode(input.replace_text.clone(), &self.device)?;
let seq_len = input_ids.dim(1)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let mut ctx = GenerationContext::new(
temperature.into(),
top_p.into(),
top_k.into(),
mes.repeat_penalty,
mes.repeat_last_n,
seed,
input_ids.dim(1)?,
sample_len,
self.device.clone(),
);
let cache_position = Tensor::arange(0u32, seq_len as u32, &self.device)?;
let data_vec = vec![
input.pixel_values,
input.image_grid_thw,
input.pixel_values_video,
input.video_grid_thw,
cache_position.into(),
];
let data = MultiModalData::new(data_vec);
generate_generic(
&mut self.qwen3_vl,
&self.tokenizer,
input_ids,
data,
&mut ctx,
&self.model_name,
)
impl<'a> GenerationDataProvider for Qwen3VLGenerateModel<'a> {
fn get_temperature(&self, req_temp: Option<f32>) -> Option<f32> {
Some(req_temp.unwrap_or(self.generation_config.temperature))
}
fn generate_stream(
&mut self,
mes: ChatCompletionParameters,
) -> Result<
Box<
dyn Stream<Item = Result<ChatCompletionChunkResponse, anyhow::Error>>
+ Send
+ Unpin
+ '_,
>,
> {
let temperature = mes
.temperature
.unwrap_or(self.generation_config.temperature);
let top_p = mes.top_p.unwrap_or(self.generation_config.top_p);
let top_k = self.generation_config.top_k;
let mes_render = self.chat_template.apply_chat_template(&mes)?;
let in_reasoning = mes_render.ends_with("<think>\n");
let input = self.pre_processor.process_info(&mes, &mes_render)?;
fn get_top_p(&self, req_top_p: Option<f32>) -> Option<f32> {
Some(req_top_p.unwrap_or(self.generation_config.top_p))
}
fn get_top_k(&self, top_k: Option<usize>) -> Option<usize> {
Some(top_k.unwrap_or(self.generation_config.top_k))
}
fn get_data(&self, mes: &ChatCompletionParameters) -> Result<PrepareData> {
let mes_render = self.chat_template.apply_chat_template(mes)?;
let in_reasoning = self.is_in_reasoning(&mes_render);
let input = self.pre_processor.process_info(mes, &mes_render)?;
let input_ids = self
.tokenizer
.text_encode(input.replace_text.clone(), &self.device)?;
let seq_len = input_ids.dim(1)?;
let cache_position = Tensor::arange(0u32, seq_len as u32, &self.device)?;
let sample_len = mes.max_tokens.unwrap_or(1024);
let data_vec = vec![
input.pixel_values,
input.image_grid_thw,
@@ -142,24 +92,13 @@ impl<'a> GenerateModel for Qwen3VLGenerateModel<'a> {
input.video_grid_thw,
cache_position.into(),
];
let data = MultiModalData::new(data_vec);
let seed = mes.seed.unwrap_or(34562) as u64;
let stream = generate_stream_generic(
&mut self.qwen3_vl,
&self.tokenizer,
input_ids,
data,
temperature.into(),
top_p.into(),
top_k.into(),
mes.repeat_penalty,
mes.repeat_last_n,
seed,
sample_len,
let multi_model_data = MultiModalData::new(data_vec);
Ok(PrepareData {
in_reasoning,
&self.device,
&self.model_name,
)?;
Ok(Box::new(Box::pin(stream)))
input_ids,
multi_model_data,
})
}
}
crate::impl_generate_model!(Qwen3VLGenerateModel<'a>);
+11 -11
View File
@@ -27,7 +27,7 @@ use crate::{
};
pub struct VoxCPMGenerate {
voxcpm: VoxCPMModel,
model: VoxCPMModel,
prompt_cache: Option<HashMap<String, Tensor>>,
out_sample_rate: usize,
model_name: String,
@@ -106,12 +106,12 @@ impl VoxCPMGenerate {
VarBuilder::from_tensors(dict_to_hashmap, m_dtype, device)
};
let tokenizer = SingleChineseTokenizer::new(path)?;
let voxcpm = VoxCPMModel::new(vb_voxcpm, config, tokenizer, audio_vae)?;
let model = VoxCPMModel::new(vb_voxcpm, config, tokenizer, audio_vae)?;
let out_sample_rate = audio_config
.out_sample_rate
.unwrap_or(audio_config.sample_rate);
Ok(Self {
voxcpm,
model,
prompt_cache: None,
out_sample_rate,
model_name,
@@ -124,7 +124,7 @@ impl VoxCPMGenerate {
prompt_wav_path: String,
) -> Result<()> {
let cache = self
.voxcpm
.model
.build_prompt_cache(prompt_text, prompt_wav_path)?;
self.prompt_cache = Some(cache);
Ok(())
@@ -143,7 +143,7 @@ impl VoxCPMGenerate {
let audio = match &self.prompt_cache {
Some(cache) => {
let prompt_cache = cache.clone();
self.voxcpm.generate_with_prompt_cache(
self.model.generate_with_prompt_cache(
target_text,
prompt_cache,
min_len,
@@ -156,7 +156,7 @@ impl VoxCPMGenerate {
}
None => self.generate_simple(target_text)?,
};
self.voxcpm.clear_kv_cache();
self.model.clear_kv_cache();
Ok(audio)
}
@@ -196,7 +196,7 @@ impl VoxCPMGenerate {
// retry_badcase: bool,
retry_badcase_ratio_threshold: f64,
) -> Result<Tensor> {
let audio = self.voxcpm.generate(
let audio = self.model.generate(
target_text,
prompt_text,
prompt_wav_path,
@@ -207,7 +207,7 @@ impl VoxCPMGenerate {
// retry_badcase,
retry_badcase_ratio_threshold,
)?;
self.voxcpm.clear_kv_cache();
self.model.clear_kv_cache();
Ok(audio)
}
@@ -250,7 +250,7 @@ impl GenerateModel for VoxCPMGenerate {
target_text = format!("({instruction}){target_text}");
}
let audio = self
.voxcpm
.model
.generate(
target_text,
prompt_text,
@@ -262,13 +262,13 @@ impl GenerateModel for VoxCPMGenerate {
retry_badcase_ratio_threshold,
)
.inspect_err(|_| {
self.voxcpm.clear_kv_cache();
self.model.clear_kv_cache();
})?;
let wav_u8 = get_audio_wav_u8(&audio, self.out_sample_rate as u32)?;
// let wave_u8_str = String::from_utf8(wav_u8)?;
let base64_audio = BASE64_STANDARD.encode(wav_u8);
let response = build_audio_completion_response(&base64_audio, &self.model_name);
self.voxcpm.clear_kv_cache();
self.model.clear_kv_cache();
Ok(response)
}
#[allow(unused_variables)]
+10 -10
View File
@@ -24,7 +24,7 @@ use crate::{
};
pub struct VoxCPMGenerateRefact {
voxcpm: VoxCPMModelRefact,
model: VoxCPMModelRefact,
tokenizer: SingleChineseTokenizer,
audio_vae: AudioVAE,
processor: VoxCPMProcessor,
@@ -113,13 +113,13 @@ impl VoxCPMGenerateRefact {
VarBuilder::from_tensors(dict_to_hashmap, m_dtype, device)
};
let tokenizer = SingleChineseTokenizer::new(path)?;
let voxcpm =
let model =
VoxCPMModelRefact::new(vb_voxcpm, config, audio_vae.latent_dim, decode_chunk_size)?;
let out_sample_rate = audio_config
.out_sample_rate
.unwrap_or(audio_config.sample_rate);
Ok(Self {
voxcpm,
model,
tokenizer,
audio_vae,
processor,
@@ -162,7 +162,7 @@ impl VoxCPMGenerateRefact {
} else {
max_len
};
let audio = self.voxcpm.inference(
let audio = self.model.inference(
&text_token,
audio_feat.as_ref(),
audio_mask.as_ref(),
@@ -172,7 +172,7 @@ impl VoxCPMGenerateRefact {
cfg_value,
&self.audio_vae,
)?;
self.voxcpm.clear_kv_cache();
self.model.clear_kv_cache();
Ok(audio)
}
@@ -240,7 +240,7 @@ impl VoxCPMGenerateRefact {
} else {
max_len
};
self.voxcpm.inference(
self.model.inference(
&text_token,
audio_feat.as_ref(),
audio_mask.as_ref(),
@@ -255,7 +255,7 @@ impl VoxCPMGenerateRefact {
return Err(anyhow!("need prompt_cache"));
}
};
self.voxcpm.clear_kv_cache();
self.model.clear_kv_cache();
Ok(audio)
}
@@ -284,7 +284,7 @@ impl VoxCPMGenerateRefact {
} else {
max_len
};
self.voxcpm.inference_stream(
self.model.inference_stream(
text_token,
audio_feat,
audio_mask,
@@ -344,13 +344,13 @@ impl GenerateModel for VoxCPMGenerateRefact {
retry_badcase_ratio_threshold,
)
.inspect_err(|_| {
self.voxcpm.clear_kv_cache();
self.model.clear_kv_cache();
})?;
let wav_u8 = get_audio_wav_u8(&audio, self.out_sample_rate as u32)?;
// let wave_u8_str = String::from_utf8(wav_u8)?;
let base64_audio = BASE64_STANDARD.encode(wav_u8);
let response = build_audio_completion_response(&base64_audio, &self.model_name);
self.voxcpm.clear_kv_cache();
self.model.clear_kv_cache();
Ok(response)
}
#[allow(unused_variables)]
+2 -2
View File
@@ -5,11 +5,11 @@
// use std::io::{Read, Seek};
// use std::{io::Cursor, time::Instant};
use aha::utils::tensor_utils::get_mask_from_lengths;
// use aha::utils::tensor_utils::get_mask_from_lengths;
// use aha::utils::tensor_utils::repeat_interleave;
// use crate::params::chat::ChatCompletionParameters;
use anyhow::Result;
use candle_core::Tensor;
// use candle_core::Tensor;
// use kaldi_native_fbank::{
// FbankComputer, FbankOptions,
// window::{Window, extract_window},
+1 -1
View File
@@ -21,7 +21,7 @@ fn fun_asr_nano_generate() -> Result<()> {
"type": "audio",
"audio_url":
{
"url": "file://./assets/audio/voice_01.wav"
"url": "https://package-release.coderbox.cn/aiway/test/other/%E5%93%AA%E5%90%92.wav"
}
},
{
+1 -1
View File
@@ -22,7 +22,7 @@ fn glm_asr_nano_generate() -> Result<()> {
"type": "audio",
"audio_url":
{
"url": "file://./assets/audio/voice_01.wav"
"url": "https://package-release.coderbox.cn/aiway/test/other/%E5%93%AA%E5%90%92.wav"
}
},
{
+1 -1
View File
@@ -52,7 +52,7 @@ fn glm_ocr_generate() -> Result<()> {
#[tokio::test]
async fn glm_ocr_stream() -> Result<()> {
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda glm_ocr_stream -r -- --nocapture
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_glm_ocr glm_ocr_stream -r -- --nocapture
let message = r#"
{
+1 -1
View File
@@ -49,7 +49,7 @@ fn hunyuan_ocr_generate() -> Result<()> {
#[tokio::test]
async fn hunyuan_ocr_stream() -> Result<()> {
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda hunyuan_ocr_stream -r -- --nocapture
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_hunyuan_ocr hunyuan_ocr_stream -r -- --nocapture
let message = r#"
{
+1 -1
View File
@@ -60,7 +60,7 @@ async fn lfm2vl_stream() -> Result<()> {
let save_dir =
aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
// let model_path = format!("{}/LiquidAI/LFM2-1.2B/", save_dir);
let model_path = format!("{}/LiquidAI/LFM2.5-VL-1.6B/", save_dir);
let model_path = format!("{}/LiquidAI/LFM2.5-VL-450M/", save_dir);
let message = r#"
{
"model": "lfm2vl",
+41 -2
View File
@@ -1,10 +1,11 @@
use std::time::Instant;
use std::{pin::pin, time::Instant};
use aha::{
models::{GenerateModel, minicpm5::generate::MiniCPM5GenerateModel},
params::chat::ChatCompletionParameters,
};
use anyhow::Result;
use rocket::futures::StreamExt;
#[test]
fn minicpm5_generate() -> Result<()> {
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_minicpm5 minicpm5_generate -r -- --nocapture
@@ -16,7 +17,7 @@ fn minicpm5_generate() -> Result<()> {
{
"temperature": 0.3,
"top_p": 0.8,
"model": "minicpm4",
"model": "minicpm5",
"messages": [
{
"role": "user",
@@ -39,3 +40,41 @@ fn minicpm5_generate() -> Result<()> {
}
Ok(())
}
#[tokio::test]
async fn minicpm5_stream() -> Result<()> {
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_minicpm5 minicpm5_stream -r -- --nocapture
let save_dir =
aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
let model_path = format!("{}/OpenBMB/MiniCPM5-1B/", save_dir);
let message = r#"
{
"model": "minicpm5",
"messages": [
{
"role": "user",
"content": "什么是AI"
}
],
"enable_thinking": true
}
"#;
let mes: ChatCompletionParameters = serde_json::from_str(message)?;
let i_start = Instant::now();
let mut model = MiniCPM5GenerateModel::init(&model_path, None, None)?;
let i_duration = i_start.elapsed();
println!("Time elapsed in load model is: {:?}", i_duration);
let i_start = Instant::now();
let mut stream = pin!(model.generate_stream(mes)?);
while let Some(item) = stream.next().await {
println!("generate: \n {:?}", item);
}
let i_duration = i_start.elapsed();
println!("Time elapsed in generate is: {:?}", i_duration);
Ok(())
}
+49
View File
@@ -5,6 +5,55 @@ use aha::params::chat::ChatCompletionParameters;
use anyhow::Result;
use rocket::futures::StreamExt;
#[test]
fn paddleocr_vl_1_6_generate() -> Result<()> {
// RUST_BACKTRACE=1 cargo test -F cuda --test test_paddleocr_vl paddleocr_vl_1_6_generate -r -- --nocapture
let message = r#"
{
"model": "paddleocr_vl1.6",
"messages": [
{
"role": "user",
"content": [
{
"type": "image",
"image_url":
{
"url": "file://./assets/img/ocr_test1.png"
}
},
{
"type": "text",
"text": "Spotting:"
}
]
}
],
"stream": false
}
"#;
let save_dir =
aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
let model_path = format!("{}/PaddlePaddle/PaddleOCR-VL-1.6/", save_dir);
let mes: ChatCompletionParameters = serde_json::from_str(message)?;
let i_start = Instant::now();
let mut model = PaddleOCRVLGenerateModel::init(&model_path, None, None)?;
let i_duration = i_start.elapsed();
println!("Time elapsed in load model is: {:?}", i_duration);
let i_start = Instant::now();
let res = model.generate(mes)?;
let i_duration = i_start.elapsed();
println!("generate: \n {:?}", res);
if let Some(usage) = &res.usage {
let num_token = usage.total_tokens;
let duration_secs = i_duration.as_secs_f64();
let tps = num_token as f64 / duration_secs;
println!("Tokens per second (TPS): {:.2}", tps);
}
println!("Time elapsed in generate is: {:?}", i_duration);
Ok(())
}
#[test]
fn paddleocr_vl_1_5_generate() -> Result<()> {
// RUST_BACKTRACE=1 cargo test -F cuda --test test_paddleocr_vl paddleocr_vl_1_5_generate -r -- --nocapture
+1 -1
View File
@@ -42,7 +42,7 @@ fn qwen3_0_6b_generate() -> Result<()> {
#[tokio::test]
async fn qwen3_0_6b_stream() -> Result<()> {
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda qwen3_0_6b_stream -r -- --nocapture
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_qwen3 qwen3_0_6b_stream -r -- --nocapture
let save_dir =
aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
+6 -6
View File
@@ -104,7 +104,7 @@ fn qwen3vl_generate() -> Result<()> {
#[tokio::test]
async fn qwen3vl_stream() -> Result<()> {
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda,ffmpeg qwen3vl_stream -r -- --nocapture
// test with cuda: RUST_BACKTRACE=1 cargo test -F cuda --test test_qwen3vl qwen3vl_stream -r -- --nocapture
let save_dir =
aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
@@ -118,15 +118,15 @@ async fn qwen3vl_stream() -> Result<()> {
"role": "user",
"content": [
{
"type": "video",
"video_url":
"type": "image",
"image_url":
{
"url": "./assets/video/video_test.mp4"
"url": "file://./assets/img/ocr_test1.png"
}
},
},
{
"type": "text",
"text": "视频中发生了什么?"
"text": "OCR"
}
]
}