Skip to main content
Glama
sumik5
by sumik5
translator.py9.98 kB
"""LM Studio API連携翻訳モジュール(一括翻訳版).""" import asyncio import json import logging import re from typing import Dict, List, Optional from urllib.parse import urljoin import httpx from pydantic import BaseModel from .srt_parser import Subtitle logger = logging.getLogger(__name__) class TranslationError(Exception): """Translation specific error.""" pass class LMStudioAPIError(Exception): """LM Studio API specific error.""" pass class TranslationRequest(BaseModel): """LM Studio API翻訳リクエスト.""" model: str messages: List[Dict[str, str]] temperature: float = 0.3 max_tokens: Optional[int] = None class Translator: """LM Studio APIと連携して字幕翻訳を行うクラス.""" def __init__( self, lm_studio_url: str, model_name: str, request_timeout: float = 300.0 ): """ 翻訳クラスを初期化. Args: lm_studio_url: LM Studio API のベースURL model_name: 使用するモデル名 request_timeout: リクエストタイムアウト(秒)- デフォルト5分 """ self.base_url = lm_studio_url.rstrip('/') # /v1が含まれていない場合は追加 if '/v1' not in self.base_url: self.base_url = self.base_url + '/v1' self.model = model_name self.timeout = httpx.Timeout(request_timeout) self.client = None async def __aenter__(self): """非同期コンテキストマネージャーのエントリー.""" self.client = httpx.AsyncClient(timeout=self.timeout) return self async def __aexit__(self, exc_type, exc_val, exc_tb): """非同期コンテキストマネージャーのエグジット.""" if self.client: await self.client.aclose() async def translate_subtitles(self, subtitles: List[Subtitle]) -> List[Subtitle]: """ 字幕リストを一括で翻訳. Args: subtitles: 翻訳対象の字幕リスト Returns: 翻訳された字幕リスト """ if not subtitles: return [] # 一時的なクライアント作成(コンテキストマネージャー外での使用用) if self.client is None: self.client = httpx.AsyncClient(timeout=self.timeout) close_client = True else: close_client = False try: # SRT形式の文字列を作成 srt_text = self._create_srt_text(subtitles) # プロンプトを構築 prompt = self._build_bulk_translation_prompt(srt_text) # 翻訳実行 logger.info(f"Translating {len(subtitles)} subtitles in bulk...") translated_srt = await self._make_api_request(prompt) # 翻訳結果をパース translated_subtitles = self._parse_translated_srt(translated_srt, subtitles) logger.info(f"Successfully translated {len(translated_subtitles)} subtitles") return translated_subtitles except Exception as e: logger.error(f"Translation failed: {str(e)}") raise TranslationError(f"Failed to translate subtitles: {str(e)}") from e finally: if close_client and self.client: await self.client.aclose() self.client = None def _create_srt_text(self, subtitles: List[Subtitle]) -> str: """字幕リストからSRT形式のテキストを作成.""" srt_lines = [] for subtitle in subtitles: srt_lines.append(str(subtitle.index)) srt_lines.append(f"{subtitle.start_time} --> {subtitle.end_time}") srt_lines.append(subtitle.text) srt_lines.append("") # 空行 return "\n".join(srt_lines) def _build_bulk_translation_prompt(self, srt_text: str) -> str: """一括翻訳用のプロンプトを構築.""" return srt_text def _parse_translated_srt(self, translated_text: str, original_subtitles: List[Subtitle]) -> List[Subtitle]: """ 翻訳されたSRTテキストをパースして字幕オブジェクトのリストに変換. Args: translated_text: 翻訳されたSRTテキスト original_subtitles: 元の字幕リスト(タイミング情報を保持) Returns: 翻訳された字幕のリスト """ # 字幕ブロックを分割 blocks = re.split(r'\n\s*\n', translated_text.strip()) translated_subtitles = [] for i, block in enumerate(blocks): if not block.strip(): continue lines = block.strip().split('\n') if len(lines) < 3: logger.warning(f"Invalid subtitle block at index {i}: {block}") continue try: # 番号をスキップ(1行目) # タイムコードをスキップ(2行目) # 3行目以降がテキスト translated_text_lines = lines[2:] translated_text = '\n'.join(translated_text_lines) # 元の字幕のタイミング情報を使用 if i < len(original_subtitles): original = original_subtitles[i] translated_subtitle = Subtitle( index=original.index, start_time=original.start_time, end_time=original.end_time, text=translated_text ) translated_subtitles.append(translated_subtitle) else: logger.warning(f"No corresponding original subtitle for index {i}") except Exception as e: logger.error(f"Failed to parse subtitle block at index {i}: {e}") # エラーの場合は元のテキストを使用 if i < len(original_subtitles): translated_subtitles.append(original_subtitles[i]) # 翻訳されなかった字幕がある場合は元のテキストを使用 if len(translated_subtitles) < len(original_subtitles): logger.warning(f"Some subtitles were not translated. Using original text for remaining {len(original_subtitles) - len(translated_subtitles)} subtitles") translated_subtitles.extend(original_subtitles[len(translated_subtitles):]) return translated_subtitles async def _make_api_request(self, prompt: str) -> str: """ LM Studio APIにリクエストを送信. Args: prompt: 翻訳プロンプト Returns: 翻訳結果 Raises: LMStudioAPIError: API呼び出しが失敗した場合 """ try: request_data = TranslationRequest( model=self.model, messages=[ { "role": "system", "content": "あなたは映像字幕の翻訳専門家です。SRT形式を正確に維持しながら、自然で読みやすい日本語字幕を作成してください。" }, { "role": "user", "content": prompt } ], temperature=0.3 ) # URLが既に/v1を含む場合は、/chat/completionsのみ追加 if self.base_url.endswith('/v1'): api_url = f"{self.base_url}/chat/completions" else: api_url = urljoin(self.base_url, "chat/completions") response = await self.client.post( api_url, json=request_data.model_dump(), headers={"Content-Type": "application/json"} ) response.raise_for_status() result = response.json() if "error" in result: error_msg = result.get("error", {}).get("message", str(result["error"])) raise LMStudioAPIError(f"API Error: {error_msg}") if "choices" not in result or not result["choices"]: raise LMStudioAPIError("APIレスポンスにchoicesが含まれていません") translated_text = result["choices"][0]["message"]["content"].strip() if not translated_text: raise LMStudioAPIError("翻訳結果が空です") return translated_text except httpx.HTTPStatusError as e: error_msg = f"HTTP Error {e.response.status_code}: {e.response.text}" logger.error(f"API request failed: {error_msg}") raise LMStudioAPIError(error_msg) from e except httpx.RequestError as e: error_msg = f"Request Error: {str(e)}" logger.error(f"API request failed: {error_msg}") raise LMStudioAPIError(error_msg) from e except (KeyError, json.JSONDecodeError) as e: error_msg = f"Response parsing error: {str(e)}" logger.error(f"Failed to parse API response: {error_msg}") raise LMStudioAPIError(error_msg) from e except Exception as e: error_msg = f"Unexpected error: {str(e)}" logger.error(f"Unexpected error during API request: {error_msg}") raise LMStudioAPIError(error_msg) from e

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sumik5/translate-srt-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server