File size: 15,186 Bytes
c7e434a de7ca56 c7e434a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 |
"""
Keyword Relevance Service
Analisis relevansi kata kunci dengan topik menggunakan BERT embeddings
"""
import json
import re
import numpy as np
from typing import Dict, List, Tuple
from collections import defaultdict
try:
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
from app.core.device import get_device
EMBEDDINGS_AVAILABLE = True
except ImportError:
EMBEDDINGS_AVAILABLE = False
print("β οΈ Warning: sentence-transformers not installed. Using fallback mode.")
class KeywordService:
"""Analisis relevansi kata kunci"""
def __init__(self, dataset_path: str, model_name: str = 'paraphrase-multilingual-MiniLM-L12-v2'):
"""
Initialize analyzer
Args:
dataset_path: Path ke file JSON dataset kata kunci
model_name: Nama model Sentence Transformer
"""
print("π Initializing Keyword Service...")
self.dataset_path = dataset_path
self.topics = {}
# Load dataset
self.load_dataset(dataset_path)
# Load BERT model
if EMBEDDINGS_AVAILABLE:
print(f"π¦ Loading BERT model: {model_name}...")
device = get_device()
# Use HF_HOME cache directory (set in Dockerfile)
import os
cache_dir = os.environ.get('HF_HOME', '/.cache')
self.model = SentenceTransformer(model_name, device=device, cache_folder=cache_dir)
print("β
Model loaded!")
else:
self.model = None
print("β οΈ Running in fallback mode (no embeddings)")
# Precompute embeddings
self.keyword_embeddings = {}
if self.model:
self._precompute_embeddings()
print("β
Keyword Service ready!\n")
def load_dataset(self, json_path: str):
"""Load dataset dari file JSON"""
try:
with open(json_path, 'r', encoding='utf-8') as f:
self.topics = json.load(f)
print(f"β
Dataset loaded: {len(self.topics)} topics")
except FileNotFoundError:
raise FileNotFoundError(f"β Dataset file not found: {json_path}")
except json.JSONDecodeError as e:
raise ValueError(f"β Invalid JSON format: {e}")
def _precompute_embeddings(self):
"""Precompute embeddings untuk semua keywords"""
print("π Precomputing embeddings...")
for topic_id, topic_data in self.topics.items():
self.keyword_embeddings[topic_id] = {}
# Embed keywords
keywords = topic_data['keywords']
self.keyword_embeddings[topic_id]['keywords'] = self.model.encode(keywords)
# Embed variants
all_variants = []
variant_mapping = []
for keyword in keywords:
variants = topic_data['variants'].get(keyword, [])
for variant in variants:
all_variants.append(variant)
variant_mapping.append(keyword)
if all_variants:
self.keyword_embeddings[topic_id]['variants'] = {
'embeddings': self.model.encode(all_variants),
'mapping': variant_mapping,
'texts': all_variants
}
print("β
Embeddings ready!")
def extract_sentences(self, text: str) -> List[str]:
"""Extract sentences dari text"""
sentences = re.split(r'[.!?]+', text)
sentences = [s.strip() for s in sentences if s.strip()]
return sentences
def semantic_keyword_detection(self, text: str, topic_id: str, threshold: float = 0.5) -> Dict:
"""Deteksi keyword menggunakan semantic similarity"""
if not self.model or topic_id not in self.keyword_embeddings:
return self._fallback_detection(text, topic_id)
sentences = self.extract_sentences(text)
sentence_embeddings = self.model.encode(sentences)
topic_data = self.topics[topic_id]
keyword_embs = self.keyword_embeddings[topic_id]
detected_keywords = defaultdict(list)
# Direct keyword matching
keyword_similarities = cosine_similarity(
sentence_embeddings,
keyword_embs['keywords']
)
for sent_idx, sentence in enumerate(sentences):
for kw_idx, keyword in enumerate(topic_data['keywords']):
similarity = keyword_similarities[sent_idx][kw_idx]
if similarity >= threshold:
detected_keywords[keyword].append({
'type': 'semantic',
'sentence': sentence,
'similarity': float(similarity)
})
# Variant matching
if 'variants' in keyword_embs:
variant_similarities = cosine_similarity(
sentence_embeddings,
keyword_embs['variants']['embeddings']
)
for sent_idx, sentence in enumerate(sentences):
for var_idx, (variant, mapped_kw) in enumerate(
zip(keyword_embs['variants']['texts'],
keyword_embs['variants']['mapping'])
):
similarity = variant_similarities[sent_idx][var_idx]
if similarity >= threshold:
if not any(d['type'] == 'variant' and d.get('variant') == variant
for d in detected_keywords[mapped_kw]):
detected_keywords[mapped_kw].append({
'type': 'variant',
'variant': variant,
'sentence': sentence,
'similarity': float(similarity)
})
# Exact string matching
text_lower = text.lower()
for keyword in topic_data['keywords']:
if keyword in text_lower:
if not any(d['type'] == 'exact' for d in detected_keywords[keyword]):
detected_keywords[keyword].insert(0, {
'type': 'exact',
'keyword': keyword,
'similarity': 1.0
})
# Check variants
for variant in topic_data['variants'].get(keyword, []):
if variant.lower() in text_lower:
if not any(d['type'] == 'exact_variant' and d.get('variant') == variant
for d in detected_keywords[keyword]):
detected_keywords[keyword].insert(0, {
'type': 'exact_variant',
'variant': variant,
'similarity': 1.0
})
return dict(detected_keywords)
def _fallback_detection(self, text: str, topic_id: str) -> Dict:
"""Fallback method tanpa embeddings"""
text_lower = text.lower()
topic_data = self.topics[topic_id]
detected_keywords = {}
for keyword in topic_data['keywords']:
detections = []
if keyword in text_lower:
detections.append({
'type': 'exact',
'keyword': keyword,
'similarity': 1.0
})
for variant in topic_data['variants'].get(keyword, []):
if variant.lower() in text_lower:
detections.append({
'type': 'variant',
'variant': variant,
'similarity': 0.9
})
if detections:
detected_keywords[keyword] = detections
return detected_keywords
def calculate_score(self, detected_count: int) -> Dict:
"""Calculate skor berdasarkan jumlah keyword terdeteksi"""
if detected_count >= 9:
return {
'score': 5,
'category': 'Sangat Baik',
'description': 'Coverage keyword sangat lengkap'
}
elif detected_count >= 7:
return {
'score': 4,
'category': 'Baik',
'description': 'Coverage keyword baik'
}
elif detected_count >= 5:
return {
'score': 3,
'category': 'Cukup',
'description': 'Coverage keyword cukup'
}
elif detected_count >= 3:
return {
'score': 2,
'category': 'Buruk',
'description': 'Coverage keyword kurang'
}
else:
return {
'score': 1,
'category': 'Perlu Ditingkatkan',
'description': 'Coverage keyword sangat rendah'
}
def analyze(
self,
speech_text: str,
topic_id: str = None,
custom_topic: str = None,
custom_keywords: List[str] = None,
threshold: float = 0.5
) -> Dict:
"""
Analisis relevansi speech dengan topik
Args:
speech_text: Teks speech hasil transcription
topic_id: ID topik dari database (untuk level 1-2)
custom_topic: Topik custom dari user (untuk level 3)
custom_keywords: List kata kunci dari GPT (untuk level 3)
threshold: Similarity threshold
Returns:
Dict berisi hasil analisis
"""
# Mode 1: Custom topic & keywords (Level 3 - dari GPT)
if custom_topic and custom_keywords:
print(f"π Analyzing custom keywords for topic: {custom_topic}...")
return self._analyze_custom_keywords(
speech_text,
custom_topic,
custom_keywords,
threshold
)
# Mode 2: Predefined topic (Level 1-2 - dari database)
if topic_id:
print(f"π Analyzing keywords for topic {topic_id}...")
if topic_id not in self.topics:
return {"error": f"Topik '{topic_id}' tidak ditemukan"}
topic_data = self.topics[topic_id]
# Deteksi keywords
detected_keywords = self.semantic_keyword_detection(
speech_text, topic_id, threshold
)
missing_keywords = [
kw for kw in topic_data['keywords']
if kw not in detected_keywords
]
# Calculate scores
total_keywords = len(topic_data['keywords'])
detected_count = len(detected_keywords)
coverage_percentage = (detected_count / total_keywords) * 100
score_result = self.calculate_score(detected_count)
print(f"β
Keyword analysis complete!\n")
return {
'score': score_result['score'],
'category': score_result['category'],
'description': score_result['description'],
'topic_id': topic_id,
'topic_title': topic_data['title'],
'detected_count': detected_count,
'total_keywords': total_keywords,
'coverage_percentage': round(coverage_percentage, 1),
'detected_keywords': list(detected_keywords.keys()),
'missing_keywords': missing_keywords
}
# Mode 3: Error - tidak ada input
return {"error": "Harus menyediakan topic_id ATAU (custom_topic + custom_keywords)"}
def _analyze_custom_keywords(
self,
speech_text: str,
custom_topic: str,
custom_keywords: List[str],
threshold: float = 0.5
) -> Dict:
"""
Analisis dengan custom keywords dari GPT (untuk Level 3)
Menghitung berapa kali setiap keyword disebutkan dalam speech
"""
speech_lower = speech_text.lower()
# Hitung kemunculan setiap keyword
keyword_mentions = {}
total_mentions = 0
for keyword in custom_keywords:
keyword_lower = keyword.lower()
# Count exact matches (case-insensitive)
count = speech_lower.count(keyword_lower)
if count > 0:
keyword_mentions[keyword] = {
'count': count,
'mentioned': True
}
total_mentions += count
else:
keyword_mentions[keyword] = {
'count': 0,
'mentioned': False
}
# Hitung statistik
total_keywords = len(custom_keywords)
mentioned_count = sum(1 for kw in keyword_mentions.values() if kw['mentioned'])
not_mentioned = [kw for kw, data in keyword_mentions.items() if not data['mentioned']]
coverage_percentage = (mentioned_count / total_keywords) * 100 if total_keywords > 0 else 0
# Calculate score berdasarkan coverage
score_result = self.calculate_score(mentioned_count)
# Semantic analysis (optional - jika ada model)
semantic_relevance = None
if self.model:
try:
# Encode speech dan keywords
speech_embedding = self.model.encode([speech_text])
keywords_text = " ".join(custom_keywords)
keywords_embedding = self.model.encode([keywords_text])
# Calculate cosine similarity
similarity = cosine_similarity(speech_embedding, keywords_embedding)[0][0]
semantic_relevance = {
'similarity_score': round(float(similarity), 3),
'percentage': round(float(similarity) * 100, 1)
}
except Exception as e:
print(f"β οΈ Semantic analysis failed: {e}")
print(f"β
Custom keyword analysis complete!\n")
return {
'score': score_result['score'],
'category': score_result['category'],
'description': score_result['description'],
'mode': 'custom',
'custom_topic': custom_topic,
'total_keywords': total_keywords,
'mentioned_count': mentioned_count,
'total_mentions': total_mentions,
'coverage_percentage': round(coverage_percentage, 1),
'keyword_details': keyword_mentions,
'not_mentioned': not_mentioned,
'semantic_relevance': semantic_relevance
}
|