真人素材库本地完成

This commit is contained in:
2026-07-06 15:13:41 +08:00
parent 75d1f7a428
commit ab67bdf19e
52 changed files with 4131 additions and 137 deletions
+3
View File
@@ -32,6 +32,7 @@ from app.models.user_oauth_account import UserOAuthAccount
from app.models.user_oauth_app import UserOAuthApp
from app.models.home_material import HomeMaterialAsset, HomeMaterialCategory, HomeMaterialWatermark
from app.models.contact_request import ContactRequest
from app.models.private_portrait import PrivatePortraitProject, PrivatePortraitValidateSession, PrivatePortraitAssetGroup, PrivatePortraitAsset
__all__ = [
"Base", "TimestampMixin", "SoftDeleteMixin", "engine", "async_session",
@@ -47,4 +48,6 @@ __all__ = [
"ShotReplicateTaskSet", "ShotReplicateSegment",
"UserOAuth", "UserOAuthAccount", "UserOAuthApp",
"HomeMaterialAsset", "HomeMaterialCategory", "HomeMaterialWatermark",
"PrivatePortraitProject", "PrivatePortraitValidateSession",
"PrivatePortraitAssetGroup", "PrivatePortraitAsset",
]
@@ -0,0 +1,11 @@
from app.models.private_portrait.project import PrivatePortraitProject
from app.models.private_portrait.validate_session import PrivatePortraitValidateSession
from app.models.private_portrait.asset_group import PrivatePortraitAssetGroup
from app.models.private_portrait.asset import PrivatePortraitAsset
__all__ = [
"PrivatePortraitProject",
"PrivatePortraitValidateSession",
"PrivatePortraitAssetGroup",
"PrivatePortraitAsset",
]
@@ -0,0 +1,69 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.enums.private_portrait import (
PrivatePortraitAssetStatus,
PrivatePortraitAssetType,
PrivatePortraitRemoteDeleteStatus,
)
from app.models.base import Base, SoftDeleteMixin, TimestampMixin
class PrivatePortraitAsset(Base, TimestampMixin, SoftDeleteMixin):
"""火山 Asset 本地映射。生成时只信任本地 id,再批量解析成 asset://remote_asset_id。"""
__tablename__ = "private_portrait_assets"
__table_args__ = (
Index("uq_private_portrait_assets_remote_asset_id", "remote_asset_id", unique=True),
Index("idx_private_portrait_assets_user_status_created", "user_id", "status", "created_at"),
Index("idx_private_portrait_assets_project_status_created", "project_id", "status", "created_at"),
Index("idx_private_portrait_assets_group_status_created", "group_id", "status", "created_at"),
Index("idx_private_portrait_assets_remote_project_name", "remote_project_name"),
Index(
"idx_private_portrait_assets_next_poll_status",
"next_poll_at",
"status",
postgresql_where=text("deleted_at IS NULL AND next_poll_at IS NOT NULL"),
),
Index("idx_private_portrait_assets_remote_delete_status", "remote_delete_status"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True)
user_id: Mapped[str] = mapped_column(String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
project_id: Mapped[str] = mapped_column(String(32), ForeignKey("private_portrait_projects.id", ondelete="CASCADE"), nullable=False, index=True)
group_id: Mapped[str] = mapped_column(String(32), ForeignKey("private_portrait_asset_groups.id", ondelete="CASCADE"), nullable=False, index=True)
remote_group_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
remote_asset_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
remote_project_name: Mapped[str] = mapped_column(String(256), nullable=False, index=True)
asset_type: Mapped[str] = mapped_column(String(16), nullable=False, default=PrivatePortraitAssetType.IMAGE.value, index=True)
name: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
source_url: Mapped[str] = mapped_column(Text, nullable=False)
preview_url: Mapped[str | None] = mapped_column(Text, nullable=True)
remote_url: Mapped[str | None] = mapped_column(Text, nullable=True)
remote_url_expired_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
status: Mapped[str] = mapped_column(
String(32),
nullable=False,
default=PrivatePortraitAssetStatus.CREATING.value,
server_default=PrivatePortraitAssetStatus.CREATING.value,
index=True,
)
moderation_json: Mapped[str | None] = mapped_column(Text, nullable=True)
last_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
next_poll_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
poll_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
remote_delete_status: Mapped[str] = mapped_column(
String(32),
nullable=False,
default=PrivatePortraitRemoteDeleteStatus.NONE.value,
server_default=PrivatePortraitRemoteDeleteStatus.NONE.value,
index=True,
)
remote_deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
remote_delete_error: Mapped[str | None] = mapped_column(Text, nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
raw_response_json: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -0,0 +1,51 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.enums.private_portrait import (
PRIVATE_PORTRAIT_GROUP_TYPE,
PrivatePortraitAssetGroupStatus,
PrivatePortraitRemoteDeleteStatus,
)
from app.models.base import Base, SoftDeleteMixin, TimestampMixin
class PrivatePortraitAssetGroup(Base, TimestampMixin, SoftDeleteMixin):
"""本地项目组与火山 Asset Group 的映射。"""
__tablename__ = "private_portrait_asset_groups"
__table_args__ = (
Index("uq_private_portrait_asset_groups_remote_group_id", "remote_group_id", unique=True),
Index("idx_private_portrait_asset_groups_user_project", "user_id", "project_id"),
Index("idx_private_portrait_asset_groups_project_status", "project_id", "status"),
Index("idx_private_portrait_asset_groups_remote_delete_status", "remote_delete_status"),
Index("idx_private_portrait_asset_groups_remote_project_name", "remote_project_name"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True)
user_id: Mapped[str] = mapped_column(String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
project_id: Mapped[str] = mapped_column(String(32), ForeignKey("private_portrait_projects.id", ondelete="CASCADE"), nullable=False, index=True)
remote_group_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
remote_group_name: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
remote_project_name: Mapped[str] = mapped_column(String(256), nullable=False, index=True)
group_type: Mapped[str] = mapped_column(String(32), nullable=False, default=PRIVATE_PORTRAIT_GROUP_TYPE, server_default=PRIVATE_PORTRAIT_GROUP_TYPE)
status: Mapped[str] = mapped_column(
String(32),
nullable=False,
default=PrivatePortraitAssetGroupStatus.ACTIVE.value,
server_default=PrivatePortraitAssetGroupStatus.ACTIVE.value,
index=True,
)
remote_delete_status: Mapped[str] = mapped_column(
String(32),
nullable=False,
default=PrivatePortraitRemoteDeleteStatus.NONE.value,
server_default=PrivatePortraitRemoteDeleteStatus.NONE.value,
index=True,
)
remote_deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
remote_delete_error: Mapped[str | None] = mapped_column(Text, nullable=True)
raw_response_json: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -0,0 +1,43 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from app.enums.private_portrait import PrivatePortraitProjectStatus
from app.models.base import Base, SoftDeleteMixin, TimestampMixin
class PrivatePortraitProject(Base, TimestampMixin, SoftDeleteMixin):
"""用户本地真人素材项目组。remote_project_name 是火山 ProjectName 快照。"""
__tablename__ = "private_portrait_projects"
__table_args__ = (
Index("idx_private_portrait_projects_user_status_created", "user_id", "status", "created_at"),
Index(
"idx_private_portrait_projects_user_deleted",
"user_id",
"deleted_at",
postgresql_where=text("deleted_at IS NULL"),
),
Index("idx_private_portrait_projects_remote_project_name", "remote_project_name"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True)
user_id: Mapped[str] = mapped_column(String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(128), nullable=False, index=True, comment="用户展示项目名")
name_slug: Mapped[str] = mapped_column(String(128), nullable=False, index=True, comment="项目名安全 slug")
remote_project_name: Mapped[str] = mapped_column(String(256), nullable=False, index=True, comment="火山 ProjectName 快照")
description: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[str] = mapped_column(
String(32),
nullable=False,
default=PrivatePortraitProjectStatus.ACTIVE.value,
server_default=PrivatePortraitProjectStatus.ACTIVE.value,
index=True,
)
asset_group_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
active_asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
@@ -0,0 +1,43 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.enums.private_portrait import PrivatePortraitValidateSessionStatus
from app.models.base import Base, TimestampMixin
class PrivatePortraitValidateSession(Base, TimestampMixin):
"""火山真人认证 H5 会话。"""
__tablename__ = "private_portrait_validate_sessions"
__table_args__ = (
Index("idx_private_portrait_validate_sessions_user_project", "user_id", "project_id"),
Index("idx_private_portrait_validate_sessions_byted_token", "byted_token"),
Index("idx_private_portrait_validate_sessions_status_created", "status", "created_at"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True)
user_id: Mapped[str] = mapped_column(String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
project_id: Mapped[str] = mapped_column(String(32), ForeignKey("private_portrait_projects.id", ondelete="CASCADE"), nullable=False, index=True)
byted_token: Mapped[str | None] = mapped_column(String(256), nullable=True, index=True)
h5_link: Mapped[str | None] = mapped_column(Text, nullable=True)
callback_url: Mapped[str | None] = mapped_column(Text, nullable=True)
result_code: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
algorithm_base_resp_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
verify_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
status: Mapped[str] = mapped_column(
String(32),
nullable=False,
default=PrivatePortraitValidateSessionStatus.CREATED.value,
server_default=PrivatePortraitValidateSessionStatus.CREATED.value,
index=True,
)
remote_group_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
remote_project_name: Mapped[str] = mapped_column(String(256), nullable=False, index=True)
expired_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
raw_callback_json: Mapped[str | None] = mapped_column(Text, nullable=True)
raw_response_json: Mapped[str | None] = mapped_column(Text, nullable=True)
+6 -1
View File
@@ -1,6 +1,6 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Float, String, JSON
from sqlalchemy import Boolean, DateTime, Float, Integer, String, JSON
from sqlalchemy.orm import Mapped, mapped_column
from app.enums.user import FrontendUserKind
@@ -39,6 +39,11 @@ class User(Base, TimestampMixin):
)
allowed_menus: Mapped[list | None] = mapped_column(JSON, nullable=True)
# 真人素材库图片总量限制。0 表示关闭真人素材模块;>0 表示启用并限制用户所有真人素材图片总量。
private_portrait_image_limit: Mapped[int] = mapped_column(
Integer, default=5, server_default="5", nullable=False
)
@property
def must_set_password(self) -> bool:
return self.user_type == "frontend" and not self.hashed_password