接入香港 Vertex SMART 个人语音输入与热词
This commit is contained in:
@@ -9,10 +9,22 @@
|
||||
`google/gemini-embedding-2` 和 `google/gemini-3.8-flash`;后两者仍通过
|
||||
Vertex AI ADC 路由。
|
||||
- 2026-10-25 同步 tools-pve 已验证的 LiteLLM 1.104.2 固定镜像,以及 `models.yaml` 中的提供商映射。`latest` 是维护的别名,不是自动发现所有厂商的新模型。
|
||||
- `provider_routes.py` 是同日 tools-pve 的路由生成器快照;更新提供商逻辑时应同步比对两端。HK 的 `render_config.py` 删除数据库管理、OAuth 和个人词库路线,保留现有主密钥认证。
|
||||
- `provider_routes.py` 是同日 tools-pve 的路由生成器快照;更新提供商逻辑时应同步比对两端。HK 的 `render_config.py` 不接入数据库管理或 OAuth,保留现有主密钥认证,个人语音输入走独立的 Vertex 路线。
|
||||
- Arcane Project 本地变量增加 `ZENMUX_SUBSCRIPTION_API_KEY`、`ZENMUX_PAYG_API_KEY`、`OPENROUTER_API_KEY`。只有相应凭据存在时才生成该组路由;真实值不进入 Git。
|
||||
- Claude 使用 ZenMux 原生 Anthropic Messages 路径,订阅与 `-payg` 显式分开;开源模型使用 ZenMux 订阅优先、同模型 OpenRouter 后备。不自动回退至 ZenMux PAYG。
|
||||
- HK 合成请求实测 AI Studio 返回 `User location is not supported for the API use`,因此 Compose 不传入 AI Studio 凭据,也不发布其对话、embedding 或转写路线。tools-pve 上原有 AI Studio 功能不受影响。个人词库、OAuth 会话和会议处理程序没有复制至 HK。
|
||||
- HK 合成请求实测 AI Studio 返回 `User location is not supported for the API use`,因此 Compose 不传入 AI Studio 凭据,也不发布其路线。tools-pve 上原有 AI Studio 功能不受影响。OAuth 会话和会议处理程序没有复制至 HK。
|
||||
- Vertex 继续使用 HK 原有 ADC。旧 `gemini-3.6-flash` 别名保留,但指向当前映射的 `gemini-3.8-flash`;其余旧别名保留。
|
||||
- 当前实际配置由启动脚本生成至 `/tmp/litellm.yaml`;旧 `config.yaml` 仅作为历史参考,不再挂载。
|
||||
- 升级只重建 LiteLLM,不变更 Caddy、HAProxy、其他项目及 ADC 卷。失败时回退本次 Git 提交并同步,再按需要 Redeploy,禁止删除卷。
|
||||
|
||||
## Spokenly 个人语音输入
|
||||
|
||||
- 基础地址:`https://api.opfo.xyz/v1`。
|
||||
- 转写完整地址:`https://api.opfo.xyz/v1/audio/transcriptions`。
|
||||
- 模型:`gemini-transcribe-personal`,真实上游为 `vertex_ai/gemini-3.5-transcribe`,区域 `global`,复用 HK ADC。
|
||||
- API Key:HK 现有 LiteLLM 主密钥,不是 tools-pve 客户端 key,也不是 ZenMux key。
|
||||
- 响应选 `json` 或 `text`;不请求时间戳、说话人分离或 `verbose_json`。额外 prompt 留空;去口语化由 SMART 负责,不靠提示词。
|
||||
- 仅显式带 `personal_dictation: true` 的这条路由经 `vertex_dictation.py` 添加 `generationConfig.audioTranscriptionConfig.mode=SMART` 和 `customVocabulary`。其他转写路线、会议录音以及 tools-pve 配置不改。
|
||||
- 个人词库从既有 ASR SQLite 中导出 100 个已确认 canonical 目标词;不把误识别别名或整个公共术语库发给 Google。用户已批准该个人词库用于 HK Vertex 语音输入。
|
||||
- `PERSONAL_VOCABULARY_B64` 只存 Arcane Project 环境(Base64 不是加密);init 将其解码至现有私有凭据卷 `/credentials/personal-vocabulary.json`,umask 077,LiteLLM 只读挂载,不进入 Git。
|
||||
- 更新词库时使用项目 `scripts/export-personal-asr-vocabulary.py`,替换 Arcane 中上述变量并重建 init;单改环境变量不会更新已生成文件。升级 LiteLLM 前重跑 `scripts/test-vertex-dictation-wire.py`,确认原生 SMART/热词和普通路线隔离。
|
||||
|
||||
@@ -4,14 +4,17 @@ services:
|
||||
restart: "no"
|
||||
environment:
|
||||
GOOGLE_ADC_B64: ${GOOGLE_ADC_B64}
|
||||
PERSONAL_VOCABULARY_B64: ${PERSONAL_VOCABULARY_B64:?Personal vocabulary is required}
|
||||
volumes:
|
||||
- google-adc:/credentials
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
umask 077
|
||||
printf '%s' "$${GOOGLE_ADC_B64}" | base64 -d > /credentials/application_default_credentials.json
|
||||
printf '%s' "$${PERSONAL_VOCABULARY_B64}" | base64 -d > /credentials/personal-vocabulary.json
|
||||
|
||||
litellm:
|
||||
image: ghcr.io/berriai/litellm:main-stable@sha256:3d88bd757134a00e8c00237b85a021ab1f6d54abb9c1df750f411dbe7256a500
|
||||
@@ -36,10 +39,11 @@ services:
|
||||
- ./models.yaml:/etc/litellm/models.yaml:ro
|
||||
- ./provider_routes.py:/etc/litellm/provider_routes.py:ro
|
||||
- ./render_config.py:/etc/litellm/render_config.py:ro
|
||||
- ./vertex_dictation.py:/etc/litellm/vertex_dictation.py:ro
|
||||
- google-adc:/credentials:ro
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
- python /etc/litellm/render_config.py && exec litellm --config /tmp/litellm.yaml --host 0.0.0.0 --port ${LITELLM_PORT:-1049} --num_workers 1
|
||||
- python /etc/litellm/render_config.py && exec python /etc/litellm/vertex_dictation.py --config /tmp/litellm.yaml --host 0.0.0.0 --port ${LITELLM_PORT:-1049} --num_workers 1
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""HK reuses tools-pve provider routing without OAuth, database, or private vocabulary."""
|
||||
"""HK provider routes and separately opted-in personal Vertex dictation."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -17,6 +17,11 @@ def build_hk_config(catalog, env):
|
||||
'model': 'gemini/gemini-3.5-transcribe',
|
||||
'api_key': 'os.environ/GOOGLE_AI_STUDIO_API_KEY'}})
|
||||
if env.get('VERTEXAI_ENABLED', '').lower() == 'true' and env.get('VERTEXAI_PROJECT'):
|
||||
routes.append({'model_name': 'gemini-transcribe-personal', 'litellm_params': {
|
||||
'model': 'vertex_ai/gemini-3.5-transcribe',
|
||||
'vertex_project': 'os.environ/VERTEXAI_PROJECT',
|
||||
'vertex_location': 'global',
|
||||
'personal_dictation': True}})
|
||||
# Keep old client aliases; the old 3.6 alias now follows the verified Flash map.
|
||||
legacy = {
|
||||
'gemini-3.6-flash': catalog['google_vertex']['gemini-flash-vertex'],
|
||||
|
||||
@@ -16,7 +16,9 @@ class RoutingTests(unittest.TestCase):
|
||||
result = build_hk_config(catalog, env)
|
||||
routes = {r['model_name']: r['litellm_params'] for r in result['model_list']}
|
||||
self.assertEqual(len(routes), len(result['model_list']))
|
||||
self.assertNotIn('gemini-transcribe-personal', routes)
|
||||
self.assertEqual(routes['gemini-transcribe-personal']['model'], 'vertex_ai/gemini-3.5-transcribe')
|
||||
self.assertIs(routes['gemini-transcribe-personal']['personal_dictation'], True)
|
||||
self.assertEqual(routes['gemini-transcribe-personal']['vertex_location'], 'global')
|
||||
self.assertNotIn('grok-latest', routes)
|
||||
self.assertNotIn('gpt-sol-latest', routes)
|
||||
self.assertEqual(routes['gemini-transcribe']['model'], 'gemini/gemini-3.5-transcribe')
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Scoped Vertex dictation settings; ordinary transcription calls stay unchanged."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
VOCABULARY = Path('/credentials/personal-vocabulary.json')
|
||||
|
||||
|
||||
def vocabulary(path=VOCABULARY):
|
||||
words = json.loads(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(word, str) or not word.strip() or len(word) > 100 for word in words):
|
||||
raise ValueError('Invalid personal vocabulary term')
|
||||
return list(dict.fromkeys(word.strip() for word in words))
|
||||
|
||||
|
||||
def install():
|
||||
from litellm.llms.vertex_ai.audio_transcription.gemini_transcribe_transformation import (
|
||||
VertexGeminiAudioTranscriptionConfig,
|
||||
)
|
||||
original = VertexGeminiAudioTranscriptionConfig.transform_audio_transcription_request
|
||||
if getattr(original, '_personal_dictation_patch', False):
|
||||
return
|
||||
|
||||
def transform(self, model, audio_file, optional_params, litellm_params):
|
||||
if optional_params.get('personal_dictation') is not True:
|
||||
return original(self, model, audio_file, optional_params, litellm_params)
|
||||
if model.removeprefix('vertex_ai/') != 'gemini-3.5-transcribe':
|
||||
raise ValueError('Personal dictation is restricted to Vertex Gemini 3.5 Transcribe')
|
||||
if optional_params.get('timestamp_granularities') or optional_params.get('response_format') not in (None, 'json', 'text'):
|
||||
raise ValueError('Personal dictation supports json/text without timestamps')
|
||||
clean = {key: val for key, val in optional_params.items() if key != 'personal_dictation'}
|
||||
result = original(self, model, audio_file, clean, litellm_params)
|
||||
config = result.data.setdefault('generationConfig', {}).setdefault('audioTranscriptionConfig', {})
|
||||
if set(config) - {'mode', 'languageCodes'} or config.get('mode') not in (None, 'VERBATIM'):
|
||||
raise ValueError('Unexpected Vertex transcription defaults; review adapter compatibility')
|
||||
config['mode'] = 'SMART'
|
||||
config['customVocabulary'] = vocabulary()
|
||||
return result
|
||||
|
||||
transform._personal_dictation_patch = True
|
||||
VertexGeminiAudioTranscriptionConfig.transform_audio_transcription_request = transform
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
from litellm import run_server
|
||||
install()
|
||||
sys.argv[0] = 'litellm'
|
||||
sys.exit(run_server())
|
||||
Reference in New Issue
Block a user