Spaces:
Running
Running
| import gradio as gr | |
| import math, re, os, json, random, time, hashlib, zlib, zipfile, tempfile | |
| from collections import Counter | |
| from datetime import datetime | |
| from pathlib import Path | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from xml.etree import ElementTree as ET | |
| from kiwipiepy import Kiwi | |
| KIWI = Kiwi() | |
| try: | |
| import httpx; HAS_HTTPX = True | |
| except ImportError: | |
| HAS_HTTPX = False | |
| try: | |
| from google import genai | |
| from google.genai import types as gtypes | |
| HAS_GENAI = True | |
| except ImportError: | |
| HAS_GENAI = False | |
| try: | |
| import olefile; HAS_OLEFILE = True | |
| except ImportError: | |
| HAS_OLEFILE = False | |
| try: | |
| import pdfplumber; HAS_PDFPLUMBER = True | |
| except ImportError: | |
| HAS_PDFPLUMBER = False | |
| try: | |
| import PyPDF2; HAS_PYPDF2 = True | |
| except ImportError: | |
| HAS_PYPDF2 = False | |
| try: | |
| from docx import Document as DocxDocument; HAS_DOCX = True | |
| except ImportError: | |
| HAS_DOCX = False | |
| GROQ_KEY = os.getenv("GROQ_API_KEY", "") | |
| GEMINI_KEY = os.getenv("GEMINI_API_KEY", "") | |
| BRAVE_KEY = os.getenv("BRAVE_API_KEY", "") | |
| def extract_text_from_pdf(file_path): | |
| """PDF โ ํ ์คํธ (ํ์ด์ง๋ณ ๋ถ๋ฆฌ)""" | |
| pages = [] | |
| if HAS_PDFPLUMBER: | |
| try: | |
| with pdfplumber.open(file_path) as pdf: | |
| for p in pdf.pages: | |
| t = p.extract_text() | |
| if t: pages.append(t) | |
| if pages: return pages, None | |
| except Exception as e: | |
| print(f"pdfplumber: {e}") | |
| if HAS_PYPDF2: | |
| try: | |
| with open(file_path, 'rb') as f: | |
| reader = PyPDF2.PdfReader(f) | |
| for p in reader.pages: | |
| t = p.extract_text() | |
| if t: pages.append(t) | |
| if pages: return pages, None | |
| except Exception as e: | |
| print(f"PyPDF2: {e}") | |
| return None, "PDF ์ถ์ถ ์คํจ (pdfplumber, PyPDF2 ์์)" | |
| def extract_text_from_docx(file_path): | |
| """DOCX โ ํ ์คํธ (๋ฌธ๋จ๋ณ ๋ถ๋ฆฌ)""" | |
| if not HAS_DOCX: return None, "python-docx ์์" | |
| try: | |
| doc = DocxDocument(file_path) | |
| sections = [] | |
| current = [] | |
| for para in doc.paragraphs: | |
| txt = para.text.strip() | |
| if not txt: | |
| if current: | |
| sections.append('\n'.join(current)) | |
| current = [] | |
| else: | |
| current.append(txt) | |
| if current: sections.append('\n'.join(current)) | |
| if sections: return sections, None | |
| return None, "DOCX ํ ์คํธ ์์" | |
| except Exception as e: | |
| return None, f"DOCX ์ค๋ฅ: {e}" | |
| def extract_text_from_txt(file_path): | |
| """TXT/MD/CSV ๋ฑ โ ํ ์คํธ""" | |
| for enc in ['utf-8', 'euc-kr', 'cp949', 'utf-16', 'latin-1']: | |
| try: | |
| with open(file_path, 'r', encoding=enc) as f: | |
| text = f.read() | |
| if text.strip(): | |
| sections = [s.strip() for s in re.split(r'\n{2,}', text) if s.strip()] | |
| return sections if sections else [text], None | |
| except: continue | |
| return None, "ํ ์คํธ ์ธ์ฝ๋ฉ ๊ฐ์ง ์คํจ" | |
| def extract_text_from_hwpx(file_path): | |
| """HWPX (ZIP ๊ธฐ๋ฐ) โ ํ ์คํธ""" | |
| try: | |
| text_parts = [] | |
| with zipfile.ZipFile(file_path, 'r') as zf: | |
| file_list = zf.namelist() | |
| section_files = sorted([f for f in file_list if f.startswith('Contents/section') and f.endswith('.xml')]) | |
| if not section_files: | |
| section_files = sorted([f for f in file_list if 'section' in f.lower() and f.endswith('.xml')]) | |
| for sf_name in section_files: | |
| try: | |
| with zf.open(sf_name) as sf: | |
| content = sf.read().decode('utf-8', errors='ignore') | |
| content = re.sub(r'\sxmlns[^"]*"[^"]*"', '', content) | |
| content = re.sub(r'<[a-zA-Z]+:', '<', content) | |
| content = re.sub(r'</[a-zA-Z]+:', '</', content) | |
| try: | |
| root = ET.fromstring(content) | |
| texts = [] | |
| for elem in root.iter(): | |
| if elem.tag.endswith('t') or elem.tag == 't': | |
| if elem.text: texts.append(elem.text) | |
| elif elem.text and elem.text.strip(): | |
| if any(x in elem.tag.lower() for x in ['text', 'run', 'para', 'char']): | |
| texts.append(elem.text.strip()) | |
| if texts: text_parts.append(' '.join(texts)) | |
| except ET.ParseError: | |
| matches = re.findall(r'>([^<]+)<', content) | |
| clean = [t.strip() for t in matches if t.strip() and len(t.strip()) > 1] | |
| if clean: text_parts.append(' '.join(clean)) | |
| except: continue | |
| if text_parts: | |
| return text_parts, None | |
| return None, "HWPX ํ ์คํธ ์์" | |
| except zipfile.BadZipFile: | |
| return None, "์ ํจํ์ง ์์ HWPX" | |
| except Exception as e: | |
| return None, f"HWPX ์ค๋ฅ: {e}" | |
| def _decode_hwp_para(data): | |
| """HWP ๋ฐ์ด๋๋ฆฌ โ ๋ฌธ๋จ ํ ์คํธ""" | |
| result = [] | |
| i = 0 | |
| while i < len(data) - 1: | |
| code = int.from_bytes(data[i:i+2], 'little') | |
| if code in (1,2,3): i += 14 | |
| elif code == 9: result.append('\t') | |
| elif code in (10,13): result.append('\n') | |
| elif code == 24: result.append('-') | |
| elif code in (30,31): result.append(' ') | |
| elif code >= 32: | |
| try: | |
| ch = chr(code) | |
| if ch.isprintable() or ch in '\n\t ': result.append(ch) | |
| except: pass | |
| i += 2 | |
| text = ''.join(result).strip() | |
| text = re.sub(r'[ \t]+', ' ', text) | |
| text = re.sub(r'\n{3,}', '\n\n', text) | |
| return text if len(text) > 2 else None | |
| def _extract_hwp_section(data): | |
| """HWP ์น์ ๋ฐ์ด๋๋ฆฌ โ ํ ์คํธ""" | |
| texts = [] | |
| pos = 0 | |
| while pos < len(data) - 4: | |
| try: | |
| header = int.from_bytes(data[pos:pos+4], 'little') | |
| tag_id = header & 0x3FF | |
| size = (header >> 20) & 0xFFF | |
| pos += 4 | |
| if size == 0xFFF: | |
| if pos + 4 > len(data): break | |
| size = int.from_bytes(data[pos:pos+4], 'little') | |
| pos += 4 | |
| if pos + size > len(data): break | |
| record_data = data[pos:pos+size] | |
| pos += size | |
| if tag_id == 67 and size > 0: | |
| t = _decode_hwp_para(record_data) | |
| if t: texts.append(t) | |
| except: | |
| pos += 1 | |
| return '\n'.join(texts) if texts else None | |
| def extract_text_from_hwp(file_path): | |
| """HWP (OLE ๊ธฐ๋ฐ) โ ํ ์คํธ""" | |
| if not HAS_OLEFILE: return None, "olefile ์์" | |
| try: | |
| ole = olefile.OleFileIO(file_path) | |
| if not ole.exists('FileHeader'): | |
| ole.close(); return None, "HWP ํค๋ ์์" | |
| header_data = ole.openstream('FileHeader').read() | |
| is_compressed = (header_data[36] & 1) == 1 if len(header_data) > 36 else True | |
| all_texts = [] | |
| for entry in ole.listdir(): | |
| entry_path = '/'.join(entry) | |
| if entry_path.startswith('BodyText/Section'): | |
| try: | |
| stream = ole.openstream(entry).read() | |
| if is_compressed: | |
| try: stream = zlib.decompress(stream, -15) | |
| except: | |
| try: stream = zlib.decompress(stream) | |
| except: pass | |
| section_text = _extract_hwp_section(stream) | |
| if section_text: all_texts.append(section_text) | |
| except: continue | |
| ole.close() | |
| if all_texts: return all_texts, None | |
| return None, "HWP ํ ์คํธ ์์" | |
| except Exception as e: | |
| return None, f"HWP ์ค๋ฅ: {e}" | |
| def extract_text_from_file(file_path): | |
| """ | |
| ๋ง๋ฅ ๋ฌธ์ ์ถ์ถ: PDF/DOCX/HWP/HWPX/TXT โ (sections_list, full_text, error) | |
| sections_list: ํ์ด์ง/์น์ ๋ณ ํ ์คํธ ๋ฆฌ์คํธ | |
| full_text: ์ ์ฒด ํฉ์น ํ ์คํธ | |
| """ | |
| if not file_path or not os.path.exists(file_path): | |
| return None, None, "ํ์ผ ์์" | |
| ext = Path(file_path).suffix.lower() | |
| sections, error = None, None | |
| if ext == '.pdf': | |
| sections, error = extract_text_from_pdf(file_path) | |
| elif ext == '.docx': | |
| sections, error = extract_text_from_docx(file_path) | |
| elif ext == '.hwpx': | |
| sections, error = extract_text_from_hwpx(file_path) | |
| elif ext == '.hwp': | |
| sections, error = extract_text_from_hwp(file_path) | |
| elif ext in ('.txt', '.md', '.csv', '.json', '.xml', '.html'): | |
| sections, error = extract_text_from_txt(file_path) | |
| else: | |
| return None, None, f"์ง์ํ์ง ์๋ ํ์: {ext}" | |
| if sections: | |
| full = '\n\n'.join(sections) | |
| return sections, full, None | |
| return None, None, error or "ํ ์คํธ ์ถ์ถ ์คํจ" | |
| def split_sentences(text): | |
| try: | |
| s = [x.text.strip() for x in KIWI.split_into_sents(text) if x.text.strip()] | |
| if s: return s | |
| except: pass | |
| return [x.strip() for x in re.split(r'(?<=[.!?ใ])\s+', text) if x.strip()] | |
| def split_words(text): | |
| return [w for w in re.findall(r'[๊ฐ-ํฃa-zA-Z0-9]+', text) if w] | |
| def get_morphemes(text): | |
| try: | |
| r = KIWI.analyze(text) | |
| if r and r[0]: return [(m.form, m.tag) for m in r[0][0]] | |
| except: pass | |
| return [] | |
| def http_get(url, headers=None, timeout=15): | |
| try: | |
| if HAS_HTTPX: | |
| r = httpx.get(url, headers=headers or {}, timeout=timeout, follow_redirects=True) | |
| return r.text if r.status_code == 200 else None | |
| else: | |
| import urllib.request | |
| req = urllib.request.Request(url, headers=headers or {}) | |
| with urllib.request.urlopen(req, timeout=timeout) as resp: | |
| return resp.read().decode('utf-8', errors='replace') | |
| except: return None | |
| def http_post_json(url, body, headers=None, timeout=30): | |
| try: | |
| h = headers or {} | |
| h["Content-Type"] = "application/json" | |
| if HAS_HTTPX: | |
| r = httpx.post(url, json=body, headers=h, timeout=timeout) | |
| if r.status_code == 200: return r.json() | |
| return None | |
| else: | |
| import urllib.request, ssl | |
| req = urllib.request.Request(url, json.dumps(body).encode(), h) | |
| with urllib.request.urlopen(req, timeout=timeout, context=ssl.create_default_context()) as resp: | |
| return json.loads(resp.read()) | |
| except: return None | |
| def call_groq(model, prompt, max_tokens=800, temperature=0.1): | |
| if not GROQ_KEY: return None, "NO_KEY" | |
| url = "https://api.groq.com/openai/v1/chat/completions" | |
| h = {"Authorization": f"Bearer {GROQ_KEY}", "Content-Type": "application/json"} | |
| b = {"model": model, "messages": [{"role":"user","content":prompt}], "max_tokens": max_tokens, "temperature": temperature} | |
| try: | |
| if HAS_HTTPX: | |
| r = httpx.post(url, json=b, headers=h, timeout=45) | |
| if r.status_code == 200: return r.json()["choices"][0]["message"]["content"], None | |
| return None, f"HTTP {r.status_code}" | |
| else: | |
| import urllib.request, ssl | |
| req = urllib.request.Request(url, json.dumps(b).encode(), h) | |
| with urllib.request.urlopen(req, timeout=45, context=ssl.create_default_context()) as resp: | |
| return json.loads(resp.read())["choices"][0]["message"]["content"], None | |
| except Exception as e: return None, str(e)[:150] | |
| AI_ENDINGS = ['ํฉ๋๋ค','์ ๋๋ค','๋ฉ๋๋ค','์ต๋๋ค','์์ต๋๋ค','ํ์ต๋๋ค','๊ฒ ์ต๋๋ค'] | |
| AI_CASUAL_ENDINGS = ['๋ผ๊ณ ํ ์ ์๋ค','๋ผ๊ณ ๋ณผ ์ ์๋ค','๋ค๊ณ ์๊ฐํ๋ค','๋ค๊ณ ํ๋จ๋๋ค', | |
| '์ธ ์ ์ด๋ค','์ธ ๊ฒ์ด๋ค','๋ ๊ฒ์ด๋ค','๋ ์ ์ด๋ค','ใน ๊ฒ์ด๋ค','์ ๊ฒ์ด๋ค', | |
| '๋ผ ํ ์ ์๋ค','๋ก ๋ณด์ธ๋ค','๋ก ํ๋จ๋๋ค','๊ณ ์๋ค','๋ ์ถ์ธ๋ค','๋ ์ํฉ์ด๋ค', | |
| '์ง ์์ ์ ์๋ค','๋ผ ํ๊ฒ ๋ค','์์ ์ ์ ์๋ค','ํ ํ์๊ฐ ์๋ค'] | |
| AI_CONNS = ['๋ํ','๋ฐ๋ผ์','๊ทธ๋ฌ๋ฏ๋ก','์ด์ ๋ฐ๋ผ','ํํธ','๋๋ถ์ด','์์ธ๋ฌ','๋ฟ๋ง ์๋๋ผ', | |
| '์ด๋ฅผ ํตํด','์ด์','๊ฒฐ๊ณผ์ ์ผ๋ก','๊ถ๊ทน์ ์ผ๋ก','ํนํ','๋์๊ฐ','์ด๋ฌํ'] | |
| AI_SOFT_CONNS = ['๋ฌผ๋ก ','๊ทธ๋ฌ๋','ํ์ง๋ง','์ด์ฒ๋ผ','์ด์ ๊ฐ์ด','์ด๋ฅผ ๋ฐํ์ผ๋ก'] | |
| AI_FILLER = ['๊ฒ์ผ๋ก ๋ณด','๊ฒ์ผ๋ก ๋ํ','๊ฒ์ผ๋ก ์์','ํ ์ ์','๋ณผ ์ ์','์ฃผ๋ชฉํ ๋ง', | |
| '์ค์ํ ์ญํ ','์ค์ํ ์๋ฏธ','๊ธ์ ์ ์ธ ์ํฅ','๋ถ์ ์ ์ธ ์ํฅ','ํ์ํฉ๋๋ค','ํ์ํ๋ค', | |
| '์ค์ํฉ๋๋ค','์ค์ํ๋ค','์ญํ ์ ํ','์ํฅ์ ๋ฏธ','๊ธฐ๋๋๋ค','์์๋ฉ๋๋ค','๋ถ๊ฐ๋๊ณ ', | |
| '๋๋๋๊ณ ','๋ค์ํ ๋ถ์ผ','๋ค์ํ ์ฐ์ ','๋๋ถ์ ์ฑ๊ณผ','ํ๊ธฐ์ ์ธ ๋ณํ','ํ์ ์ ์ธ', | |
| '์ ์์','์ธก๋ฉด์์','๊ด์ ์์'] | |
| AI_CASUAL_FILLER = ['๋ฌด๊ถ๋ฌด์งํ๋ค','๋ฌด๊ถ๋ฌด์งํ','๊ณผ์ธ์ด ์๋','๋ํ๊ตฌ๊ฐ ๋ ','์ ํ์ ์ด ๋ ', | |
| '๊ธฐ๋ฐ์ผ๋ก','๋ฐํ์ผ๋ก','์๋๋ ฅ์ด','์ด์์ด ๋ ','๊ฐ์ํ๋','๊ธ๋ถ์','ํจ๋ฌ๋ค์', | |
| '์งํ์ ์ด','์๋ก์ด ์ฅ์','๋์ ํ','๋ณธ๊ฒฉํ๋','๊ณ ๋ํ','์ด์ ํ'] | |
| AI_CONCESSION = re.compile(r'๋ฌผ๋ก .{2,20}(ํ์ง๋ง|๊ทธ๋ฌ๋|๊ทธ๋ ์ง๋ง|๋ค๋ง)|.{2,15}(์ด๊ธด ํ์ง๋ง|๊ธฐ๋ ํ์ง๋ง|์ ์์ง๋ง|์๋ ์์ง๋ง)') | |
| EN_AI_MARKERS = ['furthermore','additionally','moreover','it is worth noting','in conclusion', | |
| 'it is important to','plays a crucial role','significant impact','various aspects', | |
| 'in this regard','consequently','nevertheless','integral part of','led to remarkable', | |
| 'fundamentally transformed','has become increasingly','it should be noted', | |
| 'in the context of','paradigm shift','landscape of','methodologies', | |
| 'transformative impact','unprecedented','in various domains'] | |
| HUMAN_MARKERS = { | |
| 'ใ ใ ใ ': re.compile(r'([ใ ใ ใ ใ ใทใฑ])\1{1,}'), | |
| '์ด๋ชจํฐ์ฝ': re.compile(r'[;:]-?[)(DPp]|\^[_\-]?\^|ใ กใ ก|;;'), | |
| '์ค์': re.compile(r'ในใ |ใ ใ |ใดใด|ใ ใ |ใทใท|ใ ใ '), | |
| '๋๋ํ': re.compile(r'[!?]{2,}'), | |
| '๋น๊ฒฉ์์ข ๊ฒฐ': re.compile(r'(๊ฑฐ๋ |์์|์ธ๋ฐ|์ธ๊ฑธ|๊ฐ์|๋๋|์๋|๋๋ฐ|๋ฏธ์ณค|ํ|ใ $|ใ $|์$|์$|๋ฏ$)'), | |
| '๊ตฌ์ด์ถ์ฝ': re.compile(r'(๊ฑ|์ข|๋ง|์์ |์ง์ง|๋ ์|์กด๋|๊ฐ|์กธ๋ผ|์กด๋ง|๊ฒ๋)'), | |
| '๋ง์ถค๋ฒ์ค๋ฅ': re.compile(r'๋ฌ|๋ช์ผ|๊ธ์|ํ ์์|๊ฒ๊ฐ[์๋ค]|๋๊ฐ|๋์|์๋|ํ๋ค'), | |
| '๋ง์ค์ํ': re.compile(r'\.{3,}|โฆ'), | |
| } | |
| FP = { | |
| "GPT": {"m":['๋ฌผ๋ก ์ด์ฃ ','๋์์ด ๋์ จ๊ธฐ๋ฅผ','์ค๋ช ํด ๋๋ฆฌ๊ฒ ์ต๋๋ค','์ถ๊ฐ ์ง๋ฌธ','๋์์ด ํ์ํ์๋ฉด', | |
| '์์ฝํ์๋ฉด','๊ฐ๋ตํ ์ ๋ฆฌํ๋ฉด','ํต์ฌ์'],"e":['์ต๋๋ค','๋๋ฆฌ๊ฒ ์ต๋๋ค'],"lp":re.compile(r'^\d+\.\s|^[-โข]\s',re.M)}, | |
| "Claude": {"m":['๋ง์ํ์ ','์ดํด๋ณด๊ฒ ์ต๋๋ค','๊ท ํ ์กํ','๋งฅ๋ฝ์์','ํ ๊ฐ์ง ์ฃผ์ํ ','๋์์ค', | |
| 'ํฅ๋ฏธ๋ก์ด ์ง๋ฌธ','๋ณต์กํ ์ฃผ์ '],"e":['๋ค์','๊ฑฐ์์'],"lp":re.compile(r'^\*\*.*\*\*|^#+\s',re.M)}, | |
| "Gemini": {"m":['๋ค์๊ณผ ๊ฐ์ต๋๋ค','์ ๋ฆฌํด ๋๋ฆฌ๊ฒ ์ต๋๋ค','ํต์ฌ ๋ด์ฉ์','๋ ์๊ณ ์ถ์ผ์๋ฉด', | |
| '์์๋ณด๊ฒ ์ต๋๋ค'],"e":['๊ฒ ์ต๋๋ค','๋ณด์ธ์'],"lp":re.compile(r'^\*\s|^-\s\*\*',re.M)}, | |
| "Perplexity": {"m":['๊ฒ์ ๊ฒฐ๊ณผ์ ๋ฐ๋ฅด๋ฉด','๋ณด๋์ ๋ฐ๋ฅด๋ฉด','์ฐ๊ตฌ์ ๋ฐ๋ฅด๋ฉด','๋ฐํ๋ค','์ ํ๋ค', | |
| '๊ฒ์ผ๋ก ๋ํ๋ฌ๋ค','๊ฒ์ผ๋ก ์กฐ์ฌ๋๋ค','๊ฒ์ผ๋ก ์ง๊ณ๋๋ค','๋ฐํํ๋ค'],"e":['๋ฐํ๋ค','๋ํ๋ฌ๋ค','์ ํ๋ค'],"lp":re.compile(r'\[\d+\]',re.M)}, | |
| } | |
| def score_sentence(sent): | |
| """๋จ์ผ ๋ฌธ์ฅ AI ์ ์ (0~100). ํญ1ยทํญ2 ๊ณต์ . v5.0 ๋ํญ ๊ฐํ.""" | |
| sc = 0; reasons = [] | |
| sl = sent.lower().strip() | |
| sr = sent.rstrip('.!?ใ') | |
| for e in AI_ENDINGS: | |
| if sr.endswith(e): sc += 22; reasons.append(f"๊ฒฉ์์ด๋ฏธ(-{e})"); break | |
| if sc == 0: # ๊ฒฉ์์ด ์๋ ๊ฒฝ์ฐ๋ง | |
| for e in AI_CASUAL_ENDINGS: | |
| if sr.endswith(e): sc += 15; reasons.append(f"๋น๊ฒฉ์AI(-{e})"); break | |
| stripped = sent.strip() | |
| for c in AI_CONNS: | |
| if stripped.startswith(c): | |
| sc += 18; reasons.append(f"AI์ ์์ฌ({c})"); break | |
| else: | |
| for c in AI_SOFT_CONNS: | |
| if stripped.startswith(c): sc += 8; reasons.append(f"์ฝํ์ ์์ฌ({c})"); break | |
| filler_found = sum(1 for f in AI_FILLER if f in sent) | |
| casual_filler = sum(1 for f in AI_CASUAL_FILLER if f in sent) | |
| total_filler = filler_found + casual_filler | |
| if total_filler >= 3: sc += 25; reasons.append(f"์ํฌํํร{total_filler}") | |
| elif total_filler == 2: sc += 18; reasons.append(f"์ํฌํํร2") | |
| elif total_filler == 1: sc += 10; reasons.append(f"์ํฌํํร1") | |
| if AI_CONCESSION.search(sent): sc += 10; reasons.append("์๋ณดํจํด") | |
| for mn, fp in FP.items(): | |
| for m in fp["m"]: | |
| if m in sent: sc += 8; reasons.append(f"{mn}์ง๋ฌธ"); break | |
| en_count = sum(1 for em in EN_AI_MARKERS if em in sl) | |
| if en_count >= 3: sc += 25; reasons.append(f"์์ดAIร{en_count}") | |
| elif en_count >= 2: sc += 18; reasons.append(f"์์ดAIร{en_count}") | |
| elif en_count >= 1: sc += 12; reasons.append(f"์์ดAIร1") | |
| has_formal = any(sr.endswith(e) for e in AI_ENDINGS) | |
| has_conn = any(stripped.startswith(c) for c in AI_CONNS) | |
| if has_formal and total_filler >= 1 and has_conn: sc += 8; reasons.append("๋ณตํฉAI") | |
| elif has_formal and total_filler >= 2: sc += 5; reasons.append("๊ฒฉ์+์ํฌ") | |
| for n, p in HUMAN_MARKERS.items(): | |
| matches = p.findall(sent) | |
| if matches: | |
| if n in ('ใ ใ ใ ','์ด๋ชจํฐ์ฝ','์ค์'): sc -= 25; reasons.append(f"์ธ๊ฐ({n})") | |
| elif n in ('๋น๊ฒฉ์์ข ๊ฒฐ','๊ตฌ์ด์ถ์ฝ'): sc -= 18; reasons.append(f"๊ตฌ์ด์ฒด({n})") | |
| elif n == '๋ง์ถค๋ฒ์ค๋ฅ': sc -= 12; reasons.append("๋ง์ถค๋ฒ์ค๋ฅ") | |
| elif n in ('๋๋ํ','๋ง์ค์ํ'): sc -= 10; reasons.append(f"์ธ๊ฐ({n})") | |
| return max(0, min(100, sc)), reasons | |
| def analyze_statistics(text, sentences, words): | |
| sl = [len(s) for s in sentences] | |
| if len(sl) < 2: return {"score":50} | |
| avg = sum(sl)/len(sl); std = math.sqrt(sum((l-avg)**2 for l in sl)/len(sl)) | |
| cv = std/avg if avg > 0 else 0 | |
| cv_score = 85 if cv<0.20 else 70 if cv<0.30 else 50 if cv<0.45 else 30 if cv<0.60 else 15 | |
| diffs = [abs(sl[i]-sl[i-1]) for i in range(1,len(sl))] | |
| burst_score = 50 | |
| if diffs: | |
| avg_d = sum(diffs)/len(diffs) | |
| max_d = max(diffs) | |
| burst_ratio = max_d / (avg_d + 1) | |
| burst_score = 85 if burst_ratio < 1.8 else 65 if burst_ratio < 2.5 else 40 if burst_ratio < 3.5 else 20 | |
| standard_ratio = sum(1 for l in sl if 20 <= l <= 60) / len(sl) | |
| std_score = 80 if standard_ratio > 0.8 else 60 if standard_ratio > 0.6 else 40 if standard_ratio > 0.4 else 20 | |
| extreme = sum(1 for l in sl if l < 10 or l > 80) | |
| if extreme >= 2: std_score = max(10, std_score - 20) | |
| elif extreme >= 1: std_score = max(15, std_score - 10) | |
| wf = Counter(words); t = len(words) | |
| ttr = len(wf)/t if t>0 else 0 | |
| vocab_score = 70 if ttr<0.45 else 55 if ttr<0.55 else 35 if ttr<0.65 else 20 | |
| wpc = [len(split_words(s)) for s in sentences] | |
| complex_score = 50 | |
| if len(wpc) >= 3: | |
| wpc_avg = sum(wpc)/len(wpc) | |
| wpc_std = math.sqrt(sum((w-wpc_avg)**2 for w in wpc)/len(wpc)) | |
| wpc_cv = wpc_std/wpc_avg if wpc_avg > 0 else 0 | |
| complex_score = 80 if wpc_cv < 0.20 else 60 if wpc_cv < 0.35 else 35 if wpc_cv < 0.50 else 15 | |
| final = int(cv_score*0.20 + burst_score*0.20 + std_score*0.25 + vocab_score*0.15 + complex_score*0.20) | |
| return {"score":final,"cv":round(cv,3),"ttr":round(ttr,3)} | |
| def analyze_korean_style(text, sentences, morphemes): | |
| if not sentences: return {"score":50} | |
| formal_cnt = sum(1 for s in sentences if any(s.rstrip('.!?').endswith(e) for e in AI_ENDINGS)) | |
| casual_ai = sum(1 for s in sentences if any(s.rstrip('.!?').endswith(e) for e in AI_CASUAL_ENDINGS)) | |
| fr = formal_cnt/len(sentences) | |
| car = casual_ai/len(sentences) | |
| ending_score = 85 if fr>0.7 else 65 if fr>0.5 else 45 if fr>0.3 else 25 if fr>0.1 else 10 | |
| ending_score = min(90, ending_score + int(car * 25)) # ๋น๊ฒฉ์AI ๋ณด๋์ค | |
| conn_positions = [] | |
| for i, s in enumerate(sentences): | |
| for c in AI_CONNS: | |
| if s.strip().startswith(c): conn_positions.append(i); break | |
| conn_density = len(conn_positions)/len(sentences) if sentences else 0 | |
| conn_score = 85 if conn_density>0.4 else 65 if conn_density>0.25 else 40 if conn_density>0.1 else 15 | |
| if len(conn_positions) >= 2: | |
| gaps = [conn_positions[i]-conn_positions[i-1] for i in range(1,len(conn_positions))] | |
| gap_cv = (math.sqrt(sum((g-sum(gaps)/len(gaps))**2 for g in gaps)/len(gaps))/(sum(gaps)/len(gaps)+0.01)) | |
| if gap_cv < 0.5: conn_score = min(90, conn_score + 10) # ๋งค์ฐ ๊ท์น์ โ AI ๋ณด๋์ค | |
| filler_cnt = sum(1 for f in AI_FILLER if f in text) + sum(1 for f in AI_CASUAL_FILLER if f in text) | |
| filler_score = 90 if filler_cnt>=6 else 75 if filler_cnt>=4 else 55 if filler_cnt>=2 else 30 if filler_cnt>=1 else 10 | |
| concession_cnt = len(AI_CONCESSION.findall(text)) | |
| conc_score = 80 if concession_cnt >= 2 else 55 if concession_cnt >= 1 else 20 | |
| human_count = sum(len(p.findall(text)) for p in HUMAN_MARKERS.values()) | |
| human_penalty = min(35, human_count * 8) | |
| pos_score = 45 | |
| if morphemes: | |
| pc = Counter(t for _,t in morphemes); tm = sum(pc.values()) | |
| noun_r = sum(pc.get(t,0) for t in ['NNG','NNP','NNB','NR'])/tm if tm else 0 | |
| pos_score = 70 if noun_r>0.42 else 55 if noun_r>0.38 else 35 if noun_r>0.32 else 20 | |
| final = max(5, int(ending_score*0.25 + conn_score*0.20 + filler_score*0.20 + | |
| conc_score*0.10 + pos_score*0.15 + 10*0.10) - human_penalty) | |
| return {"score":final,"formal":f"{fr:.0%}","conn":f"{conn_density:.2f}","filler":filler_cnt,"human":human_count} | |
| def analyze_repetition(text, sentences, words): | |
| if not sentences or len(sentences) < 2: return {"score":35} | |
| tr = 0 | |
| if len(words)>=5: | |
| tg = Counter(tuple(words[i:i+3]) for i in range(len(words)-2)) | |
| tr = sum(1 for c in tg.values() if c>1)/len(tg) if tg else 0 | |
| ngram_score = 80 if tr>0.15 else 60 if tr>0.08 else 35 if tr>0.03 else 15 | |
| openers_2 = [] | |
| openers_3 = [] | |
| for s in sentences: | |
| ws = split_words(s) | |
| if len(ws) >= 2: openers_2.append(tuple(ws[:2])) | |
| if len(ws) >= 3: openers_3.append(tuple(ws[:3])) | |
| opener2_score = 50 | |
| if openers_2: | |
| unique2 = len(set(openers_2))/len(openers_2) | |
| opener2_score = 80 if unique2 < 0.5 else 60 if unique2 < 0.7 else 35 if unique2 < 0.85 else 15 | |
| ai_only_conns = ['๋ํ','๋ฐ๋ผ์','๊ทธ๋ฌ๋ฏ๋ก','์ด์ ๋ฐ๋ผ','๋๋ถ์ด','์์ธ๋ฌ','๋ฟ๋ง ์๋๋ผ', | |
| '์ด๋ฅผ ํตํด','์ด์','๊ฒฐ๊ณผ์ ์ผ๋ก','๊ถ๊ทน์ ์ผ๋ก','๋์๊ฐ','์ด๋ฌํ'] | |
| cr = sum(1 for s in sentences if any(s.strip().startswith(c) for c in ai_only_conns)) | |
| crr = cr/len(sentences) if sentences else 0 | |
| ai_conn_score = 85 if crr>0.35 else 65 if crr>0.2 else 40 if crr>0.08 else 15 | |
| templates = [] | |
| for s in sentences: | |
| ws = split_words(s) | |
| if len(ws) >= 4: | |
| templates.append((ws[0], ws[-1])) | |
| template_rep = 0 | |
| if templates: | |
| tc = Counter(templates) | |
| template_rep = sum(1 for c in tc.values() if c > 1) / len(tc) if tc else 0 | |
| template_score = 80 if template_rep > 0.3 else 55 if template_rep > 0.1 else 25 | |
| endings = [] | |
| for s in sentences: | |
| sr = s.rstrip('.!?ใ') | |
| for e in AI_ENDINGS + ['์๋ค','ํ๋ค','๋๋ค','ํ๋ค','์ด๋ค','๋๋ค']: | |
| if sr.endswith(e): endings.append(e); break | |
| ending_div = 50 | |
| if endings: | |
| unique_e = len(set(endings))/len(endings) | |
| ending_div = 80 if unique_e < 0.3 else 60 if unique_e < 0.5 else 35 if unique_e < 0.7 else 15 | |
| final = int(ngram_score*0.15 + opener2_score*0.20 + ai_conn_score*0.25 + | |
| template_score*0.15 + ending_div*0.25) | |
| return {"score":final} | |
| AI_VAGUE = re.compile(r'๋ค์ํ|์ค์ํ|๊ธ์ ์ ์ธ|๋ถ์ ์ ์ธ|ํ๊ธฐ์ ์ธ|ํ์ ์ ์ธ|ํจ์จ์ ์ธ|์ฒด๊ณ์ ์ธ|์ข ํฉ์ ์ธ|์ ๋ฐ์ ์ธ|์ง์์ ์ธ|์ ๊ทน์ ์ธ|์๋นํ|์ฃผ์ํ') | |
| CONCRETE_PROPER = re.compile(r'์ผ์ฑ|LG|ํ๋|SK|์นด์นด์ค|๋ค์ด๋ฒ|๋ทํ๋ฆญ์ค|๊ตฌ๊ธ|์ ํ|ํ ์ฌ๋ผ|์๋ง์กด|๋ง์ดํฌ๋ก์ํํธ|[๊ฐ-ํฃ]{2,}๋ํ|[๊ฐ-ํฃ]{2,}๋ณ์|[๊ฐ-ํฃ]{1,3}์[๋์์ด๊ฐ]|[๊ฐ-ํฃ]{1,3}๊ตฌ[๋์์ด๊ฐ]|[๊ฐ-ํฃ]{2,}๋[์์]') | |
| CONCRETE_NUMBER = re.compile(r'\d{2,}[๋ง์ต์กฐ์๋ฌ๋ฌ%๊ฐ๋ ์์ผ์๋ฑํธ]|\d+\.\d+%|\d{4}๋ |\d{1,2}์') | |
| CONCRETE_QUOTE = re.compile(r'์ ๋ฐ๋ฅด๋ฉด|๋ฐํํ|๋ฐํ๋ค|๋ณด๋ํ|์ ํ๋ค|๋ผ๊ณ ๋ง|์ธํฐ๋ทฐ|์ค๋ฌธ|์กฐ์ฌ|ํต๊ณ์ฒญ|๋ณด๊ณ ์') | |
| def analyze_structure(text, sentences): | |
| if not sentences: return {"score":35} | |
| lt = (len(re.findall(r'^\d+[.)]\s',text,re.M)) + len(re.findall(r'^[-โข*]\s',text,re.M)) + | |
| len(re.findall(r'^#+\s',text,re.M)) + len(re.findall(r'\*\*[^*]+\*\*',text))) | |
| list_score = 90 if lt>=5 else 70 if lt>=3 else 45 if lt>=1 else 10 | |
| vague_cnt = len(AI_VAGUE.findall(text)) | |
| proper_cnt = len(CONCRETE_PROPER.findall(text)) | |
| number_cnt = len(CONCRETE_NUMBER.findall(text)) | |
| quote_cnt = len(CONCRETE_QUOTE.findall(text)) | |
| concrete_total = proper_cnt + number_cnt + quote_cnt | |
| if vague_cnt >= 3 and concrete_total == 0: abstract_score = 90 | |
| elif vague_cnt >= 2 and concrete_total <= 1: abstract_score = 70 | |
| elif vague_cnt >= 1 and concrete_total == 0: abstract_score = 55 | |
| elif concrete_total >= 3: abstract_score = 10 | |
| elif concrete_total >= 2: abstract_score = 20 | |
| elif concrete_total >= 1: abstract_score = 30 | |
| else: abstract_score = 45 | |
| has_question = any(s.strip().endswith('?') for s in sentences) | |
| has_exclaim = any(s.strip().endswith('!') for s in sentences) | |
| has_ellipsis = any('...' in s or 'โฆ' in s for s in sentences) | |
| variety = sum([has_question, has_exclaim, has_ellipsis]) | |
| type_score = 15 if variety >= 2 else 40 if variety >= 1 else 65 | |
| puncts = re.findall(r'[!?,;:โฆโ\-~]', text) | |
| unique_punct = len(set(puncts)) | |
| punct_score = 65 if unique_punct <= 1 else 45 if unique_punct <= 3 else 20 | |
| paras = [p.strip() for p in text.split('\n\n') if p.strip()] | |
| para_score = 35 | |
| if len(paras) >= 2: | |
| pl = [len(split_sentences(p)) for p in paras] | |
| avg_p = sum(pl)/len(pl) | |
| if avg_p > 0: | |
| pcv = math.sqrt(sum((l-avg_p)**2 for l in pl)/len(pl))/avg_p | |
| para_score = 75 if pcv < 0.2 else 55 if pcv < 0.35 else 30 | |
| if len(paras) >= 3 and pl[0] < avg_p and pl[-1] < avg_p: | |
| para_score = min(85, para_score + 10) | |
| final = int(list_score*0.10 + abstract_score*0.40 + type_score*0.20 + punct_score*0.10 + para_score*0.20) | |
| return {"score":final} | |
| def analyze_model_fingerprint(text, sentences): | |
| ms = {} | |
| sl = text.lower() | |
| for mn, fp in FP.items(): | |
| sc = sum(min(15,text.count(m)*5) for m in fp["m"] if text.count(m)>0) | |
| lm = fp["lp"].findall(text) | |
| if lm: sc += min(20,len(lm)*3) | |
| em = sum(1 for s in sentences if any(s.rstrip('.!?').endswith(e) for e in fp.get("e",[]))) | |
| if sentences: sc += int((em/len(sentences))*20) | |
| ms[mn] = min(100,sc) | |
| general_ai = 0 | |
| general_ai += sum(5 for f in AI_CASUAL_FILLER if f in text) | |
| casual_end_cnt = sum(1 for s in sentences if any(s.rstrip('.!?').endswith(e) for e in AI_CASUAL_ENDINGS)) | |
| general_ai += casual_end_cnt * 5 | |
| general_ai += len(AI_CONCESSION.findall(text)) * 8 | |
| ms["๋น๊ฒฉ์AI"] = min(100, general_ai) | |
| en_score = sum(5 for em in EN_AI_MARKERS if em in sl) | |
| ms["์์ดAI"] = min(100, en_score) | |
| mx = max(ms.values()) if ms else 0 | |
| multi = sum(1 for v in ms.values() if v >= 10) | |
| multi_bonus = 10 if multi >= 3 else 5 if multi >= 2 else 0 | |
| base = 85 if mx>=50 else 65 if mx>=35 else 45 if mx>=20 else 25 if mx>=10 else 10 | |
| 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}} | |
| def analyze_perplexity(text, sentences, morphemes): | |
| """ํ๊ตญ์ด ํนํ Perplexity + Burstiness โ ๋ฌธ์ ์ํธ๋กํผ ๋ณด์ """ | |
| if len(sentences) < 2: return {"score": 40, "entropy": 0, "variance": 0, "order": 0, "zipf": 0} | |
| chars = [c for c in text if c.strip()] | |
| char_score = 45 | |
| if len(chars) >= 30: | |
| cbigrams = [(chars[i], chars[i+1]) for i in range(len(chars)-1)] | |
| cb_freq = Counter(cbigrams) | |
| total_cb = len(cbigrams) | |
| char_entropy = -sum((cnt/total_cb)*math.log2(cnt/total_cb) for cnt in cb_freq.values()) | |
| if char_entropy < 7.5: char_score = 78 | |
| elif char_entropy < 8.5: char_score = 62 | |
| elif char_entropy < 9.5: char_score = 42 | |
| elif char_entropy < 10.5: char_score = 25 | |
| else: char_score = 12 | |
| sl = [len(s) for s in sentences] | |
| burst_score = 45 | |
| if len(sl) >= 3: | |
| avg = sum(sl)/len(sl) | |
| std = math.sqrt(sum((l-avg)**2 for l in sl)/len(sl)) | |
| cv = std/(avg+1e-10) | |
| if cv < 0.15: burst_score = 82 | |
| elif cv < 0.25: burst_score = 62 | |
| elif cv < 0.40: burst_score = 38 | |
| elif cv < 0.60: burst_score = 20 | |
| else: burst_score = 8 | |
| sent_ttr = [] | |
| for s in sentences: | |
| sw = split_words(s) | |
| if len(sw) >= 3: | |
| sent_ttr.append(len(set(sw))/len(sw)) | |
| ttr_score = 42 | |
| if len(sent_ttr) >= 3: | |
| avg_ttr = sum(sent_ttr)/len(sent_ttr) | |
| std_ttr = math.sqrt(sum((t-avg_ttr)**2 for t in sent_ttr)/len(sent_ttr)) | |
| if std_ttr < 0.04: ttr_score = 75 | |
| elif std_ttr < 0.08: ttr_score = 55 | |
| elif std_ttr < 0.15: ttr_score = 35 | |
| else: ttr_score = 15 | |
| endings = [s.rstrip('.!?\u2026')[-3:] for s in sentences if len(s) >= 5] | |
| end_score = 40 | |
| if len(endings) >= 3: | |
| ef = Counter(endings) | |
| end_ent = -sum((c/len(endings))*math.log2(c/len(endings)) for c in ef.values()) | |
| max_ent = math.log2(len(ef)) if len(ef) > 1 else 1 | |
| norm_ent = end_ent / (max_ent + 1e-10) | |
| if norm_ent < 0.5: end_score = 72 | |
| elif norm_ent < 0.7: end_score = 50 | |
| elif norm_ent < 0.85: end_score = 32 | |
| else: end_score = 15 | |
| final = int(char_score * 0.30 + burst_score * 0.30 + ttr_score * 0.20 + end_score * 0.20) | |
| return {"score": final, "entropy": char_score, "variance": burst_score, "order": ttr_score, "zipf": end_score} | |
| HUMANIZER_OVERSUBST = re.compile(r'ํ์ฉํ๋ค|์ด์ฉํ๋ค|์ฌ์ฉํ๋ค|์ ์ฉํ๋ค|๋์ ํ๋ค|์ฑํํ๋ค|์ํํ๋ค|์งํํ๋ค|์ค์ํ๋ค|์คํํ๋ค') | |
| HUMANIZER_AWKWARD = re.compile(r'๊ทธ๊ฒ์|์ด๊ฒ์|์ ๊ฒ์|ํด๋น ์ฌํญ|์์ ์ธ๊ธํ|์ ์ ํ|์๊ธฐํ|๊ธฐ์ ๋') | |
| HUMANIZER_PASSIVE = re.compile(r'๋์ด์ง[๊ณ ๋๋ฉฐ]|ํ๊ฒ ๋์[๋ค์ต]|์ํ๋[์์ด]|์งํ๋[์์ด]|์ค์๋[์์ด]|ํ์ฉ๋[์์ด]') | |
| def analyze_humanizer(text, sentences, words, morphemes): | |
| """Humanizer/Bypasser ํ์ง โ AI ์๋ฌธ ํจ๋ฌํ๋ ์ด์ฆ ํ์ ๋ถ์""" | |
| if len(sentences) < 2: return {"score": 20, "signals": []} | |
| signals = [] | |
| content_words = [f for f, t in morphemes if t in ('NNG', 'NNP', 'VV', 'VA')] | |
| if len(content_words) >= 10: | |
| cw_freq = Counter(content_words) | |
| hapax = sum(1 for c in cw_freq.values() if c == 1) | |
| hapax_ratio = hapax / len(cw_freq) if cw_freq else 0 | |
| if hapax_ratio > 0.95 and len(content_words) >= 30: | |
| signals.append(("๋์์ด๊ณผ๋ค์นํ", 20, "ํต์ฌ ์ดํ๊ฐ ๊ณผ๋ํ๊ฒ ๋ถ์ฐ")) | |
| elif hapax_ratio > 0.90 and len(content_words) >= 25: | |
| signals.append(("๋์์ด์นํ์์ฌ", 12, "์ดํ ๋ฐ๋ณต ํํผ ํจํด")) | |
| sl = [len(s) for s in sentences] | |
| if len(sl) >= 4: | |
| avg = sum(sl) / len(sl) | |
| cv = math.sqrt(sum((l - avg)**2 for l in sl) / len(sl)) / (avg + 1e-10) | |
| unique_ratio = len(set(words)) / len(words) if words else 0 | |
| if cv < 0.20 and unique_ratio > 0.80 and len(sentences) >= 5: | |
| signals.append(("๊ตฌ์กฐ๋ณด์กด์ดํ๋ณ๊ฒฝ", 18, "๋ฌธ์ฅ ๊ตฌ์กฐ ๊ท ์ผ + ๋น์ ์์ ์ดํ ๋ค์์ฑ")) | |
| residual = 0 | |
| conn_positions = [] | |
| for i, s in enumerate(sentences): | |
| stripped = s.strip() | |
| for c in ['๋ํ','ํนํ','ํํธ','๋๋ถ์ด','์์ธ๋ฌ','๋์๊ฐ','์ด์','๊ฒ๋ค๊ฐ','๋ฐ๋ฉด','๊ฒฐ๊ตญ']: | |
| if stripped.startswith(c): | |
| conn_positions.append(i) | |
| break | |
| if len(conn_positions) >= 2: | |
| gaps = [conn_positions[i] - conn_positions[i-1] for i in range(1, len(conn_positions))] | |
| if gaps and max(gaps) - min(gaps) <= 1: # ๊ฑฐ์ ๋ฑ๊ฐ๊ฒฉ | |
| signals.append(("์ ์์ฌ๋ฑ๊ฐ๊ฒฉ์์กด", 15, "์ ์์ฌ ๋ฐฐ์น๊ฐ ๊ท์น์ (AI ์๋ฌธ ๊ตฌ์กฐ ์์กด)")) | |
| residual += 15 | |
| oversubst = len(HUMANIZER_OVERSUBST.findall(text)) | |
| awkward = len(HUMANIZER_AWKWARD.findall(text)) | |
| passive = len(HUMANIZER_PASSIVE.findall(text)) | |
| if oversubst >= 3: | |
| signals.append(("์ ์ฌ๋์ฌ๋๋ฌด", 12, f"ํ์ฉ/์ด์ฉ/์ฌ์ฉ/์ ์ฉ ๋ฑ {oversubst}๊ฐ")) | |
| if awkward >= 2: | |
| signals.append(("์ด์ํ์ง์์ด", 10, f"ํด๋น/์ ์ /์๊ธฐ ๋ฑ {awkward}๊ฐ")) | |
| if passive >= 3: | |
| signals.append(("์ด์คํผ๋๊ณผ๋ค", 15, f"๋์ด์ง/์ํ๋ ๋ฑ {passive}๊ฐ")) | |
| endings = [s.rstrip('.!?')[-2:] for s in sentences if len(s) >= 4] | |
| end_types = len(set(endings)) / len(endings) if endings else 0 | |
| has_question = any(s.strip().endswith('?') for s in sentences) | |
| has_exclaim = any(s.strip().endswith('!') for s in sentences) | |
| sent_type_variety = sum([has_question, has_exclaim]) | |
| if sent_type_variety == 0 and end_types > 0.85 and len(sentences) >= 6: | |
| signals.append(("์ ํ๋จ์กฐ์ด๋ฏธ๋ค์", 12, "์์ ๋ฌธ๋ง + ์ข ๊ฒฐ์ด๋ฏธ ๊ณผ๋ค ๋ค์ = Humanizer ํจํด")) | |
| starters = [s.strip()[:3] for s in sentences if len(s) >= 6] | |
| starter_unique = len(set(starters)) / len(starters) if starters else 0 | |
| if starter_unique >= 0.98 and len(sentences) >= 7: | |
| signals.append(("๋ฌธ๋๊ณผ๋ค๋ค์", 8, "๋ชจ๋ ๋ฌธ์ฅ ์์์ด ๋ค๋ฆ (์์ฐ์ค๋ฝ์ง ์์ ๋ค์์ฑ)")) | |
| total = sum(s[1] for s in signals) | |
| if total >= 45: score = 85 | |
| elif total >= 30: score = 68 | |
| elif total >= 20: score = 52 | |
| elif total >= 10: score = 35 | |
| else: score = 15 | |
| return {"score": score, "signals": signals, "total_evidence": total} | |
| MODEL_PROFILES = { | |
| "GPT": { | |
| "style": ["๊ฒฉ์์ฒด ~์ต๋๋ค", "๋ํ/ํนํ ์ ์์ฌ", "~์ ๋ํด", "~๊ฒ์ผ๋ก ์์๋ฉ๋๋ค"], | |
| "markers": ["๋ค์ํ", "์ค์ํ ์ญํ ", "๊ธ์ ์ ์ธ", "๋๋ถ์ ", "์ฃผ๋ชฉํ ๋งํ", "์ดํด๋ณด๊ฒ ์ต๋๋ค"], | |
| "structure": "๊ท ์ผํ ๋ฌธ๋จ, ์๋ก -๋ณธ๋ก -๊ฒฐ๋ก ๊ตฌ์กฐ, ๋งํฌ๋ค์ด ์ ํธ", | |
| "endings": ["์ต๋๋ค", "์์ต๋๋ค", "๋ฉ๋๋ค", "์ ๋๋ค"], | |
| "connectors": ["๋ํ", "ํนํ", "ํํธ", "์ด์ฒ๋ผ"], | |
| }, | |
| "Claude": { | |
| "style": ["๋งฅ๋ฝ ์ ์", "๊ท ํ ์กํ", "์ฌ๋ ค ๊น์ ์ด์กฐ", "์๋ณด ํ ์ฃผ์ฅ"], | |
| "markers": ["ํฅ๋ฏธ๋ก์ด ์ง๋ฌธ", "๋ณต์กํ ์ฃผ์ ", "๋งฅ๋ฝ์์", "๊ท ํ ์กํ", "์ดํด๋ณด๊ฒ ์ต๋๋ค", "ํ ๊ฐ์ง ์ฃผ์ํ "], | |
| "structure": "์์ฐ์ค๋ฌ์ด ํ๋ฆ, ์๋ณด-์ฃผ์ฅ ๊ตฌ๋ฌธ ์ ํธ, ๋ถ๋๋ฌ์ด ์ ํ", | |
| "endings": ["๋ค์", "์ ๋๋ค", "์์ต๋๋ค", "์ต๋๋ค"], | |
| "connectors": ["ํํธ", "๋ฌผ๋ก ", "๋ค๋ง", "์ด์ ๊ด๋ จํด"], | |
| }, | |
| "Gemini": { | |
| "style": ["์ ๋ณด ๋์ดํ", "~์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค", "๊ฒ์ ๊ฒฐ๊ณผ ๊ธฐ๋ฐ"], | |
| "markers": ["์์๋ณด๊ฒ ์ต๋๋ค", "๋์์ด ๋์ จ์ผ๋ฉด", "์ถ๊ฐ ์ง๋ฌธ", "์ฐธ๊ณ ๋ก"], | |
| "structure": "๋ฆฌ์คํธ/๋ฒํธ ๋งค๊ธฐ๊ธฐ ์ ํธ, ํค๋ ํ์ฉ, ์ ๋ณด ๋ฐ๋ ๋์", | |
| "endings": ["์ต๋๋ค", "์์ต๋๋ค", "์ธ์"], | |
| "connectors": ["๋ํ", "๊ทธ๋ฆฌ๊ณ ", "์ฐธ๊ณ ๋ก"], | |
| }, | |
| "Perplexity": { | |
| "style": ["์ถ์ฒ ์ธ์ฉํ", "~์ ๋ฐ๋ฅด๋ฉด", "์์น ์ ์", "๊ฒ์ผ๋ก ๋ํ๋ฌ๋ค"], | |
| "markers": ["์ ๋ฐ๋ฅด๋ฉด", "๊ฒ์ผ๋ก ๋ํ๋ฌ", "๊ฒ์ผ๋ก ์กฐ์ฌ๋", "๊ฒ์ผ๋ก ์ง๊ณ๋", "๋ฐํํ", "๋ณด๋์ ๋ฐ๋ฅด๋ฉด"], | |
| "structure": "ํฉํธ ์ค์ฌ, ์์น ์ธ์ฉ ๋ค์, ์ถ์ฒ ๋ช ์ ์คํ์ผ", | |
| "endings": ["์ต๋๋ค", "๋ํ๋ฌ๋ค", "๋ฐํ๋ค", "์ ํ๋ค"], | |
| "connectors": ["ํํธ", "๋ํ", "์ด์"], | |
| }, | |
| } | |
| def estimate_model(text, sentences, morphemes, model_scores): | |
| """AI ๋ชจ๋ธ ์ถ์ โ ๋ณตํฉ ์ฆ๊ฑฐ ๊ธฐ๋ฐ""" | |
| evidence = {m: {"score": 0, "reasons": []} for m in MODEL_PROFILES} | |
| sl = text.lower() | |
| for model, profile in MODEL_PROFILES.items(): | |
| fp_score = model_scores.get(model, 0) | |
| evidence[model]["score"] += fp_score * 0.4 | |
| if fp_score >= 20: | |
| evidence[model]["reasons"].append(f"์ง๋ฌธ ๋งค์นญ {fp_score}์ ") | |
| marker_cnt = sum(1 for m in profile["markers"] if m in text) | |
| if marker_cnt >= 2: | |
| evidence[model]["score"] += marker_cnt * 8 | |
| evidence[model]["reasons"].append(f"ํน์ ํํ {marker_cnt}๊ฐ") | |
| end_match = 0 | |
| for s in sentences: | |
| for e in profile["endings"]: | |
| if s.rstrip('.!?').endswith(e): | |
| end_match += 1; break | |
| if sentences: | |
| end_ratio = end_match / len(sentences) | |
| if end_ratio > 0.7: | |
| evidence[model]["score"] += 12 | |
| evidence[model]["reasons"].append(f"์ข ๊ฒฐ์ด๋ฏธ {end_ratio:.0%} ์ผ์น") | |
| conn_match = sum(1 for s in sentences if any(s.strip().startswith(c) for c in profile["connectors"])) | |
| if conn_match >= 2: | |
| evidence[model]["score"] += conn_match * 4 | |
| evidence[model]["reasons"].append(f"์ ์์ฌ ํจํด {conn_match}ํ") | |
| number_citations = len(re.findall(r'\d+[%๋ง์ต์กฐ]|์ ๋ฐ๋ฅด๋ฉด|๊ฒ์ผ๋ก ๋ํ๋ฌ|๋ฐํํ', text)) | |
| if number_citations >= 3: | |
| evidence["Perplexity"]["score"] += number_citations * 5 | |
| evidence["Perplexity"]["reasons"].append(f"์์น/์ธ์ฉ {number_citations}ํ") | |
| concession_cnt = len(AI_CONCESSION.findall(text)) | |
| if concession_cnt >= 1: | |
| evidence["Claude"]["score"] += concession_cnt * 10 | |
| evidence["Claude"]["reasons"].append(f"์๋ณด-์ฃผ์ฅ ๊ตฌ๋ฌธ {concession_cnt}ํ") | |
| ranked = sorted(evidence.items(), key=lambda x: x[1]["score"], reverse=True) | |
| top = ranked[0] | |
| second = ranked[1] if len(ranked) > 1 else None | |
| if top[1]["score"] < 10: | |
| return {"model": "ํน์ ๋ถ๊ฐ", "confidence": "๋ฎ์", "detail": evidence, "ranked": ranked} | |
| gap = top[1]["score"] - (second[1]["score"] if second else 0) | |
| if gap >= 20 and top[1]["score"] >= 30: | |
| conf = "๋์" | |
| elif gap >= 10 and top[1]["score"] >= 20: | |
| conf = "์ค๊ฐ" | |
| else: | |
| conf = "๋ฎ์" | |
| return { | |
| "model": top[0], | |
| "confidence": conf, | |
| "score": top[1]["score"], | |
| "reasons": top[1]["reasons"], | |
| "detail": evidence, | |
| "ranked": ranked | |
| } | |
| def analyze_quality(text, sentences, words, morphemes): | |
| qs = {}; sl = [len(s) for s in sentences]; tw = len(words) | |
| ideal = sum(1 for l in sl if 15<=l<=70)/len(sentences) if sentences else 0 | |
| qs["๊ฐ๋ ์ฑ"] = min(100,int(ideal*70+(1-sum(1 for l in sl if l>100)/max(1,len(sentences)))*30)) | |
| wf = Counter(words); uw = len(wf) | |
| mattr = (sum(len(set(words[i:i+50]))/50 for i in range(max(1,tw-50)))/max(1,tw-50)) if tw>=100 else (uw/tw if tw>0 else 0.5) | |
| hr = sum(1 for c in wf.values() if c==1)/tw if tw>0 else 0 | |
| qs["์ดํํ๋ถ๋"] = min(100,int(mattr*80+hr*40)) | |
| lc = {'์์ ':['๊ทธ๋์','๋ฐ๋ผ์'],'์ญ์ ':['๊ทธ๋ฌ๋','ํ์ง๋ง','๋ค๋ง'],'์ฒจ๊ฐ':['๋ํ','๊ทธ๋ฆฌ๊ณ ','๊ฒ๋ค๊ฐ'],'์ ํ':['ํํธ'],'์์':['์๋ฅผ ๋ค์ด'],'์์ฝ':['๊ฒฐ๊ตญ','๊ฒฐ๋ก ์ ์ผ๋ก']} | |
| ut = sum(1 for cw in lc.values() if any(w in text for w in cw)) | |
| qs["๋ ผ๋ฆฌ๊ตฌ์กฐ"] = min(100,int(ut/len(lc)*60+min(40,ut*10))) | |
| si = sum(1 for p in [re.compile(r'๋ฌ'),re.compile(r'๋ช์ผ'),re.compile(r'๊ธ์')] if p.search(text)) | |
| spi = sum(1 for p in [re.compile(r'ํ ์์'),re.compile(r'๊ฒ๊ฐ')] if p.search(text)) | |
| qs["์ ํ์ฑ"] = max(0,100-(si+spi)*15) | |
| ar=0;vv=0 | |
| if morphemes: | |
| pc = Counter(t for _,t in morphemes); tm = sum(pc.values()) | |
| ar = sum(pc.get(t,0) for t in ['VA','MAG','MAJ'])/tm if tm else 0 | |
| vv = len(set(f for f,t in morphemes if t in ['VV','VA']))/max(1,sum(1 for _,t in morphemes if t in ['VV','VA'])) | |
| qs["ํํํ๋ถ์ฑ"] = min(100,int(ar*200+vv*30)) | |
| cr = 0.5 | |
| if morphemes: | |
| ct={'NNG','NNP','VV','VA','MAG'}; ft={'JKS','JKC','JKG','JKO','JX','JC','EP','EF','EC','ETN','ETM'} | |
| cc=sum(1 for _,t in morphemes if t in ct); fc=sum(1 for _,t in morphemes if t in ft) | |
| cr = cc/(cc+fc) if (cc+fc)>0 else 0.5 | |
| qs["์ ๋ณด๋ฐ๋"] = min(100,int(cr*80)) | |
| wq = {"๊ฐ๋ ์ฑ":.20,"์ดํํ๋ถ๋":.18,"๋ ผ๋ฆฌ๊ตฌ์กฐ":.18,"์ ํ์ฑ":.18,"ํํํ๋ถ์ฑ":.13,"์ ๋ณด๋ฐ๋":.13} | |
| total = int(sum(qs[k]*wq[k] for k in wq)) | |
| grade = "S" if total>=85 else "A" if total>=72 else "B" if total>=58 else "C" if total>=42 else "D" if total>=28 else "F" | |
| return {"score":total,"grade":grade,"sub_scores":qs} | |
| LLM_JUDGES = [("openai/gpt-oss-120b","GPT-OSS 120B"),("qwen/qwen3-32b","Qwen3 32B"),("moonshotai/kimi-k2-instruct-0905","Kimi-K2")] | |
| def _parse_ai_probability(raw_resp): | |
| """LLM ์๋ต์์ AI ํ๋ฅ (0~100)์ ์ถ์ถ. ํ๊ตญ์ด/์์ด ๋ค์ํ ํ์ ๋์.""" | |
| if not raw_resp: return -1 | |
| think_content = '' | |
| think_m = re.search(r'<think>(.*?)</think>', raw_resp, flags=re.S) | |
| if think_m: think_content = think_m.group(1) | |
| resp = re.sub(r'<think>.*?</think>', '', raw_resp, flags=re.S).strip() | |
| if not resp or len(resp) < 5: | |
| resp = raw_resp # think๋ง ์์ผ๋ฉด ์๋ณธ ํฌํจ | |
| specific_patterns = [ | |
| r'AI\s*ํ๋ฅ \s*[:๏ผ]\s*(?:์ฝ\s*)?(\d+)\s*%?', | |
| r'AI\s*[Pp]robability\s*[:๏ผ]\s*(?:about|approximately?\s*)?(\d+)\s*%?', | |
| r'[Pp]robability\s*(?:of\s*)?(?:being\s*)?AI\s*[:๏ผ\-]\s*(?:about|approximately?\s*)?(\d+)\s*%?', | |
| r'AI\s*(?:์์ฑ|์์ฑ|ํ์ )?\s*ํ๋ฅ \s*[:๏ผ]?\s*(?:์ฝ\s*)?(\d+)', | |
| r'(?:Score|Rating|Confidence)\s*[:๏ผ]\s*(\d+)', | |
| r'(\d+)\s*%\s*(?:์\s*)?(?:ํ๋ฅ |๊ฐ๋ฅ์ฑ|probability|likely|chance|likelihood)', | |
| r'(?:์ ๋ขฐ๋|ํ์ ๋)\s*[:๏ผ]?\s*(?:์ฝ\s*)?(\d+)\s*(?:%|ํผ์ผํธ)', | |
| r'(?:์ฝ\s*)?(\d+)\s*(?:%|ํผ์ผํธ)\s*(?:์ ๋|์์ค)', | |
| ] | |
| for pat in specific_patterns: | |
| m = re.search(pat, resp, re.I) | |
| if m: | |
| v = int(m.group(1)) | |
| if 0 <= v <= 100: return v | |
| lines = [l.strip() for l in resp.strip().split('\n') if l.strip()] | |
| for line in reversed(lines[-5:]): | |
| if re.search(r'AI|ํ๋ฅ |[Pp]robab|์ ๋ขฐ|ํ[์ ๋จ]', line): | |
| nums = re.findall(r'(\d+)\s*%', line) | |
| if nums: | |
| v = int(nums[-1]) | |
| if 0 <= v <= 100: return v | |
| nums = re.findall(r'(\d+)\s*ํผ์ผํธ', line) | |
| if nums: | |
| v = int(nums[-1]) | |
| if 0 <= v <= 100: return v | |
| all_pcts = list(re.finditer(r'(\d+)\s*(?:%|ํผ์ผํธ|percent)', resp, re.I)) | |
| for m in reversed(all_pcts): | |
| v = int(m.group(1)) | |
| ctx_start = max(0, m.start()-50) | |
| ctx = resp[ctx_start:m.end()+20] | |
| if re.search(r'AI|ํ๋ฅ |[Pp]robab|์ ๋ขฐ|ํ[์ ๋จ]|๊ฐ๋ฅ์ฑ|likelihood', ctx, re.I): | |
| if 0 <= v <= 100: return v | |
| if all_pcts: | |
| v = int(all_pcts[-1].group(1)) | |
| if 5 <= v <= 99: return v # 100% ์ ์ธ (ํต๊ณ ์์น ์คํ ๋ฐฉ์ง) | |
| if think_content: | |
| for pat in specific_patterns: | |
| m = re.search(pat, think_content, re.I) | |
| if m: | |
| v = int(m.group(1)) | |
| if 0 <= v <= 100: return v | |
| think_pcts = re.findall(r'(\d+)\s*%', think_content) | |
| if think_pcts: | |
| v = int(think_pcts[-1]) | |
| if 5 <= v <= 99: return v | |
| return -1 | |
| def llm_cross_check(text): | |
| if not GROQ_KEY: return {"score":-1,"detail":{}} | |
| prompt = f"""Analyze whether this text was written by AI. | |
| [Instructions] | |
| 1. Determine AI vs Human with 3 brief reasons | |
| 2. IMPORTANT - Your LAST line MUST be exactly this format: | |
| AIํ๋ฅ : XX% | |
| (Replace XX with your estimated probability 0-100) | |
| Example of correct last line: | |
| AIํ๋ฅ : 75% | |
| [Text to analyze] | |
| {text[:2000]}""" | |
| votes=[]; rpt={} | |
| for mid,mn in LLM_JUDGES: | |
| resp,err = call_groq(mid,prompt) | |
| if resp: | |
| p = _parse_ai_probability(resp) | |
| if p >= 0: | |
| votes.append(p); rpt[mn]=f"{p}%" | |
| else: | |
| cleaned = re.sub(r'<think>.*?</think>', '', resp, flags=re.S).strip() | |
| tail = cleaned[-60:].replace('\n',' ') if len(cleaned) > 60 else cleaned.replace('\n',' ') | |
| rpt[mn]=f"ํ์ฑ์คํจ({tail[:40]})" | |
| else: rpt[mn]=f"ERR:{err[:30] if err else '?'}" | |
| if votes: return {"score":int(sum(votes)/len(votes)),"detail":rpt} | |
| return {"score":-1,"detail":rpt} | |
| def compute_verdict(scores, llm_score=-1, sent_avg=-1, ppx_score=-1, hum_score=-1): | |
| w={"ํต๊ณ":.06,"๋ฌธ์ฒด":.25,"๋ฐ๋ณต์ฑ":.10,"๊ตฌ์กฐ":.12,"์ง๋ฌธ":.30} | |
| ws=sum(scores[k]*w[k] for k in w) | |
| if ppx_score >= 0: ws += ppx_score * 0.17 | |
| style=scores["๋ฌธ์ฒด"]; fp=scores["์ง๋ฌธ"]; rep=scores["๋ฐ๋ณต์ฑ"]; struct=scores["๊ตฌ์กฐ"] | |
| if style>=35 and fp>=35: ws+=8 | |
| elif style>=30 and fp>=25: ws+=4 | |
| if style>=30 and rep>=25 and fp>=20: ws+=4 | |
| if fp>=45: ws+=3 | |
| if struct>=50 and style>=30: ws+=3 | |
| if ppx_score>=55 and fp>=35: ws+=5 | |
| if ppx_score>=65 and style>=35: ws+=3 | |
| if hum_score>=50: | |
| ws=max(ws, 45) # Humanizer ํ์ธ โ ์ต์ AI ์์ฌ ์ค๊ฐ | |
| ws += (hum_score-50)*0.15 | |
| if sent_avg>=0 and sent_avg>ws: ws=ws*0.80+sent_avg*0.20 | |
| hi=sum(1 for v in scores.values() if v>=50) | |
| if hi>=4: ws+=8 | |
| elif hi>=3: ws+=5 | |
| elif hi>=2: ws+=2 | |
| if style<40 and fp<=20 and rep<22 and struct<35 and (ppx_score<0 or ppx_score<40): | |
| ws-=5 | |
| lo=sum(1 for v in scores.values() if v<20) | |
| if lo>=3: ws-=8 | |
| elif lo>=2: ws-=3 | |
| if llm_score>=0: ws=ws*0.70+llm_score*0.30 | |
| fs=max(0,min(100,int(ws))) | |
| if fs>=75: return fs,"AI ์์ฑ ํ์ ","ai_high" | |
| if fs>=60: return fs,"AI ์์ฌ ๋์","ai_medium" | |
| if fs>=45: return fs,"AI ์์ฌ ์ค๊ฐ","ai_low" | |
| if fs>=30: return fs,"ํ๋จ ์ ๋ณด","uncertain" | |
| return fs,"์ธ๊ฐ ์์ฑ ์ถ์ ","human" | |
| def quick_score(text): | |
| sents=split_sentences(text); words=split_words(text); morphs=get_morphemes(text) | |
| sc={"ํต๊ณ":analyze_statistics(text,sents,words)["score"],"๋ฌธ์ฒด":analyze_korean_style(text,sents,morphs)["score"], | |
| "๋ฐ๋ณต์ฑ":analyze_repetition(text,sents,words)["score"],"๊ตฌ์กฐ":analyze_structure(text,sents)["score"], | |
| "์ง๋ฌธ":analyze_model_fingerprint(text,sents)["score"]} | |
| sent_scores=[score_sentence(s)[0] for s in sents] | |
| sent_avg=sum(sent_scores)/len(sent_scores) if sent_scores else -1 | |
| ppx=analyze_perplexity(text,sents,morphs) | |
| hum=analyze_humanizer(text,sents,words,morphs) | |
| fs,v,lv=compute_verdict(sc, sent_avg=sent_avg, ppx_score=ppx["score"], hum_score=hum["score"]) | |
| return fs,v,lv,sc,ppx,hum | |
| from plagiarism_check import run_plagiarism | |
| def run_detection(text, progress=gr.Progress()): | |
| if not text or len(text.strip())<50: return "<div style='padding:20px;text-align:center;color:#888;'>โ ๏ธ ์ต์ 50์</div>","" | |
| text=text.strip() | |
| progress(0.05); sents=split_sentences(text); words=split_words(text); morphs=get_morphemes(text) | |
| progress(0.12); s1=analyze_statistics(text,sents,words) | |
| progress(0.22); s2=analyze_korean_style(text,sents,morphs) | |
| progress(0.30); s3=analyze_repetition(text,sents,words) | |
| progress(0.38); s4=analyze_structure(text,sents) | |
| progress(0.45); s5=analyze_model_fingerprint(text,sents) | |
| progress(0.52); ppx=analyze_perplexity(text,sents,morphs) | |
| progress(0.58); hum=analyze_humanizer(text,sents,words,morphs) | |
| progress(0.65); qr=analyze_quality(text,sents,words,morphs) | |
| progress(0.75); lr=llm_cross_check(text) | |
| sc={"ํต๊ณ":s1["score"],"๋ฌธ์ฒด":s2["score"],"๋ฐ๋ณต์ฑ":s3["score"],"๊ตฌ์กฐ":s4["score"],"์ง๋ฌธ":s5["score"]} | |
| sent_scores=[score_sentence(s)[0] for s in sents] | |
| sent_avg=sum(sent_scores)/len(sent_scores) if sent_scores else -1 | |
| ms_raw=s5.get("model_scores",{}) | |
| model_est=estimate_model(text,sents,morphs,ms_raw) | |
| fs,verdict,level=compute_verdict(sc,lr["score"],sent_avg=sent_avg,ppx_score=ppx["score"],hum_score=hum["score"]) | |
| progress(0.95) | |
| cm={"ai_high":("#FF4444","#FFE0E0","๋์"),"ai_medium":("#FF8800","#FFF0DD","์ค๊ฐ~๋์"),"ai_low":("#DDAA00","#FFFBE0","์ค๊ฐ"),"uncertain":("#888","#F0F0F0","๋ฎ์"),"human":("#22AA44","#E0FFE8","๋งค์ฐ ๋ฎ์")} | |
| fg,bg,conf=cm.get(level,("#888","#F0F0F0","?")) | |
| est_model=model_est.get("model","ํน์ ๋ถ๊ฐ") | |
| est_conf=model_est.get("confidence","๋ฎ์") | |
| est_reasons=model_est.get("reasons",[]) | |
| if est_model!="ํน์ ๋ถ๊ฐ" and est_conf!="๋ฎ์": | |
| mt=f"{est_model} (์ ๋ขฐ: {est_conf})" | |
| elif est_model!="ํน์ ๋ถ๊ฐ": | |
| mt=f"{est_model} (์ฐธ๊ณ )" | |
| else: | |
| mt="ํน์ ๋ถ๊ฐ" | |
| ai_sents=sum(1 for s in sent_scores if s>=40) | |
| human_sents=sum(1 for s in sent_scores if s<20) | |
| def gb(l,s,w="",desc=""): | |
| c="#FF4444" if s>=70 else "#FF8800" if s>=50 else "#DDAA00" if s>=35 else "#22AA44" | |
| wt=f"<span style='color:#999;font-size:9px;'> ร{w}</span>" if w else "" | |
| dt=f"<div style='font-size:9px;color:#888;margin-top:1px;'>{desc}</div>" if desc else "" | |
| 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>" | |
| mb="" | |
| for mn in ["GPT","Claude","Gemini","Perplexity"]: | |
| s=ms_raw.get(mn,0); mc="#FF4444" if s>=40 else "#FF8800" if s>=20 else "#CCC" | |
| tag="" | |
| if mn==est_model and est_conf!="๋ฎ์": | |
| tag=f" <span style='background:#FF4444;color:white;font-size:7px;padding:0 3px;border-radius:3px;'>์ถ์ </span>" | |
| 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>" | |
| ls="" | |
| if lr["score"]>=0: | |
| lsc=lr["score"] | |
| lr_rows="".join(f"<div style='font-size:9px;color:#555;'>{mn}: {lr['detail'].get(mn,'โ')}</div>" for _,mn in LLM_JUDGES) | |
| 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>" | |
| else: ls="<div style='margin-top:6px;padding:4px 8px;background:#F5F5F5;border-radius:4px;color:#999;font-size:9px;'>๐ค GROQ_API_KEY ๋ฏธ์ค์ </div>" | |
| qs=qr["sub_scores"]; gc={"S":"#FF6B6B","A":"#4ECDC4","B":"#45B7D1","C":"#DDAA00","D":"#FF8800","F":"#FF4444"}.get(qr["grade"],"#888") | |
| def qgb(l,s): | |
| c="#22AA44" if s>=70 else "#4ECDC4" if s>=55 else "#DDAA00" if s>=40 else "#FF8800" | |
| 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>" | |
| reasons=[] | |
| if sc["๋ฌธ์ฒด"]>=70: reasons.append("๊ฒฉ์์ฒด ์ข ๊ฒฐ์ด๋ฏธ๊ฐ ๋๋ถ๋ถ, AIํ ์ ์์ฌยท์ํฌํํ ๋ค์ ๊ฐ์ง") | |
| elif sc["๋ฌธ์ฒด"]>=50: reasons.append("๊ฒฉ์์ฒด์ AIํ ํํ์ด ํผ์ฌ") | |
| if ppx["score"]>=65: reasons.append(f"ํ ์คํธ ์์ธก ๊ฐ๋ฅ๋๊ฐ ๋งค์ฐ ๋์ (Perplexity {ppx['score']}์ )") | |
| elif ppx["score"]>=50: reasons.append(f"ํ ์คํธ ์์ธก ๊ฐ๋ฅ๋๊ฐ ๋์ (Perplexity {ppx['score']}์ )") | |
| if hum["score"]>=50: | |
| hum_sigs=", ".join(s[0] for s in hum["signals"][:3]) | |
| reasons.append(f"โ ๏ธ Humanizer/ํจ๋ฌํ๋ ์ด์ฆ ํ์ ๊ฐ์ง ({hum_sigs})") | |
| if sc["ํต๊ณ"]>=60: reasons.append("๋ฌธ์ฅ ๊ธธ์ด๊ฐ ๋งค์ฐ ๊ท ์ผํ์ฌ ๊ธฐ๊ณ์ ํจํด") | |
| if sc["๋ฐ๋ณต์ฑ"]>=50: reasons.append("๋ฌธ๋ ์ ์์ฌ ๋ฐ๋ณต, n-gram ํจํด ๊ฐ์ง") | |
| if sc["๊ตฌ์กฐ"]>=50: reasons.append("์ถ์์ ์์์ด ๋ค์, ๊ตฌ์ฒด์ ์ฌ์ค ๋ถ์กฑ") | |
| if est_model!="ํน์ ๋ถ๊ฐ" and est_conf!="๋ฎ์": | |
| est_why=", ".join(est_reasons[:2]) if est_reasons else "" | |
| reasons.append(f"๐ ์ถ์ ๋ชจ๋ธ: <b>{est_model}</b> ({est_why})") | |
| if not reasons: reasons.append("์ธ๊ฐ์ ํํ์ด ์ฐ์ธํ๋ฉฐ AI ํจํด์ด ์ฝํจ") | |
| reason_html='<br>'.join(f"โข {r}" for r in reasons) | |
| ppx_c="#FF4444" if ppx["score"]>=65 else "#FF8800" if ppx["score"]>=50 else "#DDAA00" if ppx["score"]>=35 else "#22AA44" | |
| ppx_html=f"""<div style='margin-top:8px;padding:8px;background:linear-gradient(135deg,#FFF8F0,#FFF0FF);border-radius:6px;border:1px solid #E8D0FF;'> | |
| <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> | |
| <div style='display:grid;grid-template-columns:1fr 1fr;gap:2px;'> | |
| <span style='font-size:9px;color:#777;'>์ํธ๋กํผ: {ppx.get("entropy",0)}</span> | |
| <span style='font-size:9px;color:#777;'>๋ถ์ฐ๊ท ์ผ: {ppx.get("variance",0)}</span> | |
| <span style='font-size:9px;color:#777;'>์ด์์์ธก: {ppx.get("order",0)}</span> | |
| <span style='font-size:9px;color:#777;'>Zipf์ ํฉ: {ppx.get("zipf",0)}</span> | |
| </div> | |
| </div>""" | |
| hum_html="" | |
| if hum["score"]>=30: | |
| hc="#FF4444" if hum["score"]>=65 else "#FF8800" if hum["score"]>=50 else "#DDAA00" | |
| sig_rows="".join(f"<div style='font-size:9px;color:#555;'>๐ธ {s[0]}: {s[2]}</div>" for s in hum["signals"][:4]) | |
| hum_html=f"""<div style='margin-top:8px;padding:8px;background:linear-gradient(135deg,#FFF0F0,#FFF5F0);border-radius:6px;border:1px solid #FFD0D0;'> | |
| <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> | |
| {sig_rows} | |
| </div>""" | |
| est_html="" | |
| if est_model!="ํน์ ๋ถ๊ฐ": | |
| ec="#FF4444" if est_conf=="๋์" else "#FF8800" if est_conf=="์ค๊ฐ" else "#DDAA00" | |
| ranked_html="" | |
| for m, ev in model_est.get("ranked",[])[:4]: | |
| ms_c="#FF4444" if ev["score"]>=30 else "#FF8800" if ev["score"]>=15 else "#CCC" | |
| bar_w=min(100,int(ev["score"]*1.5)) | |
| 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>" | |
| est_html=f"""<div style='margin-top:8px;padding:8px;background:linear-gradient(135deg,#F0F8FF,#F0FFF0);border-radius:6px;border:1px solid #D0E8FF;'> | |
| <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> | |
| {ranked_html} | |
| <div style='font-size:8px;color:#999;margin-top:2px;'>๊ทผ๊ฑฐ: {", ".join(est_reasons[:3]) if est_reasons else "๋ณตํฉ ์งํ"}</div> | |
| </div>""" | |
| html=f"""<div style="font-family:'Pretendard','Noto Sans KR',sans-serif;max-width:720px;margin:0 auto;"> | |
| <div style="background:{bg};border:2px solid {fg};border-radius:14px;padding:20px;margin-bottom:12px;"> | |
| <div style="display:flex;align-items:center;gap:16px;"> | |
| <div style="text-align:center;min-width:100px;"> | |
| <div style="font-size:48px;font-weight:900;color:{fg};line-height:1;">{fs}</div> | |
| <div style="font-size:11px;color:{fg};font-weight:600;">/ 100์ </div> | |
| </div> | |
| <div style="flex:1;"> | |
| <div style="font-size:18px;font-weight:800;color:{fg};margin-bottom:4px;">{"๐ด" if level=="ai_high" else "๐ " if level=="ai_medium" else "๐ก" if level in ["ai_low","uncertain"] else "๐ข"} {verdict}</div> | |
| <div style="font-size:11px;color:#666;line-height:1.6;"> | |
| AI ์์ฑ ๊ฐ๋ฅ์ฑ: <b style="color:{fg};">{conf}</b> | | |
| ์ถ์ ๋ชจ๋ธ: <b>{mt}</b><br> | |
| {len(sents)}๋ฌธ์ฅ ์ค AI์์ฌ <b>{ai_sents}๋ฌธ์ฅ</b>, ์ธ๊ฐ์ถ์ <b>{human_sents}๋ฌธ์ฅ</b> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="margin-top:12px;padding:10px;background:rgba(255,255,255,0.6);border-radius:8px;"> | |
| <div style="font-size:11px;font-weight:700;margin-bottom:4px;">๐ ํ์ ๊ทผ๊ฑฐ</div> | |
| <div style="font-size:11px;color:#555;line-height:1.7;">{reason_html}</div> | |
| </div> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;"> | |
| <div style="background:#FAFAFA;border-radius:8px;padding:10px;"> | |
| <div style="font-size:10px;font-weight:700;margin-bottom:4px;">๐ AI ํ์ง 5์ถ + Perplexity</div> | |
| {gb('โ ํต๊ณ',sc['ํต๊ณ'],'.06','Burstinessยท๋ณต์ก๋ ๊ท ์ผ์ฑ')} | |
| {gb('โก ๋ฌธ์ฒด',sc['๋ฌธ์ฒด'],'.25','๊ฒฉ์ยท์ ์์ฌยท์๋ณด๊ตฌ๋ฌธ')} | |
| {gb('โข ๋ฐ๋ณต',sc['๋ฐ๋ณต์ฑ'],'.10','n-gramยท๋ฌธ๋ยท์ข ๊ฒฐ๋ค์์ฑ')} | |
| {gb('โฃ ๊ตฌ์กฐ',sc['๊ตฌ์กฐ'],'.12','์ถ์์ฑ/๊ตฌ์ฒด์ฑ')} | |
| {gb('โค ์ง๋ฌธ',sc['์ง๋ฌธ'],'.30','GPT/Claude/Gemini/PPX')} | |
| {gb('โฅ PPX',ppx['score'],'.17','์์ธก๊ฐ๋ฅ๋ยท์ํธ๋กํผ')} | |
| </div> | |
| <div style="background:#FAFAFA;border-radius:8px;padding:10px;"> | |
| <div style="font-size:10px;font-weight:700;margin-bottom:4px;">๐ ๋ชจ๋ธ ์ง๋ฌธ</div> | |
| {mb} | |
| <div style="margin-top:8px;padding:6px;background:linear-gradient(135deg,#FAFAFA,#F0F4FF);border-radius:6px;"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;"> | |
| <span style="font-size:10px;font-weight:700;">๐ ํ์ง</span> | |
| <span style="font-size:18px;font-weight:900;color:{gc};">{qr['grade']}<span style="font-size:10px;color:#666;"> {qr['score']}์ </span></span> | |
| </div> | |
| {qgb('๊ฐ๋ ์ฑ',qs['๊ฐ๋ ์ฑ'])}{qgb('์ดํ',qs['์ดํํ๋ถ๋'])}{qgb('๋ ผ๋ฆฌ',qs['๋ ผ๋ฆฌ๊ตฌ์กฐ'])}{qgb('์ ํ',qs['์ ํ์ฑ'])} | |
| </div> | |
| </div> | |
| </div> | |
| {ppx_html}{hum_html}{est_html}{ls} | |
| </div>""" | |
| 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']}" | |
| return html, log | |
| def run_highlight(text): | |
| if not text or len(text.strip())<30: return "<div style='color:#888;'>ํ ์คํธ ํ์</div>" | |
| sents=split_sentences(text) | |
| hl=[] | |
| for s in sents: | |
| sc, reasons = score_sentence(s) | |
| if sc >= 60: bg="rgba(220,38,38,0.35)"; level="AIํ์ " | |
| elif sc >= 40: bg="rgba(249,115,22,0.30)"; level="AI์์ฌ" | |
| elif sc >= 25: bg="rgba(234,179,8,0.25)"; level="์ฃผ์" | |
| elif sc >= 10: bg="rgba(132,204,22,0.15)"; level="์ธ๊ฐ์ถ์ " | |
| else: bg="rgba(34,197,94,0.20)"; level="์ธ๊ฐ" | |
| detail_parts = [] | |
| for r in reasons: | |
| if '๊ฒฉ์' in r or '๋น๊ฒฉ์AI' in r: detail_parts.append(f"๐ค {r}") | |
| elif '์ ์์ฌ' in r: detail_parts.append(f"๐ {r}") | |
| elif '์ํฌ' in r: detail_parts.append(f"๐ {r}") | |
| elif '์ง๋ฌธ' in r: detail_parts.append(f"๐ {r}") | |
| elif '์ธ๊ฐ' in r or '๊ตฌ์ด' in r or '๋ง์ถค๋ฒ' in r: detail_parts.append(f"โ {r}") | |
| else: detail_parts.append(r) | |
| tt = ' | '.join(detail_parts) if detail_parts else 'ํน์ด ํจํด ์์' | |
| hl.append(f'<span style="background:{bg};padding:2px 4px;border-radius:4px;display:inline;line-height:2.2;border-bottom:2px solid {"#DC2626" if sc>=60 else "#F97316" if sc>=40 else "#EAB308" if sc>=25 else "#84CC16" if sc>=10 else "#22C55E"};" title="[{level}] {tt} ({sc}์ )">{s}</span>') | |
| total_scores = [score_sentence(s)[0] for s in sents] | |
| avg_sc = sum(total_scores)/len(total_scores) if total_scores else 0 | |
| ai_high = sum(1 for s in total_scores if s >= 60) | |
| ai_mid = sum(1 for s in total_scores if 40 <= s < 60) | |
| human_cnt = sum(1 for s in total_scores if s < 25) | |
| return f"""<div style='font-family:Pretendard,sans-serif;'> | |
| <div style='margin-bottom:10px;padding:10px;background:#F8F8FF;border-radius:8px;'> | |
| <div style='display:flex;gap:8px;align-items:center;font-size:11px;margin-bottom:6px;flex-wrap:wrap;'> | |
| <span style='background:rgba(220,38,38,0.35);padding:2px 8px;border-radius:3px;'>๐ด AIํ์ {ai_high}</span> | |
| <span style='background:rgba(249,115,22,0.30);padding:2px 8px;border-radius:3px;'>๐ AI์์ฌ {ai_mid}</span> | |
| <span style='background:rgba(234,179,8,0.25);padding:2px 8px;border-radius:3px;'>๐ก ์ฃผ์</span> | |
| <span style='background:rgba(132,204,22,0.15);padding:2px 8px;border-radius:3px;'>๐ข ์ธ๊ฐ {human_cnt}</span> | |
| <span style='color:#888;'>ํ๊ท {avg_sc:.0f}์ | ๋ง์ฐ์ค ์ค๋ฒโ์์ธ ๊ทผ๊ฑฐ</span> | |
| </div> | |
| <div style='font-size:10px;color:#888;'>๐ก ๊ฒฉ์์ด๋ฏธ(22) + AI์ ์์ฌ(18) + ์ํฌํํ(10~25) + ์๋ณดํจํด(10) + ๋ชจ๋ธ์ง๋ฌธ(8) + ๋ณตํฉ๋ณด๋์ค(8) โ ์ธ๊ฐ๋ง์ปค(~25)</div> | |
| </div> | |
| <div style='line-height:2.4;font-size:14px;'>{' '.join(hl)}</div> | |
| </div>""" | |
| SAMPLE_AI = """์ธ๊ณต์ง๋ฅ ๊ธฐ์ ์ ํ๋ ์ฌํ์์ ๋งค์ฐ ์ค์ํ ์ญํ ์ ํ๊ณ ์์ต๋๋ค. ํนํ ์์ฐ์ด ์ฒ๋ฆฌ ๋ถ์ผ์์์ ๋ฐ์ ์ ๋๋ถ์ ์ฑ๊ณผ๋ฅผ ๊ฑฐ๋๊ณ ์์ต๋๋ค. ์ด๋ฌํ ๊ธฐ์ ์ ๋ฐ์ ์ ๋ค์ํ ์ฐ์ ๋ถ์ผ์ ๊ธ์ ์ ์ธ ์ํฅ์ ๋ฏธ์น๊ณ ์์ผ๋ฉฐ, ํฅํ ๋์ฑ ๋ฐ์ ํ ๊ฒ์ผ๋ก ์์๋ฉ๋๋ค. | |
| ๋ํ ์์ฑํ AI์ ๋ฑ์ฅ์ผ๋ก ์ฝํ ์ธ ์ ์ ๋ฐฉ์์ด ํฌ๊ฒ ๋ณํํ๊ณ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ธฐ์ ๋ค์ ํจ์จ์ ์ธ ์ฝํ ์ธ ์์ฐ์ด ๊ฐ๋ฅํด์ก์ผ๋ฉฐ, ๊ฐ์ธ ์ฌ์ฉ์๋ค๋ ๋ค์ํ ์ฐฝ์ ํ๋์ AI๋ฅผ ํ์ฉํ ์ ์๊ฒ ๋์์ต๋๋ค. ๋ฐ๋ผ์ AI ๋ฆฌํฐ๋ฌ์์ ์ค์์ฑ์ด ๋์ฑ ๋ถ๊ฐ๋๊ณ ์์ต๋๋ค. | |
| ๋์๊ฐ AI ์ค๋ฆฌ์ ๊ท์ ์ ๋ํ ๋ ผ์๋ ํ๋ฐํ ์งํ๋๊ณ ์์ต๋๋ค. ํนํ AI๊ฐ ์์ฑํ ์ฝํ ์ธ ์ ์ ์๊ถ ๋ฌธ์ ๋ ์ค์ํ ์๋ฏธ๋ฅผ ๊ฐ์ง๋ฉฐ, ์ด์ ๋ํ ๋ฒ์ ํ๋ ์์ํฌ ๊ตฌ์ถ์ด ํ์ํฉ๋๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก ๊ธฐ์ ๋ฐ์ ๊ณผ ํจ๊ป ์ฌํ์ ํฉ์๋ฅผ ์ด๋ฃจ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.""" | |
| SAMPLE_HUMAN = """์ ์ง์ง ์์ฆ AI ๋๋ฌธ์ ๋จธ๋ฆฌ ์ํ๋คใ ใ ใ ์ด์ chatgptํํ ๋ ํฌํธ ์จ๋ฌ๋ผ๊ณ ํ๋๋ฐ ์์ ๊ต๊ณผ์ ๊ฐ์ ๊ธ๋ง ์จ์ค์ ๊ทธ๋ฅ ๋ด๊ฐ ๋ค์ ์ผ์;; | |
| ๊ทผ๋ฐ ์๊ฐํด๋ณด๋ฉด AI๊ฐ ์ด ๊ธ์ด๋ ์ฌ๋์ด ์ด ๊ธ์ด ํ์คํ ๋ค๋ฅด๊ธด ํด. ๋ญ๊ฐ... ๋๋ฌด ๊น๋ํ๋ฌ๊น? ์ฌ๋์ ์ด๋ ๊ฒ ํก์ค์์ค๋ ํ๊ณ ๋ง์ถค๋ฒ๋ ํ๋ฆฌ๊ณ ๊ทธ๋ฌ์์. | |
| ๊ต์๋์ด AI ํ์ง๊ธฐ ๋๋ฆฐ๋ค๊ณ ํด์ ์ข ๋ฌด์์ด๋ฐ ใ ใ ๋๋ ์ง์ง ์ง์ ์ด ๊ฑด๋ฐ ํน์ ์คํ ๋๋ฉด ์ด์ฉ์ง... ๊ฑฑ์ ๋๋ค ์ง์ฌ์ผ๋ก.""" | |
| def run_document_analysis(file, progress=gr.Progress()): | |
| """๋ฌธ์ ํ์ผ ์ ๋ก๋ โ ์น์ ๋ณ AI ํ์ง ํํธ๋งต + PDF ๋ณด๊ณ ์ ์์ฑ""" | |
| if file is None: | |
| return "<div style='padding:20px;text-align:center;color:#888;'>๐ ํ์ผ์ ์ ๋ก๋ํ์ธ์ (PDF, DOCX, HWP, HWPX, TXT)</div>", "", None | |
| if hasattr(file, 'path'): | |
| file_path = file.path | |
| elif hasattr(file, 'name'): | |
| file_path = file.name | |
| else: | |
| file_path = str(file) | |
| fname = os.path.basename(file_path) | |
| progress(0.05, f"๐ {fname} ์ฝ๋ ์ค...") | |
| sections, full_text, error = extract_text_from_file(file_path) | |
| if error: | |
| return f"<div style='padding:20px;text-align:center;color:#E44;'>โ ๏ธ {error}</div>", "", None | |
| if not sections or not full_text or len(full_text.strip()) < 50: | |
| return "<div style='padding:20px;text-align:center;color:#E44;'>โ ๏ธ ํ ์คํธ๊ฐ ์ถฉ๋ถํ์ง ์์ต๋๋ค (50์ ๋ฏธ๋ง)</div>", "", None | |
| progress(0.15, "์ ์ฒด ํ ์คํธ ๋ถ์...") | |
| sents_all = split_sentences(full_text) | |
| words_all = split_words(full_text) | |
| morphs_all = get_morphemes(full_text) | |
| total_score, total_verdict, total_level, total_axes, total_ppx, total_hum = quick_score(full_text) | |
| quality = analyze_quality(full_text, sents_all, words_all, morphs_all) | |
| progress(0.30, "LLM ๊ต์ฐจ๊ฒ์ฆ...") | |
| llm_result = llm_cross_check(full_text[:3000]) | |
| if llm_result["score"] >= 0: | |
| _sent_scores = [score_sentence(s)[0] for s in sents_all] | |
| _sent_avg = sum(_sent_scores)/len(_sent_scores) if _sent_scores else -1 | |
| 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"]) | |
| progress(0.45, f"{len(sections)}๊ฐ ์น์ ๋ถ์...") | |
| section_results = [] | |
| for i, sec in enumerate(sections): | |
| if len(sec.strip()) < 20: | |
| section_results.append({"idx": i+1, "text": sec, "score": -1, "verdict": "๋๋ฌด ์งง์", "skipped": True}) | |
| continue | |
| s_score, s_verdict, s_level, s_axes, _, _ = quick_score(sec) | |
| sec_sents = split_sentences(sec) | |
| sent_scores = [] | |
| for sent in sec_sents: | |
| ss = score_sentence(sent) | |
| sent_scores.append({"text": sent, "score": ss}) | |
| section_results.append({ | |
| "idx": i+1, "text": sec[:200], "score": s_score, | |
| "verdict": s_verdict, "level": s_level, "axes": s_axes, | |
| "sent_scores": sent_scores, "skipped": False | |
| }) | |
| pct = 0.45 + (i / max(len(sections), 1)) * 0.30 | |
| progress(pct, f"์น์ {i+1}/{len(sections)}") | |
| now = datetime.now().strftime("%Y-%m-%d %H:%M") | |
| ext = Path(file_path).suffix.upper() | |
| cm_map = {"ai_high": ("#FF4444", "#FFE0E0"), "ai_medium": ("#FF8800", "#FFF0DD"), | |
| "ai_low": ("#DDAA00", "#FFFBE0"), "uncertain": ("#888", "#F5F5F5"), "human": ("#22AA44", "#E0FFE8")} | |
| tc, tbg = cm_map.get(total_level, ("#888", "#F5F5F5")) | |
| heatmap_cells = [] | |
| for sr in section_results: | |
| sidx = sr["idx"] | |
| if sr["skipped"]: | |
| heatmap_cells.append(f"<div style='flex:1;min-width:18px;height:28px;background:#E0E0E0;border-radius:3px;' title='์น์ {sidx}: ์งง์'></div>") | |
| else: | |
| sc, sbg = cm_map.get(sr.get("level", "uncertain"), ("#888", "#F5F5F5")) | |
| ssc = sr["score"]; svd = sr["verdict"] | |
| heatmap_cells.append(f"<div style='flex:1;min-width:18px;height:28px;background:{sc};border-radius:3px;opacity:0.8;cursor:pointer;' title='์น์ {sidx}: AI {ssc}์ ({svd})'></div>") | |
| heatmap_bar = f"<div style='display:flex;gap:3px;margin:12px 0;'>" + ''.join(heatmap_cells) + "</div>" | |
| section_cards = [] | |
| for sr in section_results: | |
| if sr["skipped"]: continue | |
| sc, sbg = cm_map.get(sr.get("level", "uncertain"), ("#888", "#F5F5F5")) | |
| sent_html = "" | |
| for ss in sr.get("sent_scores", []): | |
| s = ss["score"] | |
| if s >= 60: sclr = "background:rgba(255,68,68,0.15);border-bottom:2px solid #FF4444;" | |
| elif s >= 40: sclr = "background:rgba(255,136,0,0.1);border-bottom:2px solid #FF8800;" | |
| elif s >= 25: sclr = "background:rgba(221,170,0,0.08);border-bottom:1px solid #DDAA00;" | |
| else: sclr = "" | |
| sent_html += f"<span style='{sclr}padding:1px 2px;border-radius:2px;' title='AI {s}์ '>{ss['text']}</span> " | |
| axes_html = "" | |
| if "axes" in sr: | |
| ax = sr["axes"] | |
| for k, v in ax.items(): | |
| axc = "#FF4444" if v >= 50 else "#FF8800" if v >= 30 else "#22AA44" | |
| axes_html += f"<span style='display:inline-block;margin:1px 3px;padding:2px 8px;background:{axc}22;border:1px solid {axc}44;border-radius:10px;font-size:9px;color:{axc};'>{k} {v}</span>" | |
| section_cards.append(f""" | |
| <div style="border:1px solid #E0E0E0;border-left:4px solid {sc};border-radius:8px;padding:14px;margin-bottom:10px;background:white;"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;"> | |
| <span style="font-weight:700;font-size:13px;">๐ ์น์ {sr['idx']}</span> | |
| <span style="background:{sbg};color:{sc};padding:4px 12px;border-radius:20px;font-weight:700;font-size:12px;">AI {sr['score']}์ ยท {sr['verdict']}</span> | |
| </div> | |
| <div style="margin-bottom:6px;">{axes_html}</div> | |
| <div style="font-size:12px;line-height:1.8;color:#333;">{sent_html}</div> | |
| </div>""") | |
| ai_high = sum(1 for s in section_results if not s["skipped"] and s["score"] >= 60) | |
| ai_med = sum(1 for s in section_results if not s["skipped"] and 35 <= s["score"] < 60) | |
| ai_low = sum(1 for s in section_results if not s["skipped"] and s["score"] < 35) | |
| valid_sections = [s for s in section_results if not s["skipped"]] | |
| llm_info = "" | |
| if llm_result["score"] >= 0: | |
| llm_rows = ''.join(f"<span style='margin-right:8px;font-size:10px;color:#555;'>{mn}: {llm_result['detail'].get(mn,'โ')}</span>" for _, mn in LLM_JUDGES) | |
| llm_info = f"<div style='margin-top:8px;padding:8px 12px;background:#F8F8FF;border-radius:6px;border:1px solid #E0E0FF;font-size:10px;'><b>๐ค LLM ๊ต์ฐจ๊ฒ์ฆ:</b> ํ๊ท {llm_result['score']}% | {llm_rows}</div>" | |
| html = f"""<div style="font-family:'Pretendard','Noto Sans KR',sans-serif;max-width:800px;margin:0 auto;background:#FAFBFC;border-radius:12px;border:1px solid #E0E0E0;overflow:hidden;"> | |
| <!-- ํค๋ --> | |
| <div style="background:linear-gradient(135deg,#1A1F36,#2D3561);padding:20px 24px;color:white;"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;"> | |
| <div> | |
| <div style="font-size:18px;font-weight:800;">๐ ๋ฌธ์ AI ๋ถ์ ๋ณด๊ณ ์</div> | |
| <div style="font-size:11px;opacity:0.7;margin-top:4px;">{fname} ยท {ext} ยท {len(sections)}๊ฐ ์น์ ยท {len(full_text)}์</div> | |
| </div> | |
| <div style="text-align:right;"> | |
| <div style="font-size:36px;font-weight:900;color:{tc};">{total_score}</div> | |
| <div style="font-size:11px;color:{tc};">{total_verdict}</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ์์ฝ --> | |
| <div style="padding:16px 24px;"> | |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:12px;"> | |
| <div style="background:white;border:1px solid #E0E0E0;border-radius:8px;padding:12px;text-align:center;"> | |
| <div style="font-size:22px;font-weight:800;color:#FF4444;">{ai_high}</div> | |
| <div style="font-size:10px;color:#888;">AI ์์ฌ ๋์</div> | |
| </div> | |
| <div style="background:white;border:1px solid #E0E0E0;border-radius:8px;padding:12px;text-align:center;"> | |
| <div style="font-size:22px;font-weight:800;color:#FF8800;">{ai_med}</div> | |
| <div style="font-size:10px;color:#888;">AI ์์ฌ ์ค๊ฐ</div> | |
| </div> | |
| <div style="background:white;border:1px solid #E0E0E0;border-radius:8px;padding:12px;text-align:center;"> | |
| <div style="font-size:22px;font-weight:800;color:#22AA44;">{ai_low}</div> | |
| <div style="font-size:10px;color:#888;">์ธ๊ฐ ํ์ </div> | |
| </div> | |
| <div style="background:white;border:1px solid #E0E0E0;border-radius:8px;padding:12px;text-align:center;"> | |
| <div style="font-size:22px;font-weight:800;color:#555;">{quality['grade']}</div> | |
| <div style="font-size:10px;color:#888;">ํ์ง ๋ฑ๊ธ</div> | |
| </div> | |
| </div> | |
| <!-- ํํธ๋งต ๋ฐ --> | |
| <div style="background:white;border:1px solid #E0E0E0;border-radius:8px;padding:12px;"> | |
| <div style="font-size:11px;font-weight:700;margin-bottom:6px;">๐บ๏ธ ์น์ ๋ณ AI ํํธ๋งต (๋นจ๊ฐ=AI์์ฌ, ์ด๋ก=์ธ๊ฐ)</div> | |
| {heatmap_bar} | |
| <div style="display:flex;gap:12px;justify-content:center;margin-top:6px;"> | |
| <span style="font-size:9px;"><span style="display:inline-block;width:10px;height:10px;background:#FF4444;border-radius:2px;"></span> AI ๋์</span> | |
| <span style="font-size:9px;"><span style="display:inline-block;width:10px;height:10px;background:#FF8800;border-radius:2px;"></span> AI ์ค๊ฐ</span> | |
| <span style="font-size:9px;"><span style="display:inline-block;width:10px;height:10px;background:#DDAA00;border-radius:2px;"></span> ๋ถํ์ค</span> | |
| <span style="font-size:9px;"><span style="display:inline-block;width:10px;height:10px;background:#22AA44;border-radius:2px;"></span> ์ธ๊ฐ</span> | |
| </div> | |
| </div> | |
| {llm_info} | |
| </div> | |
| <!-- ์น์ ์์ธ --> | |
| <div style="padding:0 24px 20px;"> | |
| <div style="font-size:13px;font-weight:700;margin-bottom:10px;">๐ ์น์ ๋ณ ์์ธ ๋ถ์ ({len(valid_sections)}๊ฐ)</div> | |
| {''.join(section_cards)} | |
| </div> | |
| <!-- ํธํฐ --> | |
| <div style="padding:10px 24px;background:#F0F3F8;border-top:1px solid #E0E0E0;display:flex;justify-content:space-between;"> | |
| <span style="font-size:11px;font-weight:800;color:#1A1F36;">AI Detector v4.0</span> | |
| <span style="font-size:9px;color:#AAA;">{now} ยท 5์ถ ์์๋ธ + LLM ๊ต์ฐจ๊ฒ์ฆ</span> | |
| </div> | |
| </div>""" | |
| progress(0.90, "PDF ๋ณด๊ณ ์ ์์ฑ...") | |
| pdf_path = _generate_pdf_report(fname, total_score, total_verdict, total_level, | |
| total_axes, quality, section_results, llm_result, now) | |
| log = f"ํ์ผ: {fname} ({ext})\n" | |
| log += f"์น์ : {len(sections)}๊ฐ | ์ ์ฒด: {len(full_text)}์\n" | |
| log += f"์ด์ : {total_score} ({total_verdict})\n" | |
| log += f"AI ์์ฌ ๋์: {ai_high} | ์ค๊ฐ: {ai_med} | ์ธ๊ฐ: {ai_low}\n" | |
| log += f"ํ์ง: {quality['grade']} ({quality['score']}์ )\n" | |
| if llm_result["score"] >= 0: | |
| log += f"LLM ๊ต์ฐจ๊ฒ์ฆ: {llm_result['score']}%\n" | |
| return html, log, pdf_path | |
| def _generate_pdf_report(fname, score, verdict, level, axes, quality, sections, llm_result, now): | |
| """HTML โ PDF ๋ณํ์ผ๋ก ๋ณด๊ณ ์ ์์ฑ""" | |
| try: | |
| cm = {"ai_high":"#FF4444","ai_medium":"#FF8800","ai_low":"#DDAA00","uncertain":"#888","human":"#22AA44"} | |
| tc = cm.get(level, "#888") | |
| sec_rows = "" | |
| for sr in sections: | |
| if sr["skipped"]: continue | |
| sc = cm.get(sr.get("level","uncertain"),"#888") | |
| sec_rows += f"<tr><td style='padding:6px 10px;border:1px solid #DDD;text-align:center;'>{sr['idx']}</td><td style='padding:6px 10px;border:1px solid #DDD;'>{sr['text'][:80]}...</td><td style='padding:6px 10px;border:1px solid #DDD;text-align:center;color:{sc};font-weight:700;'>{sr['score']}</td><td style='padding:6px 10px;border:1px solid #DDD;text-align:center;'>{sr['verdict']}</td></tr>" | |
| ax_rows = ''.join(f"<tr><td style='padding:4px 10px;border:1px solid #DDD;'>{k}</td><td style='padding:4px 10px;border:1px solid #DDD;text-align:center;font-weight:700;'>{v}/100</td></tr>" for k, v in axes.items()) | |
| html_content = f"""<!DOCTYPE html><html><head><meta charset="UTF-8"> | |
| <style> | |
| body{{font-family:sans-serif;margin:30px;font-size:12px;color:#333;}} | |
| h1{{color:#1A1F36;font-size:20px;border-bottom:3px solid #1A1F36;padding-bottom:8px;}} | |
| h2{{color:#333;font-size:14px;margin-top:20px;border-bottom:1px solid #DDD;padding-bottom:4px;}} | |
| .score-box{{display:inline-block;padding:12px 24px;background:{tc};color:white;border-radius:8px;font-size:28px;font-weight:900;margin:10px 0;}} | |
| table{{border-collapse:collapse;width:100%;margin:8px 0;}} | |
| th{{background:#F0F3F8;padding:6px 10px;border:1px solid #DDD;font-size:11px;text-align:left;}} | |
| .footer{{margin-top:30px;padding-top:10px;border-top:2px solid #1A1F36;font-size:9px;color:#888;}} | |
| </style></head><body> | |
| <h1>๐ AI ๊ธ ํ๋ณ ๋ณด๊ณ ์</h1> | |
| <p><b>ํ์ผ:</b> {fname} | <b>์์ฑ:</b> {now} | <b>์์ง:</b> AI Detector v4.0</p> | |
| <h2>์ข ํฉ ๊ฒฐ๊ณผ</h2> | |
| <div class="score-box">{score}์ </div> | |
| <span style="margin-left:12px;font-size:16px;font-weight:700;">{verdict}</span> | |
| <p><b>ํ์ง:</b> {quality['grade']} ({quality['score']}์ )</p> | |
| <h2>5์ถ ๋ถ์</h2> | |
| <table><tr><th>์ถ</th><th>์ ์</th></tr>{ax_rows}</table> | |
| <h2>์น์ ๋ณ ๋ถ์ ({len([s for s in sections if not s['skipped']])}๊ฐ)</h2> | |
| <table> | |
| <tr><th>No</th><th>๋ด์ฉ (๋ฐ์ท)</th><th>AI ์ ์</th><th>ํ์ </th></tr> | |
| {sec_rows} | |
| </table> | |
| <div class="footer"> | |
| AI Detector v4.0 โ 5์ถ ์์๋ธ + LLM ๊ต์ฐจ๊ฒ์ฆ | {now}<br> | |
| ๋ณธ ๋ณด๊ณ ์๋ AI ํ๋ณ ์ฐธ๊ณ ์๋ฃ์ด๋ฉฐ, ์ต์ข ํ๋จ์ ๊ฒํ ์์ ํ์ธ์ด ํ์ํฉ๋๋ค. | |
| </div> | |
| </body></html>""" | |
| report_dir = tempfile.mkdtemp() | |
| html_path = os.path.join(report_dir, f"AI_Report_{fname}.html") | |
| with open(html_path, 'w', encoding='utf-8') as f: | |
| f.write(html_content) | |
| return html_path | |
| except Exception as e: | |
| print(f"PDF ๋ณด๊ณ ์ ์์ฑ ์ค๋ฅ: {e}") | |
| return None | |
| def extract_file_text_api(file): | |
| """ํ์ผ ์ ๋ก๋ โ ํ ์คํธ ์ถ์ถ API (ํญ1~4์ฉ)""" | |
| if file is None: | |
| return "โ ๏ธ ํ์ผ์ด ์์ต๋๋ค." | |
| if hasattr(file, 'path'): | |
| file_path = file.path | |
| elif hasattr(file, 'name'): | |
| file_path = file.name | |
| else: | |
| file_path = str(file) | |
| if not os.path.exists(file_path): | |
| return f"โ ๏ธ ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: {os.path.basename(file_path)}" | |
| fname = os.path.basename(file_path) | |
| sections, full_text, error = extract_text_from_file(file_path) | |
| if error: | |
| return f"โ ๏ธ {fname}: {error}" | |
| if not full_text or len(full_text.strip()) < 10: | |
| return f"โ ๏ธ {fname}: ํ ์คํธ ์ถ์ถ ๊ฒฐ๊ณผ ์์" | |
| return full_text.strip() | |
| with gr.Blocks(title="AI ๊ธ ํ๋ณ๊ธฐ v5.1") as demo: | |
| gr.Markdown("# ๐ AI ๊ธ ํ๋ณ๊ธฐ v5.1\n**5์ถ+Perplexity+Humanizerํ์ง+๋ชจ๋ธ์ถ์ ยท ํ์ง ์ธก์ ยท LLM ๊ต์ฐจ๊ฒ์ฆ ยท ํ์ ๊ฒ์ฌ**") | |
| with gr.Tab("๐ ๊ฒฝ์๋ ฅ ๋น๊ต"): | |
| gr.HTML(""" | |
| <style> | |
| @keyframes fadeInUp { from { opacity:0; transform:translateY(30px); } to { opacity:1; transform:translateY(0); } } | |
| @keyframes slideInLeft { from { opacity:0; transform:translateX(-40px); } to { opacity:1; transform:translateX(0); } } | |
| @keyframes slideInRight { from { opacity:0; transform:translateX(40px); } to { opacity:1; transform:translateX(0); } } | |
| @keyframes pulse { 0%,100% { transform:scale(1); } 50% { transform:scale(1.05); } } | |
| @keyframes shimmer { 0% { background-position:-200% center; } 100% { background-position:200% center; } } | |
| @keyframes barGrow { from { width:0%; } } | |
| @keyframes countUp { from { opacity:0; transform:scale(0.5); } to { opacity:1; transform:scale(1); } } | |
| @keyframes glow { 0%,100% { box-shadow:0 0 8px rgba(99,102,241,0.3); } 50% { box-shadow:0 0 20px rgba(99,102,241,0.6); } } | |
| @keyframes borderPulse { 0%,100% { border-color:rgba(99,102,241,0.3); } 50% { border-color:rgba(99,102,241,0.8); } } | |
| @keyframes checkPop { from { transform:scale(0) rotate(-180deg); opacity:0; } to { transform:scale(1) rotate(0deg); opacity:1; } } | |
| .comp-wrap { font-family:'Pretendard','Noto Sans KR',system-ui,sans-serif; max-width:860px; margin:0 auto; padding:8px; } | |
| /* ํ์ด๋ก ๋ฐฐ๋ */ | |
| .hero-banner { | |
| background: linear-gradient(135deg, #1e1b4b 0%, #312e81 30%, #4338ca 60%, #6366f1 100%); | |
| border-radius:20px; padding:32px 28px; margin-bottom:20px; position:relative; overflow:hidden; | |
| animation: fadeInUp 0.8s ease-out; | |
| } | |
| .hero-banner::before { | |
| content:''; position:absolute; top:-50%; left:-50%; width:200%; height:200%; | |
| background:radial-gradient(circle, rgba(255,255,255,0.06) 0%, transparent 60%); | |
| animation: shimmer 6s linear infinite; | |
| } | |
| .hero-title { font-size:28px; font-weight:900; color:white; margin-bottom:6px; position:relative; } | |
| .hero-sub { font-size:13px; color:rgba(255,255,255,0.75); line-height:1.6; position:relative; } | |
| .hero-badge { | |
| display:inline-block; background:linear-gradient(135deg,#f59e0b,#ef4444); color:white; | |
| font-size:11px; font-weight:800; padding:4px 12px; border-radius:20px; margin-top:10px; | |
| animation: pulse 2s ease-in-out infinite; position:relative; | |
| } | |
| /* ํต์ฌ ์์น */ | |
| .stats-row { display:grid; grid-template-columns:repeat(4,1fr); gap:10px; margin-bottom:18px; animation:fadeInUp 1s ease-out 0.2s both; } | |
| .stat-card { | |
| background:white; border-radius:14px; padding:16px 10px; text-align:center; | |
| border:2px solid #e0e7ff; transition:all 0.3s; cursor:default; | |
| } | |
| .stat-card:hover { transform:translateY(-4px); border-color:#6366f1; box-shadow:0 8px 25px rgba(99,102,241,0.15); } | |
| .stat-num { font-size:32px; font-weight:900; background:linear-gradient(135deg,#6366f1,#8b5cf6); -webkit-background-clip:text; -webkit-text-fill-color:transparent; animation:countUp 0.6s ease-out; } | |
| .stat-label { font-size:10px; color:#64748b; margin-top:2px; font-weight:600; } | |
| /* ๋น๊ต ํ ์ด๋ธ */ | |
| .comp-table-wrap { animation:fadeInUp 1.1s ease-out 0.4s both; margin-bottom:18px; } | |
| .comp-table { | |
| width:100%; border-collapse:separate; border-spacing:0; | |
| border-radius:14px; overflow:hidden; box-shadow:0 4px 20px rgba(0,0,0,0.06); | |
| } | |
| .comp-table thead th { | |
| background:linear-gradient(135deg,#1e1b4b,#3730a3); color:white; | |
| padding:12px 8px; font-size:11px; font-weight:700; text-align:center; white-space:nowrap; | |
| } | |
| .comp-table thead th:first-child { text-align:left; padding-left:14px; min-width:130px; } | |
| .comp-table thead th.ours { background:linear-gradient(135deg,#dc2626,#ef4444); font-size:12px; } | |
| .comp-table tbody td { | |
| padding:10px 8px; font-size:11px; text-align:center; border-bottom:1px solid #f1f5f9; | |
| transition:background 0.2s; | |
| } | |
| .comp-table tbody tr:hover td { background:#f8fafc; } | |
| .comp-table tbody td:first-child { text-align:left; padding-left:14px; font-weight:600; color:#1e293b; } | |
| .comp-table tbody td:nth-child(2) { background:rgba(239,68,68,0.04); font-weight:700; } | |
| .comp-table tbody tr:last-child td { border-bottom:none; } | |
| /* ์ฒดํฌ/์์ค ์์ด์ฝ */ | |
| .ck { color:#22c55e; font-weight:800; font-size:15px; animation:checkPop 0.4s ease-out; } | |
| .cx { color:#ef4444; font-weight:700; font-size:13px; } | |
| .cp { color:#f59e0b; font-weight:700; font-size:12px; } | |
| .cstar { color:#6366f1; font-weight:900; font-size:14px; } | |
| /* ์ฐจ๋ณํ ์นด๋ */ | |
| .diff-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:18px; } | |
| .diff-card { | |
| background:white; border-radius:14px; padding:16px; border:1px solid #e2e8f0; | |
| transition:all 0.3s; position:relative; overflow:hidden; | |
| } | |
| .diff-card:hover { transform:translateY(-3px); box-shadow:0 8px 25px rgba(0,0,0,0.08); } | |
| .diff-card::after { | |
| content:''; position:absolute; top:0; left:0; right:0; height:3px; | |
| background:linear-gradient(90deg,#6366f1,#8b5cf6,#a78bfa); | |
| } | |
| .diff-card:nth-child(2)::after { background:linear-gradient(90deg,#ef4444,#f97316); } | |
| .diff-card:nth-child(3)::after { background:linear-gradient(90deg,#22c55e,#10b981); } | |
| .diff-card:nth-child(4)::after { background:linear-gradient(90deg,#3b82f6,#06b6d4); } | |
| .diff-icon { font-size:24px; margin-bottom:6px; } | |
| .diff-title { font-size:13px; font-weight:800; color:#1e293b; margin-bottom:4px; } | |
| .diff-desc { font-size:10px; color:#64748b; line-height:1.5; } | |
| .diff-vs { display:inline-block; background:#fef2f2; color:#dc2626; font-size:9px; font-weight:700; padding:2px 6px; border-radius:4px; margin-top:4px; } | |
| /* ๊ฒฝ์์ฌ ๊ณต๋ต ๋ฐ */ | |
| .attack-section { animation:fadeInUp 1.3s ease-out 0.8s both; margin-bottom:14px; } | |
| .attack-title { font-size:13px; font-weight:800; color:#1e293b; margin-bottom:10px; } | |
| .attack-item { margin-bottom:8px; } | |
| .attack-label { display:flex; justify-content:space-between; align-items:center; margin-bottom:3px; } | |
| .attack-name { font-size:11px; font-weight:700; color:#334155; } | |
| .attack-weak { font-size:9px; color:#ef4444; font-weight:600; } | |
| .attack-bar { height:8px; background:#f1f5f9; border-radius:4px; overflow:hidden; } | |
| .attack-fill { height:100%; border-radius:4px; animation:barGrow 1.5s ease-out; } | |
| /* ์ฌ๋ก๊ฑด */ | |
| .slogan-box { | |
| background:linear-gradient(135deg,#faf5ff,#ede9fe,#e0e7ff); border-radius:16px; | |
| padding:20px; text-align:center; border:2px solid #c7d2fe; | |
| animation:glow 3s ease-in-out infinite; | |
| } | |
| .slogan-main { font-size:16px; font-weight:900; color:#3730a3; margin-bottom:6px; } | |
| .slogan-sub { font-size:11px; color:#6366f1; line-height:1.7; } | |
| </style> | |
| <div class="comp-wrap"> | |
| <!-- ํ์ด๋ก ๋ฐฐ๋ --> | |
| <div class="hero-banner"> | |
| <div class="hero-title">๐ฐ๐ท ํ๊ตญ์ด AI ํ๋ณ No.1</div> | |
| <div class="hero-sub"> | |
| ์นดํผํฌ๋ฌ ยท GPTZero ยท Turnitin ยท JustDone ยท Originality.ai ๋ฑ<br> | |
| ๊ธ๋ก๋ฒ 8๊ฐ ๊ฒฝ์ ์๋น์ค ์ ๋ฐ ๋ถ์ ํ ์ค๊ณ๋ ์ฐจ์ธ๋ ํ๋ณ๊ธฐ | |
| </div> | |
| <div class="hero-badge">๐ฌ v5.1 โ 3๋ ํฌ๋ฌ ๊ธฐ๋ฅ ํ์ฌ</div> | |
| </div> | |
| <!-- ํต์ฌ ์์น --> | |
| <div class="stats-row"> | |
| <div class="stat-card"><div class="stat-num">8์ถ</div><div class="stat-label">ํ์ง ์ฐจ์<br>(์ ๊ณ ์ต๋ค)</div></div> | |
| <div class="stat-card"><div class="stat-num">100%</div><div class="stat-label">ํ ์คํธ ์ ํ๋<br>(11/11 ์ํ)</div></div> | |
| <div class="stat-card"><div class="stat-num">๋ฌด๋ฃ</div><div class="stat-label">์์ ๋ฌด๋ฃ<br>(ํ์ ๋ฌด์ ํ)</div></div> | |
| <div class="stat-card"><div class="stat-num">4์ข </div><div class="stat-label">๋ชจ๋ธ ์ถ์ <br>(GPT/Claude/Gemini/PPX)</div></div> | |
| </div> | |
| <!-- ๋น๊ต ํ ์ด๋ธ --> | |
| <div class="comp-table-wrap"> | |
| <table class="comp-table"> | |
| <thead> | |
| <tr> | |
| <th>๊ธฐ๋ฅ</th> | |
| <th class="ours">๐ AI ํ๋ณ๊ธฐ<br>v5.1</th> | |
| <th>๐ ์นดํผํฌ๋ฌ<br>GPTํฌ๋ฌ</th> | |
| <th>๐ น JustDone<br>.com</th> | |
| <th>๐ GPTZero</th> | |
| <th>๐ พ Originality<br>.ai</th> | |
| <th>๐ Turnitin</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td>๐ฏ ํ๊ตญ์ด ํํ์ ๋ถ์</td> | |
| <td><span class="cstar">โ ์ ์ฉ</span></td> | |
| <td><span class="ck">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cp">โณ</span></td> | |
| </tr> | |
| <tr> | |
| <td>๐ ํ์ง ์ถ ์</td> | |
| <td><span class="cstar">8์ถ</span></td> | |
| <td>1~2์ถ</td> | |
| <td>1์ถ</td> | |
| <td>2์ถ</td> | |
| <td>1์ถ</td> | |
| <td>1์ถ</td> | |
| </tr> | |
| <tr> | |
| <td>๐ง Perplexity ํ๋ฅ ๋ถ์</td> | |
| <td><span class="ck">โ</span> 4์ค</td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="ck">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| </tr> | |
| <tr> | |
| <td>๐ก๏ธ Humanizer/Bypasser ํ์ง</td> | |
| <td><span class="ck">โ</span> 6์๊ทธ๋</td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="ck">โ</span></td> | |
| <td><span class="ck">โ</span></td> | |
| </tr> | |
| <tr> | |
| <td>๐ฏ AI ๋ชจ๋ธ ์ถ์ </td> | |
| <td><span class="cstar">โ 4๋ชจ๋ธ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cp">โณ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| </tr> | |
| <tr> | |
| <td>๐ค LLM ๊ต์ฐจ๊ฒ์ฆ</td> | |
| <td><span class="cstar">โ 3๋ชจ๋ธ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| </tr> | |
| <tr> | |
| <td>๐ ํ์ ๊ทผ๊ฑฐ ํฌ๋ช ๊ณต๊ฐ</td> | |
| <td><span class="cstar">โ 8์ถ๋ณ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cp">โณ</span></td> | |
| <td><span class="cp">โณ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| </tr> | |
| <tr> | |
| <td>๐จ ๋ฌธ์ฅ๋ณ ํ์ด๋ผ์ดํธ</td> | |
| <td><span class="ck">โ</span> 5๋จ๊ณ</td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="ck">โ</span></td> | |
| <td><span class="ck">โ</span></td> | |
| <td><span class="ck">โ</span></td> | |
| </tr> | |
| <tr> | |
| <td>๐ HWP/HWPX ๋ฌธ์</td> | |
| <td><span class="ck">โ</span></td> | |
| <td><span class="ck">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| </tr> | |
| <tr> | |
| <td>๐ ๊ธ ํ์ง ์ธก์ </td> | |
| <td><span class="ck">โ</span> 6ํญ๋ชฉ</td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cp">โณ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="ck">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| </tr> | |
| <tr> | |
| <td>๐ ํ์ ๊ฒ์ฌ</td> | |
| <td><span class="ck">โ</span> 5์์ค</td> | |
| <td><span class="ck">โ</span> DB</td> | |
| <td><span class="ck">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="cx">โ</span></td> | |
| <td><span class="ck">โ</span> DB</td> | |
| </tr> | |
| <tr> | |
| <td>๐ฐ ๊ฐ๊ฒฉ</td> | |
| <td><b style="color:#22c55e;">์์ ๋ฌด๋ฃ</b></td> | |
| <td>9,900์/๊ฑด</td> | |
| <td>$7.99~/์</td> | |
| <td>$8.33~/์</td> | |
| <td>$14.95/์</td> | |
| <td>๊ธฐ๊ด ์ ์ฉ</td> | |
| </tr> | |
| <tr> | |
| <td>๐ฏ ๋์ ๋ชจ๋ธ</td> | |
| <td><b style="color:#6366f1;">์ AI ๋ชจ๋ธ</b></td> | |
| <td>GPT๋ง</td> | |
| <td>๋ฒ์ฉ</td> | |
| <td>๋ฒ์ฉ</td> | |
| <td>๋ฒ์ฉ</td> | |
| <td>๋ฒ์ฉ</td> | |
| </tr> | |
| <tr> | |
| <td>โ ๏ธ ํต์ฌ ์ฝ์ </td> | |
| <td style="font-size:9px;color:#6366f1;">๊ท์น ๊ธฐ๋ฐ<br>(ML ๋ฏธ์ ์ฉ)</td> | |
| <td style="font-size:9px;color:#dc2626;">GPT๋งยท์คํ๋ฅ <br>89% ๋ถ๋ง</td> | |
| <td style="font-size:9px;color:#dc2626;">์ ์ 30%+ ํธ์ฐจ<br>"Hello"โ92%AI</td> | |
| <td style="font-size:9px;color:#dc2626;">ํผํฉํ ์คํธ 41%<br>ํธ์งAI 35% ์ค๋ฅ</td> | |
| <td style="font-size:9px;color:#dc2626;">$14.95/์<br>๋น์์ด ์ฝํจ</td> | |
| <td style="font-size:9px;color:#dc2626;">๊ธฐ๊ด๋ง ์ฌ์ฉ<br>ํ์ ์ ๊ทผ ๋ถ๊ฐ</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- ์ฐจ๋ณํ 4๋ ์นด๋ --> | |
| <div class="diff-grid" style="animation:fadeInUp 1.2s ease-out 0.6s both;"> | |
| <div class="diff-card"> | |
| <div class="diff-icon">๐ง </div> | |
| <div class="diff-title">Perplexity ํ๋ฅ ๋ถ์</div> | |
| <div class="diff-desc">GPTZero์ ํต์ฌ ๊ธฐ์ ์ ํ๊ตญ์ด์ ํนํ.<br>๋ฌธ์ ์ํธ๋กํผ + Burstiness + TTRํธ์ฐจ + ์ข ๊ฒฐ์ํธ๋กํผ 4์ค ๋ถ์์ผ๋ก AI ํ ์คํธ์ ์์ธก ๊ฐ๋ฅ๋๋ฅผ ์ ๋ฐ ์ธก์ </div> | |
| <div class="diff-vs">vs GPTZero: ํ๊ตญ์ด ํํ์ ๊ธฐ๋ฐ โ ๋ ์ ํ</div> | |
| </div> | |
| <div class="diff-card"> | |
| <div class="diff-icon">๐ก๏ธ</div> | |
| <div class="diff-title">Humanizer/Bypasser ํ์ง</div> | |
| <div class="diff-desc">QuillBot ๋ฑ ํจ๋ฌํ๋ ์ด์ฆ ๋๊ตฌ๋ก ์์ ๋ AI ๊ธ์ ์์กด ํ์ 6๊ฐ์ง ์๊ทธ๋๋ก ํ์ง.<br>๋์์ด ๊ณผ๋ค์นํ, ๊ตฌ์กฐ๋ณด์กด+์ดํ๋ณ๊ฒฝ, ์ ์์ฌ ๋ฑ๊ฐ๊ฒฉ ์์กด ํจํด</div> | |
| <div class="diff-vs">vs Turnitin ์ ์ฉ ๋ชจ๋ โ ํ๊ตญ์ด ์ต์ด ๊ตฌํ</div> | |
| </div> | |
| <div class="diff-card"> | |
| <div class="diff-icon">๐ฏ</div> | |
| <div class="diff-title">AI ๋ชจ๋ธ ์ถ์ (์ ๊ณ ์ ์ผ)</div> | |
| <div class="diff-desc">GPT ยท Claude ยท Gemini ยท Perplexity 4๊ฐ ๋ชจ๋ธ์ ๊ณ ์ ์ง๋ฌธ์ ๋ณตํฉ ์ฆ๊ฑฐ๋ก ๋ถ์.<br>"์ด๋ค AI๊ฐ ์ผ๋์ง"๊น์ง ์ถ์ ํ๋ ์ ์ผํ ํ๊ตญ์ด ์๋น์ค</div> | |
| <div class="diff-vs">vs ์ ์ฒด ๊ฒฝ์์ฌ: ๋ชจ๋ธ ์ถ์ ๊ธฐ๋ฅ ์์</div> | |
| </div> | |
| <div class="diff-card"> | |
| <div class="diff-icon">๐ค</div> | |
| <div class="diff-title">LLM 3์ค ๊ต์ฐจ๊ฒ์ฆ (์ ๊ณ ์ ์ผ)</div> | |
| <div class="diff-desc">GPT-OSS-120B ยท Qwen3-32B ยท Kimi-K2<br>3๊ฐ ๋ ๋ฆฝ AI๊ฐ ์ง์ ํ๋ โ ๋ค์๊ฒฐ ํฌํ๋ก ๊ท์น ๊ธฐ๋ฐ์ ํ๊ณ ๋ณด์</div> | |
| <div class="diff-vs">vs ์ ์ฒด ๊ฒฝ์์ฌ: LLM ๊ฒ์ฆ ๋ฏธ๋ณด์ </div> | |
| </div> | |
| </div> | |
| <!-- ๊ฒฝ์์ฌ ์ทจ์ฝ์ ๊ณต๋ต --> | |
| <div class="attack-section"> | |
| <div class="attack-title">โ๏ธ ๊ฒฝ์์ฌ ์ทจ์ฝ์ vs ์ฐ๋ฆฌ์ ๊ฐ์ </div> | |
| <div class="attack-item"> | |
| <div class="attack-label"><span class="attack-name">๐ ์นดํผํฌ๋ฌ โ GPT๋ง ํ์ง, 89% ๋ถ๋ง์จ</span><span class="attack-weak">โ ์ AI ๋ชจ๋ธ + 8์ถ ํฌ๋ช ๊ทผ๊ฑฐ</span></div> | |
| <div class="attack-bar"><div class="attack-fill" style="width:92%;background:linear-gradient(90deg,#6366f1,#8b5cf6);"></div></div> | |
| </div> | |
| <div class="attack-item"> | |
| <div class="attack-label"><span class="attack-name">๐ น JustDone โ ๋์ผ ํ ์คํธ 30%+ ์ ์ ํธ์ฐจ</span><span class="attack-weak">โ ๊ฒฐ์ ๋ก ์ ์ ์ ๋ณด์ฅ</span></div> | |
| <div class="attack-bar"><div class="attack-fill" style="width:95%;background:linear-gradient(90deg,#ef4444,#f97316);"></div></div> | |
| </div> | |
| <div class="attack-item"> | |
| <div class="attack-label"><span class="attack-name">๐ GPTZero โ ํผํฉํ ์คํธ 41%, ํธ์งAI 35% ์ค๋ฅ</span><span class="attack-weak">โ Humanizer ํ์ง + ์น์ ๋ถ๋ฆฌ</span></div> | |
| <div class="attack-bar"><div class="attack-fill" style="width:88%;background:linear-gradient(90deg,#22c55e,#10b981);"></div></div> | |
| </div> | |
| <div class="attack-item"> | |
| <div class="attack-label"><span class="attack-name">๐ Turnitin โ ๊ธฐ๊ด ์ ์ฉ, ํ์ ์ฌ์ ๊ฒ์ฌ ๋ถ๊ฐ</span><span class="attack-weak">โ ๋๊ตฌ๋ ๋ฌด๋ฃ ์ฆ์ ์ฌ์ฉ</span></div> | |
| <div class="attack-bar"><div class="attack-fill" style="width:90%;background:linear-gradient(90deg,#3b82f6,#06b6d4);"></div></div> | |
| </div> | |
| <div class="attack-item"> | |
| <div class="attack-label"><span class="attack-name">๐ พ Originality โ $14.95/์, ๋น์์ด ์ทจ์ฝ</span><span class="attack-weak">โ ์์ ๋ฌด๋ฃ + ํ๊ตญ์ด ์ ๋ฌธ</span></div> | |
| <div class="attack-bar"><div class="attack-fill" style="width:94%;background:linear-gradient(90deg,#a855f7,#ec4899);"></div></div> | |
| </div> | |
| </div> | |
| <!-- ์ฌ๋ก๊ฑด --> | |
| <div class="slogan-box"> | |
| <div class="slogan-main">๐ฐ๐ท ํ๊ตญ์ด AI ๊ธ์ ๊ฐ์ฅ ์ ํํ๊ฒ, ๊ฐ์ฅ ํฌ๋ช ํ๊ฒ, ์์ ๋ฌด๋ฃ๋ก ํ๋ณํฉ๋๋ค</div> | |
| <div class="slogan-sub"> | |
| vs ์นดํผํฌ๋ฌ: "GPT๋ง์ด ์๋, <b>๋ชจ๋ AI</b>๋ฅผ ํ์งํฉ๋๋ค"<br> | |
| vs GPTZero: "ํ๊ตญ์ด์ ํนํ๋ <b>ํํ์ ๋ถ์</b>์ผ๋ก ๋ ์ ํํฉ๋๋ค"<br> | |
| vs Turnitin: "๋๊ตฌ๋, <b>๋ฌด๋ฃ</b>๋ก, ์ง๊ธ ๋ฐ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค"<br> | |
| vs JustDone: "๋์ผํ ํ ์คํธ์ <b>ํญ์ ๋์ผํ ๊ฒฐ๊ณผ</b>๋ฅผ ๋ณด์ฅํฉ๋๋ค" | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Tab("๐ ๋ถ์"): | |
| gr.Markdown("ํ ์คํธ๊ฐ AI์ ์ํด ์์ฑ๋์๋์ง 5๊ฐ ์ถ์ผ๋ก ๋ถ์ํฉ๋๋ค. 0~100์ (๋์์๋ก AI ๊ฐ๋ฅ์ฑ ๋์)") | |
| inp=gr.Textbox(label="๋ถ์ํ ํ ์คํธ",placeholder="์ต์ 50์ ์ด์...",lines=10) | |
| with gr.Row(): | |
| btn_a=gr.Button("๐ AI ํ๋ณ + ํ์ง ๋ถ์",variant="primary",size="lg") | |
| btn_sa=gr.Button("๐ AI ์์",size="sm"); btn_sh=gr.Button("โ๏ธ ์ธ๊ฐ ์์",size="sm") | |
| rh=gr.HTML(); rl=gr.Textbox(label="์์ธ ๋ก๊ทธ",lines=3,elem_classes=["mono"]) | |
| btn_a.click(run_detection,[inp],[rh,rl],api_name="run_detection") | |
| btn_sa.click(lambda:SAMPLE_AI,outputs=[inp]); btn_sh.click(lambda:SAMPLE_HUMAN,outputs=[inp]) | |
| with gr.Tab("๐จ ํ์ด๋ผ์ดํธ"): | |
| gr.Markdown("๋ฌธ์ฅ๋ณ๋ก AI ํ๋ฅ ์ ์์ ํ์ํฉ๋๋ค. **ํญ1๊ณผ ๋์ผํ ๊ธฐ์ค**์ผ๋ก ํ์ ํฉ๋๋ค. ๋ง์ฐ์ค ์ค๋ฒ ์ ๊ทผ๊ฑฐ ํ์ธ.") | |
| ih=gr.Textbox(label="ํ ์คํธ",lines=8); bh=gr.Button("๐จ ํ์ด๋ผ์ดํธ ๋ถ์",variant="primary"); hr=gr.HTML() | |
| bh.click(run_highlight,[ih],[hr],api_name="run_highlight") | |
| with gr.Tab("๐ ํ์ ๊ฒ์ฌ"): | |
| gr.Markdown("**Brave Search ๋ณ๋ ฌ(์ต๋20) + KCI ยท RISS ยท arXiv + Gemini Google Search** ๊ธฐ๋ฐ ํ์ ๊ฒ์ฌ. CopyKiller ์คํ์ผ ๋ณด๊ณ ์.") | |
| inp_plag=gr.Textbox(label="๊ฒ์ฌํ ํ ์คํธ",placeholder="ํ์ ๊ฒ์ฌํ ํ ์คํธ (์ต์ 50์)...",lines=10) | |
| with gr.Row(): | |
| btn_plag=gr.Button("๐ ํ์ ๊ฒ์ฌ ์์",variant="primary",size="lg") | |
| btn_ps=gr.Button("๐ AI ์์",size="sm") | |
| plag_html=gr.HTML(); plag_log=gr.Textbox(label="๊ฒ์ฌ ๋ก๊ทธ",lines=4,elem_classes=["mono"]) | |
| btn_plag.click(run_plagiarism,[inp_plag],[plag_html,plag_log],api_name="run_plagiarism") | |
| btn_ps.click(lambda:SAMPLE_AI,outputs=[inp_plag]) | |
| with gr.Tab("๐ ์ค๋ช "): | |
| gr.Markdown(""" | |
| - **ํ์ง 5์ถ:** ํต๊ณ(6%)ยท๋ฌธ์ฒด(25%)ยท๋ฐ๋ณต(10%)ยท๊ตฌ์กฐ(12%)ยท์ง๋ฌธ(30%) | |
| - **โ Perplexity ํ๋ฅ ๋ถ์(17%):** ๋ฌธ์ ์ํธ๋กํผยทBurstinessยทTTRํธ์ฐจยท์ข ๊ฒฐ์ํธ๋กํผ | |
| - **โ Humanizer ํ์ง:** ๋์์ด๊ณผ๋ค์นํยท๊ตฌ์กฐ๋ณด์กดยท์ ์์ฌ์์กดยท์ด์คํผ๋ ๋ฑ 6์๊ทธ๋ | |
| - **โ ๋ชจ๋ธ ์ถ์ :** GPTยทClaudeยทGeminiยทPerplexity 4๋ชจ๋ธ ์ฆ๊ฑฐ๊ธฐ๋ฐ ์ถ์ | |
| - **ํ์ง 6ํญ๋ชฉ:** ๊ฐ๋ ์ฑยท์ดํยท๋ ผ๋ฆฌยท์ ํ์ฑยทํํยท์ ๋ณด๋ฐ๋ | |
| - **LLM ๊ต์ฐจ๊ฒ์ฆ:** GPT-OSS-120BยทQwen3-32BยทKimi-K2 (GROQ) | |
| - **Brave Search**: ๋ณ๋ ฌ 20๊ฐ ๋์ ์น๊ฒ์ | |
| - **ํ์ DB**: KCI(ํ๊ตญํ์ ์ง์ธ์ฉ์์ธ), RISS(ํ์ ์ฐ๊ตฌ์ ๋ณด), arXiv | |
| - **Gemini**: Google Search Grounding | |
| - **๋ณด๊ณ ์**: CopyKiller ์คํ์ผ โ ์ ์ฌ๋%, ์ถ์ฒํ, ๋ฌธ์ฅ๋ณ ํ์ด๋ผ์ดํธ | |
| - `GROQ_API_KEY` โ LLM ๊ต์ฐจ๊ฒ์ฆ | |
| - `GEMINI_API_KEY` โ ํ์ ๊ฒ์ฌ (Google Search Grounding) | |
| - `BRAVE_API_KEY` โ ํ์ ๊ฒ์ฌ (Brave Search ๋ณ๋ ฌ) | |
| """) | |
| with gr.Row(visible=False): | |
| _file_in = gr.File(label="hidden_file") | |
| _text_out = gr.Textbox(label="hidden_text") | |
| _file_in.change(extract_file_text_api, [_file_in], [_text_out], api_name="extract_file_text") | |
| import shutil, pathlib | |
| static_dir = pathlib.Path("static") | |
| static_dir.mkdir(exist_ok=True) | |
| if pathlib.Path("index.html").exists(): | |
| shutil.copy("index.html", static_dir / "index.html") | |
| from fastapi import FastAPI | |
| from fastapi.responses import HTMLResponse, FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| server = FastAPI() | |
| async def serve_root(): | |
| """๋ฃจํธ URL์์ ํ๋ฆฌ๋ฏธ์ index.html ์๋น""" | |
| fp = pathlib.Path("static/index.html") | |
| if fp.exists(): | |
| return HTMLResponse(fp.read_text(encoding="utf-8")) | |
| from fastapi.responses import RedirectResponse | |
| return RedirectResponse("/gradio/") | |
| app = gr.mount_gradio_app(server, demo, path="/gradio", allowed_paths=["static"]) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |