Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class Config(QConfig):
)
deeplx_endpoint = ConfigItem("Translate", "DeeplxEndpoint", "")
batch_size = RangeConfigItem("Translate", "BatchSize", 10, RangeValidator(5, 50))
thread_num = RangeConfigItem("Translate", "ThreadNum", 10, RangeValidator(1, 100))
thread_num = RangeConfigItem("Translate", "ThreadNum", 10, RangeValidator(1, 50))

# ------------------- 转录配置 -------------------
transcribe_model = OptionsConfigItem(
Expand Down Expand Up @@ -273,7 +273,7 @@ class Config(QConfig):
)

# 圆角背景模式配置
rounded_bg_font_name = ConfigItem("RoundedBgStyle", "FontName", "Noto Sans SC")
rounded_bg_font_name = ConfigItem("RoundedBgStyle", "FontName", "LXGW WenKai")
rounded_bg_font_size = RangeConfigItem(
"RoundedBgStyle", "FontSize", 52, RangeValidator(16, 120)
)
Expand Down
21 changes: 21 additions & 0 deletions app/core/entities.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Literal, Optional
Expand All @@ -7,6 +8,11 @@
from app.core.translate.types import TargetLanguage


def _generate_task_id() -> str:
"""生成 8 位任务 ID"""
return uuid.uuid4().hex[:8]


@dataclass
class SubtitleProcessData:
"""字幕处理数据(翻译/优化通用)"""
Expand Down Expand Up @@ -660,6 +666,9 @@ def print_config(self) -> str:
class TranscribeTask:
"""转录任务类"""

# 任务标识
task_id: str = field(default_factory=_generate_task_id)

queued_at: Optional[datetime.datetime] = None
started_at: Optional[datetime.datetime] = None
completed_at: Optional[datetime.datetime] = None
Expand All @@ -683,6 +692,9 @@ class TranscribeTask:
class SubtitleTask:
"""字幕任务类"""

# 任务标识
task_id: str = field(default_factory=_generate_task_id)

queued_at: Optional[datetime.datetime] = None
started_at: Optional[datetime.datetime] = None
completed_at: Optional[datetime.datetime] = None
Expand All @@ -705,6 +717,9 @@ class SubtitleTask:
class SynthesisTask:
"""视频合成任务类"""

# 任务标识
task_id: str = field(default_factory=_generate_task_id)

queued_at: Optional[datetime.datetime] = None
started_at: Optional[datetime.datetime] = None
completed_at: Optional[datetime.datetime] = None
Expand All @@ -726,6 +741,9 @@ class SynthesisTask:
class TranscriptAndSubtitleTask:
"""转录和字幕任务类"""

# 任务标识
task_id: str = field(default_factory=_generate_task_id)

queued_at: Optional[datetime.datetime] = None
started_at: Optional[datetime.datetime] = None
completed_at: Optional[datetime.datetime] = None
Expand All @@ -744,6 +762,9 @@ class TranscriptAndSubtitleTask:
class FullProcessTask:
"""完整处理任务类(转录+字幕+合成)"""

# 任务标识
task_id: str = field(default_factory=_generate_task_id)

queued_at: Optional[datetime.datetime] = None
started_at: Optional[datetime.datetime] = None
completed_at: Optional[datetime.datetime] = None
Expand Down
2 changes: 1 addition & 1 deletion app/core/llm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""LLM unified client module."""

from .client import call_llm, get_llm_client
from .check_llm import check_llm_connection, get_available_models
from .check_whisper import check_whisper_connection
from .client import call_llm, get_llm_client

__all__ = [
"call_llm",
Expand Down
80 changes: 27 additions & 53 deletions app/core/llm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,16 @@
from app.core.utils.cache import get_llm_cache, memoize
from app.core.utils.logger import setup_logger

from .request_logger import create_logging_http_client, log_llm_response

_global_client: Optional[OpenAI] = None
_client_lock = threading.Lock()

logger = setup_logger("llm_client")


def normalize_base_url(base_url: str) -> str:
"""Normalize API base URL by ensuring /v1 suffix when needed.

Handles various edge cases:
- Removes leading/trailing whitespace
- Only adds /v1 if domain has no path, or path is empty/root
- Removes trailing slashes from /v1 (e.g., /v1/ -> /v1)
- Preserves custom paths (e.g., /custom stays as /custom)

Args:
base_url: Raw base URL string

Returns:
Normalized base URL

Examples:
>>> normalize_base_url("https://api.openai.com")
'https://api.openai.com/v1'
>>> normalize_base_url("https://api.openai.com/v1/")
'https://api.openai.com/v1'
>>> normalize_base_url("https://api.openai.com/custom")
'https://api.openai.com/custom'
>>> normalize_base_url(" https://api.openai.com ")
'https://api.openai.com/v1'
"""
"""Normalize API base URL by ensuring /v1 suffix when needed."""
url = base_url.strip()
parsed = urlparse(url)
path = parsed.path.rstrip("/")
Expand All @@ -71,19 +50,11 @@ def normalize_base_url(base_url: str) -> str:


def get_llm_client() -> OpenAI:
"""Get global LLM client instance (thread-safe singleton).

Returns:
Global OpenAI client instance

Raises:
ValueError: If OPENAI_BASE_URL or OPENAI_API_KEY env vars not set
"""
"""Get global LLM client instance (thread-safe singleton)."""
global _global_client

if _global_client is None:
with _client_lock:
# Double-check locking pattern
if _global_client is None:
base_url = os.getenv("OPENAI_BASE_URL", "").strip()
base_url = normalize_base_url(base_url)
Expand All @@ -94,7 +65,11 @@ def get_llm_client() -> OpenAI:
"OPENAI_BASE_URL and OPENAI_API_KEY environment variables must be set"
)

_global_client = OpenAI(base_url=base_url, api_key=api_key)
_global_client = OpenAI(
base_url=base_url,
api_key=api_key,
http_client=create_logging_http_client(),
)

return _global_client

Expand All @@ -105,35 +80,19 @@ def before_sleep_log(retry_state: RetryCallState) -> None:
)


@memoize(get_llm_cache(), expire=3600, typed=True)
@retry(
stop=stop_after_attempt(10),
wait=wait_random_exponential(multiplier=1, min=5, max=60),
retry=retry_if_exception_type(openai.RateLimitError),
before_sleep=before_sleep_log,
)
def call_llm(
def _call_llm_api(
messages: List[dict],
model: str,
temperature: float = 1,
**kwargs: Any,
) -> Any:
"""Call LLM API with automatic caching.

Uses global LLM client configured via environment variables.

Args:
messages: Chat messages list
model: Model name
temperature: Sampling temperature
**kwargs: Additional parameters for API call

Returns:
API response object

Raises:
ValueError: If response is invalid (empty choices or content)
"""
"""实际调用 LLM API(带重试)"""
client = get_llm_client()

response = client.chat.completions.create(
Expand All @@ -143,7 +102,22 @@ def call_llm(
**kwargs,
)

# Validate response (exceptions are not cached by diskcache)
# 记录响应内容
log_llm_response(response)

return response


@memoize(get_llm_cache(), expire=3600, typed=True)
def call_llm(
messages: List[dict],
model: str,
temperature: float = 1,
**kwargs: Any,
) -> Any:
"""Call LLM API with automatic caching."""
response = _call_llm_api(messages, model, temperature, **kwargs)

if not (
response
and hasattr(response, "choices")
Expand Down
59 changes: 59 additions & 0 deletions app/core/llm/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""任务上下文管理

使用模块级变量存储任务上下文,确保跨线程池传递(ThreadPoolExecutor 不会自动复制 contextvars)。
"""

import threading
import uuid
from dataclasses import dataclass
from typing import Optional


@dataclass
class TaskContext:
"""任务上下文"""

task_id: str # 任务唯一标识,如 "a1b2c3d4"
file_name: str # 处理的文件名,如 "video.mp4"
stage: str # 当前阶段: transcribe / split / optimize / translate / synthesis


_lock = threading.Lock()
_current_context: Optional[TaskContext] = None


def generate_task_id() -> str:
"""生成 8 位任务 ID"""
return uuid.uuid4().hex[:8]


def set_task_context(task_id: str, file_name: str, stage: str) -> None:
"""设置当前任务上下文"""
global _current_context
with _lock:
_current_context = TaskContext(task_id=task_id, file_name=file_name, stage=stage)


def get_task_context() -> Optional[TaskContext]:
"""获取当前任务上下文"""
with _lock:
return _current_context


def update_stage(stage: str) -> None:
"""更新当前阶段"""
global _current_context
with _lock:
if _current_context:
_current_context = TaskContext(
task_id=_current_context.task_id,
file_name=_current_context.file_name,
stage=stage,
)


def clear_task_context() -> None:
"""清除任务上下文"""
global _current_context
with _lock:
_current_context = None
Loading
Loading