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.
- context_aware_translation/AGENTS.md +134 -0
- context_aware_translation/__init__.py +114 -0
- context_aware_translation/adapters/__init__.py +1 -0
- context_aware_translation/adapters/files/__init__.py +3 -0
- context_aware_translation/adapters/files/glossary_io.py +203 -0
- context_aware_translation/adapters/qt/__init__.py +1 -0
- context_aware_translation/adapters/qt/application_event_bridge.py +78 -0
- context_aware_translation/adapters/qt/task_engine.py +392 -0
- context_aware_translation/adapters/qt/workers/__init__.py +1 -0
- context_aware_translation/adapters/qt/workers/base_worker.py +61 -0
- context_aware_translation/adapters/qt/workers/batch_task_overlap_guard.py +57 -0
- context_aware_translation/adapters/qt/workers/batch_translation_task_worker.py +228 -0
- context_aware_translation/adapters/qt/workers/chunk_retranslation_task_worker.py +136 -0
- context_aware_translation/adapters/qt/workers/export_worker.py +75 -0
- context_aware_translation/adapters/qt/workers/glossary_export_task_worker.py +119 -0
- context_aware_translation/adapters/qt/workers/glossary_extraction_task_worker.py +102 -0
- context_aware_translation/adapters/qt/workers/glossary_review_task_worker.py +98 -0
- context_aware_translation/adapters/qt/workers/glossary_translation_task_worker.py +97 -0
- context_aware_translation/adapters/qt/workers/image_reembedding_task_worker.py +114 -0
- context_aware_translation/adapters/qt/workers/import_worker.py +49 -0
- context_aware_translation/adapters/qt/workers/ocr_task_worker.py +149 -0
- context_aware_translation/adapters/qt/workers/operation_tracker.py +94 -0
- context_aware_translation/adapters/qt/workers/translate_and_export_task_worker.py +394 -0
- context_aware_translation/adapters/qt/workers/translation_manga_task_worker.py +133 -0
- context_aware_translation/adapters/qt/workers/translation_text_task_worker.py +158 -0
- context_aware_translation/app_identity.py +33 -0
- context_aware_translation/application/__init__.py +7 -0
- context_aware_translation/application/composition.py +104 -0
- context_aware_translation/application/contracts/__init__.py +57 -0
- context_aware_translation/application/contracts/app_setup.py +188 -0
- context_aware_translation/application/contracts/common.py +193 -0
- context_aware_translation/application/contracts/document.py +235 -0
- context_aware_translation/application/contracts/project_setup.py +19 -0
- context_aware_translation/application/contracts/projects.py +36 -0
- context_aware_translation/application/contracts/queue.py +35 -0
- context_aware_translation/application/contracts/terms.py +148 -0
- context_aware_translation/application/contracts/work.py +111 -0
- context_aware_translation/application/errors.py +35 -0
- context_aware_translation/application/events.py +146 -0
- context_aware_translation/application/runtime.py +1302 -0
- context_aware_translation/application/services/__init__.py +19 -0
- context_aware_translation/application/services/_export_support.py +281 -0
- context_aware_translation/application/services/app_setup.py +532 -0
- context_aware_translation/application/services/document.py +2309 -0
- context_aware_translation/application/services/project_setup.py +161 -0
- context_aware_translation/application/services/projects.py +152 -0
- context_aware_translation/application/services/queue.py +118 -0
- context_aware_translation/application/services/terms.py +582 -0
- context_aware_translation/application/services/work.py +525 -0
- context_aware_translation/cli/__init__.py +1 -0
- context_aware_translation/cli/config_file.py +337 -0
- context_aware_translation/cli/main.py +362 -0
- context_aware_translation/cli/output.py +91 -0
- context_aware_translation/cli/runtime.py +26 -0
- context_aware_translation/cli/wait.py +51 -0
- context_aware_translation/config.py +1354 -0
- context_aware_translation/core/AGENTS.md +133 -0
- context_aware_translation/core/__init__.py +1 -0
- context_aware_translation/core/cancellation.py +17 -0
- context_aware_translation/core/context_extractor.py +68 -0
- context_aware_translation/core/context_manager.py +2257 -0
- context_aware_translation/core/manga_document_handler.py +253 -0
- context_aware_translation/core/models.py +211 -0
- context_aware_translation/core/progress.py +42 -0
- context_aware_translation/core/term_memory.py +14 -0
- context_aware_translation/core/term_memory_builder.py +246 -0
- context_aware_translation/core/translation_strategies.py +202 -0
- context_aware_translation/documents/AGENTS.md +139 -0
- context_aware_translation/documents/__init__.py +0 -0
- context_aware_translation/documents/base.py +377 -0
- context_aware_translation/documents/content/AGENTS.md +162 -0
- context_aware_translation/documents/content/__init__.py +0 -0
- context_aware_translation/documents/content/ocr_content.py +235 -0
- context_aware_translation/documents/content/ocr_items.py +940 -0
- context_aware_translation/documents/epub.py +2669 -0
- context_aware_translation/documents/epub_container.py +23 -0
- context_aware_translation/documents/epub_support/AGENTS.md +183 -0
- context_aware_translation/documents/epub_support/__init__.py +1 -0
- context_aware_translation/documents/epub_support/container_model.py +75 -0
- context_aware_translation/documents/epub_support/container_patch.py +43 -0
- context_aware_translation/documents/epub_support/container_reader.py +579 -0
- context_aware_translation/documents/epub_support/container_shared.py +109 -0
- context_aware_translation/documents/epub_support/container_writer.py +413 -0
- context_aware_translation/documents/epub_support/inline_markers.py +285 -0
- context_aware_translation/documents/epub_support/nav_ops.py +384 -0
- context_aware_translation/documents/epub_support/slot_lines.py +61 -0
- context_aware_translation/documents/epub_support/xml_utils.py +38 -0
- context_aware_translation/documents/epub_xhtml_utils.py +1457 -0
- context_aware_translation/documents/manga.py +695 -0
- context_aware_translation/documents/manga_alignment.py +88 -0
- context_aware_translation/documents/manga_reembed_planner.py +476 -0
- context_aware_translation/documents/pdf.py +757 -0
- context_aware_translation/documents/scanned_book.py +408 -0
- context_aware_translation/documents/subtitle.py +334 -0
- context_aware_translation/documents/text.py +265 -0
- context_aware_translation/llm/AGENTS.md +144 -0
- context_aware_translation/llm/__init__.py +1 -0
- context_aware_translation/llm/batch_jobs/AGENTS.md +148 -0
- context_aware_translation/llm/batch_jobs/__init__.py +21 -0
- context_aware_translation/llm/batch_jobs/base.py +101 -0
- context_aware_translation/llm/batch_jobs/gemini_gateway.py +668 -0
- context_aware_translation/llm/client.py +430 -0
- context_aware_translation/llm/epub_ocr.py +131 -0
- context_aware_translation/llm/extractor.py +341 -0
- context_aware_translation/llm/glossary_translator.py +254 -0
- context_aware_translation/llm/image_backend_base.py +94 -0
- context_aware_translation/llm/image_backends/AGENTS.md +199 -0
- context_aware_translation/llm/image_backends/__init__.py +1 -0
- context_aware_translation/llm/image_backends/gemini_backend.py +169 -0
- context_aware_translation/llm/image_backends/openai_backend.py +139 -0
- context_aware_translation/llm/image_backends/qwen_backend.py +172 -0
- context_aware_translation/llm/image_generator.py +109 -0
- context_aware_translation/llm/language_detector.py +162 -0
- context_aware_translation/llm/manga_ocr.py +302 -0
- context_aware_translation/llm/manga_translator.py +127 -0
- context_aware_translation/llm/ocr.py +205 -0
- context_aware_translation/llm/reviewer.py +173 -0
- context_aware_translation/llm/session_trace.py +38 -0
- context_aware_translation/llm/summarizor.py +352 -0
- context_aware_translation/llm/token_tracker.py +170 -0
- context_aware_translation/llm/translation_strategies.py +280 -0
- context_aware_translation/llm/translator.py +771 -0
- context_aware_translation/resources/opencc/config/hk2s.json +33 -0
- context_aware_translation/resources/opencc/config/jp2s.json +33 -0
- context_aware_translation/resources/opencc/config/s2hk.json +27 -0
- context_aware_translation/resources/opencc/config/s2t.json +22 -0
- context_aware_translation/resources/opencc/config/s2tw.json +27 -0
- context_aware_translation/resources/opencc/config/s2twp.json +32 -0
- context_aware_translation/resources/opencc/config/t2hk.json +16 -0
- context_aware_translation/resources/opencc/config/t2s.json +22 -0
- context_aware_translation/resources/opencc/config/t2tw.json +16 -0
- context_aware_translation/resources/opencc/config/tw2s.json +33 -0
- context_aware_translation/resources/opencc/config/tw2sp.json +36 -0
- context_aware_translation/resources/opencc/dictionary/HKVariants.txt +63 -0
- context_aware_translation/resources/opencc/dictionary/HKVariantsPhrases.txt +17 -0
- context_aware_translation/resources/opencc/dictionary/HKVariantsRev.txt +70 -0
- context_aware_translation/resources/opencc/dictionary/HKVariantsRevPhrases.txt +156 -0
- context_aware_translation/resources/opencc/dictionary/JPVariants.txt +367 -0
- context_aware_translation/resources/opencc/dictionary/JPVariantsRev.txt +367 -0
- context_aware_translation/resources/opencc/dictionary/STCharacters.txt +3980 -0
- context_aware_translation/resources/opencc/dictionary/STPhrases.txt +49051 -0
- context_aware_translation/resources/opencc/dictionary/TSCharacters.txt +4113 -0
- context_aware_translation/resources/opencc/dictionary/TSPhrases.txt +277 -0
- context_aware_translation/resources/opencc/dictionary/TWPhrases.txt +509 -0
- context_aware_translation/resources/opencc/dictionary/TWPhrasesRev.txt +518 -0
- context_aware_translation/resources/opencc/dictionary/TWVariants.txt +39 -0
- context_aware_translation/resources/opencc/dictionary/TWVariantsRev.txt +39 -0
- context_aware_translation/resources/opencc/dictionary/TWVariantsRevPhrases.txt +68 -0
- context_aware_translation/resources/tokenizers/deepseek-v3/special_tokens_map.json +23 -0
- context_aware_translation/resources/tokenizers/deepseek-v3/tokenizer.json +646418 -0
- context_aware_translation/resources/tokenizers/deepseek-v3/tokenizer_config.json +6562 -0
- context_aware_translation/storage/AGENTS.md +192 -0
- context_aware_translation/storage/__init__.py +3 -0
- context_aware_translation/storage/library/__init__.py +3 -0
- context_aware_translation/storage/library/book_manager.py +670 -0
- context_aware_translation/storage/models/__init__.py +3 -0
- context_aware_translation/storage/models/book.py +85 -0
- context_aware_translation/storage/models/config_profile.py +67 -0
- context_aware_translation/storage/models/endpoint_profile.py +97 -0
- context_aware_translation/storage/repositories/__init__.py +80 -0
- context_aware_translation/storage/repositories/document_repository.py +325 -0
- context_aware_translation/storage/repositories/llm_batch_store.py +165 -0
- context_aware_translation/storage/repositories/task_store.py +295 -0
- context_aware_translation/storage/repositories/term_repository.py +431 -0
- context_aware_translation/storage/repositories/translation_batch_task_store.py +315 -0
- context_aware_translation/storage/schema/__init__.py +17 -0
- context_aware_translation/storage/schema/book_db.py +1958 -0
- context_aware_translation/storage/schema/registry_db.py +949 -0
- context_aware_translation/storage/sqlite_locking.py +17 -0
- context_aware_translation/ui/AGENTS.md +115 -0
- context_aware_translation/ui/__init__.py +0 -0
- context_aware_translation/ui/chrome_sizing.py +21 -0
- context_aware_translation/ui/constants.py +104 -0
- context_aware_translation/ui/features/app_settings_pane.py +585 -0
- context_aware_translation/ui/features/app_setup_view.py +921 -0
- context_aware_translation/ui/features/document_images_view.py +725 -0
- context_aware_translation/ui/features/document_ocr_tab.py +1038 -0
- context_aware_translation/ui/features/document_translation_view.py +1401 -0
- context_aware_translation/ui/features/document_workspace_view.py +806 -0
- context_aware_translation/ui/features/library_view.py +401 -0
- context_aware_translation/ui/features/project_settings_pane.py +448 -0
- context_aware_translation/ui/features/queue_drawer_view.py +472 -0
- context_aware_translation/ui/features/terms_table_widget.py +599 -0
- context_aware_translation/ui/features/terms_view.py +1149 -0
- context_aware_translation/ui/features/work_view.py +697 -0
- context_aware_translation/ui/features/workflow_profile_editor.py +1312 -0
- context_aware_translation/ui/i18n.py +954 -0
- context_aware_translation/ui/json_utils.py +23 -0
- context_aware_translation/ui/main.py +140 -0
- context_aware_translation/ui/main_window.py +585 -0
- context_aware_translation/ui/qml/BootstrapProbe.qml +10 -0
- context_aware_translation/ui/qml/app/AppShellChrome.qml +161 -0
- context_aware_translation/ui/qml/dialogs/app_settings/AppSettingsDialogChrome.qml +77 -0
- context_aware_translation/ui/qml/dialogs/app_settings/AppSettingsPane.qml +131 -0
- context_aware_translation/ui/qml/dialogs/project_settings/ProjectSettingsDialogChrome.qml +77 -0
- context_aware_translation/ui/qml/dialogs/project_settings/ProjectSettingsPane.qml +205 -0
- context_aware_translation/ui/qml/document/DocumentShellChrome.qml +205 -0
- context_aware_translation/ui/qml/document/export/DocumentExportPaneChrome.qml +89 -0
- context_aware_translation/ui/qml/document/images/DocumentImagesPaneChrome.qml +457 -0
- context_aware_translation/ui/qml/document/ocr/DocumentOCRPaneChrome.qml +416 -0
- context_aware_translation/ui/qml/document/translation/DocumentTranslationPaneChrome.qml +136 -0
- context_aware_translation/ui/qml/project/ProjectShellChrome.qml +175 -0
- context_aware_translation/ui/qml/project/terms/TermsPaneChrome.qml +143 -0
- context_aware_translation/ui/qml/project/work_home/WorkHomeChrome.qml +331 -0
- context_aware_translation/ui/qml/queue/QueueShellChrome.qml +70 -0
- context_aware_translation/ui/qml_resources.py +57 -0
- context_aware_translation/ui/resources/__init__.py +0 -0
- context_aware_translation/ui/resources/styles.qss +283 -0
- context_aware_translation/ui/shell_hosts/__init__.py +11 -0
- context_aware_translation/ui/shell_hosts/app_settings_dialog_host.py +55 -0
- context_aware_translation/ui/shell_hosts/app_shell_host.py +77 -0
- context_aware_translation/ui/shell_hosts/document_shell_host.py +190 -0
- context_aware_translation/ui/shell_hosts/hybrid.py +156 -0
- context_aware_translation/ui/shell_hosts/project_settings_dialog_host.py +55 -0
- context_aware_translation/ui/shell_hosts/project_shell_host.py +144 -0
- context_aware_translation/ui/shell_hosts/queue_shell_host.py +60 -0
- context_aware_translation/ui/sleep_inhibitor.py +135 -0
- context_aware_translation/ui/startup.py +78 -0
- context_aware_translation/ui/tips.py +16 -0
- context_aware_translation/ui/translations/zh_CN.qm +0 -0
- context_aware_translation/ui/translations/zh_CN.ts +4351 -0
- context_aware_translation/ui/viewmodels/__init__.py +30 -0
- context_aware_translation/ui/viewmodels/app_settings_dialog.py +53 -0
- context_aware_translation/ui/viewmodels/app_settings_pane.py +63 -0
- context_aware_translation/ui/viewmodels/app_shell.py +87 -0
- context_aware_translation/ui/viewmodels/base.py +114 -0
- context_aware_translation/ui/viewmodels/document_export_pane.py +65 -0
- context_aware_translation/ui/viewmodels/document_images_pane.py +338 -0
- context_aware_translation/ui/viewmodels/document_ocr_pane.py +261 -0
- context_aware_translation/ui/viewmodels/document_shell.py +122 -0
- context_aware_translation/ui/viewmodels/document_translation_pane.py +113 -0
- context_aware_translation/ui/viewmodels/project_settings_dialog.py +51 -0
- context_aware_translation/ui/viewmodels/project_settings_pane.py +194 -0
- context_aware_translation/ui/viewmodels/project_shell.py +98 -0
- context_aware_translation/ui/viewmodels/queue_shell.py +51 -0
- context_aware_translation/ui/viewmodels/router.py +202 -0
- context_aware_translation/ui/viewmodels/terms_pane.py +230 -0
- context_aware_translation/ui/viewmodels/work_home.py +250 -0
- context_aware_translation/ui/widgets/AGENTS.md +90 -0
- context_aware_translation/ui/widgets/hybrid_controls.py +153 -0
- context_aware_translation/ui/widgets/image_viewer.py +343 -0
- context_aware_translation/ui/widgets/progress_widget.py +136 -0
- context_aware_translation/ui/widgets/table_support.py +59 -0
- context_aware_translation/ui/window_controllers.py +310 -0
- context_aware_translation/utils/AGENTS.md +157 -0
- context_aware_translation/utils/__init__.py +3 -0
- context_aware_translation/utils/chunking.py +147 -0
- context_aware_translation/utils/cjk_normalize.py +141 -0
- context_aware_translation/utils/compression_marker.py +18 -0
- context_aware_translation/utils/file_utils.py +34 -0
- context_aware_translation/utils/hard_wrap.py +87 -0
- context_aware_translation/utils/hashing.py +20 -0
- context_aware_translation/utils/image_utils.py +79 -0
- context_aware_translation/utils/llm_json_cleaner.py +91 -0
- context_aware_translation/utils/markdown_escape.py +195 -0
- context_aware_translation/utils/pandoc_export.py +52 -0
- context_aware_translation/utils/semantic_chunker.py +92 -0
- context_aware_translation/utils/string_similarity.py +33 -0
- context_aware_translation/utils/symbol_check.py +29 -0
- context_aware_translation/workflow/AGENTS.md +171 -0
- context_aware_translation/workflow/__init__.py +1 -0
- context_aware_translation/workflow/bootstrap.py +123 -0
- context_aware_translation/workflow/image_fetcher.py +53 -0
- context_aware_translation/workflow/ops/__init__.py +1 -0
- context_aware_translation/workflow/ops/bootstrap_ops.py +202 -0
- context_aware_translation/workflow/ops/export_ops.py +234 -0
- context_aware_translation/workflow/ops/glossary_ops.py +116 -0
- context_aware_translation/workflow/ops/import_ops.py +108 -0
- context_aware_translation/workflow/ops/import_support.py +209 -0
- context_aware_translation/workflow/ops/ocr_ops.py +118 -0
- context_aware_translation/workflow/ops/translation_ops.py +156 -0
- context_aware_translation/workflow/runtime.py +27 -0
- context_aware_translation/workflow/session.py +69 -0
- context_aware_translation/workflow/task_runtime.py +89 -0
- context_aware_translation/workflow/tasks/AGENTS.md +176 -0
- context_aware_translation/workflow/tasks/__init__.py +0 -0
- context_aware_translation/workflow/tasks/claims.py +83 -0
- context_aware_translation/workflow/tasks/engine_core.py +740 -0
- context_aware_translation/workflow/tasks/exceptions.py +13 -0
- context_aware_translation/workflow/tasks/execution/AGENTS.md +126 -0
- context_aware_translation/workflow/tasks/execution/__init__.py +0 -0
- context_aware_translation/workflow/tasks/execution/batch_translation_executor.py +819 -0
- context_aware_translation/workflow/tasks/execution/batch_translation_ops.py +1190 -0
- context_aware_translation/workflow/tasks/glossary_preflight.py +138 -0
- context_aware_translation/workflow/tasks/handlers/AGENTS.md +245 -0
- context_aware_translation/workflow/tasks/handlers/__init__.py +0 -0
- context_aware_translation/workflow/tasks/handlers/base.py +42 -0
- context_aware_translation/workflow/tasks/handlers/batch_translation.py +221 -0
- context_aware_translation/workflow/tasks/handlers/chunk_retranslation.py +194 -0
- context_aware_translation/workflow/tasks/handlers/glossary_export.py +182 -0
- context_aware_translation/workflow/tasks/handlers/glossary_extraction.py +255 -0
- context_aware_translation/workflow/tasks/handlers/glossary_review.py +196 -0
- context_aware_translation/workflow/tasks/handlers/glossary_translation.py +177 -0
- context_aware_translation/workflow/tasks/handlers/image_reembedding.py +371 -0
- context_aware_translation/workflow/tasks/handlers/ocr.py +333 -0
- context_aware_translation/workflow/tasks/handlers/translate_and_export.py +183 -0
- context_aware_translation/workflow/tasks/handlers/translation_manga.py +252 -0
- context_aware_translation/workflow/tasks/handlers/translation_text.py +207 -0
- context_aware_translation/workflow/tasks/models.py +70 -0
- context_aware_translation/workflow/tasks/translate_and_export_support.py +410 -0
- context_aware_translation/workflow/tasks/worker_deps.py +28 -0
- contextweave-0.2.0.dist-info/METADATA +185 -0
- contextweave-0.2.0.dist-info/RECORD +306 -0
- contextweave-0.2.0.dist-info/WHEEL +4 -0
- contextweave-0.2.0.dist-info/entry_points.txt +3 -0
- 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))
|