baseline: Clover 独立仓库首次基线提交

将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-16 11:30:22 +08:00
commit 6a2632da70
253 changed files with 27467 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
"""
AI 引擎单元测试(核心逻辑覆盖)
对照 JS 版逻辑验证 Python 重写正确性
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import pytest
from app.services.ai_engine.text_scoring import score_copy, is_similar_copy, dedupe_copies
from app.services.ai_engine.banned_word_checker import check_and_fix, BannedWordEntry
from app.services.ai_engine.storyboard import get_narrative_roles, plan_image_set, clamp_count
from app.services.ai_engine.preference_aggregator import aggregate_preference_context, collect_preference_event
from app.services.ai_engine.prompt_composer import (
compose_variants, compose_preference_context, compose_image_prompt, parse_model_output
)
# ── 测试用 product ─────────────────────────────────────────
PRODUCT = {
"name": "倍分子素颜霜",
"category": "美妆护肤",
"selling_points": ["轻薄不厚重", "水润自然", "不卡粉"],
"keywords": ["素颜霜", "日常通勤"],
"style_tone": "素人分享风",
"text_angles": ["痛点切入", "场景型", "避坑型"],
"custom_prompt": "",
"target_audience": "上班族",
}
GOOD_COPY = {
"title": "早八素颜也能有点气色!✨",
"content": "姐妹们,最近实测下来,轻薄不厚重是我最在意的。✅ 通勤路上随手一抹,水润自然有气色。不卡粉这点太加分了。整体就是那种上班族能马上代入的日常好物。",
"tags": ["#素颜霜", "#日常通勤", "#好物分享", "#真实测评"],
"angle": "场景型",
"buyingPoint": "轻薄不厚重,适合上班族",
"coverTitle": "早八素颜也能有点气色",
"imageBrief": "封面自然光上脸局部,内页质地推开+软性转化。",
"source": "ai",
}
# ── 五维打分 ───────────────────────────────────────────────
class TestScoreCopy:
def test_good_copy_passes(self):
result = score_copy(GOOD_COPY, PRODUCT)
assert result["passed"] is True
assert result["score"] >= 90
def test_banned_word_kills_compliance(self):
bad = {**GOOD_COPY, "title": "美白素颜霜强推!", "content": GOOD_COPY["content"] + "美白效果显著"}
result = score_copy(bad, PRODUCT)
# 含美白 → 合规0分 + 总分不过
assert result["passed"] is False
compliance = next(d for d in result["score_detail"] if d["item"] == "合规性")
assert compliance["score"] == 0
def test_score_detail_has_five_dims(self):
result = score_copy(GOOD_COPY, PRODUCT)
assert len(result["score_detail"]) == 5
items = {d["item"] for d in result["score_detail"]}
assert "标题吸引力" in items
assert "情绪共鸣" in items
assert "买点表达" in items
assert "关键词覆盖" in items
assert "合规性" in items
# ── 去重 ───────────────────────────────────────────────────
class TestDedup:
def test_identical_title_deduped(self):
a = {**GOOD_COPY}
b = {**GOOD_COPY, "content": "略有不同的内容,但标题一样"}
result = dedupe_copies([a, b])
assert len(result) == 1
def test_different_angle_both_kept(self):
a = {**GOOD_COPY, "angle": "痛点切入"}
b = {**GOOD_COPY, "title": "通勤懒人必囤!换季不再干脸", "angle": "避坑型"}
result = dedupe_copies([a, b])
assert len(result) == 2
def test_similar_body_deduped(self):
a = {**GOOD_COPY}
b = {**GOOD_COPY, "title": "不一样的标题", "content": GOOD_COPY["content"][:150] + "稍有变动"}
assert is_similar_copy(a, b) or len(dedupe_copies([a, b])) <= 2
# ── 违禁词三级 ──────────────────────────────────────────────
class TestBannedWordChecker:
def test_hard_block(self):
# "美白"已改为 auto_fix提亮肤色感hard_block 用速效/医用等词
result = check_and_fix("速效医用护肤品")
assert result.status == "hard_block"
def test_meibaibanned_now_auto_fix(self):
# Q5对齐美白→auto_fix 提亮肤色感(不再 hard_block
result = check_and_fix("这款美白效果很好")
assert result.status == "auto_fixed"
assert result.fixed_text is not None
assert "美白" not in result.fixed_text
def test_auto_fix(self):
result = check_and_fix("这款神器真的好用")
assert result.status == "auto_fixed"
assert result.fixed_text is not None
assert "神器" not in result.fixed_text
def test_soft_warn(self):
result = check_and_fix("这绝对是最好的护肤品")
assert result.status == "soft_warn"
def test_clean_text_passes(self):
result = check_and_fix("这款素颜霜轻薄水润,通勤必备。")
assert result.status == "pass"
def test_custom_entries_override(self):
entries = [BannedWordEntry("必备", "hard_block")]
result = check_and_fix("通勤必备好物", entries)
assert result.status == "hard_block"
# ── storyboard ─────────────────────────────────────────────
class TestStoryboard:
def test_clamp_count(self):
assert clamp_count(0) == 1
assert clamp_count(9) == 8
assert clamp_count(3) == 3
def test_narrative_roles_3(self):
roles = get_narrative_roles(3)
assert len(roles) == 3
assert roles[0]["role"] == "hook"
assert roles[-1]["role"] == "closer"
def test_narrative_roles_6(self):
roles = get_narrative_roles(6)
assert len(roles) == 6
# Q6对齐北哥套路①hook ②product_closeup(单品特写) ③ingredient ④texture ⑤applied_proof ⑥closer
assert roles[0]["role"] == "hook"
assert roles[1]["role"] == "product_closeup"
assert roles[2]["role"] == "ingredient"
assert roles[4]["role"] == "applied_proof"
assert roles[5]["role"] == "closer"
def test_narrative_roles_8(self):
roles = get_narrative_roles(8)
assert len(roles) == 8
def test_plan_image_set_structure(self):
plan = plan_image_set(GOOD_COPY, PRODUCT, image_count=3)
assert "storyboard" in plan
assert "base_prompt" in plan
assert len(plan["storyboard"]) == 3
# 产品图锚点说明必须在 base_prompt 中
assert "不可修改" in plan["base_prompt"]
# ── 飞轮聚合 ───────────────────────────────────────────────
class TestPreferenceAggregator:
def test_cold_start_when_few_events(self):
result = aggregate_preference_context([], PRODUCT, workspace_id=1, product_id=1)
assert result["injected_count"] == 0
assert "冷启动" in result["recent_preference"]
def test_aggregate_top_angles(self):
events = [
{"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "痛点切入", "signal_weight": 3, "reason": ""},
{"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "痛点切入", "signal_weight": 3, "reason": ""},
{"signal_type": "approve", "workspace_id": 1, "product_id": 1, "angle_label": "场景型", "signal_weight": 5, "reason": ""},
{"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "避坑型", "signal_weight": 3, "reason": ""},
{"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "痛点切入", "signal_weight": 3, "reason": ""},
{"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "避坑型", "signal_weight": 3, "reason": ""},
]
result = aggregate_preference_context(events, PRODUCT, workspace_id=1, product_id=1)
assert result["injected_count"] == 6
assert "痛点切入" in result["recent_preference"]
assert "偏好角度" in result["prompt_fragment"]
def test_reject_reason_in_prompt(self):
events = [
{"signal_type": "reject_with_reason", "workspace_id": 1, "product_id": 1,
"angle_label": "", "signal_weight": -3, "reason": "标题太硬广"},
*[{"signal_type": "text_select", "workspace_id": 1, "product_id": 1,
"angle_label": "痛点切入", "signal_weight": 3, "reason": ""} for _ in range(5)],
]
result = aggregate_preference_context(events, PRODUCT, workspace_id=1, product_id=1)
assert "标题太硬广" in result["prompt_fragment"]
def test_product_isolation(self):
"""不同 product_id 的事件不会混进来"""
events = [
{"signal_type": "text_select", "workspace_id": 1, "product_id": 2,
"angle_label": "成分党", "signal_weight": 3, "reason": ""},
*[{"signal_type": "text_select", "workspace_id": 1, "product_id": 1,
"angle_label": "场景型", "signal_weight": 3, "reason": ""} for _ in range(5)],
]
result = aggregate_preference_context(events, PRODUCT, workspace_id=1, product_id=1)
assert "成分党" not in result["prompt_fragment"]
def test_collect_event_structure(self):
event = collect_preference_event("text_select", user_id=1, workspace_id=1, product_id=1, angle_label="痛点切入")
assert event["signal_weight"] == 3
assert event["data_ownership"] == "client_data"
# ── prompt_composer ────────────────────────────────────────
class TestPromptComposer:
def test_compose_variants_returns_two_strings(self):
sys_p, user_p = compose_variants(PRODUCT, count=5)
assert isinstance(sys_p, str) and len(sys_p) > 0
assert isinstance(user_p, str) and len(user_p) > 0
def test_compose_variants_includes_product_name(self):
_, user_p = compose_variants(PRODUCT, count=3)
assert "倍分子素颜霜" in user_p
def test_compose_variants_includes_count(self):
_, user_p = compose_variants(PRODUCT, count=7)
assert "7" in user_p
def test_compose_variants_flywheel_injected(self):
fragment = "偏好角度参考:场景型、痛点切入"
_, user_p = compose_variants(PRODUCT, count=3, flywheel_context=fragment)
assert "场景型" in user_p
def test_compose_preference_context_delegates(self):
# compose_preference_context 应委托给 aggregate_preference_context
events = [
{"signal_type": "text_select", "workspace_id": 1, "product_id": 1,
"angle_label": "场景型", "signal_weight": 3, "reason": ""}
for _ in range(6)
]
result = compose_preference_context(events, PRODUCT, workspace_id=1, product_id=1)
assert "injected_count" in result
assert result["injected_count"] == 6
assert "prompt_fragment" in result
def test_parse_model_output_valid_json(self):
raw = '[{"title":"标题1","content":"正文","tags":[],"angle":"场景型"}]'
parsed = parse_model_output(raw)
assert len(parsed) == 1 and parsed[0]["title"] == "标题1"
def test_parse_model_output_markdown_wrapped(self):
raw = "```json\n[{\"title\":\"a\",\"content\":\"b\",\"tags\":[]}]\n```"
parsed = parse_model_output(raw)
assert len(parsed) == 1
def test_compose_image_prompt_includes_role_and_product(self):
vs = {"style": "ins摆拍风", "color_palette": "米白+杏色", "base_prompt": "产品近景"}
prompt = compose_image_prompt("hook", vs, PRODUCT)
assert "hook" in prompt
assert "倍分子素颜霜" in prompt
assert "ins摆拍风" in prompt