Files

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)