# -*- coding: utf-8 -*- """ 리뷰 자동 검수 서비스 Hugging Face의 Zero-Shot Classification 모델을 사용하여 리뷰를 3단계로 분석합니다. 분석 단계: 1. 감정 분석: 긍정 / 중립 / 부정 2. 카테고리 분석: 배송 / 품질 / 사이즈 / 교환 / 서비스 등 3. 리뷰 톤 탐지: 불만 / 욕설 / 허위후기 / 광고 등 """ from transformers import pipeline import json from typing import List, Dict, Tuple from datetime import datetime import gradio as gr class ReviewAnalyzer: """리뷰를 3단계로 분석하는 클래스 1. 감정 분석: 긍정 / 중립 / 부정 2. 카테고리 분석: 배송 / 품질 / 사이즈 / 교환 / 서비스 등 3. 리뷰 톤 탐지: 불만 / 욕설 / 허위후기 / 광고 등 """ def __init__(self): """Zero-Shot Classification 파이프라인 초기화""" print("모델 로딩 중...") # 한국어를 잘 이해하는 multilingual 모델 사용 self.classifier = pipeline( "zero-shot-classification", model="MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7" ) # 1단계: 감정 분석 (개선된 프롬프트 - 구체적 예시 포함) self.sentiment_categories = [ "이 리뷰는 제품이나 서비스에 만족하며 좋아하고 추천하는 긍정적인 감정을 표현합니다. 예: 좋아요, 만족, 추천, 훌륭, 최고, 감사, 마음에 들어요", "이 리뷰는 제품이나 서비스에 대해 중립적이고 객관적으로 사실이나 상태만을 나열하며 특별한 감정 표현이 없습니다. 예: 그냥 그래요, 보통, 무난, 평범", "이 리뷰는 제품이나 서비스에 실망하고 불만족스러운 부정적인 감정을 표현합니다. 예: 별로, 실망, 불만족, 최악, 화남, 후회, 환불" ] self.sentiment_mapping = { "이 리뷰는 제품이나 서비스에 만족하며 좋아하고 추천하는 긍정적인 감정을 표현합니다. 예: 좋아요, 만족, 추천, 훌륭, 최고, 감사, 마음에 들어요": "긍정", "이 리뷰는 제품이나 서비스에 대해 중립적이고 객관적으로 사실이나 상태만을 나열하며 특별한 감정 표현이 없습니다. 예: 그냥 그래요, 보통, 무난, 평범": "중립", "이 리뷰는 제품이나 서비스에 실망하고 불만족스러운 부정적인 감정을 표현합니다. 예: 별로, 실망, 불만족, 최악, 화남, 후회, 환불": "부정" } # 2단계: 카테고리 분석 (개선된 프롬프트) self.topic_categories = [ "이 리뷰는 배송과 관련된 내용을 언급합니다. 예: 배송 빠름, 배송 늦음, 포장 상태, 택배, 도착, 파손", "이 리뷰는 제품 품질 또는 디자인과 관련된 내용을 언급합니다. 예: 재질, 내구성, 완성도, 품질 좋음, 품질 나쁨, 튼튼, 약함, 디자인, 색상, 외관, 예쁨, 스타일, 모양, 색깔", "이 리뷰는 제품 사이즈와 관련된 내용을 언급합니다. 예: 크기, 사이즈, 핏, 작음, 큼, 딱 맞음, 치수", "이 리뷰는 교환/환불과 관련된 내용을 언급합니다. 예: 교환, 환불, 반품, 환불 신청, 교환 절차", "이 리뷰는 고객 서비스와 관련된 내용을 언급합니다. 예: 고객센터, 응대, 상담, A/S, 친절, 불친절", "이 리뷰는 가격과 관련된 내용을 언급합니다. 예: 가격, 가성비, 비쌈, 저렴, 할인, 비용, 돈", "이 리뷰는 제품 기능/성능과 관련된 내용을 언급합니다. 예: 기능, 성능, 작동, 효과, 사용감, 편리함" ] self.topic_mapping = { "이 리뷰는 배송과 관련된 내용을 언급합니다. 예: 배송 빠름, 배송 늦음, 포장 상태, 택배, 도착, 파손": "배송", "이 리뷰는 제품 품질 또는 디자인과 관련된 내용을 언급합니다. 예: 재질, 내구성, 완성도, 품질 좋음, 품질 나쁨, 튼튼, 약함, 디자인, 색상, 외관, 예쁨, 스타일, 모양, 색깔": "품질/디자인", "이 리뷰는 제품 사이즈와 관련된 내용을 언급합니다. 예: 크기, 사이즈, 핏, 작음, 큼, 딱 맞음, 치수": "사이즈", "이 리뷰는 교환/환불과 관련된 내용을 언급합니다. 예: 교환, 환불, 반품, 환불 신청, 교환 절차": "교환/환불", "이 리뷰는 고객 서비스와 관련된 내용을 언급합니다. 예: 고객센터, 응대, 상담, A/S, 친절, 불친절": "서비스", "이 리뷰는 가격과 관련된 내용을 언급합니다. 예: 가격, 가성비, 비쌈, 저렴, 할인, 비용, 돈": "가격", "이 리뷰는 제품 기능/성능과 관련된 내용을 언급합니다. 예: 기능, 성능, 작동, 효과, 사용감, 편리함": "기능/성능" } # 3단계: 리뷰 톤 탐지 (개선된 프롬프트 - 일반 우선) self.tone_categories = [ "이 리뷰는 제품에 대한 솔직한 감상과 평가를 담고 있으며, 긍정적이든 부정적이든 진실된 사용 경험을 공유합니다", "이 리뷰는 제품의 결함, 배송지연, 서비스 문제 등 명백한 불만사항을 언급하며 부정적인 경험을 표현합니다", "이 리뷰는 텔레그램, 카카오톡 등 메신저 아이디(@로 시작), 전화번호, 이메일 같은 연락처를 포함하거나, '연락주세요', '도매가', '반값', '할인', '쿠폰' 등으로 다른 판매처나 거래를 유도하는 명백한 광고/스팸 내용입니다" ] self.tone_mapping = { "이 리뷰는 제품에 대한 솔직한 감상과 평가를 담고 있으며, 긍정적이든 부정적이든 진실된 사용 경험을 공유합니다": "일반", "이 리뷰는 제품의 결함, 배송지연, 서비스 문제 등 명백한 불만사항을 언급하며 부정적인 경험을 표현합니다": "불만", "이 리뷰는 텔레그램, 카카오톡 등 메신저 아이디(@로 시작), 전화번호, 이메일 같은 연락처를 포함하거나, '연락주세요', '도매가', '반값', '할인', '쿠폰' 등으로 다른 판매처나 거래를 유도하는 명백한 광고/스팸 내용입니다": "광고" } print("모델 로딩 완료!") print("✓ 3단계 분석 모드 활성화 (감정 → 카테고리 → 톤)") def preprocess_text(self, text: str) -> str: """ 텍스트 전처리 (성능 개선용) Args: text: 원본 텍스트 Returns: 전처리된 텍스트 """ # 앞뒤 공백 제거 text = text.strip() # 연속된 공백을 하나로 import re text = re.sub(r'\s+', ' ', text) return text def split_into_sentences(self, text: str) -> List[str]: """ 텍스트를 문장 단위로 분리 Args: text: 원본 텍스트 Returns: 문장 리스트 """ import re # 문장 종결 기호를 기준으로 분리 (., !, ?, ~, ㅎㅎ, ㅋㅋ 등 고려) # 이모티콘과 특수문자 패턴 보존 sentences = re.split(r'[.!?~]+\s*', text) # 빈 문장 제거 및 정리 sentences = [s.strip() for s in sentences if s.strip() and len(s.strip()) > 2] return sentences if sentences else [text] def analyze_sentiment(self, text: str, use_sentence_split: bool = True) -> Dict: """ 1단계: 감정 분석 (긍정 / 중립 / 부정) Args: text: 리뷰 텍스트 use_sentence_split: 문장 분리 후 분석 여부 (긴 문장 개선용) Returns: 감정 분석 결과 """ # 긴 문장(100자 이상)인 경우 문장 분리 후 분석 if use_sentence_split and len(text) > 100: sentences = self.split_into_sentences(text) if len(sentences) > 1: # 각 문장별 감정 점수 수집 all_scores = {cat: [] for cat in self.sentiment_mapping.values()} for sentence in sentences: result = self.classifier( sentence, self.sentiment_categories, multi_label=False ) # 각 카테고리별 점수 수집 for label, score in zip(result['labels'], result['scores']): category = self.sentiment_mapping[label] all_scores[category].append(score) # 평균 점수 계산 avg_scores = { cat: sum(scores) / len(scores) if scores else 0 for cat, scores in all_scores.items() } # 가장 높은 점수의 감정 선택 top_sentiment = max(avg_scores.items(), key=lambda x: x[1]) sentiment = top_sentiment[0] confidence = top_sentiment[1] scores_dict = { cat: round(score * 100, 2) for cat, score in avg_scores.items() } return { "sentiment": sentiment, "confidence": round(confidence * 100, 2), "scores": scores_dict, "method": "sentence_split" } # 기본 단일 분석 result = self.classifier( text, self.sentiment_categories, multi_label=False ) top_category = result['labels'][0] top_score = result['scores'][0] sentiment = self.sentiment_mapping[top_category] scores_dict = { self.sentiment_mapping[label]: round(score * 100, 2) for label, score in zip(result['labels'], result['scores']) } return { "sentiment": sentiment, "confidence": round(top_score * 100, 2), "scores": scores_dict, "method": "single" } def analyze_category(self, text: str, top_k: int = 3, use_sentence_split: bool = True, min_threshold: float = 0.25) -> Dict: """ 2단계: 카테고리 분석 (배송 / 품질 / 사이즈 / 교환 / 서비스 등) Args: text: 리뷰 텍스트 top_k: 상위 몇 개 카테고리를 반환할지 (기본 3개) use_sentence_split: 문장 분리 후 분석 여부 (긴 문장 개선용) min_threshold: 카테고리 선택 최소 임계값 (기본 0.25 = 25%) Returns: 카테고리 분석 결과 """ # 긴 문장인 경우 문장별로 분석 후 집계 if use_sentence_split and len(text) > 100: sentences = self.split_into_sentences(text) if len(sentences) > 1: # 각 카테고리별 점수 누적 accumulated_scores = {cat: [] for cat in self.topic_mapping.values()} for sentence in sentences: result = self.classifier( sentence, self.topic_categories, multi_label=True ) # 카테고리별 점수 수집 for label, score in zip(result['labels'], result['scores']): category = self.topic_mapping[label] accumulated_scores[category].append(score) # 최대 점수로 집계 (어느 한 문장에서라도 높게 나오면 해당 카테고리로 인정) max_scores = { cat: max(scores) if scores else 0 for cat, scores in accumulated_scores.items() } # 점수 기준으로 정렬 sorted_categories = sorted(max_scores.items(), key=lambda x: x[1], reverse=True) # 상위 k개 선택 (임계값 이상만) categories = [] for cat, score in sorted_categories[:top_k]: if score >= min_threshold: categories.append({ "category": cat, "confidence": round(score * 100, 2) }) all_scores = { cat: round(score * 100, 2) for cat, score in sorted_categories } return { "main_categories": categories, "all_scores": all_scores, "method": "sentence_split" } # 기본 단일 분석 result = self.classifier( text, self.topic_categories, multi_label=True # 여러 카테고리가 동시에 해당될 수 있음 ) # 상위 k개의 카테고리 추출 categories = [] for i in range(min(top_k, len(result['labels']))): label = result['labels'][i] score = result['scores'][i] # 임계값 이상의 확신도를 가진 카테고리만 포함 if score >= min_threshold: categories.append({ "category": self.topic_mapping[label], "confidence": round(score * 100, 2) }) all_scores = { self.topic_mapping[label]: round(score * 100, 2) for label, score in zip(result['labels'], result['scores']) } return { "main_categories": categories, "all_scores": all_scores, "method": "single" } def analyze_tone(self, text: str) -> Dict: """ 3단계: 리뷰 톤 탐지 (불만 / 욕설 / 허위후기 / 광고 등) Args: text: 리뷰 텍스트 Returns: 톤 분석 결과 """ result = self.classifier( text, self.tone_categories, multi_label=False ) top_category = result['labels'][0] top_score = result['scores'][0] tone = self.tone_mapping[top_category] scores_dict = { self.tone_mapping[label]: round(score * 100, 2) for label, score in zip(result['labels'], result['scores']) } return { "tone": tone, "confidence": round(top_score * 100, 2), "scores": scores_dict } def generate_rating_from_sentiment(self, category: str, confidence: float, sentiment: str) -> int: """ 카테고리별 감정과 확신도를 기반으로 별점 생성 Args: category: 카테고리명 confidence: 확신도 (0-100) sentiment: 감정 (긍정/중립/부정) Returns: 별점 (1-5) """ # 기본 점수: 감정에 따라 if sentiment == "긍정": base_score = 4.5 elif sentiment == "중립": base_score = 3.0 else: # 부정 base_score = 1.5 # 확신도에 따라 점수 조정 confidence_factor = confidence / 100.0 final_score = base_score * confidence_factor + 2.5 * (1 - confidence_factor) # 1-5 사이로 클램핑 final_score = max(1, min(5, final_score)) return round(final_score) def extract_evidence_from_text(self, text: str, category: str, sentiment: str = None) -> str: """ 텍스트에서 특정 카테고리 관련 근거 문장 추출 카테고리 키워드가 포함된 조각만 추출하며, 감정과 일치하는 근거 우선 Args: text: 리뷰 텍스트 category: 카테고리명 sentiment: 해당 카테고리의 감정 ("긍정"/"부정"/"중립", None이면 무시) Returns: 근거 문장 (따옴표로 감싸진 형태) """ import re # 카테고리별 키워드 매핑 keywords = { "배송": ["배송", "택배", "도착", "포장"], "품질/디자인": ["품질", "재질", "튼튼", "내구", "완성도", "털빠짐", "빠짐", "디자인", "색상", "스타일", "외관"], "사이즈": ["사이즈", "크기", "핏", "치수"], "교환/환불": ["교환", "환불", "반품"], "서비스": ["서비스", "고객센터", "응대", "친절"], "가격": ["가격", "가성비", "비싸", "저렴", "할인", "돈"], "기능/성능": ["기능", "성능", "작동", "효과", "사용"] } if category not in keywords: return "-" category_keywords = keywords[category] # 감정 키워드 positive_keywords = ["좋", "훌륭", "만족", "최고", "예쁘", "이쁘", "딱맞", "빠르", "괜찮", "완벽", "멋지", "감사"] negative_keywords = ["별로", "아쉽", "실망", "최악", "짜증", "문제", "나쁘", "형편없", "엉망", "후회", "다르", "안", "못", "복잡"] # 전체 텍스트를 조각으로 나누기 chunks = re.split(r'[,]|\s+그리고\s+|\s+근데\s+|\s+하지만\s+|\s+인데\s+', text) matching_chunks = [] for chunk in chunks: chunk = chunk.strip() # 이 조각에 카테고리 키워드가 있는지 확인 has_category = False for keyword in category_keywords: if keyword in chunk and len(chunk) > 5: has_category = True break if not has_category: continue # sentiment가 지정된 경우, 감정과 일치하는 조각 찾기 if sentiment: chunk_lower = chunk.lower() has_positive = any(kw in chunk_lower for kw in positive_keywords) has_negative = any(kw in chunk_lower for kw in negative_keywords) # 감정과 일치하는지 확인 if sentiment == "긍정" and has_positive and not has_negative: matching_chunks.append((chunk, True)) # 감정 일치 elif sentiment == "부정" and has_negative: matching_chunks.append((chunk, True)) # 감정 일치 else: matching_chunks.append((chunk, False)) # 감정 불일치 else: matching_chunks.append((chunk, True)) # 감정이 일치하는 조각 우선, 없으면 첫 번째 조각 for chunk, is_match in matching_chunks: if is_match: if len(chunk) > 20: chunk = chunk[:20] return f'"{chunk}"' # 감정 일치 조각이 없으면 첫 번째 조각 반환 if matching_chunks: chunk = matching_chunks[0][0] if len(chunk) > 20: chunk = chunk[:20] return f'"{chunk}"' return "-" def analyze_sentiment_for_category(self, text: str, category: str) -> str: """ 특정 카테고리에 대한 감정 분석 카테고리 키워드 근처의 감정 표현만 분석합니다. Args: text: 리뷰 텍스트 category: 카테고리명 Returns: 감정 (긍정/중립/부정) """ import re # 카테고리 관련 키워드 keywords = { "배송": ["배송", "택배", "도착", "포장"], "품질/디자인": ["품질", "재질", "튼튼", "내구", "완성도", "털빠짐", "빠짐", "디자인", "색상", "스타일", "외관"], "사이즈": ["사이즈", "크기", "핏", "치수"], "교환/환불": ["교환", "환불", "반품"], "서비스": ["서비스", "고객센터", "응대", "친절"], "가격": ["가격", "가성비", "비싸", "저렴", "할인", "돈"], "기능/성능": ["기능", "성능", "작동", "효과", "사용"] } # 부정 키워드 (부정 키워드를 먼저 체크해야 정확함) negative_keywords = ["별로", "아쉽", "실망", "최악", "짜증", "문제", "나쁘", "형편없", "엉망", "후회", "다르", "안", "못"] # 긍정 키워드 (명시적 긍정 표현) positive_keywords = ["좋", "훌륭", "만족", "최고", "예쁘", "이쁘", "딱맞", "빠르", "괜찮", "완벽", "멋지", "감사"] if category not in keywords: return "중립" # 카테고리 키워드가 포함된 구간 찾기 category_keywords = keywords[category] # 전체 텍스트를 조각으로 나누기 (쉼표, 그리고, 하지만 등으로 분리) # 예: "배송은 빠른데 품질이 별로예요" -> ["배송은 빠른데", "품질이 별로예요"] chunks = re.split(r'[,]|\s+그리고\s+|\s+근데\s+|\s+하지만\s+|\s+인데\s+', text) for chunk in chunks: # 이 조각에 카테고리 키워드가 있는지 확인 has_category = False for keyword in category_keywords: if keyword in chunk: has_category = True break if not has_category: continue # 이 조각 내에서만 감정 판단 chunk_lower = chunk.lower() # 부정 키워드를 먼저 체크 (우선순위가 높음) for neg_keyword in negative_keywords: if neg_keyword in chunk_lower: return "부정" # 긍정 키워드 체크 for pos_keyword in positive_keywords: if pos_keyword in chunk_lower: return "긍정" # 기본값은 중립 return "중립" def extract_tone_evidence(self, text: str) -> Dict[str, str]: """ 전체 톤의 긍정/부정 근거 추출 Args: text: 리뷰 텍스트 Returns: {"positive": "긍정 근거", "negative": "부정 근거"} """ import re # 긍정 키워드 positive_keywords = ["좋", "훌륭", "만족", "최고", "예쁘", "이쁘", "딱맞", "빠르", "괜찮", "완벽", "멋지", "감사"] # 부정 키워드 negative_keywords = ["별로", "아쉽", "실망", "최악", "짜증", "문제", "나쁘", "형편없", "엉망", "후회", "다르", "복잡", "불편","느리", "느림", "늦", "지연"] # 텍스트를 조각으로 나누기 chunks = re.split(r'[,.]|\s+그리고\s+|\s+근데\s+|\s+하지만\s+|\s+인데\s+', text) positive_evidence = [] negative_evidence = [] for chunk in chunks: chunk = chunk.strip() if len(chunk) < 3: continue chunk_lower = chunk.lower() # 긍정 키워드 체크 - chunk 그대로 사용 for keyword in positive_keywords: if keyword in chunk_lower: # chunk를 그대로 사용 (이미 조각으로 분리되어 있으므로) evidence = chunk if len(evidence) > 20: evidence = evidence[:20] positive_evidence.append(f'"{evidence}"') break # 부정 키워드 체크 - chunk 그대로 사용 for keyword in negative_keywords: if keyword in chunk_lower: # chunk를 그대로 사용 (이미 조각으로 분리되어 있으므로) evidence = chunk if len(evidence) > 20: evidence = evidence[:20] negative_evidence.append(f'"{evidence}"') break # 최대 2개씩만 표시 positive_text = ", ".join(positive_evidence[:2]) if positive_evidence else "-" negative_text = ", ".join(negative_evidence[:2]) if negative_evidence else "-" return { "positive": positive_text, "negative": negative_text } def generate_comprehensive_analysis(self, review_text: str, analysis_result: Dict) -> Dict: """ 종합 분석 생성 - 항목별 평가 및 요약 Args: review_text: 원본 리뷰 텍스트 analysis_result: 3단계 분석 결과 Returns: 종합 분석 결과 """ sentiment = analysis_result['sentiment']['sentiment'] sentiment_scores = analysis_result['sentiment']['scores'] tone = analysis_result['tone']['tone'] # 모든 가능한 카테고리를 검사 (AI 결과와 무관하게) all_possible_categories = ["배송", "품질/디자인", "사이즈", "교환/환불", "서비스", "가격", "기능/성능"] # 항목별 평가 item_ratings = [] for category in all_possible_categories: # 해당 카테고리의 감정 분석 (먼저 감정을 파악) category_sentiment = self.analyze_sentiment_for_category(review_text, category) # 근거 추출 (감정과 일치하는 근거 우선) evidence = self.extract_evidence_from_text(review_text, category, category_sentiment) # 근거가 없으면 해당 항목 제외 if evidence == "-": continue # 별점 계산 (카테고리별 감정 기반) if category_sentiment == "부정": rating = 2 elif category_sentiment == "긍정": rating = 5 else: rating = 3 item_ratings.append({ "category": category, "rating": rating, "evidence": evidence, "sentiment": category_sentiment }) # 전체 톤 비율 positive_ratio = sentiment_scores.get('긍정', 0) negative_ratio = sentiment_scores.get('부정', 0) neutral_ratio = sentiment_scores.get('중립', 0) # 전체 톤 근거 추출 tone_evidence = self.extract_tone_evidence(review_text) # 요약 문장 생성 summary = self.generate_summary_sentence(review_text, item_ratings, sentiment) return { "item_ratings": item_ratings, "tone_ratio": { "positive": round(positive_ratio), "negative": round(negative_ratio), "neutral": round(neutral_ratio) }, "tone_evidence": tone_evidence, "summary": summary, "overall_sentiment": sentiment } def generate_summary_sentence(self, review_text: str, item_ratings: List[Dict], sentiment: str) -> str: """ 요약 문장 자동 생성 Args: review_text: 원본 리뷰 item_ratings: 항목별 평가 sentiment: 전체 감정 Returns: 요약 문장 """ # 높은 평가 항목과 낮은 평가 항목 찾기 high_rated = [item for item in item_ratings if item['rating'] >= 4] low_rated = [item for item in item_ratings if item['rating'] <= 2] if high_rated and low_rated: # 장단점이 모두 있는 경우 high_cats = ", ".join([item['category'] for item in high_rated[:2]]) low_cats = ", ".join([item['category'] for item in low_rated[:2]]) return f"{high_cats}은(는) 좋지만, {low_cats} 부분이 아쉬운 제품이에요." elif high_rated: # 긍정적인 경우 high_cats = ", ".join([item['category'] for item in high_rated[:3]]) return f"{high_cats} 모두 만족스러운 제품이에요." elif low_rated: # 부정적인 경우 low_cats = ", ".join([item['category'] for item in low_rated[:3]]) return f"{low_cats} 부분이 기대에 못 미치는 제품이에요." else: # 중립적인 경우 if sentiment == "긍정": return "전반적으로 만족스러운 제품이에요." elif sentiment == "부정": return "전반적으로 아쉬움이 남는 제품이에요." else: return "무난한 수준의 제품이에요." def analyze_review(self, review_text: str, include_comprehensive: bool = True) -> Dict: """ 단일 리뷰를 3단계로 분석합니다. Args: review_text: 분석할 리뷰 텍스트 include_comprehensive: 종합 분석 포함 여부 Returns: 3단계 분석 결과를 포함한 딕셔너리 """ # 텍스트 전처리 processed_text = self.preprocess_text(review_text) # 1단계: 감정 분석 sentiment_result = self.analyze_sentiment(processed_text) # 2단계: 카테고리 분석 category_result = self.analyze_category(processed_text) # 3단계: 톤 분석 tone_result = self.analyze_tone(processed_text) result = { "review": review_text, "sentiment": sentiment_result, "categories": category_result, "tone": tone_result, "timestamp": datetime.now().isoformat() } # 종합 분석 추가 if include_comprehensive: result["comprehensive"] = self.generate_comprehensive_analysis(review_text, result) return result def analyze_reviews(self, reviews: List[str]) -> List[Dict]: """ 여러 리뷰를 일괄 분석합니다. Args: reviews: 분석할 리뷰 텍스트 리스트 Returns: 분류 결과 리스트 """ results = [] for idx, review in enumerate(reviews, 1): print(f"\n[{idx}/{len(reviews)}] 분석 중...") result = self.analyze_review(review) results.append(result) return results def print_results(self, results: List[Dict]): """분석 결과를 보기 좋게 출력합니다.""" print("\n" + "="*80) print("리뷰 3단계 분석 결과") print("="*80) for idx, result in enumerate(results, 1): print(f"\n[리뷰 #{idx}]") print(f"내용: {result['review']}") print(f"\n1️⃣ 감정: {result['sentiment']['sentiment']} ({result['sentiment']['confidence']}%)") # 카테고리 출력 categories_str = ', '.join([f"{c['category']} ({c['confidence']}%)" for c in result['categories']['main_categories']]) print(f"2️⃣ 카테고리: {categories_str}") print(f"3️⃣ 톤: {result['tone']['tone']} ({result['tone']['confidence']}%)") print("\n" + "="*80) def save_results(self, results: List[Dict], filename: str = "review_results.json"): """분석 결과를 JSON 파일로 저장합니다.""" with open(filename, 'w', encoding='utf-8') as f: json.dump(results, f, ensure_ascii=False, indent=2) print(f"\n결과가 {filename}에 저장되었습니다.") def load_reviews_from_csv(self, csv_file: str) -> List[str]: """ CSV 파일에서 리뷰를 로드합니다. Args: csv_file: CSV 파일 경로 Returns: 리뷰 텍스트 리스트 """ reviews = [] with open(csv_file, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: reviews.append(row['review_text']) return reviews def analyze_for_gradio(self, review_text: str): """ Gradio UI용 리뷰 분석 함수 Args: review_text: 분석할 리뷰 텍스트 Returns: (감정 결과, 카테고리 결과, 톤 결과, 감정 분포, 카테고리 분포, 톤 분포) 튜플 """ if not review_text or review_text.strip() == "": return "⚠️ 리뷰를 입력해주세요", "", "", {}, {}, {} result = self.analyze_review(review_text, include_comprehensive=False) # 1단계: 감정 분석 결과 sentiment = result['sentiment']['sentiment'] sentiment_conf = result['sentiment']['confidence'] sentiment_output = f"{sentiment} ({sentiment_conf}%)" # 2단계: 카테고리 분석 결과 categories = result['categories']['main_categories'] if categories: category_list = [f"• {c['category']}" for c in categories] category_output = "\n".join(category_list) else: category_output = "해당 카테고리 없음" # 3단계: 톤 분석 결과 tone = result['tone']['tone'] tone_conf = result['tone']['confidence'] tone_output = f"{tone} ({tone_conf}%)" # 확률 분포 딕셔너리들 (Gradio Label 컴포넌트용) sentiment_probs = { k: v / 100.0 for k, v in result['sentiment']['scores'].items() } category_probs = { k: v / 100.0 for k, v in result['categories']['all_scores'].items() } tone_probs = { k: v / 100.0 for k, v in result['tone']['scores'].items() } return sentiment_output, category_output, tone_output, sentiment_probs, category_probs, tone_probs def format_comprehensive_analysis(self, comprehensive: Dict) -> str: """ 종합 분석 결과를 마크다운 형식으로 포맷팅 Args: comprehensive: 종합 분석 딕셔너리 Returns: 마크다운 형식의 문자열 """ output = "| 항목 | 감정 | 만족도 | 근거 |\n" output += "|------|------|--------|------|\n" # 항목별 평가 for item in comprehensive['item_ratings']: stars = "⭐️" * item['rating'] sentiment = item.get('sentiment', '중립') output += f"| {item['category']} | {sentiment} | {stars} | {item['evidence']} |\n" # 전체 톤 tone_ratio = comprehensive['tone_ratio'] tone_evidence = comprehensive.get('tone_evidence', {"positive": "-", "negative": "-"}) tone_summary = "" if tone_ratio['positive'] > tone_ratio['negative'] + 20: tone_summary = "긍정이 우세함" elif tone_ratio['negative'] > tone_ratio['positive'] + 20: tone_summary = "부정이 우세함" else: tone_summary = "긍정과 부정이 혼재됨" # 전체 톤 근거 포맷팅: "긍정: xxx / 부정: xxx" tone_evidence_text = f"긍정: {tone_evidence['positive']} / 부정: {tone_evidence['negative']}" output += f"| 전체 톤 | {tone_summary} | 긍정 {tone_ratio['positive']} : 부정 {tone_ratio['negative']} | {tone_evidence_text} |\n" return output # 전역 분석기 인스턴스 (Gradio 앱 시작 시 한 번만 로드) analyzer = None def get_analyzer(): """분석기 싱글톤 인스턴스 반환""" global analyzer if analyzer is None: analyzer = ReviewAnalyzer() return analyzer def create_gradio_app(): """Gradio 웹 애플리케이션 생성""" # 분석기 초기화 review_analyzer = get_analyzer() # 샘플 리뷰 예시 examples = [ ["정말 좋은 제품이에요! 배송도 빠르고 품질도 훌륭합니다. 다음에도 또 구매할게요!"], ["완전 실망이에요. 사진이랑 완전 다르고 품질도 별로입니다. 환불 신청했습니다. 다만 환불 처리는 빨라서 좋았어요."], ["핏도 넘이쁘고 사이즈도 딱맞고 다좋은데 털빠짐이 장난이 아니예요~~감수할만한데 은근 짜증날수도? 그냥 입으면 고양이마냥 털을 뿜내요 ㅎㅎ"], ["텔레그램 @abcd1234로 연락주시면 반값에 드립니다. 도매가로 판매중!"], ["배송이 생각보다 빨라서 좋았어요. 품질도 괜찮고 가격대비 만족합니다."], ["사이즈가 너무 작아요. 교환하려고 했는데 절차가 복잡하네요."], ["디자인은 예쁜데 품질이 가격에 비해 별로입니다. 그냥저냥이에요."], ["세트 가격 가성비 최고예용❤️🤍 따뜻하고 폭닥폭닥한 느낌 너무 조아여!! 핏 너무 예뻐용!!!"] ] # Gradio 인터페이스 생성 - 모던 대시보드 레이아웃 with gr.Blocks( title="리뷰 3단계 분석 서비스", theme=gr.themes.Default( primary_hue="blue", secondary_hue="slate", neutral_hue="slate", font=gr.themes.GoogleFont("Noto Sans KR") ), css=""" .card-header { font-size: 1.2em; font-weight: bold; margin-bottom: 10px; padding: 10px; border-radius: 8px; text-align: center; } .sentiment-positive { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .sentiment-neutral { background: #6b7280; color: white; } .sentiment-negative { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); color: white; } .metric-card { border: 2px solid #e5e7eb; border-radius: 12px; padding: 10px; background: white; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .big-emoji { font-size: 3em; text-align: center; margin: 10px 0; } .big-text { font-size: 1.8em; font-weight: bold; text-align: center; margin: 5px 0; } .confidence { font-size: 1.2em; color: #6b7280; text-align: center; } /* Label 컴포넌트 패딩 조정 - 각 확률 항목들의 패딩 줄이기 */ .label .output-class { padding: 6px 12px !important; } .label-wrap .output-class { padding: 6px 12px !important; } .compact-label .output-class { padding: 6px 12px !important; } """ ) as demo: # 헤더 gr.Markdown(""" # 🔍 리뷰 분석 대시보드 AI 기반 3단계 분석으로 리뷰를 자동으로 검수하고 인사이트를 추출합니다. """) # 2단 레이아웃: 리뷰입력 / 분석결과 with gr.Row(): # 왼쪽: 입력 섹션 with gr.Column(scale=1): gr.Markdown("## 리뷰 입력") review_input = gr.Textbox( label="TextBox", placeholder="분석할 리뷰 내용을 입력해주세요...", lines=10, max_lines=20 ) submit_btn = gr.Button("🔍 분석 시작", variant="primary", size="lg") gr.Examples( examples=examples, inputs=review_input, label="💡 예시 리뷰" ) # 오른쪽: 3단계 분석 with gr.Column(scale=1): gr.Markdown("## 분석 결과") # 1단계: 감정 분석 gr.HTML('
1. 감정 분석
') with gr.Group(elem_classes="metric-card"): sentiment_output = gr.Textbox( label="", lines=1, interactive=False, show_label=False, container=False, elem_classes="big-text", visible=False ) sentiment_prob = gr.Label( label="확률 분포", num_top_classes=3, show_label=False, elem_classes="compact-label" ) # 2단계: 카테고리 분석 gr.HTML('
2. 카테고리 분석
') with gr.Group(elem_classes="metric-card"): category_output = gr.Textbox( label="", lines=4, interactive=False, show_label=False, container=False, visible=False ) category_prob = gr.Label( label="확률 분포", num_top_classes=5, show_label=False, elem_classes="compact-label" ) # 3단계: 톤 탐지 gr.HTML('
3. 리뷰 톤 탐지
') with gr.Group(elem_classes="metric-card"): tone_output = gr.Textbox( label="", lines=1, interactive=False, show_label=False, container=False, elem_classes="big-text", visible=False ) tone_prob = gr.Label( label="확률 분포", num_top_classes=3, show_label=False, elem_classes="compact-label" ) # 이벤트 핸들러 submit_btn.click( fn=review_analyzer.analyze_for_gradio, inputs=review_input, outputs=[sentiment_output, category_output, tone_output, sentiment_prob, category_prob, tone_prob] ) review_input.submit( fn=review_analyzer.analyze_for_gradio, inputs=review_input, outputs=[sentiment_output, category_output, tone_output, sentiment_prob, category_prob, tone_prob] ) return demo def main(): """메인 실행 함수""" print("리뷰 자동 검수 서비스 시작") print("-" * 80) # Gradio 앱 실행 app = create_gradio_app() app.launch( server_name="0.0.0.0", server_port=7860, share=False, inbrowser=True ) if __name__ == "__main__": main()