feat(knowledge): add configurable ASR model in knowledge system settings

Expose optional speech-to-text model on the knowledge-base model tab and
resolve media transcription from knowledge LLM config instead of workbench ASR.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
open-master
2026-07-06 17:42:05 +08:00
parent 4bbbcad77a
commit ccc8b9b397
10 changed files with 75 additions and 8 deletions
@@ -91,11 +91,11 @@ class KnowledgeMediaTranscriptionService:
@classmethod
def _resolve_asr_model(cls, tenant_id: int | None) -> tuple[LLMModel, LLMServer]:
workbench_llm = LLMService.get_workbench_llm_sync(tenant_id=tenant_id)
if not workbench_llm.asr_model or not workbench_llm.asr_model.id:
knowledge_llm = LLMService.get_knowledge_llm(tenant_id=tenant_id)
if not knowledge_llm.asr_model_id:
raise NoAsrModelConfigError()
model_info = LLMDao.get_model_by_id(int(workbench_llm.asr_model.id))
model_info = LLMDao.get_model_by_id(int(knowledge_llm.asr_model_id))
if not model_info:
raise AsrModelConfigDeletedError()
if model_info.model_type != LLMModelType.ASR.value:
@@ -131,6 +131,7 @@ class KnowledgeLLMConfig(BaseModel):
None, description="Documentation Knowledge Base Extraction Header Model'sID"
)
qa_similar_model_id: int | None = Field(None, description="QAThe Knowledge Base Similarity Question Model'sID")
asr_model_id: int | None = Field(None, description="Knowledge base media transcription ASR model ID")
abstract_enabled: bool = Field(default=True, description="Whether to generate file summaries after parsing")
auto_tag_enabled: bool = Field(default=True, description="Whether to generate file tags after upload parsing")
abstract_prompt: str | None = Field(None, description="Summary Prompt")
@@ -910,7 +910,13 @@ class LLMService:
"""Update default model configuration for knowledge base"""
target = _resolve_tenant_id(tenant_id)
await avalidate_system_model_refs(
[data.embedding_model_id, data.source_model_id, data.extract_title_model_id, data.qa_similar_model_id],
[
data.embedding_model_id,
data.source_model_id,
data.extract_title_model_id,
data.qa_similar_model_id,
data.asr_model_id,
],
target,
)
await cls._base_update_llm_config(
@@ -1,12 +1,17 @@
import subprocess
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from bisheng.common.errcode.knowledge import KnowledgeMediaNoRecognizableAudioError
from bisheng.common.errcode.server import NoAsrModelConfigError
from bisheng.knowledge.domain.services.media_transcription_service import (
KnowledgeMediaTranscriptionService,
TranscriptSegment,
)
from bisheng.llm.domain.const import LLMModelType, LLMServerType
from bisheng.llm.domain.schemas import KnowledgeLLMConfig
def test_normalize_segments_keeps_aliyun_millisecond_timestamps_consistent() -> None:
@@ -113,6 +118,45 @@ def test_convert_to_wav_reports_missing_audio_stream(monkeypatch, tmp_path) -> N
KnowledgeMediaTranscriptionService._convert_to_wav(str(media_path))
def test_resolve_asr_model_reads_knowledge_config(monkeypatch) -> None:
model_info = SimpleNamespace(
id=42,
model_name="paraformer-realtime-v2",
model_type=LLMModelType.ASR.value,
server_id=7,
online=True,
config={},
)
server_info = SimpleNamespace(name="Aliyun", type=LLMServerType.QWEN.value, config={"api_key": "sk-test"})
monkeypatch.setattr(
"bisheng.knowledge.domain.services.media_transcription_service.LLMService.get_knowledge_llm",
lambda tenant_id=None: KnowledgeLLMConfig(asr_model_id=42),
)
monkeypatch.setattr(
"bisheng.knowledge.domain.services.media_transcription_service.LLMDao.get_model_by_id",
lambda model_id: model_info if model_id == 42 else None,
)
monkeypatch.setattr(
"bisheng.knowledge.domain.services.media_transcription_service.LLMDao.get_server_by_id",
lambda server_id: server_info if server_id == 7 else None,
)
resolved_model, resolved_server = KnowledgeMediaTranscriptionService._resolve_asr_model(tenant_id=1)
assert resolved_model is model_info
assert resolved_server is server_info
def test_resolve_asr_model_requires_knowledge_config() -> None:
with patch(
"bisheng.knowledge.domain.services.media_transcription_service.LLMService.get_knowledge_llm",
return_value=KnowledgeLLMConfig(asr_model_id=None),
):
with pytest.raises(NoAsrModelConfigError):
KnowledgeMediaTranscriptionService._resolve_asr_model(tenant_id=1)
def test_empty_asr_text_reports_missing_recognizable_audio(monkeypatch, tmp_path) -> None:
media_path = tmp_path / "silent.mp4"
wav_path = tmp_path / "silent.wav"
@@ -367,7 +367,7 @@ async def test_update_knowledge_llm_calls_validator_with_all_model_ids():
payload = KnowledgeLLMConfig(
embedding_model_id=4, source_model_id=7,
extract_title_model_id=7, qa_similar_model_id=7,
extract_title_model_id=7, qa_similar_model_id=7, asr_model_id=9,
)
with patch(
'bisheng.llm.domain.services.llm.avalidate_system_model_refs',
@@ -380,7 +380,7 @@ async def test_update_knowledge_llm_calls_validator_with_all_model_ids():
args, kwargs = mock_validate.call_args
passed_ids = list(args[0])
assert set(passed_ids) == {4, 7}
assert set(passed_ids) == {4, 7, 9}
assert (kwargs.get('target_tenant_id') if 'target_tenant_id' in kwargs else args[1]) == 1
@@ -17,6 +17,7 @@
"sessionTitleGenerationModel": "Session Title Generation Model",
"asrModel": "Speech-to-Text (ASR) Model",
"asrModelTooltip": "Used for workbench speech-to-text scenarios",
"knowledgeAsrModelTooltip": "Used for speech-to-text in audio/video parsing",
"ttsModel": "Text-to-Speech (TTS) Model",
"ttsModelTooltip": "Used for workbench text-to-speech scenarios",
"saveFailed": "Save failed, please try again!",
@@ -15,6 +15,7 @@
"sessionTitleGenerationModel": "セッションタイトル生成モデル",
"asrModel": "音声認識(ASR)モデル",
"asrModelTooltip": "ワークベンチ音声認識シナリオに使用",
"knowledgeAsrModelTooltip": "音声・動画解析の音声認識シナリオに使用",
"ttsModel": "テキスト読み上げ(TTS)モデル",
"ttsModelTooltip": "ワークベンチテキスト読み上げシナリオに使用",
"saveFailed": "保存に失敗しました。再試行してください!",
@@ -15,6 +15,7 @@
"sessionTitleGenerationModel": "应用会话标题生成模型",
"asrModel": "语音转文字(ASR)模型",
"asrModelTooltip": "用于工作台语音转文字场景",
"knowledgeAsrModelTooltip": "用于音视频解析中语音转文字场景",
"ttsModel": "文字转语音(TTS)模型",
"ttsModelTooltip": "用于工作台文字转语音场景",
"saveFailed": "保存失败,请重试!",
@@ -147,7 +147,7 @@ export default function SystemModelConfig({
<WorkbenchModel llmOptions={llmOptions} embeddings={embeddings} asrModel={asrModel} ttsModel={ttsModel} onBack={onBack}></WorkbenchModel>
</TabsContent>
<TabsContent value="knowledge">
<KnowledgeModle llmOptions={llmOptions} embeddings={embeddings} onBack={onBack}></KnowledgeModle>
<KnowledgeModle llmOptions={llmOptions} embeddings={embeddings} asrModel={asrModel} onBack={onBack}></KnowledgeModle>
</TabsContent>
<TabsContent value="assis">
<AssisModel llmOptions={llmOptions} onBack={onBack}></AssisModel>
@@ -119,7 +119,7 @@ const PromptDialog = ({ value, onChange, onRestore, onSave, label, children }) =
</Dialog>
}
export default function KnowledgeModel({ llmOptions, embeddings, onBack }) {
export default function KnowledgeModel({ llmOptions, embeddings, asrModel = [], onBack }) {
const { t } = useTranslation('model')
const [form, setForm] = useState({
@@ -127,6 +127,7 @@ export default function KnowledgeModel({ llmOptions, embeddings, onBack }) {
sourceModelId: null,
extractModelId: null,
qaSimilarModelId: null,
asrModelId: null,
abstractEnabled: true,
autoTagEnabled: true,
abstractPrompt: '',
@@ -144,6 +145,7 @@ export default function KnowledgeModel({ llmOptions, embeddings, onBack }) {
embedding_model_id,
extract_title_model_id,
qa_similar_model_id,
asr_model_id,
source_model_id,
abstract_enabled,
auto_tag_enabled,
@@ -155,6 +157,7 @@ export default function KnowledgeModel({ llmOptions, embeddings, onBack }) {
sourceModelId: source_model_id,
extractModelId: extract_title_model_id,
qaSimilarModelId: qa_similar_model_id,
asrModelId: asr_model_id ?? null,
abstractEnabled: abstract_enabled ?? true,
autoTagEnabled: auto_tag_enabled ?? true,
abstractPrompt: abstract_prompt ?? defalutPrompt,
@@ -182,6 +185,7 @@ export default function KnowledgeModel({ llmOptions, embeddings, onBack }) {
embeddingModelId,
extractModelId,
qaSimilarModelId,
asrModelId,
sourceModelId,
abstractEnabled,
autoTagEnabled,
@@ -204,6 +208,7 @@ export default function KnowledgeModel({ llmOptions, embeddings, onBack }) {
embedding_model_id: embeddingModelId,
extract_title_model_id: extractModelId,
qa_similar_model_id: qaSimilarModelId,
asr_model_id: asrModelId || null,
source_model_id: sourceModelId,
abstract_enabled: abstractEnabled,
auto_tag_enabled: autoTagEnabled,
@@ -322,6 +327,14 @@ export default function KnowledgeModel({ llmOptions, embeddings, onBack }) {
options={llmOptions}
onChange={(val) => setFormAndClearInherited({ ...form, qaSimilarModelId: val })}
/>
<ModelSelect
close
label={t('model.asrModel')}
tooltipText={t('model.knowledgeAsrModelTooltip')}
value={form.asrModelId}
options={asrModel}
onChange={(val) => setFormAndClearInherited({ ...form, asrModelId: val })}
/>
<div className="mt-10 text-center space-x-6">
<Button className="px-6" variant="outline" onClick={onBack}>{t('model.cancel')}</Button>
<Button