seawolf2357 commited on
Commit
2931217
ยท
verified ยท
1 Parent(s): c95c402

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +413 -61
app.py CHANGED
@@ -1,8 +1,8 @@
1
  """
2
- AI ๊ธ€ ํŒ๋ณ„๊ธฐ v5.0 โ€” 5์ถ• AI ํƒ์ง€ + ํ’ˆ์งˆ ์ธก์ • + LLM ๊ต์ฐจ๊ฒ€์ฆ + ํ‘œ์ ˆ ๊ฒ€์‚ฌ
3
  โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
4
- 5์ถ• AI ํƒ์ง€ | 6ํ•ญ๋ชฉ ํ’ˆ์งˆ | LLM ๊ต์ฐจ๊ฒ€์ฆ (GPT-OSS-120B ยท Qwen3-32B ยท Kimi-K2)
5
- โ˜… LLM ๊ต์ฐจ๊ฒ€์ฆ: 3๋ชจ๋ธ (GPT-OSS/Qwen3/Kimi-K2) ํˆฌํ‘œ + ๊ฐ•๊ฑดํ•œ ํŒŒ์‹ฑ
6
  โ˜… ํ‘œ์ ˆ: Brave Search ๋ณ‘๋ ฌ(์ตœ๋Œ€20) + KCI/RISS/ARXIV + Gemini + CopyKiller ๋ณด๊ณ ์„œ
7
  โ˜… ๋ฌธ์„œ: PDFยทDOCXยทHWPยทHWPXยทTXT ์—…๋กœ๋“œ โ†’ ์„น์…˜๋ณ„ ํžˆํŠธ๋งต + PDF ๋ณด๊ณ ์„œ
8
  """
@@ -688,6 +688,283 @@ def analyze_model_fingerprint(text, sentences):
688
  base = 85 if mx>=50 else 65 if mx>=35 else 45 if mx>=20 else 25 if mx>=10 else 10
689
  return {"score":min(95, base + multi_bonus),"model_scores":{k:v for k,v in ms.items() if k not in ("๋น„๊ฒฉ์‹AI","์˜์–ดAI") or v > 0}}
690
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
692
  # ํ’ˆ์งˆ
693
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -835,30 +1112,40 @@ AIํ™•๋ฅ : 75%
835
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
836
  # ์ข…ํ•ฉ ํŒ์ • (์ผ๊ด€๋œ ๊ธฐ์ค€)
837
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
838
- def compute_verdict(scores, llm_score=-1, sent_avg=-1):
839
- w={"ํ†ต๊ณ„":.08,"๋ฌธ์ฒด":.30,"๋ฐ˜๋ณต์„ฑ":.12,"๊ตฌ์กฐ":.15,"์ง€๋ฌธ":.35}
840
  ws=sum(scores[k]*w[k] for k in w)
841
 
842
- # โ˜… ๊ต์ฐจ ์‹ ํ˜ธ ๋ถ€์ŠคํŠธ โ€” ๋ฌธ์ฒด/์ง€๋ฌธ ์ค‘์‹ฌ
843
- style = scores["๋ฌธ์ฒด"]; fp = scores["์ง€๋ฌธ"]; rep = scores["๋ฐ˜๋ณต์„ฑ"]; struct = scores["๊ตฌ์กฐ"]
844
- if style >= 35 and fp >= 35: ws += 8 # ๋ฌธ์ฒด+์ง€๋ฌธ ๋™์‹œ โ†’ ๊ฐ•ํ•œ AI ์‹ ํ˜ธ
845
- elif style >= 30 and fp >= 25: ws += 4
846
- if style >= 30 and rep >= 25 and fp >= 20: ws += 4 # 3์ถ• ์•ฝ์‹ ํ˜ธ
847
- if fp >= 45: ws += 3 # ๊ฐ•ํ•œ ์ง€๋ฌธ ๋‹จ๋… ๋ถ€์ŠคํŠธ
848
- if struct >= 50 and style >= 30: ws += 3 # ์ถ”์ƒ์ +๊ฒฉ์‹ ๋ฌธ์ฒด
 
 
 
 
 
 
849
 
850
- # โ˜… ๋ฌธ์žฅ ์ˆ˜์ค€ ๋ถ€์ŠคํŠธ (๋Œ์–ด๋‚ด๋ฆฌ์ง€ ์•Š์Œ)
851
- if sent_avg >= 0 and sent_avg > ws:
852
- ws = ws * 0.80 + sent_avg * 0.20
 
 
 
 
853
 
854
  hi=sum(1 for v in scores.values() if v>=50)
855
  if hi>=4: ws+=8
856
  elif hi>=3: ws+=5
857
  elif hi>=2: ws+=2
858
 
859
- # โ˜… ์ธ๊ฐ„ ๊ฒฉ์‹๋ฌธ ํ• ์ธ โ€” ์ง€๋ฌธ์ด ๋‚ฎ๊ณ  ๊ตฌ์กฐ๊ฐ€ ๊ตฌ์ฒด์ (๋‚ฎ์€)์ธ ๊ฒฝ์šฐ๋งŒ
860
- if style < 40 and fp <= 20 and rep < 22 and struct < 35:
861
- ws -= 5 # ๊ฒฉ์‹์ด์ง€๋งŒ AI ์ง€๋ฌธ ์—†๊ณ  ๊ตฌ์ฒด์  = ์ธ๊ฐ„
862
 
863
  lo=sum(1 for v in scores.values() if v<20)
864
  if lo>=3: ws-=8
@@ -876,10 +1163,12 @@ def quick_score(text):
876
  sc={"ํ†ต๊ณ„":analyze_statistics(text,sents,words)["score"],"๋ฌธ์ฒด":analyze_korean_style(text,sents,morphs)["score"],
877
  "๋ฐ˜๋ณต์„ฑ":analyze_repetition(text,sents,words)["score"],"๊ตฌ์กฐ":analyze_structure(text,sents)["score"],
878
  "์ง€๋ฌธ":analyze_model_fingerprint(text,sents)["score"]}
879
- # ๋ฌธ์žฅ ์ˆ˜์ค€ ํ‰๊ท  ๊ณ„์‚ฐ
880
- sent_scores = [score_sentence(s)[0] for s in sents]
881
- sent_avg = sum(sent_scores)/len(sent_scores) if sent_scores else -1
882
- fs,v,lv=compute_verdict(sc, sent_avg=sent_avg); return fs,v,lv,sc
 
 
883
 
884
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
885
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -1433,26 +1722,41 @@ def run_detection(text, progress=gr.Progress()):
1433
  if not text or len(text.strip())<50: return "<div style='padding:20px;text-align:center;color:#888;'>โš ๏ธ ์ตœ์†Œ 50์ž</div>",""
1434
  text=text.strip()
1435
  progress(0.05); sents=split_sentences(text); words=split_words(text); morphs=get_morphemes(text)
1436
- progress(0.15); s1=analyze_statistics(text,sents,words)
1437
- progress(0.28); s2=analyze_korean_style(text,sents,morphs)
1438
- progress(0.38); s3=analyze_repetition(text,sents,words)
1439
- progress(0.48); s4=analyze_structure(text,sents)
1440
- progress(0.55); s5=analyze_model_fingerprint(text,sents)
1441
- progress(0.62); qr=analyze_quality(text,sents,words,morphs)
 
 
1442
  progress(0.75); lr=llm_cross_check(text)
1443
  sc={"ํ†ต๊ณ„":s1["score"],"๋ฌธ์ฒด":s2["score"],"๋ฐ˜๋ณต์„ฑ":s3["score"],"๊ตฌ์กฐ":s4["score"],"์ง€๋ฌธ":s5["score"]}
1444
- # ๋ฌธ์žฅ๋ณ„ ์ ์ˆ˜ (ํƒญ2์™€ ๋™์ผ ๊ธฐ์ค€)
1445
- sent_scores = [score_sentence(s)[0] for s in sents]
1446
- sent_avg = sum(sent_scores)/len(sent_scores) if sent_scores else -1
1447
- fs,verdict,level=compute_verdict(sc,lr["score"],sent_avg=sent_avg)
 
 
 
 
1448
  progress(0.95)
1449
  cm={"ai_high":("#FF4444","#FFE0E0","๋†’์Œ"),"ai_medium":("#FF8800","#FFF0DD","์ค‘๊ฐ„~๋†’์Œ"),"ai_low":("#DDAA00","#FFFBE0","์ค‘๊ฐ„"),"uncertain":("#888","#F0F0F0","๋‚ฎ์Œ"),"human":("#22AA44","#E0FFE8","๋งค์šฐ ๋‚ฎ์Œ")}
1450
  fg,bg,conf=cm.get(level,("#888","#F0F0F0","?"))
1451
- ms=s5.get("model_scores",{}); tm=max(ms,key=ms.get) if ms else "N/A"; tms=ms.get(tm,0)
1452
- mt=f"{tm} ({tms}์ )" if tms>=15 else "ํŠน์ • ๋ถˆ๊ฐ€"
1453
 
1454
- ai_sents = sum(1 for s in sent_scores if s >= 40)
1455
- human_sents = sum(1 for s in sent_scores if s < 20)
 
 
 
 
 
 
 
 
 
 
 
1456
 
1457
  def gb(l,s,w="",desc=""):
1458
  c="#FF4444" if s>=70 else "#FF8800" if s>=50 else "#DDAA00" if s>=35 else "#22AA44"
@@ -1460,15 +1764,20 @@ def run_detection(text, progress=gr.Progress()):
1460
  dt=f"<div style='font-size:9px;color:#888;margin-top:1px;'>{desc}</div>" if desc else ""
1461
  return f"<div style='margin:4px 0;'><div style='display:flex;justify-content:space-between;'><span style='font-size:11px;font-weight:600;'>{l}{wt}</span><span style='font-size:11px;font-weight:700;color:{c};'>{s}</span></div><div style='background:#E8E8E8;border-radius:4px;height:7px;'><div style='background:{c};height:100%;width:{s}%;border-radius:4px;'></div></div>{dt}</div>"
1462
 
 
1463
  mb=""
1464
  for mn in ["GPT","Claude","Gemini","Perplexity"]:
1465
- s=ms.get(mn,0); mc="#FF4444" if s>=40 else "#FF8800" if s>=20 else "#CCC"
1466
- mb+=f"<div style='display:flex;align-items:center;gap:4px;margin:2px 0;'><span style='width:60px;font-size:10px;font-weight:600;'>{mn}</span><div style='flex:1;background:#E8E8E8;border-radius:3px;height:5px;'><div style='background:{mc};height:100%;width:{s}%;'></div></div><span style='font-size:9px;width:18px;text-align:right;color:{mc};'>{s}</span></div>"
 
 
 
 
1467
 
1468
  # LLM ์„น์…˜
1469
  ls=""
1470
  if lr["score"]>=0:
1471
- lsc=lr["score"]; lc="#FF4444" if lsc>=70 else "#FF8800" if lsc>=50 else "#22AA44"
1472
  lr_rows="".join(f"<div style='font-size:9px;color:#555;'>{mn}: {lr['detail'].get(mn,'โ€”')}</div>" for _,mn in LLM_JUDGES)
1473
  ls=f"<div style='margin-top:8px;padding:8px;background:#F8F8FF;border-radius:6px;border:1px solid #E0E0FF;'><div style='font-size:10px;font-weight:700;margin-bottom:3px;'>๐Ÿค– LLM ๊ต์ฐจ๊ฒ€์ฆ (ํ‰๊ท  {lsc}%)</div>{lr_rows}</div>"
1474
  else: ls="<div style='margin-top:6px;padding:4px 8px;background:#F5F5F5;border-radius:4px;color:#999;font-size:9px;'>๐Ÿค– GROQ_API_KEY ๋ฏธ์„ค์ •</div>"
@@ -1479,20 +1788,62 @@ def run_detection(text, progress=gr.Progress()):
1479
  c="#22AA44" if s>=70 else "#4ECDC4" if s>=55 else "#DDAA00" if s>=40 else "#FF8800"
1480
  return f"<div style='margin:2px 0;display:flex;align-items:center;gap:4px;'><span style='width:50px;font-size:10px;'>{l}</span><div style='flex:1;background:#E8E8E8;border-radius:3px;height:5px;'><div style='background:{c};height:100%;width:{s}%;'></div></div><span style='font-size:9px;color:{c};width:18px;text-align:right;'>{s}</span></div>"
1481
 
1482
- # ํŒ์ • ์ด์œ  ์„ค๋ช…
1483
- reasons = []
1484
- if sc["๋ฌธ์ฒด"] >= 70: reasons.append("๊ฒฉ์‹์ฒด ์ข…๊ฒฐ์–ด๋ฏธ๊ฐ€ ๋Œ€๋ถ€๋ถ„, AIํ˜• ์ ‘์†์‚ฌยท์ƒํˆฌํ‘œํ˜„ ๋‹ค์ˆ˜ ๊ฐ์ง€")
1485
- elif sc["๋ฌธ์ฒด"] >= 50: reasons.append("๊ฒฉ์‹์ฒด์™€ AIํ˜• ํ‘œํ˜„์ด ํ˜ผ์žฌ")
1486
- if sc["ํ†ต๊ณ„"] >= 70: reasons.append("๋ฌธ์žฅ ๊ธธ์ด๊ฐ€ ๋งค์šฐ ๊ท ์ผํ•˜์—ฌ ๊ธฐ๊ณ„์  ํŒจํ„ด")
1487
- elif sc["ํ†ต๊ณ„"] >= 50: reasons.append("๋ฌธ์žฅ ๊ธธ์ด ๋ณ€๋™์„ฑ์ด ๋‚ฎ์Œ")
1488
- if sc["๋ฐ˜๋ณต์„ฑ"] >= 50: reasons.append("๋ฌธ๋‘ ์ ‘์†์‚ฌ ๋ฐ˜๋ณต, n-gram ํŒจํ„ด ๊ฐ์ง€")
1489
- if sc["๊ตฌ์กฐ"] >= 50: reasons.append("๋ฆฌ์ŠคํŠธ/๋งˆํฌ๋‹ค์šด ๋“ฑ ๊ตฌ์กฐ์  ์„œ์‹ ์‚ฌ์šฉ")
1490
- if tms >= 20: reasons.append(f"{tm} ๋ชจ๋ธ์˜ ํŠน์ง•์  ํ‘œํ˜„ ๊ฐ์ง€")
 
 
 
 
 
 
1491
  if not reasons: reasons.append("์ธ๊ฐ„์  ํ‘œํ˜„์ด ์šฐ์„ธํ•˜๋ฉฐ AI ํŒจํ„ด์ด ์•ฝํ•จ")
1492
- reason_html = '<br>'.join(f"โ€ข {r}" for r in reasons)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1493
 
1494
  html=f"""<div style="font-family:'Pretendard','Noto Sans KR',sans-serif;max-width:720px;margin:0 auto;">
1495
- <!-- ํŒ์ • ์นด๋“œ -->
1496
  <div style="background:{bg};border:2px solid {fg};border-radius:14px;padding:20px;margin-bottom:12px;">
1497
  <div style="display:flex;align-items:center;gap:16px;">
1498
  <div style="text-align:center;min-width:100px;">
@@ -1516,12 +1867,13 @@ def run_detection(text, progress=gr.Progress()):
1516
 
1517
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
1518
  <div style="background:#FAFAFA;border-radius:8px;padding:10px;">
1519
- <div style="font-size:10px;font-weight:700;margin-bottom:4px;">๐Ÿ“Š AI ํƒ์ง€ 5์ถ•</div>
1520
- {gb('โ‘  ํ†ต๊ณ„',sc['ํ†ต๊ณ„'],'.25','๋ฌธ์žฅ ๊ธธ์ด ๊ท ์ผ๋„ยท์—”ํŠธ๋กœํ”ผ')}
1521
- {gb('โ‘ก ๋ฌธ์ฒด',sc['๋ฌธ์ฒด'],'.30','๊ฒฉ์‹์ฒดยท์ ‘์†์‚ฌยท์ƒํˆฌํ‘œํ˜„')}
1522
- {gb('โ‘ข ๋ฐ˜๋ณต',sc['๋ฐ˜๋ณต์„ฑ'],'.15','n-gramยท๋ฌธ๋‘ ๋ฐ˜๋ณต')}
1523
- {gb('โ‘ฃ ๊ตฌ์กฐ',sc['๊ตฌ์กฐ'],'.15','๋ฌธ๋‹จยท๋ฆฌ์ŠคํŠธยท์„œ์‹')}
1524
- {gb('โ‘ค ์ง€๋ฌธ',sc['์ง€๋ฌธ'],'.15','GPT/Claude/Gemini ํŠน์ง•')}
 
1525
  </div>
1526
  <div style="background:#FAFAFA;border-radius:8px;padding:10px;">
1527
  <div style="font-size:10px;font-weight:700;margin-bottom:4px;">๐Ÿ” ๋ชจ๋ธ ์ง€๋ฌธ</div>
@@ -1535,9 +1887,9 @@ def run_detection(text, progress=gr.Progress()):
1535
  </div>
1536
  </div>
1537
  </div>
1538
- {ls}
1539
  </div>"""
1540
- log=f"AI:{fs}์  [{verdict}] ์‹ ๋ขฐ:{conf} | ๋ชจ๋ธ:{mt} | ํ’ˆ์งˆ:{qr['grade']}({qr['score']})\n์ถ•: ํ†ต๊ณ„{sc['ํ†ต๊ณ„']} ๋ฌธ์ฒด{sc['๋ฌธ์ฒด']} ๋ฐ˜๋ณต{sc['๋ฐ˜๋ณต์„ฑ']} ๊ตฌ์กฐ{sc['๊ตฌ์กฐ']} ์ง€๋ฌธ{sc['์ง€๋ฌธ']}"
1541
  return html, log
1542
 
1543
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@@ -1630,7 +1982,7 @@ def run_document_analysis(file, progress=gr.Progress()):
1630
  sents_all = split_sentences(full_text)
1631
  words_all = split_words(full_text)
1632
  morphs_all = get_morphemes(full_text)
1633
- total_score, total_verdict, total_level, total_axes = quick_score(full_text)
1634
  quality = analyze_quality(full_text, sents_all, words_all, morphs_all)
1635
 
1636
  # LLM ๊ต์ฐจ๊ฒ€์ฆ (์ „์ฒด)
@@ -1639,7 +1991,7 @@ def run_document_analysis(file, progress=gr.Progress()):
1639
  if llm_result["score"] >= 0:
1640
  _sent_scores = [score_sentence(s)[0] for s in sents_all]
1641
  _sent_avg = sum(_sent_scores)/len(_sent_scores) if _sent_scores else -1
1642
- total_score, total_verdict, total_level = compute_verdict(total_axes, llm_result["score"], sent_avg=_sent_avg)
1643
 
1644
  # ์„น์…˜๋ณ„ ๋ถ„์„
1645
  progress(0.45, f"{len(sections)}๊ฐœ ์„น์…˜ ๋ถ„์„...")
@@ -1648,7 +2000,7 @@ def run_document_analysis(file, progress=gr.Progress()):
1648
  if len(sec.strip()) < 20:
1649
  section_results.append({"idx": i+1, "text": sec, "score": -1, "verdict": "๋„ˆ๋ฌด ์งง์Œ", "skipped": True})
1650
  continue
1651
- s_score, s_verdict, s_level, s_axes = quick_score(sec)
1652
  # ๋ฌธ์žฅ๋ณ„ ํ•˜์ด๋ผ์ดํŠธ
1653
  sec_sents = split_sentences(sec)
1654
  sent_scores = []
 
1
  """
2
+ AI ๊ธ€ ํŒ๋ณ„๊ธฐ v5.1 โ€” 5์ถ•+Perplexity+Humanizer+๋ชจ๋ธ์ถ”์ • + ํ’ˆ์งˆ + LLM๊ต์ฐจ๊ฒ€์ฆ + ํ‘œ์ ˆ
3
  โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
4
+ โ˜… v5.1 3๋Œ€ ํ‚ฌ๋Ÿฌ: Perplexity ํ™•๋ฅ ๋ถ„์„ ยท Humanizer/Bypasser ํƒ์ง€ ยท AI ๋ชจ๋ธ ์ถ”์ •
5
+ โ˜… 5์ถ• AI ํƒ์ง€ | 6ํ•ญ๋ชฉ ํ’ˆ์งˆ | LLM ๊ต์ฐจ๊ฒ€์ฆ (GPT-OSS-120B ยท Qwen3-32B ยท Kimi-K2)
6
  โ˜… ํ‘œ์ ˆ: Brave Search ๋ณ‘๋ ฌ(์ตœ๋Œ€20) + KCI/RISS/ARXIV + Gemini + CopyKiller ๋ณด๊ณ ์„œ
7
  โ˜… ๋ฌธ์„œ: PDFยทDOCXยทHWPยทHWPXยทTXT ์—…๋กœ๋“œ โ†’ ์„น์…˜๋ณ„ ํžˆํŠธ๋งต + PDF ๋ณด๊ณ ์„œ
8
  """
 
688
  base = 85 if mx>=50 else 65 if mx>=35 else 45 if mx>=20 else 25 if mx>=10 else 10
689
  return {"score":min(95, base + multi_bonus),"model_scores":{k:v for k,v in ms.items() if k not in ("๋น„๊ฒฉ์‹AI","์˜์–ดAI") or v > 0}}
690
 
691
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
692
+ # โ˜…โ˜…โ˜… ํ‚ฌ๋Ÿฌ ๊ธฐ๋Šฅ โ‘  โ€” Perplexity ๊ธฐ๋ฐ˜ AI ํ™•๋ฅ  (v5.1)
693
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
694
+ # AI ํ…์ŠคํŠธ๋Š” ์˜ˆ์ธก ๊ฐ€๋Šฅ๋„๊ฐ€ ๋†’์Œ (๋‚ฎ์€ Perplexity) โ†’ ๋ฌธ์ž/ํ˜•ํƒœ์†Œ n-gram ๊ธฐ๋ฐ˜
695
+ def analyze_perplexity(text, sentences, morphemes):
696
+ """ํ•œ๊ตญ์–ด ํŠนํ™” Perplexity + Burstiness โ€” ๋ฌธ์ž ์—”ํŠธ๋กœํ”ผ ๋ณด์ •"""
697
+ if len(sentences) < 2: return {"score": 40, "entropy": 0, "variance": 0, "order": 0, "zipf": 0}
698
+
699
+ # === 1. ๋ฌธ์ž ๋ฐ”์ด๊ทธ๋žจ ์—”ํŠธ๋กœํ”ผ (ํ˜•ํƒœ์†Œ๋ณด๋‹ค ์•ˆ์ •์ ) ===
700
+ chars = [c for c in text if c.strip()]
701
+ char_score = 45
702
+ if len(chars) >= 30:
703
+ cbigrams = [(chars[i], chars[i+1]) for i in range(len(chars)-1)]
704
+ cb_freq = Counter(cbigrams)
705
+ total_cb = len(cbigrams)
706
+ char_entropy = -sum((cnt/total_cb)*math.log2(cnt/total_cb) for cnt in cb_freq.values())
707
+ # AI ํ•œ๊ตญ์–ด: ~7~9๋น„ํŠธ, ์ธ๊ฐ„: ~9~12๋น„ํŠธ
708
+ if char_entropy < 7.5: char_score = 78
709
+ elif char_entropy < 8.5: char_score = 62
710
+ elif char_entropy < 9.5: char_score = 42
711
+ elif char_entropy < 10.5: char_score = 25
712
+ else: char_score = 12
713
+
714
+ # === 2. ๋ฌธ์žฅ ๊ธธ์ด Burstiness (CV) ===
715
+ sl = [len(s) for s in sentences]
716
+ burst_score = 45
717
+ if len(sl) >= 3:
718
+ avg = sum(sl)/len(sl)
719
+ std = math.sqrt(sum((l-avg)**2 for l in sl)/len(sl))
720
+ cv = std/(avg+1e-10)
721
+ if cv < 0.15: burst_score = 82
722
+ elif cv < 0.25: burst_score = 62
723
+ elif cv < 0.40: burst_score = 38
724
+ elif cv < 0.60: burst_score = 20
725
+ else: burst_score = 8
726
+
727
+ # === 3. ๋ฌธ์žฅ๊ฐ„ ์–ดํœ˜๋ฐ€๋„(TTR) ํŽธ์ฐจ ===
728
+ sent_ttr = []
729
+ for s in sentences:
730
+ sw = split_words(s)
731
+ if len(sw) >= 3:
732
+ sent_ttr.append(len(set(sw))/len(sw))
733
+ ttr_score = 42
734
+ if len(sent_ttr) >= 3:
735
+ avg_ttr = sum(sent_ttr)/len(sent_ttr)
736
+ std_ttr = math.sqrt(sum((t-avg_ttr)**2 for t in sent_ttr)/len(sent_ttr))
737
+ if std_ttr < 0.04: ttr_score = 75
738
+ elif std_ttr < 0.08: ttr_score = 55
739
+ elif std_ttr < 0.15: ttr_score = 35
740
+ else: ttr_score = 15
741
+
742
+ # === 4. ์ข…๊ฒฐ์–ด๋ฏธ ์—”ํŠธ๋กœํ”ผ ===
743
+ endings = [s.rstrip('.!?\u2026')[-3:] for s in sentences if len(s) >= 5]
744
+ end_score = 40
745
+ if len(endings) >= 3:
746
+ ef = Counter(endings)
747
+ end_ent = -sum((c/len(endings))*math.log2(c/len(endings)) for c in ef.values())
748
+ max_ent = math.log2(len(ef)) if len(ef) > 1 else 1
749
+ norm_ent = end_ent / (max_ent + 1e-10)
750
+ if norm_ent < 0.5: end_score = 72
751
+ elif norm_ent < 0.7: end_score = 50
752
+ elif norm_ent < 0.85: end_score = 32
753
+ else: end_score = 15
754
+
755
+ final = int(char_score * 0.30 + burst_score * 0.30 + ttr_score * 0.20 + end_score * 0.20)
756
+ return {"score": final, "entropy": char_score, "variance": burst_score, "order": ttr_score, "zipf": end_score}
757
+
758
+
759
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
760
+ # โ˜…โ˜…โ˜… ํ‚ฌ๋Ÿฌ ๊ธฐ๋Šฅ โ‘ก โ€” Humanizer/Bypasser ํƒ์ง€ (v5.1)
761
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
762
+ # ํŒจ๋Ÿฌํ”„๋ ˆ์ด์ฆˆ ๋„๊ตฌ(QuillBot ๋“ฑ)๋กœ ์ˆ˜์ •๋œ AI ๊ธ€์˜ ์ž”์กด ํ”์  ํƒ์ง€
763
+
764
+ # Humanizer ํŠน์œ  ํŒจํ„ด
765
+ HUMANIZER_OVERSUBST = re.compile(r'ํ™œ์šฉํ•˜๋‹ค|์ด์šฉํ•˜๋‹ค|์‚ฌ์šฉํ•˜๋‹ค|์ ์šฉํ•˜๋‹ค|๋„์ž…ํ•˜๋‹ค|์ฑ„ํƒํ•˜๋‹ค|์ˆ˜ํ–‰ํ•˜๋‹ค|์ง„ํ–‰ํ•˜๋‹ค|์‹ค์‹œํ•˜๋‹ค|์‹คํ–‰ํ•˜๋‹ค')
766
+ HUMANIZER_AWKWARD = re.compile(r'๊ทธ๊ฒƒ์€|์ด๊ฒƒ์€|์ €๊ฒƒ์€|ํ•ด๋‹น ์‚ฌํ•ญ|์•ž์„œ ์–ธ๊ธ‰ํ•œ|์ „์ˆ ํ•œ|์ƒ๊ธฐํ•œ|๊ธฐ์ˆ ๋œ')
767
+ HUMANIZER_PASSIVE = re.compile(r'๋˜์–ด์ง€[๊ณ ๋Š”๋ฉฐ]|ํ•˜๊ฒŒ ๋˜์—ˆ[๋‹ค์Šต]|์ˆ˜ํ–‰๋˜[์—ˆ์–ด]|์ง„ํ–‰๋˜[์—ˆ์–ด]|์‹ค์‹œ๋˜[์—ˆ์–ด]|ํ™œ์šฉ๋˜[์—ˆ์–ด]')
768
+
769
+ def analyze_humanizer(text, sentences, words, morphemes):
770
+ """Humanizer/Bypasser ํƒ์ง€ โ€” AI ์›๋ฌธ ํŒจ๋Ÿฌํ”„๋ ˆ์ด์ฆˆ ํ”์  ๋ถ„์„"""
771
+ if len(sentences) < 2: return {"score": 20, "signals": []}
772
+ signals = []
773
+
774
+ # === 1. ๋™์˜์–ด ๊ณผ๋‹ค ์น˜ํ™˜ ํŒจํ„ด ===
775
+ # Humanizer๋Š” ๊ฐ™์€ ์˜๋ฏธ๋ฅผ ๋‹ค์–‘ํ•œ ๋™์˜์–ด๋กœ ๋ฐ”๊ฟˆ โ†’ ๋น„์ž์—ฐ์  ์–ดํœ˜ ๋ถ„์‚ฐ
776
+ content_words = [f for f, t in morphemes if t in ('NNG', 'NNP', 'VV', 'VA')]
777
+ if len(content_words) >= 10:
778
+ cw_freq = Counter(content_words)
779
+ # Hapax ratio ๊ทน๋‹จ์ ์œผ๋กœ ๋†’์œผ๋ฉด ๋™์˜์–ด ์น˜ํ™˜ ์˜์‹ฌ
780
+ hapax = sum(1 for c in cw_freq.values() if c == 1)
781
+ hapax_ratio = hapax / len(cw_freq) if cw_freq else 0
782
+ # ์ž์—ฐ์–ด: 0.4~0.7, Humanizer: 0.8+ (๋ชจ๋“  ๋‹จ์–ด๋ฅผ ๋‹ค ๋ฐ”๊ฟ”์„œ)
783
+ if hapax_ratio > 0.95 and len(content_words) >= 30:
784
+ signals.append(("๋™์˜์–ด๊ณผ๋‹ค์น˜ํ™˜", 20, "ํ•ต์‹ฌ ์–ดํœ˜๊ฐ€ ๊ณผ๋„ํ•˜๊ฒŒ ๋ถ„์‚ฐ"))
785
+ elif hapax_ratio > 0.90 and len(content_words) >= 25:
786
+ signals.append(("๋™์˜์–ด์น˜ํ™˜์˜์‹ฌ", 12, "์–ดํœ˜ ๋ฐ˜๋ณต ํšŒํ”ผ ํŒจํ„ด"))
787
+
788
+ # === 2. ๊ตฌ์กฐ ๋ณด์กด + ์–ดํœ˜๋งŒ ๋ณ€๊ฒฝ ํŒจํ„ด ===
789
+ # ์›๋ฌธ AI์˜ ๋ฌธ์žฅ ๊ตฌ์กฐ(๊ธธ์ด, ์–ด์ˆœ)๋Š” ์œ ์ง€๋˜๋ฉด์„œ ๋‹จ์–ด๋งŒ ๋ฐ”๋€œ
790
+ sl = [len(s) for s in sentences]
791
+ if len(sl) >= 4:
792
+ avg = sum(sl) / len(sl)
793
+ cv = math.sqrt(sum((l - avg)**2 for l in sl) / len(sl)) / (avg + 1e-10)
794
+ # ๋ฌธ์žฅ ๊ธธ์ด ๊ท ์ผ + ์–ดํœ˜ ๋‹ค์–‘ = Humanizer ํŒจํ„ด
795
+ unique_ratio = len(set(words)) / len(words) if words else 0
796
+ if cv < 0.20 and unique_ratio > 0.80 and len(sentences) >= 5:
797
+ signals.append(("๊ตฌ์กฐ๋ณด์กด์–ดํœ˜๋ณ€๊ฒฝ", 18, "๋ฌธ์žฅ ๊ตฌ์กฐ ๊ท ์ผ + ๋น„์ •์ƒ์  ์–ดํœ˜ ๋‹ค์–‘์„ฑ"))
798
+
799
+ # === 3. ์ž”์กด AI ํŒจํ„ด ===
800
+ # Humanizer๊ฐ€ ๋†“์น˜๊ธฐ ์‰ฌ์šด AI ํ”์ 
801
+ residual = 0
802
+ # ์ ‘์†์‚ฌ ์œ„์น˜ ๊ทœ์น™์„ฑ (Humanizer๋Š” ์ ‘์†์‚ฌ๋ฅผ ์ž˜ ์•ˆ ๋ฐ”๊ฟˆ)
803
+ conn_positions = []
804
+ for i, s in enumerate(sentences):
805
+ stripped = s.strip()
806
+ for c in ['๋˜ํ•œ','ํŠนํžˆ','ํ•œํŽธ','๋”๋ถˆ์–ด','์•„์šธ๋Ÿฌ','๋‚˜์•„๊ฐ€','์ด์—','๊ฒŒ๋‹ค๊ฐ€','๋ฐ˜๋ฉด','๊ฒฐ๊ตญ']:
807
+ if stripped.startswith(c):
808
+ conn_positions.append(i)
809
+ break
810
+ if len(conn_positions) >= 2:
811
+ # ๋“ฑ๊ฐ„๊ฒฉ ์ ‘์†์‚ฌ = AI ์›๋ฌธ ๊ตฌ์กฐ ์ž”์กด
812
+ gaps = [conn_positions[i] - conn_positions[i-1] for i in range(1, len(conn_positions))]
813
+ if gaps and max(gaps) - min(gaps) <= 1: # ๊ฑฐ์˜ ๋“ฑ๊ฐ„๊ฒฉ
814
+ signals.append(("์ ‘์†์‚ฌ๋“ฑ๊ฐ„๊ฒฉ์ž”์กด", 15, "์ ‘์†์‚ฌ ๋ฐฐ์น˜๊ฐ€ ๊ทœ์น™์  (AI ์›๋ฌธ ๊ตฌ์กฐ ์ž”์กด)"))
815
+ residual += 15
816
+
817
+ # === 4. ๋ถ€์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€์ฒด ํ‘œํ˜„ ===
818
+ oversubst = len(HUMANIZER_OVERSUBST.findall(text))
819
+ awkward = len(HUMANIZER_AWKWARD.findall(text))
820
+ passive = len(HUMANIZER_PASSIVE.findall(text))
821
+ if oversubst >= 3:
822
+ signals.append(("์œ ์‚ฌ๋™์‚ฌ๋‚œ๋ฌด", 12, f"ํ™œ์šฉ/์ด์šฉ/์‚ฌ์šฉ/์ ์šฉ ๋“ฑ {oversubst}๊ฐœ"))
823
+ if awkward >= 2:
824
+ signals.append(("์–ด์ƒ‰ํ•œ์ง€์‹œ์–ด", 10, f"ํ•ด๋‹น/์ „์ˆ /์ƒ๊ธฐ ๋“ฑ {awkward}๊ฐœ"))
825
+ if passive >= 3:
826
+ signals.append(("์ด์ค‘ํ”ผ๋™๊ณผ๋‹ค", 15, f"๋˜์–ด์ง€/์ˆ˜ํ–‰๋˜ ๋“ฑ {passive}๊ฐœ"))
827
+
828
+ # === 5. ๋ฌธ์žฅ ์œ ํ˜• ๋‹จ์กฐ + ์–ด๋ฏธ ๋‹ค์–‘ = Humanizer ์‹œ๊ทธ๋‹ˆ์ฒ˜ ===
829
+ # AI ์›๋ฌธ: ๋ฌธ์žฅ์œ ํ˜• ๋‹จ์กฐ + ์–ด๋ฏธ ๋‹จ์กฐ
830
+ # ์ธ๊ฐ„: ๋ฌธ์žฅ์œ ํ˜• ๋‹ค์–‘ + ์–ด๋ฏธ ๋‹ค์–‘
831
+ # Humanizer: ๋ฌธ์žฅ์œ ํ˜• ๋‹จ์กฐ(๋ฐ”๊ฟ€ ์ˆ˜ ์—†์Œ) + ์–ด๋ฏธ ๋‹ค์–‘(๋ฐ”๊ฟˆ) โ†’ ๋ถ€์กฐํ™”
832
+ endings = [s.rstrip('.!?')[-2:] for s in sentences if len(s) >= 4]
833
+ end_types = len(set(endings)) / len(endings) if endings else 0
834
+ has_question = any(s.strip().endswith('?') for s in sentences)
835
+ has_exclaim = any(s.strip().endswith('!') for s in sentences)
836
+ sent_type_variety = sum([has_question, has_exclaim])
837
+ if sent_type_variety == 0 and end_types > 0.85 and len(sentences) >= 6:
838
+ signals.append(("์œ ํ˜•๋‹จ์กฐ์–ด๋ฏธ๋‹ค์–‘", 12, "์„œ์ˆ ๋ฌธ๋งŒ + ์ข…๊ฒฐ์–ด๋ฏธ ๊ณผ๋‹ค ๋‹ค์–‘ = Humanizer ํŒจํ„ด"))
839
+
840
+ # === 6. ๋ฌธ์žฅ ์‹œ์ž‘ ํŒจํ„ด ๋ถˆ์ผ์น˜ ===
841
+ # Humanizer๋Š” ๋ฌธ๋‘๋ฅผ ๋‹ค์–‘ํ•˜๊ฒŒ ๋ฐ”๊พธ๋ ค ํ•˜๋‚˜, ํ•œ๊ตญ์–ด์—์„œ๋Š” ๋ถ€์ž์—ฐ์Šค๋Ÿฌ์›€ ์œ ๋ฐœ
842
+ starters = [s.strip()[:3] for s in sentences if len(s) >= 6]
843
+ starter_unique = len(set(starters)) / len(starters) if starters else 0
844
+ if starter_unique >= 0.98 and len(sentences) >= 7:
845
+ signals.append(("๋ฌธ๋‘๊ณผ๋‹ค๋‹ค์–‘", 8, "๋ชจ๋“  ๋ฌธ์žฅ ์‹œ์ž‘์ด ๋‹ค๋ฆ„ (์ž์—ฐ์Šค๋Ÿฝ์ง€ ์•Š์€ ๋‹ค์–‘์„ฑ)"))
846
+
847
+ total = sum(s[1] for s in signals)
848
+ # ์ ์ˆ˜ํ™”
849
+ if total >= 45: score = 85
850
+ elif total >= 30: score = 68
851
+ elif total >= 20: score = 52
852
+ elif total >= 10: score = 35
853
+ else: score = 15
854
+
855
+ return {"score": score, "signals": signals, "total_evidence": total}
856
+
857
+ # โ•โ•โ•โ•โ•โ•โ•๏ฟฝ๏ฟฝ๏ฟฝโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
858
+ # โ˜…โ˜…โ˜… ํ‚ฌ๋Ÿฌ ๊ธฐ๋Šฅ โ‘ข โ€” AI ๋ชจ๋ธ ์ถ”์ • (v5.1)
859
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
860
+ # ๋ชจ๋ธ๋ณ„ ๊ณ ์œ  ํŠน์„ฑ์œผ๋กœ ์ž‘์„ฑ ๋ชจ๋ธ ์ถ”์ •
861
+
862
+ MODEL_PROFILES = {
863
+ "GPT": {
864
+ "style": ["๊ฒฉ์‹์ฒด ~์Šต๋‹ˆ๋‹ค", "๋˜ํ•œ/ํŠนํžˆ ์ ‘์†์‚ฌ", "~์— ๋Œ€ํ•ด", "~๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค"],
865
+ "markers": ["๋‹ค์–‘ํ•œ", "์ค‘์š”ํ•œ ์—ญํ• ", "๊ธ์ •์ ์ธ", "๋ˆˆ๋ถ€์‹ ", "์ฃผ๋ชฉํ•  ๋งŒํ•œ", "์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค"],
866
+ "structure": "๊ท ์ผํ•œ ๋ฌธ๋‹จ, ์„œ๋ก -๋ณธ๋ก -๊ฒฐ๋ก  ๊ตฌ์กฐ, ๋งˆํฌ๋‹ค์šด ์„ ํ˜ธ",
867
+ "endings": ["์Šต๋‹ˆ๋‹ค", "์žˆ์Šต๋‹ˆ๋‹ค", "๋ฉ๋‹ˆ๋‹ค", "์ž…๋‹ˆ๋‹ค"],
868
+ "connectors": ["๋˜ํ•œ", "ํŠนํžˆ", "ํ•œํŽธ", "์ด์ฒ˜๋Ÿผ"],
869
+ },
870
+ "Claude": {
871
+ "style": ["๋งฅ๋ฝ ์ œ์‹œ", "๊ท ํ˜• ์žกํžŒ", "์‚ฌ๋ ค ๊นŠ์€ ์–ด์กฐ", "์–‘๋ณด ํ›„ ์ฃผ์žฅ"],
872
+ "markers": ["ํฅ๋ฏธ๋กœ์šด ์งˆ๋ฌธ", "๋ณต์žกํ•œ ์ฃผ์ œ", "๋งฅ๋ฝ์—์„œ", "๊ท ํ˜• ์žกํžŒ", "์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค", "ํ•œ ๊ฐ€์ง€ ์ฃผ์˜ํ• "],
873
+ "structure": "์ž์—ฐ์Šค๋Ÿฌ์šด ํ๋ฆ„, ์–‘๋ณด-์ฃผ์žฅ ๊ตฌ๋ฌธ ์„ ํ˜ธ, ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜",
874
+ "endings": ["๋„ค์š”", "์ž…๋‹ˆ๋‹ค", "์žˆ์Šต๋‹ˆ๋‹ค", "์Šต๋‹ˆ๋‹ค"],
875
+ "connectors": ["ํ•œํŽธ", "๋ฌผ๋ก ", "๋‹ค๋งŒ", "์ด์™€ ๊ด€๋ จํ•ด"],
876
+ },
877
+ "Gemini": {
878
+ "style": ["์ •๋ณด ๋‚˜์—ดํ˜•", "~์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค", "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ธฐ๋ฐ˜"],
879
+ "markers": ["์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค", "๋„์›€์ด ๋˜์…จ์œผ๋ฉด", "์ถ”๊ฐ€ ์งˆ๋ฌธ", "์ฐธ๊ณ ๋กœ"],
880
+ "structure": "๋ฆฌ์ŠคํŠธ/๋ฒˆํ˜ธ ๋งค๊ธฐ๊ธฐ ์„ ํ˜ธ, ํ—ค๋” ํ™œ์šฉ, ์ •๋ณด ๋ฐ€๋„ ๋†’์Œ",
881
+ "endings": ["์Šต๋‹ˆ๋‹ค", "์žˆ์Šต๋‹ˆ๋‹ค", "์„ธ์š”"],
882
+ "connectors": ["๋˜ํ•œ", "๊ทธ๋ฆฌ๊ณ ", "์ฐธ๊ณ ๋กœ"],
883
+ },
884
+ "Perplexity": {
885
+ "style": ["์ถœ์ฒ˜ ์ธ์šฉํ˜•", "~์— ๋”ฐ๋ฅด๋ฉด", "์ˆ˜์น˜ ์ œ์‹œ", "๊ฒƒ์œผ๋กœ ๋‚˜ํƒ€๋‚ฌ๋‹ค"],
886
+ "markers": ["์— ๋”ฐ๋ฅด๋ฉด", "๊ฒƒ์œผ๋กœ ๋‚˜ํƒ€๋‚ฌ", "๊ฒƒ์œผ๋กœ ์กฐ์‚ฌ๋", "๊ฒƒ์œผ๋กœ ์ง‘๊ณ„๋", "๋ฐœํ‘œํ–ˆ", "๋ณด๋„์— ๋”ฐ๋ฅด๋ฉด"],
887
+ "structure": "ํŒฉํŠธ ์ค‘์‹ฌ, ์ˆ˜์น˜ ์ธ์šฉ ๋‹ค์ˆ˜, ์ถœ์ฒ˜ ๋ช…์‹œ ์Šคํƒ€์ผ",
888
+ "endings": ["์Šต๋‹ˆ๋‹ค", "๋‚˜ํƒ€๋‚ฌ๋‹ค", "๋ฐํ˜”๋‹ค", "์ „ํ–ˆ๋‹ค"],
889
+ "connectors": ["ํ•œํŽธ", "๋˜ํ•œ", "์ด์—"],
890
+ },
891
+ }
892
+
893
+ def estimate_model(text, sentences, morphemes, model_scores):
894
+ """AI ๋ชจ๋ธ ์ถ”์ • โ€” ๋ณตํ•ฉ ์ฆ๊ฑฐ ๊ธฐ๋ฐ˜"""
895
+ evidence = {m: {"score": 0, "reasons": []} for m in MODEL_PROFILES}
896
+
897
+ sl = text.lower()
898
+
899
+ for model, profile in MODEL_PROFILES.items():
900
+ # 1. FP ์ ์ˆ˜ ๋ฐ˜์˜ (๊ธฐ์กด ์ง€๋ฌธ ๋ถ„์„)
901
+ fp_score = model_scores.get(model, 0)
902
+ evidence[model]["score"] += fp_score * 0.4
903
+ if fp_score >= 20:
904
+ evidence[model]["reasons"].append(f"์ง€๋ฌธ ๋งค์นญ {fp_score}์ ")
905
+
906
+ # 2. ๋งˆ์ปค ๋งค์นญ
907
+ marker_cnt = sum(1 for m in profile["markers"] if m in text)
908
+ if marker_cnt >= 2:
909
+ evidence[model]["score"] += marker_cnt * 8
910
+ evidence[model]["reasons"].append(f"ํŠน์œ  ํ‘œํ˜„ {marker_cnt}๊ฐœ")
911
+
912
+ # 3. ์ข…๊ฒฐ์–ด๋ฏธ ํŒจํ„ด
913
+ end_match = 0
914
+ for s in sentences:
915
+ for e in profile["endings"]:
916
+ if s.rstrip('.!?').endswith(e):
917
+ end_match += 1; break
918
+ if sentences:
919
+ end_ratio = end_match / len(sentences)
920
+ if end_ratio > 0.7:
921
+ evidence[model]["score"] += 12
922
+ evidence[model]["reasons"].append(f"์ข…๊ฒฐ์–ด๋ฏธ {end_ratio:.0%} ์ผ์น˜")
923
+
924
+ # 4. ์ ‘์†์‚ฌ ํŒจํ„ด
925
+ conn_match = sum(1 for s in sentences if any(s.strip().startswith(c) for c in profile["connectors"]))
926
+ if conn_match >= 2:
927
+ evidence[model]["score"] += conn_match * 4
928
+ evidence[model]["reasons"].append(f"์ ‘์†์‚ฌ ํŒจํ„ด {conn_match}ํšŒ")
929
+
930
+ # Perplexity ํŠนํ™”: ์ˆ˜์น˜ + ์ถœ์ฒ˜ ์ธ์šฉ
931
+ number_citations = len(re.findall(r'\d+[%๋งŒ์–ต์กฐ]|์— ๋”ฐ๋ฅด๋ฉด|๊ฒƒ์œผ๋กœ ๋‚˜ํƒ€๋‚ฌ|๋ฐœํ‘œํ–ˆ', text))
932
+ if number_citations >= 3:
933
+ evidence["Perplexity"]["score"] += number_citations * 5
934
+ evidence["Perplexity"]["reasons"].append(f"์ˆ˜์น˜/์ธ์šฉ {number_citations}ํšŒ")
935
+
936
+ # Claude ํŠนํ™”: ์–‘๋ณด-์ฃผ์žฅ ๊ตฌ๋ฌธ
937
+ concession_cnt = len(AI_CONCESSION.findall(text))
938
+ if concession_cnt >= 1:
939
+ evidence["Claude"]["score"] += concession_cnt * 10
940
+ evidence["Claude"]["reasons"].append(f"์–‘๋ณด-์ฃผ์žฅ ๊ตฌ๋ฌธ {concession_cnt}ํšŒ")
941
+
942
+ # ์ •๋ ฌ ๋ฐ ํŒ์ •
943
+ ranked = sorted(evidence.items(), key=lambda x: x[1]["score"], reverse=True)
944
+ top = ranked[0]
945
+ second = ranked[1] if len(ranked) > 1 else None
946
+
947
+ if top[1]["score"] < 10:
948
+ return {"model": "ํŠน์ • ๋ถˆ๊ฐ€", "confidence": "๋‚ฎ์Œ", "detail": evidence, "ranked": ranked}
949
+
950
+ # ์‹ ๋ขฐ๋„ ๊ณ„์‚ฐ
951
+ gap = top[1]["score"] - (second[1]["score"] if second else 0)
952
+ if gap >= 20 and top[1]["score"] >= 30:
953
+ conf = "๋†’์Œ"
954
+ elif gap >= 10 and top[1]["score"] >= 20:
955
+ conf = "์ค‘๏ฟฝ๏ฟฝ"
956
+ else:
957
+ conf = "๋‚ฎ์Œ"
958
+
959
+ return {
960
+ "model": top[0],
961
+ "confidence": conf,
962
+ "score": top[1]["score"],
963
+ "reasons": top[1]["reasons"],
964
+ "detail": evidence,
965
+ "ranked": ranked
966
+ }
967
+
968
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
969
  # ํ’ˆ์งˆ
970
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 
1112
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1113
  # ์ข…ํ•ฉ ํŒ์ • (์ผ๊ด€๋œ ๊ธฐ์ค€)
1114
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1115
+ def compute_verdict(scores, llm_score=-1, sent_avg=-1, ppx_score=-1, hum_score=-1):
1116
+ w={"ํ†ต๊ณ„":.06,"๋ฌธ์ฒด":.25,"๋ฐ˜๋ณต์„ฑ":.10,"๊ตฌ์กฐ":.12,"์ง€๋ฌธ":.30}
1117
  ws=sum(scores[k]*w[k] for k in w)
1118
 
1119
+ # โ˜… Perplexity ์ถ• ํ†ตํ•ฉ (17%)
1120
+ if ppx_score >= 0: ws += ppx_score * 0.17
1121
+
1122
+ # โ˜… ๊ต์ฐจ ์‹ ํ˜ธ ๋ถ€์ŠคํŠธ
1123
+ style=scores["๋ฌธ์ฒด"]; fp=scores["์ง€๋ฌธ"]; rep=scores["๋ฐ˜๋ณต์„ฑ"]; struct=scores["๊ตฌ์กฐ"]
1124
+ if style>=35 and fp>=35: ws+=8
1125
+ elif style>=30 and fp>=25: ws+=4
1126
+ if style>=30 and rep>=25 and fp>=20: ws+=4
1127
+ if fp>=45: ws+=3
1128
+ if struct>=50 and style>=30: ws+=3
1129
+ # Perplexity + ์ง€๋ฌธ ๋™์‹œ ๋ถ€์ŠคํŠธ
1130
+ if ppx_score>=55 and fp>=35: ws+=5
1131
+ if ppx_score>=65 and style>=35: ws+=3
1132
 
1133
+ # โ˜… Humanizer ํƒ์ง€ ์‹œ ํŠน๋ณ„ ๋ถ€์ŠคํŠธ
1134
+ if hum_score>=50:
1135
+ ws=max(ws, 45) # Humanizer ํ™•์ธ โ†’ ์ตœ์†Œ AI ์˜์‹ฌ ์ค‘๊ฐ„
1136
+ ws += (hum_score-50)*0.15
1137
+
1138
+ # โ˜… ๋ฌธ์žฅ ์ˆ˜์ค€ ๋ถ€์ŠคํŠธ
1139
+ if sent_avg>=0 and sent_avg>ws: ws=ws*0.80+sent_avg*0.20
1140
 
1141
  hi=sum(1 for v in scores.values() if v>=50)
1142
  if hi>=4: ws+=8
1143
  elif hi>=3: ws+=5
1144
  elif hi>=2: ws+=2
1145
 
1146
+ # โ˜… ์ธ๊ฐ„ ๊ฒฉ์‹๋ฌธ ํ• ์ธ
1147
+ if style<40 and fp<=20 and rep<22 and struct<35 and (ppx_score<0 or ppx_score<40):
1148
+ ws-=5
1149
 
1150
  lo=sum(1 for v in scores.values() if v<20)
1151
  if lo>=3: ws-=8
 
1163
  sc={"ํ†ต๊ณ„":analyze_statistics(text,sents,words)["score"],"๋ฌธ์ฒด":analyze_korean_style(text,sents,morphs)["score"],
1164
  "๋ฐ˜๋ณต์„ฑ":analyze_repetition(text,sents,words)["score"],"๊ตฌ์กฐ":analyze_structure(text,sents)["score"],
1165
  "์ง€๋ฌธ":analyze_model_fingerprint(text,sents)["score"]}
1166
+ sent_scores=[score_sentence(s)[0] for s in sents]
1167
+ sent_avg=sum(sent_scores)/len(sent_scores) if sent_scores else -1
1168
+ ppx=analyze_perplexity(text,sents,morphs)
1169
+ hum=analyze_humanizer(text,sents,words,morphs)
1170
+ fs,v,lv=compute_verdict(sc, sent_avg=sent_avg, ppx_score=ppx["score"], hum_score=hum["score"])
1171
+ return fs,v,lv,sc,ppx,hum
1172
 
1173
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
1174
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 
1722
  if not text or len(text.strip())<50: return "<div style='padding:20px;text-align:center;color:#888;'>โš ๏ธ ์ตœ์†Œ 50์ž</div>",""
1723
  text=text.strip()
1724
  progress(0.05); sents=split_sentences(text); words=split_words(text); morphs=get_morphemes(text)
1725
+ progress(0.12); s1=analyze_statistics(text,sents,words)
1726
+ progress(0.22); s2=analyze_korean_style(text,sents,morphs)
1727
+ progress(0.30); s3=analyze_repetition(text,sents,words)
1728
+ progress(0.38); s4=analyze_structure(text,sents)
1729
+ progress(0.45); s5=analyze_model_fingerprint(text,sents)
1730
+ progress(0.52); ppx=analyze_perplexity(text,sents,morphs)
1731
+ progress(0.58); hum=analyze_humanizer(text,sents,words,morphs)
1732
+ progress(0.65); qr=analyze_quality(text,sents,words,morphs)
1733
  progress(0.75); lr=llm_cross_check(text)
1734
  sc={"ํ†ต๊ณ„":s1["score"],"๋ฌธ์ฒด":s2["score"],"๋ฐ˜๋ณต์„ฑ":s3["score"],"๊ตฌ์กฐ":s4["score"],"์ง€๋ฌธ":s5["score"]}
1735
+ sent_scores=[score_sentence(s)[0] for s in sents]
1736
+ sent_avg=sum(sent_scores)/len(sent_scores) if sent_scores else -1
1737
+
1738
+ # โ˜… ๋ชจ๋ธ ์ถ”์ •
1739
+ ms_raw=s5.get("model_scores",{})
1740
+ model_est=estimate_model(text,sents,morphs,ms_raw)
1741
+
1742
+ fs,verdict,level=compute_verdict(sc,lr["score"],sent_avg=sent_avg,ppx_score=ppx["score"],hum_score=hum["score"])
1743
  progress(0.95)
1744
  cm={"ai_high":("#FF4444","#FFE0E0","๋†’์Œ"),"ai_medium":("#FF8800","#FFF0DD","์ค‘๊ฐ„~๋†’์Œ"),"ai_low":("#DDAA00","#FFFBE0","์ค‘๊ฐ„"),"uncertain":("#888","#F0F0F0","๋‚ฎ์Œ"),"human":("#22AA44","#E0FFE8","๋งค์šฐ ๋‚ฎ์Œ")}
1745
  fg,bg,conf=cm.get(level,("#888","#F0F0F0","?"))
 
 
1746
 
1747
+ # ๋ชจ๋ธ ์ถ”์ • ํ‘œ์‹œ
1748
+ est_model=model_est.get("model","ํŠน์ • ๋ถˆ๊ฐ€")
1749
+ est_conf=model_est.get("confidence","๋‚ฎ์Œ")
1750
+ est_reasons=model_est.get("reasons",[])
1751
+ if est_model!="ํŠน์ • ๋ถˆ๊ฐ€" and est_conf!="๋‚ฎ์Œ":
1752
+ mt=f"{est_model} (์‹ ๋ขฐ: {est_conf})"
1753
+ elif est_model!="ํŠน์ • ๋ถˆ๊ฐ€":
1754
+ mt=f"{est_model} (์ฐธ๊ณ )"
1755
+ else:
1756
+ mt="ํŠน์ • ๋ถˆ๊ฐ€"
1757
+
1758
+ ai_sents=sum(1 for s in sent_scores if s>=40)
1759
+ human_sents=sum(1 for s in sent_scores if s<20)
1760
 
1761
  def gb(l,s,w="",desc=""):
1762
  c="#FF4444" if s>=70 else "#FF8800" if s>=50 else "#DDAA00" if s>=35 else "#22AA44"
 
1764
  dt=f"<div style='font-size:9px;color:#888;margin-top:1px;'>{desc}</div>" if desc else ""
1765
  return f"<div style='margin:4px 0;'><div style='display:flex;justify-content:space-between;'><span style='font-size:11px;font-weight:600;'>{l}{wt}</span><span style='font-size:11px;font-weight:700;color:{c};'>{s}</span></div><div style='background:#E8E8E8;border-radius:4px;height:7px;'><div style='background:{c};height:100%;width:{s}%;border-radius:4px;'></div></div>{dt}</div>"
1766
 
1767
+ # ๋ชจ๋ธ ์ง€๋ฌธ ๋ฐ”
1768
  mb=""
1769
  for mn in ["GPT","Claude","Gemini","Perplexity"]:
1770
+ s=ms_raw.get(mn,0); mc="#FF4444" if s>=40 else "#FF8800" if s>=20 else "#CCC"
1771
+ # ์ถ”์ • ๋ชจ๋ธ ํ‘œ์‹œ
1772
+ tag=""
1773
+ if mn==est_model and est_conf!="๋‚ฎ์Œ":
1774
+ tag=f" <span style='background:#FF4444;color:white;font-size:7px;padding:0 3px;border-radius:3px;'>์ถ”์ •</span>"
1775
+ mb+=f"<div style='display:flex;align-items:center;gap:4px;margin:2px 0;'><span style='width:66px;font-size:10px;font-weight:600;'>{mn}{tag}</span><div style='flex:1;background:#E8E8E8;border-radius:3px;height:5px;'><div style='background:{mc};height:100%;width:{s}%;'></div></div><span style='font-size:9px;width:18px;text-align:right;color:{mc};'>{s}</span></div>"
1776
 
1777
  # LLM ์„น์…˜
1778
  ls=""
1779
  if lr["score"]>=0:
1780
+ lsc=lr["score"]
1781
  lr_rows="".join(f"<div style='font-size:9px;color:#555;'>{mn}: {lr['detail'].get(mn,'โ€”')}</div>" for _,mn in LLM_JUDGES)
1782
  ls=f"<div style='margin-top:8px;padding:8px;background:#F8F8FF;border-radius:6px;border:1px solid #E0E0FF;'><div style='font-size:10px;font-weight:700;margin-bottom:3px;'>๐Ÿค– LLM ๊ต์ฐจ๊ฒ€์ฆ (ํ‰๊ท  {lsc}%)</div>{lr_rows}</div>"
1783
  else: ls="<div style='margin-top:6px;padding:4px 8px;background:#F5F5F5;border-radius:4px;color:#999;font-size:9px;'>๐Ÿค– GROQ_API_KEY ๋ฏธ์„ค์ •</div>"
 
1788
  c="#22AA44" if s>=70 else "#4ECDC4" if s>=55 else "#DDAA00" if s>=40 else "#FF8800"
1789
  return f"<div style='margin:2px 0;display:flex;align-items:center;gap:4px;'><span style='width:50px;font-size:10px;'>{l}</span><div style='flex:1;background:#E8E8E8;border-radius:3px;height:5px;'><div style='background:{c};height:100%;width:{s}%;'></div></div><span style='font-size:9px;color:{c};width:18px;text-align:right;'>{s}</span></div>"
1790
 
1791
+ # โ˜… ํŒ์ • ์ด์œ  (3๋Œ€ ํ‚ฌ๋Ÿฌ ํ†ตํ•ฉ)
1792
+ reasons=[]
1793
+ if sc["๋ฌธ์ฒด"]>=70: reasons.append("๊ฒฉ์‹์ฒด ์ข…๊ฒฐ์–ด๋ฏธ๊ฐ€ ๋Œ€๋ถ€๋ถ„, AIํ˜• ์ ‘์†์‚ฌยท์ƒํˆฌํ‘œํ˜„ ๋‹ค์ˆ˜ ๊ฐ์ง€")
1794
+ elif sc["๋ฌธ์ฒด"]>=50: reasons.append("๊ฒฉ์‹์ฒด์™€ AIํ˜• ํ‘œํ˜„์ด ํ˜ผ์žฌ")
1795
+ if ppx["score"]>=65: reasons.append(f"ํ…์ŠคํŠธ ์˜ˆ์ธก ๊ฐ€๋Šฅ๋„๊ฐ€ ๋งค์šฐ ๋†’์Œ (Perplexity {ppx['score']}์ )")
1796
+ elif ppx["score"]>=50: reasons.append(f"ํ…์ŠคํŠธ ์˜ˆ์ธก ๊ฐ€๋Šฅ๋„๊ฐ€ ๋†’์Œ (Perplexity {ppx['score']}์ )")
1797
+ if hum["score"]>=50:
1798
+ hum_sigs=", ".join(s[0] for s in hum["signals"][:3])
1799
+ reasons.append(f"โš ๏ธ Humanizer/ํŒจ๋Ÿฌํ”„๋ ˆ์ด์ฆˆ ํ”์  ๊ฐ์ง€ ({hum_sigs})")
1800
+ if sc["ํ†ต๊ณ„"]>=60: reasons.append("๋ฌธ์žฅ ๊ธธ์ด๊ฐ€ ๋งค์šฐ ๊ท ์ผํ•˜์—ฌ ๊ธฐ๊ณ„์  ํŒจํ„ด")
1801
+ if sc["๋ฐ˜๋ณต์„ฑ"]>=50: reasons.append("๋ฌธ๋‘ ์ ‘์†์‚ฌ ๋ฐ˜๋ณต, n-gram ํŒจํ„ด ๊ฐ์ง€")
1802
+ if sc["๊ตฌ์กฐ"]>=50: reasons.append("์ถ”์ƒ์  ์ˆ˜์‹์–ด ๋‹ค์ˆ˜, ๊ตฌ์ฒด์  ์‚ฌ์‹ค ๋ถ€์กฑ")
1803
+ if est_model!="ํŠน์ • ๋ถˆ๊ฐ€" and est_conf!="๋‚ฎ์Œ":
1804
+ est_why=", ".join(est_reasons[:2]) if est_reasons else ""
1805
+ reasons.append(f"๐Ÿ” ์ถ”์ • ๋ชจ๋ธ: <b>{est_model}</b> ({est_why})")
1806
  if not reasons: reasons.append("์ธ๊ฐ„์  ํ‘œํ˜„์ด ์šฐ์„ธํ•˜๋ฉฐ AI ํŒจํ„ด์ด ์•ฝํ•จ")
1807
+ reason_html='<br>'.join(f"โ€ข {r}" for r in reasons)
1808
+
1809
+ # โ˜… Perplexity ์นด๋“œ
1810
+ ppx_c="#FF4444" if ppx["score"]>=65 else "#FF8800" if ppx["score"]>=50 else "#DDAA00" if ppx["score"]>=35 else "#22AA44"
1811
+ ppx_html=f"""<div style='margin-top:8px;padding:8px;background:linear-gradient(135deg,#FFF8F0,#FFF0FF);border-radius:6px;border:1px solid #E8D0FF;'>
1812
+ <div style='font-size:10px;font-weight:700;margin-bottom:4px;'>๐Ÿง  Perplexity ๋ถ„์„ <span style='color:{ppx_c};font-size:12px;font-weight:900;'>{ppx["score"]}์ </span></div>
1813
+ <div style='display:grid;grid-template-columns:1fr 1fr;gap:2px;'>
1814
+ <span style='font-size:9px;color:#777;'>์—”ํŠธ๋กœํ”ผ: {ppx.get("entropy",0)}</span>
1815
+ <span style='font-size:9px;color:#777;'>๋ถ„์‚ฐ๊ท ์ผ: {ppx.get("variance",0)}</span>
1816
+ <span style='font-size:9px;color:#777;'>์–ด์ˆœ์˜ˆ์ธก: {ppx.get("order",0)}</span>
1817
+ <span style='font-size:9px;color:#777;'>Zipf์ ํ•ฉ: {ppx.get("zipf",0)}</span>
1818
+ </div>
1819
+ </div>"""
1820
+
1821
+ # โ˜… Humanizer ํƒ์ง€ ์นด๋“œ
1822
+ hum_html=""
1823
+ if hum["score"]>=30:
1824
+ hc="#FF4444" if hum["score"]>=65 else "#FF8800" if hum["score"]>=50 else "#DDAA00"
1825
+ sig_rows="".join(f"<div style='font-size:9px;color:#555;'>๐Ÿ”ธ {s[0]}: {s[2]}</div>" for s in hum["signals"][:4])
1826
+ hum_html=f"""<div style='margin-top:8px;padding:8px;background:linear-gradient(135deg,#FFF0F0,#FFF5F0);border-radius:6px;border:1px solid #FFD0D0;'>
1827
+ <div style='font-size:10px;font-weight:700;margin-bottom:3px;'>๐Ÿ›ก๏ธ Humanizer ํƒ์ง€ <span style='color:{hc};font-size:12px;font-weight:900;'>{hum["score"]}์ </span></div>
1828
+ {sig_rows}
1829
+ </div>"""
1830
+
1831
+ # โ˜… ๋ชจ๋ธ ์ถ”์ • ์นด๋“œ
1832
+ est_html=""
1833
+ if est_model!="ํŠน์ • ๋ถˆ๊ฐ€":
1834
+ ec="#FF4444" if est_conf=="๋†’์Œ" else "#FF8800" if est_conf=="์ค‘๊ฐ„" else "#DDAA00"
1835
+ ranked_html=""
1836
+ for m, ev in model_est.get("ranked",[])[:4]:
1837
+ ms_c="#FF4444" if ev["score"]>=30 else "#FF8800" if ev["score"]>=15 else "#CCC"
1838
+ bar_w=min(100,int(ev["score"]*1.5))
1839
+ ranked_html+=f"<div style='display:flex;align-items:center;gap:4px;margin:1px 0;'><span style='width:55px;font-size:9px;font-weight:600;'>{m}</span><div style='flex:1;background:#E8E8E8;border-radius:3px;height:4px;'><div style='background:{ms_c};height:100%;width:{bar_w}%;border-radius:3px;'></div></div><span style='font-size:8px;color:{ms_c};'>{ev['score']:.0f}</span></div>"
1840
+ est_html=f"""<div style='margin-top:8px;padding:8px;background:linear-gradient(135deg,#F0F8FF,#F0FFF0);border-radius:6px;border:1px solid #D0E8FF;'>
1841
+ <div style='font-size:10px;font-weight:700;margin-bottom:3px;'>๐ŸŽฏ AI ๋ชจ๋ธ ์ถ”์ •: <span style='color:{ec};font-size:12px;font-weight:900;'>{est_model}</span> <span style='font-size:9px;color:#888;'>(์‹ ๋ขฐ: {est_conf})</span></div>
1842
+ {ranked_html}
1843
+ <div style='font-size:8px;color:#999;margin-top:2px;'>๊ทผ๊ฑฐ: {", ".join(est_reasons[:3]) if est_reasons else "๋ณตํ•ฉ ์ง€ํ‘œ"}</div>
1844
+ </div>"""
1845
 
1846
  html=f"""<div style="font-family:'Pretendard','Noto Sans KR',sans-serif;max-width:720px;margin:0 auto;">
 
1847
  <div style="background:{bg};border:2px solid {fg};border-radius:14px;padding:20px;margin-bottom:12px;">
1848
  <div style="display:flex;align-items:center;gap:16px;">
1849
  <div style="text-align:center;min-width:100px;">
 
1867
 
1868
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
1869
  <div style="background:#FAFAFA;border-radius:8px;padding:10px;">
1870
+ <div style="font-size:10px;font-weight:700;margin-bottom:4px;">๐Ÿ“Š AI ํƒ์ง€ 5์ถ• + Perplexity</div>
1871
+ {gb('โ‘  ํ†ต๊ณ„',sc['ํ†ต๊ณ„'],'.06','Burstinessยท๋ณต์žก๋„ ๊ท ์ผ์„ฑ')}
1872
+ {gb('โ‘ก ๋ฌธ์ฒด',sc['๋ฌธ์ฒด'],'.25','๊ฒฉ์‹ยท์ ‘์†์‚ฌยท์–‘๋ณด๊ตฌ๋ฌธ')}
1873
+ {gb('โ‘ข ๋ฐ˜๋ณต',sc['๋ฐ˜๋ณต์„ฑ'],'.10','n-gramยท๋ฌธ๋‘ยท์ข…๊ฒฐ๋‹ค์–‘์„ฑ')}
1874
+ {gb('โ‘ฃ ๊ตฌ์กฐ',sc['๊ตฌ์กฐ'],'.12','์ถ”์ƒ์„ฑ/๊ตฌ์ฒด์„ฑ')}
1875
+ {gb('โ‘ค ์ง€๋ฌธ',sc['์ง€๋ฌธ'],'.30','GPT/Claude/Gemini/PPX')}
1876
+ {gb('โ‘ฅ PPX',ppx['score'],'.17','์˜ˆ์ธก๊ฐ€๋Šฅ๋„ยท์—”ํŠธ๋กœํ”ผ')}
1877
  </div>
1878
  <div style="background:#FAFAFA;border-radius:8px;padding:10px;">
1879
  <div style="font-size:10px;font-weight:700;margin-bottom:4px;">๐Ÿ” ๋ชจ๋ธ ์ง€๋ฌธ</div>
 
1887
  </div>
1888
  </div>
1889
  </div>
1890
+ {ppx_html}{hum_html}{est_html}{ls}
1891
  </div>"""
1892
+ log=f"AI:{fs}์  [{verdict}] ์‹ ๋ขฐ:{conf} | ๋ชจ๋ธ:{mt} | PPX:{ppx['score']} HUM:{hum['score']} | ํ’ˆ์งˆ:{qr['grade']}({qr['score']})\n์ถ•: ํ†ต๊ณ„{sc['ํ†ต๊ณ„']} ๋ฌธ์ฒด{sc['๋ฌธ์ฒด']} ๋ฐ˜๋ณต{sc['๋ฐ˜๋ณต์„ฑ']} ๊ตฌ์กฐ{sc['๊ตฌ์กฐ']} ์ง€๋ฌธ{sc['์ง€๋ฌธ']} PPX{ppx['score']} HUM{hum['score']}"
1893
  return html, log
1894
 
1895
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 
1982
  sents_all = split_sentences(full_text)
1983
  words_all = split_words(full_text)
1984
  morphs_all = get_morphemes(full_text)
1985
+ total_score, total_verdict, total_level, total_axes, total_ppx, total_hum = quick_score(full_text)
1986
  quality = analyze_quality(full_text, sents_all, words_all, morphs_all)
1987
 
1988
  # LLM ๊ต์ฐจ๊ฒ€์ฆ (์ „์ฒด)
 
1991
  if llm_result["score"] >= 0:
1992
  _sent_scores = [score_sentence(s)[0] for s in sents_all]
1993
  _sent_avg = sum(_sent_scores)/len(_sent_scores) if _sent_scores else -1
1994
+ total_score, total_verdict, total_level = compute_verdict(total_axes, llm_result["score"], sent_avg=_sent_avg, ppx_score=total_ppx["score"], hum_score=total_hum["score"])
1995
 
1996
  # ์„น์…˜๋ณ„ ๋ถ„์„
1997
  progress(0.45, f"{len(sections)}๊ฐœ ์„น์…˜ ๋ถ„์„...")
 
2000
  if len(sec.strip()) < 20:
2001
  section_results.append({"idx": i+1, "text": sec, "score": -1, "verdict": "๋„ˆ๋ฌด ์งง์Œ", "skipped": True})
2002
  continue
2003
+ s_score, s_verdict, s_level, s_axes, _, _ = quick_score(sec)
2004
  # ๋ฌธ์žฅ๋ณ„ ํ•˜์ด๋ผ์ดํŠธ
2005
  sec_sents = split_sentences(sec)
2006
  sent_scores = []