aad1548348
- Extract all path/API config to config.py (single source of truth) - Add run.py / burn_only.py / run.bat / burn.bat entry points - burn_only: skip transcription/subtitle gen, fast reburn existing SRTs - Fix title_segments: use transcript keyword time for split point - Fix subtitle: each overlapping title shows max title_duration (not full clip) - Fix burn_only font size: default from 90 to 60 - Delete old run_lesson1.bat/py, temp debug scripts - Update README, ARCHITECTURE, CHANGELOG, add USAGE.md
103 lines
3.1 KiB
Python
103 lines
3.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
LLM调用封装
|
|
|
|
统一管理火山方舟API调用,包含重试和错误处理
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import logging
|
|
from .constants import (
|
|
DEFAULT_API_HOST, LLM_MODEL, LLM_TIMEOUT,
|
|
LLM_MAX_RETRIES, LLM_TITLE_TIMEOUT, LLM_VALIDATE_TIMEOUT,
|
|
get_api_key
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
import requests
|
|
|
|
|
|
class LLMClient:
|
|
"""LLM客户端封装"""
|
|
|
|
def __init__(self, api_key=None, api_host=None):
|
|
# 优先使用传入的参数,其次使用环境变量
|
|
self.api_key = api_key or get_api_key()
|
|
self.api_host = api_host or DEFAULT_API_HOST
|
|
if not self.api_key:
|
|
logger.warning("No API key configured - LLM calls will be skipped")
|
|
|
|
def chat(self, prompt, max_tokens=500, timeout=LLM_TIMEOUT):
|
|
"""
|
|
发送聊天请求到LLM
|
|
|
|
Args:
|
|
prompt: 提示词
|
|
max_tokens: 最大token数
|
|
timeout: 超时时间
|
|
|
|
Returns:
|
|
LLM回复文本,失败返回None
|
|
"""
|
|
if not self.api_key:
|
|
logger.info("LLM: No API key, skipping")
|
|
return None
|
|
|
|
url = f"{self.api_host}/chat/completions"
|
|
headers = {
|
|
"Authorization": f"Bearer {self.api_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
payload = {
|
|
"model": LLM_MODEL,
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"max_tokens": max_tokens
|
|
}
|
|
|
|
logger.info(f"[LLM] request chars={len(prompt)}, max_tokens={max_tokens}")
|
|
|
|
for attempt in range(LLM_MAX_RETRIES):
|
|
try:
|
|
response = requests.post(url, headers=headers, json=payload, timeout=timeout)
|
|
# 401错误立即停止,不重试
|
|
if response.status_code == 401:
|
|
logger.error(f"LLM: 401 Unauthorized - API key invalid, stopping immediately")
|
|
return None
|
|
response.raise_for_status()
|
|
result = response.json()
|
|
|
|
choices = result.get("choices", [])
|
|
if not choices:
|
|
logger.warning(f"LLM: No choices in response (attempt {attempt+1})")
|
|
continue
|
|
|
|
content = choices[0].get("message", {}).get("content", "").strip()
|
|
if content:
|
|
logger.info(f"[LLM] response chars={len(content)}")
|
|
return content
|
|
|
|
logger.warning(f"LLM: Empty content (attempt {attempt+1})")
|
|
|
|
except requests.exceptions.Timeout:
|
|
logger.warning(f"LLM: Timeout (attempt {attempt+1}/{LLM_MAX_RETRIES})")
|
|
if attempt < LLM_MAX_RETRIES - 1:
|
|
time.sleep(1)
|
|
except Exception as e:
|
|
logger.error(f"LLM: Error - {e}")
|
|
if attempt < LLM_MAX_RETRIES - 1:
|
|
time.sleep(1)
|
|
|
|
return None
|
|
|
|
# 全局LLM客户端实例
|
|
_llm_client = None
|
|
|
|
|
|
def get_llm_client():
|
|
"""获取LLM客户端单例"""
|
|
global _llm_client
|
|
if _llm_client is None:
|
|
_llm_client = LLMClient()
|
|
return _llm_client |