视频提词优化管理后台配置BUG修复
This commit is contained in:
-407
File diff suppressed because one or more lines are too long
+407
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-CYRc4RUo.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-Te6nFn2L.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -58,6 +58,17 @@ const NODE_TYPES: { label: string; value: VideoPromptSchemaNodeType }[] = [
|
||||
{ label: '流程数组', value: 'flow' },
|
||||
];
|
||||
|
||||
const VIDEO_SPEC_SECTION_KEY = '画面属性';
|
||||
const VIDEO_SPEC_LOCKED_FIELDS = new Set(['视频时长', '视频比例', '清晰度', '帧率', '推荐分辨率']);
|
||||
|
||||
function isLockedVideoSpecField(sectionKey: string | undefined, fieldKey: string | undefined): boolean {
|
||||
return sectionKey === VIDEO_SPEC_SECTION_KEY && !!fieldKey && VIDEO_SPEC_LOCKED_FIELDS.has(fieldKey);
|
||||
}
|
||||
|
||||
function isLockedVideoSpecSection(sectionKey: string | undefined): boolean {
|
||||
return sectionKey === VIDEO_SPEC_SECTION_KEY;
|
||||
}
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value ?? null));
|
||||
}
|
||||
@@ -137,17 +148,19 @@ function normalizeConfig(data: any): VideoPromptSchemaConfigData {
|
||||
};
|
||||
}
|
||||
|
||||
function toApiNode(node: VideoPromptSchemaNode): any {
|
||||
function toApiNode(node: VideoPromptSchemaNode, parentKey?: string): any {
|
||||
const lockedSection = isLockedVideoSpecSection(node.key);
|
||||
const lockedField = isLockedVideoSpecField(parentKey, node.key);
|
||||
const base: any = {
|
||||
key: node.key,
|
||||
label: node.label,
|
||||
type: node.type,
|
||||
enabled: !!node.enabled,
|
||||
editable: !!node.editable,
|
||||
type: lockedField ? 'string' : node.type,
|
||||
enabled: lockedSection || lockedField ? true : !!node.enabled,
|
||||
editable: lockedField ? false : !!node.editable,
|
||||
max_length: node.maxLength,
|
||||
};
|
||||
if (node.type === 'object') {
|
||||
base.children = (node.children || []).map(toApiNode);
|
||||
base.children = (node.children || []).map(child => toApiNode(child, node.key));
|
||||
} else if (node.type === 'flow') {
|
||||
base.content_key = node.contentKey;
|
||||
base.content_aliases = node.contentAliases || [];
|
||||
@@ -169,7 +182,7 @@ function toApiConfig(config: VideoPromptSchemaConfigData, enabled: boolean): any
|
||||
return {
|
||||
version: config.version,
|
||||
enabled,
|
||||
sections: (config.sections || []).map(toApiNode),
|
||||
sections: (config.sections || []).map(section => toApiNode(section)),
|
||||
time_plan_rules: (config.timePlanRules || []).map(rule => ({
|
||||
min_duration: rule.minDuration,
|
||||
max_duration: rule.maxDuration,
|
||||
@@ -364,6 +377,12 @@ const AdminVideoPromptSchemaConfig: React.FC = () => {
|
||||
|
||||
const updateSection = (index: number, patch: Partial<VideoPromptSchemaNode>) => {
|
||||
updateConfig(draft => {
|
||||
const current = draft.sections[index];
|
||||
if (isLockedVideoSpecSection(current?.key)) {
|
||||
delete patch.key;
|
||||
delete patch.type;
|
||||
delete patch.enabled;
|
||||
}
|
||||
draft.sections[index] = { ...draft.sections[index], ...patch };
|
||||
if (patch.type === 'object') {
|
||||
draft.sections[index].children = draft.sections[index].children || [];
|
||||
@@ -392,7 +411,15 @@ const AdminVideoPromptSchemaConfig: React.FC = () => {
|
||||
|
||||
const updateChild = (sectionIndex: number, childIndex: number, patch: Partial<VideoPromptSchemaNode>) => {
|
||||
updateConfig(draft => {
|
||||
const children = draft.sections[sectionIndex].children || [];
|
||||
const section = draft.sections[sectionIndex];
|
||||
const children = section.children || [];
|
||||
const child = children[childIndex];
|
||||
if (isLockedVideoSpecField(section?.key, child?.key)) {
|
||||
delete patch.key;
|
||||
delete patch.type;
|
||||
delete patch.enabled;
|
||||
patch.editable = false;
|
||||
}
|
||||
children[childIndex] = { ...children[childIndex], ...patch };
|
||||
draft.sections[sectionIndex].children = children;
|
||||
});
|
||||
@@ -470,29 +497,30 @@ const AdminVideoPromptSchemaConfig: React.FC = () => {
|
||||
};
|
||||
|
||||
const sectionItems = useMemo(() => sections.map((section, sectionIndex) => ({
|
||||
key: `${section.key}-${sectionIndex}`,
|
||||
key: `section-${sectionIndex}`,
|
||||
label: (
|
||||
<Space>
|
||||
<Text strong>{section.label || section.key || '未命名分组'}</Text>
|
||||
{section.key && section.key !== section.label && <Tag color="blue">key: {section.key}</Tag>}
|
||||
<Tag>{section.type}</Tag>
|
||||
{!section.enabled && <Tag color="default">已禁用</Tag>}
|
||||
{isLockedVideoSpecSection(section.key) && <Tag color="red">系统锁定</Tag>}
|
||||
{!section.enabled && !isLockedVideoSpecSection(section.key) && <Tag color="default">已禁用</Tag>}
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
<Row gutter={12} align="middle">
|
||||
<Col span={4}><Input addonBefore="key" value={section.key} maxLength={64} placeholder="传给 AI 的字段名" onChange={e => updateSection(sectionIndex, { key: e.target.value })} /></Col>
|
||||
<Col span={4}><Input addonBefore="key" value={section.key} maxLength={64} placeholder="传给 AI 的字段名" disabled={isLockedVideoSpecSection(section.key)} onChange={e => updateSection(sectionIndex, { key: e.target.value })} /></Col>
|
||||
<Col span={4}><Input addonBefore="label" value={section.label} maxLength={64} placeholder="后台显示名称" onChange={e => updateSection(sectionIndex, { label: e.target.value })} /></Col>
|
||||
<Col span={4}>
|
||||
<Select value={section.type as any} options={NODE_TYPES} style={{ width: '100%' }} onChange={value => updateSection(sectionIndex, { type: value })} />
|
||||
<Select value={section.type as any} options={NODE_TYPES} style={{ width: '100%' }} disabled={isLockedVideoSpecSection(section.key)} onChange={value => updateSection(sectionIndex, { type: value })} />
|
||||
</Col>
|
||||
<Col span={3}><Space>启用<Switch checked={section.enabled} onChange={checked => updateSection(sectionIndex, { enabled: checked })} /></Space></Col>
|
||||
<Col span={3}><Space>启用<Switch checked={isLockedVideoSpecSection(section.key) ? true : section.enabled} disabled={isLockedVideoSpecSection(section.key)} onChange={checked => updateSection(sectionIndex, { enabled: checked })} /></Space></Col>
|
||||
<Col span={3}><Space>可编辑<Switch checked={section.editable} onChange={checked => updateSection(sectionIndex, { editable: checked })} /></Space></Col>
|
||||
<Col span={4}><InputNumber min={1} max={20000} value={section.maxLength} addonBefore="长度" style={{ width: '100%' }} onChange={value => updateSection(sectionIndex, { maxLength: Number(value || 1000) })} /></Col>
|
||||
<Col span={2} style={{ textAlign: 'right' }}>
|
||||
<Popconfirm title="确认删除该分组?" onConfirm={() => deleteSection(sectionIndex)}>
|
||||
<Button danger icon={<DeleteOutlined />} />
|
||||
<Popconfirm title="确认删除该分组?" disabled={isLockedVideoSpecSection(section.key)} onConfirm={() => deleteSection(sectionIndex)}>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={isLockedVideoSpecSection(section.key)} />
|
||||
</Popconfirm>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -501,18 +529,18 @@ const AdminVideoPromptSchemaConfig: React.FC = () => {
|
||||
<Card size="small" title="对象字段" extra={<Button size="small" icon={<PlusOutlined />} onClick={() => addChild(sectionIndex)}>新增字段</Button>}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{(section.children || []).map((child, childIndex) => (
|
||||
<Row gutter={8} align="middle" key={`${child.key}-${childIndex}`}>
|
||||
<Col span={4}><Input addonBefore="key" value={child.key} maxLength={64} placeholder="传给 AI 的字段名" onChange={e => updateChild(sectionIndex, childIndex, { key: e.target.value })} /></Col>
|
||||
<Row gutter={8} align="middle" key={`child-${sectionIndex}-${childIndex}`}>
|
||||
<Col span={4}><Input addonBefore="key" value={child.key} maxLength={64} placeholder="传给 AI 的字段名" disabled={isLockedVideoSpecField(section.key, child.key)} onChange={e => updateChild(sectionIndex, childIndex, { key: e.target.value })} /></Col>
|
||||
<Col span={4}><Input addonBefore="label" value={child.label} maxLength={64} placeholder="后台显示名称" onChange={e => updateChild(sectionIndex, childIndex, { label: e.target.value })} /></Col>
|
||||
<Col span={3}><Select value={child.type as any} options={NODE_TYPES.filter(item => item.value !== 'object' && item.value !== 'flow')} style={{ width: '100%' }} onChange={value => updateChild(sectionIndex, childIndex, { type: value })} /></Col>
|
||||
<Col span={2}><Space>启用<Switch checked={child.enabled} onChange={checked => updateChild(sectionIndex, childIndex, { enabled: checked })} /></Space></Col>
|
||||
<Col span={2}><Space>编辑<Switch checked={child.editable} onChange={checked => updateChild(sectionIndex, childIndex, { editable: checked })} /></Space></Col>
|
||||
<Col span={3}><Select value={child.type as any} options={NODE_TYPES.filter(item => item.value !== 'object' && item.value !== 'flow')} style={{ width: '100%' }} disabled={isLockedVideoSpecField(section.key, child.key)} onChange={value => updateChild(sectionIndex, childIndex, { type: value })} /></Col>
|
||||
<Col span={2}><Space>启用<Switch checked={isLockedVideoSpecField(section.key, child.key) ? true : child.enabled} disabled={isLockedVideoSpecField(section.key, child.key)} onChange={checked => updateChild(sectionIndex, childIndex, { enabled: checked })} /></Space></Col>
|
||||
<Col span={2}><Space>编辑<Switch checked={isLockedVideoSpecField(section.key, child.key) ? false : child.editable} disabled={isLockedVideoSpecField(section.key, child.key)} onChange={checked => updateChild(sectionIndex, childIndex, { editable: checked })} /></Space></Col>
|
||||
<Col span={3}><InputNumber min={1} max={20000} value={child.maxLength} addonBefore="长度" style={{ width: '100%' }} onChange={value => updateChild(sectionIndex, childIndex, { maxLength: Number(value || 1000) })} /></Col>
|
||||
<Col span={5}>
|
||||
<Input value={Array.isArray(child.value) ? child.value.join('/') : String(child.value ?? '')} maxLength={child.maxLength || 1000} placeholder="默认值" onChange={e => updateChild(sectionIndex, childIndex, { value: child.type === 'array' ? e.target.value.split('/').filter(Boolean) : e.target.value })} />
|
||||
</Col>
|
||||
<Col span={1} style={{ textAlign: 'right' }}>
|
||||
<Button danger size="small" icon={<DeleteOutlined />} onClick={() => deleteChild(sectionIndex, childIndex)} />
|
||||
<Button danger size="small" icon={<DeleteOutlined />} disabled={isLockedVideoSpecField(section.key, child.key)} onClick={() => deleteChild(sectionIndex, childIndex)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
@@ -528,7 +556,7 @@ const AdminVideoPromptSchemaConfig: React.FC = () => {
|
||||
</Row>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{(section.itemFields || []).map((field, fieldIndex) => (
|
||||
<Row gutter={8} align="middle" key={`${field.key}-${fieldIndex}`}>
|
||||
<Row gutter={8} align="middle" key={`flow-${sectionIndex}-${fieldIndex}`}>
|
||||
<Col span={5}><Input addonBefore="key" value={field.key} maxLength={64} placeholder="流程对象字段名" onChange={e => updateFlowField(sectionIndex, fieldIndex, { key: e.target.value })} /></Col>
|
||||
<Col span={5}><Input addonBefore="label" value={field.label} maxLength={64} placeholder="后台显示名称" onChange={e => updateFlowField(sectionIndex, fieldIndex, { label: e.target.value })} /></Col>
|
||||
<Col span={3}><Space>启用<Switch checked={field.enabled} onChange={checked => updateFlowField(sectionIndex, fieldIndex, { enabled: checked })} /></Space></Col>
|
||||
@@ -612,7 +640,7 @@ const AdminVideoPromptSchemaConfig: React.FC = () => {
|
||||
type="warning"
|
||||
showIcon
|
||||
message="字段说明"
|
||||
description="key 是最终传给 AI 和保存到 prompt_schema 的真实字段名,例如「素材理解」「动作流程」;label 只用于管理后台显示,帮助运营/管理员理解这个字段,不会传给 AI;启用关闭后该字段不会进入运行时 schema;可编辑关闭后用户在第 4 步修改时不能改这个字段。"
|
||||
description="key 是最终传给 AI 和保存到 prompt_schema 的真实字段名,例如「素材理解」「动作流程」;label 只用于管理后台显示,帮助运营/管理员理解这个字段,不会传给 AI;启用关闭后该字段不会进入运行时 schema;可编辑关闭后用户在第 4 步修改时不能改这个字段。「画面属性」里的视频时长、视频比例、清晰度、帧率、推荐分辨率属于系统锁定参数,不能改 key、不能禁用、不能开放用户编辑。"
|
||||
/>
|
||||
<Button icon={<PlusOutlined />} onClick={addSection}>新增一级分组</Button>
|
||||
<Collapse items={sectionItems} />
|
||||
|
||||
@@ -28,6 +28,13 @@ from app.services.resource_signed_url_service import build_resource_signed_url
|
||||
DEFAULT_FRAME_RATE = "30fps"
|
||||
DEFAULT_REFERENCE_VIDEO_FPS = 1
|
||||
|
||||
# 后台配置可以控制业务字段是否启用/可编辑,但视频规格字段属于接口参数,
|
||||
# 必须强制保留并锁定,避免管理端误删/禁用后导致第 4 步和第 5 步参数不一致。
|
||||
VIDEO_SPEC_SECTION_KEY = "画面属性"
|
||||
VIDEO_SPEC_LOCKED_FIELDS = {"视频时长", "视频比例", "清晰度", "帧率", "推荐分辨率"}
|
||||
ALWAYS_ALLOWED_TOP_LEVEL_KEYS = {"schema_version", "schema_usage", "动态时间规划", "输出规格限制", VIDEO_SPEC_SECTION_KEY}
|
||||
TIME_PLAN_PLACEHOLDER_TEXTS = {"", "无", "阶段说明", "说明", "null", "None", "none", "未提及", "不适用"}
|
||||
|
||||
CLIENT_SCHEMA_V1: dict[str, Any] = {
|
||||
"schema_version": PromptSchemaVersionEnum.CLIENT_V1.value,
|
||||
"schema_usage": VideoPromptSchemaUsageEnum.CLIENT_DISPLAY.value,
|
||||
@@ -332,11 +339,14 @@ def normalize_video_prompt_schema_config(config: Any | None) -> dict[str, Any]:
|
||||
if not key:
|
||||
continue
|
||||
section_type = str(section.get("type") or "object")
|
||||
if key == VIDEO_SPEC_SECTION_KEY:
|
||||
section_type = "object"
|
||||
|
||||
item: dict[str, Any] = {
|
||||
"key": key,
|
||||
"label": _truncate_for_config(section.get("label") or key, 64),
|
||||
"type": section_type,
|
||||
"enabled": _normalize_bool(section.get("enabled"), True),
|
||||
"enabled": True if key == VIDEO_SPEC_SECTION_KEY else _normalize_bool(section.get("enabled"), True),
|
||||
"editable": _normalize_bool(section.get("editable"), True),
|
||||
"max_length": _normalize_int(section.get("max_length"), VIDEO_SCHEMA_EDITABLE_TEXT_MAX_LEN, min_value=1, max_value=20000),
|
||||
}
|
||||
@@ -349,17 +359,33 @@ def normalize_video_prompt_schema_config(config: Any | None) -> dict[str, Any]:
|
||||
child_key = _truncate_for_config(child.get("key") or child.get("label"), 64)
|
||||
if not child_key:
|
||||
continue
|
||||
is_locked_video_spec = key == VIDEO_SPEC_SECTION_KEY and child_key in VIDEO_SPEC_LOCKED_FIELDS
|
||||
item["children"].append(
|
||||
{
|
||||
"key": child_key,
|
||||
"label": _truncate_for_config(child.get("label") or child_key, 64),
|
||||
"type": str(child.get("type") or _schema_value_type(child.get("value"))),
|
||||
"enabled": _normalize_bool(child.get("enabled"), True),
|
||||
"editable": _normalize_bool(child.get("editable"), True),
|
||||
"max_length": _normalize_int(child.get("max_length"), VIDEO_SCHEMA_EDITABLE_TEXT_MAX_LEN, min_value=1, max_value=20000),
|
||||
"enabled": True if is_locked_video_spec else _normalize_bool(child.get("enabled"), True),
|
||||
"editable": False if is_locked_video_spec else _normalize_bool(child.get("editable"), True),
|
||||
"max_length": _normalize_int(child.get("max_length") or child.get("maxLength"), VIDEO_SCHEMA_EDITABLE_TEXT_MAX_LEN, min_value=1, max_value=20000),
|
||||
"value": copy.deepcopy(child.get("value", "无")),
|
||||
}
|
||||
)
|
||||
if key == VIDEO_SPEC_SECTION_KEY:
|
||||
existing_child_keys = {str(child.get("key") or "") for child in item["children"] if isinstance(child, dict)}
|
||||
for locked_key in ("视频时长", "视频比例", "清晰度", "帧率", "推荐分辨率"):
|
||||
if locked_key not in existing_child_keys:
|
||||
item["children"].append(
|
||||
{
|
||||
"key": locked_key,
|
||||
"label": locked_key,
|
||||
"type": "string",
|
||||
"enabled": True,
|
||||
"editable": False,
|
||||
"max_length": VIDEO_SCHEMA_EDITABLE_TEXT_MAX_LEN,
|
||||
"value": "无",
|
||||
}
|
||||
)
|
||||
elif section_type == "flow":
|
||||
content_key = _truncate_for_config(section.get("content_key") or section.get("contentKey") or ("镜头内容" if key == "镜头流程" else "动作内容"), 64)
|
||||
aliases = section.get("content_aliases") or section.get("contentAliases")
|
||||
@@ -542,6 +568,27 @@ def build_time_plan(duration: int, schema_config_snapshot: Any | None = None) ->
|
||||
)
|
||||
|
||||
|
||||
def _time_plan_schema_for_ai_input(plan: list[dict[str, str]]) -> list[dict[str, str]]:
|
||||
"""
|
||||
AI 入参 schema 只固定时间段和字段结构,不把后台配置的阶段/说明当成最终分析结果。
|
||||
阶段/说明应由 AI 结合参考素材和新项目重新填写,后续归一化仅在 AI 未填写时兜底。
|
||||
"""
|
||||
result: list[dict[str, str]] = []
|
||||
for item in plan:
|
||||
result.append(
|
||||
{
|
||||
"时间段": item.get("时间段") or "无",
|
||||
"阶段": "",
|
||||
"说明": "",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _time_plan_time_ranges_for_ai(plan: list[dict[str, str]]) -> list[dict[str, str]]:
|
||||
return [{"时间段": item.get("时间段") or "无"} for item in plan]
|
||||
|
||||
|
||||
def build_dynamic_schema(video_config: dict[str, Any], schema_config_snapshot: Any | None = None) -> dict[str, Any]:
|
||||
schema = build_client_schema_from_config(schema_config_snapshot)
|
||||
duration = int(video_config["duration"])
|
||||
@@ -564,11 +611,11 @@ def build_dynamic_schema(video_config: dict[str, Any], schema_config_snapshot: A
|
||||
}
|
||||
)
|
||||
time_plan = build_time_plan(duration, schema_config_snapshot)
|
||||
schema["动态时间规划"] = time_plan
|
||||
schema["动态时间规划"] = _time_plan_schema_for_ai_input(time_plan)
|
||||
|
||||
# 预览/AI 入参阶段也要把流程数组按秒数切片规则初始化出来。
|
||||
# 这样管理后台配置了 4 段/5 段规则后,动作流程、镜头流程会和动态时间规划保持相同长度,
|
||||
# AI 生成时也能明确知道每个时间段需要填哪个流程字段。
|
||||
# 预览/AI 入参阶段只锁定时间段和字段结构,不把后台阶段/说明/流程说明写死到 schema 值里。
|
||||
# 阶段、说明、动作内容、动作详解、镜头内容、镜头详解都应由 AI 基于素材分析填写;
|
||||
# 只有 AI 返回缺失/空/无/阶段说明等占位内容时,归一化阶段才使用后台规则兜底。
|
||||
for flow_key in ("动作流程", "镜头流程"):
|
||||
if flow_key not in schema:
|
||||
continue
|
||||
@@ -579,6 +626,7 @@ def build_dynamic_schema(video_config: dict[str, Any], schema_config_snapshot: A
|
||||
content_key,
|
||||
content_keys=_flow_content_aliases(schema_config_snapshot, flow_key),
|
||||
item_fields=_flow_item_field_rules(schema_config_snapshot, flow_key),
|
||||
fallback_to_plan=False,
|
||||
)
|
||||
|
||||
schema["输出规格限制"] = {
|
||||
@@ -660,9 +708,14 @@ def build_user_text(
|
||||
"参考素材": references,
|
||||
"输出要求": {
|
||||
"生成类型": infer_generation_type(references),
|
||||
"必须填充动态时间规划": build_time_plan(duration, schema_config_snapshot),
|
||||
"必须填充动作流程": "动作流程时间段必须覆盖完整视频时长",
|
||||
"必须填充镜头流程": "镜头流程时间段必须覆盖完整视频时长",
|
||||
"动态时间规划时间段必须严格等于": _time_plan_time_ranges_for_ai(build_time_plan(duration, schema_config_snapshot)),
|
||||
"动态时间规划兜底参考": build_time_plan(duration, schema_config_snapshot),
|
||||
"动态时间规划填写要求": "时间段必须和给定时间段一致;阶段、说明必须结合参考素材、新项目、核心内容点、动作和镜头重新分析填写;只有无法判断时才允许使用兜底参考;不能原样复制后台配置里的阶段说明。",
|
||||
"必须填充动作流程": "动作流程时间段必须覆盖完整视频时长,每一段都必须填写动作流程对象中的全部字段。",
|
||||
"动作流程字段要求": [field.get("key") for field in _flow_item_field_rules(schema_config_snapshot, "动作流程")],
|
||||
"必须填充镜头流程": "镜头流程时间段必须覆盖完整视频时长,每一段都必须填写镜头流程对象中的全部字段。",
|
||||
"镜头流程字段要求": [field.get("key") for field in _flow_item_field_rules(schema_config_snapshot, "镜头流程")],
|
||||
"字段启用规则": "只输出 schema 中存在的启用字段;不要输出已禁用字段;不要新增 schema 外字段。",
|
||||
"最终提示词限制": "最终提示词下所有字段都不能写入视频时长、秒数、视频比例、清晰度、分辨率、帧率、推荐像素、竖屏、横屏等视频规格参数,这些规格只能写在画面属性/动态时间规划/输出规格限制。",
|
||||
"禁止": ["输出 Markdown", "输出 schema 之外的解释文字", "照抄参考素材品牌水印", "生成违法违规内容", "在最终提示词中写入秒数/比例/分辨率/帧率"],
|
||||
},
|
||||
@@ -917,6 +970,7 @@ def _normalize_flow_item(
|
||||
content_key: str,
|
||||
content_keys: tuple[str, ...],
|
||||
item_fields: list[dict[str, Any]] | None = None,
|
||||
fallback_to_plan: bool = True,
|
||||
) -> dict[str, str]:
|
||||
item = raw_item if isinstance(raw_item, dict) else {}
|
||||
field_rules = item_fields or [{"key": content_key, "value": ""}]
|
||||
@@ -924,15 +978,27 @@ def _normalize_flow_item(
|
||||
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 "无"
|
||||
empty_fallback = "无" if fallback_to_plan else ""
|
||||
result: dict[str, str] = {"时间段": plan_item.get("时间段") or _clean_schema_text(item.get("时间段")) or "无"}
|
||||
for field in field_rules:
|
||||
field_key = str(field.get("key") or "").strip()
|
||||
if not field_key:
|
||||
continue
|
||||
if field_key == content_key:
|
||||
result[field_key] = _merge_flow_content(base_content, extra_texts, fallback)
|
||||
if fallback_to_plan:
|
||||
result[field_key] = _merge_flow_content(base_content, extra_texts, fallback)
|
||||
else:
|
||||
result[field_key] = _merge_flow_content(base_content, extra_texts, "")
|
||||
else:
|
||||
result[field_key] = _clean_schema_text(item.get(field_key)) or _clean_schema_text(field.get("value")) or "无"
|
||||
# AI 入参预览阶段不能把后台默认值写死到流程字段中;只输出字段结构。
|
||||
# AI 返回归一化阶段才允许在缺失/无时用字段默认值或通用占位兜底。
|
||||
raw_value = _clean_schema_text(item.get(field_key))
|
||||
if raw_value and raw_value not in EMPTY_VALUE_TEXTS and raw_value not in TIME_PLAN_PLACEHOLDER_TEXTS:
|
||||
result[field_key] = raw_value
|
||||
elif fallback_to_plan:
|
||||
result[field_key] = _clean_schema_text(field.get("value")) or "无"
|
||||
else:
|
||||
result[field_key] = empty_fallback
|
||||
return result
|
||||
|
||||
|
||||
@@ -941,18 +1007,25 @@ def _normalize_time_plan(plan: list[dict[str, str]], value: Any) -> list[dict[st
|
||||
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 {}
|
||||
raw_stage = _clean_schema_text(raw_item.get("阶段"))
|
||||
raw_desc = _clean_schema_text(raw_item.get("说明"))
|
||||
if raw_stage in TIME_PLAN_PLACEHOLDER_TEXTS:
|
||||
raw_stage = ""
|
||||
if raw_desc in TIME_PLAN_PLACEHOLDER_TEXTS:
|
||||
raw_desc = ""
|
||||
# 只有时间段由服务端锁定;阶段、说明优先保留 AI 结合素材分析后的结果。
|
||||
# 仅当 AI 未填写、填写“无/阶段说明/说明”等占位内容时,才使用后台时间切片规则兜底。
|
||||
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 "无",
|
||||
"阶段": raw_stage or plan_item.get("阶段") or "无",
|
||||
"说明": raw_desc or plan_item.get("说明") or "无",
|
||||
}
|
||||
# 动态时间规划只保留 时间段/阶段/说明,多余字段不入库。
|
||||
normalized.append(item)
|
||||
return normalized
|
||||
|
||||
|
||||
def ensure_flow_matches_time_plan(result: dict[str, Any], duration: int, schema_config_snapshot: Any | None = None) -> dict[str, Any]:
|
||||
plan = build_time_plan(duration, schema_config_snapshot)
|
||||
base_plan = build_time_plan(duration, schema_config_snapshot)
|
||||
plan = _normalize_time_plan(base_plan, result.get("动态时间规划"))
|
||||
action_key = _flow_content_key(schema_config_snapshot, "动作流程")
|
||||
camera_key = _flow_content_key(schema_config_snapshot, "镜头流程")
|
||||
result["动作流程"] = _align_flow_time_ranges(
|
||||
@@ -969,7 +1042,7 @@ def ensure_flow_matches_time_plan(result: dict[str, Any], duration: int, schema_
|
||||
content_keys=_flow_content_aliases(schema_config_snapshot, "镜头流程"),
|
||||
item_fields=_flow_item_field_rules(schema_config_snapshot, "镜头流程"),
|
||||
)
|
||||
result["动态时间规划"] = _normalize_time_plan(plan, result.get("动态时间规划"))
|
||||
result["动态时间规划"] = plan
|
||||
return result
|
||||
|
||||
|
||||
@@ -1071,6 +1144,7 @@ def _align_flow_time_ranges(
|
||||
*,
|
||||
content_keys: tuple[str, ...] | None = None,
|
||||
item_fields: list[dict[str, Any]] | None = None,
|
||||
fallback_to_plan: bool = True,
|
||||
) -> list[dict[str, str]]:
|
||||
source = flow if isinstance(flow, list) else []
|
||||
aliases = content_keys or (ACTION_FLOW_CONTENT_KEYS if default_content_key == "动作内容" else CAMERA_FLOW_CONTENT_KEYS)
|
||||
@@ -1084,6 +1158,7 @@ def _align_flow_time_ranges(
|
||||
content_key=default_content_key,
|
||||
content_keys=aliases,
|
||||
item_fields=item_fields,
|
||||
fallback_to_plan=fallback_to_plan,
|
||||
)
|
||||
)
|
||||
return aligned
|
||||
@@ -1178,6 +1253,26 @@ def _merge_flow_patch(base_flow: Any, patch_flow: Any, flow_key: str, schema_con
|
||||
return result
|
||||
|
||||
|
||||
def _prune_schema_by_runtime_config(schema: dict[str, Any], schema_config_snapshot: Any | None = None) -> dict[str, Any]:
|
||||
default_schema = build_client_schema_from_config(schema_config_snapshot)
|
||||
allowed_top_keys = set(default_schema.keys()) | ALWAYS_ALLOWED_TOP_LEVEL_KEYS
|
||||
pruned: dict[str, Any] = {}
|
||||
for key, value in schema.items():
|
||||
if key not in allowed_top_keys:
|
||||
continue
|
||||
if isinstance(value, dict) and isinstance(default_schema.get(key), dict):
|
||||
allowed_fields = set(default_schema[key].keys())
|
||||
if key == VIDEO_SPEC_SECTION_KEY:
|
||||
allowed_fields |= VIDEO_SPEC_LOCKED_FIELDS
|
||||
pruned[key] = {field_key: field_value for field_key, field_value in value.items() if field_key in allowed_fields}
|
||||
else:
|
||||
pruned[key] = value
|
||||
|
||||
if VIDEO_SPEC_SECTION_KEY not in pruned or not isinstance(pruned.get(VIDEO_SPEC_SECTION_KEY), dict):
|
||||
pruned[VIDEO_SPEC_SECTION_KEY] = {}
|
||||
return pruned
|
||||
|
||||
|
||||
def apply_locked_video_schema_fields(schema: dict[str, Any], video_config: dict[str, Any], schema_config_snapshot: Any | None = None) -> dict[str, Any]:
|
||||
duration = int(video_config["duration"])
|
||||
video_ratio = str(video_config["aspect_ratio"])
|
||||
@@ -1204,32 +1299,39 @@ def apply_locked_video_schema_fields(schema: dict[str, Any], video_config: dict[
|
||||
}
|
||||
)
|
||||
|
||||
schema["动态时间规划"] = plan
|
||||
schema["动态时间规划"] = _normalize_time_plan(plan, schema.get("动态时间规划"))
|
||||
schema["输出规格限制"] = dynamic_schema.get("输出规格限制", {})
|
||||
|
||||
schema["动作流程"] = _align_flow_time_ranges(
|
||||
schema.get("动作流程"),
|
||||
plan,
|
||||
_flow_content_key(schema_config_snapshot, "动作流程"),
|
||||
content_keys=_flow_content_aliases(schema_config_snapshot, "动作流程"),
|
||||
item_fields=_flow_item_field_rules(schema_config_snapshot, "动作流程"),
|
||||
)
|
||||
schema["镜头流程"] = _align_flow_time_ranges(
|
||||
schema.get("镜头流程"),
|
||||
plan,
|
||||
_flow_content_key(schema_config_snapshot, "镜头流程"),
|
||||
content_keys=_flow_content_aliases(schema_config_snapshot, "镜头流程"),
|
||||
item_fields=_flow_item_field_rules(schema_config_snapshot, "镜头流程"),
|
||||
)
|
||||
|
||||
# 合规和质量控制不能被前端降低;AI 返回缺失时使用服务端默认结构补齐。
|
||||
default_schema = build_client_schema_from_config(schema_config_snapshot)
|
||||
if "动作流程" in default_schema:
|
||||
schema["动作流程"] = _align_flow_time_ranges(
|
||||
schema.get("动作流程"),
|
||||
schema["动态时间规划"],
|
||||
_flow_content_key(schema_config_snapshot, "动作流程"),
|
||||
content_keys=_flow_content_aliases(schema_config_snapshot, "动作流程"),
|
||||
item_fields=_flow_item_field_rules(schema_config_snapshot, "动作流程"),
|
||||
)
|
||||
else:
|
||||
schema.pop("动作流程", None)
|
||||
|
||||
if "镜头流程" in default_schema:
|
||||
schema["镜头流程"] = _align_flow_time_ranges(
|
||||
schema.get("镜头流程"),
|
||||
schema["动态时间规划"],
|
||||
_flow_content_key(schema_config_snapshot, "镜头流程"),
|
||||
content_keys=_flow_content_aliases(schema_config_snapshot, "镜头流程"),
|
||||
item_fields=_flow_item_field_rules(schema_config_snapshot, "镜头流程"),
|
||||
)
|
||||
else:
|
||||
schema.pop("镜头流程", None)
|
||||
|
||||
# 合规和质量控制不能被前端降低;仅当配置启用对应分组时才补齐,禁用分组必须从最终 schema 中移除。
|
||||
if not isinstance(schema.get("合规控制"), dict) and isinstance(default_schema.get("合规控制"), dict):
|
||||
schema["合规控制"] = copy.deepcopy(default_schema["合规控制"])
|
||||
if not isinstance(schema.get("质量控制"), dict) and isinstance(default_schema.get("质量控制"), dict):
|
||||
schema["质量控制"] = copy.deepcopy(default_schema["质量控制"])
|
||||
|
||||
return clean_final_prompt_specs(schema, video_config)
|
||||
return _prune_schema_by_runtime_config(clean_final_prompt_specs(schema, video_config), schema_config_snapshot)
|
||||
|
||||
|
||||
def normalize_video_prompt_schema_from_ai(result: dict[str, Any], video_config: dict[str, Any], schema_config_snapshot: Any | None = None) -> dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user