Compare commits

...

4 Commits

Author SHA1 Message Date
CI Bot a05edc7f0c fix(ci): 修复增量扫描误检问题 - 改用 Gitea API 获取 PR 改动文件列表
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 1m13s
CI/CD Pipeline / Frontend Lint (push) Successful in 1m13s
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Successful in 3m12s
CI/CD Pipeline / Integration Tests (push) Successful in 2m31s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m9s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m13s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Successful in 2m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m6s
根因: 原实现用 git init + 浅克隆 fetch 后直接 git diff,由于本地 commit 与远程 develop
      没有共同祖先,diff 结果是两棵独立树的全量对比,导致大量历史文件被误检。

修复: 改用 Gitea API /pulls/{n}/files 获取 PR 改动文件列表,准确可靠。
      API 失败时 fallback 到全量扫描,保证不阻塞。
2026-07-15 11:50:19 +08:00
xiaoxia 7c8748ece6 perf(ci): 扩展CI触发分支模式 + 增加手动触发入口 (#349)
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
2026-07-15 11:20:08 +08:00
CI Bot f0a5f4fe42 fix: 硬切场景改用concat filter替代xfade,提升多素材场景性能与稳定性
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 46s
CI/CD Pipeline / Frontend Lint (push) Successful in 48s
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Successful in 2m29s
CI/CD Pipeline / Integration Tests (push) Successful in 2m24s
- 全硬切(no transition)场景下使用concat filter,性能远优于xfade
- 有转场效果时继续走xfade路径,保持兼容
- 修复硬切默认走xfade(fade fallback)的设计问题
- 新增单元测试覆盖concat和xfade两种路径
- 87个unified_render测试全绿,全套2147测试零回归
2026-07-15 11:18:28 +08:00
xiaoxia eba96fc864 feat(api): 剪辑计划生成状态API增强 - 新增progress/video_url/error_message等字段 (#351)
CI/CD Pipeline / Frontend Lint (push) Successful in 1m35s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 2m16s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Successful in 2m29s
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
2026-07-15 11:15:55 +08:00
10 changed files with 161 additions and 29 deletions
+15 -2
View File
@@ -5,19 +5,32 @@ on:
- main
- develop
- feature/**
- feat/**
- bugfix/**
- fix/**
- hotfix/**
- release/**
- refactor/**
- perf/**
- docs/**
- chore/**
- ci/**
tags:
- v*
pull_request:
branches:
- main
- develop
workflow_dispatch:
inputs:
reason:
description: "触发原因"
required: false
default: "手动触发 - CI漏触发补跑"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
jobs:
validate:
@@ -83,7 +96,7 @@ jobs:
shell: sh
env:
GITHUB_TOKEN: ${{ github.token }}
run: "set -eu\nSCAN_MODE=\"full\"\nCHANGED_PY_FILES=\"\"\n\nif [ \"${GITHUB_EVENT_NAME:-}\" = \"pull_request\" ] && [ -n \"${GITHUB_BASE_REF:-}\" ]; then\n echo \"PR mode (base: ${GITHUB_BASE_REF}) - preparing incremental scan\"\n git init -q\n git config user.email \"ci@localhost\"\n git config user.name \"CI\"\n git add .\n git commit -q -m \"current\"\n REPO_URL=\"https://x-access-token:${GITHUB_TOKEN}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git\"\n git remote add origin \"$REPO_URL\"\n if git fetch origin \"${GITHUB_BASE_REF}\" --depth=50 -q 2>/dev/null; then\n CHANGED_PY_FILES=$(git diff --name-only --diff-filter=ACMRT \"origin/${GITHUB_BASE_REF}\" HEAD -- '*.py' 2>/dev/null | tr '\\n' ' ')\n if [ -n \"$CHANGED_PY_FILES\" ]; then\n SCAN_MODE=\"incremental\"\n FILE_COUNT=$(echo \"$CHANGED_PY_FILES\" | wc -w)\n echo \"Changed Python files: ${FILE_COUNT}\"\n echo \"$CHANGED_PY_FILES\" | tr ' ' '\\n' | grep -v '^$'\n else\n SCAN_MODE=\"skip_py\"\n echo \"No Python files changed in this PR\"\n fi\n else\n echo \"WARN: failed to fetch base branch, falling back to full scan\"\n fi\nelse\n echo \"Full scan mode (not a PR event)\"\nfi\n\necho \"SCAN_MODE=$SCAN_MODE\" >> $GITHUB_ENV\necho \"CHANGED_PY_FILES=$CHANGED_PY_FILES\" >> $GITHUB_ENV\n"
run: "set -eu\nSCAN_MODE=\"full\"\nCHANGED_PY_FILES=\"\"\n\nif [ \"${GITHUB_EVENT_NAME:-}\" = \"pull_request\" ] && [ -n \"${GITHUB_REF_NAME:-}\" ]; then\n echo \"PR mode (#${GITHUB_REF_NAME}) - fetching changed files from API\"\n\n PR_NUMBER=$(echo \"$GITHUB_REF\" | sed 's|refs/pull/||; s|/.*||')\n API_URL=\"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=100\"\n\n set +e\n RESPONSE=$(curl -s -w \"\\n%{http_code}\" -H \"Authorization: token ${GITHUB_TOKEN}\" \"${API_URL}\")\n HTTP_CODE=$(echo \"$RESPONSE\" | tail -n1)\n BODY=$(echo \"$RESPONSE\" | sed '$d')\n set -e\n\n if [ \"$HTTP_CODE\" = \"200\" ]; then\n CHANGED_PY_FILES=$(echo \"$BODY\" | python3 -c \"\nimport json, sys\ntry:\n files = json.load(sys.stdin)\n py_files = [f['filename'] for f in files\n if f['filename'].endswith('.py') and f['status'] != 'removed']\n print(' '.join(py_files))\nexcept Exception:\n print('')\n\")\n if [ -n \"$CHANGED_PY_FILES\" ]; then\n SCAN_MODE=\"incremental\"\n FILE_COUNT=$(echo \"$CHANGED_PY_FILES\" | wc -w)\n echo \"Changed Python files: ${FILE_COUNT}\"\n echo \"$CHANGED_PY_FILES\" | tr ' ' '\\n' | grep -v '^$'\n else\n SCAN_MODE=\"skip_py\"\n echo \"No Python files changed in this PR\"\n fi\n else\n echo \"WARN: API returned HTTP $HTTP_CODE, falling back to full scan\"\n fi\nelse\n echo \"Full scan mode (not a PR event)\"\nfi\n\necho \"SCAN_MODE=$SCAN_MODE\" >> $GITHUB_ENV\necho \"CHANGED_PY_FILES=$CHANGED_PY_FILES\" >> $GITHUB_ENV\n"
- name: Run code quality checks
shell: sh
run: "set -eu\n\nif [ \"$SCAN_MODE\" = \"incremental\" ]; then\n echo \"=== Incremental scan mode ===\"\n\n python3 -m compileall -q $CHANGED_PY_FILES\n\n python3 -m black --check --fast $CHANGED_PY_FILES\n\n python3 -m isort --check-only $CHANGED_PY_FILES\n\n RUFF_FILES=$(echo \"$CHANGED_PY_FILES\" | tr ' ' '\\n' | grep -v '^scripts/' | tr '\\n' ' ')\n if [ -n \"$RUFF_FILES\" ]; then\n python3 -m ruff check $RUFF_FILES --statistics\n else\n echo \"No ruff-checkable files changed, skipping\"\n fi\n\nelif [ \"$SCAN_MODE\" = \"skip_py\" ]; then\n echo \"No Python files changed - skipping Python lint checks\"\n\nelse\n echo \"=== Full scan mode ===\"\n\n python3 -m compileall -q alembic apps packages tests scripts\n\n python3 -m black --check --fast alembic apps packages tests scripts\n\n python3 -m isort --check-only alembic apps packages tests scripts\n\n python3 -m ruff check apps packages tests --statistics\nfi\n"
+4
View File
@@ -108,6 +108,10 @@ class EditPlanGenerationStatusResponse(BaseModel):
plan_id: str
plan_status: str
generation_task_id: Optional[str] = None
generation_task_status: Optional[str] = None
progress: float = 0.0
video_url: str = ""
error_message: str = ""
clips: List[ClipStatusItem]
+17 -4
View File
@@ -183,9 +183,7 @@ def _auto_fallback_auto_material_mode(
def _check_queue_limits(gen_task_repo, user_id: str) -> None:
"""队列限流预检查"""
try:
has_count = hasattr(gen_task_repo, "count_pending_by_user") and hasattr(
gen_task_repo, "count_pending_total"
)
has_count = hasattr(gen_task_repo, "count_pending_by_user") and hasattr(gen_task_repo, "count_pending_total")
if has_count:
user_pending = gen_task_repo.count_pending_by_user(user_id)
global_pending = gen_task_repo.count_pending_total()
@@ -333,10 +331,25 @@ def get_generation_status(
for c in clips
]
# 从 plan.config 中取渲染结果 URL
video_url = (plan.config or {}).get("rendered_url", "")
# 从 gen_status 中取进度、错误信息、任务状态
progress = gen_status.get("progress", 0.0)
error_message = gen_status.get("error_message", "")
gen_task_status = gen_status.get("generation_task_status")
# 如果计划已完成但进度还是0,补100
plan_status_val = plan.status.value if hasattr(plan.status, "value") else plan.status
if plan_status_val == "completed" and progress < 100:
progress = 100.0
return EditPlanGenerationStatusResponse(
plan_id=plan_id,
plan_status=plan.status.value if hasattr(plan.status, "value") else plan.status,
plan_status=plan_status_val,
generation_task_id=gen_status["generation_task_id"],
generation_task_status=gen_task_status,
progress=progress,
video_url=video_url,
error_message=error_message,
clips=clip_items,
)
@@ -431,6 +431,8 @@ class EditPlanService:
"clips": List[EditPlanClip],
"generation_task_id": Optional[str],
"generation_task_status": Optional[str],
"progress": float,
"error_message": str,
}
Raises:
@@ -442,17 +444,23 @@ class EditPlanService:
# 从 plan.config 中获取 generation_task_id
generation_task_id = plan.config.get("generation_task_id")
generation_task_status = None
progress = 0.0
error_message = ""
if generation_task_id:
task = self._generation_task_repo.get(generation_task_id)
if task:
generation_task_status = task.status.value if hasattr(task.status, "value") else task.status
progress = getattr(task, "progress", 0.0) or 0.0
error_message = getattr(task, "error_message", "") or ""
return {
"plan": plan,
"clips": clips,
"generation_task_id": generation_task_id,
"generation_task_status": generation_task_status,
"progress": progress,
"error_message": error_message,
}
def can_generate(self, plan_id: str) -> tuple[bool, str]:
@@ -1363,23 +1363,39 @@ class UnifiedRenderService:
# 单 clip 层,直接使用预处理标签
layer_output_labels[layer.role] = layer_labels[0]
else:
# 多 clip 层,用 TransitionEngine 构建转场链
out_label = f"{layer.role}_merged"
# 计算该层使用的转场时长(取首个非零值,否则用默认)
layer_dur = 0.0
for d in layer_transition_durations:
if d > 0:
layer_dur = d
break
xfade_filter, _ = self._transition_engine.build_xfade_chain(
clip_durations=layer_durations,
clip_video_labels=layer_labels,
transitions=layer_transitions,
transition_duration=layer_dur if layer_dur > 0 else None,
output_label=out_label,
# 判断是否全部为硬切:是则用 concat filter,否则用 xfade 转场链
all_cut = all(
t is None or t == "" or str(t).lower() == "cut"
for t in layer_transitions[1:] # 第一个 clip 的转场忽略
)
if xfade_filter:
filter_parts.append(xfade_filter)
if all_cut:
# 全硬切:用 concat filter,性能远优于 xfade
concat_inputs = "".join(f"[{label}]" for label in layer_labels)
filter_parts.append(
f"{concat_inputs}concat=n={len(layer_labels)}:v=1:a=0[{out_label}]"
)
logger.info(
"[unified-render] layer=%s clips=%d using concat (all hard-cut)",
layer.role,
len(layer_labels),
)
else:
# 有转场效果:用 TransitionEngine 构建 xfade 链
layer_dur = 0.0
for d in layer_transition_durations:
if d > 0:
layer_dur = d
break
xfade_filter, _ = self._transition_engine.build_xfade_chain(
clip_durations=layer_durations,
clip_video_labels=layer_labels,
transitions=layer_transitions,
transition_duration=layer_dur if layer_dur > 0 else None,
output_label=out_label,
)
if xfade_filter:
filter_parts.append(xfade_filter)
layer_output_labels[layer.role] = out_label
# Step 3: 合成各层
@@ -63,6 +63,18 @@ def compose_video(self, job_id: str, **kwargs):
resolver = get_render_engine_resolver()
user_id = job.created_by_user_id or None
engine = resolver.get_engine(user_id=user_id)
# 灰度期间打印详细 flag 配置,便于排查
config = resolver.get_config_snapshot()
logger.info(
"compose_video 引擎选择: job_id=%s engine=%s user_id=%s enabled=%s percentage=%s whitelist=%d default=%s",
job_id,
engine,
user_id,
config.get("enabled"),
config.get("percentage"),
len(config.get("whitelist", [])),
config.get("default_engine"),
)
if engine == "unified":
return _compose_with_unified_engine(self, job_service, job, plan_id, db)
@@ -77,9 +77,21 @@ def _resolve_render_engine(user_id: str) -> str:
from video_processing.render_engine_resolver import get_render_engine_resolver
resolver = get_render_engine_resolver()
return resolver.get_engine(user_id=user_id)
engine = resolver.get_engine(user_id=user_id)
# 灰度期间打印详细 flag 配置,便于排查
config = resolver.get_config_snapshot()
logger.info(
"edit_plan 引擎选择: user_id=%s engine=%s enabled=%s percentage=%s whitelist=%d default=%s",
user_id,
engine,
config.get("enabled"),
config.get("percentage"),
len(config.get("whitelist", [])),
config.get("default_engine"),
)
return engine
except Exception as exc:
logger.warning("获取渲染引擎配置失败,fallback 到 legacy: %s", exc)
logger.warning("获取渲染引擎配置失败,fallback 到 legacy: %s", exc, exc_info=True)
return "legacy"
+16 -3
View File
@@ -787,10 +787,23 @@ def _resolve_render_engine(user_id: str) -> str:
from video_processing.render_engine_resolver import get_render_engine_resolver
resolver = get_render_engine_resolver()
return resolver.get_engine(user_id=user_id)
engine = resolver.get_engine(user_id=user_id)
# 灰度期间打印详细 flag 配置,便于排查
config = resolver.get_config_snapshot()
logger.info(
"[渲染引擎] flag 解析: user_id=%s engine=%s enabled=%s percentage=%s whitelist=%d default=%s",
user_id,
engine,
config.get("enabled"),
config.get("percentage"),
len(config.get("whitelist", [])),
config.get("default_engine"),
)
return engine
except Exception as exc:
logger.warning("获取渲染引擎配置失败,fallback 到 unified: %s", exc)
return ENGINE_UNIFIED
# 异常时 fallback 到 legacy(保守策略,与 edit_plan_generation 一致)
logger.warning("获取渲染引擎配置失败,fallback 到 legacy: %s", exc, exc_info=True)
return ENGINE_LEGACY
# ── 旧引擎渲染(FFmpeg filter_complex) ────────────────────────────────────────
+10 -1
View File
@@ -665,7 +665,16 @@ class TestResponseSchema:
resp = client.get(f"/api/v1/edit-plans/{plan.id}/generation-status")
assert resp.status_code == 200
data = resp.json()
expected_keys = {"plan_id", "plan_status", "generation_task_id", "clips"}
expected_keys = {
"plan_id",
"plan_status",
"generation_task_id",
"generation_task_status",
"progress",
"video_url",
"error_message",
"clips",
}
assert set(data.keys()) == expected_keys
+34 -2
View File
@@ -45,6 +45,7 @@ class FakeClip:
start_time: float = 0.0
duration: float = 0.0
transition_effect: str = "cut"
transition_duration: float = 0.0
status: str = "ready"
config: dict[str, Any] = field(default_factory=dict)
@@ -65,6 +66,7 @@ def _make_clip(
asset_id: str = "",
duration: float = 0.0,
transition_effect: str = "cut",
transition_duration: float = 0.0,
config: dict[str, Any] | None = None,
) -> FakeClip:
return FakeClip(
@@ -74,6 +76,7 @@ def _make_clip(
asset_id=asset_id or f"asset_{clip_id}.mp4",
duration=duration,
transition_effect=transition_effect,
transition_duration=transition_duration,
config=config or {},
)
@@ -332,7 +335,7 @@ class TestBuildFilterComplex:
assert "[final_video]" in fc
def test_single_layer_multi_clips(self):
"""多个 main clips → xfade 串联。"""
"""多个 main clips(默认硬切)→ concat 串联。"""
clips = [
_make_clip("c1", "main", order=0, duration=3.0),
_make_clip("c2", "main", order=1, duration=3.0),
@@ -343,6 +346,35 @@ class TestBuildFilterComplex:
}
svc = _make_service(clips, asset_paths)
with _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0):
resolved = svc._resolve_clips()
layers = svc._group_clips_into_layers(resolved)
fc, input_args = svc._build_filter_complex(layers)
assert input_args.count("-i") == 2
# 全硬切场景走 concat filter(性能远优于 xfade
assert "concat=n=2:v=1:a=0" in fc
assert "[final_video]" in fc
def test_single_layer_multi_clips_with_transition(self):
"""多个 main clips 带转场效果 → xfade 串联。"""
clips = [
_make_clip("c1", "main", order=0, duration=3.0),
_make_clip(
"c2",
"main",
order=1,
duration=3.0,
transition_effect="fade",
transition_duration=0.5,
),
]
asset_paths = {
"asset_c1.mp4": Path("/tmp/asset_c1.mp4"),
"asset_c2.mp4": Path("/tmp/asset_c2.mp4"),
}
svc = _make_service(clips, asset_paths)
with _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0):
resolved = svc._resolve_clips()
layers = svc._group_clips_into_layers(resolved)
@@ -380,7 +412,7 @@ class TestBuildFilterComplex:
"""
clips = [
_make_clip("c1", "main", order=0, duration=3.0),
_make_clip("c2", "main", order=1, duration=5.0),
_make_clip("c2", "main", order=1, duration=5.0, transition_effect="fade", transition_duration=0.5),
]
asset_paths = {
"asset_c1.mp4": Path("/tmp/asset_c1.mp4"),