Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/app/api/v1/routes/film/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class TaskStatusRead(BaseModel):
started_at_ts: float | None = Field(None, description="任务开始执行时间戳")
finished_at_ts: float | None = Field(None, description="任务结束时间戳")
elapsed_ms: int | None = Field(None, description="任务累计执行耗时(毫秒)")
error: str = Field("", description="失败时的错误信息(成功/进行中通常为空)")


class TaskListItemRead(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions backend/app/api/v1/routes/film/task_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ async def get_task_status(
started_at_ts=view.started_at_ts,
finished_at_ts=view.finished_at_ts,
elapsed_ms=view.elapsed_ms,
error=view.error or "",
)
)

Expand Down
41 changes: 40 additions & 1 deletion backend/app/api/v1/routes/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
from app.schemas.common import ApiResponse, PaginatedData, created_response, empty_response, success_response
from app.schemas.llm import (
ImageGenerationOptionsRead,
ModelChatTestRead,
ModelChatTestRequest,
ModelCreate,
ModelRead,
ModelSettingsRead,
ModelSettingsUpdate,
ModelUpdate,
ModelVerifyRead,
ProviderCreate,
ProviderRead,
ProviderSupportedRead,
Expand All @@ -38,14 +41,17 @@
update_model_settings as update_model_settings_service,
update_provider as update_provider_service,
)
from app.services.llm.model_chat_test import chat_test_with_model as chat_test_with_model_service
from app.services.llm.model_verify import verify_model_config as verify_model_config_service

router = APIRouter()

# 列表排序允许的字段(避免注入)
PROVIDER_ORDER_FIELDS = {"name", "created_at", "updated_at"}
MODEL_ORDER_FIELDS = {"name", "category", "created_at", "updated_at"}
DEFAULT_PAGE_SIZE = 10
MAX_PAGE_SIZE = 100
# 管理端列表可能一次拉较多供应商/模型(如筛选项、统计);略高于通用列表上限 100。
MAX_PAGE_SIZE = 200


# ---------- Provider ----------
Expand Down Expand Up @@ -211,6 +217,39 @@ async def create_model(
return created_response(ModelRead.model_validate(model))


@router.post(
"/models/{model_id}/verify",
response_model=ApiResponse[ModelVerifyRead],
summary="验证模型配置(同步探测,不创建生成任务)",
)
async def verify_llm_model(
model_id: str,
db: AsyncSession = Depends(get_db),
) -> ApiResponse[ModelVerifyRead]:
"""对已保存模型做一次短超时探测:文本走极小对话;图/视频走列表接口鉴权与模型名匹配。"""
data = await verify_model_config_service(db, model_id=model_id)
return success_response(data)


@router.post(
"/models/{model_id}/chat-test",
response_model=ApiResponse[ModelChatTestRead],
summary="文本模型试聊(管理页调试发消息)",
)
async def chat_test_llm_model(
model_id: str,
body: ModelChatTestRequest,
db: AsyncSession = Depends(get_db),
) -> ApiResponse[ModelChatTestRead]:
"""对已保存的文本模型发送一条用户消息并返回模型回复(不计入业务任务)。"""
data = await chat_test_with_model_service(
db,
model_id=model_id,
user_message=body.message,
)
return success_response(data)


@router.get(
"/models/{model_id}",
response_model=ApiResponse[ModelRead],
Expand Down
117 changes: 116 additions & 1 deletion backend/app/api/v1/routes/studio/chapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from fastapi import APIRouter, Depends, Query, status
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

Expand All @@ -21,13 +21,128 @@
patch_model,
require_entity,
)
from app.api.v1.routes.film.common import TaskCreated, _CreateOnlyTask
from app.core.task_manager import DeliveryMode, SqlAlchemyTaskStore, TaskManager
from app.models.task_links import GenerationTaskLink
from app.schemas.studio.chapter_timeline import (
ChapterTimelineExportRequest,
ChapterTimelineRead,
ChapterTimelineWrite,
)
from app.schemas.studio.projects import ChapterCreate, ChapterRead, ChapterUpdate
from app.services.studio.chapter_timeline import (
TimelineLayoutConflictError,
build_timeline_read,
replace_timeline_segments,
)
from app.services.studio.chapter_timeline_export import (
EXPORT_RELATION_TYPE,
EXPORT_RESOURCE_TYPE,
ensure_timeline_exportable,
find_active_chapter_timeline_export_task_id,
)
from app.tasks.execute_task import enqueue_task_execution

router = APIRouter()

CHAPTER_ORDER_FIELDS = {"index", "title", "created_at", "updated_at", "storyboard_count", "status"}


@router.get(
"/{chapter_id}/timeline",
response_model=ApiResponse[ChapterTimelineRead],
summary="获取章节剪辑时间线(含镜头成片解析状态)",
)
async def get_chapter_timeline(
chapter_id: str,
db: AsyncSession = Depends(get_db),
) -> ApiResponse[ChapterTimelineRead]:
await get_or_404(db, Chapter, chapter_id, detail=entity_not_found("Chapter"))
data = await build_timeline_read(db, chapter_id)
return success_response(data)


@router.put(
"/{chapter_id}/timeline",
response_model=ApiResponse[ChapterTimelineRead],
summary="全量保存章节剪辑时间线片段顺序",
)
async def put_chapter_timeline(
chapter_id: str,
body: ChapterTimelineWrite,
db: AsyncSession = Depends(get_db),
) -> ApiResponse[ChapterTimelineRead]:
await get_or_404(db, Chapter, chapter_id, detail=entity_not_found("Chapter"))
try:
data = await replace_timeline_segments(db, chapter_id, body)
except TimelineLayoutConflictError as exc:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={
"message": "layout_version conflict",
"server_layout_version": exc.server_version,
"client_layout_version": exc.client_version,
},
) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
return success_response(data)


@router.post(
"/{chapter_id}/timeline/export",
response_model=ApiResponse[TaskCreated],
status_code=status.HTTP_201_CREATED,
summary="发起章节时间线拼接导出任务",
)
async def post_chapter_timeline_export(
chapter_id: str,
body: ChapterTimelineExportRequest | None = Body(default=None),
db: AsyncSession = Depends(get_db),
) -> ApiResponse[TaskCreated]:
await get_or_404(db, Chapter, chapter_id, detail=entity_not_found("Chapter"))
read = await build_timeline_read(db, chapter_id)
try:
ensure_timeline_exportable(read)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc

active = await find_active_chapter_timeline_export_task_id(db, chapter_id)
if active:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={
"message": "章节时间线导出任务进行中",
"task_id": active,
},
)

eff = body or ChapterTimelineExportRequest()
store = SqlAlchemyTaskStore(db)
tm = TaskManager(store=store, strategies={})
task_record = await tm.create(
task=_CreateOnlyTask(),
mode=DeliveryMode.async_polling,
task_kind="chapter_timeline_export",
run_args={
"chapter_id": chapter_id,
"encode_mode": eff.encode_mode.value,
"idempotency_key": eff.idempotency_key,
},
)
db.add(
GenerationTaskLink(
task_id=task_record.id,
resource_type=EXPORT_RESOURCE_TYPE,
relation_type=EXPORT_RELATION_TYPE,
relation_entity_id=chapter_id,
),
)
await db.commit()
enqueue_task_execution(task_record.id)
return created_response(TaskCreated(task_id=task_record.id))


@router.get(
"",
response_model=ApiResponse[PaginatedData[ChapterRead]],
Expand Down
19 changes: 19 additions & 0 deletions backend/app/api/v1/routes/studio/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
ProjectUpdate,
StyleOption,
)
from app.schemas.studio.timeline import TimelineClipRead

router = APIRouter()

Expand Down Expand Up @@ -124,6 +125,24 @@ async def create_project(
return created_response(ProjectRead.model_validate(obj))


@router.get(
"/{project_id}/timeline",
response_model=ApiResponse[list[TimelineClipRead]],
summary="项目时间线片段列表",
)
async def list_project_timeline(
project_id: str,
db: AsyncSession = Depends(get_db),
) -> ApiResponse[list[TimelineClipRead]]:
"""返回项目关联的时间线片段。

说明:`timeline_clips` 表当前无 `project_id` 字段,无法在数据库层按项目过滤;
在引入归属字段或关联表之前,对已存在的项目返回空列表(接口可用,不再 404)。
"""
await get_or_404(db, Project, project_id, detail=entity_not_found("Project"))
return success_response([])


@router.get(
"/{project_id}",
response_model=ApiResponse[ProjectRead],
Expand Down
2 changes: 1 addition & 1 deletion backend/app/core/contracts/image_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class ImageGenerationResult(BaseModel):
model_config = ConfigDict(extra="ignore")

images: list[ImageItem] = Field(..., description="图片列表")
provider: ProviderKey = Field(..., description="供应商标识:openai | volcengine")
provider: ProviderKey = Field(..., description="供应商标识:openai | volcengine | aliyun_bailian")
provider_task_id: Optional[str] = Field(
None,
description="供应商内部任务 ID(若存在),用于调试/追踪",
Expand Down
2 changes: 1 addition & 1 deletion backend/app/core/contracts/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dataclasses import dataclass
from typing import Literal

ProviderKey = Literal["openai", "volcengine"]
ProviderKey = Literal["openai", "volcengine", "aliyun_bailian"]


@dataclass(frozen=True, slots=True)
Expand Down
8 changes: 8 additions & 0 deletions backend/app/core/integrations/aliyun/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""阿里云相关出站适配(DashScope 等)。"""

from __future__ import annotations

from app.core.integrations.aliyun.dashscope_images import DashScopeImageApiAdapter
from app.core.integrations.aliyun.dashscope_videos import DashScopeVideoApiAdapter

__all__ = ["DashScopeImageApiAdapter", "DashScopeVideoApiAdapter"]
Loading