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
}