拆镜复刻开发完成

This commit is contained in:
2026-06-11 17:54:40 +08:00
parent a6b6e5f822
commit 74266a126c
34 changed files with 7105 additions and 470 deletions
+3
View File
@@ -23,6 +23,8 @@ from app.models.user_resource_month_stat import UserResourceMonthStat
from app.models.user_resource_total_stat import UserResourceTotalStat
from app.models.module_generation_project import ModuleGenerationProject
from app.models.module_generation_step import ModuleGenerationStep
from app.models.shot_replicate_task_set import ShotReplicateTaskSet
from app.models.shot_replicate_segment import ShotReplicateSegment
from app.models.user_oauth import UserOAuth
from app.models.user_oauth_account import UserOAuthAccount
from app.models.user_oauth_app import UserOAuthApp
@@ -37,5 +39,6 @@ __all__ = [
"ChatGenerationTask", "ChatGenerationTaskEvent", "ChatProviderCallLog",
"GeneratedResource", "UserResourceMonthStat", "UserResourceTotalStat",
"ModuleGenerationProject", "ModuleGenerationStep",
"ShotReplicateTaskSet", "ShotReplicateSegment",
"UserOAuth", "UserOAuthAccount", "UserOAuthApp",
]
+13 -8
View File
@@ -4,6 +4,7 @@ import os
from sqlalchemy import DateTime, func
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.pool import NullPool
from app.config import settings
@@ -18,14 +19,18 @@ engine_kwargs = {
"pool_pre_ping": True,
}
# SQLite 本地调试时不要乱塞 pool_size/max_overflowPostgreSQL/asyncpg 才建议配置
# SQLite 本地调试时不要乱塞 pool_size/max_overflowPostgreSQL/asyncpg 才建议配置
# 默认保持 Celery 连接池复用;只有显式开启 CELERY_DB_USE_NULLPOOL=true 时才降级 NullPool。
if _is_celery_process() and not settings.DATABASE_URL.startswith("sqlite"):
engine_kwargs.update(
pool_size=settings.CELERY_DB_POOL_SIZE,
max_overflow=settings.CELERY_DB_MAX_OVERFLOW,
pool_timeout=settings.CELERY_DB_POOL_TIMEOUT,
pool_recycle=settings.CELERY_DB_POOL_RECYCLE,
)
if bool(getattr(settings, "CELERY_DB_USE_NULLPOOL", False)):
engine_kwargs.update(poolclass=NullPool)
else:
engine_kwargs.update(
pool_size=settings.CELERY_DB_POOL_SIZE,
max_overflow=settings.CELERY_DB_MAX_OVERFLOW,
pool_timeout=settings.CELERY_DB_POOL_TIMEOUT,
pool_recycle=settings.CELERY_DB_POOL_RECYCLE,
)
engine = create_async_engine(settings.DATABASE_URL, **engine_kwargs)
@@ -63,4 +68,4 @@ async def init_database() -> None:
async def close_database() -> None:
await engine.dispose()
await engine.dispose()
@@ -0,0 +1,80 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, JSON, String, Text, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, SoftDeleteMixin, TimestampMixin
_JSON_TYPE = JSON().with_variant(JSONB, "postgresql")
class ShotReplicateSegment(Base, TimestampMixin, SoftDeleteMixin):
"""拆镜复刻片段表。"""
__tablename__ = "shot_replicate_segments"
__table_args__ = (
Index("idx_shot_replicate_segments_task_set", "task_set_id", "segment_index"),
Index("idx_shot_replicate_segments_user_created", "user_id", "created_at"),
Index("idx_shot_replicate_segments_source_mode", "source_mode"),
Index("idx_shot_replicate_segments_split_status", "split_status"),
Index("idx_shot_replicate_segments_analysis_status", "analysis_status"),
Index("idx_shot_replicate_segments_replicate_status", "replicate_status"),
Index("idx_shot_replicate_segments_project", "module_project_id"),
Index(
"uq_shot_replicate_segments_task_set_index_active",
"task_set_id",
"segment_index",
unique=True,
postgresql_where=text("deleted_at IS NULL"),
),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True)
task_set_id: Mapped[str] = mapped_column(
String(32), ForeignKey("shot_replicate_task_sets.id", ondelete="CASCADE"), index=True, nullable=False
)
user_id: Mapped[str] = mapped_column(
String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False
)
segment_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
source_mode: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
start_second: Mapped[float] = mapped_column(Float, nullable=False)
end_second: Mapped[float] = mapped_column(Float, nullable=False)
duration_seconds: Mapped[float] = mapped_column(Float, nullable=False)
time_node: Mapped[str] = mapped_column(String(64), nullable=False)
split_status: Mapped[str] = mapped_column(String(32), default="pending", index=True, nullable=False)
analysis_status: Mapped[str] = mapped_column(String(32), default="pending", index=True, nullable=False)
replicate_status: Mapped[str] = mapped_column(String(32), default="not_started", index=True, nullable=False)
segment_video_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
segment_video_path: Mapped[str | None] = mapped_column(String(512), nullable=True)
original_video_content: Mapped[str | None] = mapped_column(Text, nullable=True)
original_video_category: Mapped[str | None] = mapped_column(String(160), nullable=True)
original_video_audience: Mapped[str | None] = mapped_column(Text, nullable=True)
segment_content: Mapped[str | None] = mapped_column(Text, nullable=True)
segment_category: Mapped[str | None] = mapped_column(String(160), nullable=True)
segment_audience: Mapped[str | None] = mapped_column(Text, nullable=True)
analysis_json: Mapped[dict[str, Any] | list[Any] | None] = mapped_column(_JSON_TYPE, nullable=True)
ai_suggestion_json: Mapped[dict[str, Any] | list[Any] | None] = mapped_column(_JSON_TYPE, nullable=True)
module_project_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
split_celery_task_id: Mapped[str | None] = mapped_column(String(160), nullable=True, index=True)
split_enqueued_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
split_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
split_lease_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
split_next_retry_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
split_retry_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
split_last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
split_completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
analysis_error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -0,0 +1,60 @@
from __future__ import annotations
from typing import Any
from sqlalchemy import Float, ForeignKey, Index, Integer, JSON, String, Text, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, SoftDeleteMixin, TimestampMixin
_JSON_TYPE = JSON().with_variant(JSONB, "postgresql")
class ShotReplicateTaskSet(Base, TimestampMixin, SoftDeleteMixin):
"""拆镜复刻总任务集。"""
__tablename__ = "shot_replicate_task_sets"
__table_args__ = (
Index("idx_shot_replicate_task_sets_user_created", "user_id", "created_at"),
Index("idx_shot_replicate_task_sets_status", "status"),
Index("idx_shot_replicate_task_sets_analysis_status", "analysis_status"),
Index("idx_shot_replicate_task_sets_split_status", "split_status"),
Index(
"uq_shot_replicate_task_sets_user_idempotency",
"user_id",
"idempotency_key",
unique=True,
postgresql_where=text("deleted_at IS NULL AND idempotency_key IS NOT NULL"),
),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True)
user_id: Mapped[str] = mapped_column(
String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False
)
title: Mapped[str | None] = mapped_column(String(160), nullable=True)
video_url: Mapped[str] = mapped_column(String(512), nullable=False)
video_path: Mapped[str] = mapped_column(String(512), nullable=False)
video_duration_seconds: Mapped[float] = mapped_column(Float, nullable=False)
status: Mapped[str] = mapped_column(String(32), default="pending_analysis", index=True, nullable=False)
analysis_status: Mapped[str] = mapped_column(String(32), default="pending", index=True, nullable=False)
split_status: Mapped[str] = mapped_column(String(32), default="none", index=True, nullable=False)
original_video_content: Mapped[str | None] = mapped_column(Text, nullable=True)
original_video_category: Mapped[str | None] = mapped_column(String(160), nullable=True)
original_video_audience: Mapped[str | None] = mapped_column(Text, nullable=True)
ai_suggestion_json: Mapped[list[Any] | dict[str, Any] | None] = mapped_column(_JSON_TYPE, nullable=True)
analysis_raw_json: Mapped[dict[str, Any] | list[Any] | None] = mapped_column(_JSON_TYPE, nullable=True)
analysis_result_json: Mapped[dict[str, Any] | list[Any] | None] = mapped_column(_JSON_TYPE, nullable=True)
segment_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
completed_segment_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
failed_segment_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
analysis_error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
split_error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
idempotency_key: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)