优化全能创作
This commit is contained in:
@@ -548,6 +548,7 @@ def tool_schemas(context: AgentContext, *, allow_plan: bool = True) -> list[dict
|
||||
"name": "write_strategy",
|
||||
"description": (
|
||||
"写「创作策略理解」卡:说清这条片给谁看、他为什么会信、你想让他信什么、整体创作方向。"
|
||||
"四个字段都必须写具体非空文案,禁止空字符串。"
|
||||
"仅当用户明确要做片/出方案时,在 write_plan 之前调一次;打招呼或闲聊不要调。"
|
||||
),
|
||||
"parameters": {
|
||||
@@ -569,6 +570,7 @@ def tool_schemas(context: AgentContext, *, allow_plan: bool = True) -> list[dict
|
||||
"description": (
|
||||
"写「视频最终方案」卡并请用户确认。**仅当用户明确要做片/出方案/改方案时调用**;"
|
||||
"打招呼或闲聊不要调。调完会等用户点确认,确认后平台直接按 video_prompt 出片。"
|
||||
"usp / points / timeline 是给用户看的卡片正文,必须写满具体文案,禁止空着只交 video_prompt。"
|
||||
"video_prompt 按系统里的「出片脚本写法」写成口播秒级分镜(专业创作同口径),不要只写大纲。"
|
||||
"先调 write_strategy 再调它。"
|
||||
),
|
||||
@@ -607,7 +609,7 @@ def tool_schemas(context: AgentContext, *, allow_plan: bool = True) -> list[dict
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["usp", "video_prompt"],
|
||||
"required": ["usp", "points", "video_prompt"],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1253,14 +1255,181 @@ def _merge_tool_call_deltas(buffer: dict, deltas: list) -> None:
|
||||
|
||||
|
||||
def _parse_arguments(raw: str) -> dict:
|
||||
try:
|
||||
parsed = json.loads(raw or "{}")
|
||||
except ValueError:
|
||||
"""解析工具 arguments。模型偶发包 markdown 围栏或夹杂前后缀,尽量救回 JSON。"""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return {}
|
||||
if text.startswith("```"):
|
||||
text = text.strip("`")
|
||||
if text.lower().startswith("json"):
|
||||
text = text[4:].lstrip()
|
||||
text = text.strip()
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except ValueError:
|
||||
start, end = text.find("{"), text.rfind("}")
|
||||
if start < 0 or end <= start:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(text[start : end + 1])
|
||||
except ValueError:
|
||||
return {}
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
|
||||
|
||||
def stream_creation_agent(
|
||||
def _pick_str(args: dict, *keys: str) -> str:
|
||||
"""按候选键取非空字符串;兼容中文别名与嵌套一层 dict。"""
|
||||
for key in keys:
|
||||
value = args.get(key)
|
||||
if isinstance(value, dict):
|
||||
# 偶发 {"text": "..."} / {"value": "..."}
|
||||
for nested in ("text", "value", "content", "desc", "description"):
|
||||
inner = value.get(nested)
|
||||
if isinstance(inner, str) and inner.strip():
|
||||
return inner.strip()
|
||||
continue
|
||||
if value is None:
|
||||
continue
|
||||
text = str(value).strip()
|
||||
if text:
|
||||
return text
|
||||
return ""
|
||||
|
||||
|
||||
def _coerce_points(raw) -> list[str]:
|
||||
"""points 规整成最多 3 条非空文案。兼容纯字符串、对象数组、dict。"""
|
||||
if raw is None:
|
||||
return []
|
||||
items: list = []
|
||||
if isinstance(raw, str):
|
||||
text = raw.strip()
|
||||
if not text:
|
||||
return []
|
||||
# 中文分号/换行拆条
|
||||
for part in text.replace("\r", "\n").replace(";", "\n").split("\n"):
|
||||
part = part.strip(" ·•-、,,")
|
||||
if part:
|
||||
items.append(part)
|
||||
elif isinstance(raw, dict):
|
||||
# {"P0": "...", "P1": "..."} 或 {"0": "..."}
|
||||
for key in sorted(raw.keys(), key=lambda k: str(k)):
|
||||
val = raw[key]
|
||||
if isinstance(val, dict):
|
||||
text = _pick_str(val, "text", "value", "content", "desc", "point", "label")
|
||||
else:
|
||||
text = str(val or "").strip()
|
||||
if text:
|
||||
items.append(text)
|
||||
elif isinstance(raw, (list, tuple)):
|
||||
for item in raw:
|
||||
if isinstance(item, dict):
|
||||
text = _pick_str(item, "text", "value", "content", "desc", "point", "label")
|
||||
else:
|
||||
text = str(item or "").strip()
|
||||
if text:
|
||||
items.append(text)
|
||||
else:
|
||||
text = str(raw).strip()
|
||||
if text:
|
||||
items.append(text)
|
||||
# 过滤单字符噪声(字符串被误当成 list 迭代时的残留)
|
||||
cleaned = [p for p in items if len(p) > 1]
|
||||
return cleaned[:3]
|
||||
|
||||
|
||||
def _coerce_voice_chars(raw, fallback: list[int] | None = None) -> list[int]:
|
||||
"""voice_chars 必须是 [下限, 上限];模型常误塞语气文案或单个整数。"""
|
||||
if isinstance(raw, (list, tuple)) and len(raw) >= 2:
|
||||
try:
|
||||
lo, hi = int(raw[0]), int(raw[1])
|
||||
if lo > 0 and hi >= lo:
|
||||
return [lo, hi]
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if isinstance(raw, (int, float)) and int(raw) > 0:
|
||||
n = int(raw)
|
||||
return [max(1, n - 5), n + 5]
|
||||
return list(fallback or [])
|
||||
|
||||
|
||||
def _coerce_timeline(raw) -> list[dict]:
|
||||
items: list[dict] = []
|
||||
for item in (raw or []) if isinstance(raw, list) else []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
try:
|
||||
start = float(item.get("start"))
|
||||
end = float(item.get("end"))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
stage = str(item.get("stage") or "").strip()
|
||||
if not stage:
|
||||
continue
|
||||
entry = {"start": start, "end": end, "stage": stage}
|
||||
desc = str(item.get("desc") or item.get("description") or "").strip()
|
||||
if desc:
|
||||
entry["desc"] = desc
|
||||
items.append(entry)
|
||||
return items
|
||||
|
||||
|
||||
def _default_plan_matrix(usp: str, points: list[str]) -> dict:
|
||||
"""设计稿里的「卖点覆盖矩阵」:有 USP/支撑点就自动铺一版,避免方案卡干瘪。"""
|
||||
rows = [{"point": "主打卖点 USP", "hits": [1, 3]}]
|
||||
labels = ["体验卖点 P0", "视觉卖点 P0", "转化卖点 P0"]
|
||||
for index, point in enumerate(points[:3]):
|
||||
label = labels[index] if index < len(labels) else f"支撑卖点 P{index}"
|
||||
# 点名用文案前缀,hits 错落分布到 4 镜
|
||||
hit = (index % 4) + 1
|
||||
rows.append({"point": label if not point else f"{label}", "hits": [hit]})
|
||||
return {"shots": 4, "rows": rows}
|
||||
|
||||
|
||||
def _coerce_strategy_args(args: dict) -> dict:
|
||||
return {
|
||||
"target": _pick_str(
|
||||
args, "target", "audience", "who", "给谁看", "目标人群", "人群",
|
||||
),
|
||||
"trust": _pick_str(
|
||||
args, "trust", "credibility", "为什么相信", "信任", "可信度",
|
||||
),
|
||||
"belief": _pick_str(
|
||||
args, "belief", "希望相信", "想让他信什么", "认知", "takeaway",
|
||||
),
|
||||
"direction": _pick_str(
|
||||
args, "direction", "创作方向", "方向", "style", "路线",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _coerce_plan_card_args(args: dict) -> dict:
|
||||
usp = _pick_str(args, "usp", "主打卖点", "卖点", "core_usp", "main_point")
|
||||
points = _coerce_points(
|
||||
args.get("points")
|
||||
if args.get("points") is not None
|
||||
else args.get("支撑点") or args.get("supports") or args.get("selling_points")
|
||||
)
|
||||
# 兼容 point1/point2/point3 展开写法(设计稿 blueprint)
|
||||
if not points:
|
||||
for key in ("point1", "point2", "point3", "P0", "P1", "P2"):
|
||||
text = _pick_str(args, key)
|
||||
if text:
|
||||
points.append(text)
|
||||
points = points[:3]
|
||||
timeline = _coerce_timeline(args.get("timeline") or args.get("时间轴"))
|
||||
matrix = args.get("matrix")
|
||||
if not isinstance(matrix, dict) or not matrix.get("rows"):
|
||||
matrix = _default_plan_matrix(usp, points) if (usp or points) else {}
|
||||
return {
|
||||
"usp": usp,
|
||||
"points": points,
|
||||
"timeline": timeline,
|
||||
"matrix": matrix,
|
||||
"voice_chars": args.get("voice_chars"),
|
||||
}
|
||||
|
||||
|
||||
def iter_creation_agent_events(
|
||||
*,
|
||||
conversation: CreationConversation,
|
||||
user,
|
||||
@@ -1270,15 +1439,15 @@ def stream_creation_agent(
|
||||
record_user_message: bool = True,
|
||||
force_creative_turn: bool = False,
|
||||
continuation_instruction: str = "",
|
||||
) -> Iterator[str]:
|
||||
"""一条用户消息 → SSE 流。生成器,由 StreamingHttpResponse 逐帧下发。"""
|
||||
) -> Iterator[dict]:
|
||||
"""一条用户消息 → 事件 dict 流(message/delta/tool/done/error)。消息已落库。"""
|
||||
refs = refs or []
|
||||
fast_greeting = record_user_message and is_greeting(text)
|
||||
# 打招呼不依赖模型,模型未配置也能即时回应;其他消息保持原有的先校验模型行为。
|
||||
if not fast_greeting:
|
||||
model_config = get_creation_chat_model(model_config)
|
||||
if model_config is None:
|
||||
yield _sse({"type": "error", "detail": "没有可用的文本模型,请先在模型库配置"})
|
||||
yield {"type": "error", "detail": "没有可用的文本模型,请先在模型库配置"}
|
||||
return
|
||||
context = AgentContext(conversation=conversation, user=user, model_config=model_config)
|
||||
|
||||
@@ -1289,7 +1458,7 @@ def stream_creation_agent(
|
||||
if record_user_message:
|
||||
user_message = append_message(conversation, role="user", text=text, refs=refs)
|
||||
if user_message is not None:
|
||||
yield _sse({"type": "message", "message": _message_payload(user_message)})
|
||||
yield {"type": "message", "message": _message_payload(user_message)}
|
||||
# 单纯打招呼无论有没有参考素材、有没有既有上下文,都不值得等模型。
|
||||
# 素材先收下,等用户说清用途再分析,避免「你好」卡住还冒出一大段建议。
|
||||
if fast_greeting:
|
||||
@@ -1301,8 +1470,8 @@ def stream_creation_agent(
|
||||
if refs else "你好。想做什么,直接说就行。"
|
||||
),
|
||||
)
|
||||
yield _sse({"type": "message", "message": _message_payload(reply)})
|
||||
yield _sse({"type": "done"})
|
||||
yield {"type": "message", "message": _message_payload(reply)}
|
||||
yield {"type": "done"}
|
||||
return
|
||||
|
||||
# 空会话里的简短应答:短回一句就够。已有商品/方向时走模型,
|
||||
@@ -1314,8 +1483,8 @@ def stream_creation_agent(
|
||||
role="assistant",
|
||||
text="我在。想做什么,直接丢一句想法给我就行。",
|
||||
)
|
||||
yield _sse({"type": "message", "message": _message_payload(reply)})
|
||||
yield _sse({"type": "done"})
|
||||
yield {"type": "message", "message": _message_payload(reply)}
|
||||
yield {"type": "done"}
|
||||
return
|
||||
|
||||
model_config = _prefer_vision_text_model(model_config, conversation.team, conversation.pinned_refs or [])
|
||||
@@ -1328,7 +1497,7 @@ def stream_creation_agent(
|
||||
conversation, role="assistant",
|
||||
text=f"有几个引用的素材已经找不到了({names}),我先按其余信息继续。",
|
||||
)
|
||||
yield _sse({"type": "message", "message": _message_payload(note)})
|
||||
yield {"type": "message", "message": _message_payload(note)}
|
||||
|
||||
# 压缩放在**建消息之前**:摘要要进这一轮的 system 提示词才有意义。
|
||||
# 用户消息已经先回显了,所以这一小段等待不会看起来像卡住。
|
||||
@@ -1347,7 +1516,13 @@ def stream_creation_agent(
|
||||
messages.append({"role": "user", "content": continuation_instruction.strip()})
|
||||
tools = tool_schemas(context, allow_plan=allow_plan)
|
||||
|
||||
from .creation import is_agent_cancel_requested
|
||||
|
||||
for _round in range(MAX_TOOL_ROUNDS):
|
||||
if is_agent_cancel_requested(conversation.id):
|
||||
# 用户终止:干净收束,不落 ERROR,已落库消息保留
|
||||
yield {"type": "cancelled"}
|
||||
return
|
||||
text_buffer: list[str] = []
|
||||
tool_buffer: dict = {}
|
||||
for chunk in provider.chat_completion_stream(
|
||||
@@ -1358,11 +1533,11 @@ def stream_creation_agent(
|
||||
):
|
||||
kind = chunk.get("type")
|
||||
if kind == "reasoning":
|
||||
yield _sse({"type": "reasoning", "text": chunk.get("text", "")})
|
||||
yield {"type": "reasoning", "text": chunk.get("text", "")}
|
||||
elif kind == "delta":
|
||||
piece = chunk.get("text", "")
|
||||
text_buffer.append(piece)
|
||||
yield _sse({"type": "delta", "text": piece})
|
||||
yield {"type": "delta", "text": piece}
|
||||
elif kind == "tool_call":
|
||||
_merge_tool_call_deltas(tool_buffer, chunk.get("tool_calls"))
|
||||
|
||||
@@ -1407,7 +1582,7 @@ def stream_creation_agent(
|
||||
asks_user = bool(fallback_fields) or any(call.get("name") == "ask_user" for call in calls)
|
||||
if said and not asks_user:
|
||||
bubble = append_message(conversation, role="assistant", text=said)
|
||||
yield _sse({"type": "message", "message": _message_payload(bubble)})
|
||||
yield {"type": "message", "message": _message_payload(bubble)}
|
||||
|
||||
if not calls:
|
||||
if fallback_fields:
|
||||
@@ -1415,7 +1590,7 @@ def stream_creation_agent(
|
||||
context, "ask_user", {"fields": fallback_fields}, allow_pick=allow_pick
|
||||
)
|
||||
for event in result.get("_events", []):
|
||||
yield _sse(event)
|
||||
yield event
|
||||
break
|
||||
|
||||
messages.append({
|
||||
@@ -1430,23 +1605,26 @@ def stream_creation_agent(
|
||||
|
||||
stop = False
|
||||
for index, call in enumerate(calls):
|
||||
if is_agent_cancel_requested(conversation.id):
|
||||
yield {"type": "cancelled"}
|
||||
return
|
||||
name = call["name"]
|
||||
args = _parse_arguments(call["arguments"])
|
||||
yield _sse({"type": "tool", "id": name, "label": _TOOL_LABELS.get(name, name), "status": "running"})
|
||||
yield {"type": "tool", "id": name, "label": _TOOL_LABELS.get(name, name), "status": "running"}
|
||||
try:
|
||||
result, stop_after = _dispatch_tool(context, name, args)
|
||||
except AgentError as exc:
|
||||
yield _sse({"type": "tool", "id": name, "label": _TOOL_LABELS.get(name, name), "status": "error"})
|
||||
yield {"type": "tool", "id": name, "label": _TOOL_LABELS.get(name, name), "status": "error"}
|
||||
failure = append_message(
|
||||
conversation, role="assistant",
|
||||
kind=CreationMessage.Kind.ERROR, text=str(exc),
|
||||
)
|
||||
yield _sse({"type": "message", "message": _message_payload(failure)})
|
||||
yield _sse({"type": "done"})
|
||||
yield {"type": "message", "message": _message_payload(failure)}
|
||||
yield {"type": "done"}
|
||||
return
|
||||
yield _sse({"type": "tool", "id": name, "label": _TOOL_LABELS.get(name, name), "status": "done"})
|
||||
yield {"type": "tool", "id": name, "label": _TOOL_LABELS.get(name, name), "status": "done"}
|
||||
for event in result.get("_events", []):
|
||||
yield _sse(event)
|
||||
yield event
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": f"call_{index}",
|
||||
@@ -1456,10 +1634,132 @@ def stream_creation_agent(
|
||||
if stop:
|
||||
break
|
||||
|
||||
yield _sse({"type": "done"})
|
||||
yield {"type": "done"}
|
||||
except Exception as exc: # noqa: BLE001 — SSE 里任何未捕获异常都会变成前端「白屏卡死」
|
||||
logger.exception("creation agent stream failed: %s", exc)
|
||||
yield _sse({"type": "error", "detail": "生成过程出错了,请再试一次"})
|
||||
yield {"type": "error", "detail": "生成过程出错了,请再试一次"}
|
||||
|
||||
|
||||
|
||||
def stream_creation_agent(
|
||||
*,
|
||||
conversation: CreationConversation,
|
||||
user,
|
||||
text: str,
|
||||
refs: list[dict] | None = None,
|
||||
model_config: ModelConfig | None = None,
|
||||
record_user_message: bool = True,
|
||||
force_creative_turn: bool = False,
|
||||
continuation_instruction: str = "",
|
||||
) -> Iterator[str]:
|
||||
"""兼容旧 SSE 消费方(单测 / 调试)。生产路径走 Celery + poll。"""
|
||||
for event in iter_creation_agent_events(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
text=text,
|
||||
refs=refs,
|
||||
model_config=model_config,
|
||||
record_user_message=record_user_message,
|
||||
force_creative_turn=force_creative_turn,
|
||||
continuation_instruction=continuation_instruction,
|
||||
):
|
||||
yield _sse(event)
|
||||
|
||||
|
||||
def run_creation_agent_turn(
|
||||
*,
|
||||
conversation_id: str,
|
||||
user_id: str,
|
||||
text: str = "",
|
||||
refs: list | None = None,
|
||||
model_config_id: str | None = None,
|
||||
record_user_message: bool = True,
|
||||
force_creative_turn: bool = False,
|
||||
continuation_instruction: str = "",
|
||||
) -> str:
|
||||
"""Celery worker 入口:跑完一轮 tool loop,消息落库;更新 agent_status;释放团队锁。
|
||||
|
||||
不复用 CreationMessage.kind=generating / AITask —— 那些是确认后出片/出图用的。
|
||||
"""
|
||||
from apps.accounts.models import User
|
||||
|
||||
from .creation import finish_agent_planning
|
||||
|
||||
conversation = (
|
||||
CreationConversation.objects.select_related("team", "created_by")
|
||||
.filter(id=conversation_id)
|
||||
.first()
|
||||
)
|
||||
if conversation is None:
|
||||
return conversation_id
|
||||
user = User.objects.filter(id=user_id).first() or conversation.created_by
|
||||
model_config = None
|
||||
if model_config_id:
|
||||
model_config = (
|
||||
ModelConfig.objects.select_related("provider")
|
||||
.filter(id=model_config_id, capability=ModelConfig.Capability.TEXT, status=ModelConfig.Status.ACTIVE)
|
||||
.first()
|
||||
)
|
||||
|
||||
awaiting_user = False
|
||||
user_cancelled = False
|
||||
try:
|
||||
for event in iter_creation_agent_events(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
text=text or "",
|
||||
refs=refs or [],
|
||||
model_config=model_config,
|
||||
record_user_message=record_user_message,
|
||||
force_creative_turn=force_creative_turn,
|
||||
continuation_instruction=continuation_instruction or "",
|
||||
):
|
||||
if not isinstance(event, dict):
|
||||
continue
|
||||
if event.get("type") == "cancelled":
|
||||
user_cancelled = True
|
||||
awaiting_user = False
|
||||
break
|
||||
if event.get("type") == "message":
|
||||
message = event.get("message") or {}
|
||||
kind = message.get("kind") if isinstance(message, dict) else None
|
||||
if kind in (CreationMessage.Kind.ELICIT, CreationMessage.Kind.CONFIRM):
|
||||
awaiting_user = True
|
||||
elif event.get("type") == "error":
|
||||
detail = str(event.get("detail") or "生成过程出错了,请再试一次")
|
||||
# 循环内多数错误已落 ERROR 消息;这里兜底一条,避免前端只看到卡在 planning
|
||||
last = conversation.messages.order_by("-seq").first()
|
||||
if last is None or last.kind != CreationMessage.Kind.ERROR:
|
||||
append_message(
|
||||
conversation,
|
||||
role="assistant",
|
||||
kind=CreationMessage.Kind.ERROR,
|
||||
text=detail,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 — worker 不能把异常冒成无限 planning
|
||||
logger.exception("creation agent turn failed: %s", exc)
|
||||
try:
|
||||
append_message(
|
||||
conversation,
|
||||
role="assistant",
|
||||
kind=CreationMessage.Kind.ERROR,
|
||||
text="生成过程出错了,请再试一次",
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception("creation agent turn: failed to append error message")
|
||||
awaiting_user = False
|
||||
finally:
|
||||
try:
|
||||
conversation.refresh_from_db(fields=["agent_status", "team_id"])
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
# 用户已终止时 API 侧多半已 idle+放锁;这里再收一次保证幂等,且不落入 awaiting_user
|
||||
finish_agent_planning(
|
||||
conversation,
|
||||
awaiting_user=(awaiting_user and not user_cancelled),
|
||||
)
|
||||
return conversation_id
|
||||
|
||||
|
||||
|
||||
_TOOL_LABELS = {
|
||||
@@ -1579,10 +1879,21 @@ def _dispatch_tool(
|
||||
return {"payload": _run_search_library(context, args)}, False
|
||||
|
||||
if name == "write_strategy":
|
||||
strategy_payload = _coerce_strategy_args(args if isinstance(args, dict) else {})
|
||||
# 空卡会落成「只有标签没有正文」——拒绝,让模型把四字段写满再调
|
||||
if not all(strategy_payload.values()):
|
||||
return {
|
||||
"payload": {
|
||||
"error": (
|
||||
"创作策略卡四字段都不能为空:请填写具体的 target / trust / belief / direction"
|
||||
"(给谁看、为什么信、希望他信什么、创作方向),不要留空。"
|
||||
)
|
||||
}
|
||||
}, False
|
||||
message = append_message(
|
||||
context.conversation, role="assistant",
|
||||
kind=CreationMessage.Kind.STRATEGY,
|
||||
payload={key: str(args.get(key) or "") for key in ("target", "trust", "belief", "direction")},
|
||||
payload=strategy_payload,
|
||||
)
|
||||
# 策略卡只是「我理解对了吗」,不打断 —— 模型接着就该写方案
|
||||
return {
|
||||
@@ -1594,11 +1905,29 @@ def _dispatch_tool(
|
||||
video_prompt = str(args.get("video_prompt") or "").strip()
|
||||
if not video_prompt:
|
||||
return {"payload": {"error": "video_prompt 不能为空,请把完整出片指令写进去"}}, False
|
||||
card = _coerce_plan_card_args(args if isinstance(args, dict) else {})
|
||||
if not card["usp"] or not card["points"]:
|
||||
return {
|
||||
"payload": {
|
||||
"error": (
|
||||
"方案卡缺正文:请填写非空的 usp(主打卖点)和 points(1–3 条核心支撑),"
|
||||
"不要只写 video_prompt。卡片上要让用户看见卖点文案。"
|
||||
)
|
||||
}
|
||||
}, False
|
||||
duration = 15
|
||||
try:
|
||||
duration = int(float(str((context.conversation.params or {}).get("duration") or "15").replace("秒", "").strip() or "15"))
|
||||
except (TypeError, ValueError):
|
||||
duration = 15
|
||||
lo = max(20, round(duration * 3.4))
|
||||
hi = max(lo + 1, round(duration * 4))
|
||||
plan_payload = {
|
||||
"usp": str(args.get("usp") or ""),
|
||||
"points": [str(p) for p in (args.get("points") or [])][:3],
|
||||
"timeline": args.get("timeline") or [],
|
||||
"voice_chars": args.get("voice_chars") or [],
|
||||
"usp": card["usp"],
|
||||
"points": card["points"],
|
||||
"timeline": card["timeline"],
|
||||
"matrix": card["matrix"],
|
||||
"voice_chars": _coerce_voice_chars(card["voice_chars"], [lo, hi]),
|
||||
"ref_count": len(context.conversation.pinned_refs or []),
|
||||
}
|
||||
events = []
|
||||
|
||||
Reference in New Issue
Block a user