diff --git a/src/co_op_translator/core/llm/markdown_evaluator.py b/src/co_op_translator/core/llm/markdown_evaluator.py index 52a49361..74c2f34d 100644 --- a/src/co_op_translator/core/llm/markdown_evaluator.py +++ b/src/co_op_translator/core/llm/markdown_evaluator.py @@ -324,9 +324,9 @@ async def evaluate_markdown( llm_response ) chunk_result = json.loads(cleaned_response) - chunk_result["chunk_index"] = ( - i # Track which chunk had issues - ) + chunk_result[ + "chunk_index" + ] = i # Track which chunk had issues chunk_evaluations.append(chunk_result) # Log progress diff --git a/src/co_op_translator/core/llm/markdown_translator.py b/src/co_op_translator/core/llm/markdown_translator.py index ef4f9abc..52f7928f 100644 --- a/src/co_op_translator/core/llm/markdown_translator.py +++ b/src/co_op_translator/core/llm/markdown_translator.py @@ -29,6 +29,8 @@ class MarkdownTranslator(ABC): Provides common utilities and abstract methods to be implemented by providers. """ + TRANSLATION_TIMEOUT_SECONDS = 300 # Translation timeout in seconds + def __init__(self, root_dir: Path = None): """Initialize translator with project configuration. @@ -130,7 +132,7 @@ async def translate_markdown( generate_prompt_template(language_code, language_name, chunk, is_rtl) for chunk in document_chunks ] - results = await self._run_prompts_sequentially(prompts) + results = await self._run_prompts_sequentially(prompts, md_file_path) translated_content = "\n".join(results) # Step 4: Restore the code blocks and inline code from placeholders @@ -151,11 +153,12 @@ async def translate_markdown( return updated_content - async def _run_prompts_sequentially(self, prompts): + async def _run_prompts_sequentially(self, prompts, md_file_path): """Execute translation prompts in sequence with timeout protection. Args: prompts: List of translation prompts to process + md_file_path: Path to the markdown file being translated Returns: List of translated text chunks or error messages @@ -164,19 +167,27 @@ async def _run_prompts_sequentially(self, prompts): for index, prompt in enumerate(prompts): try: result = await asyncio.wait_for( - self._run_prompt(prompt, index + 1, len(prompts)), timeout=300 + self._run_prompt(prompt, index + 1, len(prompts)), + timeout=self.TRANSLATION_TIMEOUT_SECONDS, ) results.append(result) except asyncio.TimeoutError: - logger.warning(f"Chunk {index + 1} translation timed out. Skipping...") + logger.warning( + f"Translation timeout for chunk {index + 1} of file '{md_file_path.name}': " + f"Request exceeded {self.TRANSLATION_TIMEOUT_SECONDS} seconds. " + f"Check your network connection and API response time." + ) results.append( - f"Translation for chunk {index + 1} skipped due to timeout." + f"Translation for chunk {index + 1} of '{md_file_path.name}' skipped due to timeout." ) except Exception as e: logger.error( - f"Error during prompt execution for chunk {index + 1}: {e}" + f"Translation failed for chunk {index + 1} of file '{md_file_path.name}': {str(e)}. " + f"Check your API configuration and network connection." + ) + results.append( + f"Error translating chunk {index + 1} of '{md_file_path.name}': {str(e)}" ) - results.append(f"Error during translation of chunk {index + 1}") return results @abstractmethod @@ -233,7 +244,9 @@ def create(cls, root_dir: Path = None) -> "MarkdownTranslator": """ provider = LLMConfig.get_available_provider() if provider is None: - raise ValueError("No valid LLM provider configured") + raise ValueError( + "No valid LLM provider configured. Please check your .env file and ensure AZURE_OPENAI_API_KEY or OPENAI_API_KEY is set." + ) if provider == LLMProvider.AZURE_OPENAI: from co_op_translator.core.llm.providers.azure.markdown_translator import ( @@ -248,4 +261,6 @@ def create(cls, root_dir: Path = None) -> "MarkdownTranslator": return OpenAIMarkdownTranslator(root_dir) else: - raise ValueError(f"Unsupported provider: {provider}") + raise ValueError( + f"Unsupported LLM provider '{provider}'. Supported providers: AZURE_OPENAI, OPENAI. Please check your configuration." + ) diff --git a/src/co_op_translator/core/project/project_evaluator.py b/src/co_op_translator/core/project/project_evaluator.py index c8bb215d..1d93a8df 100644 --- a/src/co_op_translator/core/project/project_evaluator.py +++ b/src/co_op_translator/core/project/project_evaluator.py @@ -160,10 +160,11 @@ async def evaluate_project(self, language_code: str) -> Tuple[int, int, float]: orig_file = file_data["orig_file"] logger.info(f"Evaluating with LLM: {trans_file}") - evaluation_result, success = ( - await self.markdown_evaluator.evaluate_markdown( - orig_file, trans_file, language_code - ) + ( + evaluation_result, + success, + ) = await self.markdown_evaluator.evaluate_markdown( + orig_file, trans_file, language_code ) if success and evaluation_result: @@ -213,10 +214,11 @@ async def evaluate_project(self, language_code: str) -> Tuple[int, int, float]: for orig_file, trans_file in translation_pairs: logger.info(f"Evaluating: {trans_file}") - evaluation_result, success = ( - await self.markdown_evaluator.evaluate_markdown( - orig_file, trans_file, language_code - ) + ( + evaluation_result, + success, + ) = await self.markdown_evaluator.evaluate_markdown( + orig_file, trans_file, language_code ) if success and evaluation_result: @@ -265,10 +267,11 @@ async def evaluate_project(self, language_code: str) -> Tuple[int, int, float]: for orig_file, trans_file in translation_pairs: logger.info(f"Evaluating: {trans_file}") - evaluation_result, success = ( - await self.markdown_evaluator.evaluate_markdown( - orig_file, trans_file, language_code - ) + ( + evaluation_result, + success, + ) = await self.markdown_evaluator.evaluate_markdown( + orig_file, trans_file, language_code ) if success and evaluation_result: diff --git a/src/co_op_translator/core/project/project_translator.py b/src/co_op_translator/core/project/project_translator.py index 92b10e60..29135fa0 100644 --- a/src/co_op_translator/core/project/project_translator.py +++ b/src/co_op_translator/core/project/project_translator.py @@ -53,12 +53,12 @@ def __init__(self, language_codes, root_dir=".", markdown_only=False): ) else: logger.info( - "Skipping image translator initialization in markdown-only mode" + f"Running in markdown-only mode for project '{self.root_dir.name}': Image translation disabled. Only .md files will be processed." ) self.image_translator = None except ValueError as e: logger.info( - "Switching to markdown-only mode due to missing Computer Vision configuration" + f"Computer Vision not configured for project '{self.root_dir.name}' - switching to markdown-only mode. Only .md files will be translated. To enable image translation, configure AZURE_COMPUTER_VISION_KEY in your .env file." ) self.markdown_only = True # Auto-switch to markdown-only mode self.image_translator = None @@ -117,7 +117,9 @@ async def check_and_retry_translations(self): modified_count, errors = await self.translation_manager.check_outdated_files() logger.info(f"Found {modified_count} outdated files") if errors: - logger.warning(f"Errors during checking outdated files: {errors}") + logger.warning( + f"Failed to check {len(errors)} outdated files in project '{self.root_dir.name}': Common causes include file permission issues, corrupted metadata, or missing source files." + ) # Translate all markdown files ( @@ -126,7 +128,9 @@ async def check_and_retry_translations(self): ) = await self.translation_manager.translate_all_markdown_files() logger.info(f"Translated {markdown_count} markdown files") if markdown_errors: - logger.warning(f"Errors during markdown translation: {markdown_errors}") + logger.warning( + f"Failed to translate {len(markdown_errors)} markdown files in project '{self.root_dir.name}': Check your API configuration and network connection." + ) # Translate images if image translator is available ( @@ -135,7 +139,9 @@ async def check_and_retry_translations(self): ) = await self.translation_manager.translate_all_image_files() logger.info(f"Translated {image_count} image files") if image_errors: - logger.warning(f"Errors during image translation: {image_errors}") + logger.warning( + f"Failed to translate {len(image_errors)} image files in project '{self.root_dir.name}': Verify Computer Vision API configuration and image file permissions." + ) return ( modified_count + markdown_count + image_count, @@ -206,15 +212,15 @@ async def retranslate_low_confidence_files( if orig_file.exists(): files_to_retranslate.append((orig_file, confidence)) else: - error_msg = f"Original file not found: {orig_file}" + error_msg = f"Original source file not found for translation '{trans_file.name}': Expected file '{orig_file}' does not exist. The source may have been moved or deleted." logger.warning(error_msg) errors.append(error_msg) else: - error_msg = f"No metadata or source_file in {trans_file}" + error_msg = f"Invalid translation metadata in file '{trans_file.name}': Missing source_file information. The file may be corrupted or not generated by Co-op Translator." logger.warning(error_msg) errors.append(error_msg) except Exception as e: - error_msg = f"Failed to process file {trans_file}: {e}" + error_msg = f"Failed to read translation file '{trans_file.name}': {str(e)}. Check file permissions and encoding." logger.error(error_msg) errors.append(error_msg) @@ -250,12 +256,12 @@ async def translate_task(file=orig_file, conf=confidence): ) return True else: - error_msg = f"Failed to retranslate {file}" + error_msg = f"Failed to retranslate file '{file.name}': Translation request returned no result. Check your API configuration and try again." logger.error(error_msg) errors.append(error_msg) return False except Exception as e: - error_msg = f"Error retranslating {file}: {str(e)}" + error_msg = f"Error retranslating file '{file.name}': {str(e)}. Verify API connectivity and file accessibility." logger.error(error_msg) errors.append(error_msg) return False diff --git a/src/co_op_translator/core/vision/image_translator.py b/src/co_op_translator/core/vision/image_translator.py index f12fbfc0..e162db4f 100644 --- a/src/co_op_translator/core/vision/image_translator.py +++ b/src/co_op_translator/core/vision/image_translator.py @@ -108,7 +108,11 @@ def extract_line_bounding_boxes(self, image_path): ) return line_bounding_boxes else: - raise Exception("No text was recognized in the image.") + raise Exception( + f"No text detected in image '{Path(image_path).name}': " + f"The image may not contain clear, high-contrast text, or the text quality is too poor for recognition. " + f"Please ensure the image contains readable text." + ) def plot_annotated_image( self, @@ -201,7 +205,10 @@ def plot_annotated_image( try: base_font = ImageFont.truetype(font_path, font_size) except IOError: - logger.error(f"Font file not found at {font_path}. Using default font.") + logger.error( + f"Font file not found for language '{target_language_code}' at '{font_path}' in image '{Path(image_path).name}': " + f"Using default font. Install the required font or check font configuration." + ) base_font = ImageFont.load_default() iterator = zip(grouped_boxes, grouped_translations) @@ -225,7 +232,9 @@ def plot_annotated_image( bounding_box_flat = line_info.get("bounding_box", []) if len(bounding_box_flat) != 8: logger.error( - f"Invalid bounding_box length: {bounding_box_flat}" + f"Invalid text detection data in image '{Path(image_path).name}': " + f"Expected 8 coordinates but got {len(bounding_box_flat)}. " + f"The text detection may be corrupted." ) continue @@ -233,7 +242,10 @@ def plot_annotated_image( zip(bounding_box_flat[::2], bounding_box_flat[1::2]) ) if len(bounding_box_tuples) < 4: - logger.error("Bounding box does not have enough points.") + logger.error( + f"Insufficient bounding box points in image '{Path(image_path).name}': " + f"Text detection data is incomplete. Try re-processing the image." + ) continue try: @@ -243,7 +255,10 @@ def plot_annotated_image( angle = math.degrees(math.atan2(p1[1] - p0[1], p1[0] - p0[0])) angle = -angle # Invert angle for proper rotation. except ValueError: - logger.error("Invalid bounding_box points for quadrilateral.") + logger.error( + f"Invalid bounding box coordinates in image '{Path(image_path).name}': " + f"Text detection geometry is malformed. The image may have distorted text regions." + ) continue bg_color, _ = get_dominant_color(image, bounding_box_flat) @@ -280,7 +295,8 @@ def plot_annotated_image( font = ImageFont.truetype(font_path, optimal_font_size) except IOError: logger.error( - f"Font file not found at {font_path}. Using default font." + f"Font file not found for language '{target_language_code}' at '{font_path}' in image '{Path(image_path).name}': " + f"Using default font. Install the required font or check font configuration." ) font = ImageFont.load_default() @@ -344,7 +360,10 @@ def plot_annotated_image( try: font = ImageFont.truetype(font_path, font_size) except IOError: - logger.error(f"Font file not found at {font_path}. Using default font.") + logger.error( + f"Font file not found for language '{target_language_code}' at '{font_path}' in image '{Path(image_path).name}': " + f"Using default font. Install the required font or check font configuration." + ) font = ImageFont.load_default() iterator = zip(grouped_boxes, grouped_translations) @@ -451,7 +470,9 @@ def translate_image( # Check if any text was recognized if not line_bounding_boxes: logger.info( - f"No text was recognized in the image: {image_path}. Saving the original image as the translated image." + f"No text detected in image '{image_path.name}': " + f"Saving original image as translation result. " + f"The image may not contain readable text or text may be too small/blurry to detect." ) # Load the original image and save it with the new name @@ -482,7 +503,8 @@ def translate_image( except Exception as e: logger.error( - f"Failed to translate image {image_path} due to an error: {e}. Saving the original image instead." + f"Failed to translate image '{image_path.name}': {str(e)}. " + f"Saving original image instead." ) # Load the original image and save it with the new name @@ -533,5 +555,7 @@ def create( except (ImportError, ValueError) as e: logger.warning(f"Computer Vision is not properly configured: {e}") raise ValueError( - "Computer Vision environment variables are not properly configured" + "Image translation is not configured: Missing required environment variables " + "(AZURE_AI_SERVICE_API_KEY, AZURE_AI_SERVICE_ENDPOINT). " + "Please check your .env file and ensure your Azure AI service credentials are set correctly." )