from sentence_transformers import SentenceTransformer, util from keybert import KeyBERT import os import sys import urllib.request # 1. requests 대신 urllib 임포트 import json # 2. JSON 파싱을 위해 임포트 # --- 1. 모델 로드 --- try: sbert_model = SentenceTransformer("jhgan/ko-sbert-nli") kw_model = KeyBERT() except Exception as e: print(f"모델 로딩 중 오류 발생: {e}") sbert_model = None kw_model = None # --- 2. 하위 함수 정의 --- def extract_keywords(text: str) -> list: """(TM 1) KeyBERT로 텍스트에서 키워드를 추출합니다.""" if not kw_model or not text: return [] keywords = kw_model.extract_keywords(text, keyphrase_ngram_range=(1, 1), top_n=5, stop_words=['기자', '특파원', '오전', '오후', '입니다', '위해']) return [kw[0] for kw in keywords] import ssl def search_naver_api(keywords: list) -> list: """(API) Naver 검색 API로 Snippet,Link 수집 (urllib.request + SSL 우회)""" NAVER_ID =os.environ.get("NAVER_ID") NAVER_SECRET =os.environ.get("NAVER_SECRET") # --- Check : 키워드 확인 --- if not keywords: print("[DEBUG] 'keywords' 리스트가 비어있습니다.") return [] query = " ".join(keywords) encText = urllib.parse.quote(query) url = f"https://openapi.naver.com/v1/search/news.json?query={encText}&display=10&sort=sim" request = urllib.request.Request(url) request.add_header("X-Naver-Client-Id", NAVER_ID) request.add_header("X-Naver-Client-Secret", NAVER_SECRET) context = ssl._create_unverified_context() try: response = urllib.request.urlopen(request, context=context) rescode = response.getcode() print(f"[DEBUG] Naver API 응답 상태 코드: {rescode}") if rescode == 200: response_body = response.read() response_text = response_body.decode('utf-8') results = json.loads(response_text).get('items', []) outputs = [] for item in results: if 'description' in item and 'link' in item: outputs.append({ "snippet": item['description'].replace('', '').replace('', ''), "url": item['link'] }) return outputs #snippets = [item['description'].replace('', '').replace('', '') for item in results if 'description' in item] #return snippets else: print(f"[DEBUG] 🚨 Naver API가 오류 코드를 반환: {rescode}") return [] except urllib.error.HTTPError as http_err: # HTTP 에러 print(f"[DEBUG] 🚨 Naver API HTTP 오류 발생: {http_err.code} - {http_err.reason}") try: print(f"[DEBUG] 🚨 응답 내용: {http_err.read().decode('utf-8')}") except: pass except urllib.error.URLError as url_err: # 네트워크 에러 (SSL 포함) print(f"[DEBUG] 🚨 Naver API URL/네트워크 오류 발생: {url_err.reason}") except Exception as e: print(f"[DEBUG] 🚨 Naver API (urllib) 호출 중 알 수 없는 오류 발생: {type(e).__name__} - {e}") return [] def get_similarity_score(original_text: str, snippets: list): # -> 반환 타입이 tensor로 바뀜! """(TM 2) SBERT로 원본과 Snippet 간의 코사인 유사도 '텐서'를 계산합니다.""" if not snippets or not sbert_model: return None # <-- 실패 시 None 반환 try: original_embedding = sbert_model.encode(original_text) snippet_embeddings = sbert_model.encode(snippets) cosine_scores = util.cos_sim(original_embedding, snippet_embeddings) return cosine_scores except Exception as e: return None # --- 3. 최종 메인 함수 --- def get_crossref_score_and_reason(article_body: str) -> dict: """'내용 비신뢰성' 모듈의 최종 결과물을 반환합니다.""" keywords = extract_keywords(article_body) if not keywords: return { "score": 1.0, "reason": "본문에서 핵심 키워드를 추출할 수 없습니다.", "recommendation": "본문이 너무 짧거나 분석할 수 없는 내용입니다.", "found_urls": [] } print(f"[DEBUG] 추출된 키워드: {keywords}") search_results = search_naver_api(keywords) if not search_results: return { "score": 1.0, "reason": "관련 주제를 다룬 교차 검증 기사가 없습니다.", "recommendation": "주요 키워드가 타 언론사에서도 다루어지는지 확인이 필요합니다.", "paired_results": [] } snippets = [item['snippet'] for item in search_results] found_urls = [item['url'] for item in search_results] cosine_scores = get_similarity_score(article_body, snippets) if cosine_scores is None: return { "score": 1.0, "reason": "SBERT 유사도 계산 중 오류가 발생했습니다.", "recommendation": "모델 서버를 확인하세요.", "paired_results": [] } avg_similarity = cosine_scores.mean().item() # URL + 개별 점수' 쌍(pair) 리스트 paired_results = [] for i in range(len(snippets)): paired_results.append({ "url": found_urls[i], "similarity": cosine_scores[0][i].item() # 0~1 사이의 SBERT 점수 }) final_score = 1.0 - avg_similarity reason = f"교차 검증된 기사 {len(snippets)}건과의 평균 내용 일치도는 {avg_similarity*100:.0f}%입니다." recommendation = "양호합니다." if avg_similarity < 0.55: reason = f"관련 기사 {len(snippets)}건과 내용 일치도가 매우 낮습니다. (평균 {avg_similarity*100:.0f}%)" recommendation = "기사의 핵심 사실관계가 타 언론사에서도 다루어지는지 확인이 필요합니다." return { "score": max(0, min(1, round(final_score,4))), "reason": reason, "recommendation": recommendation, "paired_results": paired_results }