将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
5.2 KiB
Python
126 lines
5.2 KiB
Python
"""
|
||
违禁词三级处理(扒 copy.js sanitizePlanningText 扩展为三级)
|
||
🟢 auto_fix = 自动改写(replacement 字段给出替换词)
|
||
🟡 soft_warn = 软提示(返回建议词,不阻塞)
|
||
🔴 hard_block= 硬拦截(直接返回 None,拦住发布)
|
||
|
||
词库来自数据库 banned_words 表(level + replacement 字段),
|
||
DB 未配时用本模块内置默认词库作冷启动。
|
||
"""
|
||
from __future__ import annotations
|
||
import re
|
||
from dataclasses import dataclass, field
|
||
from typing import Literal
|
||
|
||
BannedLevel = Literal["auto_fix", "soft_warn", "hard_block"]
|
||
|
||
|
||
@dataclass
|
||
class BannedWordEntry:
|
||
word: str
|
||
level: BannedLevel
|
||
replacement: str | None = None # auto_fix 时提供替换词
|
||
|
||
|
||
# ── 默认词库(北哥回填解读与落点 §4.3,数据库未配时使用)─
|
||
DEFAULT_BANNED_WORDS: list[BannedWordEntry] = [
|
||
# 功效违禁(auto_fix:改写成合规表达,对应北哥"提亮肤色感/改善暗沉观感")
|
||
BannedWordEntry("美白", "auto_fix", "提亮肤色感"),
|
||
BannedWordEntry("祛斑", "auto_fix", "改善暗沉观感"),
|
||
# 功效违禁(hard_block:无法合规改写,直接拦截)
|
||
BannedWordEntry("速效", "hard_block"),
|
||
BannedWordEntry("医用", "hard_block"),
|
||
BannedWordEntry("药妆", "hard_block"),
|
||
BannedWordEntry("强效焕白", "hard_block"),
|
||
# 保证性词(soft_warn)
|
||
BannedWordEntry("绝对", "soft_warn"),
|
||
BannedWordEntry("第一名", "soft_warn"),
|
||
BannedWordEntry("再也不", "soft_warn"),
|
||
# 夸张词(soft_warn)
|
||
BannedWordEntry("杀疯了", "soft_warn"),
|
||
BannedWordEntry("秒杀", "soft_warn"),
|
||
BannedWordEntry("震撼", "soft_warn"),
|
||
# AI 味词(auto_fix,置换为口语表达;同时在 _NEGATIVE_WORDS prompt负向约束里已禁止AI写进正文)
|
||
BannedWordEntry("神器", "auto_fix", "好用的"),
|
||
BannedWordEntry("福音", "auto_fix", "适合的"),
|
||
BannedWordEntry("救急单品", "auto_fix", "随手备用的"),
|
||
BannedWordEntry("遮羞布", "auto_fix", "底妆感"), # 北哥原文补录
|
||
BannedWordEntry("不仅而且", "auto_fix", ",另外"),
|
||
BannedWordEntry("焕发", "auto_fix", "呈现"),
|
||
BannedWordEntry("守护", "auto_fix", ""),
|
||
BannedWordEntry("尽享", "auto_fix", "使用"),
|
||
BannedWordEntry("日常维稳", "auto_fix", "日常保养"),
|
||
BannedWordEntry("精简底妆", "auto_fix", "轻便底妆"),
|
||
# 视觉违禁(hard_block,文案含这些词不许过)
|
||
BannedWordEntry("前后对比", "hard_block"),
|
||
BannedWordEntry("使用前后", "hard_block"),
|
||
BannedWordEntry("变白", "auto_fix", "自然光泽感"),
|
||
BannedWordEntry("瑕疵消失", "auto_fix", "妆感更服帖"),
|
||
]
|
||
|
||
|
||
@dataclass
|
||
class CheckResult:
|
||
text: str # 原文(soft_warn/hard_block 场景下保持原文)
|
||
fixed_text: str | None # auto_fix 后的文本;其他级别为 None
|
||
status: Literal["pass", "auto_fixed", "soft_warn", "hard_block"]
|
||
found: list[dict] = field(default_factory=list)
|
||
# found 每项: {"word": str, "level": BannedLevel, "replacement": str|None}
|
||
|
||
|
||
def check_and_fix(
|
||
text: str,
|
||
entries: list[BannedWordEntry] | None = None,
|
||
) -> CheckResult:
|
||
"""
|
||
对一段文本做三级违禁词扫描。
|
||
entries:优先用 DB 词条,为 None 时用默认词库。
|
||
"""
|
||
word_list = entries if entries is not None else DEFAULT_BANNED_WORDS
|
||
found: list[dict] = []
|
||
working = text
|
||
|
||
# 先扫描所有命中
|
||
for entry in word_list:
|
||
if entry.word.lower() in working.lower():
|
||
found.append({
|
||
"word": entry.word,
|
||
"level": entry.level,
|
||
"replacement": entry.replacement,
|
||
})
|
||
|
||
if not found:
|
||
return CheckResult(text=text, fixed_text=None, status="pass", found=[])
|
||
|
||
# 有 hard_block → 直接拦截
|
||
if any(f["level"] == "hard_block" for f in found):
|
||
return CheckResult(text=text, fixed_text=None, status="hard_block", found=found)
|
||
|
||
# 只有 soft_warn → 软提示,不改文字
|
||
if any(f["level"] == "soft_warn" for f in found) and \
|
||
all(f["level"] in ("soft_warn", "auto_fix") for f in found):
|
||
# 仍执行 auto_fix 改写,但结果状态是 soft_warn(优先级高)
|
||
for f in found:
|
||
if f["level"] == "auto_fix" and f["replacement"] is not None:
|
||
working = re.sub(re.escape(f["word"]), f["replacement"], working, flags=re.IGNORECASE)
|
||
return CheckResult(text=text, fixed_text=working, status="soft_warn", found=found)
|
||
|
||
# 只有 auto_fix → 自动改写,返回 fixed_text
|
||
for f in found:
|
||
if f["level"] == "auto_fix" and f["replacement"] is not None:
|
||
working = re.sub(re.escape(f["word"]), f["replacement"], working, flags=re.IGNORECASE)
|
||
return CheckResult(text=text, fixed_text=working, status="auto_fixed", found=found)
|
||
|
||
|
||
def build_entries_from_db(rows: list[dict]) -> list[BannedWordEntry]:
|
||
"""把 DB banned_words 行转成 BannedWordEntry 列表"""
|
||
return [
|
||
BannedWordEntry(
|
||
word=r["word"],
|
||
level=r["level"],
|
||
replacement=r.get("replacement"),
|
||
)
|
||
for r in rows
|
||
if r.get("word") and r.get("level") in ("auto_fix", "soft_warn", "hard_block")
|
||
]
|