polytext 0.2.8b6__tar.gz → 0.2.8b7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. {polytext-0.2.8b6 → polytext-0.2.8b7}/PKG-INFO +1 -1
  2. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/audio_to_text.py +37 -1
  3. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/base.py +8 -3
  4. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext.egg-info/PKG-INFO +1 -1
  5. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext.egg-info/SOURCES.txt +1 -0
  6. {polytext-0.2.8b6 → polytext-0.2.8b7}/setup.py +1 -1
  7. polytext-0.2.8b7/tests/test_audio_chunk_quality.py +76 -0
  8. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_base_loader_error_mapping.py +19 -0
  9. {polytext-0.2.8b6 → polytext-0.2.8b7}/LICENSE +0 -0
  10. {polytext-0.2.8b6 → polytext-0.2.8b7}/README.md +0 -0
  11. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/__init__.py +0 -0
  12. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/__init__.py +0 -0
  13. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/base.py +0 -0
  14. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/beautiful_text.py +0 -0
  15. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/document_ocr_to_text.py +0 -0
  16. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/document_ocr_to_text_azure_oai.py +0 -0
  17. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/gemini_quality_guards.py +0 -0
  18. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/html_to_md.py +0 -0
  19. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/md_to_text.py +0 -0
  20. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/ocr_to_text.py +0 -0
  21. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/ocr_to_text_azure_oai.py +0 -0
  22. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/pdf.py +0 -0
  23. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/text_to_md.py +0 -0
  24. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/converter/video_to_audio.py +0 -0
  25. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/exceptions/__init__.py +0 -0
  26. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/exceptions/base.py +0 -0
  27. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/generator/__init__.py +0 -0
  28. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/generator/pdf.py +0 -0
  29. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/__init__.py +0 -0
  30. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/audio.py +0 -0
  31. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/aws_auth.py +0 -0
  32. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/document.py +0 -0
  33. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/document_ocr.py +0 -0
  34. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/downloader/__init__.py +0 -0
  35. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/downloader/downloader.py +0 -0
  36. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/html.py +0 -0
  37. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/markdown.py +0 -0
  38. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/notebook.py +0 -0
  39. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/ocr.py +0 -0
  40. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/plain_text.py +0 -0
  41. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/video.py +0 -0
  42. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/xml_xbrl.py +0 -0
  43. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/youtube.py +0 -0
  44. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/loader/youtube_llm.py +0 -0
  45. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/processor/__init__.py +0 -0
  46. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/processor/audio_chunker.py +0 -0
  47. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/processor/text_merger.py +0 -0
  48. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/processor/transcript_chunker.py +0 -0
  49. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/prompts/__init__.py +0 -0
  50. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/prompts/beautiful_text.py +0 -0
  51. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/prompts/ocr.py +0 -0
  52. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/prompts/text_merging.py +0 -0
  53. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/prompts/text_to_md.py +0 -0
  54. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/prompts/transcription.py +0 -0
  55. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/utils/__init__.py +0 -0
  56. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext/utils/utils.py +0 -0
  57. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext.egg-info/dependency_links.txt +0 -0
  58. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext.egg-info/not-zip-safe +0 -0
  59. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext.egg-info/requires.txt +0 -0
  60. {polytext-0.2.8b6 → polytext-0.2.8b7}/polytext.egg-info/top_level.txt +0 -0
  61. {polytext-0.2.8b6 → polytext-0.2.8b7}/pyproject.toml +0 -0
  62. {polytext-0.2.8b6 → polytext-0.2.8b7}/setup.cfg +0 -0
  63. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_audio_chunker.py +0 -0
  64. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_audio_comparison_helpers.py +0 -0
  65. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_audio_transcription_model_migration.py +0 -0
  66. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_aws_auth.py +0 -0
  67. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_beautiful_text_manual.py +0 -0
  68. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_compare_audio_models.py +0 -0
  69. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_compare_document_ocr_to_text_models.py +0 -0
  70. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_compare_ocr_to_text_models.py +0 -0
  71. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_compare_youtube_models.py +0 -0
  72. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_dowload_audio_from_youtube.py +0 -0
  73. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_dowload_audio_from_youtube_helpers.py +0 -0
  74. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_extracted_text_whitespace.py +0 -0
  75. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_gemini_quality_guards.py +0 -0
  76. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_get_audio_transcript_from_gcs.py +0 -0
  77. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_get_customized_pdf_from_markdown.py +0 -0
  78. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_get_document_ocr.py +0 -0
  79. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_get_document_ocr_azure_oai.py +0 -0
  80. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_get_document_text.py +0 -0
  81. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_get_document_text_from_gcs.py +0 -0
  82. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_get_ocr_from_image.py +0 -0
  83. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_get_text_from_markdown.py +0 -0
  84. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_get_video_transcript_from_gcs.py +0 -0
  85. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_library.py +0 -0
  86. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_markdown_loader_gzip.py +0 -0
  87. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_markitdown_html.py +0 -0
  88. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_notebook_loader.py +0 -0
  89. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_ocr_fallbacks.py +0 -0
  90. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_ocr_image_descriptions.py +0 -0
  91. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_pain_text.py +0 -0
  92. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_pdf_conversion_error.py +0 -0
  93. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_python_version_metadata.py +0 -0
  94. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_split_audio_with_llm.py +0 -0
  95. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_transcribe_s3_images_from_csv.py +0 -0
  96. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_transcribe_s3_images_from_csv_script.py +0 -0
  97. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_xml_xbrl_loader.py +0 -0
  98. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_youtube_gemini_minimal_check.py +0 -0
  99. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_youtube_llm_fallbacks.py +0 -0
  100. {polytext-0.2.8b6 → polytext-0.2.8b7}/tests/test_youtube_transcript.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polytext
3
- Version: 0.2.8b6
3
+ Version: 0.2.8b7
4
4
  Summary: Python utilities to simplify document files management
5
5
  Home-page: https://github.com/docsity/polytext
6
6
  Author: Matteo Senardi
@@ -63,6 +63,8 @@ AUDIO_DEFAULT_MAX_OUTPUT_TOKENS = 4096
63
63
  AUDIO_MAX_ADAPTIVE_SPLIT_DEPTH = 1
64
64
  AUDIO_ADAPTIVE_SPLIT_OVERLAP_MS = 2000
65
65
  AUDIO_LONG_DURATION_THRESHOLD_MS = 80 * 60 * 1000
66
+ AUDIO_LOW_TEXT_THRESHOLD_WORDS_PER_MINUTE = 20
67
+ AUDIO_LOW_TEXT_MIN_DURATION_MS = 60_000
66
68
  AUDIO_TAIL_REPETITION_LINES = int(os.getenv("AUDIO_TAIL_REPETITION_LINES", "200"))
67
69
  AUDIO_TAIL_REPETITION_THRESHOLD = float(os.getenv("AUDIO_TAIL_REPETITION_THRESHOLD", "0.35"))
68
70
  AUDIO_FALLBACK_SOURCE_PATTERN = os.getenv("AUDIO_FALLBACK_SOURCE_PATTERN", "flash-lite")
@@ -284,6 +286,37 @@ def normalize_no_human_speech_marker(text: str) -> tuple[str, bool]:
284
286
  return cleaned_text, False
285
287
 
286
288
 
289
+ def summarize_audio_chunk_quality(chunks: list[dict], transcripts: list[str]) -> dict:
290
+ """Report empty and unusually short transcripts without discarding any text.
291
+
292
+ Indices are one-based. Short audio chunks are excluded from the low-text
293
+ count because a brief spoken phrase can be a complete transcription.
294
+ """
295
+ empty_indices = []
296
+ low_text_indices = []
297
+ for index, (chunk, transcript) in enumerate(zip(chunks, transcripts), start=1):
298
+ word_count = len(transcript.split())
299
+ if word_count == 0:
300
+ empty_indices.append(index)
301
+ else:
302
+ duration_ms = chunk.get("duration_ms", 0)
303
+ if (
304
+ duration_ms >= AUDIO_LOW_TEXT_MIN_DURATION_MS
305
+ and word_count * 60_000 < AUDIO_LOW_TEXT_THRESHOLD_WORDS_PER_MINUTE * duration_ms
306
+ ):
307
+ low_text_indices.append(index)
308
+
309
+ return {
310
+ "total_chunks": len(chunks),
311
+ "empty_chunks": len(empty_indices),
312
+ "low_text_chunks": len(low_text_indices),
313
+ "empty_chunk_indices": empty_indices,
314
+ "low_text_chunk_indices": low_text_indices,
315
+ "low_text_threshold_words_per_minute": AUDIO_LOW_TEXT_THRESHOLD_WORDS_PER_MINUTE,
316
+ "low_text_min_duration_ms": AUDIO_LOW_TEXT_MIN_DURATION_MS,
317
+ }
318
+
319
+
287
320
  def add_line_break_after_each_sentence(text: str) -> str:
288
321
  if not text:
289
322
  return text
@@ -1030,6 +1063,8 @@ class AudioToTextConverter:
1030
1063
  - prompt_tokens (int): Total number of prompt tokens used
1031
1064
  - completion_model (str): Name of the transcription model used
1032
1065
  - completion_model_provider (str): Provider of the transcription model
1066
+ - audio_chunk_quality (dict): Counts of empty and low-text chunks,
1067
+ with one-based indices and the applied words-per-minute threshold
1033
1068
 
1034
1069
  Raises:
1035
1070
  ValueError: If the audio file format is not recognized
@@ -1121,7 +1156,8 @@ class AudioToTextConverter:
1121
1156
  "completion_tokens": completion_tokens + full_text_merged_dict["completion_tokens"],
1122
1157
  "prompt_tokens": prompt_tokens + full_text_merged_dict["prompt_tokens"],
1123
1158
  "completion_model": self.transcription_model,
1124
- "completion_model_provider": self.transcription_model_provider
1159
+ "completion_model_provider": self.transcription_model_provider,
1160
+ "audio_chunk_quality": summarize_audio_chunk_quality(chunks, transcript_chunks),
1125
1161
  }
1126
1162
  if len(chunk_results) == 1 and chunk_results[0]:
1127
1163
  for key in (
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
42
42
 
43
43
  MIN_DOC_TEXT_LENGTH_ACCEPTED = int(os.getenv("MIN_DOC_TEXT_LENGTH_ACCEPTED", "400"))
44
44
  OCR_INCLUDE_IMAGE_DESCRIPTIONS_ENV = "OCR_INCLUDE_IMAGE_DESCRIPTIONS"
45
+ FALLBACK_OCR_ENV = "FALLBACK_OCR"
45
46
  LLM_OUTPUT_ERROR_CODES = {
46
47
  995: "INVALID_ARGUMENT",
47
48
  996: "RECITATION",
@@ -119,7 +120,7 @@ class BaseLoader:
119
120
  - target_size (int, optional): Target file size in bytes. Defaults to 1MB
120
121
  - source (str): Source of the document. Must be either "cloud" or "local"
121
122
  - fallback_ocr (bool, optional): If True, Optical Character Recognition (OCR) will be used as a fallback
122
- if direct text extraction fails, particularly for image-based content. Defaults to True.
123
+ if direct text extraction fails. When omitted, defaults from FALLBACK_OCR and otherwise to False.
123
124
  - save_transcript_chunks (bool, optional): Whether to save chunk transcripts in final output. Defaults to False.
124
125
  - bitrate_quality (int, optional): Variable bitrate quality from 0-9 (9 being lowest). Defaults to 9
125
126
 
@@ -143,7 +144,10 @@ class BaseLoader:
143
144
  self.kwargs = kwargs
144
145
  self.target_size = kwargs.get("target_size", 1)
145
146
  self.source = kwargs.get("source", "cloud")
146
- self.fallback_ocr = kwargs.get("fallback_ocr", False)
147
+ self.fallback_ocr = kwargs.get(
148
+ "fallback_ocr",
149
+ _read_bool_env(FALLBACK_OCR_ENV),
150
+ )
147
151
  self.save_transcript_chunks = kwargs.get("save_transcript_chunks", False)
148
152
  self.bitrate_quality = kwargs.get("bitrate_quality", 9)
149
153
  self.aws_auth_mode = kwargs.get("aws_auth_mode")
@@ -548,6 +552,7 @@ class BaseLoader:
548
552
  - completion_model (str): Model name used, from the first processed source.
549
553
  - completion_model_provider (str): Provider of the model, from the first processed source.
550
554
  - text_chunks (list, optional): List of text chunks, if chunking was applied.
555
+ - audio_chunk_quality (dict, optional): Counts of empty and low-text audio chunks.
551
556
  - type (str): Type of the processed source (e.g., ocr, video, audio, text).
552
557
  - input (str): The input path or URL of the first processed source.
553
558
  - output_list (list): List of individual extraction results (one per input).
@@ -608,7 +613,7 @@ class BaseLoader:
608
613
  "output_list": [result_dict],
609
614
  }
610
615
 
611
- for metadata_key in ("ocr_failed_pages", "ocr_failed_pages_detail"):
616
+ for metadata_key in ("ocr_failed_pages", "ocr_failed_pages_detail", "audio_chunk_quality"):
612
617
  if metadata_key in result_dict:
613
618
  final_result[metadata_key] = result_dict[metadata_key]
614
619
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polytext
3
- Version: 0.2.8b6
3
+ Version: 0.2.8b7
4
4
  Summary: Python utilities to simplify document files management
5
5
  Home-page: https://github.com/docsity/polytext
6
6
  Author: Matteo Senardi
@@ -56,6 +56,7 @@ polytext/prompts/text_to_md.py
56
56
  polytext/prompts/transcription.py
57
57
  polytext/utils/__init__.py
58
58
  polytext/utils/utils.py
59
+ tests/test_audio_chunk_quality.py
59
60
  tests/test_audio_chunker.py
60
61
  tests/test_audio_comparison_helpers.py
61
62
  tests/test_audio_transcription_model_migration.py
@@ -51,7 +51,7 @@ def get_requirements(*requirements_file):
51
51
 
52
52
  setup(
53
53
  name='polytext',
54
- version='0.2.8b6',
54
+ version='0.2.8b7',
55
55
  url='https://github.com/docsity/polytext',
56
56
  # download_url='https://github.com/pualien/py-polytext/archive/0.1.23.tar.gz',
57
57
  license='MIT',
@@ -0,0 +1,76 @@
1
+ import tempfile
2
+ import unittest
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from polytext.converter import audio_to_text
6
+ from polytext.loader import BaseLoader
7
+
8
+
9
+ class AudioChunkQualityTests(unittest.TestCase):
10
+ def test_counts_empty_and_low_text_chunks_separately(self):
11
+ chunks = [
12
+ {"duration_ms": 120_000},
13
+ {"duration_ms": 120_000},
14
+ {"duration_ms": 120_000},
15
+ {"duration_ms": 20_000},
16
+ {"duration_ms": 1_200_000},
17
+ ]
18
+ transcripts = ["parola " * 40, " \n", "parola " * 12, "ciao", "parola " * 100]
19
+
20
+ quality = audio_to_text.summarize_audio_chunk_quality(chunks, transcripts)
21
+
22
+ self.assertEqual(quality, {
23
+ "total_chunks": 5,
24
+ "empty_chunks": 1,
25
+ "low_text_chunks": 2,
26
+ "empty_chunk_indices": [2],
27
+ "low_text_chunk_indices": [3, 5],
28
+ "low_text_threshold_words_per_minute": 20,
29
+ "low_text_min_duration_ms": 60_000,
30
+ })
31
+
32
+ @patch("polytext.converter.audio_to_text.TextMerger")
33
+ @patch("polytext.converter.audio_to_text.AudioChunker")
34
+ @patch.object(audio_to_text.AudioToTextConverter, "process_chunk")
35
+ def test_quality_is_returned_without_saving_chunk_transcripts(
36
+ self, mock_process_chunk, mock_chunker_cls, mock_merger_cls
37
+ ):
38
+ chunker = mock_chunker_cls.return_value
39
+ chunker.duration_ms = 240_000
40
+ chunker.extract_chunks.return_value = [
41
+ {"file_path": "/tmp/chunk1.mp3", "duration_ms": 120_000},
42
+ {"file_path": "/tmp/chunk2.mp3", "duration_ms": 120_000},
43
+ ]
44
+ mock_process_chunk.side_effect = lambda chunk, index: (
45
+ index,
46
+ {"transcript": "parola " * 40 if index == 0 else "", "completion_tokens": 4, "prompt_tokens": 5},
47
+ )
48
+ mock_merger_cls.return_value.merge_chunks_with_llm_sequential.return_value = {
49
+ "full_text_merged": "testo finale", "completion_tokens": 0, "prompt_tokens": 0
50
+ }
51
+
52
+ with tempfile.NamedTemporaryFile(suffix=".mp3") as source_audio:
53
+ converter = audio_to_text.AudioToTextConverter()
54
+ result = converter.transcribe_full_audio(source_audio.name)
55
+
56
+ self.assertEqual(result["text"], "testo finale")
57
+ self.assertEqual(result["audio_chunk_quality"]["total_chunks"], 2)
58
+ self.assertEqual(result["audio_chunk_quality"]["empty_chunks"], 1)
59
+ self.assertNotIn("text_chunks", result)
60
+
61
+ def test_quality_reaches_top_level_loader_response(self):
62
+ quality = {"total_chunks": 2, "empty_chunks": 1, "low_text_chunks": 0}
63
+ file_loader = MagicMock()
64
+ file_loader.load.return_value = {
65
+ "text": "trascrizione", "completion_tokens": 3, "prompt_tokens": 4,
66
+ "audio_chunk_quality": quality,
67
+ }
68
+
69
+ response = BaseLoader().run_loader_class(file_loader, ["audio.mp3"])
70
+
71
+ self.assertEqual(response["audio_chunk_quality"], quality)
72
+ self.assertEqual(response["output_list"][0]["audio_chunk_quality"], quality)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ unittest.main()
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import unittest
2
3
  from types import SimpleNamespace
3
4
  from unittest.mock import Mock, patch
@@ -60,6 +61,24 @@ class _BeautifulTextLoader(BaseLoader):
60
61
 
61
62
 
62
63
  class TestBaseLoaderErrorMapping(unittest.TestCase):
64
+ def test_document_ocr_fallback_is_enabled_by_environment(self):
65
+ with patch.dict(os.environ, {"FALLBACK_OCR": "true"}, clear=True):
66
+ loader = BaseLoader()
67
+
68
+ self.assertTrue(loader.fallback_ocr)
69
+
70
+ def test_explicit_document_ocr_fallback_setting_overrides_environment(self):
71
+ with patch.dict(os.environ, {"FALLBACK_OCR": "true"}, clear=True):
72
+ loader = BaseLoader(fallback_ocr=False)
73
+
74
+ self.assertFalse(loader.fallback_ocr)
75
+
76
+ def test_document_ocr_fallback_remains_disabled_without_configuration(self):
77
+ with patch.dict(os.environ, {}, clear=True):
78
+ loader = BaseLoader()
79
+
80
+ self.assertFalse(loader.fallback_ocr)
81
+
63
82
  def test_llm_output_empty_document_codes_are_raised_as_loader_errors(self):
64
83
  cases = [
65
84
  (995, "INVALID_ARGUMENT"),
File without changes
File without changes
File without changes
File without changes