fix(#1834): 批量修复 UP 系列静态分析警告(UP007/UP006/UP017/UP035) #1928

Merged
auto-approve-bot merged 2 commits from fix/1834-ruff-static-fixes into develop 2026-09-15 12:59:19 +08:00
224 changed files with 1098 additions and 1088 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
"""路由层共享辅助函数 — 消除跨文件重复定义。"""
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from fastapi import HTTPException, status
@@ -138,4 +138,4 @@ def format_utc_datetime(dt: datetime | None) -> str:
return dt
if dt.tzinfo is None:
return dt.isoformat() + "Z"
return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
return dt.astimezone(UTC).isoformat().replace("+00:00", "Z")
+6 -6
View File
@@ -5,7 +5,7 @@
from __future__ import annotations
from typing import List, Literal
from typing import Literal
from app.services.ai_service import TITLE_STYLES, generate_smart_titles, semantic_match_assets
from fastapi import APIRouter
@@ -31,7 +31,7 @@ class GenerateTitlesRequest(BaseModel):
class GenerateTitlesResponse(BaseModel):
"""智能标题生成响应."""
titles: List[str] = Field(..., description="生成的标题列表")
titles: list[str] = Field(..., description="生成的标题列表")
style: str = Field(..., description="实际使用的风格")
source: str = Field(..., description="来源:doubao 或 fallback")
description: str = Field(..., description="原始描述")
@@ -53,7 +53,7 @@ class AssetMatchItem(BaseModel):
id: str = Field(..., description="素材ID")
name: str = Field(default="", description="素材名称")
tags: List[str] = Field(default_factory=list, description="标签列表")
tags: list[str] = Field(default_factory=list, description="标签列表")
description: str = Field(default="", description="素材描述")
@@ -61,7 +61,7 @@ class SemanticMatchRequest(BaseModel):
"""语义匹配请求."""
description: str = Field(..., min_length=1, max_length=500, description="目标视频内容描述")
assets: List[AssetMatchItem] = Field(..., min_length=1, max_length=100, description="待匹配素材列表")
assets: list[AssetMatchItem] = Field(..., min_length=1, max_length=100, description="待匹配素材列表")
top_k: int = Field(default=0, ge=0, le=100, description="返回前K个,0返回全部")
@@ -75,7 +75,7 @@ class SemanticMatchResultItem(AssetMatchItem):
class SemanticMatchResponse(BaseModel):
"""语义匹配响应."""
matches: List[SemanticMatchResultItem] = Field(..., description="按匹配度降序排列的素材列表")
matches: list[SemanticMatchResultItem] = Field(..., description="按匹配度降序排列的素材列表")
source: str = Field(..., description="来源:doubao / fallback")
description: str = Field(..., description="原始描述")
total: int = Field(..., description="输入素材总数")
@@ -99,7 +99,7 @@ def generate_titles(request: GenerateTitlesRequest):
return GenerateTitlesResponse(**result)
@router.get("/titles/styles", response_model=List[TitleStyleInfo])
@router.get("/titles/styles", response_model=list[TitleStyleInfo])
def list_title_styles():
"""获取支持的标题风格列表."""
return [
+4 -4
View File
@@ -11,7 +11,7 @@
from __future__ import annotations
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_db_session
@@ -82,7 +82,7 @@ def create_render_job(
logger.exception("Celery 任务投递失败(创建): job_id=%s err=%s", job.id, exc)
job.status = "failed"
job.error_message = f"任务提交失败:{exc}"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
svc.db.commit()
svc.db.refresh(job)
return AiAvatarRenderJobResponse.model_validate(job)
@@ -183,7 +183,7 @@ def retry_render_job(
logger.exception("Celery 任务投递失败(重试): job_id=%s err=%s", job.id, exc)
job.status = "failed"
job.error_message = f"任务提交失败:{exc}"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
svc.db.commit()
svc.db.refresh(job)
return AiAvatarRenderJobResponse.model_validate(job)
@@ -246,7 +246,7 @@ def generate_render_smart_cover(
"url": cover_url,
}
job.output_cover_url = cover_url
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
db.commit()
except Exception as exc:
logger.warning("更新 render_job 封面字段失败(不影响返回): job_id=%s err=%s", job_id, exc)
+2 -2
View File
@@ -1,5 +1,5 @@
import logging
from typing import Any, List, Optional
from typing import Any, Optional
from app.api.routes._helpers import check_project_access, format_utc_datetime
from app.auth import AuthenticatedUser, get_current_user
@@ -390,7 +390,7 @@ def update_asset_review_status(
return _to_asset_response(updated)
@router.post("/batch", response_model=List[AssetResponse])
@router.post("/batch", response_model=list[AssetResponse])
def batch_get_assets(
request: BatchGetRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
+6 -6
View File
@@ -8,7 +8,7 @@ import json
import logging
import shutil
import tempfile
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any
from uuid import uuid4
@@ -156,7 +156,7 @@ def _cleanup_expired_uploads() -> int:
if not CHUNK_STORAGE_ROOT.exists():
return 0
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
cleaned = 0
for meta_file in CHUNK_STORAGE_ROOT.glob("*.meta.json"):
@@ -166,7 +166,7 @@ def _cleanup_expired_uploads() -> int:
expires_at = datetime.fromisoformat(meta["expires_at"])
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
expires_at = expires_at.replace(tzinfo=UTC)
# Only cleanup uploads that are not actively being uploaded
if expires_at < now and meta.get("status") != "uploading":
@@ -226,7 +226,7 @@ async def init_chunked_upload(
# Generate upload ID
upload_id = uuid4().hex
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
expires_at = now + timedelta(hours=CHUNK_EXPIRY_HOURS)
# Create chunk directory
@@ -421,9 +421,9 @@ async def upload_chunk(
# Check expiry
expires_at = datetime.fromisoformat(meta["expires_at"])
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
expires_at = expires_at.replace(tzinfo=UTC)
if expires_at < datetime.now(timezone.utc):
if expires_at < datetime.now(UTC):
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Upload has expired")
# Validate chunk index
+2 -2
View File
@@ -11,7 +11,7 @@ from __future__ import annotations
import ipaddress
import logging
import re
from typing import Any, List, Optional
from typing import Any, Optional
from urllib.parse import urlparse
from app.auth import AuthenticatedUser, get_current_user
@@ -41,7 +41,7 @@ router = APIRouter(tags=["Generation"])
class GenerateCoverRequest(BaseModel):
"""AI 封面生成请求体"""
asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表(确定视频来源)")
asset_ids: list[str] = Field(default_factory=list, description="素材 ID 列表(确定视频来源)")
cover_type: str = Field(
default="ai_frame",
description="封面类型: ai_frame / manual / upload / ai_regenerate",
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
import psycopg
import redis
@@ -13,7 +13,7 @@ router = APIRouter(tags=["Health"])
async def health_check():
return {
"status": "healthy",
"timestamp": datetime.now(timezone.utc).isoformat(),
"timestamp": datetime.now(UTC).isoformat(),
"version": settings.APP_VERSION,
}
@@ -33,7 +33,7 @@ async def startup_check():
all_ready = all(check["status"] == "healthy" for check in checks.values())
response = {
"status": "started" if all_ready else "starting",
"timestamp": datetime.now(timezone.utc).isoformat(),
"timestamp": datetime.now(UTC).isoformat(),
"checks": checks,
}
if not all_ready:
+2 -2
View File
@@ -12,6 +12,7 @@
from __future__ import annotations
import logging
from datetime import UTC
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import (
@@ -196,9 +197,8 @@ def get_lipsync_job(
# 三层防御 ①:如果距上次更新超过 30 秒,同步刷新一次(避免 background task
# 静默失败导致前端永远看到 running);否则挂后台异步刷新(避免阻塞轮询)。
from datetime import datetime as _dt
from datetime import timezone as _tz
_now = _dt.now(_tz.utc)
_now = _dt.now(UTC)
_stale = job.updated_at is None or (_now - job.updated_at).total_seconds() > 30
if _stale:
try:
+5 -6
View File
@@ -4,8 +4,7 @@ from __future__ import annotations
import logging
from dataclasses import replace
from datetime import datetime, timezone
from typing import List
from datetime import UTC, datetime
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_user_repository
@@ -67,7 +66,7 @@ def _get_plan_price(plan_id: str, billing_cycle: str) -> float:
def _build_subscription_info(user: AuthenticatedUser) -> SubscriptionInfo:
"""构建订阅信息响应"""
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
if user.user.subscription_expires_at:
period_end = user.user.subscription_expires_at.isoformat()
period_start = now.isoformat()
@@ -100,10 +99,10 @@ async def get_current_subscription(
return _build_subscription_info(current_user)
@router.get("/billing-records", response_model=List[BillingRecord])
@router.get("/billing-records", response_model=list[BillingRecord])
async def get_billing_records(
current_user: AuthenticatedUser = Depends(get_current_user),
) -> List[BillingRecord]:
) -> list[BillingRecord]:
"""获取账单记录列表"""
from packages.adapters.sqlalchemy_impl.billing_repository import SQLAlchemyBillingRepository
from packages.adapters.sqlalchemy_impl.session import SessionLocal
@@ -251,7 +250,7 @@ async def payment_callback(
# 计算到期时间
days = 365 if billing_cycle == "yearly" else 30
expires_at = datetime.now(timezone.utc) + timedelta(days=days)
expires_at = datetime.now(UTC) + timedelta(days=days)
repo.update_subscription_on_payment(user_id, plan, expires_at)
return {"success": True, "message": "支付成功", "record_id": record_id}
@@ -6,7 +6,7 @@
from __future__ import annotations
import re as _re
from typing import Any, List, Optional
from typing import Any, Optional
from pydantic import BaseModel, Field, model_validator, validator
@@ -21,7 +21,7 @@ _EXPORT_VALID_FORMATS = {"mp4", "mov"}
class AIRecommendRequest(BaseModel):
"""AI 推荐片段方案请求体"""
asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表")
asset_ids: list[str] = Field(default_factory=list, description="素材 ID 列表")
editing_mode: str = Field(default="one_take", description="剪辑模式: one_take / pip / voice_over / voice_pip")
target_duration: float = Field(default=30.0, ge=1.0, le=600.0, description="目标时长(秒)")
@@ -44,7 +44,7 @@ class AIRecommendResponse(BaseModel):
"""AI 推荐片段方案响应体"""
plan_id: str = Field(..., description="剪辑计划 ID")
clips: List[AIRecommendClipItem] = Field(..., description="推荐的片段列表")
clips: list[AIRecommendClipItem] = Field(..., description="推荐的片段列表")
config: dict[str, Any] = Field(..., description="推荐的 plan config(cover/title/subtitle/bgm)")
total_duration: float = Field(..., ge=0.0, description="推荐方案总时长(秒)")
confidence: float = Field(..., ge=0.0, le=1.0, description="AI 推荐置信度 (0~1)")
@@ -137,7 +137,7 @@ class ClipReorderItem(BaseModel):
class ClipReorderRequest(BaseModel):
"""片段重排序请求"""
items: List[ClipReorderItem] = Field(..., min_length=1, max_length=500, description="重排序条目列表")
items: list[ClipReorderItem] = Field(..., min_length=1, max_length=500, description="重排序条目列表")
class ClipReorderResponse(BaseModel):
@@ -151,7 +151,7 @@ class ClipReorderResponse(BaseModel):
class ClipBatchDeleteRequest(BaseModel):
"""批量删除片段请求"""
clip_ids: List[str] = Field(..., min_length=1, max_length=500, description="要删除的片段ID列表")
clip_ids: list[str] = Field(..., min_length=1, max_length=500, description="要删除的片段ID列表")
class ClipBatchDeleteResponse(BaseModel):
@@ -169,7 +169,7 @@ _UNSET = object()
class ClipsFromAssetsRequest(BaseModel):
"""从素材批量创建片段请求"""
asset_ids: List[str] = Field(..., min_length=1, max_length=200, description="素材 ID 列表,按顺序追加到时间线末尾")
asset_ids: list[str] = Field(..., min_length=1, max_length=200, description="素材 ID 列表,按顺序追加到时间线末尾")
clip_type: str = Field(default="main", description="片段类型,默认 main")
clip_count: Optional[int] = Field(
default=None,
@@ -218,7 +218,7 @@ class ClipsFromAssetsResponse(BaseModel):
created_count: int
plan_id: str = ""
message: str = ""
clip_ids: List[str] = Field(default_factory=list, description="创建的片段ID列表")
clip_ids: list[str] = Field(default_factory=list, description="创建的片段ID列表")
duplicate_warning: Optional[str] = Field(default=None, description="查重率超标警告")
exhaustion_warning: Optional[str] = Field(default=None, description="素材耗尽警告")
@@ -302,7 +302,7 @@ class ExportPresetItem(BaseModel):
class ExportPresetListResponse(BaseModel):
"""导出预设列表响应"""
items: List[ExportPresetItem]
items: list[ExportPresetItem]
total: int
@@ -316,7 +316,7 @@ class FilterPresetResponse(BaseModel):
name: str
category: str
description: str
tags: List[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
class FilterConfigResponse(BaseModel):
@@ -346,7 +346,7 @@ class FilterUpdateRequest(BaseModel):
class FilterPresetListResponse(BaseModel):
"""滤镜预设列表响应"""
items: List[FilterPresetResponse]
items: list[FilterPresetResponse]
total: int
@@ -360,7 +360,7 @@ class TransitionPresetResponse(BaseModel):
name: str
category: str
description: str
tags: List[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
default_duration: float
min_duration: float
max_duration: float
@@ -402,7 +402,7 @@ class BatchTransitionResponse(BaseModel):
class TransitionPresetListResponse(BaseModel):
"""转场预设列表响应"""
items: List[TransitionPresetResponse]
items: list[TransitionPresetResponse]
total: int
@@ -458,7 +458,7 @@ class EditorClipResponse(BaseModel):
class EditorClipListResponse(BaseModel):
"""片段列表响应"""
items: List[EditorClipResponse]
items: list[EditorClipResponse]
total: int
@@ -496,7 +496,7 @@ class EditorClipBatchItem(BaseModel):
class EditorClipBatchUpdateRequest(BaseModel):
"""批量替换clips请求(全量覆盖)"""
clips: List[EditorClipBatchItem] = Field(default_factory=list)
clips: list[EditorClipBatchItem] = Field(default_factory=list)
class EditorClipBatchUpdateResponse(BaseModel):
@@ -584,4 +584,4 @@ class EditorTimelineResponse(BaseModel):
plan_id: str
total_duration: float
scenes: List[EditorTimelineSceneResponse]
scenes: list[EditorTimelineSceneResponse]
+1 -1
View File
@@ -6,7 +6,7 @@ ensuring proper lifecycle management and testability.
from __future__ import annotations
from typing import Generator
from collections.abc import Generator
import redis
from app.config import settings
+1 -1
View File
@@ -4,7 +4,7 @@
import logging
import time
from typing import Callable
from collections.abc import Callable
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
@@ -10,7 +10,7 @@ Exposes:
import re
import time
from typing import Callable
from collections.abc import Callable
from fastapi import Request, Response
from prometheus_client import (
+7 -7
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import datetime
from typing import List, Optional
from typing import Optional
from pydantic import BaseModel, Field
@@ -20,8 +20,8 @@ class ScriptResponse(BaseModel):
user_id: str
title: str
content: str
segments: List[ScriptSegment] = Field(default_factory=list)
tags: List[str] = Field(default_factory=list)
segments: list[ScriptSegment] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
created_at: datetime
updated_at: datetime
@@ -34,12 +34,12 @@ class ScriptListResponse(BaseModel):
class CreateScriptRequest(BaseModel):
title: str = Field(..., min_length=1, max_length=255)
content: str = ""
segments: List[ScriptSegment] = Field(default_factory=list)
tags: List[str] = Field(default_factory=list)
segments: list[ScriptSegment] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
class UpdateScriptRequest(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=255)
content: Optional[str] = None
segments: Optional[List[ScriptSegment]] = None
tags: Optional[List[str]] = None
segments: Optional[list[ScriptSegment]] = None
tags: Optional[list[str]] = None
+13 -13
View File
@@ -8,7 +8,7 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from pydantic import BaseModel, Field
@@ -42,12 +42,12 @@ class TemplateResponse(BaseModel):
name: str
mode: str
category: str = ""
tags: List[str] = Field(default_factory=list)
title_config: Dict[str, Any] = Field(default_factory=dict)
subtitle_config: Dict[str, Any] = Field(default_factory=dict)
bgm_config: Dict[str, Any] = Field(default_factory=dict)
tags: list[str] = Field(default_factory=list)
title_config: dict[str, Any] = Field(default_factory=dict)
subtitle_config: dict[str, Any] = Field(default_factory=dict)
bgm_config: dict[str, Any] = Field(default_factory=dict)
estimated_duration: float = 0.0
segments: List[SegmentResponse] = Field(default_factory=list)
segments: list[SegmentResponse] = Field(default_factory=list)
is_active: bool = True
is_favorite: bool = False
usage_count: int = 0
@@ -56,7 +56,7 @@ class TemplateResponse(BaseModel):
class ListTemplatesResponse(BaseModel):
items: List[TemplateResponse]
items: list[TemplateResponse]
total: int = 0
@@ -67,12 +67,12 @@ class CreateTemplateRequest(BaseModel):
name: str
mode: str
category: str = ""
tags: List[str] = Field(default_factory=list)
title_config: Dict[str, Any] = Field(default_factory=dict)
subtitle_config: Dict[str, Any] = Field(default_factory=dict)
bgm_config: Dict[str, Any] = Field(default_factory=dict)
tags: list[str] = Field(default_factory=list)
title_config: dict[str, Any] = Field(default_factory=dict)
subtitle_config: dict[str, Any] = Field(default_factory=dict)
bgm_config: dict[str, Any] = Field(default_factory=dict)
estimated_duration: float = 0.0
segments: List[SegmentRequest] = Field(default_factory=list)
segments: list[SegmentRequest] = Field(default_factory=list)
class GenerateWarningResponse(BaseModel):
@@ -80,4 +80,4 @@ class GenerateWarningResponse(BaseModel):
code: str
message: str
details: Dict[str, Any] = Field(default_factory=dict)
details: dict[str, Any] = Field(default_factory=dict)
+4 -4
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import datetime
from typing import List, Optional
from typing import Optional
from pydantic import BaseModel, Field
@@ -15,7 +15,7 @@ class TitleLibraryItemResponse(BaseModel):
text: str
category: str = "default"
description: str = ""
tags: List[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
usage_count: int = 0
is_active: bool = True
created_at: datetime
@@ -32,7 +32,7 @@ class CreateTitleLibraryRequest(BaseModel):
text: str = Field(..., min_length=1, max_length=500)
category: str = "default"
description: str = ""
tags: List[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
class UpdateTitleLibraryRequest(BaseModel):
@@ -40,4 +40,4 @@ class UpdateTitleLibraryRequest(BaseModel):
text: Optional[str] = Field(None, min_length=1, max_length=500)
category: Optional[str] = None
description: Optional[str] = None
tags: Optional[List[str]] = None
tags: Optional[list[str]] = None
+4 -4
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from pydantic import BaseModel, Field
@@ -23,7 +23,7 @@ class TTSSynthesizeRequest(BaseModel):
voice_model: str = Field("", description="语音模型名称")
voice_clone_profile_id: str = Field("", description="关联的音色克隆档案 ID")
format: str = Field("mp3", description="输出格式(mp3/wav/pcm)")
metadata_: Optional[Dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据")
metadata_: Optional[dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据")
class Config:
populate_by_name = True
@@ -49,7 +49,7 @@ class TTSJobResponse(BaseModel):
error_message: str = ""
retry_count: int = 0
max_retries: int = 3
metadata_: Optional[Dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据")
metadata_: Optional[dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据")
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
created_at: datetime
@@ -83,7 +83,7 @@ class TTSSynthesizeResponse(BaseModel):
class ListTTSJobResponse(BaseModel):
"""TTS 任务列表响应。"""
items: List[TTSJobResponse]
items: list[TTSJobResponse]
total: int
page: int
page_size: int
+2 -2
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import datetime
from typing import List, Optional
from typing import Optional
from pydantic import BaseModel, Field
@@ -61,7 +61,7 @@ class ShareResponse(BaseModel):
class ShareListResponse(BaseModel):
"""分享列表响应."""
items: List[ShareResponse]
items: list[ShareResponse]
total: int = 0
skip: int = 0
limit: int = 20
+3 -3
View File
@@ -6,7 +6,7 @@
from __future__ import annotations
from datetime import datetime
from typing import List, Literal, Optional
from typing import Literal, Optional
from pydantic import BaseModel, Field
@@ -56,7 +56,7 @@ class UnifiedVoiceItemResponse(BaseModel):
status: str = "completed"
"""状态"""
tags: List[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
"""标签列表"""
# 克隆音色特有字段
@@ -113,7 +113,7 @@ class PresetVoiceItemResponse(BaseModel):
preview_url: str = ""
"""预览音频 URL"""
tags: List[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
"""标签列表"""
+4 -4
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from pydantic import BaseModel, Field
@@ -19,7 +19,7 @@ class CreateVoiceCloneRequest(BaseModel):
language: str = Field("zh-CN", description="语言")
gender: str = Field("unknown", description="性别")
max_retries: int = Field(3, ge=1, le=10, description="最大重试次数")
metadata_: Optional[Dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据")
metadata_: Optional[dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据")
class Config:
populate_by_name = True
@@ -41,7 +41,7 @@ class VoiceCloneProfileResponse(BaseModel):
error_message: str = ""
retry_count: int = 0
max_retries: int = 3
metadata_: Optional[Dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据")
metadata_: Optional[dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据")
created_at: datetime
updated_at: datetime
@@ -62,7 +62,7 @@ class VoiceCloneStatusResponse(BaseModel):
class ListVoiceCloneResponse(BaseModel):
"""音色克隆列表响应。"""
items: List[VoiceCloneProfileResponse]
items: list[VoiceCloneProfileResponse]
total: int
+4 -4
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from datetime import datetime
from typing import List, Optional
from typing import Optional
from pydantic import BaseModel, Field
@@ -21,7 +21,7 @@ class VoiceLibraryItemResponse(BaseModel):
file_size: int = 0
status: str = "completed"
project_id: Optional[str] = None
tags: List[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
created_at: datetime
updated_at: datetime
@@ -42,7 +42,7 @@ class CreateVoiceLibraryRequest(BaseModel):
file_size: int = 0
status: str = "completed"
project_id: Optional[str] = None
tags: List[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
class UpdateVoiceLibraryRequest(BaseModel):
@@ -55,4 +55,4 @@ class UpdateVoiceLibraryRequest(BaseModel):
duration: Optional[float] = None
file_size: Optional[int] = None
status: Optional[str] = None
tags: Optional[List[str]] = None
tags: Optional[list[str]] = None
@@ -16,7 +16,7 @@ import os
import subprocess
import tempfile
import uuid
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any, Optional
from sqlalchemy.orm import Session
@@ -117,7 +117,7 @@ class AiAvatarRenderService:
self.db.add(job)
self.db.flush()
job.submitted_at = datetime.now(timezone.utc)
job.submitted_at = datetime.now(UTC)
self.db.commit()
self.db.refresh(job)
return job
@@ -164,7 +164,7 @@ class AiAvatarRenderService:
return None
if job.status in ("pending", "submitted"):
job.status = "cancelled"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
self.db.commit()
self.db.refresh(job)
return job
@@ -186,7 +186,7 @@ class AiAvatarRenderService:
job.output_duration = 0.0
job.started_at = None
job.completed_at = None
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
self.db.commit()
self.db.refresh(job)
return job
@@ -215,9 +215,9 @@ class AiAvatarRenderService:
try:
# 更新状态为 processing
job.status = "processing"
job.started_at = datetime.now(timezone.utc)
job.started_at = datetime.now(UTC)
job.progress = 5
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
self.db.commit()
# 获取对口型任务信息
@@ -394,8 +394,8 @@ class AiAvatarRenderService:
# 6. 完成
job.status = "completed"
job.progress = 100
job.completed_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(timezone.utc)
job.completed_at = datetime.now(UTC)
job.updated_at = datetime.now(UTC)
self.db.commit()
logger.info("渲染任务完成: %s", job_id)
@@ -406,14 +406,14 @@ class AiAvatarRenderService:
except AiAvatarRenderError as exc:
job.status = "failed"
job.error_message = str(exc)
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
self.db.commit()
logger.error("渲染任务失败 [%s]: %s", job_id, exc)
raise
except Exception as exc:
job.status = "failed"
job.error_message = f"渲染异常: {str(exc)}"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
self.db.commit()
logger.exception("渲染任务异常 [%s]", job_id)
raise
@@ -494,7 +494,7 @@ class AiAvatarRenderService:
# 同步更新 cover_config,保持 smart-cover 路径一致
if isinstance(job.cover_config, dict):
job.cover_config = {**job.cover_config, "mode": "auto_frame", "url": cover_url.strip()}
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
self.db.commit()
return self._persist_to_library(job, cover_url=cover_url)
+12 -12
View File
@@ -13,7 +13,7 @@
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from packages.domain.ai_parsing import generate_titles_fallback as _generate_titles_fallback_base
from packages.domain.ai_parsing import keyword_match_fallback as _semantic_match_fallback_base
@@ -64,7 +64,7 @@ def _generate_titles_fallback(
description: str,
style: str = "viral",
count: int = 5,
) -> List[str]:
) -> list[str]:
"""本地降级:基于模板规则生成标题(薄包装,转发到 ai_parsing 模块)."""
style_info = TITLE_STYLES.get(style, TITLE_STYLES["viral"])
return _generate_titles_fallback_base(description, style_info, count)
@@ -74,7 +74,7 @@ def generate_smart_titles(
description: str,
style: str = "viral",
count: int = 5,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""生成智能标题.
Args:
@@ -164,16 +164,16 @@ def generate_smart_titles(
def _semantic_match_fallback(
description: str,
assets: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
assets: list[dict[str, Any]],
) -> list[dict[str, Any]]:
"""本地降级:基于关键词的简单匹配(薄包装,转发到 ai_parsing 模块)."""
return _semantic_match_fallback_base(description, assets)
def _parse_semantic_match_response(
content: str,
asset_ids: List[str],
) -> Optional[Dict[str, float]]:
asset_ids: list[str],
) -> Optional[dict[str, float]]:
"""从模型返回中解析素材匹配度(薄包装,转发到 ai_parsing 模块)."""
result = _parse_semantic_match_base(content, asset_ids)
if result is None:
@@ -183,9 +183,9 @@ def _parse_semantic_match_response(
def semantic_match_assets(
description: str,
assets: List[Dict[str, Any]],
assets: list[dict[str, Any]],
top_k: int = 0,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""智能素材语义匹配.
根据用户描述,评估每个素材的语义匹配度并排序。
@@ -336,13 +336,13 @@ class AIService:
description: str,
style: str = "viral",
count: int = 5,
) -> Dict[str, Any]:
) -> dict[str, Any]:
return generate_smart_titles(description, style, count)
def semantic_match(
self,
description: str,
assets: List[Dict[str, Any]],
assets: list[dict[str, Any]],
top_k: int = 0,
) -> Dict[str, Any]:
) -> dict[str, Any]:
return semantic_match_assets(description, assets, top_k)
@@ -28,8 +28,8 @@ from __future__ import annotations
import json
import logging
from datetime import datetime, timezone
from typing import Callable
from collections.abc import Callable
from datetime import UTC, datetime
from sqlalchemy.orm import Session
@@ -57,7 +57,7 @@ _REUSE_OVERLAP_RATIO = 0.6
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
return datetime.now(UTC).isoformat()
def _read_meta(model) -> dict:
@@ -156,7 +156,7 @@ def record_used_segments(
r["plan_id"] = plan_id
meta[USED_RANGES_KEY] = ranges
model.classification_result = json.dumps(meta, ensure_ascii=False)
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
return
ranges.append(
@@ -171,7 +171,7 @@ def record_used_segments(
)
meta[USED_RANGES_KEY] = ranges
model.classification_result = json.dumps(meta, ensure_ascii=False)
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
def remove_used_segment(
@@ -215,7 +215,7 @@ def remove_used_segment(
if removed:
meta[USED_RANGES_KEY] = remaining
model.classification_result = json.dumps(meta, ensure_ascii=False)
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
return removed
@@ -231,7 +231,7 @@ def reset_used_segments(db: Session, asset_id: str) -> None:
if meta.get(USED_RANGES_KEY):
meta[USED_RANGES_KEY] = []
model.classification_result = json.dumps(meta, ensure_ascii=False)
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
logger.info("[片段追踪] 素材区间记录手动清空: asset_id=%s", asset_id)
+9 -9
View File
@@ -7,7 +7,7 @@
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from app.services.asset_segment_tracker import (
REUSE_RATIO_LIMIT,
@@ -206,7 +206,7 @@ class EditPlanService:
status: Optional[EditPlanClipStatus] = None,
skip: int = 0,
limit: int = 100,
) -> List[EditPlanClip]:
) -> list[EditPlanClip]:
"""列出计划的片段"""
# 确保计划存在
self.get_plan_or_raise(plan_id)
@@ -503,7 +503,7 @@ class EditPlanService:
source = self.get_plan_or_raise(source_plan_id)
# 分页读取源 plan 全部片段
clips: List[EditPlanClip] = []
clips: list[EditPlanClip] = []
skip, page = 0, 500
while True:
batch = self._clip_repo.list_by_plan(source_plan_id, skip=skip, limit=page)
@@ -665,7 +665,7 @@ class EditPlanService:
source = self.get_plan_or_raise(source_plan_id)
# 分页读取源 plan 全部片段
clips: List[EditPlanClip] = []
clips: list[EditPlanClip] = []
skip, page = 0, 500
while True:
batch = self._clip_repo.list_by_plan(source_plan_id, skip=skip, limit=page)
@@ -797,7 +797,7 @@ class EditPlanService:
except Exception:
pass
clips: List[EditPlanClip] = []
clips: list[EditPlanClip] = []
skip, page = 0, 500
while True:
batch = self._clip_repo.list_by_plan(plan_id, skip=skip, limit=page)
@@ -1063,7 +1063,7 @@ class EditPlanService:
# ── 片段分割与合并 ──────────────────────────────────────────────────────
def split_clip(self, clip_id: str, split_time: float) -> Dict[str, Any]:
def split_clip(self, clip_id: str, split_time: float) -> dict[str, Any]:
"""将一个片段从指定位置分割为两个片段
Args:
@@ -1151,7 +1151,7 @@ class EditPlanService:
"right_clip": created_right,
}
def merge_clips(self, clip_ids: List[str]) -> EditPlanClip:
def merge_clips(self, clip_ids: list[str]) -> EditPlanClip:
"""合并多个连续片段为一个片段
Args:
@@ -1217,7 +1217,7 @@ class EditPlanService:
# ── 渲染生成流程 ────────────────────────────────────────────────────────
def get_generation_status(self, plan_id: str) -> Dict[str, Any]:
def get_generation_status(self, plan_id: str) -> dict[str, Any]:
"""获取渲染进度状态
Returns:
@@ -1364,7 +1364,7 @@ class EditPlanService:
)
return count
def update_plan_config(self, plan_id: str, config_updates: Dict[str, Any]) -> EditPlan:
def update_plan_config(self, plan_id: str, config_updates: dict[str, Any]) -> EditPlan:
"""更新计划配置(合并更新)
Args:
@@ -7,7 +7,7 @@
from __future__ import annotations
import logging
from typing import Any, List, Optional
from typing import Any, Optional
from sqlalchemy.orm import Session
@@ -76,7 +76,7 @@ class EditTemplateService:
active_only: bool = False,
skip: int = 0,
limit: int = 50,
) -> List[EditTemplate]:
) -> list[EditTemplate]:
"""列出模板
Args:
@@ -227,7 +227,7 @@ class EditTemplateService:
clip_type: Optional[ClipType] = None,
skip: int = 0,
limit: int = 100,
) -> List[TemplateClipConfig]:
) -> list[TemplateClipConfig]:
"""列出模板的片段配置
注意:本方法要求模板存在于新表 ``edit_templates``(全局模板库),
@@ -253,7 +253,7 @@ class EditTemplateService:
clip_type: Optional[ClipType] = None,
skip: int = 0,
limit: int = 100,
) -> List[TemplateClipConfig]:
) -> list[TemplateClipConfig]:
"""编辑器读取模板片段配置的单一数据源入口.
片段配置主表是 ``template_clip_configs``(直接读取,不抛异常、不降级)。
@@ -404,8 +404,8 @@ class EditTemplateService:
def reorder_clip_configs(
self,
template_id: str,
config_ids: List[str],
) -> List[TemplateClipConfig]:
config_ids: list[str],
) -> list[TemplateClipConfig]:
"""重新排序片段配置
Args:
@@ -560,7 +560,7 @@ class EditTemplateService:
)
# 5. 转换每个片段为模板片段配置
created_configs: List[TemplateClipConfig] = []
created_configs: list[TemplateClipConfig] = []
for clip_config_obj in clips_to_template_clip_configs(created_template.id, clips):
created = self._clip_config_repo.create(clip_config_obj)
created_configs.append(created)
+8 -8
View File
@@ -17,7 +17,7 @@ from __future__ import annotations
import io
import logging
import uuid
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Optional
from urllib.parse import urlparse
@@ -229,7 +229,7 @@ class LipsyncService:
)
job.mediakit_task_id = result["task_id"]
job.status = "submitted"
job.submitted_at = datetime.now(timezone.utc)
job.submitted_at = datetime.now(UTC)
self.db.commit()
logger.info(
"[lipsync] 直传音频已提交 MediaKit: job_id=%s task_id=%s",
@@ -338,7 +338,7 @@ class LipsyncService:
job.status = "failed"
job.error_message = f"Celery 任务投递失败: {exc}"
job.error_code = "AsyncDispatchFailed"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
self.db.commit()
else:
# 2b/2c. 直接音频 / 预合成音频:同步路径
@@ -498,8 +498,8 @@ class LipsyncService:
temp_url = result.get("video_url", "")
job.output_video_url = temp_url
job.output_duration = result.get("duration", 0.0)
job.completed_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(timezone.utc)
job.completed_at = datetime.now(UTC)
job.updated_at = datetime.now(UTC)
self.db.commit()
# 异步转存自家 OSS
try:
@@ -517,12 +517,12 @@ class LipsyncService:
job.status = "failed"
job.error_message = error.get("message", "任务执行失败")
job.error_code = error.get("code", "TaskFailed")
job.completed_at = datetime.now(timezone.utc)
job.completed_at = datetime.now(UTC)
else:
# 中间状态(running/processing/queued 等)同步到 DB,避免前端永远卡在 submitted
if isinstance(mk_status, str) and mk_status:
job.status = mk_status
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
self.db.commit()
except Exception as exc: # noqa: BLE001 - DB 提交失败必须记录日志并重试,否则后台任务静默失败
logger.error(
@@ -589,7 +589,7 @@ class LipsyncService:
if job.status in ("pending", "tts_processing", "submitted"):
job.status = "cancelled"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
self.db.commit()
self.db.refresh(job)
+13 -13
View File
@@ -14,7 +14,7 @@ from __future__ import annotations
import logging
import random
from typing import Any, List
from typing import Any
from sqlalchemy.orm import Session
@@ -62,8 +62,8 @@ class PlanGeneratorService:
def generate_from_template(
self,
template: EditTemplate,
clip_configs: List[TemplateClipConfig],
asset_ids: List[str],
clip_configs: list[TemplateClipConfig],
asset_ids: list[str],
*,
project_id: str = "",
created_by_user_id: str = "",
@@ -135,7 +135,7 @@ class PlanGeneratorService:
)
# 5. 持久化所有 clips 并计算总时长
created_clips: List[EditPlanClip] = []
created_clips: list[EditPlanClip] = []
total_duration = 0.0
for clip in clips:
saved = self._clip_repo.create(clip)
@@ -184,15 +184,15 @@ class PlanGeneratorService:
def _create_clips_from_configs(
self,
plan_id: str,
clip_configs: List[TemplateClipConfig],
) -> List[EditPlanClip]:
clip_configs: list[TemplateClipConfig],
) -> list[EditPlanClip]:
"""从 TemplateClipConfig 列表创建 EditPlanClip 列表(未持久化).
委托给 plan_generator_utils.create_clips_from_configs 纯函数。
"""
return create_clips_from_configs(plan_id, clip_configs)
def _map_clip_types_for_mode(self, clips: List[EditPlanClip], editing_mode: str) -> None:
def _map_clip_types_for_mode(self, clips: list[EditPlanClip], editing_mode: str) -> None:
"""将 MAIN 类型片段按 editing_mode 映射为对应角色类型.
委托给 plan_generator_utils.map_clip_types_for_mode 纯函数。
@@ -204,7 +204,7 @@ class PlanGeneratorService:
plan_id: str,
editing_mode: str,
asset_count: int,
) -> List[EditPlanClip]:
) -> list[EditPlanClip]:
"""无 clip_configs 时,根据 editing_mode 生成默认 clip 结构.
委托给 plan_generator_utils.generate_default_clips 纯函数。
@@ -213,8 +213,8 @@ class PlanGeneratorService:
def _distribute_assets(
self,
clips: List[EditPlanClip],
asset_ids: List[str],
clips: list[EditPlanClip],
asset_ids: list[str],
editing_mode: str,
*,
random_selection: bool = False,
@@ -259,7 +259,7 @@ class PlanGeneratorService:
external_used_segments=external_used_segments,
)
def _fetch_asset_scene_points(self, asset_ids: List[str]) -> dict[str, list[float]]:
def _fetch_asset_scene_points(self, asset_ids: list[str]) -> dict[str, list[float]]:
"""从素材 metadata 读取场景切换点缓存(无缓存的素材不包含在结果中)。"""
points_map: dict[str, list[float]] = {}
if not self._asset_repo:
@@ -272,7 +272,7 @@ class PlanGeneratorService:
points_map[asset_id] = points
return points_map
def _sort_assets_by_smart_score(self, asset_ids: List[str]) -> List[str]:
def _sort_assets_by_smart_score(self, asset_ids: list[str]) -> list[str]:
"""按 smart_match 综合评分降序排列素材 ID(注入随机噪声)。
评分高的素材(质量好、时长合适、新鲜、使用次数少)倾向排在前面;
@@ -295,7 +295,7 @@ class PlanGeneratorService:
)
return [aid for aid, _ in scored]
def _fetch_asset_durations(self, asset_ids: List[str]) -> dict[str, float]:
def _fetch_asset_durations(self, asset_ids: list[str]) -> dict[str, float]:
"""从数据库获取素材时长信息.
Args:
+2 -2
View File
@@ -6,7 +6,7 @@
from __future__ import annotations
import uuid
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Optional
from sqlalchemy.orm import Session
@@ -93,7 +93,7 @@ class ScriptService:
script.segments = segments
if tags is not None:
script.tags = tags
script.updated_at = datetime.now(timezone.utc)
script.updated_at = datetime.now(UTC)
self.db.commit()
self.db.refresh(script)
return script
+12 -12
View File
@@ -20,7 +20,7 @@ _estimate_sentence_timings_by_chars 等内部函数已复用共享实现,避
import io
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from urllib.parse import urlparse
from celery import shared_task
@@ -167,7 +167,7 @@ def tts_synthesize_and_submit(
job.status = "failed"
job.error_message = f"TTS 合成失败: {exc}"
job.error_code = "TTSSynthesisFailed"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
db.commit()
return
except ValueError as exc:
@@ -175,7 +175,7 @@ def tts_synthesize_and_submit(
job.status = "failed"
job.error_message = f"TTS 参数错误: {exc}"
job.error_code = "TTSInvalidParam"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
db.commit()
return
@@ -185,7 +185,7 @@ def tts_synthesize_and_submit(
job.status = "failed"
job.error_message = "TTS 未返回音频 URL"
job.error_code = "TTSNoAudio"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
db.commit()
return
@@ -271,7 +271,7 @@ def tts_synthesize_and_submit(
)
job.mediakit_task_id = mk_result["task_id"]
job.status = "submitted"
job.submitted_at = datetime.now(timezone.utc)
job.submitted_at = datetime.now(UTC)
logger.info(
"[lipsync_tts] 已提交 MediaKit: job_id=%s task_id=%s",
job_id,
@@ -305,7 +305,7 @@ def tts_synthesize_and_submit(
job.status = "failed"
job.error_message = "TTS 异步任务执行异常"
job.error_code = "AsyncTaskError"
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
db.commit()
except Exception:
logger.exception("[lipsync_tts] 回写失败状态时异常: job_id=%s", job_id)
@@ -374,8 +374,8 @@ def poll_mediakit_status(self, job_id: str, user_id: str):
logger.warning("[lipsync_poll] 转存 OSS 失败,保留临时 URL: job_id=%s err=%s", job_id, exc)
job.output_video_url = output_url
job.output_duration = result.get("duration", 0.0)
job.completed_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(timezone.utc)
job.completed_at = datetime.now(UTC)
job.updated_at = datetime.now(UTC)
db.commit()
logger.info("[lipsync_poll] 任务完成: job_id=%s", job_id)
elif mk_status in ("failed", "error", _SF):
@@ -383,13 +383,13 @@ def poll_mediakit_status(self, job_id: str, user_id: str):
job.status = "failed"
job.error_message = error.get("message", "任务执行失败")
job.error_code = error.get("code", "TaskFailed")
job.completed_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(timezone.utc)
job.completed_at = datetime.now(UTC)
job.updated_at = datetime.now(UTC)
db.commit()
logger.info("[lipsync_poll] 任务失败: job_id=%s err=%s", job_id, job.error_message)
else:
# 中间状态,更新时间戳,继续重试
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
if isinstance(mk_status, str) and mk_status:
job.status = mk_status
db.commit()
@@ -446,7 +446,7 @@ def persist_output_video_task(job_id: str, user_id: str, temp_url: str):
permanent_url = storage.upload_file(io.BytesIO(data), storage_key, content_type="video/mp4")
final_url = _sign_media_url(permanent_url) if permanent_url else temp_url
job.output_video_url = final_url
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
db.commit()
logger.info("[lipsync_tts.persist] 输出视频已转存 OSS: job_id=%s key=%s", job_id, storage_key)
except Exception as exc:
+1 -2
View File
@@ -6,7 +6,6 @@ import logging
import os
import tempfile
from dataclasses import dataclass
from typing import List
import ffmpeg
from shared.ffmpeg_utils import FFMPEG_ENCODE_CRF, FFMPEG_ENCODE_PRESET
@@ -41,7 +40,7 @@ class VideoProcessor:
def concatenate_videos(
self,
input_paths: List[str],
input_paths: list[str],
output_path: str,
resolution: tuple[int, int] = (1920, 1080),
fps: int = 25,
@@ -18,9 +18,10 @@ from __future__ import annotations
import logging
import subprocess
import tempfile
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable
from typing import Any
from sqlalchemy.orm import Session
from video_processing.oss_helpers import download_asset, upload_to_oss
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
def mark_asset_used_for_generation(asset, times: int = 1) -> None:
@@ -15,6 +15,6 @@ def mark_asset_used_for_generation(asset, times: int = 1) -> None:
asset.metadata = {
**asset.metadata,
"generation_use_count": int(asset.metadata.get("generation_use_count") or 0) + times,
"last_used_at": datetime.now(timezone.utc).isoformat(),
"last_used_at": datetime.now(UTC).isoformat(),
"review_status": asset.metadata.get("review_status") or "pending_review",
}
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from packages.adapters.sqlalchemy_impl.models import TitleLibraryModel
@@ -10,6 +10,6 @@ def mark_title_used_for_generation(db, task) -> None:
if title is None:
return
title.usage_count = int(title.usage_count or 0) + 1
title.updated_at = datetime.now(timezone.utc)
title.updated_at = datetime.now(UTC)
db.add(title)
db.commit()
+3 -2
View File
@@ -1,6 +1,7 @@
"""Worker 启动时的初始化任务 — 孤儿任务清理等."""
import logging
from datetime import UTC
from celery.signals import worker_ready
from worker_app.db import SessionLocal
@@ -130,14 +131,14 @@ def cleanup_stale_jobs(timeout_minutes: int = ORPHAN_TASK_TIMEOUT_MINUTES) -> in
Returns:
清理的任务数量
"""
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
from packages.adapters.sqlalchemy_impl.models import JobModel
from packages.domain.job import JobStatus
try:
session = SessionLocal()
cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes)
cutoff = datetime.now(UTC) - timedelta(minutes=timeout_minutes)
stale_jobs = (
session.query(JobModel)
.filter(
@@ -12,7 +12,7 @@ from __future__ import annotations
import traceback
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
# ── 数据类 ───────────────────────────────────────────────────────────────────
@@ -322,5 +322,5 @@ def build_error_info(error: Exception, stage: str = "render") -> dict[str, Any]:
"message": str(error),
"stack_trace": tb_summary,
"stage": stage,
"failed_at": datetime.now(timezone.utc).isoformat(),
"failed_at": datetime.now(UTC).isoformat(),
}
+8 -8
View File
@@ -2,7 +2,7 @@ import json
import shutil
import subprocess
import tempfile
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from celery.utils.log import get_task_logger
@@ -447,7 +447,7 @@ def ingest_asset(job_id: str) -> dict:
# Update job status to PROCESSING
job.status = IngestJobStatus.PROCESSING
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
job_repo.update(job)
db.commit()
@@ -703,7 +703,7 @@ def ingest_asset(job_id: str) -> dict:
if codec_val:
asset.codec = str(codec_val)
asset.status = AssetStatus.ERROR
asset.updated_at = datetime.now(timezone.utc)
asset.updated_at = datetime.now(UTC)
asset_repo.update(asset)
else:
# 旧链路兜底:无占位记录(如历史 job 重跑)才新建
@@ -729,7 +729,7 @@ def ingest_asset(job_id: str) -> dict:
job.status = IngestJobStatus.FAILED
job.error_message = f"Invalid media file: {error_reason}"
job.result_asset_id = asset.id
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
job_repo.update(job)
db.commit()
@@ -797,13 +797,13 @@ def ingest_asset(job_id: str) -> dict:
pass
asset.status = AssetStatus.READY
asset.thumbnail_url = thumbnail_url
asset.updated_at = datetime.now(timezone.utc)
asset.updated_at = datetime.now(UTC)
asset_repo.update(asset)
# Update job status to COMPLETED
job.status = IngestJobStatus.COMPLETED
job.result_asset_id = asset.id
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
job_repo.update(job)
db.commit()
@@ -825,7 +825,7 @@ def ingest_asset(job_id: str) -> dict:
if job:
job.status = IngestJobStatus.FAILED
job.error_message = str(e)
job.updated_at = datetime.now(timezone.utc)
job.updated_at = datetime.now(UTC)
job_repo.update(job)
# 将上传时创建的占位 Asset(PROCESSING/UPLOADING)标记为 ERROR,
@@ -857,7 +857,7 @@ def ingest_asset(job_id: str) -> dict:
):
existing.status = AssetStatus.ERROR
existing.metadata = {**(existing.metadata or {}), "ingest_error": str(e)}
existing.updated_at = datetime.now(timezone.utc)
existing.updated_at = datetime.now(UTC)
asset_repo.update(existing)
logger.info(
"Marked asset as ERROR due to ingest failure: asset_id=%s job_id=%s",
+12 -10
View File
@@ -1,5 +1,7 @@
"""Asset InMemory Repository 实现"""
from datetime import UTC
from packages.domain import Asset
@@ -45,7 +47,7 @@ class InMemoryAssetRepository:
def batch_delete(self, asset_ids: list[str]) -> int:
"""批量删除素材(软删除,标记 status=deleted),返回实际影响数量。"""
from datetime import datetime, timezone
from datetime import datetime
from packages.domain import AssetStatus
@@ -54,26 +56,26 @@ class InMemoryAssetRepository:
asset = self._assets.get(aid)
if asset and asset.status != AssetStatus.DELETED:
asset.status = AssetStatus.DELETED
asset.updated_at = datetime.now(timezone.utc)
asset.updated_at = datetime.now(UTC)
count += 1
return count
def batch_update_metadata(self, asset_ids: list[str], metadata_patch: dict[str, object]) -> int:
"""批量更新素材 metadata(合并 patch),返回实际影响数量。"""
from datetime import datetime, timezone
from datetime import datetime
count = 0
for aid in asset_ids:
asset = self._assets.get(aid)
if asset:
asset.metadata = {**asset.metadata, **metadata_patch}
asset.updated_at = datetime.now(timezone.utc)
asset.updated_at = datetime.now(UTC)
count += 1
return count
def batch_add_tags(self, asset_ids: list[str], tag_ids: list[str]) -> int:
"""批量给素材添加标签(合并去重),返回实际影响数量。"""
from datetime import datetime, timezone
from datetime import datetime
count = 0
for aid in asset_ids:
@@ -85,20 +87,20 @@ class InMemoryAssetRepository:
asset.tag_ids.append(tid)
changed = True
if changed:
asset.updated_at = datetime.now(timezone.utc)
asset.updated_at = datetime.now(UTC)
count += 1
return count
def batch_replace_tags(self, asset_ids: list[str], tag_ids: list[str]) -> int:
"""批量替换素材标签(全量覆盖),返回实际影响数量。"""
from datetime import datetime, timezone
from datetime import datetime
count = 0
for aid in asset_ids:
asset = self._assets.get(aid)
if asset:
asset.tag_ids = list(tag_ids)
asset.updated_at = datetime.now(timezone.utc)
asset.updated_at = datetime.now(UTC)
count += 1
return count
@@ -168,13 +170,13 @@ class InMemoryAssetRepository:
file_size: int = 0,
) -> Asset | None:
"""兜底去重:同库 + 同文件名(+同大小)且近期活动状态的素材。"""
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
if not name:
return None
from packages.domain import AssetStatus
cutoff = datetime.now(timezone.utc) - timedelta(minutes=within_minutes)
cutoff = datetime.now(UTC) - timedelta(minutes=within_minutes)
candidates = [
a
for a in self._assets.values()
@@ -3,7 +3,7 @@
"""
import copy
from typing import Dict, Optional
from typing import Optional
from packages.domain.entities import User
from packages.ports.user_repository import UserRepository
@@ -13,14 +13,14 @@ class InMemoryUserRepository(UserRepository):
"""用户仓储内存实现"""
def __init__(self):
self._users: Dict[str, User] = {}
self._email_index: Dict[str, str] = {} # email -> user_id
self._username_index: Dict[str, str] = {} # username -> user_id
self._verification_token_index: Dict[str, str] = {} # token -> user_id
self._reset_token_index: Dict[str, str] = {} # token -> user_id
self._wechat_openid_index: Dict[str, str] = {} # openid -> user_id
self._wechat_unionid_index: Dict[str, str] = {} # unionid -> user_id
self._phone_index: Dict[str, str] = {} # phone -> user_id
self._users: dict[str, User] = {}
self._email_index: dict[str, str] = {} # email -> user_id
self._username_index: dict[str, str] = {} # username -> user_id
self._verification_token_index: dict[str, str] = {} # token -> user_id
self._reset_token_index: dict[str, str] = {} # token -> user_id
self._wechat_openid_index: dict[str, str] = {} # openid -> user_id
self._wechat_unionid_index: dict[str, str] = {} # unionid -> user_id
self._phone_index: dict[str, str] = {} # phone -> user_id
def save(self, user: User) -> None:
"""保存用户(存储独立副本,避免外部修改影响内部状态)"""
@@ -14,6 +14,7 @@
from __future__ import annotations
import builtins
import hashlib
import json
import logging
@@ -21,7 +22,7 @@ import threading
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional, Set
from typing import Optional
logger = logging.getLogger(__name__)
@@ -36,7 +37,7 @@ class FeatureFlagConfig:
name: str
enabled: bool = False
percentage: int = 0 # 0-100
whitelist: Set[str] = field(default_factory=set)
whitelist: set[str] = field(default_factory=set)
def to_dict(self) -> dict:
return {
@@ -171,7 +172,7 @@ class RedisFeatureFlagStore(FeatureFlagStore):
def _redis_key(self, name: str) -> str:
return f"{self._key_prefix}{name}"
def _parse_whitelist(self, raw: Optional[str]) -> Set[str]:
def _parse_whitelist(self, raw: Optional[str]) -> builtins.set[str]:
if not raw:
return set()
try:
+3 -3
View File
@@ -5,7 +5,7 @@ Redis Session 存储
import json
import logging
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Optional
import redis
@@ -118,7 +118,7 @@ class SessionStore(SessionStorePort):
是否保存成功
"""
try:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
expires_at = now + timedelta(seconds=expires_in_seconds)
session_data = {
@@ -230,7 +230,7 @@ class SessionStore(SessionStorePort):
if not session:
return False
session["last_active_at"] = datetime.now(timezone.utc).isoformat()
session["last_active_at"] = datetime.now(UTC).isoformat()
session_key = self._session_key(session_id)
ttl = self.redis.ttl(session_key)
+3 -3
View File
@@ -6,7 +6,7 @@
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import List, Optional
from typing import Optional
from packages.domain.auth.email_service import EmailConfig, EmailServicePort
@@ -37,8 +37,8 @@ class EmailService(EmailServicePort):
subject: str,
html_body: str,
text_body: Optional[str] = None,
cc: Optional[List[str]] = None,
bcc: Optional[List[str]] = None,
cc: Optional[list[str]] = None,
bcc: Optional[list[str]] = None,
) -> tuple[bool, Optional[str]]:
"""
发送邮件
@@ -1,5 +1,5 @@
import json
from datetime import datetime, timezone
from datetime import UTC, datetime
from sqlalchemy.orm import Session
@@ -112,7 +112,7 @@ class SQLAlchemyAssetRepository:
return self.find_by_id(asset_id)
def create(self, asset: Asset) -> Asset:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
model = AssetModel(
id=asset.id,
project_id=asset.project_id,
@@ -176,7 +176,7 @@ class SQLAlchemyAssetRepository:
model.file_hash = asset.file_hash or model.file_hash
if getattr(model, "client_upload_id", None) is None and asset.client_upload_id:
model.client_upload_id = asset.client_upload_id
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
self.session.flush()
self._sync_asset_tags(asset.id, asset.tag_ids)
self.session.commit()
@@ -214,9 +214,9 @@ class SQLAlchemyAssetRepository:
"""批量删除素材(软删除,标记 status=deleted),返回实际影响数量。"""
if not asset_ids:
return 0
from datetime import datetime, timezone
from datetime import datetime
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
# 先查询待删除素材的库分布(用于更新计数)
to_delete = (
self.session.query(AssetModel.asset_library_id, AssetModel.file_size)
@@ -262,9 +262,9 @@ class SQLAlchemyAssetRepository:
"""批量更新素材 metadata(合并 patch),返回实际影响数量。"""
if not asset_ids:
return 0
from datetime import datetime, timezone
from datetime import datetime
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
# 逐条读取 + 合并 + 更新,保证 JSON 合并正确
models = self.session.query(AssetModel).filter(AssetModel.id.in_(asset_ids)).all()
count = 0
@@ -286,9 +286,9 @@ class SQLAlchemyAssetRepository:
"""批量给素材添加标签(合并去重),返回实际影响数量。"""
if not asset_ids or not tag_ids:
return 0
from datetime import datetime, timezone
from datetime import datetime
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
clean_tag_ids = list(set(tag_ids))
count = 0
for aid in asset_ids:
@@ -313,9 +313,9 @@ class SQLAlchemyAssetRepository:
"""批量替换素材标签(全量覆盖),返回实际影响数量。"""
if not asset_ids:
return 0
from datetime import datetime, timezone
from datetime import datetime
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
clean_tag_ids = list(set(tag_ids))
count = 0
for aid in asset_ids:
@@ -556,13 +556,13 @@ class SQLAlchemyAssetRepository:
file_size=0(大小未知)时直接返回 None——宁可漏判(极端情况下多建一条
占位)也不可仅凭同名 + processing 误杀内容全新的视频。
"""
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta
if not name:
return None
if not file_size or file_size <= 0:
return None
cutoff = datetime.now(timezone.utc) - timedelta(minutes=within_minutes)
cutoff = datetime.now(UTC) - timedelta(minutes=within_minutes)
query = self.session.query(AssetModel).filter(
AssetModel.asset_library_id == library_id,
AssetModel.name == name,
@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
from sqlalchemy.orm import Session
@@ -36,7 +36,7 @@ class SQLAlchemyBillingRepository:
model.status = "paid"
model.payment_method = payment_method
model.payment_id = payment_id
model.paid_at = datetime.now(timezone.utc)
model.paid_at = datetime.now(UTC)
self.session.commit()
return True
@@ -1,4 +1,4 @@
from datetime import date, datetime, timezone
from datetime import UTC, date, datetime
from sqlalchemy.orm import Session
@@ -44,7 +44,7 @@ class SQLAlchemyDailyUsageRepository:
if model is None:
return record
model.count = record.count
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
self.session.add(model)
self.session.commit()
return record
@@ -69,14 +69,14 @@ class SQLAlchemyDailyUsageRepository:
usage_date=record.usage_date,
usage_type=record.usage_type,
count=1,
updated_at=datetime.now(timezone.utc),
updated_at=datetime.now(UTC),
)
self.session.add(model)
self.session.commit()
return record
model.count += 1
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
self.session.add(model)
self.session.commit()
return self._to_domain(model)
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from sqlalchemy.orm import Session
@@ -23,7 +23,7 @@ class SQLAlchemyEditPlanClipRepository:
status: Optional[EditPlanClipStatus] = None,
skip: int = 0,
limit: int = 100,
) -> List[EditPlanClip]:
) -> list[EditPlanClip]:
"""按计划列出片段,按 order 排序"""
query = self.session.query(EditPlanClipModel).filter(
EditPlanClipModel.plan_id == plan_id,
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from sqlalchemy.orm import Session
@@ -23,7 +23,7 @@ class SQLAlchemyEditPlanRepository:
status: Optional[EditPlanStatus] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditPlan]:
) -> list[EditPlan]:
"""按模板列出剪辑计划"""
query = self.session.query(EditPlanModel).filter(
EditPlanModel.template_id == template_id,
@@ -40,7 +40,7 @@ class SQLAlchemyEditPlanRepository:
status: Optional[EditPlanStatus] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditPlan]:
) -> list[EditPlan]:
"""列出所有剪辑计划"""
query = self.session.query(EditPlanModel)
if status:
@@ -63,7 +63,7 @@ class SQLAlchemyEditPlanRepository:
status: Optional[EditPlanStatus] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditPlan]:
) -> list[EditPlan]:
"""按项目列出剪辑计划"""
query = self.session.query(EditPlanModel).filter(
EditPlanModel.project_id == project_id,
@@ -81,7 +81,7 @@ class SQLAlchemyEditPlanRepository:
status: Optional[EditPlanStatus] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditPlan]:
) -> list[EditPlan]:
"""列出用户创建的剪辑计划"""
query = self.session.query(EditPlanModel).filter(
EditPlanModel.created_by_user_id == user_id,
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from sqlalchemy.orm import Session
@@ -22,7 +22,7 @@ class SQLAlchemyEditTemplateRepository:
template_type: Optional[str] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditTemplate]:
) -> list[EditTemplate]:
"""列出所有激活的模板"""
query = self.session.query(EditTemplateModel).filter(
EditTemplateModel.status == EditTemplateStatus.ACTIVE,
@@ -43,7 +43,7 @@ class SQLAlchemyEditTemplateRepository:
status: Optional[EditTemplateStatus] = None,
skip: int = 0,
limit: int = 50,
) -> List[EditTemplate]:
) -> list[EditTemplate]:
"""列出所有模板(含非激活)"""
query = self.session.query(EditTemplateModel)
if template_type:
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from sqlalchemy.orm import Session
@@ -339,7 +339,7 @@ class SQLAlchemyGenerationTaskRepository:
"""同 cleanup_stale_running,但返回 [(task_id, celery_task_id), ...] 供撤销队列消息。"""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes)
cutoff = datetime.now(UTC) - timedelta(minutes=timeout_minutes)
models = (
self.session.query(GenerationTaskModel)
.filter(
@@ -358,9 +358,9 @@ class SQLAlchemyGenerationTaskRepository:
model.error_info = {
"error_type": "WorkerInterrupted",
"message": "任务在运行中中断,可能因 worker 重启或超时",
"failed_at": datetime.now(timezone.utc).isoformat(),
"failed_at": datetime.now(UTC).isoformat(),
}
model.completed_at = datetime.now(timezone.utc)
model.completed_at = datetime.now(UTC)
self.session.commit()
return result
@@ -377,7 +377,7 @@ class SQLAlchemyGenerationTaskRepository:
"""同 cleanup_stale_pending,但返回 [(task_id, celery_task_id), ...] 供撤销队列消息。"""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes)
cutoff = datetime.now(UTC) - timedelta(minutes=timeout_minutes)
models = (
self.session.query(GenerationTaskModel)
.filter(
@@ -391,7 +391,7 @@ class SQLAlchemyGenerationTaskRepository:
error_info = {
"error_type": "PendingTimeout",
"message": f"任务在 pending 状态停留超过 {timeout_minutes} 分钟,自动清理",
"failed_at": datetime.now(timezone.utc).isoformat(),
"failed_at": datetime.now(UTC).isoformat(),
}
result: list[tuple[str, str]] = []
for model in models:
@@ -399,6 +399,6 @@ class SQLAlchemyGenerationTaskRepository:
model.status = GenerationTaskStatus.FAILED.value
model.error_message = "pending timeout: auto cleanup"
model.error_info = error_info
model.completed_at = datetime.now(timezone.utc)
model.completed_at = datetime.now(UTC)
self.session.commit()
return result
+64 -64
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import JSON, Boolean, Column, DateTime, Float, Index, Integer, String, Text, UniqueConstraint, text
@@ -44,7 +44,7 @@ class UserModel(Base):
member_type = Column(String(20), nullable=True)
member_expires_at = Column(DateTime, nullable=True)
points_balance = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class ProjectModel(Base):
@@ -62,7 +62,7 @@ class ProjectModel(Base):
shared_users = Column(JSON, nullable=False, default=list) # 被共享的用户 ID 列表
is_default = Column(Boolean, nullable=False, default=False, server_default="false")
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class AssetLibraryModel(Base):
@@ -76,8 +76,8 @@ class AssetLibraryModel(Base):
asset_count = Column(Float, nullable=False, default=0)
total_size = Column(Float, nullable=False, default=0)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class AssetModel(Base):
@@ -108,8 +108,8 @@ class AssetModel(Base):
file_hash = Column(String(64), nullable=True, index=True)
client_upload_id = Column(String(64), nullable=True, index=True)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), index=True)
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC), index=True)
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class TagModel(Base):
@@ -120,7 +120,7 @@ class TagModel(Base):
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
name = Column(String(100), nullable=False)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
__table_args__ = (UniqueConstraint("user_id", "name", name="uq_tags_user_name"),)
@@ -132,7 +132,7 @@ class AssetTagModel(Base):
asset_id = Column(String(36), primary_key=True)
tag_id = Column(String(36), primary_key=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class EditTemplateModel(Base):
@@ -153,8 +153,8 @@ class EditTemplateModel(Base):
sort_weight = Column(Integer, nullable=False, default=0, index=True)
status = Column(String(20), nullable=False, default="active", index=True)
version = Column(Integer, nullable=False, default=1)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class EditTemplateVersionModel(Base):
@@ -174,7 +174,7 @@ class EditTemplateVersionModel(Base):
clip_configs = Column(JSON, nullable=False, default=list)
change_note = Column(String(500), nullable=False, default="")
published_by = Column(String(36), nullable=False, default="")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class EditPlanModel(Base):
@@ -194,8 +194,8 @@ class EditPlanModel(Base):
source_edit_plan_id = Column(String(36), nullable=True, index=True)
project_id = Column(String(36), nullable=False, default="", index=True)
created_by_user_id = Column(String(36), nullable=False, default="", index=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class TemplateClipConfigModel(Base):
@@ -216,8 +216,8 @@ class TemplateClipConfigModel(Base):
material_requirements = Column(JSON, nullable=False, default=dict)
transition_effect = Column(String(20), nullable=False, default="cut")
config = Column(JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class EditPlanClipModel(Base):
@@ -242,8 +242,8 @@ class EditPlanClipModel(Base):
playback_speed = Column(Float, nullable=False, default=1.0)
status = Column(String(20), nullable=False, default="pending", index=True)
config = Column(JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class IngestJobModel(Base):
@@ -259,8 +259,8 @@ class IngestJobModel(Base):
file_hash = Column(String(64), nullable=True, index=True)
asset_id = Column(String(36), nullable=False, default="", index=True)
celery_task_id = Column(String(64), nullable=False, default="", server_default="")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class ClassificationJobModel(Base):
@@ -273,8 +273,8 @@ class ClassificationJobModel(Base):
classification = Column(String(50), nullable=False, default="")
confidence = Column(Float, nullable=False, default=0.0)
error_message = Column(Text, nullable=False, default="")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class GenerationTaskModel(Base):
@@ -320,12 +320,12 @@ class GenerationTaskModel(Base):
bgm_config = Column(JSON, nullable=False, default=dict)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
logs = Column(Text, nullable=False, default="[]", server_default="[]")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(
DateTime,
nullable=False,
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
default=lambda: datetime.now(UTC),
onupdate=lambda: datetime.now(UTC),
)
@@ -349,8 +349,8 @@ class GeneratedVideoModel(Base):
status = Column(String(20), nullable=False, default="completed", index=True)
review_status = Column(String(20), nullable=False, default="pending_review", index=True)
generation_params = Column(Text, nullable=False, default="{}")
generated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
generated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=True)
video_fingerprint = Column(Text, nullable=True)
is_duplicate = Column(Boolean, nullable=False, default=False)
@@ -373,8 +373,8 @@ class TitleLibraryModel(Base):
usage_count = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, nullable=False, default=True, index=True)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class VoiceCloneProfileModel(Base):
@@ -396,8 +396,8 @@ class VoiceCloneProfileModel(Base):
retry_count = Column(Integer, nullable=False, default=0)
max_retries = Column(Integer, nullable=False, default=3)
metadata_ = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class VoiceLibraryModel(Base):
@@ -417,8 +417,8 @@ class VoiceLibraryModel(Base):
status = Column(String(20), nullable=False, default="completed", index=True)
tags = Column(JSON, nullable=False, default=list)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class DuplicationRecordModel(Base):
@@ -438,8 +438,8 @@ class DuplicationRecordModel(Base):
match_count = Column(Integer, nullable=True)
video_fingerprint = Column(Text, nullable=True)
error_message = Column(Text, nullable=False, default="")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class DuplicationSegmentModel(Base):
@@ -467,8 +467,8 @@ class RecipeModel(Base):
generation_params = Column(JSON, nullable=False, default=dict)
is_active = Column(Boolean, nullable=False, default=True)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class RecipeItemModel(Base):
@@ -496,8 +496,8 @@ class TemplateModel(Base):
bgm_config = Column(JSON, nullable=False, default=dict)
estimated_duration = Column(Float, nullable=False, default=0.0)
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class TemplateSegmentModel(Base):
@@ -509,8 +509,8 @@ class TemplateSegmentModel(Base):
duration_min = Column(Float, nullable=False)
duration_max = Column(Float, nullable=False)
material_type = Column(String(20), nullable=True) # 仅 voice_over 模式: 人物/场景
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class TemplateCategoryModel(Base):
@@ -520,7 +520,7 @@ class TemplateCategoryModel(Base):
user_id = Column(String(36), nullable=False, index=True)
name = Column(String(100), nullable=False)
sort_order = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class JobModel(Base):
@@ -544,8 +544,8 @@ class JobModel(Base):
created_by_user_id = Column(String(36), nullable=False, default="", index=True)
started_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class TTSJobModel(Base):
@@ -573,8 +573,8 @@ class TTSJobModel(Base):
metadata_ = Column("metadata", JSON, nullable=False, default=dict)
started_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class BillingRecordModel(Base):
@@ -591,7 +591,7 @@ class BillingRecordModel(Base):
payment_method = Column(String(50), nullable=True)
payment_id = Column(String(100), nullable=True) # 第三方支付流水号
invoice_url = Column(String(500), nullable=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
paid_at = Column(DateTime, nullable=True)
@@ -607,7 +607,7 @@ class VerificationCodeModel(Base):
expires_at = Column(DateTime, nullable=False)
used_at = Column(DateTime, nullable=True)
attempts = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class VideoShareModel(Base):
@@ -624,8 +624,8 @@ class VideoShareModel(Base):
view_count = Column(Integer, nullable=False, default=0)
download_count = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
class CoverTemplateModel(Base):
@@ -639,8 +639,8 @@ class CoverTemplateModel(Base):
thumbnail_url = Column(String(1000), nullable=False, default="")
is_system = Column(Boolean, nullable=False, default=False, index=True)
config = Column(JSON, nullable=False, default=dict)
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
class VideoFingerprintChunkModel(Base):
@@ -657,7 +657,7 @@ class VideoFingerprintChunkModel(Base):
phash_binary = Column(String(16), nullable=False)
color_histogram = Column(JSON, nullable=False)
frame_count = Column(Integer, nullable=False, default=1)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class ScriptModel(Base):
@@ -671,8 +671,8 @@ class ScriptModel(Base):
content = Column(Text, nullable=False, default="")
segments = Column(JSON, nullable=False, default=list)
tags = Column(JSON, nullable=False, default=list)
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
class LipsyncJobModel(Base):
@@ -714,8 +714,8 @@ class LipsyncJobModel(Base):
# 时间戳
submitted_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class AiAvatarRenderJob(Base):
@@ -746,8 +746,8 @@ class AiAvatarRenderJob(Base):
submitted_at = Column(DateTime, nullable=True)
started_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
class PointsAccountModel(Base):
@@ -760,8 +760,8 @@ class PointsAccountModel(Base):
balance = Column(Integer, nullable=False, default=0)
total_earned = Column(Integer, nullable=False, default=0)
total_spent = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class PointsTransactionModel(Base):
@@ -778,7 +778,7 @@ class PointsTransactionModel(Base):
balance_after = Column(Integer, nullable=False)
description = Column(String(255), nullable=False, default="")
ref_id = Column(String(100), nullable=False, default="")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class PointsOrderModel(Base):
@@ -798,7 +798,7 @@ class PointsOrderModel(Base):
payment_method = Column(String(50), nullable=True)
payment_id = Column(String(100), nullable=True)
paid_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
class DailyUsageRecordModel(Base):
@@ -812,4 +812,4 @@ class DailyUsageRecordModel(Base):
usage_date = Column(DateTime, nullable=False) # stored as DATE in SQL but DateTime for ORM compat
usage_type = Column(String(50), nullable=False, default="free_clip")
count = Column(Integer, nullable=False, default=0)
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from sqlalchemy.orm import Session
@@ -37,7 +37,7 @@ class SQLAlchemyPointsAccountRepository:
model.balance = account.balance
model.total_earned = account.total_earned
model.total_spent = account.total_spent
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
self.session.add(model)
self.session.commit()
return account
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from sqlalchemy.orm import Session
@@ -22,7 +22,7 @@ class SQLAlchemyRecipeRepository:
*,
skip: int = 0,
limit: int = 50,
) -> List[Recipe]:
) -> list[Recipe]:
models = (
self.session.query(RecipeModel)
.filter(
@@ -121,7 +121,7 @@ class SQLAlchemyRecipeRepository:
.count()
)
def list_items(self, recipe_id: str) -> List[RecipeItem]:
def list_items(self, recipe_id: str) -> list[RecipeItem]:
models = (
self.session.query(RecipeItemModel)
.filter(RecipeItemModel.recipe_id == recipe_id)
@@ -130,7 +130,7 @@ class SQLAlchemyRecipeRepository:
)
return [self._item_model_to_entity(m) for m in models]
def create_items(self, items: List[RecipeItem]) -> List[RecipeItem]:
def create_items(self, items: list[RecipeItem]) -> list[RecipeItem]:
for item in items:
model = RecipeItemModel(
id=item.id,
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from sqlalchemy.orm import Session
@@ -30,7 +30,7 @@ class SQLAlchemyTemplateClipConfigRepository:
clip_type: Optional[ClipType] = None,
skip: int = 0,
limit: int = 100,
) -> List[TemplateClipConfig]:
) -> list[TemplateClipConfig]:
"""按模板列出片段配置,按 order 排序"""
query = self.session.query(TemplateClipConfigModel).filter(
TemplateClipConfigModel.template_id == template_id,
@@ -8,7 +8,7 @@
from __future__ import annotations
import uuid
from typing import List, Optional
from typing import Optional
from sqlalchemy import or_
from sqlalchemy.orm import Session
@@ -56,7 +56,7 @@ class SQLAlchemyTemplateRepository:
keyword: Optional[str] = None,
mode: Optional[str] = None,
valid_only: bool = False,
) -> List[Template]:
) -> list[Template]:
query = self.session.query(TemplateModel).filter(
TemplateModel.user_id == user_id,
TemplateModel.is_active.is_(True),
@@ -252,7 +252,7 @@ class SQLAlchemyTemplateRepository:
created = self.create(new_template)
# 复用 create_segments 写入 template_clip_configs
new_segments: List[TemplateSegment] = []
new_segments: list[TemplateSegment] = []
for seg in source.segments:
new_segments.append(
TemplateSegment(
@@ -274,7 +274,7 @@ class SQLAlchemyTemplateRepository:
# ── Segments ──
def list_segments(self, template_id: str) -> List[TemplateSegment]:
def list_segments(self, template_id: str) -> list[TemplateSegment]:
"""优先从 template_clip_configs 读取,回退读 template_segments。"""
clips = (
self.session.query(TemplateClipConfigModel)
@@ -293,7 +293,7 @@ class SQLAlchemyTemplateRepository:
)
return [self._segment_model_to_entity(m) for m in old]
def create_segments(self, segments: List[TemplateSegment]) -> List[TemplateSegment]:
def create_segments(self, segments: list[TemplateSegment]) -> list[TemplateSegment]:
"""写入 template_clip_configs 表。material_type 存入 config JSON。"""
for seg in segments:
config = {"material_type": seg.material_type} if seg.material_type else {}
@@ -330,7 +330,7 @@ class SQLAlchemyTemplateRepository:
# ── Categories ──
def list_categories(self, user_id: str) -> List[TemplateCategory]:
def list_categories(self, user_id: str) -> list[TemplateCategory]:
models = (
self.session.query(TemplateCategoryModel)
.filter(TemplateCategoryModel.user_id == user_id)
@@ -380,7 +380,7 @@ class SQLAlchemyTemplateRepository:
# ── Tags ──
def list_tags(self, user_id: str) -> List[str]:
def list_tags(self, user_id: str) -> list[str]:
"""获取用户所有模板的标签(去重)。"""
models = (
self.session.query(TemplateModel)
@@ -2,8 +2,6 @@
from __future__ import annotations
from typing import List
from sqlalchemy.orm import Session
from packages.domain.template_version import EditTemplateVersion
@@ -51,7 +49,7 @@ class SQLAlchemyTemplateVersionRepository:
return None
return self._to_entity(model)
def list_by_template(self, template_id: str, limit: int = 50) -> List[EditTemplateVersion]:
def list_by_template(self, template_id: str, limit: int = 50) -> list[EditTemplateVersion]:
"""列出模板的所有历史版本(按版本号倒序)"""
from packages.adapters.sqlalchemy_impl.models import EditTemplateVersionModel
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from sqlalchemy.orm import Session
@@ -24,7 +24,7 @@ class SQLAlchemyTitleLibraryRepository:
is_active: bool = True,
skip: int = 0,
limit: int = 50,
) -> List[TitleLibraryItem]:
) -> list[TitleLibraryItem]:
query = self.session.query(TitleLibraryModel).filter(
TitleLibraryModel.user_id == user_id,
TitleLibraryModel.is_active == is_active,
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from sqlalchemy.orm import Session
@@ -98,7 +98,7 @@ class SQLAlchemyTTSJobRepository:
status: Optional[str] = None,
limit: int = 50,
offset: int = 0,
) -> List[TTSJob]:
) -> list[TTSJob]:
query = self.session.query(TTSJobModel).filter(
TTSJobModel.user_id == user_id,
TTSJobModel.status != "deleted",
@@ -125,7 +125,7 @@ class SQLAlchemyTTSJobRepository:
status: Optional[str] = None,
limit: int = 50,
offset: int = 0,
) -> List[TTSJob]:
) -> list[TTSJob]:
query = self.session.query(TTSJobModel).filter(
TTSJobModel.voice_clone_profile_id == voice_clone_profile_id,
TTSJobModel.status != "deleted",
@@ -4,7 +4,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Optional
from sqlalchemy.orm import Session
@@ -51,7 +51,7 @@ class SQLAlchemyVerificationCodeRepository(VerificationCodeRepository):
return self._to_entity(self.session.get(VerificationCodeModel, code_id))
def count_today(self, recipient: str, code_type: str) -> int:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
return (
self.session.query(VerificationCodeModel)
@@ -75,7 +75,7 @@ class SQLAlchemyVerificationCodeRepository(VerificationCodeRepository):
if dt is None:
return None
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt.replace(tzinfo=UTC)
return dt
return VerificationCode(
@@ -2,8 +2,8 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import List, Optional
from datetime import UTC, datetime
from typing import Optional
from sqlalchemy.orm import Session
@@ -69,7 +69,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
return None
return _model_to_domain(model)
def list_by_video(self, video_id: str, user_id: str) -> List[VideoShare]:
def list_by_video(self, video_id: str, user_id: str) -> list[VideoShare]:
models = (
self.session.query(VideoShareModel)
.filter(
@@ -81,7 +81,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
)
return [_model_to_domain(m) for m in models]
def list_by_user(self, user_id: str, skip: int = 0, limit: int = 20) -> List[VideoShare]:
def list_by_user(self, user_id: str, skip: int = 0, limit: int = 20) -> list[VideoShare]:
models = (
self.session.query(VideoShareModel)
.filter(VideoShareModel.user_id == user_id)
@@ -104,7 +104,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
model.is_active = share.is_active
model.view_count = share.view_count
model.download_count = share.download_count
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
self.session.add(model)
self.session.commit()
return share
@@ -121,7 +121,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
if model is None:
return False
model.is_active = False
model.updated_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(UTC)
self.session.add(model)
self.session.commit()
return True
@@ -130,7 +130,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
self.session.query(VideoShareModel).filter(VideoShareModel.id == share_id).update(
{
"view_count": VideoShareModel.view_count + 1,
"updated_at": datetime.now(timezone.utc),
"updated_at": datetime.now(UTC),
},
synchronize_session=False,
)
@@ -140,7 +140,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort):
self.session.query(VideoShareModel).filter(VideoShareModel.id == share_id).update(
{
"download_count": VideoShareModel.download_count + 1,
"updated_at": datetime.now(timezone.utc),
"updated_at": datetime.now(UTC),
},
synchronize_session=False,
)
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import Dict, List, Optional
from typing import Optional
from sqlalchemy.orm import Session
@@ -86,7 +86,7 @@ class SQLAlchemyVoiceCloneProfileRepository:
status: Optional[str] = None,
limit: int = 50,
offset: int = 0,
) -> List[VoiceCloneProfile]:
) -> list[VoiceCloneProfile]:
query = self.session.query(VoiceCloneProfileModel).filter(
VoiceCloneProfileModel.user_id == user_id,
VoiceCloneProfileModel.status != "deleted",
@@ -119,7 +119,7 @@ class SQLAlchemyVoiceCloneProfileRepository:
return None
return self._model_to_entity(model)
def find_profile_ids_by_voice_ids(self, voice_ids: List[str]) -> Dict[str, str]:
def find_profile_ids_by_voice_ids(self, voice_ids: list[str]) -> dict[str, str]:
"""批量查询 voice_id → profile_id 映射。用于填充统一列表的 voice_clone_profile_id。"""
if not voice_ids:
return {}
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from sqlalchemy.orm import Session
@@ -23,7 +23,7 @@ class SQLAlchemyVoiceLibraryRepository:
status: Optional[str] = None,
skip: int = 0,
limit: int = 50,
) -> List[VoiceLibraryItem]:
) -> list[VoiceLibraryItem]:
query = self.session.query(VoiceLibraryModel).filter(
VoiceLibraryModel.user_id == user_id,
)
@@ -5,7 +5,7 @@
from __future__ import annotations
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Optional
from packages.application.auth.verification_code_service import (
@@ -135,7 +135,7 @@ class BindContactUseCase:
# 5. 判断是否完成绑定
if user.phone_verified and user.email_verified and "@wechat.local" not in user.email:
user.binding_completed_at = datetime.now(timezone.utc)
user.binding_completed_at = datetime.now(UTC)
# 6. 保存
self.user_repo.save(user)
+4 -4
View File
@@ -12,7 +12,7 @@ JWT 处理器委托层
payload = jwt_handler.verify_access_token(token)
"""
from typing import Any, Dict, Optional
from typing import Any, Optional
from packages.application.auth.jwt_service import JWTConfig, JWTService
@@ -45,7 +45,7 @@ class JWTHandler:
self,
user_id: str,
role: str = "",
additional_claims: Optional[Dict[str, Any]] = None,
additional_claims: Optional[dict[str, Any]] = None,
) -> str:
"""
创建 access_token
@@ -64,7 +64,7 @@ class JWTHandler:
additional_claims=additional_claims,
)
def verify_access_token(self, token: str) -> Dict[str, Any]:
def verify_access_token(self, token: str) -> dict[str, Any]:
"""
验证 access_token
@@ -80,7 +80,7 @@ class JWTHandler:
"""
return self._service.verify_access_token(token)
def verify_token(self, token: str) -> Dict[str, Any]:
def verify_token(self, token: str) -> dict[str, Any]:
"""
验证任意 Token
+8 -8
View File
@@ -1,7 +1,7 @@
"""JWT Token 生成、验证、解析服务"""
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional
from datetime import UTC, datetime, timedelta
from typing import Any, Optional
import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
@@ -75,7 +75,7 @@ class JWTService(JWTServicePort):
self,
user_id: str,
role: str = "",
additional_claims: Optional[Dict[str, Any]] = None,
additional_claims: Optional[dict[str, Any]] = None,
) -> str:
"""
创建 access_token
@@ -88,7 +88,7 @@ class JWTService(JWTServicePort):
Returns:
JWT Token 字符串
"""
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
expire = now + timedelta(minutes=self.config.ACCESS_TOKEN_EXPIRE_MINUTES) # noqa: E501
payload = {
@@ -115,7 +115,7 @@ class JWTService(JWTServicePort):
Returns:
JWT Token 字符串
"""
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
expire = now + timedelta(days=self.config.REFRESH_TOKEN_EXPIRE_DAYS)
payload = {
@@ -128,7 +128,7 @@ class JWTService(JWTServicePort):
return jwt.encode(payload, self.config.SECRET_KEY, algorithm=self.config.ALGORITHM)
def verify_token(self, token: str) -> Dict[str, Any]:
def verify_token(self, token: str) -> dict[str, Any]:
"""
验证 Token
@@ -154,7 +154,7 @@ class JWTService(JWTServicePort):
except InvalidTokenError as e:
raise InvalidTokenError(f"Invalid token: {str(e)}") from e
def verify_access_token(self, token: str) -> Dict[str, Any]:
def verify_access_token(self, token: str) -> dict[str, Any]:
"""
验证 access_token
@@ -171,7 +171,7 @@ class JWTService(JWTServicePort):
return payload
def verify_refresh_token(self, token: str) -> Dict[str, Any]:
def verify_refresh_token(self, token: str) -> dict[str, Any]:
"""
验证 refresh_token
+4 -4
View File
@@ -4,7 +4,7 @@
import hashlib
import secrets
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Optional
import jwt as pyjwt
@@ -112,7 +112,7 @@ class LoginUseCase:
refresh_token = secrets.token_urlsafe(32)
# 5. 生成 JWT token
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
access_token_payload = {
"sub": user.id,
"sid": session_id,
@@ -135,7 +135,7 @@ class LoginUseCase:
)
# 6. 更新最后登录信息
user.last_login_at = datetime.now(timezone.utc)
user.last_login_at = datetime.now(UTC)
user.last_login_ip = request.ip_address
self.user_repository.save(user)
@@ -213,7 +213,7 @@ class RefreshTokenUseCase:
return None, "User not found"
# 4. 生成新的 access_token
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
access_token_payload = {
"sub": user.id,
"sid": session_id,
@@ -12,7 +12,7 @@
is_valid = password_handler.verify_password("my_secure_password", hashed)
"""
from typing import Optional, Tuple
from typing import Optional
from packages.application.auth.password_hasher import PasswordHasher, PasswordValidator
@@ -81,7 +81,7 @@ class PasswordHandler:
"""
return self._hasher.needs_rehash(hashed_password)
def validate_strength(self, password: str) -> Tuple[bool, Optional[str]]:
def validate_strength(self, password: str) -> tuple[bool, Optional[str]]:
"""
验证密码强度
@@ -4,7 +4,7 @@
import logging
import secrets
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Optional
from packages.adapters.smtp import get_email_service
@@ -71,7 +71,7 @@ class RequestPasswordResetUseCase:
# 设置令牌和过期时间
user.password_reset_token = reset_token
user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=self.token_expire_hours)
user.password_reset_expires_at = datetime.now(UTC) + timedelta(hours=self.token_expire_hours)
# 保存用户
self.user_repository.save(user)
@@ -141,8 +141,8 @@ class ResetPasswordUseCase:
if user.password_reset_expires_at:
expires_at = user.password_reset_expires_at
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
if datetime.now(timezone.utc) > expires_at:
expires_at = expires_at.replace(tzinfo=UTC)
if datetime.now(UTC) > expires_at:
return False, "Reset token has expired"
# 哈希新密码
@@ -4,7 +4,7 @@
import logging
import secrets
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Optional
from uuid import uuid4
@@ -120,7 +120,7 @@ class RegisterUserUseCase:
password_hash=hashed_password,
email_verified=False,
email_verification_token=verification_token,
created_at=datetime.now(timezone.utc),
created_at=datetime.now(UTC),
)
# 8. 保存用户
@@ -9,7 +9,7 @@ from __future__ import annotations
import logging
import re
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Optional
from packages.domain.verification_code import VerificationCode
@@ -162,7 +162,7 @@ class VerificationCodeService:
# 检查冷却时间
latest = self.repo.find_latest(recipient, code_type)
if latest:
elapsed = (datetime.now(timezone.utc) - latest.created_at).total_seconds()
elapsed = (datetime.now(UTC) - latest.created_at).total_seconds()
if elapsed < self.resend_cooldown:
wait = int(self.resend_cooldown - elapsed)
return False, wait
@@ -10,7 +10,7 @@
"""
import secrets
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Optional
from uuid import uuid4
@@ -158,7 +158,7 @@ class WechatSyncUseCase:
session_id = secrets.token_urlsafe(16)
refresh_token = secrets.token_urlsafe(32)
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
access_token_payload = {
"sub": user.id,
"sid": session_id,
+4 -4
View File
@@ -3,7 +3,7 @@
"""
from math import ceil
from typing import Generic, List, TypeVar
from typing import Generic, TypeVar
from pydantic import BaseModel, Field
@@ -59,13 +59,13 @@ class PaginationMeta(BaseModel):
class PaginatedResponse(BaseModel, Generic[T]):
"""分页响应"""
data: List[T] = Field(..., description="数据列表")
data: list[T] = Field(..., description="数据列表")
pagination: PaginationMeta = Field(..., description="分页信息")
@classmethod
def create(
cls,
data: List[T],
data: list[T],
params: PaginationParams,
total: int,
) -> "PaginatedResponse[T]":
@@ -77,7 +77,7 @@ class PaginatedResponse(BaseModel, Generic[T]):
def paginate(
items: List[T],
items: list[T],
params: PaginationParams,
) -> PaginatedResponse[T]:
"""
+2 -1
View File
@@ -14,8 +14,9 @@ from __future__ import annotations
import logging
import time
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Callable, Optional
from typing import Any, Optional
import httpx
@@ -16,8 +16,9 @@ celery 消息会丢失(transcode 队列 worker_prefetch_multiplier=1,消息
from __future__ import annotations
import logging
from datetime import datetime, timedelta, timezone
from typing import Any, Callable
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from typing import Any
logger = logging.getLogger(__name__)
@@ -39,7 +40,7 @@ _TERMINAL_ASSET_STATUSES = ("ready", "error", "deleted")
def _now() -> datetime:
return datetime.now(timezone.utc)
return datetime.now(UTC)
def cleanup_stale_ingest_jobs(
@@ -86,7 +87,7 @@ def cleanup_stale_ingest_jobs(
for job_model in stale_jobs:
ref_time = job_model.updated_at or job_model.created_at
if ref_time.tzinfo is None: # SQLite 读回 naive datetime 的防御
ref_time = ref_time.replace(tzinfo=timezone.utc)
ref_time = ref_time.replace(tzinfo=UTC)
stale_minutes = int((now - ref_time).total_seconds() // 60)
job_model.status = "failed"
job_model.error_message = (
+3 -3
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
@dataclass
@@ -21,7 +21,7 @@ class CreateRecipeCommand:
description: str = ""
template_id: str = ""
generation_params: dict = field(default_factory=dict)
items: List[RecipeItemCommand] = field(default_factory=list)
items: list[RecipeItemCommand] = field(default_factory=list)
metadata_: dict = field(default_factory=dict)
@@ -33,5 +33,5 @@ class UpdateRecipeCommand:
description: Optional[str] = None
template_id: Optional[str] = None
generation_params: Optional[dict] = None
items: Optional[List[RecipeItemCommand]] = None
items: Optional[list[RecipeItemCommand]] = None
metadata_: Optional[dict] = None
+4 -4
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import uuid
from dataclasses import dataclass
from typing import List, Optional
from typing import Optional
from packages.adapters.sqlalchemy_impl.recipe_repository import SQLAlchemyRecipeRepository
from packages.application.recipe.commands import (
@@ -75,7 +75,7 @@ class ListRecipesUseCase:
*,
skip: int = 0,
limit: int = 50,
) -> List[Recipe]:
) -> list[Recipe]:
return self.repository.list_by_user(user_id, skip=skip, limit=limit)
@@ -144,7 +144,7 @@ class UseRecipeResult:
"""使用配方的结果"""
recipe: Recipe
warnings: List[MissingAssetWarning]
warnings: list[MissingAssetWarning]
class UseRecipeUseCase:
@@ -173,7 +173,7 @@ class UseRecipeUseCase:
raise NotFoundError(f"Recipe {recipe_id} not found")
# 3. 校验引用的素材/标题/配音是否仍存在
warnings: List[MissingAssetWarning] = []
warnings: list[MissingAssetWarning] = []
# Note: 实际项目中这里需要注入 asset/title/voice repository
# 来校验每个 item 是否仍然存在。当前版本返回空警告列表,
# 由调用方(路由层)决定是否传入额外的校验逻辑。
+5 -5
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
@dataclass
@@ -20,12 +20,12 @@ class CreateTemplateCommand:
name: str
mode: str
category: str = ""
tags: List[str] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
title_config: dict = field(default_factory=dict)
subtitle_config: dict = field(default_factory=dict)
bgm_config: dict = field(default_factory=dict)
estimated_duration: float = 0.0
segments: List[SegmentCommand] = field(default_factory=list)
segments: list[SegmentCommand] = field(default_factory=list)
@dataclass
@@ -35,12 +35,12 @@ class UpdateTemplateCommand:
name: Optional[str] = None
mode: Optional[str] = None
category: Optional[str] = None
tags: Optional[List[str]] = None
tags: Optional[list[str]] = None
title_config: Optional[dict] = None
subtitle_config: Optional[dict] = None
bgm_config: Optional[dict] = None
estimated_duration: Optional[float] = None
segments: Optional[List[SegmentCommand]] = None
segments: Optional[list[SegmentCommand]] = None
@dataclass
+6 -6
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
from packages.application.template.commands import (
CopyTemplateCommand,
@@ -37,7 +37,7 @@ class ValidateResult:
"""模板校验结果."""
template: Template
warnings: List[GenerateWarning] = field(default_factory=list)
warnings: list[GenerateWarning] = field(default_factory=list)
# ── Template CRUD ──
@@ -95,7 +95,7 @@ class ListTemplatesUseCase:
skip: int = 0,
limit: int = 50,
filter: Optional[ListTemplatesFilter] = None,
) -> List[Template]:
) -> list[Template]:
if filter is None:
return self.repository.list_by_user(user_id, skip=skip, limit=limit)
return self.repository.list_by_user(
@@ -232,7 +232,7 @@ class ValidateTemplateUseCase:
if template is None:
raise NotFoundError(f"Template {command.template_id} not found")
warnings: List[GenerateWarning] = []
warnings: list[GenerateWarning] = []
# 业务规则 1: one_take 必须恰好 1 个片段
if template.mode == EditingMode.ONE_TAKE.value:
@@ -291,7 +291,7 @@ class ListCategoriesUseCase:
def __init__(self, repository: TemplateRepositoryPort) -> None:
self.repository = repository
def execute(self, user_id: str) -> List[TemplateCategory]:
def execute(self, user_id: str) -> list[TemplateCategory]:
return self.repository.list_categories(user_id)
@@ -310,7 +310,7 @@ class ListTagsUseCase:
def __init__(self, repository: TemplateRepositoryPort) -> None:
self.repository = repository
def execute(self, user_id: str) -> List[str]:
def execute(self, user_id: str) -> list[str]:
return self.repository.list_tags(user_id)
@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
@dataclass
@@ -13,7 +13,7 @@ class CreateTitleLibraryCommand:
text: str
category: str = "default"
description: str = ""
tags: List[str] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
metadata_: dict = field(default_factory=dict)
@@ -25,7 +25,7 @@ class UpdateTitleLibraryCommand:
text: Optional[str] = None
category: Optional[str] = None
description: Optional[str] = None
tags: Optional[List[str]] = None
tags: Optional[list[str]] = None
is_active: Optional[bool] = None
metadata_: Optional[dict] = None
@@ -41,4 +41,4 @@ class IncrementTitleUsageCommand:
class PickTitleCommand:
user_id: str
category: Optional[str] = None
exclude_ids: List[str] = field(default_factory=list)
exclude_ids: list[str] = field(default_factory=list)
@@ -3,7 +3,7 @@
from __future__ import annotations
import uuid
from typing import List, Optional
from typing import Optional
from packages.adapters.sqlalchemy_impl.title_library_repository import SQLAlchemyTitleLibraryRepository
from packages.application.title_library.commands import (
@@ -28,7 +28,7 @@ class ListTitleLibraryUseCase:
category: Optional[str] = None,
skip: int = 0,
limit: int = 50,
) -> List[TitleLibraryItem]:
) -> list[TitleLibraryItem]:
return self.repository.list_by_user(user_id, category=category, skip=skip, limit=limit)
+2 -2
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from packages.application.tts_job.exceptions import TTSJobNotFoundError
from packages.domain.tts_job import TTSJob
@@ -58,7 +58,7 @@ class ListTTSJobsUseCase:
status: Optional[str] = None,
skip: int = 0,
limit: int = 50,
) -> tuple[List[TTSJob], int]:
) -> tuple[list[TTSJob], int]:
items = self.repository.list_by_user(user_id, status=status, limit=limit, offset=skip)
total = self.repository.count_by_user(user_id, status=status)
return items, total
@@ -3,8 +3,8 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import List, Optional
from datetime import UTC, datetime
from typing import Optional
from packages.application.video_share.commands import (
CreateShareCommand,
@@ -143,7 +143,7 @@ class ListSharesByVideoUseCase:
def __init__(self, share_repository: VideoShareRepositoryPort) -> None:
self.share_repo = share_repository
def execute(self, video_id: str, user_id: str) -> List[VideoShare]:
def execute(self, video_id: str, user_id: str) -> list[VideoShare]:
return self.share_repo.list_by_video(video_id, user_id)
@@ -153,7 +153,7 @@ class ListSharesByUserUseCase:
def __init__(self, share_repository: VideoShareRepositoryPort) -> None:
self.share_repo = share_repository
def execute(self, user_id: str, skip: int = 0, limit: int = 20) -> tuple[List[VideoShare], int]:
def execute(self, user_id: str, skip: int = 0, limit: int = 20) -> tuple[list[VideoShare], int]:
items = self.share_repo.list_by_user(user_id, skip=skip, limit=limit)
total = self.share_repo.count_by_user(user_id)
return items, total
@@ -181,7 +181,7 @@ class UpdateShareUseCase:
# expires_at=None表示不修改
if command.expires_at is not None:
if command.expires_at < datetime.now(timezone.utc):
if command.expires_at < datetime.now(UTC):
raise ValueError("expires_at cannot be in the past")
share.expires_at = command.expires_at
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import List, Optional
from typing import Optional
from packages.domain.voice_clone_profile import VoiceCloneProfile
from packages.ports.voice_clone_profile_repository import VoiceCloneProfileRepository
@@ -66,7 +66,7 @@ class ListVoiceClonesUseCase:
status: Optional[str] = None,
skip: int = 0,
limit: int = 50,
) -> tuple[List[VoiceCloneProfile], int]:
) -> tuple[list[VoiceCloneProfile], int]:
items = self.repository.list_by_user(user_id, status=status, limit=limit, offset=skip)
total = self.repository.count_by_user(user_id, status=status)
return items, total
@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
@dataclass
@@ -19,7 +19,7 @@ class CreateVoiceLibraryCommand:
file_size: int = 0
status: str = "completed"
project_id: Optional[str] = None
tags: List[str] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
metadata_: dict = field(default_factory=dict)
@@ -36,5 +36,5 @@ class UpdateVoiceLibraryCommand:
duration: Optional[float] = None
file_size: Optional[int] = None
status: Optional[str] = None
tags: Optional[List[str]] = None
tags: Optional[list[str]] = None
metadata_: Optional[dict] = None
@@ -3,7 +3,7 @@
from __future__ import annotations
import uuid
from typing import List, Optional
from typing import Optional
from packages.adapters.sqlalchemy_impl.voice_library_repository import SQLAlchemyVoiceLibraryRepository
from packages.application.voice_library.commands import (
@@ -26,7 +26,7 @@ class ListVoiceLibraryUseCase:
status: Optional[str] = None,
skip: int = 0,
limit: int = 50,
) -> tuple[List[VoiceLibraryItem], int]:
) -> tuple[list[VoiceLibraryItem], int]:
"""返回 (items, total_count),避免调用方再单独查一次 count。"""
items = self.repository.list_by_user(user_id, status=status, skip=skip, limit=limit)
total = (
+2 -2
View File
@@ -6,7 +6,7 @@ API 和 Worker 各自的 Settings 类继承本类,只追加服务特有字段
"""
import os
from typing import Optional, Type, TypeVar
from typing import Optional, TypeVar
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -105,7 +105,7 @@ def _get_env_file() -> str:
return env_file if os.path.exists(env_file) else ".env"
def get_cached_settings(settings_class: Type[T], cache_key: Optional[str] = None) -> T:
def get_cached_settings(settings_class: type[T], cache_key: Optional[str] = None) -> T:
"""统一的 Settings 单例获取函数。
所有服务都通过这个函数获取配置,消除重复的单例实现。
+3 -3
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional
from typing import Optional
@dataclass
@@ -34,8 +34,8 @@ class EmailServicePort(ABC):
subject: str,
html_body: str,
text_body: Optional[str] = None,
cc: Optional[List[str]] = None,
bcc: Optional[List[str]] = None,
cc: Optional[list[str]] = None,
bcc: Optional[list[str]] = None,
) -> tuple[bool, Optional[str]]:
"""发送邮件"""
...
+5 -5
View File
@@ -7,7 +7,7 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Dict
from typing import Any
class JWTServicePort(ABC):
@@ -18,7 +18,7 @@ class JWTServicePort(ABC):
self,
user_id: str,
role: str = "",
additional_claims: Dict[str, Any] | None = None,
additional_claims: dict[str, Any] | None = None,
) -> str:
"""创建 access_token"""
...
@@ -29,16 +29,16 @@ class JWTServicePort(ABC):
...
@abstractmethod
def verify_token(self, token: str) -> Dict[str, Any]:
def verify_token(self, token: str) -> dict[str, Any]:
"""验证任意 Token"""
...
@abstractmethod
def verify_access_token(self, token: str) -> Dict[str, Any]:
def verify_access_token(self, token: str) -> dict[str, Any]:
"""验证 access_token"""
...
@abstractmethod
def verify_refresh_token(self, token: str) -> Dict[str, Any]:
def verify_refresh_token(self, token: str) -> dict[str, Any]:
"""验证 refresh_token"""
...
+2 -2
View File
@@ -7,7 +7,7 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional, Tuple
from typing import Optional
class PasswordHasherPort(ABC):
@@ -33,6 +33,6 @@ class PasswordValidatorPort(ABC):
"""密码强度验证端口(抽象接口)"""
@abstractmethod
def validate(self, password: str) -> Tuple[bool, Optional[str]]:
def validate(self, password: str) -> tuple[bool, Optional[str]]:
"""验证密码强度"""
...
+3 -3
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
if sys.version_info >= (3, 11):
from enum import StrEnum
@@ -80,8 +80,8 @@ class ClassificationJob:
classification: str = "" # Result: AssetClassification value
confidence: float = 0.0 # 0.0 - 1.0
error_message: str = ""
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
+4 -4
View File
@@ -1,7 +1,7 @@
"""封面模板领域实体。"""
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from uuid import uuid4
@@ -16,8 +16,8 @@ class CoverTemplate:
thumbnail_url: str
is_system: bool
config: dict[str, Any]
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create_system(
@@ -75,4 +75,4 @@ class CoverTemplate:
self.config = config
if thumbnail_url is not None:
self.thumbnail_url = thumbnail_url
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
+2 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date, datetime, timezone
from datetime import UTC, date, datetime
from uuid import uuid4
@@ -12,7 +12,7 @@ class DailyUsageRecord:
usage_date: date
usage_type: str = "free_clip"
count: int = 0
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
+7 -7
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from uuid import uuid4
@@ -69,8 +69,8 @@ class DuplicationRecord:
video_fingerprint: dict[str, Any] | None = None
error_message: str = ""
segments: list[DuplicateSegment] = field(default_factory=list)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
@@ -99,7 +99,7 @@ class DuplicationRecord:
def mark_processing(self) -> None:
self.status = "processing"
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def mark_completed(
self,
@@ -118,12 +118,12 @@ class DuplicationRecord:
self.segments = segments
self.visual_similarity = visual_similarity
self.match_count = match_count
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def mark_failed(self, error_message: str) -> None:
self.status = "failed"
self.error_message = error_message
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def can_retry(self) -> bool:
"""
@@ -151,4 +151,4 @@ class DuplicationRecord:
self.error_message = ""
self.segments = []
self.video_fingerprint = None
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
+9 -9
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
if sys.version_info >= (3, 11):
from enum import StrEnum
@@ -68,8 +68,8 @@ class EditPlan:
project_id: str = ""
created_by_user_id: str = ""
config: dict[str, Any] = field(default_factory=dict)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
@@ -106,39 +106,39 @@ class EditPlan:
if self.status != EditPlanStatus.DRAFT:
raise ValueError(f"只有 draft 状态的计划可以开始编辑,当前状态: {self.status}")
self.status = EditPlanStatus.EDITING
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def start_rendering(self) -> None:
"""开始渲染"""
if self.status != EditPlanStatus.EDITING:
raise ValueError(f"只有 editing 状态的计划可以开始渲染,当前状态: {self.status}")
self.status = EditPlanStatus.RENDERING
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def mark_completed(self) -> None:
"""标记为完成"""
if self.status != EditPlanStatus.RENDERING:
raise ValueError(f"只有 rendering 状态的计划可以标记完成,当前状态: {self.status}")
self.status = EditPlanStatus.COMPLETED
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def mark_failed(self) -> None:
"""标记为失败"""
if self.status != EditPlanStatus.RENDERING:
raise ValueError(f"只有 rendering 状态的计划可以标记失败,当前状态: {self.status}")
self.status = EditPlanStatus.FAILED
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def resume_editing(self) -> None:
"""重新进入编辑状态(完成/失败后重新编辑)"""
if self.status not in (EditPlanStatus.COMPLETED, EditPlanStatus.FAILED):
raise ValueError(f"只有 completed/failed 状态的计划可以重新编辑,当前状态: {self.status}")
self.status = EditPlanStatus.EDITING
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def reset_to_draft(self) -> None:
"""重置为草稿状态(仅从 failed 状态可重置)"""
if self.status != EditPlanStatus.FAILED:
raise ValueError(f"只有 failed 状态的计划可以重置,当前状态: {self.status}")
self.status = EditPlanStatus.DRAFT
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
+7 -7
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
if sys.version_info >= (3, 11):
from enum import StrEnum
@@ -73,8 +73,8 @@ class EditPlanClip:
playback_speed: float = 1.0 # 0 或 1.0 表示原速,范围 0.25~4.0
status: EditPlanClipStatus = EditPlanClipStatus.PENDING
config: dict[str, Any] = field(default_factory=dict)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
@@ -139,28 +139,28 @@ class EditPlanClip:
self.asset_id = asset_id.strip()
if start_time is not None and start_time >= 0:
self.start_time = start_time
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def mark_ready(self) -> None:
"""标记为就绪"""
if self.status != EditPlanClipStatus.PENDING:
raise ValueError(f"只有 pending 状态的片段可以标记就绪,当前状态: {self.status}")
self.status = EditPlanClipStatus.READY
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def mark_rendered(self) -> None:
"""标记为已渲染"""
if self.status != EditPlanClipStatus.READY:
raise ValueError(f"只有 ready 状态的片段可以标记已渲染,当前状态: {self.status}")
self.status = EditPlanClipStatus.RENDERED
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def mark_failed(self) -> None:
"""标记为失败"""
if self.status != EditPlanClipStatus.READY:
raise ValueError(f"只有 ready 状态的片段可以标记失败,当前状态: {self.status}")
self.status = EditPlanClipStatus.FAILED
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
@property
def end_time(self) -> float:
+6 -6
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
if sys.version_info >= (3, 11):
from enum import StrEnum
@@ -52,8 +52,8 @@ class EditTemplate:
sort_weight: int = 0
status: EditTemplateStatus = EditTemplateStatus.ACTIVE
version: int = 1
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
@@ -94,12 +94,12 @@ class EditTemplate:
def activate(self) -> None:
"""激活模板"""
self.status = EditTemplateStatus.ACTIVE
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def deactivate(self) -> None:
"""停用模板"""
self.status = EditTemplateStatus.INACTIVE
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
@property
def is_active(self) -> bool:
@@ -109,4 +109,4 @@ class EditTemplate:
def bump_version(self) -> None:
"""版本号+1,发布时调用"""
self.version += 1
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
+11 -11
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
if sys.version_info >= (3, 11):
from enum import StrEnum
@@ -60,7 +60,7 @@ class User:
# 资料是否已完善(微信新用户首次设置昵称后置 True;邮箱注册默认 True)
profile_completed: bool = True
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@dataclass(slots=True)
@@ -71,7 +71,7 @@ class Project:
description: str = ""
shared_users: list[str] = field(default_factory=list) # 被共享的用户 ID 列表
is_default: bool = False # 是否为用户的默认项目(小程序自动创建),DB 部分唯一索引保证每人至多一个
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(cls, owner_user_id: str, name: str, description: str = "", is_default: bool = False) -> "Project":
@@ -108,8 +108,8 @@ class AssetLibrary:
kind: AssetLibraryKind
asset_count: int = 0
total_size: int = 0
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
@@ -181,8 +181,8 @@ class Asset:
client_upload_id: str = ""
metadata: dict[str, Any] = field(default_factory=dict)
tag_ids: list[str] = field(default_factory=list)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@property
def file_type(self) -> str:
@@ -253,14 +253,14 @@ class Asset:
raise ValueError("标签 ID 不能为空")
if clean_id not in self.tag_ids:
self.tag_ids.append(clean_id)
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def remove_tag(self, tag_id: str) -> None:
"""删除标签 ID。如果标签不存在,不报错(幂等性)。"""
clean_id = tag_id.strip()
if clean_id in self.tag_ids:
self.tag_ids.remove(clean_id)
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
@dataclass(slots=True)
@@ -275,8 +275,8 @@ class IngestJob:
file_hash: str = ""
asset_id: str = ""
celery_task_id: str = ""
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
+4 -4
View File
@@ -7,7 +7,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Optional
@dataclass(frozen=True)
@@ -18,7 +18,7 @@ class FilterPreset:
name: str
category: str # 分类:basic / cinematic / vintage / bw / style
description: str = ""
tags: List[str] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
# FFmpeg eq 滤镜参数(基准值,实际应用时乘以强度系数)
brightness: float = 0.0 # -1.0 ~ 1.0
contrast: float = 1.0 # 0.0 ~ 2.0,1.0为原值
@@ -34,7 +34,7 @@ class FilterPreset:
# ── 预设库清单 ────────────────────────────────────────────────────────────────
FILTER_PRESET_LIBRARY: List[FilterPreset] = [
FILTER_PRESET_LIBRARY: list[FilterPreset] = [
# ── 基础 basic ─────────────────────────────────────────────────────
FilterPreset(
id="filter_none",
@@ -281,7 +281,7 @@ def list_filter_presets(
*,
category: Optional[str] = None,
keyword: Optional[str] = None,
) -> List[FilterPreset]:
) -> list[FilterPreset]:
"""筛选滤镜预设列表
Args:
+3 -3
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from uuid import uuid4
@@ -29,8 +29,8 @@ class GeneratedVideo:
duplicate_rate: float | None = None
match_count: int | None = None
visual_similarity: float | None = None
generated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
generated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
+10 -10
View File
@@ -11,7 +11,7 @@ from __future__ import annotations
import json
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
if sys.version_info >= (3, 11):
from enum import StrEnum
@@ -124,8 +124,8 @@ class GenerationTask:
title_config: dict = field(default_factory=dict)
extra_meta: dict = field(default_factory=dict)
logs: str = "[]"
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
@@ -245,7 +245,7 @@ class GenerationTask:
ValueError: 当前状态不允许转换到 running
"""
self.transition_to(GenerationTaskStatus.RUNNING)
self.started_at = datetime.now(timezone.utc)
self.started_at = datetime.now(UTC)
self.error_message = ""
def mark_completed(self, result_count: int = 1) -> None:
@@ -260,7 +260,7 @@ class GenerationTask:
ValueError: 当前状态不允许转换到 completed
"""
self.transition_to(GenerationTaskStatus.COMPLETED)
self.completed_at = datetime.now(timezone.utc)
self.completed_at = datetime.now(UTC)
self.progress = 100.0
self.result_count = result_count
self.error_message = ""
@@ -279,14 +279,14 @@ class GenerationTask:
"""
self.transition_to(GenerationTaskStatus.FAILED)
self.error_message = error_message
self.completed_at = datetime.now(timezone.utc)
self.completed_at = datetime.now(UTC)
if error_info is not None:
self.error_info = error_info
else:
self.error_info = {
"error_type": "UnknownError",
"message": error_message,
"failed_at": datetime.now(timezone.utc).isoformat(),
"failed_at": datetime.now(UTC).isoformat(),
}
def mark_cancelled(self) -> None:
@@ -298,7 +298,7 @@ class GenerationTask:
ValueError: 当前状态不允许转换到 cancelled
"""
self.transition_to(GenerationTaskStatus.CANCELLED)
self.completed_at = datetime.now(timezone.utc)
self.completed_at = datetime.now(UTC)
def mark_confirmed(
self,
@@ -325,7 +325,7 @@ class GenerationTask:
self.title_config = dict(title_config)
if extra_meta:
self.extra_meta.update(extra_meta)
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
# ── 日志辅助 ────────────────────────────────────────────────────────────
@@ -345,7 +345,7 @@ class GenerationTask:
except (json.JSONDecodeError, TypeError):
entries = []
entry = {
"ts": datetime.now(timezone.utc).isoformat(),
"ts": datetime.now(UTC).isoformat(),
"level": level,
"stage": stage,
"message": message,
+5 -5
View File
@@ -12,7 +12,7 @@ from __future__ import annotations
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
if sys.version_info >= (3, 11):
from enum import StrEnum
@@ -110,8 +110,8 @@ class Job:
created_by_user_id: str = ""
started_at: datetime | None = None
completed_at: datetime | None = None
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@classmethod
def create(
@@ -192,7 +192,7 @@ class Job:
f"允许: {{{', '.join(s.value for s in allowed)}}}"
)
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
self.status = new_status
self.updated_at = now
@@ -242,7 +242,7 @@ class Job:
self.progress = progress
if stage:
self.current_stage = stage
self.updated_at = datetime.now(timezone.utc)
self.updated_at = datetime.now(UTC)
def prepare_retry(self) -> None:
"""准备重试:重置状态为 pending。

Some files were not shown because too many files have changed in this diff Show More