# wm_constants.py — StealthMark 상수·사전·스타일·HTML 헬퍼 모음 # app.py에서 `from wm_constants import *` 로 임포트 import html as html_lib, re, unicodedata from difflib import SequenceMatcher # ─── Unicode 워터마크 문자 집합 ─────────────────────────────────────────────── ZW_CHARS = {'0': '\u200B', '1': '\u200C'} ZW_START = '\uFEFF' ZW_END = '\u200D' ALL_ZW = {'\u200B', '\u200C', '\u200D', '\u2060', '\uFEFF'} ZW_NAMES = { '\u200B': ('ZWSP', '#b91c1c'), # 빨강 — ZWSP (L1=0) '\u200C': ('ZWNJ', '#065f46'), # 초록 — ZWNJ (L1=1) '\u200D': ('ZWJ', '#1d4ed8'), # 파랑 — ZWJ '\u2060': ('WJ', '#0d9488'), # 틸 — Word Joiner '\uFEFF': ('BOM', '#854d0e'), # 황갈 — BOM } VS_CHARS = [chr(0xFE00 + i) for i in range(16)] CGJ = '\u034F' ALL_MICRO = set(VS_CHARS) | {CGJ} PUNCT_TARGETS = set('.,!?;:·') # ─── 샘플 텍스트 ────────────────────────────────────────────────────────────── SAMPLE_TEXT_KO = ( "이라크 쿠르드군 수천 명이 이란에 진입해 지상전을 벌이고 있다는 보도가 나왔다. " "미국 폭스뉴스는 4일(현지시간) 미국 정부 관계자를 인용해 이라크에 거주해온 이란 쿠르드족이 " "이번 공격 작전의 일환으로 이란 북서부로 향하고 있다고 보도했다. " "이들은 이란 정권에 맞서는 이란계 쿠르드 민병대인 것으로 전해졌다. " "이날 이스라엘 매체 타임스오브이스라엘도 이라크에 주둔 중이던 쿠르드 민병대가 이란 국경을 넘어 공격을 개시했다고 보도했다. " "이스라엘 정부 당국자는 '우리(이스라엘)는 서부 이란에서 활동하는 쿠르드 민병대를 지원하고 있다'며 " "이들이 이란 정권에 도전하도록 해 더 큰 봉기를 유도하는 게 작전의 목표라고 밝혔다. " "이라크 쿠르드족 반군은 반(反)이란 세력 중 가장 조직력이 큰 곳으로 평가된다. " "다만 해당 내용과 상반된 정보도 나오고 있다. " "캐럴라인 레빗 백악관 대변인은 이날 열린 브리핑에서 '트럼프 대통령의 쿠르드족 세력 접촉이 " "이란의 체제 전복을 위해 무장세력을 지원하기 위한 것'이라는 언론 보도에 대해서 " "'대통령이 그런 계획에 동의했다는 것은 전혀 사실이 아니다'고 부인했다. " "앞서 미 월스트리트저널(WSJ)은 전날 트럼프 대통령이 1일 쿠르드족 지도자들과 접촉했으며, " "이들 무장세력에 무기 및 군사훈련 지원과 정보 지원을 할지와 관련해 최종 결정을 내리지는 않았다고 보도했다. " "이란 반관영 타스님통신도 이라크 쿠르드군이 무장한 채 이란으로 진입, " "지상 공격을 시작했다는 보도에 대해 '사실이 아니다'라고 전했다." ) SAMPLE_TEXT_EN = ( "The technology sector witnessed significant developments as major artificial intelligence companies " "announced groundbreaking partnerships. Industry leaders emphasized the importance of responsible AI " "development during the annual summit held in San Francisco. Several prominent researchers highlighted " "that current AI systems still face substantial challenges in areas such as reliability, safety, and " "ethical deployment. The conference attracted participants from over 40 countries, marking record " "attendance. Leading companies presented their latest innovations in content protection, watermarking " "technology, and digital rights management. Experts warned that without proper safeguards, AI-generated " "content could undermine trust in digital media. The summit concluded with a joint declaration calling " "for international cooperation on AI governance standards." ) SAMPLE_TEXT = SAMPLE_TEXT_KO # ─── 한국어 언어 사전 ───────────────────────────────────────────────────────── KO_SYNONYM = { '발': ['구축', '제작', '설계', '구현'], '기술': ['테크놀로지', '방법론', '솔루션', '기법'], '보호': ['방어', '수호', '보전', '보장'], '사용': ['활용', '이용', '적용', '운용'], '콘텐츠': ['컨텐츠', '자료', '내용물'], '데이터': ['자료', '정보', '데이타'], '플랫폼': ['시스템', '서비스', '환경'], '침해': ['위반', '훼손', '침범'], '입증': ['증명', '검증', '확인', '규명'], '대응': ['대처', '조치', '방안'], '확보': ['마련', '구축', '수립'], '실증': ['검증', '실험', '시범'], '성과': ['결과', '실적', '성취'], '의미': ['의의', '중요성', '가치'], } KO_ENDING = { '한다': ['하고 있다', '하게 된다'], '했다': ['하였다', '한 바 있다'], '된다': ['이뤄진다', '이루어진다'], '이다': ['에 해당한다'], '졌다': ['되었다'], '않다': ['아니하다'], } KO_CONNECTIVE = { '하지만': ['그러나', '다만', '그렇지만'], '또한': ['아울러', '더불어'], '따라서': ['이에 따라', '그러므로'], '때문에': ['탓에', '까닭에'], '위해': ['위하여', '목적으로'], '통해': ['통하여', '거쳐'], } KO_PARTICLE = { '에서는': ['에서', '에선'], '으로는': ['으로', '으론'], '이라고': ['라고', '이라'], } # ─── 영어 언어 사전 ─────────────────────────────────────────────────────────── EN_SYNONYM = { 'development': ['construction', 'creation', 'design', 'implementation'], 'technology': ['methodology', 'solution', 'technique', 'approach'], 'protection': ['defense', 'safeguard', 'preservation', 'security'], 'important': ['significant', 'crucial', 'essential', 'vital'], 'content': ['material', 'data', 'information'], 'analysis': ['examination', 'assessment', 'evaluation'], 'detect': ['identify', 'discover', 'find', 'recognize'], 'system': ['platform', 'framework', 'infrastructure'], 'evidence': ['proof', 'documentation', 'verification'], 'attack': ['threat', 'intrusion', 'compromise'], 'improve': ['enhance', 'optimize', 'strengthen', 'refine'], 'create': ['generate', 'produce', 'develop', 'establish'], 'reduce': ['decrease', 'minimize', 'lower', 'diminish'], 'increase': ['boost', 'expand', 'elevate', 'amplify'], 'provide': ['supply', 'deliver', 'offer', 'furnish'], 'maintain': ['sustain', 'preserve', 'uphold', 'retain'], 'demonstrate': ['illustrate', 'showcase', 'exhibit', 'display'], 'implement': ['execute', 'deploy', 'carry out', 'apply'], 'require': ['demand', 'necessitate', 'need', 'call for'], 'significant': ['substantial', 'considerable', 'notable', 'meaningful'], } EN_CONNECTIVE = { 'however': ['nevertheless', 'nonetheless', 'yet'], 'also': ['additionally', 'furthermore', 'moreover'], 'therefore': ['consequently', 'thus', 'hence'], 'because': ['since', 'as', 'due to'], 'although': ['though', 'even though', 'while'], 'meanwhile': ['in the meantime', 'at the same time', 'concurrently'], 'specifically': ['in particular', 'notably', 'especially'], 'instead': ['alternatively', 'rather', 'in place of'], 'similarly': ['likewise', 'in the same way', 'equally'], 'ultimately': ['in the end', 'eventually', 'finally'], } EN_ENDING = { 'is important': ['matters greatly', 'is significant', 'is crucial'], 'was announced': ['has been revealed', 'was disclosed', 'was reported'], 'will be': ['is expected to be', 'shall be'], 'have been': ['were', 'had been'], 'is used': ['is employed', 'is utilized', 'is applied'], 'can be': ['may be', 'is able to be', 'could be'], 'should be': ['ought to be', 'needs to be', 'must be'], 'has shown': ['has demonstrated', 'has revealed', 'has indicated'], 'is known': ['is recognized', 'is acknowledged', 'is understood'], 'was developed': ['was created', 'was designed', 'was built'], } # ─── 영어 전치사·접속사·관계사 집합 ────────────────────────────────────────── EN_PREP = { 'in', 'on', 'at', 'by', 'for', 'with', 'from', 'to', 'of', 'about', 'into', 'through', 'during', 'before', 'after', 'between', 'under', 'above', 'below', 'against', 'among', 'within', 'without', 'toward', 'upon', 'across', 'along', 'behind', 'beyond', 'except', 'since', 'until', 'around', 'beside', 'beneath', 'despite', 'regarding', 'concerning', 'throughout', } EN_CONJ = { 'and', 'but', 'or', 'nor', 'yet', 'so', 'because', 'although', 'while', 'whereas', 'unless', 'since', 'however', 'therefore', 'moreover', 'furthermore', 'nevertheless', 'consequently', 'meanwhile', 'otherwise', 'hence', 'thus', 'accordingly', 'besides', 'nonetheless', 'alternatively', 'specifically', 'additionally', 'subsequently', } EN_REL = { 'that', 'which', 'who', 'whom', 'whose', 'where', 'when', 'while', 'if', 'whether', 'although', 'though', 'because', 'since', 'as', 'until', 'before', 'after', 'once', 'unless', 'whereas', 'whenever', 'wherever', } # ─── LLM 모델 목록 & 태스크 ─────────────────────────────────────────────────── GROQ_MODELS = [ ("openai/gpt-oss-120b", "GPT-OSS 120B"), ("openai/gpt-oss-20b", "GPT-OSS 20B"), ("qwen/qwen3-32b", "Qwen3 32B"), ("llama-3.3-70b-versatile", "LLaMA 3.3 70B"), ("meta-llama/llama-4-scout-17b-16e-instruct","LLaMA 4 Scout"), ("moonshotai/kimi-k2-instruct-0905", "Kimi K2"), ("llama-3.1-8b-instant", "LLaMA 3.1 8B"), ("gemma2-9b-it", "Gemma2 9B"), ("mixtral-8x7b-32768", "Mixtral 8x7B"), ("gpt-5.2", "GPT-5.2 (OpenAI)"), ] LLM_TASKS = { "📝 요약": "다음 글을 핵심 내용 위주로 3문장 이내로 요약해 주세요:\n\n", "🔄 패러프레이징": "다음 텍스트를 같은 의미를 유지하면서 다른 표현으로 재작성해 주세요:\n\n", "📖 확장": "다음 텍스트를 2배 분량으로 배경 설명을 추가하여 확장해 주세요:\n\n", "🌐 역번역": "다음 텍스트를 영어로 번역한 뒤 다시 한국어로 번역해 주세요:\n\n", "✂️ 핵심 발췌": "다음 텍스트에서 가장 중요한 내용만 추출해 주세요:\n\n", "🎭 문체 변환": "다음 글을 편안한 구어체 스타일로 재작성해 주세요:\n\n", "📋 글머리 정리": "다음 텍스트를 핵심 글머리 목록으로 정리해 주세요:\n\n", "🔀 콜라주 재작성": "다음 텍스트의 문장 순서를 바꾸고 일부를 합치거나 나누어 재구성해 주세요:\n\n", } # ─── 분석 모드 목록 ─────────────────────────────────────────────────────────── PLAG_MODES = [ "🔍 종합 분석 — 전체 표절 검사 (Comprehensive)", "📊 N-gram 정밀 — 어구 패턴 비교", "📝 Sentence 유사성 — 문장 1:1 비교", "🔀 Structure 분석 — 순서·배치 보존", "🧠 AI 재작성 탐지 — 패러프레이징 (AI Rewrite)", "✂️ Mosaic 표절 — 짜깁기 탐지", "📖 Excerpt 발췌 — 부분 인용 탐지", ] IMG_MODES = [ "🔍 종합 유사성 분석", "🔢 Hash 지각 해시 비교", "📐 SSIM 구조적 유사도", "🎨 색상 분포 히스토그램 비교", "🧩 특징점 엣지 매칭", ] VIDEO_SIM_MODES = [ "🔍 종합 유사성 분석", "🕐 DTW 시간축 동적 매칭", "🔢 Hash 키프레임 해시 비교", "📐 SSIM 구조적 유사도", ] # ─── 기본 인라인 스타일 상수 ────────────────────────────────────────────────── BOX = "font-family:'Pretendard','Inter','Noto Sans KR',sans-serif;background:#ffffff;color:#1e293b;padding:20px;border-radius:12px;border:1px solid #e2e8f0;box-shadow:0 2px 12px rgba(99,102,241,.06);" BOX_IN = "background:#f0f4ff;color:#1e293b;border-radius:8px;padding:12px;margin-bottom:8px;border:1px solid #e2e8f0;" _CARD = "text-align:center;padding:14px;background:#f4f7ff;border-radius:12px;border:1px solid #e2e8f0;" # ─── Gradio 라이트 테마 CSS (index.html 색상 기반) ────────────────────────── DARK_CSS = ( # ── 폰트 임포트 "@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.min.css');" # ── 전체 배경·폰트 "body,.gradio-container,.main,.app,.contain,div[class*='wrap'],div.app" "{background:#f4f7ff!important;font-family:'Pretendard',-apple-system,sans-serif!important;max-width:100%!important;}" # ── 스크롤바 "*{scrollbar-width:thin;scrollbar-color:#c7d2e0 #f4f7ff;}" "::-webkit-scrollbar{width:6px;height:6px;}" "::-webkit-scrollbar-track{background:#f4f7ff;}" "::-webkit-scrollbar-thumb{background:#c7d2e0;border-radius:3px;}" # ── 푸터 숨기기 "footer,.footer,footer.svelte-1ax1toq{display:none!important;}" # ── 탭 네비게이션 ".tab-nav,.tabs>.tab-nav,div[class*='tab-nav'],div[role='tablist'],.tabitem>.tab-nav," "div.tab-nav{background:linear-gradient(180deg,#ffffff,#f0f4ff)!important;" "border-bottom:2px solid #e2e8f0!important;border-radius:16px 16px 0 0!important;" "padding:8px 10px!important;gap:6px!important;display:flex!important;flex-wrap:wrap!important;" "box-shadow:0 1px 8px rgba(99,102,241,.06)!important;}" # ── 탭 버튼 기본 ".tab-nav button,.tab-nav>button,div[class*='tab-nav'] button,div[role='tablist'] button," "div[role='tablist']>button,button[role='tab'],.tab-nav button:not(.selected)" "{color:#475569!important;font-weight:700!important;font-size:14px!important;" "border:1px solid #e2e8f0!important;border-radius:10px!important;" "padding:11px 18px!important;background:#ffffff!important;" "transition:all .25s ease!important;opacity:1!important;" "letter-spacing:-.2px!important;}" # ── 탭 버튼 hover ".tab-nav button:hover,div[role='tablist'] button:hover,button[role='tab']:hover" "{color:#1e293b!important;background:rgba(0,184,148,.06)!important;" "border-color:rgba(0,184,148,.3)!important;}" # ── 탭 버튼 선택 ".tab-nav button.selected,div[role='tablist'] button[aria-selected='true']," "button[role='tab'][aria-selected='true'],.tab-nav button.selected:hover," "div[class*='tab-nav'] button.selected" "{color:#00b894!important;font-weight:900!important;" "background:linear-gradient(135deg,rgba(0,184,148,.1),rgba(124,58,237,.06))!important;" "border-color:rgba(0,184,148,.35)!important;" "box-shadow:0 2px 12px rgba(0,184,148,.12)!important;}" # ── 탭 패널 ".tabitem,div[class*='tabitem'],div[role='tabpanel']{background:#f4f7ff!important;" "border:1px solid #e2e8f0!important;border-top:none!important;" "border-radius:0 0 14px 14px!important;padding:24px!important;}" # ── 블록 ".block,div.block{background:#ffffff!important;border:1px solid #e2e8f0!important;" "border-radius:12px!important;transition:border-color .3s,box-shadow .3s!important;" "box-shadow:0 2px 8px rgba(99,102,241,.04)!important;}" ".block:focus-within{border-color:rgba(0,184,148,.4)!important;" "box-shadow:0 0 12px rgba(0,184,148,.08)!important;}" # ── 레이블 "label,span.label-wrap,.label-wrap>span,label>span,.label-wrap,.input-label," "span[data-testid='block-label']{color:#1e293b!important;font-weight:700!important;font-size:14px!important;}" # ── textarea·input "textarea,input[type=text],input[type=password],input[type=number],textarea.scroll-hide," ".textbox textarea,.textbox input,input.border-none,div[data-testid='textbox'] textarea," "div[data-testid='password'] input{background:#ffffff!important;color:#1e293b!important;" "border-color:#1e293b!important;border-radius:10px!important;font-size:15px!important;" "caret-color:#00b894!important;line-height:1.6!important;}" "textarea::placeholder,input::placeholder,.textbox textarea::placeholder," ".textbox input::placeholder{color:#64748b!important;font-size:14px!important;}" "textarea:focus,input:focus,input[type=password]:focus{border-color:rgba(0,184,148,.5)!important;" "box-shadow:0 0 10px rgba(0,184,148,.08)!important;color:#1e293b!important;}" # ── mono textarea (로그) ".mono textarea{font-family:'D2Coding','Consolas','Courier New',monospace!important;" "font-size:13px!important;color:#1e293b!important;background:#f8fafc!important;line-height:1.7!important;" "border:1px solid #e2e8f0!important;}" # ── 드롭다운 ".dropdown,.dropdown-container,select,div[class*='dropdown'],div[data-testid='dropdown']," ".dropdown input,button[class*='dropdown']{background:#ffffff!important;color:#1e293b!important;" "border-color:#1e293b!important;border-radius:10px!important;font-size:14px!important;font-weight:600!important;}" "ul[role='listbox']{background:#ffffff!important;border-color:#1e293b!important;border-radius:10px!important;box-shadow:0 4px 16px rgba(99,102,241,.1)!important;}" "ul[role='listbox'] li{color:#1e293b!important;font-size:14px!important;}" "ul[role='listbox'] li:hover{background:rgba(0,184,148,.06)!important;}" # ── Primary 버튼 (민트 그라디언트) ".primary,.btn-primary,button.primary,button.lg.primary,.gradio-button.primary" "{background:linear-gradient(135deg,#00b894,#7c3aed)!important;color:#ffffff!important;" "font-weight:900!important;border:none!important;border-radius:12px!important;" "box-shadow:0 4px 16px rgba(0,184,148,.25)!important;font-size:15px!important;" "letter-spacing:-.3px!important;transition:all .3s!important;}" ".primary:hover,button.primary:hover{box-shadow:0 6px 24px rgba(0,184,148,.4)!important;" "transform:translateY(-1px)!important;}" # ── Secondary 버튼 ".secondary,.btn-secondary,button.secondary,.gradio-button.secondary" "{background:#ffffff!important;color:#475569!important;" "font-weight:700!important;border:1px solid #e2e8f0!important;border-radius:12px!important;" "box-shadow:0 1px 4px rgba(99,102,241,.06)!important;}" ".secondary:hover,button.secondary:hover{background:#f0f4ff!important;color:#1e293b!important;border-color:rgba(0,184,148,.3)!important;}" # ── 아코디언 ".accordion,div[class*='accordion']{background:#ffffff!important;border-color:#1e293b!important;border-radius:12px!important;}" ".accordion .label-wrap{color:#1e293b!important;font-weight:700!important;}" # ── 헤딩·텍스트 "h1,h2,h3,.markdown h1,.markdown h2,.markdown h3{color:#1e293b!important;font-weight:800!important;}" ".markdown,.prose,p,.block p,.block span{color:#475569!important;}" ".block .wrap,.info,.hint,div[class*='description'],.block .info{color:#64748b!important;font-size:13px!important;}" # ── 슬라이더 "input[type=range]{accent-color:#00b894!important;}" ".slider{--slider-color:#00b894!important;}" ".range-slider input{accent-color:#00b894!important;}" # ── 프로그레스바 ".progress-bar,.progress-bar>div{background:linear-gradient(90deg,#00b894,#7c3aed)!important;border-radius:4px!important;}" ".progress-text{color:#1e293b!important;}" # ── 라디오 그룹 ".radio-group,.wrap[data-testid='radio-group'],div[class*='radio'],fieldset[class*='radio']," "div.radio-group{background:#f4f7ff!important;border-radius:12px!important;border:none!important;}" ".radio-group label,.wrap[data-testid='radio-group'] label,div[class*='radio'] label," "div[class*='radio'] label span,fieldset label,input[type='radio']+label," "input[type='radio']~label,input[type='radio']~span,.gradio-radio label," "label[data-testid='radio-label'],div[role='radiogroup'] label,div[role='radiogroup'] div" "{color:#475569!important;font-size:14px!important;font-weight:600!important;" "background:#ffffff!important;border:1px solid #e2e8f0!important;border-radius:8px!important;" "padding:10px 14px!important;transition:all .2s!important;cursor:pointer!important;}" ".radio-group label:hover,div[class*='radio'] label:hover,div[role='radiogroup'] label:hover," "div[role='radiogroup'] div:hover{background:#f0f4ff!important;color:#1e293b!important;" "border-color:rgba(0,184,148,.3)!important;}" "input[type='radio']:checked+label,input[type='radio']:checked~label," "input[type='radio']:checked~span,.radio-group label.selected," "div[class*='radio'] label.selected,div[role='radiogroup'] label[data-checked='true']," "div[role='radiogroup'] div[data-checked],label:has(input[type='radio']:checked)," "div[class*='radio'] label:has(input:checked)" "{background:rgba(0,184,148,.08)!important;color:#00b894!important;font-weight:700!important;" "border-color:rgba(0,184,148,.35)!important;box-shadow:0 0 10px rgba(0,184,148,.08)!important;}" "input[type='radio']{accent-color:#00b894!important;}" ".info,span[class*='info'],.block .info,.gradio-container .info{color:#64748b!important;font-size:12px!important;}" "@media(max-width:768px){.tab-nav button,div[role='tablist'] button{padding:8px 10px!important;font-size:11px!important;}}" ) # ─── Gradio head 인젝션 (HF 헤더 숨기기 + 라이트 탭 강제 스타일) ────────────── HEAD_INJECT = ( "" ) # ─── 탭·섹션 HTML 헬퍼 ─────────────────────────────────────────────────────── def _tab_hdr(icon: str, color: str, title: str, desc: str) -> str: """최상위 탭 헤더 HTML 생성 (color = 'r,g,b' 형식)""" return ( f'
| 모델명 | ' + f'모델 ID | ' + f'L1 | ' + f'L2 | ' + f'L4 | ' + f'계층 | ' + f'판정 | ' + f'
|---|