콘텐츠 대표 이미지 - Python으로 AI 글쓰기 품질 평가 시스템 구현
🤖 AI / 머신러닝 개발

Python으로 AI 글쓰기 품질 평가 시스템 구현

✍️ 내 글이 진짜 잘 쓴 건지 AI한테 물어보자ㅋㅋ — 실전 코드와 함께하는 완전 정복 가이드

야 솔직히 말해봐ㅋㅋ 글 쓰고 나서 "이거 괜찮은 건가...?" 하고 혼자 멍하니 화면 바라본 적 있지 않아? 🤔
맞춤법 검사기는 있는데, "이 글이 얼마나 잘 쓰인 글인가"를 객관적으로 평가해주는 시스템은 생각보다 없거든.
그래서 오늘은 Python이랑 AI 기술을 활용해서 글쓰기 품질 평가 시스템을 직접 만들어보는 거 해볼 거야!

이거 단순히 맞춤법 체크 수준이 아니야. 가독성, 문장 구조, 감성 분석, 키워드 밀도, 논리 흐름까지 — 진짜 종합 평가 시스템이야.
재능넷 같은 플랫폼에서 글쓰기 재능을 판매하거나 콘텐츠 제작자로 활동하는 분들한테 특히 유용할 거야 😎

💡 이 글에서 배울 것들
텍스트 전처리 → 가독성 지수 계산 → 감성 분석 → 키워드 분석 → GPT API 연동 → 종합 점수 산출까지!
Python 중급 이상이면 충분히 따라올 수 있어.
AI 글쓰기 품질 평가 시스템 — 전체 흐름 📝 원문 텍스트 입력 단계 🔧 전처리 토큰화·정제 📊 지표 분석 가독성·감성·키워드 🤖 AI 평가 GPT API 연동 🏆 종합 점수 리포트 출력 📐 분석 지표 목록 • 가독성 지수 (Flesch-Kincaid 한국어 변형) • 문장 평균 길이 / 단어 다양성 (TTR) • 감성 분석 (긍정/부정/중립 비율) • 키워드 밀도 및 TF-IDF 점수 • 논리 흐름 일관성 (AI 기반) • 문법 오류 빈도 / 반복 표현 탐지 🛠️ 사용 기술 스택 • Python 3.10+ • KoNLPy (한국어 형태소 분석) • scikit-learn (TF-IDF) • transformers (감성 분석 모델) • OpenAI API (GPT 평가) • FastAPI (서비스 배포)

🛠️ 1. 개발 환경 세팅 — 일단 준비부터 빡세게

뭐든 환경 세팅이 제일 귀찮은 거 알지ㅋㅋ 근데 이거 제대로 안 하면 나중에 더 고생해. 차근차근 따라와봐.

📦 필수 라이브러리 설치

먼저 가상환경 만들고 필요한 패키지들 설치해줘야 해. 아래 명령어 그대로 복붙해도 돼.


# 가상환경 생성 및 활성화
python -m venv writing_eval_env
source writing_eval_env/bin/activate  # Windows: writing_eval_env\Scripts\activate

# 핵심 라이브러리 설치
pip install konlpy
pip install scikit-learn
pip install transformers torch
pip install openai
pip install fastapi uvicorn
pip install pandas numpy
pip install matplotlib seaborn
pip install python-dotenv
pip install textstat
pip install kiwipiepy  # KoNLPy 대안, 더 빠름
⚠️ KoNLPy 설치 주의사항
KoNLPy는 Java 의존성이 있어서 JDK 1.7 이상이 설치되어 있어야 해.
java -version 으로 확인해봐. 없으면 OpenJDK 설치 먼저!
Windows 환경에서는 kiwipiepy가 훨씬 설치 편하니까 대안으로 써도 돼.
🔑 환경변수 설정 (.env 파일)

OpenAI API 키는 절대 코드에 하드코딩하면 안 돼! 진짜로ㅋㅋ 깃허브에 올렸다가 키 털리는 사람 엄청 많아.


# .env 파일 생성
OPENAI_API_KEY=sk-your-api-key-here
MODEL_NAME=gpt-4o-mini
MAX_TOKENS=2000
TEMPERATURE=0.3

# config.py
from dotenv import load_dotenv
import os

load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
MAX_TOKENS = int(os.getenv("MAX_TOKENS", 2000))
TEMPERATURE = float(os.getenv("TEMPERATURE", 0.3))

🔧 2. 텍스트 전처리 모듈 — 데이터 정제의 기술

AI한테 날 것의 텍스트 그대로 던져주면 결과가 엉망이 돼. 전처리가 품질 평가의 80%를 좌우한다고 해도 과언이 아님ㅋㅋ

🧹 기본 전처리 클래스 구현

# text_preprocessor.py
import re
import unicodedata
from kiwipiepy import Kiwi
from typing import List, Dict, Tuple

class TextPreprocessor:
    """한국어 텍스트 전처리 클래스"""
    
    def __init__(self):
        self.kiwi = Kiwi()
        
    def clean_text(self, text: str) -> str:
        """기본 텍스트 정제"""
        # 유니코드 정규화
        text = unicodedata.normalize('NFC', text)
        
        # HTML 태그 제거
        text = re.sub(r'<[^>]+>', '', text)
        
        # URL 제거
        text = re.sub(r'http[s]?://\S+', '[URL]', text)
        
        # 이메일 제거
        text = re.sub(r'\S+@\S+\.\S+', '[EMAIL]', text)
        
        # 과도한 공백 정리
        text = re.sub(r'\s+', ' ', text).strip()
        
        return text
    
    def split_sentences(self, text: str) -> List[str]:
        """문장 분리"""
        result = self.kiwi.split_into_sents(text)
        sentences = [sent.text.strip() for sent in result 
                    if len(sent.text.strip()) > 0]
        return sentences
    
    def tokenize(self, text: str) -> List[Dict]:
        """형태소 분석 및 토큰화"""
        result = self.kiwi.analyze(text)
        tokens = []
        
        if result:
            for token in result[0].tokens:
                tokens.append({
                    'form': token.form,
                    'tag': token.tag,
                    'start': token.start,
                    'len': token.len
                })
        return tokens
    
    def extract_morphemes(self, text: str) -> Dict[str, List[str]]:
        """품사별 형태소 추출"""
        tokens = self.tokenize(text)
        morphemes = {
            'nouns': [],      # 명사
            'verbs': [],      # 동사
            'adjectives': [], # 형용사
            'adverbs': [],    # 부사
            'all': []         # 전체
        }
        
        pos_map = {
            'NNG': 'nouns', 'NNP': 'nouns', 'NNB': 'nouns',
            'VV': 'verbs', 'VA': 'adjectives', 'MAG': 'adverbs'
        }
        
        for token in tokens:
            morphemes['all'].append(token['form'])
            category = pos_map.get(token['tag'])
            if category:
                morphemes[category].append(token['form'])
        
        return morphemes
    
    def get_basic_stats(self, text: str) -> Dict:
        """기본 통계 추출"""
        sentences = self.split_sentences(text)
        morphemes = self.extract_morphemes(text)
        
        # 문장 길이 계산
        sent_lengths = [len(s) for s in sentences]
        
        return {
            'total_chars': len(text),
            'total_sentences': len(sentences),
            'total_words': len(morphemes['all']),
            'avg_sentence_length': sum(sent_lengths) / len(sent_lengths) if sent_lengths else 0,
            'max_sentence_length': max(sent_lengths) if sent_lengths else 0,
            'min_sentence_length': min(sent_lengths) if sent_lengths else 0,
            'noun_count': len(morphemes['nouns']),
            'verb_count': len(morphemes['verbs']),
            'adj_count': len(morphemes['adjectives']),
        }
💡 kiwipiepy vs KoNLPy 선택 가이드
kiwipiepy: 설치 쉬움, 속도 빠름, 최신 한국어 처리 우수, 추천!
KoNLPy (Okt, Komoran): 레퍼런스 많음, Java 필요, 대용량 처리 시 안정적
개인 프로젝트라면 kiwipiepy, 팀 프로젝트라면 KoNLPy 고려해봐.

📊 3. 가독성 분석 모듈 — 얼마나 읽기 쉬운 글인가

가독성은 글쓰기 품질에서 진짜 핵심이야. 아무리 내용이 좋아도 읽기 어려우면 독자가 이탈하거든ㅋㅋ
영어권에서는 Flesch-Kincaid 지수가 유명한데, 한국어는 음절 구조가 달라서 변형이 필요해.

📏 한국어 가독성 지수 계산기

# readability_analyzer.py
import math
from typing import Dict, List
from text_preprocessor import TextPreprocessor

class ReadabilityAnalyzer:
    """한국어 가독성 분석기"""
    
    def __init__(self):
        self.preprocessor = TextPreprocessor()
    
    def count_syllables_korean(self, text: str) -> int:
        """한국어 음절 수 계산"""
        # 한글 음절 범위: 0xAC00 ~ 0xD7A3
        syllable_count = sum(1 for char in text 
                           if '\uAC00' <= char <= '\uD7A3')
        return syllable_count
    
    def calculate_ari(self, text: str) -> float:
        """
        한국어 변형 ARI (Automated Readability Index)
        공식: 4.71 * (음절수/단어수) + 0.5 * (단어수/문장수) - 21.43
        """
        stats = self.preprocessor.get_basic_stats(text)
        syllables = self.count_syllables_korean(text)
        
        if stats['total_words'] == 0 or stats['total_sentences'] == 0:
            return 0
        
        chars_per_word = syllables / stats['total_words']
        words_per_sent = stats['total_words'] / stats['total_sentences']
        
        ari = 4.71 * chars_per_word + 0.5 * words_per_sent - 21.43
        return round(ari, 2)
    
    def calculate_ttr(self, text: str) -> float:
        """
        Type-Token Ratio (어휘 다양성 지수)
        TTR = 고유 단어 수 / 전체 단어 수
        1에 가까울수록 어휘가 다양함
        """
        morphemes = self.preprocessor.extract_morphemes(text)
        all_words = morphemes['all']
        
        if not all_words:
            return 0
        
        unique_words = set(all_words)
        ttr = len(unique_words) / len(all_words)
        return round(ttr, 4)
    
    def calculate_mtld(self, text: str, threshold: float = 0.72) -> float:
        """
        MTLD (Measure of Textual Lexical Diversity)
        TTR보다 텍스트 길이에 덜 민감한 어휘 다양성 지표
        """
        morphemes = self.preprocessor.extract_morphemes(text)
        words = morphemes['all']
        
        if len(words) < 10:
            return 0
        
        def _mtld_forward(word_list):
            factors = 0
            start = 0
            
            for end in range(1, len(word_list) + 1):
                segment = word_list[start:end]
                unique = len(set(segment))
                ttr = unique / len(segment)
                
                if ttr <= threshold:
                    factors += 1
                    start = end
            
            # 마지막 불완전 세그먼트 처리
            if start < len(word_list):
                segment = word_list[start:]
                unique = len(set(segment))
                ttr = unique / len(segment)
                factors += (1 - ttr) / (1 - threshold)
            
            return len(word_list) / factors if factors > 0 else 0
        
        forward = _mtld_forward(words)
        backward = _mtld_forward(words[::-1])
        
        return round((forward + backward) / 2, 2)
    
    def analyze_sentence_variety(self, text: str) -> Dict:
        """문장 길이 다양성 분석"""
        sentences = self.preprocessor.split_sentences(text)
        lengths = [len(s) for s in sentences]
        
        if not lengths:
            return {}
        
        avg = sum(lengths) / len(lengths)
        variance = sum((l - avg) ** 2 for l in lengths) / len(lengths)
        std_dev = math.sqrt(variance)
        
        # 문장 길이 분포
        short = sum(1 for l in lengths if l < 20)
        medium = sum(1 for l in lengths if 20 <= l < 60)
        long = sum(1 for l in lengths if l >= 60)
        
        return {
            'avg_length': round(avg, 1),
            'std_deviation': round(std_dev, 2),
            'variety_score': round(min(std_dev / avg * 100, 100), 1) if avg > 0 else 0,
            'short_sentences': short,
            'medium_sentences': medium,
            'long_sentences': long,
            'total_sentences': len(sentences)
        }
    
    def detect_repetition(self, text: str) -> Dict:
        """반복 표현 탐지"""
        sentences = self.preprocessor.split_sentences(text)
        morphemes = self.preprocessor.extract_morphemes(text)
        
        # 단어 빈도 계산
        from collections import Counter
        word_freq = Counter(morphemes['nouns'] + morphemes['verbs'])
        
        # 과도하게 반복되는 단어 (전체의 3% 이상)
        total_words = len(morphemes['all'])
        repeated = {
            word: count for word, count in word_freq.items()
            if count / total_words > 0.03 and len(word) > 1
        }
        
        return {
            'repeated_words': repeated,
            'repetition_score': max(0, 100 - len(repeated) * 10)
        }
    
    def get_readability_report(self, text: str) -> Dict:
        """종합 가독성 리포트"""
        return {
            'ari_score': self.calculate_ari(text),
            'ttr': self.calculate_ttr(text),
            'mtld': self.calculate_mtld(text),
            'sentence_variety': self.analyze_sentence_variety(text),
            'repetition': self.detect_repetition(text)
        }
지표 설명 이상적인 범위
ARI 점수 자동 가독성 지수 (낮을수록 쉬움) 6~10 (일반 독자 기준)
TTR 어휘 다양성 (1에 가까울수록 다양) 0.4~0.7
MTLD 텍스트 어휘 다양성 (높을수록 좋음) 70 이상
문장 다양성 문장 길이의 표준편차 표준편차 10~25

💬 4. 감성 분석 모듈 — 글의 톤앤매너 파악하기

글의 감성 톤은 독자 경험에 엄청난 영향을 줘. 너무 부정적이거나 너무 중립적이면 독자가 지루해하거든.
한국어 감성 분석은 Hugging Face의 사전학습 모델을 활용하면 꽤 정확하게 할 수 있어! 🎯

🤗 Transformers 기반 감성 분석기

# sentiment_analyzer.py
from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
import torch
from typing import Dict, List
from text_preprocessor import TextPreprocessor

class SentimentAnalyzer:
    """한국어 감성 분석기"""
    
    def __init__(self, model_name: str = "snunlp/KR-FinBert-SC"):
        """
        추천 한국어 감성 분석 모델:
        - snunlp/KR-FinBert-SC (금융 도메인)
        - monologg/koelectra-base-finetuned-sentiment
        - klue/roberta-base (범용)
        """
        self.preprocessor = TextPreprocessor()
        
        try:
            # 경량 모델 우선 시도
            self.classifier = pipeline(
                "text-classification",
                model="monologg/koelectra-base-finetuned-sentiment",
                device=0 if torch.cuda.is_available() else -1,
                max_length=512,
                truncation=True
            )
            self.model_loaded = True
        except Exception as e:
            print(f"모델 로드 실패: {e}")
            print("규칙 기반 감성 분석으로 대체합니다.")
            self.model_loaded = False
            self._load_sentiment_dict()
    
    def _load_sentiment_dict(self):
        """규칙 기반 감성 사전 (모델 로드 실패 시 대체)"""
        self.positive_words = [
            '좋다', '훌륭하다', '멋지다', '아름답다', '즐겁다',
            '행복하다', '기쁘다', '뛰어나다', '완벽하다', '최고',
            '유익하다', '도움', '감사', '사랑', '희망'
        ]
        self.negative_words = [
            '나쁘다', '싫다', '힘들다', '어렵다', '실망',
            '문제', '오류', '실패', '부족', '불만',
            '걱정', '두렵다', '슬프다', '화나다', '짜증'
        ]
    
    def analyze_sentence_sentiment(self, sentence: str) -> Dict:
        """단일 문장 감성 분석"""
        if self.model_loaded:
            try:
                result = self.classifier(sentence[:512])
                label = result[0]['label']
                score = result[0]['score']
                
                # 레이블 정규화
                if 'positive' in label.lower() or label == '1':
                    sentiment = 'positive'
                elif 'negative' in label.lower() or label == '0':
                    sentiment = 'negative'
                else:
                    sentiment = 'neutral'
                
                return {'sentiment': sentiment, 'confidence': round(score, 4)}
            except:
                pass
        
        # 규칙 기반 대체
        return self._rule_based_sentiment(sentence)
    
    def _rule_based_sentiment(self, text: str) -> Dict:
        """규칙 기반 감성 분석"""
        pos_count = sum(1 for w in self.positive_words if w in text)
        neg_count = sum(1 for w in self.negative_words if w in text)
        
        if pos_count > neg_count:
            return {'sentiment': 'positive', 'confidence': 0.6}
        elif neg_count > pos_count:
            return {'sentiment': 'negative', 'confidence': 0.6}
        else:
            return {'sentiment': 'neutral', 'confidence': 0.5}
    
    def analyze_full_text(self, text: str) -> Dict:
        """전체 텍스트 감성 분석"""
        sentences = self.preprocessor.split_sentences(text)
        
        if not sentences:
            return {}
        
        results = []
        for sent in sentences:
            if len(sent.strip()) > 5:  # 너무 짧은 문장 제외
                result = self.analyze_sentence_sentiment(sent)
                results.append(result)
        
        # 감성 분포 계산
        sentiment_counts = {'positive': 0, 'negative': 0, 'neutral': 0}
        for r in results:
            sentiment_counts[r['sentiment']] += 1
        
        total = len(results)
        sentiment_ratio = {
            k: round(v / total * 100, 1) 
            for k, v in sentiment_counts.items()
        } if total > 0 else {}
        
        # 전반적 감성 결정
        dominant = max(sentiment_counts, key=sentiment_counts.get)
        
        # 감성 균형 점수 (너무 한쪽으로 치우치지 않을수록 좋음)
        balance_score = self._calculate_balance_score(sentiment_ratio)
        
        return {
            'dominant_sentiment': dominant,
            'sentiment_distribution': sentiment_ratio,
            'balance_score': balance_score,
            'analyzed_sentences': total,
            'sentiment_details': results[:5]  # 처음 5개만 샘플로
        }
    
    def _calculate_balance_score(self, ratio: Dict) -> float:
        """
        감성 균형 점수 계산
        - 정보성 글: 중립 60%+ 이상이 이상적
        - 감성적 글: 긍정 50%+ 이상이 이상적
        """
        positive = ratio.get('positive', 0)
        negative = ratio.get('negative', 0)
        neutral = ratio.get('neutral', 0)
        
        # 부정 비율이 40% 이상이면 감점
        if negative > 40:
            return max(0, 100 - (negative - 40) * 2)
        
        # 중립+긍정 비율이 높을수록 좋음
        score = (neutral * 0.5 + positive * 0.8)
        return round(min(score, 100), 1)
모델 선택 팁
GPU가 없는 환경이라면 monologg/koelectra-base-finetuned-sentiment 모델이 CPU에서도 비교적 빠르게 돌아가.
배치 처리가 필요하다면 pipelinebatch_size 파라미터를 활용해봐!

🔍 5. 키워드 분석 & TF-IDF — 핵심 단어 뽑아내기

글에서 어떤 단어가 핵심인지 파악하는 건 SEO 최적화나 주제 일관성 평가에 필수야.
TF-IDF는 단순 빈도 분석보다 훨씬 스마트하게 중요 키워드를 뽑아줘 😎

📈 TF-IDF 기반 키워드 분석기

# keyword_analyzer.py
from sklearn.feature_extraction.text import TfidfVectorizer
from collections import Counter
import numpy as np
from typing import Dict, List, Tuple
from text_preprocessor import TextPreprocessor

class KeywordAnalyzer:
    """키워드 분석 및 TF-IDF 계산기"""
    
    def __init__(self):
        self.preprocessor = TextPreprocessor()
    
    def extract_keywords_tfidf(
        self, 
        text: str, 
        top_n: int = 10,
        reference_corpus: List[str] = None
    ) -> List[Tuple[str, float]]:
        """
        TF-IDF 기반 키워드 추출
        reference_corpus: 비교 문서 목록 (없으면 단일 문서 분석)
        """
        morphemes = self.preprocessor.extract_morphemes(text)
        
        # 명사 위주로 키워드 추출 (동사, 형용사도 포함 가능)
        target_words = morphemes['nouns'] + morphemes['verbs']
        processed_text = ' '.join(target_words)
        
        if reference_corpus:
            # 참조 코퍼스가 있을 때 - 진짜 TF-IDF
            corpus = [processed_text] + [
                ' '.join(self.preprocessor.extract_morphemes(doc)['nouns'])
                for doc in reference_corpus
            ]
        else:
            # 단일 문서 - 문장 단위로 분리해서 TF-IDF 계산
            sentences = self.preprocessor.split_sentences(text)
            corpus = [
                ' '.join(self.preprocessor.extract_morphemes(s)['nouns'])
                for s in sentences
                if len(s) > 10
            ]
            if not corpus:
                corpus = [processed_text]
        
        # TF-IDF 계산
        vectorizer = TfidfVectorizer(
            max_features=100,
            min_df=1,
            ngram_range=(1, 2)  # 단어 + 2-gram
        )
        
        try:
            tfidf_matrix = vectorizer.fit_transform(corpus)
            feature_names = vectorizer.get_feature_names_out()
            
            # 첫 번째 문서(원문)의 TF-IDF 점수
            scores = tfidf_matrix[0].toarray()[0]
            
            # 점수 순으로 정렬
            keyword_scores = [
                (feature_names[i], round(float(scores[i]), 4))
                for i in range(len(feature_names))
                if scores[i] > 0
            ]
            keyword_scores.sort(key=lambda x: x[1], reverse=True)
            
            return keyword_scores[:top_n]
        
        except Exception as e:
            # 폴백: 단순 빈도 기반
            return self._frequency_based_keywords(text, top_n)
    
    def _frequency_based_keywords(
        self, text: str, top_n: int = 10
    ) -> List[Tuple[str, float]]:
        """빈도 기반 키워드 추출 (폴백)"""
        morphemes = self.preprocessor.extract_morphemes(text)
        nouns = [n for n in morphemes['nouns'] if len(n) > 1]
        
        freq = Counter(nouns)
        total = sum(freq.values())
        
        return [
            (word, round(count / total, 4))
            for word, count in freq.most_common(top_n)
        ]
    
    def calculate_keyword_density(
        self, text: str, target_keywords: List[str]
    ) -> Dict:
        """특정 키워드의 밀도 계산"""
        morphemes = self.preprocessor.extract_morphemes(text)
        all_words = morphemes['all']
        total = len(all_words)
        
        density_report = {}
        for keyword in target_keywords:
            count = all_words.count(keyword)
            density = round(count / total * 100, 2) if total > 0 else 0
            
            density_report[keyword] = {
                'count': count,
                'density': density,
                'status': self._evaluate_density(density)
            }
        
        return density_report
    
    def _evaluate_density(self, density: float) -> str:
        """키워드 밀도 평가"""
        if density < 0.5:
            return '부족 (0.5% 미만)'
        elif density <= 2.5:
            return '적정 (0.5~2.5%)'
        elif density <= 4.0:
            return '약간 과다 (2.5~4%)'
        else:
            return '과다 (4% 초과, 스팸 위험)'
    
    def analyze_topic_coherence(self, text: str) -> Dict:
        """주제 일관성 분석"""
        sentences = self.preprocessor.split_sentences(text)
        
        if len(sentences) < 3:
            return {'coherence_score': 100, 'note': '문장 수 부족'}
        
        # 각 문장의 명사 집합 추출
        sent_nouns = []
        for sent in sentences:
            morphemes = self.preprocessor.extract_morphemes(sent)
            sent_nouns.append(set(morphemes['nouns']))
        
        # 인접 문장 간 공유 단어 비율 계산
        coherence_scores = []
        for i in range(len(sent_nouns) - 1):
            if sent_nouns[i] and sent_nouns[i+1]:
                intersection = sent_nouns[i] & sent_nouns[i+1]
                union = sent_nouns[i] | sent_nouns[i+1]
                jaccard = len(intersection) / len(union) if union else 0
                coherence_scores.append(jaccard)
        
        avg_coherence = sum(coherence_scores) / len(coherence_scores) if coherence_scores else 0
        
        return {
            'coherence_score': round(avg_coherence * 100, 1),
            'avg_sentence_overlap': round(avg_coherence, 4),
            'evaluation': '높음' if avg_coherence > 0.15 else '보통' if avg_coherence > 0.05 else '낮음'
        }

📊 글쓰기 품질 점수 산출 방식 종합 품질 점수 100점 가독성 25점 감성 분석 20점 키워드 20점 AI 평가 35점 S등급: 90+ A등급: 80~89 B등급: 70~79 C등급: 60~69 D등급: 60 미만

🤖 6. GPT API 연동 — AI의 눈으로 글 평가받기

이제 진짜 핵심이야! 수치 기반 분석만으로는 잡기 어려운 논리 흐름, 설득력, 창의성 같은 요소들을
GPT한테 평가받는 거야. 프롬프트 엔지니어링이 여기서 진짜 중요해 🎯

🧠 GPT 평가 모듈 구현

# gpt_evaluator.py
from openai import OpenAI
import json
from typing import Dict, Optional
from config import OPENAI_API_KEY, MODEL_NAME, MAX_TOKENS, TEMPERATURE

class GPTEvaluator:
    """GPT 기반 글쓰기 품질 평가기"""
    
    def __init__(self):
        self.client = OpenAI(api_key=OPENAI_API_KEY)
        self.model = MODEL_NAME
    
    def _build_evaluation_prompt(
        self, 
        text: str, 
        writing_type: str = "일반",
        target_audience: str = "일반 독자"
    ) -> str:
        """평가용 시스템 프롬프트 생성"""
        
        system_prompt = """당신은 전문 글쓰기 평가 AI입니다.
주어진 텍스트를 다음 기준으로 엄격하고 객관적으로 평가하세요.

평가 기준 (각 항목 0~100점):
1. 논리적 흐름 (logical_flow): 내용의 논리적 연결성과 일관성
2. 설득력 (persuasiveness): 독자를 설득하는 능력
3. 명확성 (clarity): 표현의 명확함과 이해 용이성
4. 창의성 (creativity): 독창적인 표현과 관점
5. 구조 완성도 (structure): 서론-본론-결론의 구조적 완성도
6. 전문성 (expertise): 주제에 대한 깊이와 전문성

반드시 아래 JSON 형식으로만 응답하세요:
{
  "scores": {
    "logical_flow": <점수>,
    "persuasiveness": <점수>,
    "clarity": <점수>,
    "creativity": <점수>,
    "structure": <점수>,
    "expertise": <점수>
  },
  "strengths": ["강점1", "강점2", "강점3"],
  "improvements": ["개선점1", "개선점2", "개선점3"],
  "overall_comment": "전반적인 평가 코멘트 (2~3문장)",
  "grade": "S/A/B/C/D 중 하나"
}"""
        
        user_prompt = f"""글쓰기 유형: {writing_type}
대상 독자: {target_audience}

평가할 텍스트:
---
{text[:3000]}  # 토큰 제한으로 3000자 제한
---

위 텍스트를 평가해주세요."""
        
        return system_prompt, user_prompt
    
    def evaluate(
        self, 
        text: str,
        writing_type: str = "일반",
        target_audience: str = "일반 독자"
    ) -> Dict:
        """GPT를 이용한 글쓰기 평가"""
        
        system_prompt, user_prompt = self._build_evaluation_prompt(
            text, writing_type, target_audience
        )
        
        try:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=[
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": user_prompt}
                ],
                max_tokens=MAX_TOKENS,
                temperature=TEMPERATURE,
                response_format={"type": "json_object"}  # JSON 모드 강제
            )
            
            result_text = response.choices[0].message.content
            result = json.loads(result_text)
            
            # 점수 유효성 검증
            result = self._validate_scores(result)
            
            # 토큰 사용량 기록
            result['token_usage'] = {
                'prompt_tokens': response.usage.prompt_tokens,
                'completion_tokens': response.usage.completion_tokens,
                'total_tokens': response.usage.total_tokens
            }
            
            return result
        
        except json.JSONDecodeError as e:
            return self._fallback_evaluation(text)
        except Exception as e:
            print(f"GPT 평가 오류: {e}")
            return self._fallback_evaluation(text)
    
    def _validate_scores(self, result: Dict) -> Dict:
        """점수 유효성 검증 및 정규화"""
        if 'scores' in result:
            for key in result['scores']:
                score = result['scores'][key]
                result['scores'][key] = max(0, min(100, int(score)))
        return result
    
    def _fallback_evaluation(self, text: str) -> Dict:
        """API 실패 시 기본 평가 반환"""
        return {
            'scores': {
                'logical_flow': 50,
                'persuasiveness': 50,
                'clarity': 50,
                'creativity': 50,
                'structure': 50,
                'expertise': 50
            },
            'strengths': ['평가 불가'],
            'improvements': ['API 연결 확인 필요'],
            'overall_comment': 'GPT 평가를 완료하지 못했습니다.',
            'grade': 'N/A'
        }
    
    def get_improvement_suggestions(
        self, text: str, weak_areas: List[str]
    ) -> str:
        """약점 영역에 대한 구체적 개선 제안"""
        
        areas_str = ', '.join(weak_areas)
        
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[{
                "role": "user",
                "content": f"""다음 텍스트의 [{areas_str}] 부분을 개선하기 위한
구체적이고 실행 가능한 제안을 3가지 제시해주세요.
각 제안은 실제 예시를 포함해야 합니다.

텍스트:
{text[:2000]}"""
            }],
            max_tokens=800,
            temperature=0.5
        )
        
        return response.choices[0].message.content
⚠️ GPT API 비용 관리 팁
gpt-4o-mini 기준 입력 $0.15/1M tokens, 출력 $0.60/1M tokens야.
텍스트를 3000자로 제한하고, 캐싱을 적극 활용해봐.
동일한 텍스트 반복 평가 방지를 위해 Redis나 SQLite로 결과 캐싱 추천!

🏆 7. 종합 평가 엔진 — 모든 걸 하나로 합치기

자, 이제 앞에서 만든 모듈들을 하나로 합쳐서 진짜 평가 시스템을 완성할 차례야!
각 지표에 가중치를 부여해서 최종 점수를 산출하는 거야 💪

⚙️ 메인 평가 엔진

# writing_quality_engine.py
from typing import Dict, List, Optional
from text_preprocessor import TextPreprocessor
from readability_analyzer import ReadabilityAnalyzer
from sentiment_analyzer import SentimentAnalyzer
from keyword_analyzer import KeywordAnalyzer
from gpt_evaluator import GPTEvaluator
import json
from datetime import datetime

class WritingQualityEngine:
    """글쓰기 품질 종합 평가 엔진"""
    
    # 가중치 설정 (합계 = 100)
    WEIGHTS = {
        'readability': 25,    # 가독성
        'sentiment': 20,      # 감성 분석
        'keyword': 20,        # 키워드 분석
        'gpt_evaluation': 35  # GPT 평가
    }
    
    def __init__(self, use_gpt: bool = True):
        self.preprocessor = TextPreprocessor()
        self.readability = ReadabilityAnalyzer()
        self.sentiment = SentimentAnalyzer()
        self.keyword = KeywordAnalyzer()
        self.use_gpt = use_gpt
        
        if use_gpt:
            self.gpt = GPTEvaluator()
    
    def evaluate(
        self,
        text: str,
        writing_type: str = "일반",
        target_audience: str = "일반 독자",
        target_keywords: Optional[List[str]] = None
    ) -> Dict:
        """종합 품질 평가 실행"""
        
        print("📝 텍스트 전처리 중...")
        basic_stats = self.preprocessor.get_basic_stats(text)
        
        # 최소 텍스트 길이 체크
        if basic_stats['total_chars'] < 100:
            return {
                'error': '텍스트가 너무 짧습니다. 최소 100자 이상 입력해주세요.',
                'total_chars': basic_stats['total_chars']
            }
        
        print("📊 가독성 분석 중...")
        readability_report = self.readability.get_readability_report(text)
        readability_score = self._calc_readability_score(readability_report)
        
        print("💬 감성 분석 중...")
        sentiment_report = self.sentiment.analyze_full_text(text)
        sentiment_score = sentiment_report.get('balance_score', 50)
        
        print("🔍 키워드 분석 중...")
        keywords = self.keyword.extract_keywords_tfidf(text, top_n=10)
        coherence = self.keyword.analyze_topic_coherence(text)
        keyword_score = self._calc_keyword_score(
            coherence, keywords, text, target_keywords
        )
        
        gpt_report = {}
        gpt_score = 50  # 기본값
        
        if self.use_gpt:
            print("🤖 GPT 평가 중... (시간이 걸릴 수 있어요)")
            gpt_report = self.gpt.evaluate(text, writing_type, target_audience)
            gpt_score = self._calc_gpt_score(gpt_report)
        
        # 종합 점수 계산
        final_score = (
            readability_score * self.WEIGHTS['readability'] / 100 +
            sentiment_score * self.WEIGHTS['sentiment'] / 100 +
            keyword_score * self.WEIGHTS['keyword'] / 100 +
            gpt_score * self.WEIGHTS['gpt_evaluation'] / 100
        )
        
        grade = self._get_grade(final_score)
        
        return {
            'metadata': {
                'evaluated_at': datetime.now().isoformat(),
                'writing_type': writing_type,
                'target_audience': target_audience,
                'text_length': basic_stats['total_chars']
            },
            'basic_stats': basic_stats,
            'scores': {
                'readability': round(readability_score, 1),
                'sentiment': round(sentiment_score, 1),
                'keyword': round(keyword_score, 1),
                'gpt_evaluation': round(gpt_score, 1),
                'final': round(final_score, 1)
            },
            'grade': grade,
            'details': {
                'readability': readability_report,
                'sentiment': sentiment_report,
                'keywords': keywords,
                'coherence': coherence,
                'gpt': gpt_report
            },
            'recommendations': self._generate_recommendations(
                readability_score, sentiment_score, 
                keyword_score, gpt_score, gpt_report
            )
        }
    
    def _calc_readability_score(self, report: Dict) -> float:
        """가독성 점수 계산 (0~100)"""
        score = 70  # 기본 점수
        
        # TTR 점수 (0.4~0.7이 이상적)
        ttr = report.get('ttr', 0.5)
        if 0.4 <= ttr <= 0.7:
            score += 10
        elif ttr < 0.3 or ttr > 0.8:
            score -= 10
        
        # 문장 다양성
        variety = report.get('sentence_variety', {})
        variety_score = variety.get('variety_score', 50)
        score += (variety_score - 50) * 0.2
        
        # 반복 표현
        repetition = report.get('repetition', {})
        rep_score = repetition.get('repetition_score', 80)
        score += (rep_score - 80) * 0.2
        
        return max(0, min(100, score))
    
    def _calc_keyword_score(
        self, coherence: Dict, keywords: List, 
        text: str, target_keywords: Optional[List[str]]
    ) -> float:
        """키워드 점수 계산"""
        score = 60
        
        # 주제 일관성
        coherence_score = coherence.get('coherence_score', 50)
        score += (coherence_score - 50) * 0.4
        
        # 키워드 다양성 (상위 10개 키워드가 있으면 좋음)
        if len(keywords) >= 5:
            score += 10
        
        # 타겟 키워드 밀도 체크
        if target_keywords:
            density_report = self.keyword.calculate_keyword_density(
                text, target_keywords
            )
            optimal_count = sum(
                1 for k, v in density_report.items()
                if '적정' in v.get('status', '')
            )
            score += optimal_count * 5
        
        return max(0, min(100, score))
    
    def _calc_gpt_score(self, gpt_report: Dict) -> float:
        """GPT 평가 점수 계산"""
        if not gpt_report or 'scores' not in gpt_report:
            return 50
        
        scores = gpt_report['scores']
        weights = {
            'logical_flow': 0.25,
            'persuasiveness': 0.15,
            'clarity': 0.25,
            'creativity': 0.10,
            'structure': 0.15,
            'expertise': 0.10
        }
        
        weighted_score = sum(
            scores.get(k, 50) * w 
            for k, w in weights.items()
        )
        
        return weighted_score
    
    def _get_grade(self, score: float) -> str:
        """점수에 따른 등급 반환"""
        if score >= 90: return 'S'
        elif score >= 80: return 'A'
        elif score >= 70: return 'B'
        elif score >= 60: return 'C'
        else: return 'D'
    
    def _generate_recommendations(
        self, read_score, sent_score, 
        key_score, gpt_score, gpt_report
    ) -> List[str]:
        """개선 추천사항 생성"""
        recommendations = []
        
        if read_score < 60:
            recommendations.append(
                "📖 가독성 개선: 문장을 더 짧고 다양하게 구성해보세요. "
                "평균 문장 길이를 30~50자 사이로 유지하는 게 좋아요."
            )
        
        if sent_score < 60:
            recommendations.append(
                "💬 감성 균형: 부정적 표현이 많습니다. "
                "긍정적이고 건설적인 표현으로 바꿔보세요."
            )
        
        if key_score < 60:
            recommendations.append(
                "🔍 주제 일관성: 글의 핵심 주제를 더 명확히 하고, "
                "관련 키워드를 일관되게 사용해보세요."
            )
        
        if gpt_score < 60 and gpt_report:
            improvements = gpt_report.get('improvements', [])
            for imp in improvements[:2]:
                recommendations.append(f"🤖 AI 제안: {imp}")
        
        if not recommendations:
            recommendations.append(
                "✨ 전반적으로 잘 작성된 글입니다! "
                "현재 수준을 유지하면서 더 발전시켜보세요."
            )
        
        return recommendations

🚀 8. FastAPI로 서비스 배포 — 실제로 쓸 수 있게 만들기

평가 엔진을 만들었으면 이제 API로 감싸서 실제 서비스로 만들어야지!
FastAPI는 자동 문서화도 되고 비동기 처리도 되니까 이런 AI 서비스에 딱이야 😎

⚡ FastAPI 서버 구현

# main.py
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Optional, List
import asyncio
from writing_quality_engine import WritingQualityEngine
import json
import hashlib
import time

app = FastAPI(
    title="AI 글쓰기 품질 평가 API",
    description="Python + GPT 기반 한국어 글쓰기 품질 평가 시스템",
    version="1.0.0"
)

# CORS 설정
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"]
)

# 간단한 메모리 캐시 (실제 서비스는 Redis 사용 권장)
cache = {}
CACHE_TTL = 3600  # 1시간

# 평가 엔진 초기화 (앱 시작 시 한 번만)
engine = WritingQualityEngine(use_gpt=True)

class EvaluationRequest(BaseModel):
    text: str = Field(..., min_length=100, max_length=10000, 
                      description="평가할 텍스트 (100~10000자)")
    writing_type: str = Field(default="일반", 
                              description="글쓰기 유형 (일반/블로그/학술/마케팅)")
    target_audience: str = Field(default="일반 독자",
                                 description="대상 독자")
    target_keywords: Optional[List[str]] = Field(
        default=None, description="타겟 키워드 목록"
    )
    use_cache: bool = Field(default=True, description="캐시 사용 여부")

class EvaluationResponse(BaseModel):
    success: bool
    data: dict
    processing_time: float

def get_cache_key(request: EvaluationRequest) -> str:
    """캐시 키 생성"""
    content = f"{request.text}{request.writing_type}{request.target_audience}"
    return hashlib.md5(content.encode()).hexdigest()

@app.post("/evaluate", response_model=EvaluationResponse)
async def evaluate_writing(request: EvaluationRequest):
    """글쓰기 품질 평가 엔드포인트"""
    
    start_time = time.time()
    
    # 캐시 확인
    if request.use_cache:
        cache_key = get_cache_key(request)
        if cache_key in cache:
            cached_data, cached_time = cache[cache_key]
            if time.time() - cached_time < CACHE_TTL:
                return EvaluationResponse(
                    success=True,
                    data={**cached_data, 'from_cache': True},
                    processing_time=0.01
                )
    
    try:
        # 비동기 실행 (블로킹 방지)
        result = await asyncio.get_event_loop().run_in_executor(
            None,
            lambda: engine.evaluate(
                request.text,
                request.writing_type,
                request.target_audience,
                request.target_keywords
            )
        )
        
        # 캐시 저장
        if request.use_cache:
            cache[cache_key] = (result, time.time())
        
        processing_time = time.time() - start_time
        
        return EvaluationResponse(
            success=True,
            data=result,
            processing_time=round(processing_time, 2)
        )
    
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/health")
async def health_check():
    """서버 상태 확인"""
    return {"status": "healthy", "version": "1.0.0"}

@app.get("/")
async def root():
    return {
        "message": "AI 글쓰기 품질 평가 API",
        "docs": "/docs",
        "redoc": "/redoc"
    }

# 서버 실행
# uvicorn main:app --reload --host 0.0.0.0 --port 8000
💡 API 사용 예시 (Python requests)

import requests

response = requests.post(
    "http://localhost:8000/evaluate",
    json={
        "text": "여기에 평가할 텍스트를 입력하세요...",
        "writing_type": "블로그",
        "target_audience": "20~30대 직장인",
        "target_keywords": ["Python", "AI", "개발"]
    }
)

result = response.json()
print(f"최종 점수: {result['data']['scores']['final']}")
print(f"등급: {result['data']['grade']}")
print(f"처리 시간: {result['processing_time']}초")

📈 9. 결과 시각화 — 보기 좋은 리포트 만들기

숫자만 나오면 재미없잖아ㅋㅋ matplotlib으로 예쁜 리포트 차트도 만들어보자!

🎨 평가 결과 시각화

# visualizer.py
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
from typing import Dict
import matplotlib
matplotlib.rcParams['font.family'] = 'NanumGothic'  # 한글 폰트

class EvaluationVisualizer:
    """평가 결과 시각화 클래스"""
    
    def __init__(self):
        self.colors = {
            'primary': '#5a4a8a',
            'secondary': '#7ab8d9',
            'success': '#7ac99a',
            'warning': '#e0a96d',
            'danger': '#c07070'
        }
    
    def create_radar_chart(self, gpt_scores: Dict, save_path: str = None):
        """레이더 차트 (GPT 평가 점수)"""
        
        categories = {
            'logical_flow': '논리 흐름',
            'persuasiveness': '설득력',
            'clarity': '명확성',
            'creativity': '창의성',
            'structure': '구조',
            'expertise': '전문성'
        }
        
        values = [gpt_scores.get(k, 50) for k in categories.keys()]
        labels = list(categories.values())
        
        # 레이더 차트 설정
        angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False)
        values_plot = values + [values[0]]
        angles_plot = np.concatenate([angles, [angles[0]]])
        
        fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
        
        ax.plot(angles_plot, values_plot, 'o-', linewidth=2, 
                color=self.colors['primary'])
        ax.fill(angles_plot, values_plot, alpha=0.25, 
                color=self.colors['primary'])
        
        ax.set_xticks(angles)
        ax.set_xticklabels(labels, size=12, fontweight='bold')
        ax.set_ylim(0, 100)
        ax.set_yticks([20, 40, 60, 80, 100])
        ax.set_yticklabels(['20', '40', '60', '80', '100'], size=9)
        ax.grid(color='gray', linestyle='--', linewidth=0.5, alpha=0.7)
        
        ax.set_title('AI 글쓰기 품질 평가\n레이더 차트', 
                    size=14, fontweight='bold', pad=20)
        
        if save_path:
            plt.savefig(save_path, dpi=150, bbox_inches='tight')
        
        plt.tight_layout()
        return fig
    
    def create_score_dashboard(self, result: Dict, save_path: str = None):
        """종합 점수 대시보드"""
        
        fig, axes = plt.subplots(2, 2, figsize=(12, 10))
        fig.suptitle('글쓰기 품질 평가 대시보드', 
                    fontsize=16, fontweight='bold', y=1.02)
        
        scores = result.get('scores', {})
        
        # 1. 종합 점수 게이지
        ax1 = axes[0, 0]
        final_score = scores.get('final', 0)
        self._draw_gauge(ax1, final_score, '종합 점수')
        
        # 2. 카테고리별 점수 바 차트
        ax2 = axes[0, 1]
        categories = ['가독성', '감성', '키워드', 'AI평가']
        cat_scores = [
            scores.get('readability', 0),
            scores.get('sentiment', 0),
            scores.get('keyword', 0),
            scores.get('gpt_evaluation', 0)
        ]
        colors = [self.colors['primary'], self.colors['secondary'],
                 self.colors['success'], self.colors['warning']]
        
        bars = ax2.barh(categories, cat_scores, color=colors, height=0.5)
        ax2.set_xlim(0, 100)
        ax2.set_title('카테고리별 점수', fontweight='bold')
        
        for bar, score in zip(bars, cat_scores):
            ax2.text(bar.get_width() + 1, bar.get_y() + bar.get_height()/2,
                    f'{score:.1f}', va='center', fontweight='bold')
        
        # 3. 감성 분포 파이 차트
        ax3 = axes[1, 0]
        sentiment = result.get('details', {}).get('sentiment', {})
        dist = sentiment.get('sentiment_distribution', 
                           {'positive': 33, 'neutral': 34, 'negative': 33})
        
        pie_labels = ['긍정', '중립', '부정']
        pie_values = [
            dist.get('positive', 0),
            dist.get('neutral', 0),
            dist.get('negative', 0)
        ]
        pie_colors = [self.colors['success'], self.colors['secondary'],
                     self.colors['danger']]
        
        ax3.pie(pie_values, labels=pie_labels, colors=pie_colors,
               autopct='%1.1f%%', startangle=90)
        ax3.set_title('감성 분포', fontweight='bold')
        
        # 4. 키워드 워드클라우드 대체 (상위 키워드 바 차트)
        ax4 = axes[1, 1]
        keywords = result.get('details', {}).get('keywords', [])[:8]
        
        if keywords:
            kw_labels = [k[0] for k in keywords]
            kw_scores = [k[1] * 100 for k in keywords]
            
            ax4.barh(kw_labels[::-1], kw_scores[::-1], 
                    color=self.colors['primary'], alpha=0.7, height=0.5)
            ax4.set_title('핵심 키워드 (TF-IDF)', fontweight='bold')
            ax4.set_xlabel('TF-IDF 점수 (%)')
        
        plt.tight_layout()
        
        if save_path:
            plt.savefig(save_path, dpi=150, bbox_inches='tight')
        
        return fig
    
    def _draw_gauge(self, ax, value: float, title: str):
        """게이지 차트 그리기"""
        ax.set_xlim(-1.5, 1.5)
        ax.set_ylim(-1.5, 1.5)
        ax.set_aspect('equal')
        ax.axis('off')
        
        # 배경 반원
        theta = np.linspace(np.pi, 0, 100)
        ax.plot(np.cos(theta), np.sin(theta), 
               color='#e0e0e0', linewidth=20, solid_capstyle='round')
        
        # 값 반원
        fill_theta = np.linspace(np.pi, np.pi - (value/100) * np.pi, 100)
        color = (self.colors['success'] if value >= 70 
                else self.colors['warning'] if value >= 50 
                else self.colors['danger'])
        ax.plot(np.cos(fill_theta), np.sin(fill_theta), 
               color=color, linewidth=20, solid_capstyle='round')
        
        ax.text(0, -0.3, f'{value:.1f}', ha='center', va='center',
               fontsize=28, fontweight='bold', color=color)
        ax.text(0, -0.7, title, ha='center', va='center',
               fontsize=12, fontweight='bold')

💡 10. 실전 활용 팁 & 고도화 아이디어

기본 시스템은 완성했는데, 이걸 더 잘 써먹으려면 몇 가지 팁이 있어!
재능넷에서 글쓰기 서비스를 제공하는 분들이라면 이 시스템으로 자신의 글 품질을 객관적으로 검증하고 포트폴리오에 활용할 수 있어 😊

🎯 도메인별 평가 기준 커스터마이징

글쓰기 유형에 따라 평가 기준이 달라야 해. 아래처럼 설정 파일로 관리하면 편해.


# evaluation_profiles.py
EVALUATION_PROFILES = {
    "블로그": {
        "weights": {
            "readability": 35,    # 가독성 더 중요
            "sentiment": 25,      # 감성 중요
            "keyword": 25,        # SEO 키워드 중요
            "gpt_evaluation": 15
        },
        "ideal_sentence_length": (20, 50),
        "ideal_ttr": (0.45, 0.65),
        "tone": "친근하고 대화체"
    },
    "학술": {
        "weights": {
            "readability": 15,
            "sentiment": 10,
            "keyword": 25,
            "gpt_evaluation": 50   # AI 논리 평가 최중요
        },
        "ideal_sentence_length": (40, 80),
        "ideal_ttr": (0.55, 0.75),
        "tone": "객관적이고 형식적"
    },
    "마케팅": {
        "weights": {
            "readability": 25,
            "sentiment": 35,      # 감성 최중요
            "keyword": 25,
            "gpt_evaluation": 15
        },
        "ideal_sentence_length": (15, 40),
        "ideal_ttr": (0.35, 0.55),
        "tone": "설득적이고 행동 유도"
    }
}
🔄 배치 처리 & 비교 분석

여러 글을 한 번에 평가하거나 A/B 테스트할 때 유용한 배치 처리 기능이야.


# batch_evaluator.py
import asyncio
from typing import List, Dict
from writing_quality_engine import WritingQualityEngine

async def batch_evaluate(
    texts: List[str], 
    engine: WritingQualityEngine,
    max_concurrent: int = 3
) -> List[Dict]:
    """비동기 배치 평가"""
    
    semaphore = asyncio.Semaphore(max_concurrent)
    
    async def evaluate_one(text: str, idx: int) -> Dict:
        async with semaphore:
            print(f"평가 중 [{idx+1}/{len(texts)}]...")
            loop = asyncio.get_event_loop()
            result = await loop.run_in_executor(
                None, engine.evaluate, text
            )
            return {'index': idx, 'result': result}
    
    tasks = [evaluate_one(text, i) for i, text in enumerate(texts)]
    results = await asyncio.gather(*tasks)
    
    return sorted(results, key=lambda x: x['index'])

def compare_texts(results: List[Dict]) -> Dict:
    """여러 텍스트 비교 분석"""
    
    comparison = []
    for i, r in enumerate(results):
        scores = r['result'].get('scores', {})
        comparison.append({
            'index': i + 1,
            'final_score': scores.get('final', 0),
            'grade': r['result'].get('grade', 'N/A'),
            'readability': scores.get('readability', 0),
            'sentiment': scores.get('sentiment', 0),
            'keyword': scores.get('keyword', 0),
            'gpt': scores.get('gpt_evaluation', 0)
        })
    
    # 최고/최저 점수 텍스트 찾기
    best = max(comparison, key=lambda x: x['final_score'])
    worst = min(comparison, key=lambda x: x['final_score'])
    
    return {
        'comparison': comparison,
        'best_text': best,
        'worst_text': worst,
        'avg_score': sum(c['final_score'] for c in comparison) / len(comparison)
    }
고도화 아이디어 리스트
🗄️ DB 연동 PostgreSQL로 평가 이력 저장 및 트렌드 분석
📱 웹 UI Streamlit이나 Gradio로 빠른 프로토타입 UI 구현
🔔 알림 점수 임계값 이하 시 Slack/이메일 알림
📊 대시보드 Grafana로 실시간 품질 모니터링
🤖 파인튜닝 도메인 특화 데이터로 감성 모델 파인튜닝
🌐 다국어 영어/일본어 텍스트 평가 확장

🏆 프로젝트 파일 구조 한눈에 보기 writing_eval_system/ ├── config.py ├── main.py (FastAPI) ├── text_preprocessor.py ├── readability_analyzer.py ├── sentiment_analyzer.py ├── keyword_analyzer.py ├── gpt_evaluator.py ├── writing_quality_engine.py ├── visualizer.py ├── batch_evaluator.py └── .env ⚡ 핵심 기술 스택 요약 🔧 전처리: kiwipiepy (형태소 분석) 📊 분석: scikit-learn TF-IDF 💬 감성: transformers (KoELECTRA) 🤖 AI 평가: OpenAI GPT-4o-mini 🚀 서비스: FastAPI + uvicorn Python 3.10+ 권장

🎯 마무리 — 이 시스템으로 뭘 할 수 있냐고?

야 솔직히 이 시스템 완성하면 진짜 쓸모가 엄청 많아ㅋㅋ 정리해줄게!

✍️ 콘텐츠 크리에이터: 블로그 포스팅 올리기 전에 품질 체크 → 독자 이탈률 감소

📚 교육 플랫폼: 학생 글쓰기 과제 자동 채점 보조 도구로 활용

🏢 기업 마케팅팀: 카피라이팅 품질 일관성 유지 및 A/B 테스트

🔍 SEO 전문가: 키워드 밀도 최적화 및 가독성 개선

🤝 프리랜서: 납품 전 품질 검증으로 클라이언트 만족도 향상

특히 재능넷에서 글쓰기 관련 재능을 판매하는 분들한테 이 시스템은 진짜 차별화 포인트가 될 수 있어.
"AI 품질 검증 완료" 배지 같은 거 달 수 있잖아ㅋㅋ 클라이언트 신뢰도 올라가는 거 보장!

⚠️ 주의사항 & 한계점
이 시스템은 보조 도구야. AI 점수가 높다고 무조건 좋은 글이 아니고,
낮다고 나쁜 글도 아니야. 창의적인 글쓰기는 수치로 완전히 평가할 수 없어.
특히 시, 소설 같은 문학 장르에는 이 평가 기준이 맞지 않을 수 있어!
전체 코드 요약
총 8개 모듈 → 약 600줄의 Python 코드
처리 시간: GPT 포함 약 5~15초 / GPT 제외 약 0.5~2초
API 비용: gpt-4o-mini 기준 글 하나당 약 $0.001~0.003 (매우 저렴!)
🐍 🤖 ✍️ 📊 🚀 💡 🏆
댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0