Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
10a3c5e
Core: Improve error messages in MarkdownTranslator
DongilMin Jun 29, 2025
87e203a
Core: Improve error messages in ProjectTranslator
DongilMin Jun 29, 2025
54327b6
Core: Improve error messages in ImageTranslator
DongilMin Jun 29, 2025
6e33ad5
Add comprehensive error message tests
DongilMin Jun 29, 2025
468d165
Apply code formatting with Black
DongilMin Jun 29, 2025
be1ddc3
chore(test): update file-level docstring in test_error_messages.py
DongilMin Jun 29, 2025
4728fb3
Fix reviewer feedback: timeout constant, simplified error message, te…
DongilMin Jul 4, 2025
8a6306a
Remove test_environment_variable_references_in_logs method
DongilMin Jul 5, 2025
82f45ff
Remove log assertion logic from error message tests
DongilMin Jul 5, 2025
235fc05
Remove test files per reviewer feedback
DongilMin Jul 9, 2025
0406c6e
Restore original test files in project module
DongilMin Jul 9, 2025
919b511
Revert Black formatting for unmodified evaluator files
DongilMin Jul 9, 2025
2fda0a9
Reapply Black formatting to evaluator files
DongilMin Jul 9, 2025
71e68ab
Restore test_project_translator.py to pre-82f45ff state as requested
DongilMin Jul 9, 2025
fd195da
Revert log assertion removal commit, then clean up test_project_trans…
DongilMin Jul 9, 2025
90c41d0
Remove TestProjectTranslatorErrorMessages class entirely as request
DongilMin Jul 9, 2025
36e3699
Add missing newline at end of file
DongilMin Jul 9, 2025
83060d2
Update tests/co_op_translator/core/project/test_project_translator.py
skytin1004 Jul 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/co_op_translator/core/llm/markdown_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 24 additions & 9 deletions src/co_op_translator/core/llm/markdown_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand All @@ -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."
)
27 changes: 15 additions & 12 deletions src/co_op_translator/core/project/project_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
26 changes: 16 additions & 10 deletions src/co_op_translator/core/project/project_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
(
Expand All @@ -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
(
Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
44 changes: 34 additions & 10 deletions src/co_op_translator/core/vision/image_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -225,15 +232,20 @@ 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

bounding_box_tuples = list(
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:
Expand All @@ -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)
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."
)