大量优化修改扣积分规则

This commit is contained in:
Azmat@qq.com
2026-09-04 17:20:22 +08:00
parent 3e904479d9
commit 15718a60bf
41 changed files with 1590 additions and 429 deletions
+136 -11
View File
@@ -66,6 +66,32 @@ IMAGE_MODEL_BY_LABEL = {
# 「智能时长」= 交给我们定,取一个口播讲得完又不烧钱的中间值
SMART_DURATION = 15
# 全能创作 video_prompt 写作规范:从专业创作 / 一键成片(口播脚本)抽硬规则,
# 适配「整段出片指令」而不是 ScriptDraft JSON。只要求口径接近,不改工具形态。
_OMNI_VIDEO_PROMPT_RULES = """
【出片脚本写法 · 对齐专业口播】
表现形式默认**口播带货**,像真人在讲刚遇到的一件事,不要详情页朗读或主播念稿。
video_prompt 必须能被出片模型直接执行,按时间轴写秒级分镜,建议结构:
- 片头写清:总时长、画幅、整体光线/色调、口播语气(口语、有停顿与立场)
- 然后按「0-3s / 3-8s / …」逐段写,每段同时写清这五项(不能省):
1. 景别(大特写/特写/近景/中景/全景;一条片里至少切两次景别)
2. 机位(平视/俯拍/仰拍/过肩/桌面视角)
3. 运镜(手持跟拍/推近/拉远/横摇/环绕/固定 —— 每段至少一个运镜词)
4. 动作(谁、哪只手、对什么、做什么;要连贯可拍,禁止「展示质感」等抽象词)
5. 信息变化(这几秒画面上多了/变了什么)
- 口播原文单独写清(可用「口播:…」),字数贴近会话时长:大约 5.0–5.7 字/秒,
15 秒约 75–85 字;太短撑不满,太长会赶。
- 钩子段画面不要「对着镜头说话」静态开场;前 15 个口播字禁止「大家好/今天分享/给你们推荐」。
- 全片只围绕一个具体情境推进一个主卖点;卖点要有看得见的证据(质地/前后变化/用法结果)。
- CTA 像跟朋友说话;禁止小黄车/立即购买/闭眼入等平台指令腔。
- **不要写字幕/花字/标题贴片/弹幕/角标/水印/购物浮层**,也不要写「无字幕」
(否定说法也容易把字画上屏)。口播只存在于声音;包装上原有印刷字除外。
- 已 @ 的角色/商品/场景参考图会自动附上,不要在 prompt 里重描长相;以图锁定性别年龄服装外形。
- 同一场戏保持地点、光线、服装连续;要换环境就明确写下一时间段切换。
"""
class AgentError(Exception):
"""Agent 循环里的业务错误,已经是可以直接给用户看的中文。"""
@@ -353,8 +379,9 @@ def tool_schemas(context: AgentContext) -> list[dict]:
"name": "write_plan",
"description": (
"写「视频最终方案」卡并请用户确认。**这是出片前的最后一步**,调完会等用户点确认,"
"确认后平台直接按 video_prompt 出片,你不会再有插话机会 —— 所以 video_prompt "
"必须是完整、可独立执行的成片指令。先调 write_strategy 再调它。"
"确认后平台直接按 video_prompt 出片,你不会再有插话机会。"
"video_prompt 按系统里的「出片脚本写法」写成口播秒级分镜(专业创作同口径),不要只写大纲。"
"先调 write_strategy 再调它。"
),
"parameters": {
"type": "object",
@@ -383,9 +410,11 @@ def tool_schemas(context: AgentContext) -> list[dict]:
"video_prompt": {
"type": "string",
"description": (
"交给出片模型的完整指令:秒级分镜(每镜画面/动作/机位/光线)、口播原文、"
"风格锚点、一致性要求。已 @ 的素材会自动作为参考图附上,"
"不要在这里重复描述它们的长相。**不要写字幕相关要求。**"
"交给出片模型的完整口播带货指令(对齐专业创作口径)。"
"必须含:总时长与画幅、整体光线色调、按 0-Ns 分段的秒级分镜"
"(每段写清景别/机位/运镜/具体动作/信息变化)、口播原文、一个主卖点与可见证据、口语 CTA。"
"禁止字幕/花字/贴片及「无字幕」字样;禁止详情页腔与「大家好」开场。"
"已 @ 素材会自动作参考图,勿重描长相。"
),
},
},
@@ -464,9 +493,68 @@ def _coerce_fields(raw) -> list[dict]:
return fields
def _normalize_model_label(value: str) -> str:
return "".join(ch for ch in str(value or "").lower() if ch.isalnum())
def image_model_name(params: dict) -> str | None:
"""出图模型 label → 供应商模型名;目录优先,认不出返回 None 让下游用默认。"""
from django.db.models import Q
from .models import ModelConfig
label = str(params.get("model") or "").strip()
if not label:
return None
mapped = IMAGE_MODEL_BY_LABEL.get(label)
if mapped:
return mapped
hit = (
ModelConfig.objects.filter(capability=ModelConfig.Capability.IMAGE, status=ModelConfig.Status.ACTIVE)
.filter(Q(display_name=label) | Q(name=label))
.order_by("created_at")
.first()
)
return hit.name if hit else None
def video_model_name(params: dict) -> str:
"""会话参数里的模型 label → 火山模型名。认不出就回落 2.5(最长 30 秒那档)。"""
return VIDEO_MODEL_BY_LABEL.get(str(params.get("model") or ""), DEFAULT_VIDEO_MODEL)
"""会话参数里的模型 label → 供应商模型名。
先认历史写死映射,再按 ModelConfig.display_name / name 查目录 —— 后台新加模型不用改代码。
"""
from django.db.models import Q
from .models import ModelConfig
label = str(params.get("model") or "").strip()
if not label:
return DEFAULT_VIDEO_MODEL
mapped = VIDEO_MODEL_BY_LABEL.get(label)
if not mapped:
norm = _normalize_model_label(label)
for k, v in VIDEO_MODEL_BY_LABEL.items():
if _normalize_model_label(k) == norm:
mapped = v
break
if mapped:
return mapped
hit = (
ModelConfig.objects.filter(capability=ModelConfig.Capability.VIDEO, status=ModelConfig.Status.ACTIVE)
.filter(Q(display_name=label) | Q(name=label))
.order_by("created_at")
.first()
)
if hit is None:
hit = (
ModelConfig.objects.filter(capability=ModelConfig.Capability.VIDEO, status=ModelConfig.Status.ACTIVE)
.filter(Q(display_name__icontains=label) | Q(name__icontains=label))
.order_by("created_at")
.first()
)
return hit.name if hit else DEFAULT_VIDEO_MODEL
def video_duration(params: dict) -> int:
@@ -531,8 +619,9 @@ def _run_generate_image(context: AgentContext, args: dict) -> tuple[dict, list]:
mode="image",
count=count,
ratio=params.get("ratio") or None,
image_model=IMAGE_MODEL_BY_LABEL.get(str(params.get("model") or ""), params.get("model") or None),
image_model=image_model_name(params) or params.get("model") or None,
reference_image_ids=reference_image_ids or None,
feature="omni_create",
)
context.generations_used += 1
return (
@@ -587,6 +676,27 @@ def estimate_video_credits(context: AgentContext) -> int:
return 0
def estimate_image_credits(context: AgentContext) -> int:
"""出图确认卡预计积分:挂牌单价(含团队系数)逐张取整后再 × 张数,与 enqueue 逐任务预留同口径。"""
from apps.billing.pricing import quote_flat
from apps.ai.services import resolve_image_model, get_default_model
params = context.conversation.params or {}
model_name = image_model_name(params) or str(params.get("model") or "").strip() or None
model_config = resolve_image_model(model_name) if model_name else None
if model_config is None:
model_config = get_default_model(ModelConfig.Capability.IMAGE)
if model_config is None:
return 0
count = _image_count(params, None)
try:
per = quote_flat(model_config, units=1, team=context.team)
return int(per.points) * count
except Exception: # noqa: BLE001
logger.warning("omni create: image estimate failed", exc_info=True)
return 0
def submit_confirmed_video(*, conversation: CreationConversation, user, confirm_message: CreationMessage):
"""用户点了确认 → 直接按方案卡里存好的 video_prompt 出片。
@@ -764,7 +874,18 @@ def build_system_prompt(context: AgentContext) -> str:
"- 用户说改时长/模型/比例/分辨率:立刻 ask_user,type=single 给出选项;选完后旧方案作废,必须按新参数重新 write_plan。不要让用户去点底部菜单。",
"- 光线、构图、镜头这些专业判断是你的活,不要反过来问用户。",
]
if not context.is_video:
if context.is_video:
lines.extend(["", _OMNI_VIDEO_PROMPT_RULES.strip()])
# 按会话时长给出口播字数锚点(与专业创作 narration_limit 同口径)
try:
dur = video_duration(params)
except Exception: # noqa: BLE001
dur = SMART_DURATION
lo = max(1, int(dur * 5.0))
hi = max(lo, min(85, int(dur * 5.7)))
lines.append(f"- 当前按约 {dur} 秒出片,口播建议 {lo}–{hi} 字;write_plan 的 voice_chars 填这个区间。")
lines.append("- 写方案时必须调用 write_plan;video_prompt 按上面的秒级分镜规范写满,不要只给大纲。")
else:
lines.extend([
"",
"【出图】",
@@ -1106,20 +1227,24 @@ def _dispatch_tool(context: AgentContext, name: str, args: dict) -> tuple[dict,
if not prompt:
return {"payload": {"error": "生成失败:模型没有给出画面描述"}}, False
# 出图也走确认卡:用户先看当前模型/比例/张数,点了才提交。
credits = estimate_image_credits(context)
confirm = append_message(
context.conversation, role="assistant",
kind=CreationMessage.Kind.CONFIRM,
payload={
"kind": "image",
"label": "开始生成",
"estimated_credits": 0,
"estimated_credits": credits,
"prompt": prompt,
"submitted": False,
"params": snapshot_session_params(context.conversation),
"param_options": confirm_param_options(False),
},
)
events = [{"type": "message", "message": _message_payload(confirm)}]
events = [
{"type": "message", "message": _message_payload(confirm)},
{"type": "credits", "estimated": credits},
]
return {"payload": {"awaiting_confirmation": True}, "_events": events}, True
return {"payload": {"error": f"未知工具 {name}"}}, False