ZedwrKc commited on
Commit
bb78fbc
Β·
1 Parent(s): 2b2a035

Add stance analysis integration

Browse files

- Integrate KoBERT stance classifier (gaaahee/political-news-stance-classifier)
- Update /batch-process-articles to include stance analysis
- Add huggingface-hub dependency
- Support 3-in-1 pipeline: Summary + Embedding + Stance

requirements.txt CHANGED
@@ -7,6 +7,7 @@ sentencepiece==0.2.0
7
  sentence-transformers==3.3.1
8
  python-dotenv==1.0.1
9
  keybert==0.8.5
 
10
 
11
  # BERTopic Clustering & Visualization
12
  bertopic==0.17.3
 
7
  sentence-transformers==3.3.1
8
  python-dotenv==1.0.1
9
  keybert==0.8.5
10
+ huggingface-hub>=0.20.0
11
 
12
  # BERTopic Clustering & Visualization
13
  bertopic==0.17.3
src/api/main.py CHANGED
@@ -21,6 +21,7 @@ from src.api.schemas import (
21
  )
22
  from src.models.summarizer import KoBARTSummarizer
23
  from src.models.embedding import KoSentenceEmbedder
 
24
  from src.utils.config import settings
25
  from src.utils.logger import setup_logger
26
  from src.utils.validation import validate_models_loaded, validate_batch_size
@@ -31,6 +32,7 @@ logger = setup_logger()
31
  # Global model instances
32
  summarizer: KoBARTSummarizer = None
33
  embedder: KoSentenceEmbedder = None
 
34
 
35
 
36
  @asynccontextmanager
@@ -39,12 +41,13 @@ async def lifespan(app: FastAPI):
39
  Application lifespan context manager
40
  Load models on startup, cleanup on shutdown
41
  """
42
- global summarizer, embedder
43
 
44
  # Startup: Load models
45
  logger.info("Starting AI Processing Service...")
46
  logger.info(f"Summarization Model: {settings.MODEL_NAME}")
47
  logger.info(f"Embedding Model: jhgan/ko-sroberta-multitask")
 
48
  logger.info(f"Max batch size: {settings.MAX_BATCH_SIZE}")
49
 
50
  try:
@@ -56,6 +59,12 @@ async def lifespan(app: FastAPI):
56
  embedder = KoSentenceEmbedder()
57
  logger.info("βœ“ Embedding model loaded successfully (768-dim)")
58
 
 
 
 
 
 
 
59
  logger.info("All models ready!")
60
  except Exception as e:
61
  logger.error(f"Failed to load models: {e}")
@@ -93,7 +102,7 @@ async def root():
93
  "version": "1.0.0",
94
  "endpoints": {
95
  "health": "/health",
96
- "process": "/batch-process-articles (summary + embedding)",
97
  "summarize": "/batch-summarize (legacy)",
98
  "cluster": "/cluster-topics (BERTopic clustering - CustomTokenizer)",
99
  "cluster_mecab": "/cluster-topics-mecab (BERTopic clustering - Mecab)",
@@ -117,7 +126,7 @@ async def health_check():
117
  status="healthy",
118
  summarization_model=summarizer.model_name,
119
  embedding_model=embedder.model_name,
120
- stance_model=None, # Not yet implemented
121
  device=summarizer.device
122
  )
123
 
@@ -199,13 +208,13 @@ async def batch_process_articles(request: BatchProcessRequest):
199
  Processing Pipeline:
200
  1. Content β†’ Summary (KoBART)
201
  2. Title + Summary β†’ Embedding (ko-sroberta-multitask, 768-dim) ⭐
202
- 3. Summary β†’ Stance (optional, not yet implemented)
203
 
204
  Args:
205
  request: BatchProcessRequest with list of articles
206
 
207
  Returns:
208
- BatchProcessResponse with summaries, embeddings, and optional stance results
209
 
210
  Raises:
211
  HTTPException: If models not loaded or batch size exceeded
@@ -301,8 +310,39 @@ async def batch_process_articles(request: BatchProcessRequest):
301
  logger.error(f"Batch embedding failed: {e}")
302
  # Embeddings will remain None for failed articles
303
 
304
- # Step 3: Stance analysis (TODO: implement when model ready)
305
- # For now, stance remains None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  # Calculate statistics
308
  successful = sum(1 for r in results if r.error is None)
 
21
  )
22
  from src.models.summarizer import KoBARTSummarizer
23
  from src.models.embedding import KoSentenceEmbedder
24
+ from src.models.stance_classifier import KoBERTStanceAnalyzer
25
  from src.utils.config import settings
26
  from src.utils.logger import setup_logger
27
  from src.utils.validation import validate_models_loaded, validate_batch_size
 
32
  # Global model instances
33
  summarizer: KoBARTSummarizer = None
34
  embedder: KoSentenceEmbedder = None
35
+ stance_analyzer: KoBERTStanceAnalyzer = None
36
 
37
 
38
  @asynccontextmanager
 
41
  Application lifespan context manager
42
  Load models on startup, cleanup on shutdown
43
  """
44
+ global summarizer, embedder, stance_analyzer
45
 
46
  # Startup: Load models
47
  logger.info("Starting AI Processing Service...")
48
  logger.info(f"Summarization Model: {settings.MODEL_NAME}")
49
  logger.info(f"Embedding Model: jhgan/ko-sroberta-multitask")
50
+ logger.info(f"Stance Model: gaaahee/political-news-stance-classifier")
51
  logger.info(f"Max batch size: {settings.MAX_BATCH_SIZE}")
52
 
53
  try:
 
59
  embedder = KoSentenceEmbedder()
60
  logger.info("βœ“ Embedding model loaded successfully (768-dim)")
61
 
62
+ # Load stance analysis model
63
+ stance_analyzer = KoBERTStanceAnalyzer(
64
+ repo_id="gaaahee/political-news-stance-classifier"
65
+ )
66
+ logger.info("βœ“ Stance analysis model loaded successfully")
67
+
68
  logger.info("All models ready!")
69
  except Exception as e:
70
  logger.error(f"Failed to load models: {e}")
 
102
  "version": "1.0.0",
103
  "endpoints": {
104
  "health": "/health",
105
+ "process": "/batch-process-articles (summary + embedding + stance)",
106
  "summarize": "/batch-summarize (legacy)",
107
  "cluster": "/cluster-topics (BERTopic clustering - CustomTokenizer)",
108
  "cluster_mecab": "/cluster-topics-mecab (BERTopic clustering - Mecab)",
 
126
  status="healthy",
127
  summarization_model=summarizer.model_name,
128
  embedding_model=embedder.model_name,
129
+ stance_model=stance_analyzer.model_name if stance_analyzer else None,
130
  device=summarizer.device
131
  )
132
 
 
208
  Processing Pipeline:
209
  1. Content β†’ Summary (KoBART)
210
  2. Title + Summary β†’ Embedding (ko-sroberta-multitask, 768-dim) ⭐
211
+ 3. Summary β†’ Stance (KoBERT fine-tuned, support/neutral/oppose) ⭐
212
 
213
  Args:
214
  request: BatchProcessRequest with list of articles
215
 
216
  Returns:
217
+ BatchProcessResponse with summaries, embeddings, and stance results
218
 
219
  Raises:
220
  HTTPException: If models not loaded or batch size exceeded
 
310
  logger.error(f"Batch embedding failed: {e}")
311
  # Embeddings will remain None for failed articles
312
 
313
+ # Step 3: Stance analysis (KoBERT fine-tuned model)
314
+ if stance_analyzer:
315
+ try:
316
+ # Collect summaries for stance analysis (only successful summaries)
317
+ summaries_for_stance = []
318
+ stance_indices = []
319
+
320
+ for idx, result in enumerate(results):
321
+ if result.summary and result.error is None:
322
+ summaries_for_stance.append(result.summary)
323
+ stance_indices.append(idx)
324
+
325
+ if summaries_for_stance:
326
+ logger.info(f"Analyzing stance for {len(summaries_for_stance)} summaries")
327
+
328
+ # Batch stance analysis
329
+ stance_results = stance_analyzer.analyze_batch(
330
+ summaries_for_stance,
331
+ batch_size=16
332
+ )
333
+
334
+ # Map stance results back to results
335
+ for idx, stance_result in zip(stance_indices, stance_results):
336
+ from src.api.schemas import StanceResult
337
+ results[idx].stance = StanceResult(**stance_result)
338
+
339
+ logger.info(f"βœ“ Stance analysis completed for {len(stance_results)} articles")
340
+
341
+ except Exception as e:
342
+ logger.error(f"Stance analysis failed: {e}")
343
+ # Stance will remain None for failed articles
344
+ else:
345
+ logger.warning("Stance analyzer not available, skipping stance analysis")
346
 
347
  # Calculate statistics
348
  successful = sum(1 for r in results if r.error is None)
src/models/stance_classifier.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ KoBERT-based Stance Classifier for Korean Political News
3
+
4
+ Loads fine-tuned stance classification model from HuggingFace Hub.
5
+ Model classifies news text into 3 stances: support/neutral/oppose
6
+ """
7
+ import torch
8
+ import torch.nn as nn
9
+ from transformers import BertModel, AutoTokenizer
10
+ from huggingface_hub import hf_hub_download
11
+ import logging
12
+ from typing import List, Dict, Optional
13
+ import json
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class StanceClassifier(nn.Module):
19
+ """KoBERT-based stance classification model"""
20
+
21
+ def __init__(self, n_classes=3, dropout=0.3, model_name="skt/kobert-base-v1"):
22
+ super(StanceClassifier, self).__init__()
23
+ self.bert = BertModel.from_pretrained(model_name)
24
+ self.dropout = nn.Dropout(dropout)
25
+ self.classifier = nn.Linear(self.bert.config.hidden_size, n_classes)
26
+
27
+ def forward(self, input_ids, attention_mask):
28
+ outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
29
+ pooled_output = outputs.pooler_output
30
+ pooled_output = self.dropout(pooled_output)
31
+ return self.classifier(pooled_output)
32
+
33
+
34
+ class KoBERTStanceAnalyzer:
35
+ """
36
+ KoBERT-based stance analyzer for Korean political news
37
+
38
+ Loads model from HuggingFace Hub (gaaahee/political-news-stance-classifier)
39
+ and performs stance classification on article summaries.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ repo_id: str = "gaaahee/political-news-stance-classifier",
45
+ device: Optional[str] = None
46
+ ):
47
+ """
48
+ Initialize stance analyzer
49
+
50
+ Args:
51
+ repo_id: HuggingFace Hub repository ID
52
+ device: Device to run model on (cpu/cuda). Auto-detects if None.
53
+ """
54
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
55
+ self.repo_id = repo_id
56
+ self.label_names = ["옹호", "쀑립", "λΉ„νŒ"]
57
+ self.label_names_en = ["support", "neutral", "oppose"]
58
+
59
+ # Model metadata (will be loaded from config.json)
60
+ self.model_name = "KoBERT Stance Classifier"
61
+ self.base_model = "skt/kobert-base-v1"
62
+ self.tokenizer_name = "monologg/kobert"
63
+ self.num_labels = 3
64
+ self.max_length = 512
65
+ self.dropout = 0.3
66
+
67
+ # Load model components from HF Hub
68
+ self._load_model()
69
+ logger.info(f"βœ“ Stance model loaded from {repo_id} on {self.device}")
70
+
71
+ def _load_model(self):
72
+ """Load tokenizer and model from HuggingFace Hub"""
73
+ try:
74
+ # Load config from HF Hub
75
+ logger.info(f"Downloading config from {self.repo_id}")
76
+ config_path = hf_hub_download(self.repo_id, "config.json")
77
+ with open(config_path, "r", encoding="utf-8") as f:
78
+ config = json.load(f)
79
+
80
+ # Update model metadata from config
81
+ self.base_model = config.get("base_model", "skt/kobert-base-v1")
82
+ self.tokenizer_name = config.get("tokenizer", "monologg/kobert")
83
+ self.num_labels = config.get("num_labels", 3)
84
+ self.max_length = config.get("max_length", 512)
85
+ self.dropout = config.get("dropout", 0.3)
86
+
87
+ # Load tokenizer (must use monologg/kobert for compatibility)
88
+ logger.info(f"Loading tokenizer: {self.tokenizer_name}")
89
+ self.tokenizer = AutoTokenizer.from_pretrained(
90
+ self.tokenizer_name,
91
+ trust_remote_code=True
92
+ )
93
+
94
+ # Initialize model architecture
95
+ logger.info(f"Initializing model architecture: {self.base_model}")
96
+ self.model = StanceClassifier(
97
+ n_classes=self.num_labels,
98
+ dropout=self.dropout,
99
+ model_name=self.base_model
100
+ )
101
+
102
+ # Download and load fine-tuned weights from HF Hub
103
+ logger.info(f"Downloading model weights from {self.repo_id}")
104
+ model_path = hf_hub_download(self.repo_id, "model.pth")
105
+ state_dict = torch.load(model_path, map_location=self.device)
106
+ self.model.load_state_dict(state_dict)
107
+
108
+ # Move to device and set to eval mode
109
+ self.model.to(self.device)
110
+ self.model.eval()
111
+
112
+ logger.info(f"βœ“ Model loaded successfully (Test Accuracy: {config.get('test_accuracy', 'N/A')})")
113
+
114
+ except Exception as e:
115
+ logger.error(f"Failed to load stance model from HF Hub: {e}")
116
+ raise
117
+
118
+ def predict_single(self, text: str) -> Dict:
119
+ """
120
+ Predict stance for a single text
121
+
122
+ Args:
123
+ text: Article summary to analyze
124
+
125
+ Returns:
126
+ Dict with stance, confidence, and probabilities
127
+ """
128
+ inputs = self.tokenizer(
129
+ text,
130
+ return_tensors="pt",
131
+ max_length=self.max_length,
132
+ truncation=True,
133
+ padding="max_length"
134
+ )
135
+
136
+ input_ids = inputs["input_ids"].to(self.device)
137
+ attention_mask = inputs["attention_mask"].to(self.device)
138
+
139
+ with torch.no_grad():
140
+ outputs = self.model(input_ids, attention_mask)
141
+ probs = torch.softmax(outputs, dim=1)[0]
142
+ pred = torch.argmax(probs).item()
143
+
144
+ return {
145
+ "stance": self.label_names_en[pred],
146
+ "stance_kr": self.label_names[pred],
147
+ "confidence": round(probs[pred].item(), 4),
148
+ "probabilities": {
149
+ "support": round(probs[0].item(), 4),
150
+ "neutral": round(probs[1].item(), 4),
151
+ "oppose": round(probs[2].item(), 4)
152
+ }
153
+ }
154
+
155
+ def predict_batch(self, texts: List[str], batch_size: int = 16) -> List[Dict]:
156
+ """
157
+ Predict stance for multiple texts in batches
158
+
159
+ Args:
160
+ texts: List of article summaries to analyze
161
+ batch_size: Batch size for processing
162
+
163
+ Returns:
164
+ List of stance prediction results
165
+ """
166
+ results = []
167
+
168
+ for i in range(0, len(texts), batch_size):
169
+ batch = texts[i:i + batch_size]
170
+ inputs = self.tokenizer(
171
+ batch,
172
+ return_tensors="pt",
173
+ max_length=self.max_length,
174
+ truncation=True,
175
+ padding="max_length"
176
+ )
177
+
178
+ input_ids = inputs["input_ids"].to(self.device)
179
+ attention_mask = inputs["attention_mask"].to(self.device)
180
+
181
+ with torch.no_grad():
182
+ outputs = self.model(input_ids, attention_mask)
183
+ probs = torch.softmax(outputs, dim=1)
184
+
185
+ for j in range(len(batch)):
186
+ pred = torch.argmax(probs[j]).item()
187
+ results.append({
188
+ "stance": self.label_names_en[pred],
189
+ "stance_kr": self.label_names[pred],
190
+ "confidence": round(probs[j][pred].item(), 4),
191
+ "probabilities": {
192
+ "support": round(probs[j][0].item(), 4),
193
+ "neutral": round(probs[j][1].item(), 4),
194
+ "oppose": round(probs[j][2].item(), 4)
195
+ }
196
+ })
197
+
198
+ return results
199
+
200
+ def analyze_stance(self, summary: str) -> Dict:
201
+ """
202
+ Analyze stance from article summary
203
+
204
+ Args:
205
+ summary: Article summary text
206
+
207
+ Returns:
208
+ Dict compatible with StanceResult schema:
209
+ {
210
+ "stance_label": "support" | "neutral" | "oppose",
211
+ "prob_positive": float, # support probability
212
+ "prob_neutral": float,
213
+ "prob_negative": float, # oppose probability
214
+ "stance_score": float # prob_positive - prob_negative
215
+ }
216
+ """
217
+ result = self.predict_single(summary)
218
+ probs = result["probabilities"]
219
+
220
+ return {
221
+ "stance_label": result["stance"],
222
+ "prob_positive": probs["support"],
223
+ "prob_neutral": probs["neutral"],
224
+ "prob_negative": probs["oppose"],
225
+ "stance_score": probs["support"] - probs["oppose"]
226
+ }
227
+
228
+ def analyze_batch(self, summaries: List[str], batch_size: int = 16) -> List[Dict]:
229
+ """
230
+ Analyze stance for multiple summaries
231
+
232
+ Args:
233
+ summaries: List of article summaries
234
+ batch_size: Batch size for processing
235
+
236
+ Returns:
237
+ List of stance results compatible with StanceResult schema
238
+ """
239
+ results = self.predict_batch(summaries, batch_size=batch_size)
240
+
241
+ return [
242
+ {
243
+ "stance_label": r["stance"],
244
+ "prob_positive": r["probabilities"]["support"],
245
+ "prob_neutral": r["probabilities"]["neutral"],
246
+ "prob_negative": r["probabilities"]["oppose"],
247
+ "stance_score": r["probabilities"]["support"] - r["probabilities"]["oppose"]
248
+ }
249
+ for r in results
250
+ ]
test_stance_integration.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script for stance analysis integration
3
+
4
+ Tests the complete pipeline:
5
+ 1. Summary generation
6
+ 2. Embedding generation
7
+ 3. Stance analysis
8
+ """
9
+ import requests
10
+ import json
11
+
12
+
13
+ def test_batch_process_with_stance():
14
+ """Test /batch-process-articles endpoint with stance analysis"""
15
+
16
+ # Test data: Korean political news articles
17
+ test_articles = [
18
+ {
19
+ "article_id": 1,
20
+ "title": "μ •λΆ€ 뢀동산 규제 μ™„ν™”",
21
+ "content": "μ •λΆ€κ°€ 였늘 뢀동산 규제 μ™„ν™” λ°©μ•ˆμ„ λ°œν‘œν–ˆλ‹€. 이번 쑰치둜 주택 ꡬ맀가 더 μ‰¬μ›Œμ§ˆ 전망이닀. "
22
+ "전문가듀은 이번 정책이 경제 ν™œμ„±ν™”μ— 도움이 될 κ²ƒμœΌλ‘œ κΈ°λŒ€ν•˜κ³  μžˆλ‹€."
23
+ },
24
+ {
25
+ "article_id": 2,
26
+ "title": "μ•Όλ‹Ή μ •λΆ€ μ •μ±… λΉ„νŒ",
27
+ "content": "야당은 였늘 μ •λΆ€μ˜ 정책에 λŒ€ν•΄ κ°•ν•˜κ²Œ λΉ„νŒν–ˆλ‹€. μ•Όλ‹Ή λŒ€ν‘œλŠ” 이번 정책이 μ„œλ―Όλ“€μ—κ²Œ "
28
+ "도움이 λ˜μ§€ μ•ŠλŠ”λ‹€κ³  μ£Όμž₯ν–ˆλ‹€. 야당은 μ •λΆ€κ°€ μž¬κ²€ν† ν•΄μ•Ό ν•œλ‹€κ³  μ΄‰κ΅¬ν–ˆλ‹€."
29
+ },
30
+ {
31
+ "article_id": 3,
32
+ "title": "ꡭ회 λ²•μ•ˆ μ‹¬μ˜",
33
+ "content": "κ΅­νšŒμ—μ„œ 였늘 λ²•μ•ˆ μ‹¬μ˜κ°€ μ§„ν–‰λ˜μ—ˆλ‹€. μ—¬μ•Ό μ˜μ›λ“€μ΄ μ°Έμ„ν•œ κ°€μš΄λ° λ‹€μ–‘ν•œ 의견이 "
34
+ "μ œμ‹œλ˜μ—ˆλ‹€. λ²•μ•ˆμ€ λ‹€μŒ μ£Ό λ³ΈνšŒμ˜μ— 상정될 μ˜ˆμ •μ΄λ‹€."
35
+ }
36
+ ]
37
+
38
+ # API endpoint
39
+ url = "http://localhost:7860/batch-process-articles"
40
+
41
+ # Request payload
42
+ payload = {
43
+ "articles": test_articles,
44
+ "max_summary_length": 300,
45
+ "min_summary_length": 150
46
+ }
47
+
48
+ print("Testing /batch-process-articles endpoint...")
49
+ print(f"Sending {len(test_articles)} articles\n")
50
+
51
+ try:
52
+ # Send request
53
+ response = requests.post(url, json=payload, timeout=120)
54
+ response.raise_for_status()
55
+
56
+ # Parse response
57
+ result = response.json()
58
+
59
+ print("=" * 80)
60
+ print("RESPONSE SUMMARY")
61
+ print("=" * 80)
62
+ print(f"Total processed: {result['total_processed']}")
63
+ print(f"Successful: {result['successful']}")
64
+ print(f"Failed: {result['failed']}")
65
+ print(f"Processing time: {result['processing_time_seconds']:.2f}s")
66
+ print()
67
+
68
+ # Display results
69
+ for i, article_result in enumerate(result['results'], 1):
70
+ print("=" * 80)
71
+ print(f"ARTICLE {i}: {test_articles[i-1]['title']}")
72
+ print("=" * 80)
73
+
74
+ # Original content
75
+ print(f"\nOriginal (first 100 chars):")
76
+ print(f" {test_articles[i-1]['content'][:100]}...")
77
+
78
+ # Summary
79
+ if article_result['summary']:
80
+ print(f"\nSummary:")
81
+ print(f" {article_result['summary']}")
82
+ else:
83
+ print(f"\nSummary: FAILED - {article_result.get('error')}")
84
+
85
+ # Embedding
86
+ if article_result['embedding']:
87
+ print(f"\nEmbedding:")
88
+ print(f" Dimension: {len(article_result['embedding'])}")
89
+ print(f" First 5 values: {article_result['embedding'][:5]}")
90
+ else:
91
+ print(f"\nEmbedding: Not generated")
92
+
93
+ # Stance
94
+ if article_result['stance']:
95
+ stance = article_result['stance']
96
+ print(f"\nStance Analysis:")
97
+ print(f" Label: {stance['stance_label'].upper()}")
98
+ print(f" Score: {stance['stance_score']:.4f}")
99
+ print(f" Probabilities:")
100
+ print(f" - Support (옹호): {stance['prob_positive']:.4f}")
101
+ print(f" - Neutral (쀑립): {stance['prob_neutral']:.4f}")
102
+ print(f" - Oppose (λΉ„νŒ): {stance['prob_negative']:.4f}")
103
+ else:
104
+ print(f"\nStance: Not analyzed")
105
+
106
+ print()
107
+
108
+ print("=" * 80)
109
+ print("TEST COMPLETED SUCCESSFULLY")
110
+ print("=" * 80)
111
+
112
+ except requests.exceptions.RequestException as e:
113
+ print(f"ERROR: Request failed")
114
+ print(f" {e}")
115
+ if hasattr(e.response, 'text'):
116
+ print(f" Response: {e.response.text}")
117
+
118
+ except Exception as e:
119
+ print(f"ERROR: {e}")
120
+
121
+
122
+ def test_health_check():
123
+ """Test /health endpoint to verify all models are loaded"""
124
+
125
+ url = "http://localhost:7860/health"
126
+
127
+ print("Testing /health endpoint...")
128
+
129
+ try:
130
+ response = requests.get(url, timeout=10)
131
+ response.raise_for_status()
132
+
133
+ result = response.json()
134
+
135
+ print("\n" + "=" * 80)
136
+ print("HEALTH CHECK")
137
+ print("=" * 80)
138
+ print(f"Status: {result['status']}")
139
+ print(f"Device: {result['device']}")
140
+ print(f"\nModels loaded:")
141
+ print(f" - Summarization: {result['summarization_model']}")
142
+ print(f" - Embedding: {result['embedding_model']}")
143
+ print(f" - Stance: {result['stance_model']}")
144
+ print("=" * 80)
145
+ print()
146
+
147
+ if result['stance_model'] is None:
148
+ print("WARNING: Stance model not loaded!")
149
+ return False
150
+
151
+ return True
152
+
153
+ except Exception as e:
154
+ print(f"ERROR: Health check failed - {e}")
155
+ return False
156
+
157
+
158
+ if __name__ == "__main__":
159
+ print("\n" + "=" * 80)
160
+ print("STANCE ANALYSIS INTEGRATION TEST")
161
+ print("=" * 80)
162
+ print()
163
+
164
+ # Step 1: Health check
165
+ if test_health_check():
166
+ print("\nβœ“ Health check passed\n")
167
+
168
+ # Step 2: Test batch processing with stance
169
+ test_batch_process_with_stance()
170
+ else:
171
+ print("\nβœ— Health check failed - skipping batch test")
172
+ print("\nMake sure the API server is running:")
173
+ print(" python app.py")