fix(#1834): 批量修复 UP 系列静态分析警告(UP007/UP006/UP017/UP035) #1928
@@ -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")
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
"""标签列表"""
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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,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,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 是否仍然存在。当前版本返回空警告列表,
|
||||
# 由调用方(路由层)决定是否传入额外的校验逻辑。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 = (
|
||||
|
||||
@@ -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 单例获取函数。
|
||||
|
||||
所有服务都通过这个函数获取配置,消除重复的单例实现。
|
||||
|
||||
@@ -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]]:
|
||||
"""发送邮件"""
|
||||
...
|
||||
|
||||
@@ -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"""
|
||||
...
|
||||
|
||||
@@ -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]]:
|
||||
"""验证密码强度"""
|
||||
...
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user