拆镜复刻、爆款开头复刻管理后台完成

This commit is contained in:
2026-06-17 15:42:07 +08:00
parent 5d5e6485ee
commit 0d647a7171
31 changed files with 3042 additions and 2777 deletions
@@ -341,33 +341,162 @@ def fill_none_with_wu(value: Any) -> Any:
return value
TOP_LEVEL_SCHEMA_KEYS = tuple(CLIENT_SCHEMA_V1.keys()) + ("动态时间规划", "输出规格限制")
OBJECT_FIELD_WHITELISTS: dict[str, set[str]] = {
key: set(value.keys())
for key, value in CLIENT_SCHEMA_V1.items()
if isinstance(value, dict)
}
OBJECT_FIELD_WHITELISTS.setdefault("画面属性", set()).add("推荐分辨率")
OBJECT_FIELD_WHITELISTS["输出规格限制"] = {"支持时长", "支持比例", "支持分辨率", "当前推荐分辨率"}
ACTION_FLOW_CONTENT_KEYS = ("动作内容", "动作", "动作说明", "内容", "说明", "主体动作", "动作变化")
CAMERA_FLOW_CONTENT_KEYS = ("镜头内容", "镜头", "镜头说明", "运镜", "运镜说明", "内容", "说明")
TIME_PLAN_ALLOWED_KEYS = ("时间段", "阶段", "说明")
PLACEHOLDER_FLOW_TEXTS = {
"展示主体动作、核心卖点或主要视觉内容",
"展示主要动作、核心卖点或主要视觉内容",
"展示核心卖点或主要视觉内容",
"展示主体动作",
"",
}
EMPTY_VALUE_TEXTS = {"", "", "null", "None", "none", "未提及", "不适用"}
def _clean_schema_text(value: Any) -> str:
if value is None:
return ""
if isinstance(value, (dict, list)):
try:
return json.dumps(value, ensure_ascii=False)
except Exception:
return str(value)
return str(value).strip()
def _is_empty_schema_value(value: Any) -> bool:
return _clean_schema_text(value) in EMPTY_VALUE_TEXTS
def _normalize_schema_object(section_key: str, value: Any) -> dict[str, Any]:
default_value = CLIENT_SCHEMA_V1.get(section_key)
if not isinstance(default_value, dict):
default_value = {}
source = value if isinstance(value, dict) else {}
merged = copy.deepcopy(default_value)
allowed_keys = OBJECT_FIELD_WHITELISTS.get(section_key, set(default_value.keys()))
for field_key in allowed_keys:
if field_key in source:
merged[field_key] = fill_none_with_wu(source.get(field_key))
return merged
def normalize_top_level_schema_fields(result: dict[str, Any]) -> dict[str, Any]:
"""按客户端视频提词 schema 白名单清洗顶层和普通对象字段。
AI 偶尔会把解释性文本作为 JSON key 输出。普通对象中的非预设字段直接过滤,
动作流程/镜头流程这类列表字段在后续 flow normalize 中单独处理。
"""
source = result if isinstance(result, dict) else {}
normalized: dict[str, Any] = {}
for key in TOP_LEVEL_SCHEMA_KEYS:
if key in OBJECT_FIELD_WHITELISTS:
normalized[key] = _normalize_schema_object(key, source.get(key))
elif key in source:
normalized[key] = fill_none_with_wu(source.get(key))
elif key in CLIENT_SCHEMA_V1:
normalized[key] = copy.deepcopy(CLIENT_SCHEMA_V1[key])
for key, default_value in CLIENT_SCHEMA_V1.items():
if key not in normalized:
normalized[key] = copy.deepcopy(default_value)
return normalized
def ensure_top_keys(result: dict[str, Any]) -> dict[str, Any]:
schema = copy.deepcopy(CLIENT_SCHEMA_V1)
for key, default_value in schema.items():
if key not in result:
result[key] = default_value
elif isinstance(default_value, dict) and isinstance(result.get(key), dict):
merged = copy.deepcopy(default_value)
merged.update(result[key])
result[key] = merged
return result
return normalize_top_level_schema_fields(result)
def _pick_flow_content(item: dict[str, Any], content_keys: tuple[str, ...]) -> str:
for key in content_keys:
if key in item and not _is_empty_schema_value(item.get(key)):
return _clean_schema_text(item.get(key))
return ""
def _collect_extra_flow_texts(
item: dict[str, Any],
*,
content_keys: tuple[str, ...],
allowed_keys: set[str],
) -> list[str]:
extras: list[str] = []
for key, value in item.items():
if key in allowed_keys or key in content_keys:
continue
key_text = _clean_schema_text(key)
value_text = _clean_schema_text(value)
if key_text and key_text not in EMPTY_VALUE_TEXTS:
extras.append(key_text)
if value_text and value_text not in EMPTY_VALUE_TEXTS and value_text != key_text:
extras.append(value_text)
# 去重但保留顺序,避免 AI 重复写入同一句。
deduped: list[str] = []
for text in extras:
if text not in deduped:
deduped.append(text)
return deduped
def _merge_flow_content(base_content: str, extra_texts: list[str], fallback: str) -> str:
base_content = _clean_schema_text(base_content)
if extra_texts and (not base_content or base_content in PLACEHOLDER_FLOW_TEXTS or base_content == fallback):
return "".join(extra_texts)
parts = [base_content] if base_content and base_content not in EMPTY_VALUE_TEXTS else []
for text in extra_texts:
if text and text not in parts:
parts.append(text)
return "".join(parts) if parts else fallback
def _normalize_flow_item(
raw_item: Any,
*,
plan_item: dict[str, str],
content_key: str,
content_keys: tuple[str, ...],
) -> dict[str, str]:
item = raw_item if isinstance(raw_item, dict) else {}
allowed_keys = {"时间段", content_key}
base_content = _pick_flow_content(item, content_keys)
extra_texts = _collect_extra_flow_texts(item, content_keys=content_keys, allowed_keys=allowed_keys)
fallback = plan_item.get("说明") or ""
return {
"时间段": plan_item.get("时间段") or _clean_schema_text(item.get("时间段")) or "",
content_key: _merge_flow_content(base_content, extra_texts, fallback),
}
def _normalize_time_plan(plan: list[dict[str, str]], value: Any) -> list[dict[str, str]]:
source = value if isinstance(value, list) else []
normalized: list[dict[str, str]] = []
for index, plan_item in enumerate(plan):
raw_item = source[index] if index < len(source) and isinstance(source[index], dict) else {}
item: dict[str, str] = {
"时间段": plan_item.get("时间段") or _clean_schema_text(raw_item.get("时间段")) or "",
"阶段": _clean_schema_text(raw_item.get("阶段")) or plan_item.get("阶段") or "",
"说明": _clean_schema_text(raw_item.get("说明")) or plan_item.get("说明") or "",
}
# 动态时间规划只保留 时间段/阶段/说明,多余字段不入库。
normalized.append(item)
return normalized
def ensure_flow_matches_time_plan(result: dict[str, Any], duration: int) -> dict[str, Any]:
plan = build_time_plan(duration)
if not isinstance(result.get("动作流程"), list) or not result["动作流程"]:
result["动作流程"] = [
{"时间段": item["时间段"], "动作内容": item["说明"]}
for item in plan
]
if not isinstance(result.get("镜头流程"), list) or not result["镜头流程"]:
result["镜头流程"] = [
{"时间段": item["时间段"], "镜头内容": item["说明"]}
for item in plan
]
result["动作流程"] = _align_flow_time_ranges(result["动作流程"], plan, "动作内容")
result["镜头流程"] = _align_flow_time_ranges(result["镜头流程"], plan, "镜头内容")
result["动态时间规划"] = plan
result["动作流程"] = _align_flow_time_ranges(result.get("动作流程"), plan, "动作内容")
result["镜头流程"] = _align_flow_time_ranges(result.get("镜头流程"), plan, "镜头内容")
result["动态时间规划"] = _normalize_time_plan(plan, result.get("动态时间规划"))
return result
@@ -462,16 +591,20 @@ def clean_final_prompt_specs(schema: dict[str, Any], video_config: dict[str, Any
return schema
def _align_flow_time_ranges(flow: Any, plan: list[dict[str, str]], default_content_key: str) -> list[dict[str, Any]]:
def _align_flow_time_ranges(flow: Any, plan: list[dict[str, str]], default_content_key: str) -> list[dict[str, str]]:
source = flow if isinstance(flow, list) else []
aligned: list[dict[str, Any]] = []
content_keys = ACTION_FLOW_CONTENT_KEYS if default_content_key == "动作内容" else CAMERA_FLOW_CONTENT_KEYS
aligned: list[dict[str, str]] = []
for index, plan_item in enumerate(plan):
old_item = source[index] if index < len(source) and isinstance(source[index], dict) else {}
item = dict(old_item)
item["时间段"] = plan_item["时间段"]
if not any(k in item and str(item.get(k)).strip() for k in (default_content_key, "动作", "镜头", "说明", "内容")):
item[default_content_key] = plan_item["说明"]
aligned.append(item)
raw_item = source[index] if index < len(source) else {}
aligned.append(
_normalize_flow_item(
raw_item,
plan_item=plan_item,
content_key=default_content_key,
content_keys=content_keys,
)
)
return aligned