120 lines
6.3 KiB
Python
120 lines
6.3 KiB
Python
"""Bridge OpenAI file uploads to Beijing Bailian's native ASR API."""
|
|
import base64
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
from urllib.parse import urlsplit
|
|
import wave
|
|
|
|
from litellm.llms.base_llm.audio_transcription.transformation import AudioTranscriptionRequestData
|
|
from litellm.llms.openai.transcriptions.whisper_transformation import OpenAIWhisperAudioTranscriptionConfig
|
|
from litellm.litellm_core_utils.audio_utils.utils import process_audio_file
|
|
from litellm.types.utils import TranscriptionResponse, TranscriptionUsageDurationObject
|
|
|
|
MODEL = 'qwen-audio-3.1-asr-flash'
|
|
FORMATS = {'audio/wav':'wav','audio/x-wav':'wav','audio/wave':'wav',
|
|
'audio/mpeg':'mp3','audio/mp3':'mp3','audio/flac':'flac','audio/x-flac':'flac',
|
|
'audio/mp4':'m4a','audio/m4a':'m4a','audio/x-m4a':'m4a',
|
|
'audio/ogg':'ogg','audio/webm':'webm','video/webm':'webm','audio/aac':'aac'}
|
|
VOCABULARY_PATHS = frozenset({'/etc/litellm/personal-vocabulary.json',
|
|
'/credentials/personal-vocabulary.json'})
|
|
|
|
|
|
def vocabulary(path):
|
|
if path not in VOCABULARY_PATHS:
|
|
raise ValueError('Unexpected personal vocabulary path')
|
|
words = json.loads(Path(path).read_text())
|
|
if not isinstance(words,list) or not 1 <= len(words) <= 100:
|
|
raise ValueError('Personal vocabulary must contain 1-100 terms')
|
|
if any(not isinstance(w,str) or not w.strip() or len(w)>100 for w in words):
|
|
raise ValueError('Invalid personal vocabulary term')
|
|
return {w.strip():3 for w in words}
|
|
|
|
|
|
class BailianTranscriptionConfig(OpenAIWhisperAudioTranscriptionConfig):
|
|
@property
|
|
def has_native_transcription_endpoint(self):
|
|
return True
|
|
|
|
def get_supported_openai_params(self,model):
|
|
return ['language','response_format','prompt']
|
|
|
|
def map_openai_params(self,non_default_params,optional_params,model,drop_params):
|
|
result=super().map_openai_params(non_default_params,optional_params,model,drop_params)
|
|
if result.get('response_format') not in (None,'json','text'):
|
|
raise ValueError('Bailian dictation supports json/text without timestamps')
|
|
return result
|
|
|
|
def validate_environment(self,headers,model,messages,optional_params,litellm_params,
|
|
api_key=None,api_base=None):
|
|
if not api_key:raise ValueError('Beijing Bailian API key required')
|
|
return {**headers,'Authorization':'Bearer '+api_key,'Content-Type':'application/json',
|
|
'X-DashScope-SSE':'disable'}
|
|
|
|
def get_complete_url(self,api_base,api_key,model,optional_params,litellm_params,stream=None):
|
|
parsed=urlsplit(api_base or '')
|
|
if (model != MODEL or parsed.scheme != 'https' or
|
|
not (parsed.hostname or '').endswith('.cn-beijing.maas.aliyuncs.com') or
|
|
parsed.path.rstrip('/') != '/api/v1' or parsed.port not in (None,443) or
|
|
parsed.username or parsed.password or parsed.query or parsed.fragment):
|
|
raise ValueError('Bailian ASR restricted to Beijing workspace native endpoints')
|
|
if stream:raise ValueError('Streaming not enabled on this dictation route')
|
|
return api_base.rstrip('/')+'/services/aigc/multimodal-generation/generation'
|
|
|
|
def transform_audio_transcription_request(self,model,audio_file,optional_params,litellm_params):
|
|
if model != MODEL or optional_params.get('timestamp_granularities'):
|
|
raise ValueError('Unsupported Bailian dictation model or timestamps')
|
|
audio=process_audio_file(audio_file)
|
|
mime=audio.content_type.split(';',1)[0].lower()
|
|
if mime not in FORMATS:raise ValueError('Unsupported Bailian audio MIME type')
|
|
if len(audio.file_content)>10_000_000:raise ValueError('Bailian inline audio exceeds 10 MB')
|
|
parameters={'format':FORMATS[mime]}
|
|
if parameters['format']=='wav':
|
|
with wave.open(io.BytesIO(audio.file_content),'rb') as wav:
|
|
parameters['sample_rate']=str(wav.getframerate())
|
|
language=optional_params.get('language')
|
|
if language:parameters['language_hints']=[language]
|
|
# LiteLLM classifies DashScope as OpenAI-compatible and nests custom kwargs.
|
|
extra=optional_params.get('extra_body') or {}
|
|
path=optional_params.get('bailian_vocabulary_path') or extra.get('bailian_vocabulary_path')
|
|
if path:parameters['vocabulary']=vocabulary(path)
|
|
prompt=optional_params.get('prompt')
|
|
messages=[]
|
|
if prompt:
|
|
if not isinstance(prompt,str) or len(prompt)>400:
|
|
raise ValueError('Bailian context prompt must be at most 400 characters')
|
|
messages.append({'role':'user','content':[{'type':'input_text','text':prompt}]})
|
|
# Always preserve the original bytes; only WAV metadata supplies sample rate.
|
|
uri_mime='audio/mp3' if parameters['format']=='mp3' else mime
|
|
data_uri='data:'+uri_mime+';base64,'+base64.b64encode(audio.file_content).decode()
|
|
if len(data_uri)>10_000_000:
|
|
raise ValueError('Bailian inline audio exceeds 10 MB after Base64 encoding')
|
|
messages.append({'role':'user','content':[{'type':'input_audio','input_audio':{
|
|
'data':data_uri}}]})
|
|
return AudioTranscriptionRequestData(data={'model':MODEL,'input':{'messages':messages},
|
|
'parameters':parameters})
|
|
|
|
def transform_audio_transcription_response(self,raw_response):
|
|
body=raw_response.json()
|
|
text=body.get('output',{}).get('text',body.get('text'))
|
|
if not isinstance(text,str):raise ValueError('Bailian returned no transcript')
|
|
result=TranscriptionResponse(text=text)
|
|
duration=(body.get('usage') or {}).get('duration')
|
|
if duration is not None:
|
|
result.usage=TranscriptionUsageDurationObject(type='duration',seconds=duration)
|
|
result['duration']=duration
|
|
return result
|
|
|
|
|
|
def install():
|
|
import litellm
|
|
from litellm.utils import ProviderConfigManager
|
|
original=ProviderConfigManager.get_provider_audio_transcription_config
|
|
if getattr(original,'_bailian_asr_adapter',False):return
|
|
def select(model,provider):
|
|
if provider==litellm.LlmProviders.DASHSCOPE and model==MODEL:
|
|
return BailianTranscriptionConfig()
|
|
return original(model,provider)
|
|
select._bailian_asr_adapter=True
|
|
ProviderConfigManager.get_provider_audio_transcription_config=staticmethod(select)
|