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.3: reason = f"관련 기사 {len(snippets)}건과 내용 일치도가 매우 낮습니다. (평균 {avg_similarity*100:.0f}%)" recommendation = "기사의 핵심 사실관계가 타 언론사에서도 다루어지는지 확인이 필요합니다." return { "score": max(0, min(1, final_score)), "reason": reason, "recommendation": recommendation, "paired_results": paired_results } # --- 4. 테스트 코드 --- if __name__ == "__main__": print("CrossrefScore 모듈 테스트 시작...") test_body=""" 배드민턴 국가대표 안세영이 인도오픈 결승전에서 2-0 완승을 거두고 2주 연속 국제 대회에서 우승을 차지하는 쾌거를 이뤘다. 그런데 안세영에게 '좋지 않은' 소식도 함께 전해졌다. 안세영이 2025년 새해 들어 치른 2차례 국제 대회를 모두 제패했다. 안세영은 지난 19일 인도 뉴델리에서 열린 세계배드민턴연맹 월드투어 슈퍼 750 인도오픈 여자 단식 결승전에서 세계 12위 태국의 포른파위 초추웡을 2-0으로 물리치며 우승을 차지했다. 안세영은 이날 결승전에서 일찌감치 승기를 잡았다. 안세영은 1게임을 21-12로 압도했다. 2게임에서도 특유의 철벽 수비로 15-6까지 격차를 벌렸다. 특히 9-18로 뒤진 상황에서 마지막 힘을 다해 태국의 초추웡의 날카로운 공격을 모두 맞받아친 끝에 상대의 범실을 유도해 내며 추격 의지를 꺾어버렸다. 2게임 스코어는 21-9였다. 안세영은 12일 말레이시아 쿠알라룸푸르에서 끝난 월드투어 슈퍼 1000 말레이시아오픈에서 올해 첫 우승을 차지한 데 이어 2주 연속으로 우승 트로피를 따냈다. 안세영은 이번에 출전한 인도오픈에서 5경기를 치르는 동안 한 게임도 내주지 않는 완벽한 경기 운영으로 배드민턴 여자단식 세계 1위 다운 '최강 실력'을 자랑했다. 해 2주 연속 국제 대회에서 우승하며 기쁨을 만끽한 안세영 입장에선 불쾌할 수 있는 소식이 함께 전해졌다. 대한배드민턴협회 김택규 회장이 차기 회장 선거에 '기호 4번'을 달고 출마할 예정인 것으로 전해졌다. 안세영은 지난해 파리올림픽에서 금메달을 딴 이후 김택규 회장이 이끌어온 배드민턴협회와 배드민턴대표팀 운영의 문제를 폭로하는 '작심 발언'을 했다. 당시 안세영의 용기 있는 외침은 한국 배드민턴계의 개혁을 촉구하는 목소리로 이어졌다. 20일 이투데이 보도에 따르면 배드민턴협회는 목요일인 23일 차기 배드민턴협회장 선거를 치르기로 했다. 궁지에 몰렸다가 극적으로 출마 자격을 회복한 김택규 회장 역시 기호 4번으로 이번 선거에 나서는 것으로 전해졌다. 배드민턴협회는 이날 "(배드민턴협회) 선거운영위원회는 미뤄졌던 차기 회장 선거를 23일 오전 10시부터 오후 5시까지 진행하기로 했다"라고 밝혔다. 배드민턴협회장 선거는 애초 16일 열렸어야 했지만 선거운영위원회가 입후보를 불허한 김택규 회장이 후보자 등록 무효 효력 정지 가처분 신청을 제기했고 법원이 이를 받아들이면서 선거가 1차례 미뤄졌다. 법원은 기존 선거운영위원회의 결정에 중대한 절차적 하자가 있는 만큼 입후보 불허 조처의 효력을 임시로라도 정지해야 한다고 판단했다. 후보 자격을 되찾은 김택규 회장은 입장문을 통해 선거운영위원회를 강하게 비판했다. 김 회장은 "선거운영위원회가 23일로 날짜를 잡은 것은 지난 9일부터 선거 운동에 돌입한 세 후보와 비교하면 (나에겐) 너무나 불공정한 결정"이라고 지적했다. 이어 "법원의 판결을 무시한 선거운영위원회와 이를 방관 중인 협회를 상대로 강력한 법적 조치와 더불어 다시 선거 중지 가처분 신청을 하려 했다. 하지만 대한민국 배드민턴과 선수, 지도자, 동호인들을 사랑하는 사람으로서 차마 그렇게까지 하면 안 된다는 결론을 냈다. 이 시간부로 선거운영위원회의 결정을 수용하고 이번 선거에 임할 것"이라고 덧붙였다. 배드민턴협회장 선거에는 최승탁 전 대구배드민턴협회장(태성산업 대표), 전경훈 한국실업배드민턴연맹 회장(열정코리아 대표이사), 올림픽 금메달리스트 출신의 김동문 원광대 스포츠과학부 교수가 후보로 등록했다. 여기에 김택규 회장이 함께 후보로 선거를 치르게 됐다. """ # test_body = """ # 세계 1위 인공지능(AI) 칩 생산기업 엔비디아의 젠슨 황 최고경영자(CEO)가 “AI 경쟁에서 중국이 미국을 이길 것”이라고 경고했다. # 황 CEO는 5일(현지 시간) 영국 런던에서 파이낸셜타임스(FT) 주최로 열린 행사에서 “미국과 영국 등 서방국가들은 냉소주의에 발목이 잡혀 있다. 우리에겐 더 많은 낙관주의가 필요하다”며 이 같이 말했다. 그는 미국 각 주(州)에서 제정 중인 AI 관련 새로운 규정을 언급하며 “그 결과 50개의 새로운 규제가 생길 수도 있다”고 우려했다. 규제 환경이 서방 국가 기술 경쟁력을 떨어뜨린다는 지적이다. # 반면 중국 기업은 정부 정책에 힘입어 빠르게 기술을 발전시킬 수 있는 환경이라고 강조했다. 황 CEO는 “중국에서는 전기가 무료”라며 “에너지 보조금 정책 덕분에 현지 기술기업들이 엔비디아 대체 AI 칩을 훨씬 저렴하게 운용할 수 있다”고 말했다. # 일반적으로 엔비디아 고성능 칩이 연산 능력과 전력 효율성 면에서 화웨이 등 중국산 칩을 압도하는 것으로 평가되지만, 중국이 에너지 보조금을 지급하면 기업들이 화웨이 칩을 쓰더라도 에너지 비용을 많이 부담하지 않게 됨으로써 엔비디아 칩 장점이 일정 부분 상쇄된다는 뜻이다. # 실제로 중국이 바이트댄스, 알리바바, 텐센트 등 주요 기술 기업이 운영하는 데이터 센터에 전력 요금을 최대 50%까지 인하하는 보조금 제도를 도입했다고 FT가 최근 보도했다. 지방 정부가 자국산 칩을 사용하면 엔비디아보다 에너지 효율이 떨어져 데이터센터 운영비 부담이 크다는 업계 불만을 접수한 뒤 인센티브를 확대했다. # 황 CEO의 이날 발언은 도널드 트럼프 미국 대통령이 엔비디아 최첨단 칩 중국 수출금지를 계속 고수하겠다는 방침을 밝힌 이후 나와 더욱 주목받았다. 트럼프 대통령은 지난 2일 공개된 CBS와의 인터뷰에서 “중국이 엔비디아와 거래하는 것을 허용하겠지만 최첨단 기술을 사용하는 것은 허용하지 않을 것”이라며 “최첨단 기술은 미국 외에는 누구도 사용하지 못하게 할 것”이라고 못 박았다. # 엔비디아는 현재 AI 칩 시장 80% 이상을 장악한 독점 기업이지만, 가장 큰 중국 시장이 트럼프 행정부가 주도하는 고강도 수출 규제로 사실상 막혀있다. 이에 엔비디아는 중국 시장용으로 저(低)성능 AI 칩을 따로 제작하고 해당 칩 매출 15%를 미 정부에 지불하기로 합의했다. 그러나 이마저도 미 정부가 관련 규정 채택을 미루고 있어 사실상 판매가 중단된 상태라고 FT는 보도했다. # 트럼프 대통령은 황 CEO의 끈질긴 로비에 한때 지난 10월 30일 열린 미중 정상회담에서 엔비디아 첨단 AI 대중 수출 문제를 의제에 포함시킬 계획이었으나 참모진의 강력한 반대로 마음을 바꾼 것으로 알려졌다. 트럼프 대통령은 미중 정상회담 이후 기자들에게 “회담에서 블랙웰(엔비디아 최첨단 AI 칩 시리즈) 이야기는 나오지 않았다”고 전했다. # """ # if "여기에" in test_body: # print("\n🚨 경고: 'test_body' 변수에 테스트할 실제 기사 본문을 넣어주세요!\n") # else: result = get_crossref_score_and_reason(test_body) print("\n--- 최종 결과 ---") print(f"Score: {result['score']}") print(f"Reason: {result['reason']}") print(f"Recommendation: {result['recommendation']}") print(f"Found URLs: {result['paired_results']}")