contextweave 0.2.0__py3-none-any.whl

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 (306) hide show
  1. context_aware_translation/AGENTS.md +134 -0
  2. context_aware_translation/__init__.py +114 -0
  3. context_aware_translation/adapters/__init__.py +1 -0
  4. context_aware_translation/adapters/files/__init__.py +3 -0
  5. context_aware_translation/adapters/files/glossary_io.py +203 -0
  6. context_aware_translation/adapters/qt/__init__.py +1 -0
  7. context_aware_translation/adapters/qt/application_event_bridge.py +78 -0
  8. context_aware_translation/adapters/qt/task_engine.py +392 -0
  9. context_aware_translation/adapters/qt/workers/__init__.py +1 -0
  10. context_aware_translation/adapters/qt/workers/base_worker.py +61 -0
  11. context_aware_translation/adapters/qt/workers/batch_task_overlap_guard.py +57 -0
  12. context_aware_translation/adapters/qt/workers/batch_translation_task_worker.py +228 -0
  13. context_aware_translation/adapters/qt/workers/chunk_retranslation_task_worker.py +136 -0
  14. context_aware_translation/adapters/qt/workers/export_worker.py +75 -0
  15. context_aware_translation/adapters/qt/workers/glossary_export_task_worker.py +119 -0
  16. context_aware_translation/adapters/qt/workers/glossary_extraction_task_worker.py +102 -0
  17. context_aware_translation/adapters/qt/workers/glossary_review_task_worker.py +98 -0
  18. context_aware_translation/adapters/qt/workers/glossary_translation_task_worker.py +97 -0
  19. context_aware_translation/adapters/qt/workers/image_reembedding_task_worker.py +114 -0
  20. context_aware_translation/adapters/qt/workers/import_worker.py +49 -0
  21. context_aware_translation/adapters/qt/workers/ocr_task_worker.py +149 -0
  22. context_aware_translation/adapters/qt/workers/operation_tracker.py +94 -0
  23. context_aware_translation/adapters/qt/workers/translate_and_export_task_worker.py +394 -0
  24. context_aware_translation/adapters/qt/workers/translation_manga_task_worker.py +133 -0
  25. context_aware_translation/adapters/qt/workers/translation_text_task_worker.py +158 -0
  26. context_aware_translation/app_identity.py +33 -0
  27. context_aware_translation/application/__init__.py +7 -0
  28. context_aware_translation/application/composition.py +104 -0
  29. context_aware_translation/application/contracts/__init__.py +57 -0
  30. context_aware_translation/application/contracts/app_setup.py +188 -0
  31. context_aware_translation/application/contracts/common.py +193 -0
  32. context_aware_translation/application/contracts/document.py +235 -0
  33. context_aware_translation/application/contracts/project_setup.py +19 -0
  34. context_aware_translation/application/contracts/projects.py +36 -0
  35. context_aware_translation/application/contracts/queue.py +35 -0
  36. context_aware_translation/application/contracts/terms.py +148 -0
  37. context_aware_translation/application/contracts/work.py +111 -0
  38. context_aware_translation/application/errors.py +35 -0
  39. context_aware_translation/application/events.py +146 -0
  40. context_aware_translation/application/runtime.py +1302 -0
  41. context_aware_translation/application/services/__init__.py +19 -0
  42. context_aware_translation/application/services/_export_support.py +281 -0
  43. context_aware_translation/application/services/app_setup.py +532 -0
  44. context_aware_translation/application/services/document.py +2309 -0
  45. context_aware_translation/application/services/project_setup.py +161 -0
  46. context_aware_translation/application/services/projects.py +152 -0
  47. context_aware_translation/application/services/queue.py +118 -0
  48. context_aware_translation/application/services/terms.py +582 -0
  49. context_aware_translation/application/services/work.py +525 -0
  50. context_aware_translation/cli/__init__.py +1 -0
  51. context_aware_translation/cli/config_file.py +337 -0
  52. context_aware_translation/cli/main.py +362 -0
  53. context_aware_translation/cli/output.py +91 -0
  54. context_aware_translation/cli/runtime.py +26 -0
  55. context_aware_translation/cli/wait.py +51 -0
  56. context_aware_translation/config.py +1354 -0
  57. context_aware_translation/core/AGENTS.md +133 -0
  58. context_aware_translation/core/__init__.py +1 -0
  59. context_aware_translation/core/cancellation.py +17 -0
  60. context_aware_translation/core/context_extractor.py +68 -0
  61. context_aware_translation/core/context_manager.py +2257 -0
  62. context_aware_translation/core/manga_document_handler.py +253 -0
  63. context_aware_translation/core/models.py +211 -0
  64. context_aware_translation/core/progress.py +42 -0
  65. context_aware_translation/core/term_memory.py +14 -0
  66. context_aware_translation/core/term_memory_builder.py +246 -0
  67. context_aware_translation/core/translation_strategies.py +202 -0
  68. context_aware_translation/documents/AGENTS.md +139 -0
  69. context_aware_translation/documents/__init__.py +0 -0
  70. context_aware_translation/documents/base.py +377 -0
  71. context_aware_translation/documents/content/AGENTS.md +162 -0
  72. context_aware_translation/documents/content/__init__.py +0 -0
  73. context_aware_translation/documents/content/ocr_content.py +235 -0
  74. context_aware_translation/documents/content/ocr_items.py +940 -0
  75. context_aware_translation/documents/epub.py +2669 -0
  76. context_aware_translation/documents/epub_container.py +23 -0
  77. context_aware_translation/documents/epub_support/AGENTS.md +183 -0
  78. context_aware_translation/documents/epub_support/__init__.py +1 -0
  79. context_aware_translation/documents/epub_support/container_model.py +75 -0
  80. context_aware_translation/documents/epub_support/container_patch.py +43 -0
  81. context_aware_translation/documents/epub_support/container_reader.py +579 -0
  82. context_aware_translation/documents/epub_support/container_shared.py +109 -0
  83. context_aware_translation/documents/epub_support/container_writer.py +413 -0
  84. context_aware_translation/documents/epub_support/inline_markers.py +285 -0
  85. context_aware_translation/documents/epub_support/nav_ops.py +384 -0
  86. context_aware_translation/documents/epub_support/slot_lines.py +61 -0
  87. context_aware_translation/documents/epub_support/xml_utils.py +38 -0
  88. context_aware_translation/documents/epub_xhtml_utils.py +1457 -0
  89. context_aware_translation/documents/manga.py +695 -0
  90. context_aware_translation/documents/manga_alignment.py +88 -0
  91. context_aware_translation/documents/manga_reembed_planner.py +476 -0
  92. context_aware_translation/documents/pdf.py +757 -0
  93. context_aware_translation/documents/scanned_book.py +408 -0
  94. context_aware_translation/documents/subtitle.py +334 -0
  95. context_aware_translation/documents/text.py +265 -0
  96. context_aware_translation/llm/AGENTS.md +144 -0
  97. context_aware_translation/llm/__init__.py +1 -0
  98. context_aware_translation/llm/batch_jobs/AGENTS.md +148 -0
  99. context_aware_translation/llm/batch_jobs/__init__.py +21 -0
  100. context_aware_translation/llm/batch_jobs/base.py +101 -0
  101. context_aware_translation/llm/batch_jobs/gemini_gateway.py +668 -0
  102. context_aware_translation/llm/client.py +430 -0
  103. context_aware_translation/llm/epub_ocr.py +131 -0
  104. context_aware_translation/llm/extractor.py +341 -0
  105. context_aware_translation/llm/glossary_translator.py +254 -0
  106. context_aware_translation/llm/image_backend_base.py +94 -0
  107. context_aware_translation/llm/image_backends/AGENTS.md +199 -0
  108. context_aware_translation/llm/image_backends/__init__.py +1 -0
  109. context_aware_translation/llm/image_backends/gemini_backend.py +169 -0
  110. context_aware_translation/llm/image_backends/openai_backend.py +139 -0
  111. context_aware_translation/llm/image_backends/qwen_backend.py +172 -0
  112. context_aware_translation/llm/image_generator.py +109 -0
  113. context_aware_translation/llm/language_detector.py +162 -0
  114. context_aware_translation/llm/manga_ocr.py +302 -0
  115. context_aware_translation/llm/manga_translator.py +127 -0
  116. context_aware_translation/llm/ocr.py +205 -0
  117. context_aware_translation/llm/reviewer.py +173 -0
  118. context_aware_translation/llm/session_trace.py +38 -0
  119. context_aware_translation/llm/summarizor.py +352 -0
  120. context_aware_translation/llm/token_tracker.py +170 -0
  121. context_aware_translation/llm/translation_strategies.py +280 -0
  122. context_aware_translation/llm/translator.py +771 -0
  123. context_aware_translation/resources/opencc/config/hk2s.json +33 -0
  124. context_aware_translation/resources/opencc/config/jp2s.json +33 -0
  125. context_aware_translation/resources/opencc/config/s2hk.json +27 -0
  126. context_aware_translation/resources/opencc/config/s2t.json +22 -0
  127. context_aware_translation/resources/opencc/config/s2tw.json +27 -0
  128. context_aware_translation/resources/opencc/config/s2twp.json +32 -0
  129. context_aware_translation/resources/opencc/config/t2hk.json +16 -0
  130. context_aware_translation/resources/opencc/config/t2s.json +22 -0
  131. context_aware_translation/resources/opencc/config/t2tw.json +16 -0
  132. context_aware_translation/resources/opencc/config/tw2s.json +33 -0
  133. context_aware_translation/resources/opencc/config/tw2sp.json +36 -0
  134. context_aware_translation/resources/opencc/dictionary/HKVariants.txt +63 -0
  135. context_aware_translation/resources/opencc/dictionary/HKVariantsPhrases.txt +17 -0
  136. context_aware_translation/resources/opencc/dictionary/HKVariantsRev.txt +70 -0
  137. context_aware_translation/resources/opencc/dictionary/HKVariantsRevPhrases.txt +156 -0
  138. context_aware_translation/resources/opencc/dictionary/JPVariants.txt +367 -0
  139. context_aware_translation/resources/opencc/dictionary/JPVariantsRev.txt +367 -0
  140. context_aware_translation/resources/opencc/dictionary/STCharacters.txt +3980 -0
  141. context_aware_translation/resources/opencc/dictionary/STPhrases.txt +49051 -0
  142. context_aware_translation/resources/opencc/dictionary/TSCharacters.txt +4113 -0
  143. context_aware_translation/resources/opencc/dictionary/TSPhrases.txt +277 -0
  144. context_aware_translation/resources/opencc/dictionary/TWPhrases.txt +509 -0
  145. context_aware_translation/resources/opencc/dictionary/TWPhrasesRev.txt +518 -0
  146. context_aware_translation/resources/opencc/dictionary/TWVariants.txt +39 -0
  147. context_aware_translation/resources/opencc/dictionary/TWVariantsRev.txt +39 -0
  148. context_aware_translation/resources/opencc/dictionary/TWVariantsRevPhrases.txt +68 -0
  149. context_aware_translation/resources/tokenizers/deepseek-v3/special_tokens_map.json +23 -0
  150. context_aware_translation/resources/tokenizers/deepseek-v3/tokenizer.json +646418 -0
  151. context_aware_translation/resources/tokenizers/deepseek-v3/tokenizer_config.json +6562 -0
  152. context_aware_translation/storage/AGENTS.md +192 -0
  153. context_aware_translation/storage/__init__.py +3 -0
  154. context_aware_translation/storage/library/__init__.py +3 -0
  155. context_aware_translation/storage/library/book_manager.py +670 -0
  156. context_aware_translation/storage/models/__init__.py +3 -0
  157. context_aware_translation/storage/models/book.py +85 -0
  158. context_aware_translation/storage/models/config_profile.py +67 -0
  159. context_aware_translation/storage/models/endpoint_profile.py +97 -0
  160. context_aware_translation/storage/repositories/__init__.py +80 -0
  161. context_aware_translation/storage/repositories/document_repository.py +325 -0
  162. context_aware_translation/storage/repositories/llm_batch_store.py +165 -0
  163. context_aware_translation/storage/repositories/task_store.py +295 -0
  164. context_aware_translation/storage/repositories/term_repository.py +431 -0
  165. context_aware_translation/storage/repositories/translation_batch_task_store.py +315 -0
  166. context_aware_translation/storage/schema/__init__.py +17 -0
  167. context_aware_translation/storage/schema/book_db.py +1958 -0
  168. context_aware_translation/storage/schema/registry_db.py +949 -0
  169. context_aware_translation/storage/sqlite_locking.py +17 -0
  170. context_aware_translation/ui/AGENTS.md +115 -0
  171. context_aware_translation/ui/__init__.py +0 -0
  172. context_aware_translation/ui/chrome_sizing.py +21 -0
  173. context_aware_translation/ui/constants.py +104 -0
  174. context_aware_translation/ui/features/app_settings_pane.py +585 -0
  175. context_aware_translation/ui/features/app_setup_view.py +921 -0
  176. context_aware_translation/ui/features/document_images_view.py +725 -0
  177. context_aware_translation/ui/features/document_ocr_tab.py +1038 -0
  178. context_aware_translation/ui/features/document_translation_view.py +1401 -0
  179. context_aware_translation/ui/features/document_workspace_view.py +806 -0
  180. context_aware_translation/ui/features/library_view.py +401 -0
  181. context_aware_translation/ui/features/project_settings_pane.py +448 -0
  182. context_aware_translation/ui/features/queue_drawer_view.py +472 -0
  183. context_aware_translation/ui/features/terms_table_widget.py +599 -0
  184. context_aware_translation/ui/features/terms_view.py +1149 -0
  185. context_aware_translation/ui/features/work_view.py +697 -0
  186. context_aware_translation/ui/features/workflow_profile_editor.py +1312 -0
  187. context_aware_translation/ui/i18n.py +954 -0
  188. context_aware_translation/ui/json_utils.py +23 -0
  189. context_aware_translation/ui/main.py +140 -0
  190. context_aware_translation/ui/main_window.py +585 -0
  191. context_aware_translation/ui/qml/BootstrapProbe.qml +10 -0
  192. context_aware_translation/ui/qml/app/AppShellChrome.qml +161 -0
  193. context_aware_translation/ui/qml/dialogs/app_settings/AppSettingsDialogChrome.qml +77 -0
  194. context_aware_translation/ui/qml/dialogs/app_settings/AppSettingsPane.qml +131 -0
  195. context_aware_translation/ui/qml/dialogs/project_settings/ProjectSettingsDialogChrome.qml +77 -0
  196. context_aware_translation/ui/qml/dialogs/project_settings/ProjectSettingsPane.qml +205 -0
  197. context_aware_translation/ui/qml/document/DocumentShellChrome.qml +205 -0
  198. context_aware_translation/ui/qml/document/export/DocumentExportPaneChrome.qml +89 -0
  199. context_aware_translation/ui/qml/document/images/DocumentImagesPaneChrome.qml +457 -0
  200. context_aware_translation/ui/qml/document/ocr/DocumentOCRPaneChrome.qml +416 -0
  201. context_aware_translation/ui/qml/document/translation/DocumentTranslationPaneChrome.qml +136 -0
  202. context_aware_translation/ui/qml/project/ProjectShellChrome.qml +175 -0
  203. context_aware_translation/ui/qml/project/terms/TermsPaneChrome.qml +143 -0
  204. context_aware_translation/ui/qml/project/work_home/WorkHomeChrome.qml +331 -0
  205. context_aware_translation/ui/qml/queue/QueueShellChrome.qml +70 -0
  206. context_aware_translation/ui/qml_resources.py +57 -0
  207. context_aware_translation/ui/resources/__init__.py +0 -0
  208. context_aware_translation/ui/resources/styles.qss +283 -0
  209. context_aware_translation/ui/shell_hosts/__init__.py +11 -0
  210. context_aware_translation/ui/shell_hosts/app_settings_dialog_host.py +55 -0
  211. context_aware_translation/ui/shell_hosts/app_shell_host.py +77 -0
  212. context_aware_translation/ui/shell_hosts/document_shell_host.py +190 -0
  213. context_aware_translation/ui/shell_hosts/hybrid.py +156 -0
  214. context_aware_translation/ui/shell_hosts/project_settings_dialog_host.py +55 -0
  215. context_aware_translation/ui/shell_hosts/project_shell_host.py +144 -0
  216. context_aware_translation/ui/shell_hosts/queue_shell_host.py +60 -0
  217. context_aware_translation/ui/sleep_inhibitor.py +135 -0
  218. context_aware_translation/ui/startup.py +78 -0
  219. context_aware_translation/ui/tips.py +16 -0
  220. context_aware_translation/ui/translations/zh_CN.qm +0 -0
  221. context_aware_translation/ui/translations/zh_CN.ts +4351 -0
  222. context_aware_translation/ui/viewmodels/__init__.py +30 -0
  223. context_aware_translation/ui/viewmodels/app_settings_dialog.py +53 -0
  224. context_aware_translation/ui/viewmodels/app_settings_pane.py +63 -0
  225. context_aware_translation/ui/viewmodels/app_shell.py +87 -0
  226. context_aware_translation/ui/viewmodels/base.py +114 -0
  227. context_aware_translation/ui/viewmodels/document_export_pane.py +65 -0
  228. context_aware_translation/ui/viewmodels/document_images_pane.py +338 -0
  229. context_aware_translation/ui/viewmodels/document_ocr_pane.py +261 -0
  230. context_aware_translation/ui/viewmodels/document_shell.py +122 -0
  231. context_aware_translation/ui/viewmodels/document_translation_pane.py +113 -0
  232. context_aware_translation/ui/viewmodels/project_settings_dialog.py +51 -0
  233. context_aware_translation/ui/viewmodels/project_settings_pane.py +194 -0
  234. context_aware_translation/ui/viewmodels/project_shell.py +98 -0
  235. context_aware_translation/ui/viewmodels/queue_shell.py +51 -0
  236. context_aware_translation/ui/viewmodels/router.py +202 -0
  237. context_aware_translation/ui/viewmodels/terms_pane.py +230 -0
  238. context_aware_translation/ui/viewmodels/work_home.py +250 -0
  239. context_aware_translation/ui/widgets/AGENTS.md +90 -0
  240. context_aware_translation/ui/widgets/hybrid_controls.py +153 -0
  241. context_aware_translation/ui/widgets/image_viewer.py +343 -0
  242. context_aware_translation/ui/widgets/progress_widget.py +136 -0
  243. context_aware_translation/ui/widgets/table_support.py +59 -0
  244. context_aware_translation/ui/window_controllers.py +310 -0
  245. context_aware_translation/utils/AGENTS.md +157 -0
  246. context_aware_translation/utils/__init__.py +3 -0
  247. context_aware_translation/utils/chunking.py +147 -0
  248. context_aware_translation/utils/cjk_normalize.py +141 -0
  249. context_aware_translation/utils/compression_marker.py +18 -0
  250. context_aware_translation/utils/file_utils.py +34 -0
  251. context_aware_translation/utils/hard_wrap.py +87 -0
  252. context_aware_translation/utils/hashing.py +20 -0
  253. context_aware_translation/utils/image_utils.py +79 -0
  254. context_aware_translation/utils/llm_json_cleaner.py +91 -0
  255. context_aware_translation/utils/markdown_escape.py +195 -0
  256. context_aware_translation/utils/pandoc_export.py +52 -0
  257. context_aware_translation/utils/semantic_chunker.py +92 -0
  258. context_aware_translation/utils/string_similarity.py +33 -0
  259. context_aware_translation/utils/symbol_check.py +29 -0
  260. context_aware_translation/workflow/AGENTS.md +171 -0
  261. context_aware_translation/workflow/__init__.py +1 -0
  262. context_aware_translation/workflow/bootstrap.py +123 -0
  263. context_aware_translation/workflow/image_fetcher.py +53 -0
  264. context_aware_translation/workflow/ops/__init__.py +1 -0
  265. context_aware_translation/workflow/ops/bootstrap_ops.py +202 -0
  266. context_aware_translation/workflow/ops/export_ops.py +234 -0
  267. context_aware_translation/workflow/ops/glossary_ops.py +116 -0
  268. context_aware_translation/workflow/ops/import_ops.py +108 -0
  269. context_aware_translation/workflow/ops/import_support.py +209 -0
  270. context_aware_translation/workflow/ops/ocr_ops.py +118 -0
  271. context_aware_translation/workflow/ops/translation_ops.py +156 -0
  272. context_aware_translation/workflow/runtime.py +27 -0
  273. context_aware_translation/workflow/session.py +69 -0
  274. context_aware_translation/workflow/task_runtime.py +89 -0
  275. context_aware_translation/workflow/tasks/AGENTS.md +176 -0
  276. context_aware_translation/workflow/tasks/__init__.py +0 -0
  277. context_aware_translation/workflow/tasks/claims.py +83 -0
  278. context_aware_translation/workflow/tasks/engine_core.py +740 -0
  279. context_aware_translation/workflow/tasks/exceptions.py +13 -0
  280. context_aware_translation/workflow/tasks/execution/AGENTS.md +126 -0
  281. context_aware_translation/workflow/tasks/execution/__init__.py +0 -0
  282. context_aware_translation/workflow/tasks/execution/batch_translation_executor.py +819 -0
  283. context_aware_translation/workflow/tasks/execution/batch_translation_ops.py +1190 -0
  284. context_aware_translation/workflow/tasks/glossary_preflight.py +138 -0
  285. context_aware_translation/workflow/tasks/handlers/AGENTS.md +245 -0
  286. context_aware_translation/workflow/tasks/handlers/__init__.py +0 -0
  287. context_aware_translation/workflow/tasks/handlers/base.py +42 -0
  288. context_aware_translation/workflow/tasks/handlers/batch_translation.py +221 -0
  289. context_aware_translation/workflow/tasks/handlers/chunk_retranslation.py +194 -0
  290. context_aware_translation/workflow/tasks/handlers/glossary_export.py +182 -0
  291. context_aware_translation/workflow/tasks/handlers/glossary_extraction.py +255 -0
  292. context_aware_translation/workflow/tasks/handlers/glossary_review.py +196 -0
  293. context_aware_translation/workflow/tasks/handlers/glossary_translation.py +177 -0
  294. context_aware_translation/workflow/tasks/handlers/image_reembedding.py +371 -0
  295. context_aware_translation/workflow/tasks/handlers/ocr.py +333 -0
  296. context_aware_translation/workflow/tasks/handlers/translate_and_export.py +183 -0
  297. context_aware_translation/workflow/tasks/handlers/translation_manga.py +252 -0
  298. context_aware_translation/workflow/tasks/handlers/translation_text.py +207 -0
  299. context_aware_translation/workflow/tasks/models.py +70 -0
  300. context_aware_translation/workflow/tasks/translate_and_export_support.py +410 -0
  301. context_aware_translation/workflow/tasks/worker_deps.py +28 -0
  302. contextweave-0.2.0.dist-info/METADATA +185 -0
  303. contextweave-0.2.0.dist-info/RECORD +306 -0
  304. contextweave-0.2.0.dist-info/WHEEL +4 -0
  305. contextweave-0.2.0.dist-info/entry_points.txt +3 -0
  306. contextweave-0.2.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,392 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QObject, Qt, QTimer, Signal, Slot
8
+
9
+ from context_aware_translation.workflow.tasks.claims import ResourceClaim
10
+ from context_aware_translation.workflow.tasks.engine_core import EngineCore
11
+ from context_aware_translation.workflow.tasks.exceptions import CancelDispatchRaceError, RunValidationError
12
+ from context_aware_translation.workflow.tasks.models import TaskAction
13
+
14
+ if TYPE_CHECKING:
15
+ from context_aware_translation.storage.repositories.task_store import TaskRecord, TaskStore
16
+ from context_aware_translation.workflow.tasks.handlers.base import TaskTypeHandler
17
+ from context_aware_translation.workflow.tasks.models import Decision
18
+ from context_aware_translation.workflow.tasks.worker_deps import WorkerDeps
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class TaskEngine(QObject):
24
+ """QObject orchestrator that drives the task lifecycle."""
25
+
26
+ tasks_changed = Signal(str) # book_id
27
+ error_occurred = Signal(str) # message
28
+ running_work_changed = Signal(bool) # is_running
29
+ enqueue_task_changed = Signal(str) # internal — connected via QueuedConnection to _emit_task_changed
30
+
31
+ def __init__(self, *, store: TaskStore, deps: WorkerDeps, parent: QObject | None = None) -> None:
32
+ super().__init__(parent)
33
+ self._core = EngineCore(store=store, deps=deps)
34
+ self._store = store
35
+ self._autorun_timer: QTimer | None = None
36
+ self._was_running: bool = self._core.has_running_work()
37
+
38
+ # Coalesce rapid-fire task-changed signals into at most one UI
39
+ # refresh per 250 ms window. Worker threads emit
40
+ # ``enqueue_task_changed`` on every DB persist; without
41
+ # coalescing, 8 connected widgets each re-query the DB on every
42
+ # emission, starving the event loop.
43
+ self._pending_book_ids: set[str] = set()
44
+ self._coalesce_timer = QTimer(self)
45
+ self._coalesce_timer.setSingleShot(True)
46
+ self._coalesce_timer.setInterval(250)
47
+ self._coalesce_timer.timeout.connect(self._flush_task_changed)
48
+
49
+ self.enqueue_task_changed.connect(self._emit_task_changed, Qt.ConnectionType.QueuedConnection)
50
+
51
+ @property
52
+ def store(self) -> TaskStore:
53
+ return self._store
54
+
55
+ # ------------------------------------------------------------------
56
+ # Delegate to core
57
+ # ------------------------------------------------------------------
58
+
59
+ def register_handler(self, handler: TaskTypeHandler) -> None:
60
+ self._core.register_handler(handler)
61
+
62
+ def get_tasks(
63
+ self,
64
+ book_id: str,
65
+ task_type: str | None = None,
66
+ limit: int | None = None,
67
+ *,
68
+ full: bool = False,
69
+ ) -> list[TaskRecord]:
70
+ if full:
71
+ return self._core.get_tasks(book_id, task_type, limit)
72
+ return self._core.get_tasks_lightweight(book_id, task_type, limit)
73
+
74
+ def get_task(self, task_id: str) -> TaskRecord | None:
75
+ return self._core.get_task(task_id)
76
+
77
+ def has_active_worker(self, task_id: str) -> bool:
78
+ return self._core.has_active_worker(task_id)
79
+
80
+ def has_running_work(self) -> bool:
81
+ return self._core.has_running_work()
82
+
83
+ def has_active_claims(self, book_id: str, wanted: frozenset[ResourceClaim]) -> bool:
84
+ return self._core.has_active_claims(book_id, wanted)
85
+
86
+ def preflight(self, task_type: str, book_id: str, params: dict, action: TaskAction) -> Decision:
87
+ return self._core.preflight(task_type, book_id, params, action)
88
+
89
+ def preflight_task(self, task_id: str, action: TaskAction) -> Decision:
90
+ return self._core.preflight_task(task_id, action)
91
+
92
+ def recover_interrupted_tasks(self) -> list[str]:
93
+ """Normalize stale active-task rows left behind by a previous shutdown."""
94
+
95
+ book_ids = sorted(self._core.recover_interrupted_tasks())
96
+ for book_id in book_ids:
97
+ self.enqueue_task_changed.emit(book_id)
98
+ self._emit_running_work_changed_if_needed()
99
+ return book_ids
100
+
101
+ # ------------------------------------------------------------------
102
+ # Mutation APIs
103
+ # ------------------------------------------------------------------
104
+
105
+ def enqueue_followup_task(self, task_type: str, book_id: str, **params: object) -> None:
106
+ """Submit a follow-up task (queued only, no immediate start).
107
+
108
+ Used by workers to chain tasks (e.g., translation -> reembedding).
109
+ Failures are logged and re-raised so caller workers can surface
110
+ partial-success state (e.g. completed_with_errors).
111
+ """
112
+ try:
113
+ record = self._core.submit(task_type, book_id, **params)
114
+ logger.info("Follow-up %s task %s enqueued for book %s", task_type, record.task_id, book_id)
115
+ self.enqueue_task_changed.emit(book_id)
116
+ except Exception:
117
+ logger.warning("Failed to enqueue follow-up %s for book %s", task_type, book_id, exc_info=True)
118
+ raise
119
+
120
+ def submit(self, task_type: str, book_id: str, **params) -> TaskRecord:
121
+ """Create a new task row then best-effort start it."""
122
+ record = self._core.submit(task_type, book_id, **params)
123
+ try:
124
+ self._start_action(record.task_id, TaskAction.RUN)
125
+ except Exception:
126
+ logger.debug("Best-effort start failed for task %s; will retry on next tick", record.task_id)
127
+ self._emit_running_work_changed_if_needed()
128
+ return record
129
+
130
+ def submit_and_start(self, task_type: str, book_id: str, **params) -> TaskRecord:
131
+ """Create a new task row and immediately start it (strict mode).
132
+
133
+ Unlike ``submit``, this method guarantees the task either starts
134
+ successfully or is immediately marked failed. It never leaves the
135
+ task in ``queued`` state and never silently discards a created row.
136
+
137
+ Raises ``ValueError`` if ``_core.submit`` itself rejects the task
138
+ (e.g. validate_submit fails).
139
+ """
140
+ record = self._core.submit(task_type, book_id, **params)
141
+ try:
142
+ self._start_action(record.task_id, TaskAction.RUN)
143
+ except Exception as exc:
144
+ reason = f"strict-start failed: {type(exc).__name__}: {exc}"
145
+ logger.warning("submit_and_start: marking task %s failed — %s", record.task_id, reason)
146
+ try:
147
+ self._store.update(record.task_id, status="failed", last_error=reason)
148
+ except Exception:
149
+ logger.exception("submit_and_start: could not mark task %s failed", record.task_id)
150
+ self.enqueue_task_changed.emit(book_id)
151
+ self._emit_running_work_changed_if_needed()
152
+ # Re-fetch the record so the caller sees the failed status.
153
+ updated = self._core.get_task(record.task_id)
154
+ return updated if updated is not None else record
155
+ self._emit_running_work_changed_if_needed()
156
+ return record
157
+
158
+ def run_task(self, task_id: str) -> TaskRecord:
159
+ """Run a task: atomically resets to queued if terminal, then starts it (strict).
160
+
161
+ Raises ``ValueError`` if the task cannot be requeued (e.g. config snapshot
162
+ capture fails or the handler denies the RUN action).
163
+
164
+ If the worker fails to start, the task is immediately marked failed so it
165
+ is never left stranded in ``queued`` state.
166
+ """
167
+ record = self._core.ensure_runnable(task_id) # may raise ValueError
168
+ try:
169
+ self._start_action(task_id, TaskAction.RUN)
170
+ except Exception as exc:
171
+ reason = f"strict-start failed: {type(exc).__name__}: {exc}"
172
+ logger.warning("run_task: marking task %s failed — %s", task_id, reason)
173
+ try:
174
+ self._store.update(task_id, status="failed", last_error=reason)
175
+ except Exception:
176
+ logger.exception("run_task: could not mark task %s failed", task_id)
177
+ self.enqueue_task_changed.emit(record.book_id)
178
+ self._emit_running_work_changed_if_needed()
179
+ updated = self._core.get_task(task_id)
180
+ return updated if updated is not None else record
181
+ self._emit_running_work_changed_if_needed()
182
+ return record
183
+
184
+ def rerun(self, task_id: str) -> TaskRecord:
185
+ """Reset a terminal task to queued then start it (strict).
186
+
187
+ Raises ``ValueError`` if the task cannot be rerun (e.g. config snapshot
188
+ capture fails or the handler denies the RUN action).
189
+
190
+ If the worker fails to start, the task is immediately marked failed so it
191
+ is never left stranded in ``queued`` state.
192
+ """
193
+ record = self._core.rerun(task_id) # may raise ValueError
194
+ try:
195
+ self._start_action(task_id, TaskAction.RUN)
196
+ except Exception as exc:
197
+ reason = f"strict-start failed: {type(exc).__name__}: {exc}"
198
+ logger.warning("rerun: marking task %s failed — %s", task_id, reason)
199
+ try:
200
+ self._store.update(task_id, status="failed", last_error=reason)
201
+ except Exception:
202
+ logger.exception("rerun: could not mark task %s failed", task_id)
203
+ self.enqueue_task_changed.emit(record.book_id)
204
+ self._emit_running_work_changed_if_needed()
205
+ updated = self._core.get_task(task_id)
206
+ return updated if updated is not None else record
207
+ self._emit_running_work_changed_if_needed()
208
+ return record
209
+
210
+ def cancel(self, task_id: str) -> None:
211
+ """Request cancellation only if handler policy allows it."""
212
+ try:
213
+ worker = self._core.cancel(task_id)
214
+ if worker is not None and hasattr(worker, "requestInterruption"):
215
+ worker.requestInterruption() # type: ignore[attr-defined]
216
+ elif worker is None:
217
+ # No active RUN worker — dispatch explicit cancel action worker
218
+ # Only if core.cancel() actually accepted the request (cancel_requested=True).
219
+ record_after = self._core.get_task(task_id)
220
+ if record_after is not None and record_after.cancel_requested:
221
+ try:
222
+ self._start_action(task_id, TaskAction.CANCEL)
223
+ except (CancelDispatchRaceError, KeyError) as exc:
224
+ logger.debug("Cancel action worker not started for task %s", task_id)
225
+ self._core.handle_cancel_dispatch_failure(
226
+ task_id,
227
+ reason=f"cancel dispatch failed: {type(exc).__name__}: {exc}",
228
+ )
229
+ record = self._core.get_task(task_id)
230
+ if record is not None:
231
+ self.enqueue_task_changed.emit(record.book_id)
232
+ finally:
233
+ self._emit_running_work_changed_if_needed()
234
+
235
+ def delete(self, task_id: str) -> None:
236
+ """Delete a task if handler policy allows it and worker is not active."""
237
+ try:
238
+ book_id = self._core.delete(task_id)
239
+ if book_id is not None:
240
+ self.enqueue_task_changed.emit(book_id)
241
+ finally:
242
+ self._emit_running_work_changed_if_needed()
243
+
244
+ def cancel_running_tasks(self, book_id: str) -> None:
245
+ """Interrupt active workers for the given book and mark them cancel_requested."""
246
+ workers = self._core.cancel_running_tasks(book_id)
247
+ for worker in workers:
248
+ if hasattr(worker, "requestInterruption"):
249
+ worker.requestInterruption() # type: ignore[attr-defined]
250
+ self._emit_running_work_changed_if_needed()
251
+
252
+ # ------------------------------------------------------------------
253
+ # Autorun timer
254
+ # ------------------------------------------------------------------
255
+
256
+ def start_autorun(self, interval_ms: int = 3000) -> None:
257
+ if self._autorun_timer is None:
258
+ self._autorun_timer = QTimer(self)
259
+ self._autorun_timer.timeout.connect(self.tick)
260
+ self._autorun_timer.start(interval_ms)
261
+ self._emit_running_work_changed_if_needed()
262
+
263
+ def stop_autorun(self) -> None:
264
+ if self._autorun_timer is not None:
265
+ self._autorun_timer.stop()
266
+
267
+ # ------------------------------------------------------------------
268
+ # Tick / scan
269
+ # ------------------------------------------------------------------
270
+
271
+ @Slot()
272
+ def tick(self) -> None:
273
+ self._core.cleanup_finished_workers()
274
+ self._emit_running_work_changed_if_needed()
275
+ try:
276
+ startable = self._core.scan_autorunnable()
277
+ except RuntimeError as exc:
278
+ self.stop_autorun()
279
+ self.error_occurred.emit(f"Fatal TaskEngine error: {exc}")
280
+ raise
281
+ for task_id in startable:
282
+ try:
283
+ self._start_action(task_id, TaskAction.RUN)
284
+ except RunValidationError as exc:
285
+ # validate_run rejected — mark task failed so it does not loop.
286
+ logger.warning("Run validation failed for task %s: %s", task_id, exc)
287
+ try:
288
+ self._store.update(task_id, status="failed", last_error=str(exc))
289
+ except Exception:
290
+ logger.exception("Could not mark task %s failed after RunValidationError", task_id)
291
+ record = self._core.get_task(task_id)
292
+ if record is not None:
293
+ self.enqueue_task_changed.emit(record.book_id)
294
+ continue
295
+ except RuntimeError as exc:
296
+ # Denied action or conflicts should not crash scheduler loop.
297
+ logger.debug("Skipped task %s in tick: %s", task_id, exc)
298
+ continue
299
+ except Exception as exc: # noqa: BLE001
300
+ logger.exception("Unexpected autorun error for task %s: %s", task_id, exc)
301
+ record = self._core.get_task(task_id)
302
+ if record is not None:
303
+ self._core._set_backoff(record.book_id, time.monotonic())
304
+
305
+ # ------------------------------------------------------------------
306
+ # Internal: start a worker
307
+ # ------------------------------------------------------------------
308
+
309
+ def _start_action(self, task_id: str, action: TaskAction) -> None:
310
+ """Authorize, build worker, connect signals, start."""
311
+ record, worker, claims = self._core.authorize_start(task_id, action)
312
+ self._core.register_active_worker(task_id, worker, claims)
313
+
314
+ try:
315
+ # Connect finished signal
316
+ if hasattr(worker, "finished"):
317
+ worker.finished.connect(
318
+ lambda tid=task_id: self._on_worker_finished(tid),
319
+ Qt.ConnectionType.QueuedConnection,
320
+ )
321
+
322
+ worker.start() # type: ignore[attr-defined]
323
+ except Exception:
324
+ self._core.release_task_resources(task_id)
325
+ self._emit_running_work_changed_if_needed()
326
+ raise
327
+
328
+ self._emit_running_work_changed_if_needed()
329
+ self.enqueue_task_changed.emit(record.book_id)
330
+
331
+ # ------------------------------------------------------------------
332
+ # Worker lifecycle callbacks
333
+ # ------------------------------------------------------------------
334
+
335
+ def _on_worker_finished(self, task_id: str) -> None:
336
+ self._core.release_task_resources(task_id)
337
+ self._emit_running_work_changed_if_needed()
338
+ record = self._core.get_task(task_id)
339
+ if record is not None:
340
+ self.enqueue_task_changed.emit(record.book_id)
341
+
342
+ def _emit_running_work_changed_if_needed(self) -> None:
343
+ is_running = self._core.has_running_work()
344
+ if is_running != self._was_running:
345
+ self._was_running = is_running
346
+ self.running_work_changed.emit(is_running)
347
+
348
+ # ------------------------------------------------------------------
349
+ # Signal relay
350
+ # ------------------------------------------------------------------
351
+
352
+ @Slot(str)
353
+ def _emit_task_changed(self, book_id: str) -> None:
354
+ self._pending_book_ids.add(book_id)
355
+ if not self._coalesce_timer.isActive():
356
+ self._coalesce_timer.start()
357
+
358
+ @Slot()
359
+ def _flush_task_changed(self) -> None:
360
+ book_ids = list(self._pending_book_ids)
361
+ self._pending_book_ids.clear()
362
+ for book_id in book_ids:
363
+ self.tasks_changed.emit(book_id)
364
+
365
+ # ------------------------------------------------------------------
366
+ # Shutdown
367
+ # ------------------------------------------------------------------
368
+
369
+ def close(self) -> None:
370
+ self.stop_autorun()
371
+ # Flush any coalesced task-changed notifications before tearing down.
372
+ if self._coalesce_timer.isActive():
373
+ self._coalesce_timer.stop()
374
+ self._flush_task_changed()
375
+ # Request interruption for all active workers
376
+ for _task_id, worker in self._core.active_worker_items():
377
+ if hasattr(worker, "requestInterruption"):
378
+ worker.requestInterruption() # type: ignore[attr-defined]
379
+ # Wait up to 5000ms for workers to finish
380
+ deadline = time.monotonic() + 5.0
381
+ for _task_id, worker in self._core.active_worker_items():
382
+ if hasattr(worker, "wait"):
383
+ remaining_ms = max(0, int((deadline - time.monotonic()) * 1000))
384
+ worker.wait(remaining_ms) # type: ignore[attr-defined]
385
+ # Release claims for any workers that finished
386
+ self._core.cleanup_finished_workers()
387
+ self._emit_running_work_changed_if_needed()
388
+ # Only close the store if no workers are still running
389
+ if not self._core.has_running_work():
390
+ self._core.close()
391
+ else:
392
+ logger.warning("Skipping store close: %d workers still active", len(self._core.active_worker_items()))
@@ -0,0 +1 @@
1
+ """Qt worker implementations used by task handlers and import/export flows."""
@@ -0,0 +1,61 @@
1
+ """Base worker class for background operations."""
2
+
3
+ import logging
4
+
5
+ from PySide6.QtCore import QThread, Signal
6
+
7
+ from context_aware_translation.core.cancellation import OperationCancelledError
8
+ from context_aware_translation.core.progress import ProgressUpdate
9
+ from context_aware_translation.ui import sleep_inhibitor
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class BaseWorker(QThread):
15
+ """Base worker with common signals and error handling template."""
16
+
17
+ progress = Signal(int, int, str) # current, total, message
18
+ finished_success = Signal(object) # result data
19
+ cancelled = Signal() # cancelled by user
20
+ error = Signal(str) # error message
21
+
22
+ def _emit_progress(self, update: ProgressUpdate) -> None:
23
+ """Convert ProgressUpdate to signal emission."""
24
+ self._raise_if_cancelled()
25
+ self.progress.emit(update.current, update.total, update.message)
26
+ self._raise_if_cancelled()
27
+
28
+ def _raise_if_cancelled(self) -> None:
29
+ """Raise cooperative cancellation if interruption was requested."""
30
+ if self.isInterruptionRequested():
31
+ raise OperationCancelledError("Worker interrupted")
32
+
33
+ def _is_cancelled(self) -> bool:
34
+ """Return True when interruption has been requested."""
35
+ return self.isInterruptionRequested()
36
+
37
+ def run(self) -> None:
38
+ """Execute worker with standard error handling.
39
+
40
+ Subclasses should override _execute() instead of run().
41
+ """
42
+ sleep_inhibitor.SleepInhibitor.acquire()
43
+ try:
44
+ self._raise_if_cancelled()
45
+ self._execute()
46
+ except OperationCancelledError:
47
+ logger.info("%s cancelled", self.__class__.__name__)
48
+ self.cancelled.emit()
49
+ except Exception as e:
50
+ logger.exception(f"{self.__class__.__name__} failed")
51
+ self.error.emit(f"{type(e).__name__}: {e}")
52
+ finally:
53
+ sleep_inhibitor.SleepInhibitor.release()
54
+
55
+ def _execute(self) -> None:
56
+ """Execute the worker's main task.
57
+
58
+ Subclasses must override this method to implement their logic.
59
+ Should emit finished_success signal when complete.
60
+ """
61
+ raise NotImplementedError("Subclasses must implement _execute()")
@@ -0,0 +1,57 @@
1
+ """DB-backed overlap guard for batch task reservations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from context_aware_translation.storage.repositories.task_store import TaskStore
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def has_any_batch_task_overlap(
16
+ task_store: TaskStore,
17
+ book_id: str,
18
+ document_ids: list[int] | None,
19
+ *,
20
+ exclude_task_ids: set[str] | None = None,
21
+ ) -> bool:
22
+ """Return True when any existing batch task overlaps selected docs.
23
+
24
+ Rules:
25
+ - Overlap semantics match DocumentOperationTracker (None = all docs overlaps everything).
26
+ - exclude_task_ids: skip these task IDs (e.g. the task being run itself).
27
+ - No status filtering: all task rows are considered blockers.
28
+ """
29
+ tasks = task_store.list_tasks(book_id=book_id, task_type="batch_translation")
30
+ for task in tasks:
31
+ if exclude_task_ids and task.task_id in exclude_task_ids:
32
+ continue
33
+ task_doc_ids = _parse_task_document_ids(task)
34
+ if _ids_overlap(task_doc_ids, document_ids):
35
+ return True
36
+ return False
37
+
38
+
39
+ def _parse_task_document_ids(task: object) -> list[int] | None:
40
+ """Extract document_ids from a batch task record."""
41
+ raw = getattr(task, "document_ids_json", None)
42
+ if raw is None or raw == "" or raw == "null":
43
+ return None
44
+ try:
45
+ parsed = json.loads(raw)
46
+ if parsed is None:
47
+ return None
48
+ return [int(doc_id) for doc_id in parsed]
49
+ except (json.JSONDecodeError, TypeError, ValueError):
50
+ return None
51
+
52
+
53
+ def _ids_overlap(a: list[int] | None, b: list[int] | None) -> bool:
54
+ """Check if two document ID sets overlap. None means 'all docs'."""
55
+ if a is None or b is None:
56
+ return True
57
+ return bool(set(a) & set(b))