feat: initialize aivideo project

This commit is contained in:
2026-04-17 18:33:05 +08:00
commit 14b18d67fe
162 changed files with 26251 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
from datetime import datetime
from sqlalchemy import and_, or_, select
from sqlalchemy.orm import Session
from app.models.entities import PricingRule, ProviderModel, VideoModel, VideoModelSupplierBinding
class VideoModelsRepository:
def __init__(self, db: Session) -> None:
self.db = db
def list_video_models(self):
return self.db.query(VideoModel).order_by(VideoModel.sort_order.asc(), VideoModel.id.asc())
def get_video_model(self, model_id: int) -> VideoModel | None:
return self.db.scalar(select(VideoModel).where(VideoModel.id == model_id))
def list_bindings(self):
return (
self.db.query(VideoModelSupplierBinding)
.order_by(
VideoModelSupplierBinding.video_model_id.asc(),
VideoModelSupplierBinding.routing_priority.asc(),
)
)
def get_binding(self, binding_id: int) -> VideoModelSupplierBinding | None:
return self.db.scalar(
select(VideoModelSupplierBinding).where(VideoModelSupplierBinding.id == binding_id)
)
def active_pricing_rule(self, video_model_id: int) -> PricingRule | None:
now = datetime.utcnow()
return self.db.scalar(
select(PricingRule)
.where(
PricingRule.video_model_id == video_model_id,
PricingRule.status == 1,
PricingRule.effective_at <= now,
or_(PricingRule.expired_at.is_(None), PricingRule.expired_at > now),
)
.order_by(PricingRule.version_no.desc(), PricingRule.id.desc())
)
def provider_models(self) -> dict[int, ProviderModel]:
rows = self.db.scalars(select(ProviderModel)).all()
return {row.id: row for row in rows}

View File

@@ -0,0 +1,71 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.common.db.session import get_db
from app.common.responses.api_response import success_response
from app.common.security.deps import require_admin_permission
from app.modules.video_models.schema import BindingPayload, VideoModelPayload
from app.modules.video_models.service import VideoModelsService
router = APIRouter(tags=["video-models"])
@router.get("/api/v1/video-models")
def list_public_video_models(db: Session = Depends(get_db)):
return success_response(VideoModelsService(db).list_public_models())
@router.get("/api/v1/admin/video-models")
def list_admin_video_models(
_=Depends(require_admin_permission()),
db: Session = Depends(get_db),
):
return success_response(VideoModelsService(db).list_admin_models())
@router.post("/api/v1/admin/video-models")
def create_video_model(
payload: VideoModelPayload,
_=Depends(require_admin_permission()),
db: Session = Depends(get_db),
):
return success_response(VideoModelsService(db).create_model(payload))
@router.put("/api/v1/admin/video-models/{model_id}")
def update_video_model(
model_id: int,
payload: VideoModelPayload,
_=Depends(require_admin_permission()),
db: Session = Depends(get_db),
):
return success_response(VideoModelsService(db).update_model(model_id, payload))
@router.get("/api/v1/admin/video-model-bindings")
def list_bindings(
_=Depends(require_admin_permission()),
db: Session = Depends(get_db),
):
return success_response(VideoModelsService(db).list_bindings())
@router.post("/api/v1/admin/video-model-bindings")
def create_binding(
payload: BindingPayload,
_=Depends(require_admin_permission()),
db: Session = Depends(get_db),
):
return success_response(VideoModelsService(db).create_binding(payload))
@router.put("/api/v1/admin/video-model-bindings/{binding_id}")
def update_binding(
binding_id: int,
payload: BindingPayload,
_=Depends(require_admin_permission()),
db: Session = Depends(get_db),
):
return success_response(VideoModelsService(db).update_binding(binding_id, payload))

View File

@@ -0,0 +1,23 @@
from pydantic import BaseModel
class VideoModelPayload(BaseModel):
model_key: str
model_name: str
frontend_title: str
frontend_description: str = ""
default_duration_seconds: int = 8
default_ratio: str = "16:9"
default_resolution: str = "1280x720"
status: int = 1
sort_order: int = 0
class BindingPayload(BaseModel):
video_model_id: int
provider_model_id: int
routing_priority: int = 10
is_primary: bool = False
status: int = 1
timeout_seconds_override: int | None = None

View File

@@ -0,0 +1,113 @@
from sqlalchemy.orm import Session
from app.common.errors.app_error import NotFoundAppError
from app.models.entities import ProviderModel, VideoModel, VideoModelSupplierBinding
from app.modules.video_models.repository import VideoModelsRepository
class VideoModelsService:
def __init__(self, db: Session) -> None:
self.db = db
self.repository = VideoModelsRepository(db)
def list_public_models(self) -> list[dict]:
items = []
for item in self.repository.list_video_models().filter(VideoModel.status == 1).all():
pricing = self.repository.active_pricing_rule(item.id)
items.append(self.serialize_model(item, pricing))
return items
def list_admin_models(self) -> list[dict]:
return [
self.serialize_model(item, self.repository.active_pricing_rule(item.id))
for item in self.repository.list_video_models().all()
]
def create_model(self, payload) -> dict:
item = VideoModel(**payload.model_dump())
self.db.add(item)
self.db.commit()
self.db.refresh(item)
return self.serialize_model(item, None)
def update_model(self, model_id: int, payload) -> dict:
item = self.repository.get_video_model(model_id)
if not item:
raise NotFoundAppError("video model not found", code=50001)
for key, value in payload.model_dump().items():
setattr(item, key, value)
self.db.commit()
return self.serialize_model(item, self.repository.active_pricing_rule(item.id))
def list_bindings(self) -> list[dict]:
provider_models = self.repository.provider_models()
video_models = {
item.id: item for item in self.repository.list_video_models().all()
}
return [
self.serialize_binding(item, provider_models, video_models)
for item in self.repository.list_bindings().all()
]
def create_binding(self, payload) -> dict:
item = VideoModelSupplierBinding(**payload.model_dump())
self.db.add(item)
self.db.commit()
self.db.refresh(item)
return self._serialize_binding_single(item)
def update_binding(self, binding_id: int, payload) -> dict:
item = self.repository.get_binding(binding_id)
if not item:
raise NotFoundAppError("binding not found", code=60003)
for key, value in payload.model_dump().items():
setattr(item, key, value)
self.db.commit()
return self._serialize_binding_single(item)
@staticmethod
def serialize_model(item: VideoModel, pricing) -> dict:
return {
"id": item.id,
"modelKey": item.model_key,
"modelName": item.model_name,
"frontendTitle": item.frontend_title,
"frontendDescription": item.frontend_description,
"defaultDurationSeconds": item.default_duration_seconds,
"defaultRatio": item.default_ratio,
"defaultResolution": item.default_resolution,
"status": item.status,
"sortOrder": item.sort_order,
"pricing": {
"pointsPerSecond": pricing.points_per_second if pricing else 0,
"minimumPoints": pricing.minimum_points if pricing else 0,
},
}
@staticmethod
def serialize_binding(
item: VideoModelSupplierBinding,
provider_models: dict[int, ProviderModel],
video_models: dict[int, VideoModel],
) -> dict:
provider_model = provider_models.get(item.provider_model_id)
video_model = video_models.get(item.video_model_id)
return {
"id": item.id,
"videoModelId": item.video_model_id,
"videoModelName": video_model.model_name if video_model else "",
"providerModelId": item.provider_model_id,
"providerModelName": provider_model.model_name if provider_model else "",
"routingPriority": item.routing_priority,
"isPrimary": item.is_primary,
"status": item.status,
"timeoutSecondsOverride": item.timeout_seconds_override,
}
def _serialize_binding_single(self, item: VideoModelSupplierBinding) -> dict:
provider_models = self.repository.provider_models()
video_models = {
row.id: row for row in self.repository.list_video_models().all()
}
return self.serialize_binding(item, provider_models, video_models)