import asyncio import os import logging import json import re import time import random from app.config import GEMINI_API_KEY, GEMINI_MODEL try: import google.genai as genai try: from google.genai import errors as genai_errors except Exception: genai_errors = None except Exception: genai = None genai_errors = None logging.warning("[normalize_service] google.genai module not found; normalization disabled") try: from google.api_core.exceptions import GoogleAPIError except Exception: GoogleAPIError = Exception gemini_client = None if not genai: logging.warning("[normalize_service] google.genai not available, normalization will be disabled") elif not GEMINI_API_KEY: logging.warning("[normalize_service] GEMINI_API_KEY is not set, normalization will be disabled") else: try: gemini_client = genai.Client(api_key=GEMINI_API_KEY) logging.info(f"[normalize_service] Initialized google.genai client with model={GEMINI_MODEL}") except Exception as e: logging.exception(f"[normalize_service] Failed to init google.genai client: {e}") gemini_client = None async def normalize_text(raw_text: str) -> str: if not raw_text: return raw_text prompt = f""" Bạn là một hệ thống Xử lý Hậu kỳ NLP (NLP Post-Processing) Tiếng Việt. Đầu vào là văn bản thô (raw transcript), có thể thiếu dấu câu, sai chính tả, Lặp từ, lặp cụm từ, hoặc lặp cả đoạn do lỗi nhận dạng giọng nói. Nhiệm vụ: - Sửa lỗi chính tả do ASR. - Thêm dấu câu phù hợp. - Viết hoa đúng chuẩn tiếng Việt (đầu câu, tên riêng nếu suy luận được). - Loại bỏ hoàn toàn các phần bị lặp (từ, cụm từ, câu hoặc đoạn), chỉ giữ MỘT phiên bản hợp lý. - Giữ nguyên nội dung và ý nghĩa gốc, không rút gọn, không thêm thông tin mới. YÊU CẦU ĐẦU RA: - Chỉ trả về văn bản đã chuẩn hóa - KHÔNG JSON, KHÔNG giải thích, KHÔNG markdown, KHÔNG bọc trong ``` hoặc bất kỳ ký tự đặc biệt nào, chỉ trả về plain text thuần túy. Văn bản đầu vào: \"\"\"{raw_text}\"\"\" """ loop = asyncio.get_event_loop() MAX_RETRIES = 3 BASE_DELAY = 1.0 def call(): last_exc = None for attempt in range(1, MAX_RETRIES + 1): try: if gemini_client: resp = gemini_client.models.generate_content( model=GEMINI_MODEL, contents=prompt, ) return resp.text or "" else: model = genai.GenerativeModel(GEMINI_MODEL) if genai else None if model: resp = model.generate_content(prompt) return getattr(resp, "text", "") or "" return "" except Exception as e: last_exc = e msg = str(e) if "503" in msg or "UNAVAILABLE" in msg: if attempt < MAX_RETRIES: delay = BASE_DELAY * (2 ** (attempt - 1)) delay += random.uniform(0, 0.5 * delay) logging.warning( f"[normalize_service] model overloaded " f"(attempt {attempt}/{MAX_RETRIES}), retrying after {delay:.2f}s" ) time.sleep(delay) continue logging.exception( f"[normalize_service] normalize call failed on attempt {attempt}: {e}" ) break if last_exc: raise last_exc return "" try: raw = await loop.run_in_executor(None, call) def is_valid_normalized(text: str, raw: str) -> bool: if not text: return False if len(text) < 50: return False if text.strip() in {".", "…"}: return False # không quá ngắn so với raw if len(text) < 0.3 * len(raw): return False return True if raw: text = raw.strip() # remove markdown text = re.sub(r"^```.*?\n", "", text, flags=re.DOTALL) text = re.sub(r"```$", "", text) # remove meta-text Gemini text = re.sub( r"^the corrected text.*?is as follows:\s*", "", text, flags=re.IGNORECASE | re.DOTALL, ) text = text.strip('"').strip("'").strip() if is_valid_normalized(text, raw_text): return text logging.warning( "[normalize_service] Gemini returned invalid normalized text, falling back" ) except GoogleAPIError as e: logging.error(f"[normalize_service] Gemini API error: {e}") except Exception as e: logging.exception(f"[normalize_service] normalize_text failed: {e}") # ===== fallback: best-effort local normalization ===== try: text = raw_text.strip() text = re.sub(r"\s+", " ", text) if not text or len(text) < 50: return raw_text.strip() if text[-1] not in ".!?": text += "." def cap_sentences(s: str) -> str: parts = re.split(r'([.!?]\s+)', s) out = "" for i in range(0, len(parts), 2): sentence = parts[i].strip() sep = parts[i + 1] if i + 1 < len(parts) else "" if sentence: sentence = sentence[0].upper() + sentence[1:] out += sentence + sep return out return cap_sentences(text) except Exception: return raw_text