#!/usr/bin/env python3
"""人味打分器 —— 把一批回复文本按 Claude/GPT 基线指标打分。

用法:
    python3 score.py <file_or_dir> [<file_or_dir> ...] [--detail]
    python3 score.py --baseline          # 直接跑两份原始语料做对照

输入支持:
    *.json / *.jsonl  -> 每项为 {"text": "..."} 或纯字符串
    *.txt / *.md      -> 用一行 `===` 分隔多条回复；若整体不分割则视为一条

指标含义见 rules.md 末尾。
"""
import sys, os, re, json, glob

BOLD = re.compile(r"\*\*[^*\n]+\*\*")
HR   = re.compile(r"^\s*(-{3,}|\*{3,}|_{3,})\s*$", re.M)
BULLET = re.compile(r"^\s*(-\s|\*\s|\d+\.\s)", re.M)
HEADER = re.compile(r"^\s{0,3}#{1,6}\s", re.M)
OFFER  = re.compile(r"要不要我|我可以帮你|需要我|希望对你有帮助|希望能帮到你|如果你愿意|如果你需要")
EMOJI  = re.compile(r"[\U0001F300-\U0001FAFF\u2600-\u27BF\uFE0F]")
EMOJI_HEAD = re.compile("^\\s*[\U0001F300-\U0001FAFF\u2600-\u27BF\uFE0F]")
TABLE  = re.compile(r"^\s*\|.*\|\s*$", re.M)
OPEN1  = re.compile(r"^\s*[哈哦对好诶嗯行哎咦]")

# 句式病（真正的 AI 味在句式和结构层，不在加粗）
PATTERNS = {
    "不是A而是B": re.compile(r"不是[^，。；\n]{1,20}[，,]?\s*而是"),
    "首先其次最后": re.compile(r"首先|其次|再者|最后一点|综上所述"),
    "各有优劣": re.compile(r"各有优劣|各有利弊|视情况而定|取决[于乎]"),
    "不一定但": re.compile(r"不一定[^。\n]{0,30}但"),
    "X不等于Y": re.compile(r"[^\s，。]{2,12}不等于[^\s，。]{2,12}"),
    "不妨/建议你": re.compile(r"不妨|建议你|你可以试试"),
    "不是没": re.compile(r"并非[^。\n]{0,20}而是|与其说[^。\n]{0,20}不如说"),
    "结尾升华": re.compile(r"(希望|愿|总之|归根结底|说到底)[^。\n]{0,40}(帮助|有用|顺利|愉快)"),
}

def load(path):
    out = []
    if os.path.isdir(path):
        for f in sorted(glob.glob(os.path.join(path, "*"))):
            out += load(f)
        return out
    ext = os.path.splitext(path)[1].lower()
    try:
        raw = open(path, encoding="utf-8").read()
    except Exception:
        return out
    if ext in (".json", ".jsonl"):
        if ext == ".jsonl":
            items = [json.loads(l) for l in raw.splitlines() if l.strip()]
        else:
            items = json.loads(raw)
            if isinstance(items, dict):
                items = items.get("items") or items.get("data") or [items]
        for it in items:
            t = it.get("text") if isinstance(it, dict) else it
            if isinstance(t, str) and t.strip():
                out.append(t.strip())
    else:
        parts = re.split(r"^\s*===\s*$", raw, flags=re.M)
        out += [p.strip() for p in parts if p.strip()]
    return out

def stats(texts):
    n = len(texts)
    if not n:
        return None
    agg = {k: 0.0 for k in
           ("chars", "lines", "linelen", "short", "bold", "bullet", "hr",
            "header", "offer", "qend", "emoji", "emojihead", "table",
            "short300", "open1", "dash")}
    for t in texts:
        lines = [l for l in t.split("\n") if l.strip()]
        nb = len(lines) or 1
        agg["chars"]   += len(t)
        agg["lines"]   += len(lines)
        agg["linelen"] += sum(len(l.strip()) for l in lines) / nb
        agg["short"]   += sum(1 for l in lines if len(l.strip()) <= 12) / nb
        agg["bold"]    += 1 if BOLD.search(t) else 0
        agg["bullet"]  += 1 if BULLET.search(t) else 0
        agg["hr"]      += 1 if HR.search(t) else 0
        agg["header"]  += 1 if HEADER.search(t) else 0
        agg["offer"]   += 1 if OFFER.search(t) else 0
        agg["emoji"]   += len(EMOJI.findall(t))
        agg["emojihead"] += 1 if any(EMOJI_HEAD.match(l) for l in lines) else 0
        agg["table"]   += 1 if TABLE.search(t) else 0
        agg["short300"] += 1 if len(t) <= 300 else 0
        agg["open1"]   += 1 if OPEN1.match(t) else 0
        agg["dash"]    += 1 if ("—" in t or " - " in t) else 0
        agg["qend"]    += 1 if lines and lines[-1].strip().endswith(("?", "？")) else 0
    pct = lambda k: 100.0 * agg[k] / n
    return {
        "n": n,
        "均字符": agg["chars"] / n,
        "均行数": agg["lines"] / n,
        "均行长": agg["linelen"] / n,
        "短行率%": pct("short"),
        "加粗率%": pct("bold"),
        "bullet率%": pct("bullet"),
        "分隔线率%": pct("hr"),
        "小标题率%": pct("header"),
        "末行问句%": pct("qend"),
        "要不要我类%": pct("offer"),
        "emoji个/条": agg["emoji"] / n,
        "emoji当分点%": pct("emojihead"),
        "表格率%": pct("table"),
        "中位字符": sorted(len(t) for t in texts)[n // 2],
        "300字内%": pct("short300"),
        "单字接话%": pct("open1"),
        "含破折号%": pct("dash"),
    }

def main():
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    detail = "--detail" in sys.argv
    if "--baseline" in sys.argv or not args:
        args = ["/root/tmp/humanvoice/claude_msgs.json",
                "/root/tmp/humanvoice/gpt_msgs.json"]
    for path in args:
        texts = load(path)
        if not texts:
            print(f"[{path}] 没读到内容"); continue
        s = stats(texts)
        # gpt_msgs.json 是混合的，按 group 再分一次
        name = os.path.basename(path.rstrip("/")) or path
        print(f"\n== {name} ==")
        print("  ".join(f"{k}={v:.1f}" if isinstance(v, float) else f"{k}={v}"
                        for k, v in s.items()))
        hits = {name: sum(1 for t in texts if rx.search(t)) for name, rx in PATTERNS.items()}
        if any(hits.values()):
            print("  句式: " + "  ".join(f"{k}={100.0*v/len(texts):.1f}%" for k, v in hits.items() if v))
        if detail:
            for t in texts:
                flag = []
                if BOLD.search(t): flag.append("加粗")
                if HR.search(t): flag.append("分隔线")
                if BULLET.search(t): flag.append("分点")
                if OFFER.search(t): flag.append("收口话术")
                for name, rx in PATTERNS.items():
                    if rx.search(t):
                        flag.append("句式:" + name)
                if len(t) > 400: flag.append("长(%d)" % len(t))
                if flag:
                    print("  ⚠", "/".join(flag), "|", t.replace("\n", " ")[:70])

if __name__ == "__main__":
    main()
