feat: 空镜素材配置后端化,视频生成流程重构

- 后端: 空镜素材迁移到 config/materials.json,duration从文件名_{N}s_自动解析
- 后端: 新增 POST /api/v1/materials/match 接口,后端做关键词匹配
- 前端: VideoGeneration 空镜匹配改为调用后端接口
- 前端: 人物出镜素材改为本地文件选择器直接选取,不走素材库
- 前端: 视频生成流程简化,移除Vidu对口型和七牛云上传
- Rust: 视频合成支持从随机起始时间截取人物素材片段
- Rust: 修复ffprobe参数错误(添加-show_entries format=duration)
This commit is contained in:
小鱼开发
2026-04-22 18:49:20 +08:00
parent 5154af777c
commit 4e06f4abe2
46 changed files with 6785 additions and 1396 deletions
+32
View File
@@ -0,0 +1,32 @@
"""
空镜素材 API
============
提供空镜素材匹配接口。
"""
from fastapi import APIRouter
from app.schemas.common import ApiResponse, success_response
from app.schemas.materials import MatchMaterialRequest, MaterialInfo
from app.services.material_service import match_material
router = APIRouter()
@router.post("/match", response_model=ApiResponse[MaterialInfo | None])
async def match_material_endpoint(request: MatchMaterialRequest):
"""
根据场景描述和所需时长匹配空镜素材
返回匹配到的素材信息,无匹配返回 data: null
"""
result = match_material(request.scene, request.duration)
if result is None:
return success_response(data=None, message="未匹配到素材")
return success_response(
data=MaterialInfo(url=result["url"], duration=result["duration"]),
message="匹配成功",
)
+12
View File
@@ -8,9 +8,12 @@ from fastapi import APIRouter
from app.api.v1 import (
auth,
caption,
materials,
script,
system,
tasks,
upload,
vidu,
voice,
)
@@ -33,3 +36,12 @@ api_router.include_router(caption.router, tags=["Caption"])
# 语音合成模块(TTS + 声音克隆)
api_router.include_router(voice.router, tags=["Voice"])
# 文件上传模块
api_router.include_router(upload.router, tags=["Upload"])
# Vidu 对口型模块
api_router.include_router(vidu.router, tags=["Vidu"])
# 空镜素材模块
api_router.include_router(materials.router, prefix="/materials", tags=["Materials"])
+189
View File
@@ -0,0 +1,189 @@
"""
文件上传 API
============
提供通用文件上传功能,直接上传到七牛云对象存储。
"""
import logging
import uuid
from pathlib import Path
import io
from fastapi import APIRouter, File, HTTPException, UploadFile
from pydantic import BaseModel, Field
from app.schemas.common import ApiResponse, success_response
from app.services.qiniu_service import get_qiniu_service
router = APIRouter(prefix="/upload", tags=["Upload"])
logger = logging.getLogger(__name__)
class UploadResponse(BaseModel):
"""上传响应"""
url: str = Field(..., description="七牛云文件 URL")
key: str = Field(..., description="七牛云文件 key")
size: int = Field(..., description="文件大小(字节)")
@router.post("/video", response_model=ApiResponse[UploadResponse])
async def upload_video(
file: UploadFile = File(..., description="视频文件"),
):
"""
上传视频到七牛云
支持格式:mp4, mov, avi, webm
返回七牛云永久访问 URL。
"""
try:
# 验证文件格式
allowed_types = {
"video/mp4",
"video/quicktime",
"video/x-msvideo",
"video/webm",
}
content_type = file.content_type or ""
# 如果 content_type 为空,尝试从文件名推断
if not content_type:
ext = Path(file.filename or "").suffix.lower()
ext_to_mime = {
".mp4": "video/mp4",
".mov": "video/quicktime",
".avi": "video/x-msvideo",
".webm": "video/webm",
}
content_type = ext_to_mime.get(ext, "")
if content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"不支持的文件格式: {content_type},请上传 mp4/mov/avi/webm 视频",
)
# 读取文件内容
content = await file.read()
if not content:
raise HTTPException(status_code=400, detail="文件内容为空")
# 生成唯一文件名
ext = Path(file.filename or "video.mp4").suffix or ".mp4"
unique_name = f"{uuid.uuid4().hex[:16]}{ext}"
# 上传到七牛云
qiniu = get_qiniu_service()
bucket, domain = qiniu._get_bucket_and_domain("video")
key = qiniu.generate_key("video", unique_name)
stream = io.BytesIO(content)
result = qiniu.upload_stream(
stream=stream,
key=key,
mime_type=content_type or "video/mp4",
bucket=bucket,
domain=domain,
)
url = result.get("url")
key = result.get("key")
if not url:
raise HTTPException(status_code=500, detail="上传到七牛云失败:未返回 URL")
logger.info(f"[Upload] 视频上传成功: {url[:80]}..., size={len(content)}")
return success_response(
data=UploadResponse(
url=url,
key=key or unique_name,
size=len(content),
)
)
except HTTPException:
raise
except Exception as e:
logger.error(f"[Upload] 视频上传失败: {e}")
raise HTTPException(status_code=500, detail=f"上传失败: {e}")
@router.post("/image", response_model=ApiResponse[UploadResponse])
async def upload_image(
file: UploadFile = File(..., description="图片文件"),
):
"""
上传图片到七牛云
支持格式:jpg, png, gif, webp
"""
try:
allowed_types = {
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
}
content_type = file.content_type or ""
if not content_type:
ext = Path(file.filename or "").suffix.lower()
ext_to_mime = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
}
content_type = ext_to_mime.get(ext, "")
if content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail=f"不支持的图片格式: {content_type}",
)
content = await file.read()
if not content:
raise HTTPException(status_code=400, detail="文件内容为空")
ext = Path(file.filename or "image.jpg").suffix or ".jpg"
unique_name = f"{uuid.uuid4().hex[:16]}{ext}"
qiniu = get_qiniu_service()
bucket, domain = qiniu._get_bucket_and_domain("image")
key = qiniu.generate_key("image", unique_name)
stream = io.BytesIO(content)
result = qiniu.upload_stream(
stream=stream,
key=key,
mime_type=content_type or "image/jpeg",
bucket=bucket,
domain=domain,
)
url = result.get("url")
key = result.get("key")
if not url:
raise HTTPException(status_code=500, detail="上传到七牛云失败:未返回 URL")
logger.info(f"[Upload] 图片上传成功: {url[:80]}..., size={len(content)}")
return success_response(
data=UploadResponse(
url=url,
key=key or unique_name,
size=len(content),
)
)
except HTTPException:
raise
except Exception as e:
logger.error(f"[Upload] 图片上传失败: {e}")
raise HTTPException(status_code=500, detail=f"上传失败: {e}")
+159
View File
@@ -0,0 +1,159 @@
"""
Vidu API 代理路由
================
提供 Vidu 对口型(lip-sync)任务的提交和查询接口。
前端通过此接口提交任务并轮询状态,无需直接访问 Vidu API。
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from app.api.deps import get_current_user
from app.models.user import User
from app.schemas.common import ApiResponse, success_response
from app.services.vidu_tts_service import ViduTTSService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/vidu", tags=["Vidu"])
# ========== 请求/响应模型 ==========
class LipSyncRequest(BaseModel):
"""对口型请求"""
video_url: str = Field(..., min_length=1, description="原视频 URL")
audio_url: str | None = Field(None, description="音频 URL(与 text 二选一)")
text: str | None = Field(None, description="文本内容(与 audio_url 二选一)")
voice_id: str | None = Field(None, description="音色 ID(文字驱动时生效)")
speed: float = Field(default=1.0, ge=0.5, le=2.0, description="语速")
volume: int = Field(default=0, ge=0, le=10, description="音量")
ref_photo_url: str | None = Field(None, description="人脸参考图 URL")
@staticmethod
def validate_at_least_one_audio_source(values: dict) -> dict:
"""验证至少提供 audio_url 或 text 之一"""
audio_url = values.get("audio_url")
text = values.get("text")
if not audio_url and not text:
raise ValueError("必须提供 audio_url 或 text 之一")
return values
class LipSyncResponse(BaseModel):
"""对口型任务提交响应"""
task_id: str = Field(..., description="Vidu 任务 ID")
message: str = Field(default="任务已提交", description="状态消息")
class LipSyncQueryResponse(BaseModel):
"""对口型任务查询响应"""
task_id: str = Field(..., description="任务 ID")
state: str = Field(..., description="任务状态: pending/processing/succeeded/failed")
video_url: str | None = Field(None, description="生成后的视频 URL(成功时)")
message: str | None = Field(None, description="状态描述或错误信息")
creations: list[dict] | None = Field(None, description="Vidu 原始 creations 数据")
# ========== API 路由 ==========
@router.post("/lip-sync", response_model=ApiResponse[LipSyncResponse])
async def create_lip_sync_task(
request: LipSyncRequest,
current_user: User = Depends(get_current_user),
):
"""
提交 Vidu 对口型任务
需要提供:
- video_url: 原视频 URL(人物出镜视频)
- audio_url: 音频 URL(与 text 二选一)
- text: 文本内容(与 audio_url 二选一,使用 Vidu 内置 TTS)
返回 Vidu task_id,用于后续轮询查询。
"""
try:
# 验证至少提供一种音频来源
if not request.audio_url and not request.text:
raise HTTPException(
status_code=400,
detail="必须提供 audio_url 或 text 之一",
)
service = ViduTTSService()
task_id = await service.lip_sync_create(
video_url=request.video_url,
audio_url=request.audio_url,
text=request.text,
voice_id=request.voice_id,
speed=request.speed,
volume=request.volume,
ref_photo_url=request.ref_photo_url,
)
logger.info(f"[Vidu] 对口型任务提交成功: task_id={task_id}, user={current_user.id}")
return success_response(
data=LipSyncResponse(
task_id=task_id,
message="对口型任务已提交,请轮询查询状态",
)
)
except HTTPException:
raise
except Exception as e:
logger.error(f"[Vidu] 提交对口型任务失败: {e}")
raise HTTPException(status_code=500, detail=f"提交对口型任务失败: {e}")
@router.get("/tasks/{task_id}/creations", response_model=ApiResponse[LipSyncQueryResponse])
async def query_lip_sync_task(
task_id: str,
current_user: User = Depends(get_current_user),
):
"""
查询 Vidu 对口型任务状态
返回任务状态及生成物信息。
当 state=succeeded 时,video_url 为生成后的对口型视频 URL。
"""
try:
service = ViduTTSService()
result = await service.lip_sync_query(task_id)
state = result.get("state", "unknown")
creations = result.get("creations", [])
# 提取视频 URL(成功时)
video_url = None
if state == "succeeded" and creations:
# creations 是数组,取第一个
first_creation = creations[0] if creations else {}
video_url = first_creation.get("url")
logger.info(
f"[Vidu] 查询对口型任务: task_id={task_id}, state={state}, user={current_user.id}"
)
return success_response(
data=LipSyncQueryResponse(
task_id=task_id,
state=state,
video_url=video_url,
message=result.get("message") if state == "failed" else None,
creations=creations if creations else None,
)
)
except Exception as e:
logger.error(f"[Vidu] 查询对口型任务失败: {e}")
raise HTTPException(status_code=500, detail=f"查询任务失败: {e}")
+9
View File
@@ -79,6 +79,15 @@ async def lifespan(app: FastAPI):
except Exception as e:
logger.warning(f"Failed to load models from config: {e}")
# 加载空镜素材配置(从 JSON 文件)
try:
from app.services.material_service import load_config
load_config()
logger.info("Loaded material config from JSON file")
except Exception as e:
logger.warning(f"Failed to load material config: {e}")
yield
# 关闭时清理
+20
View File
@@ -0,0 +1,20 @@
"""
空镜素材 Schema
==============
"""
from pydantic import BaseModel, Field
class MaterialInfo(BaseModel):
"""素材条目"""
url: str = Field(description="素材 URL(远程或本地路径)")
duration: float = Field(description="素材时长(秒),从文件名解析)")
class MatchMaterialRequest(BaseModel):
"""匹配素材请求"""
scene: str = Field(description="分镜场景描述")
duration: float = Field(description="所需时长(秒)")
@@ -0,0 +1,81 @@
"""
空镜素材服务
============
从本地 JSON 配置文件加载素材库,提供匹配逻辑。
duration 从文件名 `_{N}s_` 中自动解析。
"""
import json
import random
import re
from pathlib import Path
# 正则:从文件名中提取时长,如 plumbing_10s_a23f8fcb.mp4 → 10
_DURATION_RE = re.compile(r"_(\d+)s_")
# 配置缓存(启动时加载,运行时只读)
_keywords: dict[str, str] = {}
_materials: dict[str, list[dict]] = {}
def _get_config_path() -> Path:
"""获取配置文件绝对路径"""
# 配置文件位于项目根目录的 config/ 下
return Path(__file__).resolve().parent.parent.parent / "config" / "materials.json"
def _parse_duration(url: str) -> float:
"""从 URL 文件名中解析时长(秒)"""
filename = url.split("/")[-1]
match = _DURATION_RE.search(filename)
if not match:
raise ValueError(f"无法从文件名解析时长: {filename}")
return float(match.group(1))
def load_config() -> None:
"""加载素材配置到内存缓存"""
global _keywords, _materials
config_path = _get_config_path()
if not config_path.exists():
raise FileNotFoundError(f"素材配置文件不存在: {config_path}")
with open(config_path, "r", encoding="utf-8") as f:
data = json.load(f)
_keywords = data.get("keywords", {})
raw_materials = data.get("materials", {})
# 解析每个素材的 duration
_materials = {}
for slug, entries in raw_materials.items():
parsed = []
for entry in entries:
url = entry["url"]
duration = _parse_duration(url)
parsed.append({"url": url, "duration": duration})
_materials[slug] = parsed
def match_material(scene: str, required_duration: float) -> dict | None:
"""
根据场景描述和所需时长匹配空镜素材
Args:
scene: 分镜场景描述
required_duration: 所需时长(秒)
Returns:
匹配到的素材 {url, duration},无匹配返回 None
"""
for keyword, slug in _keywords.items():
if keyword in scene:
candidates = [
m for m in _materials.get(slug, [])
if m["duration"] >= required_duration
]
if candidates:
return random.choice(candidates)
return None
+635
View File
@@ -0,0 +1,635 @@
{
"version": "1.0",
"keywords": {
"吊顶": "ceiling",
"天花": "ceiling",
"水电": "plumbing",
"水管": "plumbing",
"油漆": "paint",
"涂料": "paint",
"刷漆": "paint",
"完工": "final",
"竣工": "final",
"交付": "final"
},
"materials": {
"ceiling": [
{
"url": "https://media.liche.cn/meijiaka-zj/material/ceiling/ceiling_5s_c1e06c14.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/ceiling/ceiling_6s_4091bf65.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/ceiling/ceiling_6s_78fdb4d1.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/ceiling/ceiling_7s_52ffb6e9.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/ceiling/ceiling_8s_14ce7f1d.mp4"
}
],
"paint": [
{
"url": "https://media.liche.cn/meijiaka-zj/material/paint/paint_4s_7981c90b.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/paint/paint_5s_94e1a99e.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/paint/paint_5s_bec25ff4.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/paint/paint_6s_178e2b25.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/paint/paint_6s_26cc917c.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/paint/paint_7s_7a16fb72.mp4"
}
],
"tile": [
{
"url": "https://media.liche.cn/meijiaka-zj/material/tile/tile_4s_7be39898.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/tile/tile_4s_aeb1343d.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/tile/tile_5s_4c1e25dc.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/tile/tile_5s_b26b8cae.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/tile/tile_5s_fe2a8645.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/tile/tile_6s_55ffa0ff.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/tile/tile_6s_a2447a54.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/tile/tile_7s_95b87885.mp4"
}
],
"waterproof": [
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_10s_a2b0f6da.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_11s_eaf4b9f0.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_5s_22f5e19b.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_5s_35498a2a.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_5s_6cbb95dc.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_5s_aa110132.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_5s_da3a9f8d.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_6s_32c90edb.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_6s_51d60412.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_6s_59cbfabd.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_6s_88d43e86.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_6s_9a6c1e20.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_7s_4be2c8b3.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_7s_efb40fd3.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_8s_13fc8a2a.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_8s_c816798f.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/waterproof/waterproof_9s_5c3820be.mp4"
}
],
"final": [
{
"url": "https://media.liche.cn/meijiaka-zj/material/final/final_10s_40b78665.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/final/final_11s_7c8c6833.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/final/final_4s_5ee06c87.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/final/final_4s_a8994032.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/final/final_5s_98b64716.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/final/final_6s_83e76197.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/final/final_7s_109bde47.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/final/final_7s_8b9df21b.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/final/final_7s_ee3be316.mp4"
}
],
"plumbing": [
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_10s_a23f8fcb.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_11s_8cad4c63.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_014d4b99.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_26c96bb3.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_88f3c2ff.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_8f9e2fd1.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_917e73d2.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_a21a93c2.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_b41346c9.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_b6c5d8d5.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_c83e401f.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_3s_edc3ed94.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_06c176f7.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_0d8188b0.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_1d024051.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_26252c4f.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_289d05b9.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_291ff9cd.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_2a24e5dd.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_2ea437ff.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_30204ab3.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_3279abb0.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_370db89d.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_3fa42431.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_46a47e19.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_4b6e8a71.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_4db3ad9d.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_5670036a.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_5de6f9c7.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_635ceca4.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_66d70c58.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_6ac4e162.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_6f61c1d7.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_77a83b95.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_80f8c4db.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_8782b24d.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_8a4aac19.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_9193c60f.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_923da0a5.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_92c20623.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_991614c2.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_a5a8f8e9.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_abac94a2.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_adda5387.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_b2386645.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_c011f332.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_d7977183.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_daf3eb45.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_ddf27707.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_e8224a6a.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_edf6cd59.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_eed46b34.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_4s_f01dbb9a.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_01547dc3.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_01a8ac16.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_03c341ba.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_03fc74f5.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_10b4f6d7.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_12d6cc7f.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_17492b31.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_192bd9cc.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_1db4fcc0.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_1e8fabdb.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_1ebe0150.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_22532b8d.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_239aa4e0.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_2461447a.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_29b29710.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_2b144bbe.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_4ac4f57e.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_4e8a4605.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_50a842d5.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_58f310b5.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_5a6d3324.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_7afd5931.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_80d28904.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_845aaeb5.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_8920d53d.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_91f27b4d.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_924538a9.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_964c51f3.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_a76f5d31.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_a7afa965.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_aa9a3927.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_aac7da24.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_aafcc59d.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_b56deb39.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_b7150433.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_b920788e.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_ba04551e.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_bbb8dc91.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_bf0f4247.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_c02abcad.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_c06f8b5c.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_cb24fc27.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_cbaa1936.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_ccec7599.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_ddcadc8c.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_e0da2b38.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_fcf594b4.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_5s_ff920a11.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_06f77a48.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_0a814b44.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_0b62bfaa.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_1850465b.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_24db5614.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_41e2f984.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_4dcfba99.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_501f7797.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_54090579.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_548c3105.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_5683347b.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_7d3db3d5.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_7e30771b.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_801e5229.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_8dc61b6c.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_8ef50b5a.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_b39a845c.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_b456e9ea.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_b570d306.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_b8952314.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_bf9443da.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_cd17be55.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_de6b43ec.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_6s_dfb8c797.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_0347af3b.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_176e9cb9.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_22a74e9e.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_2b31aa82.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_2e62e3c0.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_3e6c3f0e.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_696efd19.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_7e1a3879.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_867c9942.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_95a93892.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_9ef1fc68.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_a9fb2286.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_ca1063e6.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_d73638e7.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_e0b10567.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_ea506fe9.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_7s_f0d979ba.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_0fe1953c.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_1f04e5f3.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_21b61c48.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_67a52a55.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_69833536.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_705db3bf.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_ab98331b.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_ae8c2c29.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_be9d5579.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_c4ad7c9f.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_d9805d78.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_8s_e77c4bed.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_9s_47be1915.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_9s_4c1e542f.mp4"
},
{
"url": "https://media.liche.cn/meijiaka-zj/material/plumbing/plumbing_9s_5e45c9e5.mp4"
}
]
}
}
+4 -4
View File
@@ -7,8 +7,8 @@ services:
environment:
- ENV=development
- DEBUG=true
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/meijiaka_zj
- REDIS_HOST=redis
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@meijiaka-db:5432/meijiaka_zj
- REDIS_HOST=meijiaka-redis
- REDIS_PORT=6379
- REDIS_DB=1
- SECRET_KEY=dev-secret-key-change-in-production
@@ -37,8 +37,8 @@ services:
environment:
- ENV=development
- DEBUG=true
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/meijiaka_zj
- REDIS_HOST=redis
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@meijiaka-db:5432/meijiaka_zj
- REDIS_HOST=meijiaka-redis
- REDIS_PORT=6379
- REDIS_DB=1
- SECRET_KEY=dev-secret-key-change-in-production