Skip to content
Merged
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
129 changes: 129 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# 更新日志 / Changelog

本文件记录 KunFlix 各版本的变更。版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。

版本号单一来源:`backend/config.py` 的 `Settings.VERSION`,并与 `frontend/package.json`、`backend/admin/package.json` 保持同步。

---

## [v0.1.1] - 2026-08-01

**主题:MiniMax-H3 视频模型接入 + AI 助手交互体验重做**

上一版本:`v0.1.0`。本版本无数据库迁移,无新增第三方依赖。

### ✨ 新增

#### MiniMax-H3(Hailuo-03)视频生成模型

在既有 MiniMax 供应商框架上接入 MiniMax 视频 v2 接口,与 v1 Hailuo 系列双代次并存、互不影响。

- **接口**:`POST /v2/video_generation` 提交、`GET /v2/query/video_generation/{task_id}` 查询、`DELETE /v2/video_generation/{task_id}` 取消/删除;v2 查询直接返回 `content.url`,不再需要 v1 的 `file_id` → `/v1/files/retrieve` 二次换取。
- **三种生成场景**(由 `video_mode` 与已绑定素材自动推导,遵循接口的互斥约束):
- 文生视频 `t2va`
- 图生视频 `i2va`:支持首帧、尾帧、首尾帧组合
- 参考生视频 `r2va`:参考图 ≤ 9、参考视频 ≤ 3、参考音频 ≤ 3
- **参数能力**:2K 输出、时长 4–15 秒整数、7 种宽高比;宽高比按场景归一(文生不接受 `adaptive` 时兜底 `16:9`,图生强制 `adaptive`,参考生默认 `adaptive`)。
- **提交前预校验**(避免无效上游请求):提示词必填、图生缺图、参考音频单独输入、参考素材数量截断、单文件体积(图 30MB / 视频 50MB / 音频 15MB)与请求体 64MB 上限。
- **错误可读化**:解析 OpenAI 风格错误体,`1000/1002/1004/1008/1026/2013` 等内部错误码转为中文提示(如余额不足、触发限流、命中敏感内容)。
- **任务清理**:删除本地视频任务时 best-effort 调用上游取消/删除(`queued` → cancel,终态 → delete),失败不阻断本地删除。
- **Endpoint 覆盖**:v2 路径支持 `LLMProvider.base_url`(自动剥离 `/v1`、`/v2` 版本后缀),便于切换国内/海外域名。
- **回调**:`callback_url` 按现有架构不下发,统一走 arq 后台轮询 + 前端轮询。

前端为能力驱动,模型能力表声明后参数控件(模式 / 时长 / 分辨率 / 宽高比 / 首尾帧 / 参考素材槽位)自动适配,无需逐模型改 UI。

#### AI 助手小球 Orbie

- 新增 `AiOrb`:基于 `@paper-design/shaders-react` Warp shader 的果冻质感动态头像,含眨眼、左右张望、开心瞪眼 + 腮红、周期蹦跳,拖拽时冒汗 / 眼睛变 `><` / 弹出气泡。
- 新增 `DraggableOrb`:以 AI 面板为空间边界构建"房间"物理感——四壁与输入区"地板"硬阻挡、撞墙按方向压扁回弹、松手弹回原位、回弹静默期屏蔽误点击,并支持气泡点击。
- AI 助手折叠态入口由静态图标按钮替换为小球,支持拖拽橡皮筋弹回,拖动过程中不会误触发展开。

### 🔧 改进

- **AI 助手欢迎页重构**:由四宫格预设按钮改为小球 + 轮播对话气泡(展示 8–12 秒、留白 3–5 秒、随机不重复推进),欢迎词支持 `{{name}}` 昵称插值。
- **预设提示词改为"注入输入框"语义**:点击气泡不再直接发送,而是把完整文案填入输入框并聚焦,供用户编辑后再发送(`MessageInput` 新增 `injectedPrompt` + nonce 机制,支持同一文案重复注入)。
- **四条预设提示词升级为专业结构化模板**(科幻爱情剧本 / 角色人物设计 / 分镜脚本 / 文案润色),中英双语文案同步。
- **剧场活跃度排序**:对话发送消息时触达 `Theater.updated_at`(限定归属当前用户,防止越权触达他人剧场),首页"最近剧场"按最后活跃时间排序。
- **剧场列表缓存新鲜窗口** 60 秒 → 6 秒,缩短活跃度变化的可见延迟。
- **版本号纳入运行时**:`FastAPI` 的 `title`/`version` 改由 `settings` 提供,`GET /` 返回 `version` 字段,便于部署后核对版本。

### 🐛 修复

- **视频分辨率提交 422**:请求 schema 的 `quality` 字面量缺少能力表已声明的 `512p`(Hailuo-02)与 `4k`(Veo 3.1),选中即被校验拦截;本次补齐并新增 `2k`。
- **AI 会话初始化重复建会话**:并发 effect 竞态与 React StrictMode 双调用会重复创建会话,新增按 `theaterId` 的防重入标记。
- **新建剧场排在列表末尾**:`updated_at` 为 NULL 时被 `nullslast` 推到最后,改用 `coalesce(updated_at, created_at)` 排序。
- **视频提示词长度限制过紧**:上限由 2000 提升至 7000(对齐 MiniMax-H3 单条 text 上限),前端提交侧同步按 7000 截断,避免超长直接 422。
- 修正视频模型能力配置的 TypedDict 声明:拆出可选能力键,消除 Seedance 等条目原有的类型不符。

### 🔢 版本管理同步

| 位置 | v0.1.0 | v0.1.1 |
|---|---|---|
| `backend/config.py` → `Settings.VERSION` | `1.0.0`(陈旧且未被使用) | `0.1.1`(单一来源,被 FastAPI 与 `GET /` 消费) |
| `frontend/package.json` | `0.1.0` | `0.1.1` |
| `backend/admin/package.json` | `0.1.0` | `0.1.1` |
| 两侧 `package-lock.json` | `0.1.0` | `0.1.1` |

### ✅ 测试与验证

- 新增 `backend/tests/services/test_minimax_h3_video.py`:63 项,覆盖场景推导、`content[]` 组装、宽高比/时长规则、素材数量截断、四类参数校验、体积守卫、状态与错误映射,并以假 HTTP 客户端断言真实请求的 URL / 方法 / 请求体(含 v1 端点回归守卫、`base_url` 覆盖、工厂层不再二次取 URL)。
- 后端全量测试:**190 passed**。
- `frontend` `tsc --noEmit` 无错误;`backend/admin` 仅剩一处与本次无关的既有测试文件类型告警。

### 📌 升级说明

1. **无需执行数据库迁移**。
2. 使用 MiniMax-H3 需在管理后台的 MiniMax 供应商下新增模型 `MiniMax-H3`,并设置 `model_metadata.model_type = "video"`;随后在定价表按该模型配置计费维度。
3. 2K 输出目前复用 `video_output_720p`(每输出秒)计费维度,与既有 1080p / 4K 的处理约定一致,未新增计费维度。
4. H3 返回的 `usage.input_seconds` / `input_image_count`(参考视频按秒、参考图计数)暂未纳入本地积分计算,如需精确对账需另行扩展计费维度。
5. 本地素材以 base64 内联上传,参考视频接近 50MB 时会触碰 64MB 请求体上限,此时返回中文提示而非上游报错;彻底解决需接入 MiniMax 文件上传以换取 `mm_file://` 引用。

### 📁 主要变更文件

**后端**

```
backend/config.py 版本号单一来源
backend/main.py FastAPI title/version、GET / 返回版本
backend/schemas.py quality 字面量、prompt 上限、字段注释
backend/routers/videos.py 轮询透传 model、删除时清理上游任务
backend/routers/chats.py 发消息触达 Theater.updated_at
backend/services/theater.py 剧场列表 coalesce 排序
backend/services/video_generation.py poll 参数映射表、cancel_video_task 入口
backend/services/video_providers/base.py 适配器基类新增 delete_task
backend/services/video_providers/minimax_provider.py MiniMax-H3 (v2) 完整实现
backend/services/video_providers/model_capabilities.py MiniMax-H3 能力声明、TypedDict 拆分
backend/services/video_providers/__init__.py 模块说明
backend/tasks_queue/tasks.py 后台轮询透传 model
backend/tests/services/test_minimax_h3_video.py 新增(63 项)
backend/admin/src/types/video.ts 2K 与宽高比标签
```

**前端**

```
frontend/src/components/canvas/AiOrb.tsx 新增
frontend/src/components/canvas/DraggableOrb.tsx 新增
frontend/src/components/canvas/AIAssistantPanel.tsx 小球入口、提示词注入、拖拽边界
frontend/src/components/ai-assistant/WelcomeMessage.tsx 欢迎页重构
frontend/src/components/ai-assistant/MessageInput.tsx injectedPrompt 注入
frontend/src/components/ai-assistant/hooks/useSessionManager.ts 初始化防重入
frontend/src/components/canvas/VideoGeneratePanel.tsx 提示词长度对齐
frontend/src/components/canvas/VideoGeneratePanel/constants.ts VIDEO_PROMPT_MAX
frontend/src/hooks/useVideoGeneration.ts 2K 分辨率标签
frontend/src/i18n/locales/{zh-CN,en-US}.json 欢迎词与提示词模板
frontend/src/lib/theaterListCache.ts 新鲜窗口 6 秒
```

**文档**

```
minimax-H3视频模型开发指南.md MiniMax 视频 v2 官方接口文档(参考资料)
CHANGELOG.md 本文件
```

---

## [v0.1.0]

首个标记版本,详见 GitHub 上的 `v0.1.0` 标签与提交历史。
4 changes: 2 additions & 2 deletions backend/admin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions backend/admin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "admin-dashboard",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"scripts": {
"dev": "next dev -p 3888",
Expand Down Expand Up @@ -64,7 +64,7 @@
"@typescript-eslint/eslint-plugin": "^8.56.0",
"@typescript-eslint/parser": "^8.56.0",
"@vitejs/plugin-react": "^6.0.1",
"@xmldom/xmldom": "0.9.10",
"@xmldom/xmldom": "0.9.10",
"autoprefixer": "^10.4.24",
"eslint": "^10.3.0",
"eslint-config-next": "^16.2.4",
Expand Down
5 changes: 5 additions & 0 deletions backend/admin/src/types/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const RESOLUTION_LABELS: Record<string, string> = {
'720p': '720p (高清)',
'768p': '768p',
'1080p': '1080p (全高清)',
'2k': '2K',
'4k': '4K (超高清)',
};

Expand All @@ -37,6 +38,10 @@ export const ASPECT_RATIO_LABELS: Record<string, string> = {
'16:9': '16:9 (横屏)',
'9:16': '9:16 (竖屏)',
'1:1': '1:1 (方形)',
'4:3': '4:3',
'3:4': '3:4',
'21:9': '21:9 (宽银幕)',
adaptive: '自适应',
};

// 默认能力配置(当模型未定义时使用)
Expand Down
2 changes: 1 addition & 1 deletion backend/admin/tsconfig.tsbuildinfo

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

class Settings(BaseSettings):
PROJECT_NAME: str = "KunFlix"
VERSION: str = "1.0.0"
# 后端版本号 —— 全项目版本的单一来源,与 frontend/package.json、
# backend/admin/package.json 及 CHANGELOG.md 保持同步
VERSION: str = "0.1.1"

# ---------------------------------------------------------------
# Database
Expand Down
4 changes: 2 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def _detect_proxy_linux() -> str | None:
from ratelimit import install_rate_limit # noqa: E402


app = FastAPI(title="KunFlix", lifespan=lifespan)
app = FastAPI(title=settings.PROJECT_NAME, version=settings.VERSION, lifespan=lifespan)
install_rate_limit(app)


Expand Down Expand Up @@ -306,7 +306,7 @@ async def _fallback_exception_handler(request: Request, exc: Exception):

@app.get("/")
async def root():
return {"message": "Welcome to the KunFlix API"}
return {"message": "Welcome to the KunFlix API", "version": settings.VERSION}


@app.websocket("/api/ws")
Expand Down
12 changes: 10 additions & 2 deletions backend/routers/chats.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
from fastapi.responses import StreamingResponse
from sqlalchemy.future import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import func
from sqlalchemy import func, update
from typing import List, Optional
import logging

from database import get_db
from models import Agent, ChatSession, ChatMessage
from models import Agent, ChatSession, ChatMessage, Theater
from schemas import ChatSessionCreate, ChatSessionUpdate, ChatSessionResponse, ChatMessageCreate
from auth import get_current_active_user_or_admin, scoped_query, is_admin_entity
from services.chat_utils import serialize_content, deserialize_content
Expand Down Expand Up @@ -181,6 +181,14 @@ async def send_message(
# 2. 保存用户消息(多模态内容序列化为 JSON)
user_msg = ChatMessage(session_id=session_id, role="user", content=serialize_content(message.content))
db.add(user_msg)
# 对话视为剧场活跃行为:触达 Theater.updated_at,驱动首页「最近剧场」按最后活跃排序
# 限定归属当前用户,防止客户端传入他人 theater_id 误触
theater_ref = chat_session.theater_id or message.theater_id
theater_ref and await db.execute(
update(Theater)
.where(Theater.id == theater_ref, Theater.user_id == current_user.id)
.values(updated_at=func.now())
)
await db.commit()

# 捕获上下文变量
Expand Down
8 changes: 5 additions & 3 deletions backend/routers/skills_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,14 @@ async def toggle_skill(skill_name: str, current_admin=Depends(require_admin)):
"""Toggle a skill's active status."""
active_path = get_active_skills_dir(get_skills_base_dir()) / skill_name

# 选择分支时直接绑定完整调用,消除“函数与参数分离选择”的歧义
# (disable_skill 无 force 参数;enable_skill 需 force=True 覆盖同步)
action, new_state = (
(_skill_service.disable_skill, "inactive")
((lambda: _skill_service.disable_skill(skill_name)), "inactive")
if active_path.exists()
else (_skill_service.enable_skill, "active")
else ((lambda: _skill_service.enable_skill(skill_name, force=True)), "active")
)
success = action(skill_name) if new_state == "inactive" else action(skill_name, force=True)
success = action()

if not success:
raise HTTPException(status_code=500, detail=f"Failed to toggle skill {skill_name}")
Expand Down
34 changes: 32 additions & 2 deletions backend/routers/videos.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from models import LLMProvider, VideoTask, ChatMessage, Asset, User, generate_uuid
from schemas import VideoGenerateRequest, VideoTaskResponse, VideoTaskListResponse, VideoConfig
from auth import get_current_active_user_or_admin, is_admin_entity, scoped_query
from services.video_generation import submit_video_task, poll_video_task, VideoContext, MAX_POLL_FAILURES, infer_provider_type
from services.video_generation import submit_video_task, poll_video_task, cancel_video_task, VideoContext, MAX_POLL_FAILURES, infer_provider_type

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'MAX_POLL_FAILURES' is not used.
Comment thread
bahamutww marked this conversation as resolved.
Dismissed
from services.video_providers import extract_video_provider_type
from services.video_providers.model_capabilities import get_model_capabilities
from services.video_providers.virtual_human_presets import list_presets as list_virtual_human_presets
Expand Down Expand Up @@ -221,7 +221,10 @@

# 轮询供应商 (根据 provider_type 自动选择适配器)
provider_type = extract_video_provider_type(provider.provider_type) or infer_provider_type(task.model or "", provider.provider_type)
poll_result = await poll_video_task(provider.api_key, task.xai_task_id, provider_type, base_url=provider.base_url)
poll_result = await poll_video_task(
provider.api_key, task.xai_task_id, provider_type,
base_url=provider.base_url, model=task.model or "",
)

# 超时保护:pending 且有错误超过 5 分钟 → 判定失败
poll_has_error = poll_result.error and poll_result.status == "pending"
Expand Down Expand Up @@ -438,6 +441,9 @@
HTTPException(status_code=400, detail="只能删除已完成或失败的任务")
)

# 顺带清理上游任务记录 (仅部分供应商支持, best-effort 不阻断本地删除)
task.xai_task_id and await _cleanup_upstream_task(task, db)

# 删除本地视频文件(路径格式: /api/media/{uuid}.mp4)
video_path = task.result_video_url
video_path and _try_delete_local_file(video_path)
Expand Down Expand Up @@ -465,6 +471,30 @@
logger.info(f"Deleted local file: {filepath}")


async def _cleanup_upstream_task(task: VideoTask, db: AsyncSession) -> None:
"""删除本地任务时顺带取消/删除上游任务记录 (best-effort)

仅部分供应商提供该端点 (如 MiniMax-H3 的 DELETE /v2/video_generation/{task_id}),
不支持或调用失败均不影响本地删除。
"""
try:
provider = (await db.execute(
select(LLMProvider).where(LLMProvider.id == task.provider_id)
)).scalar_one_or_none()
if not provider:
return
provider_type = extract_video_provider_type(provider.provider_type) or infer_provider_type(
task.model or "", provider.provider_type
)
ok = await cancel_video_task(
provider.api_key, task.xai_task_id, provider_type,
model=task.model or "", base_url=provider.base_url,
)
ok and logger.info(f"Upstream video task cleaned up: {task.xai_task_id} ({provider_type})")
except Exception as e:
logger.warning(f"Upstream video task cleanup failed for {task.xai_task_id}: {e}")


def _build_task_response(task: VideoTask, provider_name: str = None, billing_underpaid: bool = False, remaining_credits: Optional[float] = None) -> VideoTaskResponse:
"""构建视频任务响应"""
return VideoTaskResponse(
Expand Down
15 changes: 9 additions & 6 deletions backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,9 @@ class AIGenerateResponse(BaseModel):
class VideoConfig(BaseModel):
"""视频生成配置"""
duration: int = Field(default=6, ge=1, le=15)
quality: Literal["480p", "720p", "768p", "1080p"] = "720p"
# 取值与 video_providers/model_capabilities.py 中各模型声明的 resolutions 保持一致
# (512p: Hailuo-02 / 2k: MiniMax-H3 / 4k: Veo 3.1)
quality: Literal["480p", "512p", "720p", "768p", "1080p", "2k", "4k"] = "720p"
aspect_ratio: str = "16:9"
mode: str = "normal" # 保留字段兼容前端,部分 API 不使用
# MiniMax 特有配置
Expand All @@ -771,13 +773,14 @@ class VideoGenerateRequest(BaseModel):
model: str
session_id: Optional[str] = None
video_mode: Literal["text_to_video", "image_to_video", "edit", "reference_images", "video_extension"] = "text_to_video"
prompt: str = Field(..., min_length=1, max_length=2000)
# 上限对齐能力最强的模型 (MiniMax-H3 单条 text 支持 7000 字符)
prompt: str = Field(..., min_length=1, max_length=7000)
image_url: Optional[str] = None # 首帧图片 (image_to_video/edit)
last_frame_image: Optional[str] = None # 尾帧图片 (MiniMax-Hailuo-02 / Seedance 2.0 支持)
reference_images: Optional[List[dict]] = None # 参考图片列表 (Grok/Gemini Veo 3.1/Seedance 2.0)
last_frame_image: Optional[str] = None # 尾帧图片 (MiniMax-Hailuo-02 / MiniMax-H3 / Seedance 2.0 支持)
reference_images: Optional[List[dict]] = None # 参考图片列表 (Grok/Gemini Veo 3.1/Seedance 2.0/MiniMax-H3)
extension_video_url: Optional[str] = None # 视频扩展源视频 URL (向后兼容)
reference_videos: Optional[List[dict]] = None # 参考视频列表 (Seedance 2.0, 最多 3 个)
reference_audios: Optional[List[dict]] = None # 参考音频列表 (Seedance 2.0, 最多 3 个)
reference_videos: Optional[List[dict]] = None # 参考视频列表 (Seedance 2.0 / MiniMax-H3, 最多 3 个)
reference_audios: Optional[List[dict]] = None # 参考音频列表 (Seedance 2.0 / MiniMax-H3, 最多 3 个)
return_last_frame: bool = False # 返回视频尾帧图像 (Seedance 2.0)
config: Optional[VideoConfig] = None

Expand Down
Loading