mirror of
https://github.com/dataelement/bisheng.git
synced 2026-08-30 17:58:00 +08:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user