Files
xiaoxia-saas/apps/api/app/services/generation_common.py
T
xiaoxia 14580f536a
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 13s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 24s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m27s
CI/CD Pipeline / Integration Tests (push) Successful in 3m38s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m45s
CI/CD Pipeline / Validate - Style (push) Successful in 4m14s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m42s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m58s
CI/CD Pipeline / Validate - Security (push) Successful in 8m1s
CI/CD Pipeline / Unit Tests (push) Successful in 9m0s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m32s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m6s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m52s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m36s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 5m31s
AI Code Review / AI Code Review (pull_request) Successful in 6m21s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 7m48s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 51s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m4s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 53h34m26s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 53h42m14s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 53h42m15s
CI/CD Pipeline / Deploy Production (push) Failing after 59h59m30s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 60h8m2s
CI/CD Pipeline / PR Build Web Image (push) Failing after 60h8m44s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 53h41m47s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 53h41m49s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 53h41m50s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 53h41m50s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 53h41m50s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 53h41m55s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 53h34m0s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 53h34m0s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 53h33m56s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 53h33m56s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 53h41m43s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 53h41m43s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 53h41m43s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 53h41m49s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 53h41m47s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 60h8m17s
CI/CD Pipeline / PR Build API Image (push) Failing after 60h8m18s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 60h7m36s
CI/CD Pipeline / CI Gate (push) Failing after 59h59m9s
CI/CD Pipeline / Build Production Web Image (push) Failing after 59h59m9s
CI/CD Pipeline / Canary Release to Production (push) Failing after 59h59m4s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 59h59m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 60h7m36s
CI/CD Pipeline / Frontend Lint (push) Failing after 60h8m10s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 60h8m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 54h17m5s
CI/CD Pipeline / Build Production API Image (push) Failing after 60h34m30s
refactor(backend): 智能剪辑后端公共逻辑抽取+解耦 (#1884)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-14 03:15:00 +08:00

176 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""智能剪辑公共服务辅助函数(从 route 层下沉)。
集中管理:
- query_voice_durations:批量查询配音素材时长
- writeback_edit_plan_config:任务入队后回写 EditPlan.config
- collect_plan_segments:分页读取 plan clips 构建素材区间表(变体避让用)
- resolve_latest_plan_by_template:按 template_id + user_id 查最新 EditPlan
设计原则:
- 无副作用的纯查询 / 幂等写回;失败一律不阻断主流程(记日志 + 返回安全默认值)
- 不依赖 FastAPI / HTTPException,便于 service 层和 worker 复用
"""
from __future__ import annotations
import logging
from typing import Any, Optional
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
def query_voice_durations(db: Session, voice_ids: list[str]) -> list[float]:
"""批量查询配音素材时长(秒),#1749 配音时长分配用。
逐项 try/float 硬化:MagicMock/异常/缺失 → 0.0(无配音不分配,不阻断)。
#1855 P0修复:不再对 voice_ids 去重,保持与调用方传入顺序/长度一致,
允许同配音id多次出现时返回相同时长(支持"同配音N变体"的时长对齐)。
"""
raw_ids = list(voice_ids or [])
if not raw_ids:
return []
unique_ids: list[str] = []
_seen: set[str] = set()
for v in raw_ids:
if v and v not in _seen:
_seen.add(v)
unique_ids.append(v)
if not unique_ids:
return [0.0 for _ in raw_ids]
try:
from packages.adapters.sqlalchemy_impl.models import AssetModel
rows = db.query(AssetModel.id, AssetModel.duration).filter(AssetModel.id.in_(unique_ids)).all()
dur_map: dict[str, float] = {}
for row in rows:
try:
dur_map[row[0]] = float(row[1] or 0.0)
except (TypeError, ValueError):
dur_map[row[0]] = 0.0
return [dur_map.get(v, 0.0) if v else 0.0 for v in raw_ids]
except Exception:
logger.warning("[generation_common] 配音时长查询失败(按无配音处理,不阻断)", exc_info=True)
return [0.0 for _ in raw_ids]
def writeback_edit_plan_config(
plan_id: str,
task_id: str,
title_config: dict | None,
db: Session,
) -> None:
"""任务入队成功后,回写 EditPlan.config:generation_task_id + title_config。
用 merge 方式更新,不整体覆盖 config,避免丢失其他字段。
失败只记日志,不影响任务创建。
"""
if not plan_id:
return
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
plan_model = db.query(EditPlanModel).filter(EditPlanModel.id == plan_id).first()
if plan_model is None:
logger.warning("[generation_common] 回写plan.config失败: plan不存在 plan_id=%s", plan_id)
return
current_config = plan_model.config if isinstance(plan_model.config, dict) else {}
merged = dict(current_config)
merged["generation_task_id"] = task_id
if title_config:
old_title_config = merged.get("title_config", {}) or {}
old_title_text = (old_title_config.get("text") or "").strip()
new_title_text = (title_config.get("text") or "").strip()
if old_title_text != new_title_text:
if "cover" in merged:
del merged["cover"]
logger.info(
"[generation_common] 标题变化,清除旧封面: plan_id=%s old_title=%s new_title=%s",
plan_id,
old_title_text,
new_title_text,
)
merged["title_config"] = title_config
plan_model.config = merged
db.commit()
logger.info(
"[generation_common] 回写plan.config成功: plan_id=%s task_id=%s keys=%s",
plan_id,
task_id,
list(merged.keys()),
)
except Exception as e:
logger.warning(
"[generation_common] 回写plan.config异常(不影响任务创建): plan_id=%s error=%s",
plan_id,
e,
exc_info=True,
)
try:
db.rollback()
except Exception:
pass
def collect_plan_segments(
plan_id: str,
clip_repo: Any,
*,
page_size: int = 500,
) -> dict[str, list[tuple[float, float]]]:
"""分页读取 plan 所有 clips,构建 {asset_id: [(start, end), ...]} 素材区间表。
用于 #1855 P0 批次内素材区间避让(变体间素材片段重叠控制)。
"""
segs: dict[str, list[tuple[float, float]]] = {}
sk, pg = 0, page_size
while True:
batch = clip_repo.list_by_plan(plan_id, skip=sk, limit=pg)
if not batch:
break
for c in batch:
if c.asset_id and float(c.duration or 0) > 0:
st = float(c.start_time or 0.0)
segs.setdefault(c.asset_id, []).append((st, st + float(c.duration)))
if len(batch) < pg:
break
sk += pg
return segs
def resolve_latest_plan_by_template(
db: Session,
*,
template_id: str,
user_id: str,
) -> Optional[str]:
"""按 template_id + user_id 查找最新的 EditPlan.id(模板兜底用)。找不到返回 None。"""
if not (template_id or "").strip():
return None
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
latest = (
db.query(EditPlanModel)
.filter(
EditPlanModel.template_id == template_id.strip(),
EditPlanModel.created_by_user_id == user_id,
)
.order_by(EditPlanModel.created_at.desc())
.first()
)
return latest.id if latest else None
except Exception:
logger.warning(
"[generation_common] 按template查找最新plan失败: template=%s user=%s",
template_id,
user_id,
exc_info=True,
)
return None