2024-08-15 09:17:36 +08:00
|
|
|
#
|
|
|
|
|
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
|
|
|
|
|
#
|
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
|
#
|
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
#
|
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
|
# limitations under the License.
|
|
|
|
|
#
|
2025-07-03 19:05:31 +08:00
|
|
|
import base64
|
2024-08-15 09:17:36 +08:00
|
|
|
import io
|
2025-07-03 19:05:31 +08:00
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import re
|
2026-07-16 10:49:02 +08:00
|
|
|
import struct
|
2024-08-15 09:17:36 +08:00
|
|
|
from abc import ABC
|
2025-12-02 11:17:31 +08:00
|
|
|
import tempfile
|
feat: add FuturMix as model provider (#14419)
## Summary
Add [FuturMix](https://futurmix.ai) as a new model provider. FuturMix is
an OpenAI-compatible unified AI gateway that provides access to 22+
models (GPT, Claude, Gemini, DeepSeek, and more) through a single API
endpoint and key.
- **API Base**: `https://futurmix.ai/v1` (OpenAI-compatible)
- **Supported capabilities**: Chat, Embedding, Image2Text, TTS,
Speech2Text, Rerank
### Changes
| File | Change |
|------|--------|
| `rag/llm/__init__.py` | Add `FuturMix` to `SupportedLiteLLMProvider`
enum, `FACTORY_DEFAULT_BASE_URL`, and `LITELLM_PROVIDER_PREFIX` |
| `rag/llm/chat_model.py` | Add `FuturMixChat(Base)` — follows
Astraflow/Avian pattern |
| `rag/llm/embedding_model.py` | Add `FuturMixEmbed(OpenAIEmbed)` —
follows Astraflow pattern |
| `rag/llm/cv_model.py` | Add `FuturMixCV(GptV4)` — follows
SILICONFLOW/OpenRouter pattern |
| `rag/llm/tts_model.py` | Add `FuturMixTTS(OpenAITTS)` — follows
CometAPI/DeerAPI pattern |
| `rag/llm/sequence2txt_model.py` | Add `FuturMixSeq2txt(GPTSeq2txt)` —
follows StepFun pattern |
| `rag/llm/rerank_model.py` | Add `FuturMixRerank(OpenAI_APIRerank)` |
| `conf/llm_factories.json` | Add factory config with 8 chat, 2
embedding, 1 image2text, 2 TTS, 1 speech2text models |
| `docs/guides/models/supported_models.mdx` | Add FuturMix to supported
models table |
### Models included
- **Chat**: claude-sonnet-4-20250514, claude-3.5-haiku, gpt-4o,
gpt-4o-mini, gemini-2.5-flash, gemini-2.0-flash, deepseek-chat,
deepseek-reasoner
- **Embedding**: text-embedding-3-small, text-embedding-3-large
- **Image2Text**: gpt-4o
- **TTS**: tts-1, tts-1-hd
- **Speech2Text**: whisper-1
## Test plan
- [ ] Verify FuturMix appears in the model provider list in RAGFlow UI
- [ ] Configure FuturMix with API key and test chat completion
- [ ] Test embedding model with document indexing
- [ ] Test image2text with a sample image
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 10:59:37 +08:00
|
|
|
import logging
|
feat(stt): support Fun-ASR-Flash in Tongyi-Qianwen provider (#16844)
## What this PR does
Adds support for Alibaba Cloud's hosted Fun-ASR-Flash snapshots to the
existing Tongyi-Qianwen speech-to-text provider.
- registers `fun-asr-flash-2026-06-15` as a speech-to-text model;
- routes only `fun-asr-flash*` models to the documented workspace-native
multimodal-generation endpoint;
- supports local audio through size-checked data URIs as well as
URL/data-URI inputs;
- uses the documented SSE response mode for incremental streaming
transcription;
- closes the streamed HTTP response on completion, failure, or early
consumer cancellation;
- preserves the existing `dashscope.MultiModalConversation` path for all
other Qwen audio models;
- keeps RAGFlow's existing synchronous and streaming adapter interfaces.
## Why
Fun-ASR-Flash does not use the legacy Qwen audio request shape currently
used by `QWenSeq2txt`. Its synchronous API expects `input_audio` at:
`/api/v1/services/aigc/multimodal-generation/generation`
Without a narrowly scoped adapter path, the hosted model cannot be
selected successfully through RAGFlow's Tongyi-Qianwen speech-to-text
provider.
Closes #16843.
## Compatibility
The new behavior is gated by the `fun-asr-flash` model-name prefix.
Existing Qwen audio models continue through the original code path
unchanged.
## Validation
- `pytest test/unit_test/rag/llm/test_sequence2txt_model.py`: 10 passed
- Ruff check: passed
- Ruff format check: passed
- `llm_factories.json` validation: passed
- Real hosted-API validation with WAV audio
- Real RAGFlow upload/indexing validation with MP3 audio
The unit tests cover the native Fun-ASR-Flash request, regression
behavior for the legacy Qwen path, SSE streaming, and early response
cleanup.
## Documentation
- https://help.aliyun.com/document_detail/2979031.html
- https://help.aliyun.com/document_detail/2869541.html
### Why a dedicated adapter path is necessary (official evidence)
Alibaba Cloud's [Fun-ASR RESTful API
reference](https://help.aliyun.com/en/model-studio/fun-asr-recorded-speech-recognition-http-api)
makes the incompatibilities with RAGFlow's existing Qwen audio path
explicit:
| Adapter change | Official API requirement | Why the existing path is
insufficient |
| --- | --- | --- |
| Call the workspace-native HTTP endpoint | The Fun-ASR-Flash
synchronous section states that SDK calls are not supported and
specifies `POST /api/v1/services/aigc/multimodal-generation/generation`.
| The existing adapter calls `dashscope.MultiModalConversation`, so a
direct HTTP path is required. |
| Use the `input_audio` message shape | `input.messages`, `content`,
`type: input_audio`, `input_audio`, and `input_audio.data` are
documented as required for an audio request. | The existing Qwen path
sends the legacy `audio` content shape, which does not match this API
contract. |
| Send `parameters.format` | The request schema marks `parameters` and
`format` as **Required**, and says the value must match the actual audio
format. | The legacy request has no Fun-ASR-Flash `parameters.format`
field, so the adapter must derive and send it. |
| Encode local files as Data URIs | `input_audio.data` accepts either a
public URL or a Base64 Data URI; the reference gives the exact
`data:{MIME_TYPE};base64,...` form. | RAGFlow supplies local file paths,
which the remote API cannot read directly. |
| Parse `output.text` | The documented non-streaming response returns
the accumulated transcription in `output.text`. | The legacy Qwen
response parser reads `output.choices[].message.content`, so a separate
response parser is required. |
| Enforce the Base64 input limit | The reference requires the
Base64-encoded audio to remain within the 10 MB input limit. | The
adapter checks encoded size before reading/sending local audio and
directs oversized inputs to the existing public-URL path. |
| Use SSE for streaming | The reference specifies `X-DashScope-SSE:
enable` and documents intermediate and final SSE events. | The adapter
parses those events instead of wrapping one blocking response as a
synthetic stream. |
| Release streamed responses | Streaming responses must be closed when
iteration completes or stops early. | A `finally` cleanup releases the
HTTP response on completion, errors, and consumer cancellation. |
`sample_rate` is documented as **Optional**. The implementation omits it
instead of declaring a fixed value that may not match remote or
compressed audio.
The [official speech-to-text model
list](https://help.aliyun.com/en/model-studio/asr-model/) separately
confirms that `fun-asr-flash-2026-06-15` is an offline HTTP model with a
five-minute audio limit.
---------
Signed-off-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
2026-07-16 09:37:37 +08:00
|
|
|
from urllib.parse import urlparse
|
2025-07-03 19:05:31 +08:00
|
|
|
|
|
|
|
|
import requests
|
2024-08-15 09:17:36 +08:00
|
|
|
from openai import OpenAI
|
2025-07-03 19:05:31 +08:00
|
|
|
from openai.lib.azure import AzureOpenAI
|
|
|
|
|
|
2025-11-03 08:50:05 +08:00
|
|
|
from common.token_utils import num_tokens_from_string
|
2026-07-09 15:53:06 +08:00
|
|
|
from rag.utils.url_utils import ensure_v1
|
2024-08-15 09:17:36 +08:00
|
|
|
|
2024-10-08 10:43:18 +08:00
|
|
|
|
2024-08-15 09:17:36 +08:00
|
|
|
class Base(ABC):
|
2025-08-07 08:45:37 +07:00
|
|
|
def __init__(self, key, model_name, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
Abstract base class constructor.
|
|
|
|
|
Parameters are not stored; initialization is left to subclasses.
|
|
|
|
|
"""
|
2024-08-15 09:17:36 +08:00
|
|
|
pass
|
|
|
|
|
|
2025-08-19 16:41:18 +08:00
|
|
|
def transcription(self, audio_path, **kwargs):
|
2026-03-11 16:47:06 +08:00
|
|
|
with open(audio_path, "rb") as audio_file:
|
|
|
|
|
transcription = self.client.audio.transcriptions.create(model=self.model_name, file=audio_file)
|
2024-08-15 09:17:36 +08:00
|
|
|
return transcription.text.strip(), num_tokens_from_string(transcription.text.strip())
|
2024-10-08 10:43:18 +08:00
|
|
|
|
2026-07-16 10:49:02 +08:00
|
|
|
@staticmethod
|
|
|
|
|
def _generate_test_wav(duration_seconds=0.5, sample_rate=16000):
|
|
|
|
|
"""Generate a minimal silent WAV file as bytes (pure stdlib, no dependencies)."""
|
|
|
|
|
n_samples = int(sample_rate * duration_seconds)
|
|
|
|
|
data_size = n_samples * 2 # 16-bit mono = 2 bytes per sample
|
|
|
|
|
header = struct.pack(
|
|
|
|
|
"<4sI4s4sIHHIIHH4sI",
|
|
|
|
|
b"RIFF",
|
|
|
|
|
36 + data_size,
|
|
|
|
|
b"WAVE",
|
|
|
|
|
b"fmt ",
|
|
|
|
|
16,
|
|
|
|
|
1,
|
|
|
|
|
1,
|
|
|
|
|
sample_rate,
|
|
|
|
|
sample_rate * 2,
|
|
|
|
|
2,
|
|
|
|
|
16,
|
|
|
|
|
b"data",
|
|
|
|
|
data_size,
|
|
|
|
|
)
|
|
|
|
|
return header + b"\x00" * data_size
|
|
|
|
|
|
|
|
|
|
def check_available(self) -> tuple[bool, str]:
|
|
|
|
|
"""Check if the ASR model is available by transcribing a minimal test WAV."""
|
|
|
|
|
try:
|
|
|
|
|
wav_data = self._generate_test_wav()
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
|
|
|
|
|
f.write(wav_data)
|
|
|
|
|
temp_path = f.name
|
|
|
|
|
try:
|
|
|
|
|
text, _ = self.transcription(temp_path)
|
|
|
|
|
if text.find("**ERROR**") >= 0:
|
|
|
|
|
return False, text.replace("**ERROR**: ", "").strip()
|
|
|
|
|
return True, ""
|
|
|
|
|
finally:
|
|
|
|
|
if os.path.exists(temp_path):
|
|
|
|
|
os.unlink(temp_path)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return False, str(e)
|
|
|
|
|
|
2024-10-08 10:43:18 +08:00
|
|
|
def audio2base64(self, audio):
|
2024-08-27 11:47:11 +08:00
|
|
|
if isinstance(audio, bytes):
|
|
|
|
|
return base64.b64encode(audio).decode("utf-8")
|
|
|
|
|
if isinstance(audio, io.BytesIO):
|
|
|
|
|
return base64.b64encode(audio.getvalue()).decode("utf-8")
|
|
|
|
|
raise TypeError("The input audio file should be in binary format.")
|
2024-08-15 09:17:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class GPTSeq2txt(Base):
|
2025-07-03 19:05:31 +08:00
|
|
|
_FACTORY_NAME = "OpenAI"
|
|
|
|
|
|
2025-08-19 16:41:18 +08:00
|
|
|
def __init__(self, key, model_name="whisper-1", base_url="https://api.openai.com/v1", **kwargs):
|
2024-12-08 14:21:12 +08:00
|
|
|
if not base_url:
|
|
|
|
|
base_url = "https://api.openai.com/v1"
|
2026-07-09 15:53:06 +08:00
|
|
|
self.base_url = ensure_v1(base_url)
|
|
|
|
|
self.client = OpenAI(api_key=key, base_url=self.base_url)
|
2024-08-15 09:17:36 +08:00
|
|
|
self.model_name = model_name
|
|
|
|
|
|
|
|
|
|
|
2026-02-05 12:47:04 +08:00
|
|
|
class StepFunSeq2txt(GPTSeq2txt):
|
|
|
|
|
_FACTORY_NAME = "StepFun"
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, model_name="step-asr", lang="Chinese", base_url="https://api.stepfun.com/v1", **kwargs):
|
|
|
|
|
if not base_url:
|
|
|
|
|
base_url = "https://api.stepfun.com/v1"
|
|
|
|
|
super().__init__(key, model_name=model_name, base_url=base_url, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
feat: add FuturMix as model provider (#14419)
## Summary
Add [FuturMix](https://futurmix.ai) as a new model provider. FuturMix is
an OpenAI-compatible unified AI gateway that provides access to 22+
models (GPT, Claude, Gemini, DeepSeek, and more) through a single API
endpoint and key.
- **API Base**: `https://futurmix.ai/v1` (OpenAI-compatible)
- **Supported capabilities**: Chat, Embedding, Image2Text, TTS,
Speech2Text, Rerank
### Changes
| File | Change |
|------|--------|
| `rag/llm/__init__.py` | Add `FuturMix` to `SupportedLiteLLMProvider`
enum, `FACTORY_DEFAULT_BASE_URL`, and `LITELLM_PROVIDER_PREFIX` |
| `rag/llm/chat_model.py` | Add `FuturMixChat(Base)` — follows
Astraflow/Avian pattern |
| `rag/llm/embedding_model.py` | Add `FuturMixEmbed(OpenAIEmbed)` —
follows Astraflow pattern |
| `rag/llm/cv_model.py` | Add `FuturMixCV(GptV4)` — follows
SILICONFLOW/OpenRouter pattern |
| `rag/llm/tts_model.py` | Add `FuturMixTTS(OpenAITTS)` — follows
CometAPI/DeerAPI pattern |
| `rag/llm/sequence2txt_model.py` | Add `FuturMixSeq2txt(GPTSeq2txt)` —
follows StepFun pattern |
| `rag/llm/rerank_model.py` | Add `FuturMixRerank(OpenAI_APIRerank)` |
| `conf/llm_factories.json` | Add factory config with 8 chat, 2
embedding, 1 image2text, 2 TTS, 1 speech2text models |
| `docs/guides/models/supported_models.mdx` | Add FuturMix to supported
models table |
### Models included
- **Chat**: claude-sonnet-4-20250514, claude-3.5-haiku, gpt-4o,
gpt-4o-mini, gemini-2.5-flash, gemini-2.0-flash, deepseek-chat,
deepseek-reasoner
- **Embedding**: text-embedding-3-small, text-embedding-3-large
- **Image2Text**: gpt-4o
- **TTS**: tts-1, tts-1-hd
- **Speech2Text**: whisper-1
## Test plan
- [ ] Verify FuturMix appears in the model provider list in RAGFlow UI
- [ ] Configure FuturMix with API key and test chat completion
- [ ] Test embedding model with document indexing
- [ ] Test image2text with a sample image
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 10:59:37 +08:00
|
|
|
class FuturMixSeq2txt(GPTSeq2txt):
|
|
|
|
|
_FACTORY_NAME = "FuturMix"
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, model_name="whisper-1", base_url="https://futurmix.ai/v1", **kwargs):
|
|
|
|
|
if not base_url:
|
|
|
|
|
base_url = "https://futurmix.ai/v1"
|
|
|
|
|
super().__init__(key, model_name=model_name, base_url=base_url, **kwargs)
|
|
|
|
|
logging.info("[FuturMix] Speech2Text initialized with model %s", model_name)
|
|
|
|
|
|
|
|
|
|
|
2024-08-15 09:17:36 +08:00
|
|
|
class QWenSeq2txt(Base):
|
2025-07-03 19:05:31 +08:00
|
|
|
_FACTORY_NAME = "Tongyi-Qianwen"
|
feat(stt): support Fun-ASR-Flash in Tongyi-Qianwen provider (#16844)
## What this PR does
Adds support for Alibaba Cloud's hosted Fun-ASR-Flash snapshots to the
existing Tongyi-Qianwen speech-to-text provider.
- registers `fun-asr-flash-2026-06-15` as a speech-to-text model;
- routes only `fun-asr-flash*` models to the documented workspace-native
multimodal-generation endpoint;
- supports local audio through size-checked data URIs as well as
URL/data-URI inputs;
- uses the documented SSE response mode for incremental streaming
transcription;
- closes the streamed HTTP response on completion, failure, or early
consumer cancellation;
- preserves the existing `dashscope.MultiModalConversation` path for all
other Qwen audio models;
- keeps RAGFlow's existing synchronous and streaming adapter interfaces.
## Why
Fun-ASR-Flash does not use the legacy Qwen audio request shape currently
used by `QWenSeq2txt`. Its synchronous API expects `input_audio` at:
`/api/v1/services/aigc/multimodal-generation/generation`
Without a narrowly scoped adapter path, the hosted model cannot be
selected successfully through RAGFlow's Tongyi-Qianwen speech-to-text
provider.
Closes #16843.
## Compatibility
The new behavior is gated by the `fun-asr-flash` model-name prefix.
Existing Qwen audio models continue through the original code path
unchanged.
## Validation
- `pytest test/unit_test/rag/llm/test_sequence2txt_model.py`: 10 passed
- Ruff check: passed
- Ruff format check: passed
- `llm_factories.json` validation: passed
- Real hosted-API validation with WAV audio
- Real RAGFlow upload/indexing validation with MP3 audio
The unit tests cover the native Fun-ASR-Flash request, regression
behavior for the legacy Qwen path, SSE streaming, and early response
cleanup.
## Documentation
- https://help.aliyun.com/document_detail/2979031.html
- https://help.aliyun.com/document_detail/2869541.html
### Why a dedicated adapter path is necessary (official evidence)
Alibaba Cloud's [Fun-ASR RESTful API
reference](https://help.aliyun.com/en/model-studio/fun-asr-recorded-speech-recognition-http-api)
makes the incompatibilities with RAGFlow's existing Qwen audio path
explicit:
| Adapter change | Official API requirement | Why the existing path is
insufficient |
| --- | --- | --- |
| Call the workspace-native HTTP endpoint | The Fun-ASR-Flash
synchronous section states that SDK calls are not supported and
specifies `POST /api/v1/services/aigc/multimodal-generation/generation`.
| The existing adapter calls `dashscope.MultiModalConversation`, so a
direct HTTP path is required. |
| Use the `input_audio` message shape | `input.messages`, `content`,
`type: input_audio`, `input_audio`, and `input_audio.data` are
documented as required for an audio request. | The existing Qwen path
sends the legacy `audio` content shape, which does not match this API
contract. |
| Send `parameters.format` | The request schema marks `parameters` and
`format` as **Required**, and says the value must match the actual audio
format. | The legacy request has no Fun-ASR-Flash `parameters.format`
field, so the adapter must derive and send it. |
| Encode local files as Data URIs | `input_audio.data` accepts either a
public URL or a Base64 Data URI; the reference gives the exact
`data:{MIME_TYPE};base64,...` form. | RAGFlow supplies local file paths,
which the remote API cannot read directly. |
| Parse `output.text` | The documented non-streaming response returns
the accumulated transcription in `output.text`. | The legacy Qwen
response parser reads `output.choices[].message.content`, so a separate
response parser is required. |
| Enforce the Base64 input limit | The reference requires the
Base64-encoded audio to remain within the 10 MB input limit. | The
adapter checks encoded size before reading/sending local audio and
directs oversized inputs to the existing public-URL path. |
| Use SSE for streaming | The reference specifies `X-DashScope-SSE:
enable` and documents intermediate and final SSE events. | The adapter
parses those events instead of wrapping one blocking response as a
synthetic stream. |
| Release streamed responses | Streaming responses must be closed when
iteration completes or stops early. | A `finally` cleanup releases the
HTTP response on completion, errors, and consumer cancellation. |
`sample_rate` is documented as **Optional**. The implementation omits it
instead of declaring a fixed value that may not match remote or
compressed audio.
The [official speech-to-text model
list](https://help.aliyun.com/en/model-studio/asr-model/) separately
confirms that `fun-asr-flash-2026-06-15` is an offline HTTP model with a
five-minute audio limit.
---------
Signed-off-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
2026-07-16 09:37:37 +08:00
|
|
|
_FUN_ASR_FLASH_PREFIX = "fun-asr-flash"
|
|
|
|
|
_FUN_ASR_BASE64_MAX_SIZE = 10 * 1024 * 1024
|
|
|
|
|
_DASHSCOPE_API_BASE = "https://dashscope.aliyuncs.com/api/v1"
|
|
|
|
|
_AUDIO_MIME_FORMATS = {
|
|
|
|
|
"audio/mpeg": "mp3",
|
|
|
|
|
"audio/mp3": "mp3",
|
|
|
|
|
"audio/wav": "wav",
|
|
|
|
|
"audio/wave": "wav",
|
|
|
|
|
"audio/x-wav": "wav",
|
|
|
|
|
}
|
2025-07-03 19:05:31 +08:00
|
|
|
|
feat(stt): support Fun-ASR-Flash in Tongyi-Qianwen provider (#16844)
## What this PR does
Adds support for Alibaba Cloud's hosted Fun-ASR-Flash snapshots to the
existing Tongyi-Qianwen speech-to-text provider.
- registers `fun-asr-flash-2026-06-15` as a speech-to-text model;
- routes only `fun-asr-flash*` models to the documented workspace-native
multimodal-generation endpoint;
- supports local audio through size-checked data URIs as well as
URL/data-URI inputs;
- uses the documented SSE response mode for incremental streaming
transcription;
- closes the streamed HTTP response on completion, failure, or early
consumer cancellation;
- preserves the existing `dashscope.MultiModalConversation` path for all
other Qwen audio models;
- keeps RAGFlow's existing synchronous and streaming adapter interfaces.
## Why
Fun-ASR-Flash does not use the legacy Qwen audio request shape currently
used by `QWenSeq2txt`. Its synchronous API expects `input_audio` at:
`/api/v1/services/aigc/multimodal-generation/generation`
Without a narrowly scoped adapter path, the hosted model cannot be
selected successfully through RAGFlow's Tongyi-Qianwen speech-to-text
provider.
Closes #16843.
## Compatibility
The new behavior is gated by the `fun-asr-flash` model-name prefix.
Existing Qwen audio models continue through the original code path
unchanged.
## Validation
- `pytest test/unit_test/rag/llm/test_sequence2txt_model.py`: 10 passed
- Ruff check: passed
- Ruff format check: passed
- `llm_factories.json` validation: passed
- Real hosted-API validation with WAV audio
- Real RAGFlow upload/indexing validation with MP3 audio
The unit tests cover the native Fun-ASR-Flash request, regression
behavior for the legacy Qwen path, SSE streaming, and early response
cleanup.
## Documentation
- https://help.aliyun.com/document_detail/2979031.html
- https://help.aliyun.com/document_detail/2869541.html
### Why a dedicated adapter path is necessary (official evidence)
Alibaba Cloud's [Fun-ASR RESTful API
reference](https://help.aliyun.com/en/model-studio/fun-asr-recorded-speech-recognition-http-api)
makes the incompatibilities with RAGFlow's existing Qwen audio path
explicit:
| Adapter change | Official API requirement | Why the existing path is
insufficient |
| --- | --- | --- |
| Call the workspace-native HTTP endpoint | The Fun-ASR-Flash
synchronous section states that SDK calls are not supported and
specifies `POST /api/v1/services/aigc/multimodal-generation/generation`.
| The existing adapter calls `dashscope.MultiModalConversation`, so a
direct HTTP path is required. |
| Use the `input_audio` message shape | `input.messages`, `content`,
`type: input_audio`, `input_audio`, and `input_audio.data` are
documented as required for an audio request. | The existing Qwen path
sends the legacy `audio` content shape, which does not match this API
contract. |
| Send `parameters.format` | The request schema marks `parameters` and
`format` as **Required**, and says the value must match the actual audio
format. | The legacy request has no Fun-ASR-Flash `parameters.format`
field, so the adapter must derive and send it. |
| Encode local files as Data URIs | `input_audio.data` accepts either a
public URL or a Base64 Data URI; the reference gives the exact
`data:{MIME_TYPE};base64,...` form. | RAGFlow supplies local file paths,
which the remote API cannot read directly. |
| Parse `output.text` | The documented non-streaming response returns
the accumulated transcription in `output.text`. | The legacy Qwen
response parser reads `output.choices[].message.content`, so a separate
response parser is required. |
| Enforce the Base64 input limit | The reference requires the
Base64-encoded audio to remain within the 10 MB input limit. | The
adapter checks encoded size before reading/sending local audio and
directs oversized inputs to the existing public-URL path. |
| Use SSE for streaming | The reference specifies `X-DashScope-SSE:
enable` and documents intermediate and final SSE events. | The adapter
parses those events instead of wrapping one blocking response as a
synthetic stream. |
| Release streamed responses | Streaming responses must be closed when
iteration completes or stops early. | A `finally` cleanup releases the
HTTP response on completion, errors, and consumer cancellation. |
`sample_rate` is documented as **Optional**. The implementation omits it
instead of declaring a fixed value that may not match remote or
compressed audio.
The [official speech-to-text model
list](https://help.aliyun.com/en/model-studio/asr-model/) separately
confirms that `fun-asr-flash-2026-06-15` is an offline HTTP model with a
five-minute audio limit.
---------
Signed-off-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
2026-07-16 09:37:37 +08:00
|
|
|
def __init__(self, key, model_name="qwen-audio-asr", base_url=None, **kwargs):
|
2024-08-15 09:17:36 +08:00
|
|
|
import dashscope
|
2025-07-03 19:05:31 +08:00
|
|
|
|
2024-08-15 09:17:36 +08:00
|
|
|
dashscope.api_key = key
|
feat(stt): support Fun-ASR-Flash in Tongyi-Qianwen provider (#16844)
## What this PR does
Adds support for Alibaba Cloud's hosted Fun-ASR-Flash snapshots to the
existing Tongyi-Qianwen speech-to-text provider.
- registers `fun-asr-flash-2026-06-15` as a speech-to-text model;
- routes only `fun-asr-flash*` models to the documented workspace-native
multimodal-generation endpoint;
- supports local audio through size-checked data URIs as well as
URL/data-URI inputs;
- uses the documented SSE response mode for incremental streaming
transcription;
- closes the streamed HTTP response on completion, failure, or early
consumer cancellation;
- preserves the existing `dashscope.MultiModalConversation` path for all
other Qwen audio models;
- keeps RAGFlow's existing synchronous and streaming adapter interfaces.
## Why
Fun-ASR-Flash does not use the legacy Qwen audio request shape currently
used by `QWenSeq2txt`. Its synchronous API expects `input_audio` at:
`/api/v1/services/aigc/multimodal-generation/generation`
Without a narrowly scoped adapter path, the hosted model cannot be
selected successfully through RAGFlow's Tongyi-Qianwen speech-to-text
provider.
Closes #16843.
## Compatibility
The new behavior is gated by the `fun-asr-flash` model-name prefix.
Existing Qwen audio models continue through the original code path
unchanged.
## Validation
- `pytest test/unit_test/rag/llm/test_sequence2txt_model.py`: 10 passed
- Ruff check: passed
- Ruff format check: passed
- `llm_factories.json` validation: passed
- Real hosted-API validation with WAV audio
- Real RAGFlow upload/indexing validation with MP3 audio
The unit tests cover the native Fun-ASR-Flash request, regression
behavior for the legacy Qwen path, SSE streaming, and early response
cleanup.
## Documentation
- https://help.aliyun.com/document_detail/2979031.html
- https://help.aliyun.com/document_detail/2869541.html
### Why a dedicated adapter path is necessary (official evidence)
Alibaba Cloud's [Fun-ASR RESTful API
reference](https://help.aliyun.com/en/model-studio/fun-asr-recorded-speech-recognition-http-api)
makes the incompatibilities with RAGFlow's existing Qwen audio path
explicit:
| Adapter change | Official API requirement | Why the existing path is
insufficient |
| --- | --- | --- |
| Call the workspace-native HTTP endpoint | The Fun-ASR-Flash
synchronous section states that SDK calls are not supported and
specifies `POST /api/v1/services/aigc/multimodal-generation/generation`.
| The existing adapter calls `dashscope.MultiModalConversation`, so a
direct HTTP path is required. |
| Use the `input_audio` message shape | `input.messages`, `content`,
`type: input_audio`, `input_audio`, and `input_audio.data` are
documented as required for an audio request. | The existing Qwen path
sends the legacy `audio` content shape, which does not match this API
contract. |
| Send `parameters.format` | The request schema marks `parameters` and
`format` as **Required**, and says the value must match the actual audio
format. | The legacy request has no Fun-ASR-Flash `parameters.format`
field, so the adapter must derive and send it. |
| Encode local files as Data URIs | `input_audio.data` accepts either a
public URL or a Base64 Data URI; the reference gives the exact
`data:{MIME_TYPE};base64,...` form. | RAGFlow supplies local file paths,
which the remote API cannot read directly. |
| Parse `output.text` | The documented non-streaming response returns
the accumulated transcription in `output.text`. | The legacy Qwen
response parser reads `output.choices[].message.content`, so a separate
response parser is required. |
| Enforce the Base64 input limit | The reference requires the
Base64-encoded audio to remain within the 10 MB input limit. | The
adapter checks encoded size before reading/sending local audio and
directs oversized inputs to the existing public-URL path. |
| Use SSE for streaming | The reference specifies `X-DashScope-SSE:
enable` and documents intermediate and final SSE events. | The adapter
parses those events instead of wrapping one blocking response as a
synthetic stream. |
| Release streamed responses | Streaming responses must be closed when
iteration completes or stops early. | A `finally` cleanup releases the
HTTP response on completion, errors, and consumer cancellation. |
`sample_rate` is documented as **Optional**. The implementation omits it
instead of declaring a fixed value that may not match remote or
compressed audio.
The [official speech-to-text model
list](https://help.aliyun.com/en/model-studio/asr-model/) separately
confirms that `fun-asr-flash-2026-06-15` is an offline HTTP model with a
five-minute audio limit.
---------
Signed-off-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
2026-07-16 09:37:37 +08:00
|
|
|
self.api_key = key
|
2024-08-15 09:17:36 +08:00
|
|
|
self.model_name = model_name
|
feat(stt): support Fun-ASR-Flash in Tongyi-Qianwen provider (#16844)
## What this PR does
Adds support for Alibaba Cloud's hosted Fun-ASR-Flash snapshots to the
existing Tongyi-Qianwen speech-to-text provider.
- registers `fun-asr-flash-2026-06-15` as a speech-to-text model;
- routes only `fun-asr-flash*` models to the documented workspace-native
multimodal-generation endpoint;
- supports local audio through size-checked data URIs as well as
URL/data-URI inputs;
- uses the documented SSE response mode for incremental streaming
transcription;
- closes the streamed HTTP response on completion, failure, or early
consumer cancellation;
- preserves the existing `dashscope.MultiModalConversation` path for all
other Qwen audio models;
- keeps RAGFlow's existing synchronous and streaming adapter interfaces.
## Why
Fun-ASR-Flash does not use the legacy Qwen audio request shape currently
used by `QWenSeq2txt`. Its synchronous API expects `input_audio` at:
`/api/v1/services/aigc/multimodal-generation/generation`
Without a narrowly scoped adapter path, the hosted model cannot be
selected successfully through RAGFlow's Tongyi-Qianwen speech-to-text
provider.
Closes #16843.
## Compatibility
The new behavior is gated by the `fun-asr-flash` model-name prefix.
Existing Qwen audio models continue through the original code path
unchanged.
## Validation
- `pytest test/unit_test/rag/llm/test_sequence2txt_model.py`: 10 passed
- Ruff check: passed
- Ruff format check: passed
- `llm_factories.json` validation: passed
- Real hosted-API validation with WAV audio
- Real RAGFlow upload/indexing validation with MP3 audio
The unit tests cover the native Fun-ASR-Flash request, regression
behavior for the legacy Qwen path, SSE streaming, and early response
cleanup.
## Documentation
- https://help.aliyun.com/document_detail/2979031.html
- https://help.aliyun.com/document_detail/2869541.html
### Why a dedicated adapter path is necessary (official evidence)
Alibaba Cloud's [Fun-ASR RESTful API
reference](https://help.aliyun.com/en/model-studio/fun-asr-recorded-speech-recognition-http-api)
makes the incompatibilities with RAGFlow's existing Qwen audio path
explicit:
| Adapter change | Official API requirement | Why the existing path is
insufficient |
| --- | --- | --- |
| Call the workspace-native HTTP endpoint | The Fun-ASR-Flash
synchronous section states that SDK calls are not supported and
specifies `POST /api/v1/services/aigc/multimodal-generation/generation`.
| The existing adapter calls `dashscope.MultiModalConversation`, so a
direct HTTP path is required. |
| Use the `input_audio` message shape | `input.messages`, `content`,
`type: input_audio`, `input_audio`, and `input_audio.data` are
documented as required for an audio request. | The existing Qwen path
sends the legacy `audio` content shape, which does not match this API
contract. |
| Send `parameters.format` | The request schema marks `parameters` and
`format` as **Required**, and says the value must match the actual audio
format. | The legacy request has no Fun-ASR-Flash `parameters.format`
field, so the adapter must derive and send it. |
| Encode local files as Data URIs | `input_audio.data` accepts either a
public URL or a Base64 Data URI; the reference gives the exact
`data:{MIME_TYPE};base64,...` form. | RAGFlow supplies local file paths,
which the remote API cannot read directly. |
| Parse `output.text` | The documented non-streaming response returns
the accumulated transcription in `output.text`. | The legacy Qwen
response parser reads `output.choices[].message.content`, so a separate
response parser is required. |
| Enforce the Base64 input limit | The reference requires the
Base64-encoded audio to remain within the 10 MB input limit. | The
adapter checks encoded size before reading/sending local audio and
directs oversized inputs to the existing public-URL path. |
| Use SSE for streaming | The reference specifies `X-DashScope-SSE:
enable` and documents intermediate and final SSE events. | The adapter
parses those events instead of wrapping one blocking response as a
synthetic stream. |
| Release streamed responses | Streaming responses must be closed when
iteration completes or stops early. | A `finally` cleanup releases the
HTTP response on completion, errors, and consumer cancellation. |
`sample_rate` is documented as **Optional**. The implementation omits it
instead of declaring a fixed value that may not match remote or
compressed audio.
The [official speech-to-text model
list](https://help.aliyun.com/en/model-studio/asr-model/) separately
confirms that `fun-asr-flash-2026-06-15` is an offline HTTP model with a
five-minute audio limit.
---------
Signed-off-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
2026-07-16 09:37:37 +08:00
|
|
|
self.base_url = (base_url or self._DASHSCOPE_API_BASE).rstrip("/")
|
2024-08-15 09:17:36 +08:00
|
|
|
|
2025-08-19 16:41:18 +08:00
|
|
|
def transcription(self, audio_path):
|
feat(stt): support Fun-ASR-Flash in Tongyi-Qianwen provider (#16844)
## What this PR does
Adds support for Alibaba Cloud's hosted Fun-ASR-Flash snapshots to the
existing Tongyi-Qianwen speech-to-text provider.
- registers `fun-asr-flash-2026-06-15` as a speech-to-text model;
- routes only `fun-asr-flash*` models to the documented workspace-native
multimodal-generation endpoint;
- supports local audio through size-checked data URIs as well as
URL/data-URI inputs;
- uses the documented SSE response mode for incremental streaming
transcription;
- closes the streamed HTTP response on completion, failure, or early
consumer cancellation;
- preserves the existing `dashscope.MultiModalConversation` path for all
other Qwen audio models;
- keeps RAGFlow's existing synchronous and streaming adapter interfaces.
## Why
Fun-ASR-Flash does not use the legacy Qwen audio request shape currently
used by `QWenSeq2txt`. Its synchronous API expects `input_audio` at:
`/api/v1/services/aigc/multimodal-generation/generation`
Without a narrowly scoped adapter path, the hosted model cannot be
selected successfully through RAGFlow's Tongyi-Qianwen speech-to-text
provider.
Closes #16843.
## Compatibility
The new behavior is gated by the `fun-asr-flash` model-name prefix.
Existing Qwen audio models continue through the original code path
unchanged.
## Validation
- `pytest test/unit_test/rag/llm/test_sequence2txt_model.py`: 10 passed
- Ruff check: passed
- Ruff format check: passed
- `llm_factories.json` validation: passed
- Real hosted-API validation with WAV audio
- Real RAGFlow upload/indexing validation with MP3 audio
The unit tests cover the native Fun-ASR-Flash request, regression
behavior for the legacy Qwen path, SSE streaming, and early response
cleanup.
## Documentation
- https://help.aliyun.com/document_detail/2979031.html
- https://help.aliyun.com/document_detail/2869541.html
### Why a dedicated adapter path is necessary (official evidence)
Alibaba Cloud's [Fun-ASR RESTful API
reference](https://help.aliyun.com/en/model-studio/fun-asr-recorded-speech-recognition-http-api)
makes the incompatibilities with RAGFlow's existing Qwen audio path
explicit:
| Adapter change | Official API requirement | Why the existing path is
insufficient |
| --- | --- | --- |
| Call the workspace-native HTTP endpoint | The Fun-ASR-Flash
synchronous section states that SDK calls are not supported and
specifies `POST /api/v1/services/aigc/multimodal-generation/generation`.
| The existing adapter calls `dashscope.MultiModalConversation`, so a
direct HTTP path is required. |
| Use the `input_audio` message shape | `input.messages`, `content`,
`type: input_audio`, `input_audio`, and `input_audio.data` are
documented as required for an audio request. | The existing Qwen path
sends the legacy `audio` content shape, which does not match this API
contract. |
| Send `parameters.format` | The request schema marks `parameters` and
`format` as **Required**, and says the value must match the actual audio
format. | The legacy request has no Fun-ASR-Flash `parameters.format`
field, so the adapter must derive and send it. |
| Encode local files as Data URIs | `input_audio.data` accepts either a
public URL or a Base64 Data URI; the reference gives the exact
`data:{MIME_TYPE};base64,...` form. | RAGFlow supplies local file paths,
which the remote API cannot read directly. |
| Parse `output.text` | The documented non-streaming response returns
the accumulated transcription in `output.text`. | The legacy Qwen
response parser reads `output.choices[].message.content`, so a separate
response parser is required. |
| Enforce the Base64 input limit | The reference requires the
Base64-encoded audio to remain within the 10 MB input limit. | The
adapter checks encoded size before reading/sending local audio and
directs oversized inputs to the existing public-URL path. |
| Use SSE for streaming | The reference specifies `X-DashScope-SSE:
enable` and documents intermediate and final SSE events. | The adapter
parses those events instead of wrapping one blocking response as a
synthetic stream. |
| Release streamed responses | Streaming responses must be closed when
iteration completes or stops early. | A `finally` cleanup releases the
HTTP response on completion, errors, and consumer cancellation. |
`sample_rate` is documented as **Optional**. The implementation omits it
instead of declaring a fixed value that may not match remote or
compressed audio.
The [official speech-to-text model
list](https://help.aliyun.com/en/model-studio/asr-model/) separately
confirms that `fun-asr-flash-2026-06-15` is an offline HTTP model with a
five-minute audio limit.
---------
Signed-off-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
2026-07-16 09:37:37 +08:00
|
|
|
# Fun-ASR-Flash uses DashScope's workspace-scoped native multimodal
|
|
|
|
|
# endpoint and payload instead of MultiModalConversation.
|
|
|
|
|
if self.model_name.startswith(self._FUN_ASR_FLASH_PREFIX):
|
|
|
|
|
return self._transcribe_fun_asr_flash(audio_path)
|
|
|
|
|
|
|
|
|
|
return self._transcribe_qwen_audio(audio_path)
|
|
|
|
|
|
|
|
|
|
def _transcribe_qwen_audio(self, audio_path):
|
2025-12-02 11:17:31 +08:00
|
|
|
import dashscope
|
2025-07-03 19:05:31 +08:00
|
|
|
|
2025-12-02 11:17:31 +08:00
|
|
|
if audio_path.startswith("http"):
|
|
|
|
|
audio_input = audio_path
|
|
|
|
|
else:
|
|
|
|
|
audio_input = f"file://{audio_path}"
|
2024-08-15 09:17:36 +08:00
|
|
|
|
2026-07-03 12:53:39 +08:00
|
|
|
messages = [{"role": "system", "content": [{"text": ""}]}, {"role": "user", "content": [{"audio": audio_input}]}]
|
|
|
|
|
|
|
|
|
|
resp = dashscope.MultiModalConversation.call(model=self.model_name, messages=messages, result_format="message", asr_options={"enable_lid": True, "enable_itn": False})
|
2025-12-02 11:17:31 +08:00
|
|
|
|
2025-08-19 16:41:18 +08:00
|
|
|
try:
|
2025-12-02 11:17:31 +08:00
|
|
|
text = resp["output"]["choices"][0]["message"].content[0]["text"]
|
2025-08-19 16:41:18 +08:00
|
|
|
except Exception as e:
|
2025-12-02 11:17:31 +08:00
|
|
|
text = "**ERROR**: " + str(e)
|
|
|
|
|
return text, num_tokens_from_string(text)
|
|
|
|
|
|
feat(stt): support Fun-ASR-Flash in Tongyi-Qianwen provider (#16844)
## What this PR does
Adds support for Alibaba Cloud's hosted Fun-ASR-Flash snapshots to the
existing Tongyi-Qianwen speech-to-text provider.
- registers `fun-asr-flash-2026-06-15` as a speech-to-text model;
- routes only `fun-asr-flash*` models to the documented workspace-native
multimodal-generation endpoint;
- supports local audio through size-checked data URIs as well as
URL/data-URI inputs;
- uses the documented SSE response mode for incremental streaming
transcription;
- closes the streamed HTTP response on completion, failure, or early
consumer cancellation;
- preserves the existing `dashscope.MultiModalConversation` path for all
other Qwen audio models;
- keeps RAGFlow's existing synchronous and streaming adapter interfaces.
## Why
Fun-ASR-Flash does not use the legacy Qwen audio request shape currently
used by `QWenSeq2txt`. Its synchronous API expects `input_audio` at:
`/api/v1/services/aigc/multimodal-generation/generation`
Without a narrowly scoped adapter path, the hosted model cannot be
selected successfully through RAGFlow's Tongyi-Qianwen speech-to-text
provider.
Closes #16843.
## Compatibility
The new behavior is gated by the `fun-asr-flash` model-name prefix.
Existing Qwen audio models continue through the original code path
unchanged.
## Validation
- `pytest test/unit_test/rag/llm/test_sequence2txt_model.py`: 10 passed
- Ruff check: passed
- Ruff format check: passed
- `llm_factories.json` validation: passed
- Real hosted-API validation with WAV audio
- Real RAGFlow upload/indexing validation with MP3 audio
The unit tests cover the native Fun-ASR-Flash request, regression
behavior for the legacy Qwen path, SSE streaming, and early response
cleanup.
## Documentation
- https://help.aliyun.com/document_detail/2979031.html
- https://help.aliyun.com/document_detail/2869541.html
### Why a dedicated adapter path is necessary (official evidence)
Alibaba Cloud's [Fun-ASR RESTful API
reference](https://help.aliyun.com/en/model-studio/fun-asr-recorded-speech-recognition-http-api)
makes the incompatibilities with RAGFlow's existing Qwen audio path
explicit:
| Adapter change | Official API requirement | Why the existing path is
insufficient |
| --- | --- | --- |
| Call the workspace-native HTTP endpoint | The Fun-ASR-Flash
synchronous section states that SDK calls are not supported and
specifies `POST /api/v1/services/aigc/multimodal-generation/generation`.
| The existing adapter calls `dashscope.MultiModalConversation`, so a
direct HTTP path is required. |
| Use the `input_audio` message shape | `input.messages`, `content`,
`type: input_audio`, `input_audio`, and `input_audio.data` are
documented as required for an audio request. | The existing Qwen path
sends the legacy `audio` content shape, which does not match this API
contract. |
| Send `parameters.format` | The request schema marks `parameters` and
`format` as **Required**, and says the value must match the actual audio
format. | The legacy request has no Fun-ASR-Flash `parameters.format`
field, so the adapter must derive and send it. |
| Encode local files as Data URIs | `input_audio.data` accepts either a
public URL or a Base64 Data URI; the reference gives the exact
`data:{MIME_TYPE};base64,...` form. | RAGFlow supplies local file paths,
which the remote API cannot read directly. |
| Parse `output.text` | The documented non-streaming response returns
the accumulated transcription in `output.text`. | The legacy Qwen
response parser reads `output.choices[].message.content`, so a separate
response parser is required. |
| Enforce the Base64 input limit | The reference requires the
Base64-encoded audio to remain within the 10 MB input limit. | The
adapter checks encoded size before reading/sending local audio and
directs oversized inputs to the existing public-URL path. |
| Use SSE for streaming | The reference specifies `X-DashScope-SSE:
enable` and documents intermediate and final SSE events. | The adapter
parses those events instead of wrapping one blocking response as a
synthetic stream. |
| Release streamed responses | Streaming responses must be closed when
iteration completes or stops early. | A `finally` cleanup releases the
HTTP response on completion, errors, and consumer cancellation. |
`sample_rate` is documented as **Optional**. The implementation omits it
instead of declaring a fixed value that may not match remote or
compressed audio.
The [official speech-to-text model
list](https://help.aliyun.com/en/model-studio/asr-model/) separately
confirms that `fun-asr-flash-2026-06-15` is an offline HTTP model with a
five-minute audio limit.
---------
Signed-off-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
2026-07-16 09:37:37 +08:00
|
|
|
@classmethod
|
|
|
|
|
def _fun_asr_audio_format(cls, audio_path):
|
|
|
|
|
"""Derive the Fun-ASR audio format from a data URI, URL, or path."""
|
|
|
|
|
|
|
|
|
|
if audio_path.startswith("data:"):
|
|
|
|
|
mime_type = audio_path[5:].split(";", 1)[0].lower()
|
|
|
|
|
if not mime_type.startswith("audio/"):
|
|
|
|
|
raise ValueError(f"Unsupported audio data URI MIME type: {mime_type or 'missing'}")
|
|
|
|
|
audio_format = cls._AUDIO_MIME_FORMATS.get(mime_type, mime_type.split("/", 1)[1].removeprefix("x-"))
|
|
|
|
|
else:
|
|
|
|
|
path = urlparse(audio_path).path if audio_path.startswith(("http://", "https://")) else audio_path
|
|
|
|
|
audio_format = os.path.splitext(path)[1].lower().lstrip(".")
|
|
|
|
|
|
|
|
|
|
if audio_format == "wave":
|
|
|
|
|
audio_format = "wav"
|
|
|
|
|
if not audio_format:
|
|
|
|
|
raise ValueError("Cannot determine audio format; use a URL/path extension or an audio data URI MIME type")
|
|
|
|
|
return audio_format
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _validate_fun_asr_base64_size(cls, encoded_size):
|
|
|
|
|
if encoded_size > cls._FUN_ASR_BASE64_MAX_SIZE:
|
|
|
|
|
raise ValueError("Fun-ASR-Flash Base64 audio exceeds the 10 MB encoded-input limit; provide a publicly accessible URL (for example, OSS) instead")
|
|
|
|
|
|
|
|
|
|
def _fun_asr_flash_request(self, audio_path, *, stream=False):
|
|
|
|
|
audio_format = self._fun_asr_audio_format(audio_path)
|
|
|
|
|
|
|
|
|
|
if audio_path.startswith(("http://", "https://")):
|
|
|
|
|
audio_input = audio_path
|
|
|
|
|
elif audio_path.startswith("data:"):
|
|
|
|
|
_, separator, encoded_audio = audio_path.partition(",")
|
|
|
|
|
if not separator:
|
|
|
|
|
raise ValueError("Invalid audio data URI: missing Base64 payload")
|
|
|
|
|
self._validate_fun_asr_base64_size(len(encoded_audio.encode("utf-8")))
|
|
|
|
|
audio_input = audio_path
|
|
|
|
|
else:
|
|
|
|
|
file_size = os.path.getsize(audio_path)
|
|
|
|
|
encoded_size = 4 * ((file_size + 2) // 3)
|
|
|
|
|
self._validate_fun_asr_base64_size(encoded_size)
|
|
|
|
|
mime_type = "audio/mpeg" if audio_format == "mp3" else f"audio/{audio_format}"
|
|
|
|
|
with open(audio_path, "rb") as audio_file:
|
|
|
|
|
audio_input = f"data:{mime_type};base64,{base64.b64encode(audio_file.read()).decode('utf-8')}"
|
|
|
|
|
|
|
|
|
|
api_base = self.base_url
|
|
|
|
|
if api_base.endswith("/compatible-mode/v1"):
|
|
|
|
|
api_base = api_base[: -len("/compatible-mode/v1")] + "/api/v1"
|
|
|
|
|
url = f"{api_base}/services/aigc/multimodal-generation/generation"
|
|
|
|
|
payload = {
|
|
|
|
|
"model": self.model_name,
|
|
|
|
|
"input": {
|
|
|
|
|
"messages": [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [{"type": "input_audio", "input_audio": {"data": audio_input}}],
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
# sample_rate is optional in the Fun-ASR-Flash API. Omitting it
|
|
|
|
|
# avoids declaring incorrect metadata for remote or compressed audio.
|
|
|
|
|
"parameters": {"format": audio_format},
|
|
|
|
|
}
|
|
|
|
|
headers = {
|
|
|
|
|
"Authorization": f"Bearer {self.api_key}",
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
"X-DashScope-SSE": "enable" if stream else "disable",
|
|
|
|
|
}
|
|
|
|
|
return url, headers, payload
|
|
|
|
|
|
|
|
|
|
def _transcribe_fun_asr_flash(self, audio_path):
|
|
|
|
|
try:
|
|
|
|
|
url, headers, payload = self._fun_asr_flash_request(audio_path)
|
|
|
|
|
|
|
|
|
|
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
result = response.json()
|
|
|
|
|
text = result.get("text") or result.get("output", {}).get("text")
|
|
|
|
|
if not text:
|
|
|
|
|
raise ValueError("Missing transcription text in Fun-ASR-Flash response")
|
|
|
|
|
text = text.strip()
|
|
|
|
|
return text, num_tokens_from_string(text)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.exception("Fun-ASR-Flash transcription failed for model %s", self.model_name)
|
|
|
|
|
return "**ERROR**: " + str(e), 0
|
|
|
|
|
|
|
|
|
|
def _stream_fun_asr_flash(self, audio_path):
|
|
|
|
|
response = None
|
|
|
|
|
try:
|
|
|
|
|
url, headers, payload = self._fun_asr_flash_request(audio_path, stream=True)
|
|
|
|
|
response = requests.post(url, headers=headers, json=payload, timeout=60, stream=True)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
full = ""
|
|
|
|
|
for line in response.iter_lines(decode_unicode=True):
|
|
|
|
|
if not line or not line.startswith("data:"):
|
|
|
|
|
continue
|
|
|
|
|
event_data = line[5:].strip()
|
|
|
|
|
if not event_data or event_data == "[DONE]":
|
|
|
|
|
continue
|
|
|
|
|
result = json.loads(event_data)
|
|
|
|
|
text = result.get("text") or result.get("output", {}).get("text")
|
|
|
|
|
if not text:
|
|
|
|
|
continue
|
|
|
|
|
full = text.strip()
|
|
|
|
|
yield {"event": "delta", "text": full}
|
|
|
|
|
|
|
|
|
|
if not full:
|
|
|
|
|
raise ValueError("Missing transcription text in Fun-ASR-Flash stream")
|
|
|
|
|
yield {"event": "final", "text": full}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logging.exception("Fun-ASR-Flash streaming transcription failed for model %s", self.model_name)
|
|
|
|
|
yield {"event": "error", "text": "**ERROR**: " + str(e)}
|
|
|
|
|
finally:
|
|
|
|
|
if response is not None:
|
|
|
|
|
response.close()
|
|
|
|
|
|
2025-12-02 11:17:31 +08:00
|
|
|
def stream_transcription(self, audio_path):
|
feat(stt): support Fun-ASR-Flash in Tongyi-Qianwen provider (#16844)
## What this PR does
Adds support for Alibaba Cloud's hosted Fun-ASR-Flash snapshots to the
existing Tongyi-Qianwen speech-to-text provider.
- registers `fun-asr-flash-2026-06-15` as a speech-to-text model;
- routes only `fun-asr-flash*` models to the documented workspace-native
multimodal-generation endpoint;
- supports local audio through size-checked data URIs as well as
URL/data-URI inputs;
- uses the documented SSE response mode for incremental streaming
transcription;
- closes the streamed HTTP response on completion, failure, or early
consumer cancellation;
- preserves the existing `dashscope.MultiModalConversation` path for all
other Qwen audio models;
- keeps RAGFlow's existing synchronous and streaming adapter interfaces.
## Why
Fun-ASR-Flash does not use the legacy Qwen audio request shape currently
used by `QWenSeq2txt`. Its synchronous API expects `input_audio` at:
`/api/v1/services/aigc/multimodal-generation/generation`
Without a narrowly scoped adapter path, the hosted model cannot be
selected successfully through RAGFlow's Tongyi-Qianwen speech-to-text
provider.
Closes #16843.
## Compatibility
The new behavior is gated by the `fun-asr-flash` model-name prefix.
Existing Qwen audio models continue through the original code path
unchanged.
## Validation
- `pytest test/unit_test/rag/llm/test_sequence2txt_model.py`: 10 passed
- Ruff check: passed
- Ruff format check: passed
- `llm_factories.json` validation: passed
- Real hosted-API validation with WAV audio
- Real RAGFlow upload/indexing validation with MP3 audio
The unit tests cover the native Fun-ASR-Flash request, regression
behavior for the legacy Qwen path, SSE streaming, and early response
cleanup.
## Documentation
- https://help.aliyun.com/document_detail/2979031.html
- https://help.aliyun.com/document_detail/2869541.html
### Why a dedicated adapter path is necessary (official evidence)
Alibaba Cloud's [Fun-ASR RESTful API
reference](https://help.aliyun.com/en/model-studio/fun-asr-recorded-speech-recognition-http-api)
makes the incompatibilities with RAGFlow's existing Qwen audio path
explicit:
| Adapter change | Official API requirement | Why the existing path is
insufficient |
| --- | --- | --- |
| Call the workspace-native HTTP endpoint | The Fun-ASR-Flash
synchronous section states that SDK calls are not supported and
specifies `POST /api/v1/services/aigc/multimodal-generation/generation`.
| The existing adapter calls `dashscope.MultiModalConversation`, so a
direct HTTP path is required. |
| Use the `input_audio` message shape | `input.messages`, `content`,
`type: input_audio`, `input_audio`, and `input_audio.data` are
documented as required for an audio request. | The existing Qwen path
sends the legacy `audio` content shape, which does not match this API
contract. |
| Send `parameters.format` | The request schema marks `parameters` and
`format` as **Required**, and says the value must match the actual audio
format. | The legacy request has no Fun-ASR-Flash `parameters.format`
field, so the adapter must derive and send it. |
| Encode local files as Data URIs | `input_audio.data` accepts either a
public URL or a Base64 Data URI; the reference gives the exact
`data:{MIME_TYPE};base64,...` form. | RAGFlow supplies local file paths,
which the remote API cannot read directly. |
| Parse `output.text` | The documented non-streaming response returns
the accumulated transcription in `output.text`. | The legacy Qwen
response parser reads `output.choices[].message.content`, so a separate
response parser is required. |
| Enforce the Base64 input limit | The reference requires the
Base64-encoded audio to remain within the 10 MB input limit. | The
adapter checks encoded size before reading/sending local audio and
directs oversized inputs to the existing public-URL path. |
| Use SSE for streaming | The reference specifies `X-DashScope-SSE:
enable` and documents intermediate and final SSE events. | The adapter
parses those events instead of wrapping one blocking response as a
synthetic stream. |
| Release streamed responses | Streaming responses must be closed when
iteration completes or stops early. | A `finally` cleanup releases the
HTTP response on completion, errors, and consumer cancellation. |
`sample_rate` is documented as **Optional**. The implementation omits it
instead of declaring a fixed value that may not match remote or
compressed audio.
The [official speech-to-text model
list](https://help.aliyun.com/en/model-studio/asr-model/) separately
confirms that `fun-asr-flash-2026-06-15` is an offline HTTP model with a
five-minute audio limit.
---------
Signed-off-by: LauraGPT <LauraGPT@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: LauraGPT <LauraGPT@users.noreply.github.com>
2026-07-16 09:37:37 +08:00
|
|
|
if self.model_name.startswith(self._FUN_ASR_FLASH_PREFIX):
|
|
|
|
|
yield from self._stream_fun_asr_flash(audio_path)
|
|
|
|
|
return
|
|
|
|
|
|
2025-12-02 11:17:31 +08:00
|
|
|
import dashscope
|
|
|
|
|
|
|
|
|
|
if audio_path.startswith("http"):
|
|
|
|
|
audio_input = audio_path
|
|
|
|
|
else:
|
|
|
|
|
audio_input = f"file://{audio_path}"
|
|
|
|
|
|
2026-07-03 12:53:39 +08:00
|
|
|
messages = [{"role": "system", "content": [{"text": ""}]}, {"role": "user", "content": [{"audio": audio_input}]}]
|
|
|
|
|
|
|
|
|
|
stream = dashscope.MultiModalConversation.call(model=self.model_name, messages=messages, result_format="message", stream=True, asr_options={"enable_lid": True, "enable_itn": False})
|
2024-08-15 09:17:36 +08:00
|
|
|
|
2025-12-02 11:17:31 +08:00
|
|
|
full = ""
|
|
|
|
|
for chunk in stream:
|
|
|
|
|
try:
|
|
|
|
|
piece = chunk["output"]["choices"][0]["message"].content[0]["text"]
|
|
|
|
|
full = piece
|
|
|
|
|
yield {"event": "delta", "text": piece}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
yield {"event": "error", "text": str(e)}
|
|
|
|
|
|
|
|
|
|
yield {"event": "final", "text": full}
|
2024-08-15 09:17:36 +08:00
|
|
|
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2024-08-15 09:17:36 +08:00
|
|
|
class AzureSeq2txt(Base):
|
2025-07-03 19:05:31 +08:00
|
|
|
_FACTORY_NAME = "Azure-OpenAI"
|
|
|
|
|
|
2024-08-15 09:17:36 +08:00
|
|
|
def __init__(self, key, model_name, lang="Chinese", **kwargs):
|
2026-07-09 15:53:06 +08:00
|
|
|
self.base_url = ensure_v1(kwargs["base_url"])
|
|
|
|
|
self.client = AzureOpenAI(api_key=key, azure_endpoint=self.base_url, api_version="2024-02-01")
|
2024-08-15 09:17:36 +08:00
|
|
|
self.model_name = model_name
|
|
|
|
|
self.lang = lang
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class XinferenceSeq2txt(Base):
|
2025-07-03 19:05:31 +08:00
|
|
|
_FACTORY_NAME = "Xinference"
|
|
|
|
|
|
2024-10-22 11:38:37 +08:00
|
|
|
def __init__(self, key, model_name="whisper-small", **kwargs):
|
2026-07-09 15:53:06 +08:00
|
|
|
self.base_url = ensure_v1(kwargs["base_url"]) if kwargs.get("base_url") else None
|
2024-08-15 09:17:36 +08:00
|
|
|
self.model_name = model_name
|
2024-10-16 10:21:08 +08:00
|
|
|
self.key = key
|
2024-08-27 11:47:11 +08:00
|
|
|
|
2024-10-08 10:43:18 +08:00
|
|
|
def transcription(self, audio, language="zh", prompt=None, response_format="json", temperature=0.7):
|
|
|
|
|
if isinstance(audio, str):
|
2026-03-11 16:47:06 +08:00
|
|
|
with open(audio, "rb") as audio_file:
|
|
|
|
|
audio_data = audio_file.read()
|
2024-10-08 10:43:18 +08:00
|
|
|
audio_file_name = audio.split("/")[-1]
|
|
|
|
|
else:
|
|
|
|
|
audio_data = audio
|
|
|
|
|
audio_file_name = "audio.wav"
|
|
|
|
|
|
2025-07-03 19:05:31 +08:00
|
|
|
payload = {"model": self.model_name, "language": language, "prompt": prompt, "response_format": response_format, "temperature": temperature}
|
2024-10-08 10:43:18 +08:00
|
|
|
|
2025-07-03 19:05:31 +08:00
|
|
|
files = {"file": (audio_file_name, audio_data, "audio/wav")}
|
2024-10-08 10:43:18 +08:00
|
|
|
|
|
|
|
|
try:
|
2026-05-11 11:19:07 +08:00
|
|
|
response = requests.post(f"{self.base_url}/v1/audio/transcriptions", files=files, data=payload, timeout=60)
|
2024-10-08 10:43:18 +08:00
|
|
|
response.raise_for_status()
|
|
|
|
|
result = response.json()
|
|
|
|
|
|
2025-07-03 19:05:31 +08:00
|
|
|
if "text" in result:
|
|
|
|
|
transcription_text = result["text"].strip()
|
2024-10-08 10:43:18 +08:00
|
|
|
return transcription_text, num_tokens_from_string(transcription_text)
|
|
|
|
|
else:
|
|
|
|
|
return "**ERROR**: Failed to retrieve transcription.", 0
|
|
|
|
|
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
return f"**ERROR**: {str(e)}", 0
|
|
|
|
|
|
2024-08-27 11:47:11 +08:00
|
|
|
|
|
|
|
|
class TencentCloudSeq2txt(Base):
|
2025-07-03 19:05:31 +08:00
|
|
|
_FACTORY_NAME = "Tencent Cloud"
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, model_name="16k_zh", base_url="https://asr.tencentcloudapi.com"):
|
2024-08-27 11:47:11 +08:00
|
|
|
from tencentcloud.asr.v20190614 import asr_client
|
2025-07-03 19:05:31 +08:00
|
|
|
from tencentcloud.common import credential
|
2024-08-27 11:47:11 +08:00
|
|
|
|
|
|
|
|
key = json.loads(key)
|
|
|
|
|
sid = key.get("tencent_cloud_sid", "")
|
|
|
|
|
sk = key.get("tencent_cloud_sk", "")
|
|
|
|
|
cred = credential.Credential(sid, sk)
|
|
|
|
|
self.client = asr_client.AsrClient(cred, "")
|
|
|
|
|
self.model_name = model_name
|
|
|
|
|
|
2026-07-16 10:49:02 +08:00
|
|
|
def check_available(self) -> tuple[bool, str]:
|
|
|
|
|
"""Tencent Cloud ASR transcription expects raw bytes, not a file path."""
|
|
|
|
|
try:
|
|
|
|
|
wav_data = self._generate_test_wav()
|
|
|
|
|
text, _ = self.transcription(wav_data)
|
|
|
|
|
if text.find("**ERROR**") >= 0:
|
|
|
|
|
return False, text.replace("**ERROR**: ", "").strip()
|
|
|
|
|
return True, ""
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return False, str(e)
|
|
|
|
|
|
2024-08-27 11:47:11 +08:00
|
|
|
def transcription(self, audio, max_retries=60, retry_interval=5):
|
2025-07-03 19:05:31 +08:00
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
from tencentcloud.asr.v20190614 import models
|
2024-08-27 11:47:11 +08:00
|
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import (
|
|
|
|
|
TencentCloudSDKException,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
b64 = self.audio2base64(audio)
|
|
|
|
|
try:
|
|
|
|
|
# dispatch disk
|
|
|
|
|
req = models.CreateRecTaskRequest()
|
|
|
|
|
params = {
|
|
|
|
|
"EngineModelType": self.model_name,
|
|
|
|
|
"ChannelNum": 1,
|
|
|
|
|
"ResTextFormat": 0,
|
|
|
|
|
"SourceType": 1,
|
|
|
|
|
"Data": b64,
|
|
|
|
|
}
|
|
|
|
|
req.from_json_string(json.dumps(params))
|
|
|
|
|
resp = self.client.CreateRecTask(req)
|
|
|
|
|
|
|
|
|
|
# loop query
|
|
|
|
|
req = models.DescribeTaskStatusRequest()
|
|
|
|
|
params = {"TaskId": resp.Data.TaskId}
|
|
|
|
|
req.from_json_string(json.dumps(params))
|
|
|
|
|
retries = 0
|
|
|
|
|
while retries < max_retries:
|
|
|
|
|
resp = self.client.DescribeTaskStatus(req)
|
|
|
|
|
if resp.Data.StatusStr == "success":
|
2025-07-03 19:05:31 +08:00
|
|
|
text = re.sub(r"\[\d+:\d+\.\d+,\d+:\d+\.\d+\]\s*", "", resp.Data.Result).strip()
|
2024-08-27 11:47:11 +08:00
|
|
|
return text, num_tokens_from_string(text)
|
|
|
|
|
elif resp.Data.StatusStr == "failed":
|
|
|
|
|
return (
|
|
|
|
|
"**ERROR**: Failed to retrieve speech recognition results.",
|
|
|
|
|
0,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
time.sleep(retry_interval)
|
|
|
|
|
retries += 1
|
|
|
|
|
return "**ERROR**: Max retries exceeded. Task may still be processing.", 0
|
|
|
|
|
|
|
|
|
|
except TencentCloudSDKException as e:
|
|
|
|
|
return "**ERROR**: " + str(e), 0
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return "**ERROR**: " + str(e), 0
|
2025-01-15 14:15:58 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class GPUStackSeq2txt(Base):
|
2025-07-03 19:05:31 +08:00
|
|
|
_FACTORY_NAME = "GPUStack"
|
|
|
|
|
|
2025-01-15 14:15:58 +08:00
|
|
|
def __init__(self, key, model_name, base_url):
|
|
|
|
|
if not base_url:
|
|
|
|
|
raise ValueError("url cannot be None")
|
2025-03-31 15:33:52 +08:00
|
|
|
if base_url.split("/")[-1] != "v1":
|
|
|
|
|
base_url = os.path.join(base_url, "v1")
|
2025-01-15 14:15:58 +08:00
|
|
|
self.base_url = base_url
|
|
|
|
|
self.model_name = model_name
|
|
|
|
|
self.key = key
|
2025-06-30 09:22:31 +08:00
|
|
|
|
2026-07-16 10:49:02 +08:00
|
|
|
def check_available(self) -> tuple[bool, str]:
|
|
|
|
|
"""GPUStack ASR transcription endpoint is not yet implemented."""
|
|
|
|
|
return False, "GPUStack ASR transcription is not yet implemented"
|
|
|
|
|
|
2025-06-30 09:22:31 +08:00
|
|
|
|
|
|
|
|
class GiteeSeq2txt(Base):
|
2025-07-03 19:05:31 +08:00
|
|
|
_FACTORY_NAME = "GiteeAI"
|
|
|
|
|
|
2025-09-18 09:31:32 +08:00
|
|
|
def __init__(self, key, model_name="whisper-1", base_url="https://ai.gitee.com/v1/", **kwargs):
|
2025-06-30 09:22:31 +08:00
|
|
|
if not base_url:
|
|
|
|
|
base_url = "https://ai.gitee.com/v1/"
|
2026-07-09 15:53:06 +08:00
|
|
|
self.base_url = ensure_v1(base_url)
|
|
|
|
|
self.client = OpenAI(api_key=key, base_url=self.base_url)
|
2025-07-03 19:05:31 +08:00
|
|
|
self.model_name = model_name
|
|
|
|
|
|
2025-08-19 16:41:18 +08:00
|
|
|
|
2025-07-23 18:10:35 +08:00
|
|
|
class DeepInfraSeq2txt(Base):
|
|
|
|
|
_FACTORY_NAME = "DeepInfra"
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, model_name, base_url="https://api.deepinfra.com/v1/openai", **kwargs):
|
|
|
|
|
if not base_url:
|
|
|
|
|
base_url = "https://api.deepinfra.com/v1/openai"
|
2026-07-09 15:53:06 +08:00
|
|
|
self.base_url = ensure_v1(base_url)
|
|
|
|
|
self.client = OpenAI(api_key=key, base_url=self.base_url)
|
2025-07-23 18:10:35 +08:00
|
|
|
self.model_name = model_name
|
2025-10-14 09:32:45 +08:00
|
|
|
|
|
|
|
|
|
2025-09-26 10:50:56 +08:00
|
|
|
class CometAPISeq2txt(Base):
|
2025-09-18 09:51:29 +08:00
|
|
|
_FACTORY_NAME = "CometAPI"
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, model_name="whisper-1", base_url="https://api.cometapi.com/v1", **kwargs):
|
|
|
|
|
if not base_url:
|
|
|
|
|
base_url = "https://api.cometapi.com/v1"
|
2026-07-09 15:53:06 +08:00
|
|
|
self.base_url = ensure_v1(base_url)
|
|
|
|
|
self.client = OpenAI(api_key=key, base_url=self.base_url)
|
2025-09-18 09:51:29 +08:00
|
|
|
self.model_name = model_name
|
2025-10-14 09:32:45 +08:00
|
|
|
|
|
|
|
|
|
2025-10-09 11:14:49 +08:00
|
|
|
class DeerAPISeq2txt(Base):
|
|
|
|
|
_FACTORY_NAME = "DeerAPI"
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, model_name="whisper-1", base_url="https://api.deerapi.com/v1", **kwargs):
|
|
|
|
|
if not base_url:
|
|
|
|
|
base_url = "https://api.deerapi.com/v1"
|
2026-07-09 15:53:06 +08:00
|
|
|
self.base_url = ensure_v1(base_url)
|
|
|
|
|
self.client = OpenAI(api_key=key, base_url=self.base_url)
|
2025-10-09 11:14:49 +08:00
|
|
|
self.model_name = model_name
|
2025-10-14 09:32:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ZhipuSeq2txt(Base):
|
|
|
|
|
_FACTORY_NAME = "ZHIPU-AI"
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, model_name="glm-asr", base_url="https://open.bigmodel.cn/api/paas/v4", **kwargs):
|
|
|
|
|
if not base_url:
|
|
|
|
|
base_url = "https://open.bigmodel.cn/api/paas/v4"
|
|
|
|
|
self.base_url = base_url
|
|
|
|
|
self.api_key = key
|
|
|
|
|
self.model_name = model_name
|
|
|
|
|
self.gen_conf = kwargs.get("gen_conf", {})
|
|
|
|
|
self.stream = kwargs.get("stream", False)
|
|
|
|
|
|
2025-12-02 11:17:31 +08:00
|
|
|
def _convert_to_wav(self, input_path):
|
|
|
|
|
ext = os.path.splitext(input_path)[1].lower()
|
|
|
|
|
if ext in [".wav", ".mp3"]:
|
|
|
|
|
return input_path
|
|
|
|
|
fd, out_path = tempfile.mkstemp(suffix=".wav")
|
|
|
|
|
os.close(fd)
|
|
|
|
|
try:
|
|
|
|
|
import ffmpeg
|
|
|
|
|
import imageio_ffmpeg as ffmpeg_exe
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2025-12-02 11:17:31 +08:00
|
|
|
ffmpeg_path = ffmpeg_exe.get_ffmpeg_exe()
|
2026-07-03 12:53:39 +08:00
|
|
|
(ffmpeg.input(input_path).output(out_path, ar=16000, ac=1).overwrite_output().run(cmd=ffmpeg_path, quiet=True))
|
2025-12-02 11:17:31 +08:00
|
|
|
return out_path
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise RuntimeError(f"audio convert failed: {e}")
|
|
|
|
|
|
2025-10-14 09:32:45 +08:00
|
|
|
def transcription(self, audio_path):
|
|
|
|
|
payload = {
|
|
|
|
|
"model": self.model_name,
|
|
|
|
|
"temperature": str(self.gen_conf.get("temperature", 0.75)) or "0.75",
|
|
|
|
|
"stream": self.stream,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
headers = {"Authorization": f"Bearer {self.api_key}"}
|
2025-12-02 11:17:31 +08:00
|
|
|
converted = self._convert_to_wav(audio_path)
|
|
|
|
|
|
|
|
|
|
with open(converted, "rb") as audio_file:
|
2025-10-14 09:32:45 +08:00
|
|
|
files = {"file": audio_file}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
response = requests.post(
|
|
|
|
|
url=f"{self.base_url}/audio/transcriptions",
|
|
|
|
|
data=payload,
|
|
|
|
|
files=files,
|
|
|
|
|
headers=headers,
|
2026-05-11 11:19:07 +08:00
|
|
|
timeout=60,
|
2025-10-14 09:32:45 +08:00
|
|
|
)
|
|
|
|
|
body = response.json()
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
full_content = body["text"]
|
|
|
|
|
return full_content, num_tokens_from_string(full_content)
|
|
|
|
|
else:
|
|
|
|
|
error = body["error"]
|
|
|
|
|
return f"**ERROR**: code: {error['code']}, message: {error['message']}", 0
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return "**ERROR**: " + str(e), 0
|
2026-03-06 02:37:27 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class RAGconSeq2txt(Base):
|
|
|
|
|
"""
|
|
|
|
|
RAGcon Sequence2Text Provider - routes through LiteLLM proxy
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2026-03-06 02:37:27 +01:00
|
|
|
Speech-to-text models routed through LiteLLM.
|
|
|
|
|
Default Base URL: https://connect.ragcon.com/v1
|
|
|
|
|
"""
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2026-03-06 02:37:27 +01:00
|
|
|
_FACTORY_NAME = "RAGcon"
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2026-03-06 02:37:27 +01:00
|
|
|
def __init__(self, key, model_name, base_url=None, lang="English", **kwargs):
|
|
|
|
|
# Use provided base_url or fallback to default
|
|
|
|
|
if not base_url:
|
|
|
|
|
base_url = "https://connect.ragcon.com/v1"
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2026-07-09 15:53:06 +08:00
|
|
|
self.base_url = ensure_v1(base_url)
|
2026-03-06 02:37:27 +01:00
|
|
|
self.model_name = model_name
|
|
|
|
|
self.key = key
|
|
|
|
|
self.lang = lang
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2026-03-06 02:37:27 +01:00
|
|
|
self.client = OpenAI(api_key=key, base_url=self.base_url)
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2026-03-06 02:37:27 +01:00
|
|
|
def transcription(self, audio_path, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
Transcribe audio file using RAGcon's OpenAI-compatible API.
|
|
|
|
|
Uses Whisper's automatic language detection for German and English audio.
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2026-03-06 02:37:27 +01:00
|
|
|
Args:
|
|
|
|
|
audio_path: Path to the audio file
|
|
|
|
|
**kwargs: Additional parameters (currently unused but maintained for compatibility)
|
2026-07-03 12:53:39 +08:00
|
|
|
|
2026-03-06 02:37:27 +01:00
|
|
|
Returns:
|
|
|
|
|
tuple: (transcribed_text, token_count)
|
|
|
|
|
"""
|
|
|
|
|
with open(audio_path, "rb") as audio_file:
|
|
|
|
|
# Call RAGcon API - Whisper will auto-detect language
|
2026-07-03 12:53:39 +08:00
|
|
|
transcription = self.client.audio.transcriptions.create(model=self.model_name, file=audio_file)
|
|
|
|
|
|
2026-03-06 02:37:27 +01:00
|
|
|
# Return text and token count
|
|
|
|
|
text = transcription.text.strip()
|
|
|
|
|
return text, num_tokens_from_string(text)
|
Feat: Add New API model provider for OpenAI-compatible gateways (#15991)
## Summary
Add support for **"New API"** as a model provider, enabling connection
to [New API](https://github.com/QuantumNous/new-api) /
[one-api](https://github.com/songquanpeng/one-api) compatible gateways
that aggregate multiple LLM backends behind a unified OpenAI-compatible
`/v1` endpoint.
### Features
- **All model types**: Chat, Embedding, Rerank, Image2Text, TTS,
Speech2Text
- **List Models discovery**: `NewAPI(OpenAIAPICompatible)` class in
`model_meta.py` queries the gateway's `/v1/models` to auto-discover
available models via the native `GET /api/v1/providers/<name>/models`
endpoint
- **Model parameter editing**: Pencil icon on each discovered model row
to edit `model_type`, `max_tokens`, and `features` (e.g. tool call
support) before submitting
- **Custom model addition**: "Add Custom Model" button at the bottom of
the List Models dropdown for models not returned by the API
- **Gear icon settings**: Enabled the Settings gear button on provider
instances to manage models on existing instances (viewMode)
- **viewMode credential passthrough**: Fixed List Models in viewMode —
merges `initialValues` credentials when `api_key`/`base_url` fields are
hidden by `hideWhenInstanceExists`
### Changes
**Backend** (8 files):
- `rag/llm/chat_model.py` — `NewAPIChat(Base)` class
- `rag/llm/embedding_model.py` — `NewAPIEmbed(OpenAIEmbed)` class (no
auto `/v1` append)
- `rag/llm/rerank_model.py` — `NewAPIRerank(Base)` class (uses `/rerank`
endpoint)
- `rag/llm/cv_model.py` — `NewAPICv(GptV4)` class
- `rag/llm/tts_model.py` — `NewAPITTS(OpenAITTS)` class
- `rag/llm/sequence2txt_model.py` — `NewAPISeq2txt(GPTSeq2txt)` class
- `rag/llm/model_meta.py` — `NewAPI(OpenAIAPICompatible)` class for List
Models discovery
- `conf/llm_factories.json` — New API factory entry with all model type
tags
**Frontend** (8 files + 1 new SVG):
- `web/src/assets/svg/llm/new-api.svg` — New API logo icon
- `web/src/constants/llm.ts` — `LLMFactory.NewAPI` enum + `IconMap`
entry
- `web/src/components/svg-icon.tsx` — `NewAPI` added to `svgIcons`
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/local-llm-configs.ts`
— New API `buildLocalConfig`
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/constants.ts`
— `LIST_MODEL_PROVIDERS` includes NewAPI
- `web/src/pages/user-setting/setting-model/components/used-model.tsx` —
Enable Settings gear button
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts`
— viewMode credential merge + model editing state/handlers
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-options.tsx`
— Pencil edit icon per model row
-
`web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx`
— `AddCustomModelDialog` import + edit dialog rendering
**Note on Go implementation**: A Go model driver (`NewAPIModel`
delegating to `OpenAIModel`) has been prepared but is deferred until the
Go runtime is enabled in a future release (current v0.26.0 images use
`API_PROXY_SCHEME=python` and do not compile Go binaries). Will submit
as a follow-up PR.
## Related
- Depends on: #15996 (provider instance API improvements — server-side
credential lookup, idempotent `add_model`, security fixes — required for
viewMode gear icon and batch model submission)
## Test plan
- [ ] Add New API provider with api_key and base_url pointing to an
OpenAI-compatible gateway
- [ ] Click "List Models" — should discover and display available models
from `/v1/models`
- [ ] Click pencil icon on a model — should open edit dialog to change
model_type, max_tokens, features
- [ ] Select multiple models and click OK — should add all selected
models
- [ ] Click gear icon on the added instance — should open viewMode with
List Models working
- [ ] In viewMode, select new models including pre-existing ones, click
OK — should succeed (requires #15996)
- [ ] Verify all model types work: create a Chat assistant, Embedding
KB, Rerank setting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Tim Wang <wanghualoong@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-26 18:47:20 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class NewAPISeq2txt(GPTSeq2txt):
|
|
|
|
|
_FACTORY_NAME = "New API"
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, model_name="whisper-1", base_url="", **kwargs):
|
|
|
|
|
if not base_url:
|
|
|
|
|
raise ValueError("url cannot be None")
|
|
|
|
|
model_name = model_name.split("___")[0]
|
|
|
|
|
super().__init__(key, model_name=model_name, base_url=base_url, **kwargs)
|
2026-07-15 19:02:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class FunASRSeq2txt(GPTSeq2txt):
|
|
|
|
|
"""FunASR speech-to-text provider for its OpenAI-compatible API."""
|
|
|
|
|
|
|
|
|
|
_FACTORY_NAME = "FunASR"
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, model_name="sensevoice", base_url="http://localhost:8000/v1", **kwargs):
|
|
|
|
|
"""Initialize a client for a FunASR OpenAI-compatible endpoint."""
|
|
|
|
|
if not base_url:
|
|
|
|
|
base_url = "http://localhost:8000/v1"
|
|
|
|
|
super().__init__(key=key or "funasr", model_name=model_name, base_url=base_url, **kwargs)
|
|
|
|
|
logging.info("[FunASR] Speech2Text initialized with model %s at %s", model_name, self.base_url)
|