paratext-cli 0.2.4__tar.gz → 0.4.0__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.
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-4 genericise-the-defaults-away-from-local-inference-servers.md +1 -2
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/AGENTS.md +24 -20
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/PKG-INFO +1 -1
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/pyproject.toml +1 -1
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/__init__.py +1 -1
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/cli.py +25 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/packaging.py +8 -3
- paratext_cli-0.4.0/src/paratext/review/runs.py +171 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/review/server.py +191 -4
- paratext_cli-0.4.0/src/paratext/review/sessions.py +125 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/review/static/app.js +212 -131
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/review/static/index.html +27 -0
- paratext_cli-0.4.0/src/paratext/workshop.py +105 -0
- paratext_cli-0.4.0/tests/test_runs.py +75 -0
- paratext_cli-0.4.0/tests/test_sessions.py +126 -0
- paratext_cli-0.4.0/tests/test_workshop.py +99 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/uv.lock +1 -1
- paratext_cli-0.2.4/backlog/config.yml +0 -16
- paratext_cli-0.2.4/backlog/milestones/m-0 - framework-split-and-first-release.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-1 - review-ui.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-2 - export-and-publishing.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-3 - oat-first-ui-rework.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-4 - packaging-and-pypi-release.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-5 - marc-fidelity.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-6 - test-coverage-and-onboarding.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-7 - export-v2.md +0 -8
- paratext_cli-0.2.4/backlog/milestones/m-8 - review-ui-polish.md +0 -8
- paratext_cli-0.2.4/backlog/tasks/task-1 - Split-the-framework-out-of-paratext-nls-and-release-it.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-10 - Remove-the-WattTime-provider.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-11 - Make-paratext-new-self-installing.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-12 - Add-paratext-guide-so-agents-can-self-orient.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-13 - Guard-schema-prompt-and-view-drift.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-14 - Build-the-human-corrected-gold-eval-set-in-review.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-15 - Add-MARC-and-Dublin-Core-export-formats.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-16 - Ship-schema.json-with-each-exported-dataset.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-17 - Add-the-project-inspector-and-fix-sqlite-thread-safety.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-18 - Point-the-default-card-detector-at-the-published-NLS-repo.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-19 - Add-the-scope-aware-export-modal.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-2 - Add-the-inbuilt-review-web-UI.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-20 - Move-the-review-UI-onto-Oat.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-21 - Render-entries-as-tables-read-only-and-editable.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-22 - Publish-gold-to-Hugging-Face-with-Sign-in-with-HF.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-23 - Warn-on-incomplete-provenance-without-blocking-publication.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-24 - Add-the-paratext-logo-and-favicon.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-25 - Remove-duplicated-helpers-across-the-codebase.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-26 - Split-Store-and-dataset-loading-out-of-the-review-server.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-27 - Lean-the-README-and-split-the-reference-into-docs.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-28 - Add-NOTICE-and-make-the-repository-public.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-29 - Rename-the-distribution-to-paratext-cli-and-publish-to-PyPI.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-3 - Make-the-review-server-configurable.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-30 - Make-project-discovery-work-from-any-environment.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-31 - Give-clear-errors-for-provider-and-source-misconfiguration.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-32 - Add-a-configurable-output-budget-and-provider-extra-body-passthrough.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-33 - Bring-MARC-output-up-to-ISBD-punctuation-and-leader-conventions.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-34 - Replace-the-blind-margin-crop-with-a-content-aware-fallback.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-35 - Add-the-opt-in-AI-assistance-note-to-MARC-and-DC-export.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-36 - Emit-only-one-main-entry-per-MARC-record.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-37 - Extend-CLI-test-coverage-to-argparse-wiring-round-resolution-and-config-layering.md +0 -30
- paratext_cli-0.2.4/backlog/tasks/task-38 - Walk-a-real-run-from-a-fresh-scaffold.md +0 -28
- paratext_cli-0.2.4/backlog/tasks/task-39 - Add-the-paratext-dependency-when-scaffolding-into-an-existing-project.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-4 - Genericise-the-defaults-away-from-local-inference-servers.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-40 - Fix-cross-batch-accumulation-overwriting-metadata.jsonl.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-41 - Remove-the-silent-hf-default-from-format.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-42 - Add-paratext-export-db.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-43 - Decide-whether-a-gold-export-ships-as-a-held-out-test-split.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-44 - HF-export-v2-multi-image-projects-and-round-versioning-as-Hub-revisions.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-45 - Add-paratext-bench.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-46 - Review-UI-polish-flagged-stat-key-score-badge-and-verdict-toggles.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-47 - Eval-editor-reach-View-hidden-fields-and-diff-corrected-against-model-output.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-48 - Add-richer-scaffolder-field-types-for-enums-and-lists.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-49 - Extend-the-carbon-providers.md +0 -25
- paratext_cli-0.2.4/backlog/tasks/task-5 - Add-source-adapters-and-slim-the-project-layout.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-50 - Build-the-wordmark-in-a-mono-typeface.md +0 -24
- paratext_cli-0.2.4/backlog/tasks/task-51 - Decide-whether-to-pursue-the-PyPI-paratext-name.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-6 - Rename-init-to-new-and-document-every-CLI-flag.md +0 -26
- paratext_cli-0.2.4/backlog/tasks/task-7 - Key-runs-and-packages-on-the-prompt-hash.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-8 - Add-paratext-export-for-publishing-a-reviewed-round.md +0 -27
- paratext_cli-0.2.4/backlog/tasks/task-9 - Add-carbon-aware-scheduling.md +0 -27
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.github/workflows/publish.yml +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.gitignore +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/intro.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-1 split-the-framework-out-of-paratext-nls-and-release-it.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-10 remove-the-watttime-provider.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-11 make-paratext-new-self-installing.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-12 add-paratext-guide-so-agents-can-self-orient.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-13 guard-schema-prompt-and-view-drift.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-14 build-the-human-corrected-gold-eval-set-in-review.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-15 add-marc-and-dublin-core-export-formats.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-16 ship-schema-json-with-each-exported-dataset.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-17 add-the-project-inspector-and-fix-sqlite-thread-safety.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-18 point-the-default-card-detector-at-the-published-nls-repo.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-19 add-the-scope-aware-export-modal.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-2 add-the-inbuilt-review-web-ui.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-20 move-the-review-ui-onto-oat.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-21 render-entries-as-tables-read-only-and-editable.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-22 publish-gold-to-hugging-face-with-sign-in-with-hf.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-23 warn-on-incomplete-provenance-without-blocking-publication.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-24 add-the-paratext-logo-and-favicon.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-25 remove-duplicated-helpers-across-the-codebase.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-26 split-store-and-dataset-loading-out-of-the-review-server.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-27 lean-the-readme-and-split-the-reference-into-docs.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-28 add-notice-and-make-the-repository-public.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-29 rename-the-distribution-to-paratext-cli-and-publish-to-pypi.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-3 make-the-review-server-configurable.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-30 make-project-discovery-work-from-any-environment.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-31 give-clear-errors-for-provider-and-source-misconfiguration.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-32 add-a-configurable-output-budget-and-provider-extra-body-pas.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-33 bring-marc-output-up-to-isbd-punctuation-and-leader-conventi.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-34 replace-the-blind-margin-crop-with-a-content-aware-fallback.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-35 add-the-opt-in-ai-assistance-note-to-marc-and-dc-export.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-36 emit-only-one-main-entry-per-marc-record.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-37 extend-cli-test-coverage-to-argparse-wiring-round-resolution.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-38 walk-a-real-run-from-a-fresh-scaffold.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-39 add-the-paratext-dependency-when-scaffolding-into-an-existin.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-40 fix-cross-batch-accumulation-overwriting-metadata-jsonl.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-41 remove-the-silent-hf-default-from-format.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-42 add-paratext-export-db.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-43 decide-whether-a-gold-export-ships-as-a-held-out-test-split.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-44 hf-export-v2-multi-image-projects-and-round-versioning-as-hu.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-45 add-paratext-bench.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-46 review-ui-polish-flagged-stat-key-score-badge-and-verdict-to.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-47 eval-editor-reach-view-hidden-fields-and-diff-corrected-agai.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-48 add-richer-scaffolder-field-types-for-enums-and-lists.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-49 extend-the-carbon-providers.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-5 add-source-adapters-and-slim-the-project-layout.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-50 build-the-wordmark-in-a-mono-typeface.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-51 decide-whether-to-pursue-the-pypi-paratext-name.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-6 rename-init-to-new-and-document-every-cli-flag.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-7 key-runs-and-packages-on-the-prompt-hash.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-8 add-paratext-export-for-publishing-a-reviewed-round.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/.helical/tasks/PTX-9 add-carbon-aware-scheduling.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/LICENSE +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/NOTICE +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/README.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/assets/logo.png +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/assets/logo.svg +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/docs/configuration.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/docs/export.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/docs/green-scheduling.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/docs/hf-export-spec.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/docs/publishing.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/docs/scanned-cards.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/paratext.example.toml +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/AGENTS.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/carbon.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/cards.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/catalogue.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/config.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/datasets.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/extract.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/hf_export.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/inspect.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/io.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/paratext.example.toml +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/projects/__init__.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/projects/card_template/__init__.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/projects/card_template/prompt.md +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/projects/card_template/schema.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/records.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/review/__init__.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/review/hf_oauth.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/review/static/favicon.svg +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/review/static/oat.min.css +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/review/static/oat.min.js +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/runner.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/scaffold.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/sources.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/src/paratext/store.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_carbon.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_catalogue.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_cli.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_config.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_extract.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_guide.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_hf_export.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_io.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_packaging.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_project_audit.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_records.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_review.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_rounds.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_runner.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_scaffold.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_show_through.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_sources.py +0 -0
- {paratext_cli-0.2.4 → paratext_cli-0.4.0}/tests/test_verso.py +0 -0
|
@@ -4,11 +4,10 @@ title: Genericise the defaults away from local inference servers
|
|
|
4
4
|
status: done
|
|
5
5
|
horizon: now
|
|
6
6
|
flow: clear
|
|
7
|
-
priority: high
|
|
8
7
|
labels:
|
|
9
8
|
- docs
|
|
10
9
|
created: '2026-06-28'
|
|
11
|
-
updated: '2026-09-
|
|
10
|
+
updated: '2026-09-03'
|
|
12
11
|
---
|
|
13
12
|
|
|
14
13
|
Defaults naming Lemonade and LM Studio make the framework look like it only runs against one person's setup.
|
|
@@ -267,35 +267,39 @@ alongside `_api_export_catalogue`.
|
|
|
267
267
|
- **Bump `schema_version`** when a schema changes.
|
|
268
268
|
- **Commits:** short messages, no Co-Authored-By trailer.
|
|
269
269
|
|
|
270
|
-
##
|
|
270
|
+
## Tasks in this repo are public
|
|
271
271
|
|
|
272
|
-
This repository is public, and
|
|
272
|
+
This repository is public, and `.helical/` is committed with it. Task titles,
|
|
273
273
|
descriptions and notes are visible to anyone.
|
|
274
274
|
|
|
275
275
|
Keep repo tasks to engineering work. Anything naming NLS internals, staff,
|
|
276
276
|
suppliers, unannounced plans or licensing negotiations belongs in the
|
|
277
|
-
`paratext-nls`
|
|
277
|
+
`paratext-nls` board or the `admin` project instead.
|
|
278
278
|
|
|
279
|
-
<!--
|
|
280
|
-
|
|
281
|
-
<CRITICAL_INSTRUCTION>
|
|
279
|
+
<!-- HELICAL START -->
|
|
280
|
+
## Tasks
|
|
282
281
|
|
|
283
|
-
|
|
282
|
+
This project's tasks live in `.helical/tasks/` and are managed with the `helical`
|
|
283
|
+
command. Read them before starting, and record what you find.
|
|
284
284
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
285
|
+
```bash
|
|
286
|
+
helical ls -p <project> # open work here
|
|
287
|
+
helical show <ID> # one task in full
|
|
288
|
+
helical set <ID> --status doing # todo | doing | done
|
|
289
|
+
helical set <ID> --horizon next # now | next | future
|
|
290
|
+
helical set <ID> --flow waiting --blocker "why"
|
|
291
|
+
helical new "Title" -p <project> --horizon next
|
|
292
|
+
```
|
|
290
293
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
- `backlog instructions task-finalization` before checking acceptance criteria, writing final summaries, or moving tasks to terminal statuses
|
|
294
|
+
`--json` works on every command. A task carries both a **status** (how far along)
|
|
295
|
+
and a **horizon** (when it matters); the routemap across all projects is built from
|
|
296
|
+
horizons, so set one deliberately.
|
|
295
297
|
|
|
296
|
-
|
|
298
|
+
**Flow** says who can unstick a task: `waiting` sits with someone else and needs
|
|
299
|
+
chasing, `blocked` sits with us and needs a decision. Neither is accepted without
|
|
300
|
+
`--blocker` explaining it.
|
|
297
301
|
|
|
298
|
-
|
|
302
|
+
Labels come from a fixed vocabulary; `helical new --help` lists it.
|
|
299
303
|
|
|
300
|
-
|
|
301
|
-
<!--
|
|
304
|
+
Boards: <https://helical.ai.nls.uk> · read-only <https://projects.ai.nls.uk>
|
|
305
|
+
<!-- HELICAL END -->
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# the plain name on PyPI belongs to an unrelated project, so the distribution
|
|
4
4
|
# carries the `-cli` suffix.
|
|
5
5
|
name = "paratext-cli"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.4.0"
|
|
7
7
|
description = "Modular, project-based metadata extraction from digitised library/archive collections with a multimodal model"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
# Core is pure-Python + numpy/pillow/pydantic. The [detector] extra needs
|
|
@@ -427,6 +427,22 @@ def _cmd_carbon(args: argparse.Namespace) -> int:
|
|
|
427
427
|
|
|
428
428
|
|
|
429
429
|
# ── Review ────────────────────────────────────────────────────────────────
|
|
430
|
+
def _workshop_endpoint(args: argparse.Namespace) -> dict:
|
|
431
|
+
"""Where workshop runs send their model calls. Comes from the same
|
|
432
|
+
config/env layer as `run`, so a Space configures it with PARATEXT_* vars."""
|
|
433
|
+
merged = {**HARDCODED_DEFAULTS, **coerce_paths(load_defaults(None))}
|
|
434
|
+
base_url, note = normalise_base_url(str(merged.get("base_url") or ""))
|
|
435
|
+
if note:
|
|
436
|
+
print(note)
|
|
437
|
+
source = args.workshop_source or merged.get("source")
|
|
438
|
+
return {
|
|
439
|
+
"base_url": base_url,
|
|
440
|
+
"api_key": merged.get("api_key"),
|
|
441
|
+
"model": merged.get("model"),
|
|
442
|
+
"source": str(source) if source else None,
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
|
|
430
446
|
def _cmd_review(args: argparse.Namespace) -> int:
|
|
431
447
|
from .review import serve
|
|
432
448
|
|
|
@@ -439,6 +455,8 @@ def _cmd_review(args: argparse.Namespace) -> int:
|
|
|
439
455
|
# A missing default root means "nothing run yet", not a mistake — serve
|
|
440
456
|
# anyway so Projects and the homepage guidance are reachable.
|
|
441
457
|
allow_empty=args.data_dir == REVIEW_ROOT,
|
|
458
|
+
workshop=args.workshop,
|
|
459
|
+
endpoint=_workshop_endpoint(args) if args.workshop else None,
|
|
442
460
|
)
|
|
443
461
|
return 0
|
|
444
462
|
|
|
@@ -779,6 +797,13 @@ def _build_parser() -> tuple[
|
|
|
779
797
|
"nginx; use 0.0.0.0 to expose directly on the network)")
|
|
780
798
|
rv.add_argument("--db", type=Path, default=None,
|
|
781
799
|
help="Annotations SQLite path (default: <data_dir>/annotations.db)")
|
|
800
|
+
rv.add_argument("--workshop", metavar="DIR", default=None,
|
|
801
|
+
help="Workshop mode: give each browser its own workspace under DIR "
|
|
802
|
+
"(prompt, fields, rounds and verdicts), seeded with the rounds "
|
|
803
|
+
"served here")
|
|
804
|
+
rv.add_argument("--workshop-source", metavar="DIR", default=None,
|
|
805
|
+
help="With --workshop: the images attendees' runs read from "
|
|
806
|
+
"(default: the configured `source`)")
|
|
782
807
|
rv.add_argument("--no-open", action="store_true", help="Don't open a browser")
|
|
783
808
|
rv.set_defaults(func=_cmd_review)
|
|
784
809
|
|
|
@@ -25,7 +25,7 @@ from pathlib import Path
|
|
|
25
25
|
from PIL import Image
|
|
26
26
|
|
|
27
27
|
from .io import iter_records, read_provenance
|
|
28
|
-
from .projects import KEEP, Curation, build_view, get_project
|
|
28
|
+
from .projects import KEEP, Curation, Project, build_view, get_project
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger(__name__)
|
|
31
31
|
|
|
@@ -71,9 +71,14 @@ def package(
|
|
|
71
71
|
*,
|
|
72
72
|
fresh: bool = False,
|
|
73
73
|
image_max_size: int = 1024,
|
|
74
|
+
proj: "Project | None" = None,
|
|
74
75
|
) -> tuple[int, dict[str, int]]:
|
|
75
|
-
"""Write samples.json + images/ for `paratext review`. Returns (kept, skipped_by_reason).
|
|
76
|
-
|
|
76
|
+
"""Write samples.json + images/ for `paratext review`. Returns (kept, skipped_by_reason).
|
|
77
|
+
|
|
78
|
+
`proj` overrides entry-point lookup, for a project built at runtime rather
|
|
79
|
+
than installed — workshop mode, where the schema is edited in the browser.
|
|
80
|
+
"""
|
|
81
|
+
proj = proj or get_project(project)
|
|
77
82
|
if fresh and out.exists():
|
|
78
83
|
shutil.rmtree(out)
|
|
79
84
|
(out / "images").mkdir(parents=True, exist_ok=True)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Background extraction runs for workshop mode.
|
|
2
|
+
|
|
3
|
+
`extract.run` is built for a sweep: resume, tqdm, a whole collection. A workshop
|
|
4
|
+
run is five cards that somebody is watching, so this is a small loop that
|
|
5
|
+
reports progress per card and can be polled.
|
|
6
|
+
|
|
7
|
+
One job per session at a time. A run costs money, so the caps here are the
|
|
8
|
+
thing standing between a public Space and someone's credit balance.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from openai import OpenAI
|
|
21
|
+
|
|
22
|
+
from ..extract import _prompt_hash
|
|
23
|
+
from ..io import append_jsonl, write_provenance_header
|
|
24
|
+
from ..packaging import package
|
|
25
|
+
from ..projects import Project
|
|
26
|
+
from ..runner import call_structured
|
|
27
|
+
from ..sources import image_source
|
|
28
|
+
from ..workshop import build_schema
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
MAX_CARDS = 5 # per run
|
|
33
|
+
MAX_RUNS_PER_SESSION = 40
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Job:
|
|
38
|
+
id: str
|
|
39
|
+
total: int
|
|
40
|
+
done: int = 0
|
|
41
|
+
status: str = "running" # running | done | error
|
|
42
|
+
error: str = ""
|
|
43
|
+
round_name: str = ""
|
|
44
|
+
failures: list[str] = field(default_factory=list)
|
|
45
|
+
|
|
46
|
+
def as_dict(self) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"id": self.id,
|
|
49
|
+
"total": self.total,
|
|
50
|
+
"done": self.done,
|
|
51
|
+
"status": self.status,
|
|
52
|
+
"error": self.error,
|
|
53
|
+
"round": self.round_name,
|
|
54
|
+
"failures": self.failures,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Runs:
|
|
59
|
+
"""Tracks one job per session, and how many runs a session has spent."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, max_runs: int = MAX_RUNS_PER_SESSION):
|
|
62
|
+
self._jobs: dict[str, Job] = {}
|
|
63
|
+
self._by_session: dict[str, str] = {}
|
|
64
|
+
self._spent: dict[str, int] = {}
|
|
65
|
+
self._lock = threading.Lock()
|
|
66
|
+
self.max_runs = max_runs
|
|
67
|
+
|
|
68
|
+
def active(self, session_id: str) -> Job | None:
|
|
69
|
+
with self._lock:
|
|
70
|
+
job = self._jobs.get(self._by_session.get(session_id, ""))
|
|
71
|
+
return job if job and job.status == "running" else None
|
|
72
|
+
|
|
73
|
+
def get(self, job_id: str) -> Job | None:
|
|
74
|
+
with self._lock:
|
|
75
|
+
return self._jobs.get(job_id)
|
|
76
|
+
|
|
77
|
+
def spent(self, session_id: str) -> int:
|
|
78
|
+
with self._lock:
|
|
79
|
+
return self._spent.get(session_id, 0)
|
|
80
|
+
|
|
81
|
+
def start(self, session_id: str, total: int, work) -> Job:
|
|
82
|
+
"""Register a job and run `work(job)` on a thread."""
|
|
83
|
+
with self._lock:
|
|
84
|
+
if self._spent.get(session_id, 0) >= self.max_runs:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"This session has used its {self.max_runs} runs. "
|
|
87
|
+
f"Reload with a fresh session to continue."
|
|
88
|
+
)
|
|
89
|
+
self._spent[session_id] = self._spent.get(session_id, 0) + 1
|
|
90
|
+
job = Job(id=uuid.uuid4().hex[:12], total=total)
|
|
91
|
+
self._jobs[job.id] = job
|
|
92
|
+
self._by_session[session_id] = job.id
|
|
93
|
+
|
|
94
|
+
def target():
|
|
95
|
+
try:
|
|
96
|
+
work(job)
|
|
97
|
+
job.status = "done" if job.status == "running" else job.status
|
|
98
|
+
except Exception as e: # noqa: BLE001 — surfaced to the UI
|
|
99
|
+
logger.exception("workshop run failed")
|
|
100
|
+
job.status, job.error = "error", str(e)
|
|
101
|
+
|
|
102
|
+
threading.Thread(target=target, daemon=True).start()
|
|
103
|
+
return job
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def workshop_project(name: str, prompt: str, fields: list[dict]) -> Project:
|
|
107
|
+
"""A Project assembled from what the attendee typed, not from an installed
|
|
108
|
+
package."""
|
|
109
|
+
return Project(
|
|
110
|
+
name=name,
|
|
111
|
+
schema_version="v1",
|
|
112
|
+
prompt=prompt,
|
|
113
|
+
schema=build_schema(fields),
|
|
114
|
+
source=image_source(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def extract_and_package(
|
|
119
|
+
job: Job,
|
|
120
|
+
*,
|
|
121
|
+
proj: Project,
|
|
122
|
+
source: Path,
|
|
123
|
+
output: Path,
|
|
124
|
+
review_out: Path,
|
|
125
|
+
base_url: str,
|
|
126
|
+
api_key: str,
|
|
127
|
+
model: str,
|
|
128
|
+
cards: int,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Run the model over `cards` samples, then package a round. Updates `job`
|
|
131
|
+
as it goes so the browser can draw a progress bar."""
|
|
132
|
+
client = OpenAI(base_url=base_url, api_key=api_key)
|
|
133
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
if output.exists():
|
|
135
|
+
output.unlink() # a workshop run is always a fresh round
|
|
136
|
+
write_provenance_header(output, {
|
|
137
|
+
"project": proj.name,
|
|
138
|
+
"schema_version": proj.schema_version,
|
|
139
|
+
"prompt_hash": _prompt_hash(proj.prompt),
|
|
140
|
+
"prompt": proj.prompt,
|
|
141
|
+
"model": model,
|
|
142
|
+
"base_url": base_url,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
for sample in proj.source.iter_samples(source, cards):
|
|
146
|
+
t0 = time.monotonic()
|
|
147
|
+
try:
|
|
148
|
+
parsed = call_structured(
|
|
149
|
+
client, model=model, prompt=proj.prompt, images=sample.images,
|
|
150
|
+
schema=proj.schema, image_max_size=proj.image_max_size,
|
|
151
|
+
image_quality=proj.image_quality,
|
|
152
|
+
)
|
|
153
|
+
append_jsonl(output, {
|
|
154
|
+
"id": sample.id,
|
|
155
|
+
"extraction": parsed.model_dump(),
|
|
156
|
+
"metadata": sample.metadata,
|
|
157
|
+
"elapsed_s": round(time.monotonic() - t0, 3),
|
|
158
|
+
})
|
|
159
|
+
except Exception as e: # noqa: BLE001
|
|
160
|
+
job.failures.append(f"{sample.id}: {e}")
|
|
161
|
+
logger.warning("[%s] failed: %s", sample.id, e)
|
|
162
|
+
finally:
|
|
163
|
+
job.done += 1
|
|
164
|
+
|
|
165
|
+
if job.done and len(job.failures) == job.done:
|
|
166
|
+
raise RuntimeError(f"every card failed — {job.failures[0]}")
|
|
167
|
+
|
|
168
|
+
kept, _ = package(output, review_out, proj.name, fresh=True, proj=proj)
|
|
169
|
+
job.round_name = review_out.name
|
|
170
|
+
if not kept:
|
|
171
|
+
raise RuntimeError("nothing survived packaging")
|
|
@@ -18,6 +18,7 @@ import io
|
|
|
18
18
|
import json
|
|
19
19
|
import logging
|
|
20
20
|
import webbrowser
|
|
21
|
+
from http.cookies import SimpleCookie
|
|
21
22
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from urllib.parse import parse_qs, unquote, urlparse
|
|
@@ -32,6 +33,9 @@ from ..datasets import (
|
|
|
32
33
|
schema_history,
|
|
33
34
|
)
|
|
34
35
|
from ..store import Store, default_db_path
|
|
36
|
+
from ..workshop import normalise_fields
|
|
37
|
+
from .runs import MAX_CARDS, Runs, extract_and_package, workshop_project
|
|
38
|
+
from .sessions import COOKIE_NAME, Sessions
|
|
35
39
|
|
|
36
40
|
logger = logging.getLogger(__name__)
|
|
37
41
|
|
|
@@ -67,18 +71,141 @@ _MIME = {
|
|
|
67
71
|
|
|
68
72
|
|
|
69
73
|
class Handler(BaseHTTPRequestHandler):
|
|
70
|
-
data_dir
|
|
71
|
-
|
|
74
|
+
# The single-tenant defaults. In workshop mode `data_dir` and `store` below
|
|
75
|
+
# resolve per request instead, so one container serves a room of people
|
|
76
|
+
# without them overwriting each other.
|
|
77
|
+
base_data_dir: Path
|
|
78
|
+
base_store: Store
|
|
79
|
+
sessions: Sessions | None = None
|
|
80
|
+
runs: Runs = Runs()
|
|
81
|
+
workshop_defaults: dict = {}
|
|
72
82
|
|
|
73
83
|
def log_message(self, *a): # quiet by default
|
|
74
84
|
pass
|
|
75
85
|
|
|
86
|
+
# -- workshop mode --
|
|
87
|
+
def _workshop_or_404(self):
|
|
88
|
+
session = self._session()
|
|
89
|
+
if session is None:
|
|
90
|
+
raise FileNotFoundError("workshop mode is not enabled on this server")
|
|
91
|
+
return session
|
|
92
|
+
|
|
93
|
+
def _api_workshop_state(self):
|
|
94
|
+
"""The attendee's prompt and fields, falling back to the defaults this
|
|
95
|
+
server was started with."""
|
|
96
|
+
session = self._workshop_or_404()
|
|
97
|
+
state = session.read_state()
|
|
98
|
+
self._json({
|
|
99
|
+
"session": session.id,
|
|
100
|
+
"prompt": state.get("prompt", type(self).workshop_defaults.get("prompt", "")),
|
|
101
|
+
"fields": state.get("fields", type(self).workshop_defaults.get("fields", [])),
|
|
102
|
+
"runs_used": self.runs.spent(session.id),
|
|
103
|
+
"max_runs": self.runs.max_runs,
|
|
104
|
+
"max_cards": MAX_CARDS,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
def _api_workshop_save(self):
|
|
108
|
+
session = self._workshop_or_404()
|
|
109
|
+
body = self._body()
|
|
110
|
+
fields = normalise_fields(body.get("fields") or [])
|
|
111
|
+
if not fields:
|
|
112
|
+
return self._json({"error": "a schema needs at least one field"}, 400)
|
|
113
|
+
session.write_state({"prompt": body.get("prompt") or "", "fields": fields})
|
|
114
|
+
self._json({"ok": True, "fields": fields})
|
|
115
|
+
|
|
116
|
+
def _api_workshop_run(self):
|
|
117
|
+
session = self._workshop_or_404()
|
|
118
|
+
if self.runs.active(session.id):
|
|
119
|
+
return self._json({"error": "a run is already going"}, 409)
|
|
120
|
+
|
|
121
|
+
body = self._body()
|
|
122
|
+
prompt = (body.get("prompt") or "").strip()
|
|
123
|
+
fields = normalise_fields(body.get("fields") or [])
|
|
124
|
+
if not prompt:
|
|
125
|
+
return self._json({"error": "the prompt is empty"}, 400)
|
|
126
|
+
if not fields:
|
|
127
|
+
return self._json({"error": "a schema needs at least one field"}, 400)
|
|
128
|
+
cards = max(1, min(int(body.get("cards") or MAX_CARDS), MAX_CARDS))
|
|
129
|
+
|
|
130
|
+
cfg = type(self).workshop_defaults
|
|
131
|
+
if not cfg.get("source") or not Path(cfg["source"]).is_dir():
|
|
132
|
+
return self._json({"error": "this server has no source images configured"}, 400)
|
|
133
|
+
|
|
134
|
+
session.write_state({"prompt": prompt, "fields": fields})
|
|
135
|
+
name = cfg.get("project", "workshop")
|
|
136
|
+
proj = workshop_project(name, prompt, fields)
|
|
137
|
+
# A new round per prompt: the round number is just how many the attendee
|
|
138
|
+
# has made, so r1/r2/r3 read as their own iteration history.
|
|
139
|
+
existing = sorted(session.data_dir.glob(f"{name}-r*"))
|
|
140
|
+
n = len(existing) + 1
|
|
141
|
+
review_out = session.data_dir / f"{name}-r{n}"
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
job = self.runs.start(
|
|
145
|
+
session.id, cards,
|
|
146
|
+
lambda j: extract_and_package(
|
|
147
|
+
j, proj=proj, source=Path(cfg["source"]),
|
|
148
|
+
output=session.dir / "output" / f"{name}-r{n}.jsonl",
|
|
149
|
+
review_out=review_out,
|
|
150
|
+
base_url=cfg["base_url"], api_key=cfg["api_key"],
|
|
151
|
+
model=cfg["model"], cards=cards,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
except ValueError as e:
|
|
155
|
+
return self._json({"error": str(e)}, 429)
|
|
156
|
+
self._json(job.as_dict(), 202)
|
|
157
|
+
|
|
158
|
+
def _api_workshop_job(self, job_id):
|
|
159
|
+
self._workshop_or_404()
|
|
160
|
+
job = self.runs.get(job_id)
|
|
161
|
+
if not job:
|
|
162
|
+
raise FileNotFoundError(f"no such job: {job_id}")
|
|
163
|
+
self._json(job.as_dict())
|
|
164
|
+
|
|
165
|
+
# -- per-request session --
|
|
166
|
+
def _session(self):
|
|
167
|
+
"""The caller's workshop session, creating one on first request. None
|
|
168
|
+
outside workshop mode, where the server is single-tenant."""
|
|
169
|
+
if self.sessions is None:
|
|
170
|
+
return None
|
|
171
|
+
if getattr(self, "_session_cache", None) is None:
|
|
172
|
+
cookie = SimpleCookie(self.headers.get("cookie") or "")
|
|
173
|
+
morsel = cookie.get(COOKIE_NAME)
|
|
174
|
+
session, created = self.sessions.get_or_create(morsel.value if morsel else None)
|
|
175
|
+
self._session_cache = session
|
|
176
|
+
self._set_cookie = created
|
|
177
|
+
return self._session_cache
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def data_dir(self) -> Path:
|
|
181
|
+
session = self._session()
|
|
182
|
+
return session.data_dir if session else type(self).base_data_dir
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def store(self) -> Store:
|
|
186
|
+
session = self._session()
|
|
187
|
+
if not session:
|
|
188
|
+
return type(self).base_store
|
|
189
|
+
if getattr(self, "_store_cache", None) is None:
|
|
190
|
+
self._store_cache = session.store()
|
|
191
|
+
return self._store_cache
|
|
192
|
+
|
|
193
|
+
def _session_headers(self) -> None:
|
|
194
|
+
"""Hand the browser its session on the response that created it."""
|
|
195
|
+
if getattr(self, "_set_cookie", False):
|
|
196
|
+
self.send_header(
|
|
197
|
+
"set-cookie",
|
|
198
|
+
f"{COOKIE_NAME}={self._session_cache.id}; Path=/; SameSite=Lax; Max-Age=86400",
|
|
199
|
+
)
|
|
200
|
+
self._set_cookie = False
|
|
201
|
+
|
|
76
202
|
# -- helpers --
|
|
77
203
|
def _json(self, obj, status=200):
|
|
78
204
|
body = json.dumps(obj).encode()
|
|
79
205
|
self.send_response(status)
|
|
80
206
|
self.send_header("content-type", "application/json")
|
|
81
207
|
self.send_header("content-length", str(len(body)))
|
|
208
|
+
self._session_headers()
|
|
82
209
|
self.end_headers()
|
|
83
210
|
self.wfile.write(body)
|
|
84
211
|
|
|
@@ -88,6 +215,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
88
215
|
self.send_header("content-length", str(len(body)))
|
|
89
216
|
for k, v in (headers or {}).items():
|
|
90
217
|
self.send_header(k, v)
|
|
218
|
+
self._session_headers()
|
|
91
219
|
self.end_headers()
|
|
92
220
|
self.wfile.write(body)
|
|
93
221
|
|
|
@@ -129,6 +257,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
129
257
|
return self._api_prompts(self._dataset(qs))
|
|
130
258
|
if path == "/api/schema":
|
|
131
259
|
return self._api_schema(self._dataset(qs))
|
|
260
|
+
if path == "/api/workshop/state":
|
|
261
|
+
return self._api_workshop_state()
|
|
262
|
+
if path.startswith("/api/workshop/job/"):
|
|
263
|
+
return self._api_workshop_job(path.rsplit("/", 1)[1])
|
|
132
264
|
if path == "/api/export/fields":
|
|
133
265
|
return self._api_export_fields(self._dataset(qs), qs)
|
|
134
266
|
if path in ("/api/export/marc", "/api/export/dc"):
|
|
@@ -164,6 +296,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
164
296
|
return self._api_export_catalogue(
|
|
165
297
|
self._dataset(qs), u.path.rsplit("/", 1)[1], qs, mapping=body.get("mapping")
|
|
166
298
|
)
|
|
299
|
+
if u.path == "/api/workshop/state":
|
|
300
|
+
return self._api_workshop_save()
|
|
301
|
+
if u.path == "/api/workshop/run":
|
|
302
|
+
return self._api_workshop_run()
|
|
167
303
|
if u.path == "/api/oauth/hf/exchange":
|
|
168
304
|
return self._api_hf_exchange()
|
|
169
305
|
if u.path == "/api/export/hf":
|
|
@@ -623,6 +759,40 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
623
759
|
)
|
|
624
760
|
|
|
625
761
|
|
|
762
|
+
def _workshop_defaults(datasets: list[dict], endpoint: dict) -> dict:
|
|
763
|
+
"""Seed an attendee's starting prompt and fields from the newest round being
|
|
764
|
+
served, so the Space needs no separate copy of either. The endpoint comes
|
|
765
|
+
from the usual config/env layer."""
|
|
766
|
+
newest = max(datasets, key=lambda d: d.get("round") or 0, default=None)
|
|
767
|
+
prompt, fields, project = "", [], "workshop"
|
|
768
|
+
if newest:
|
|
769
|
+
project = newest.get("base") or project
|
|
770
|
+
try:
|
|
771
|
+
prov = json.loads((newest["dir"] / "provenance.json").read_text())
|
|
772
|
+
prompt = prov.get("prompt", "")
|
|
773
|
+
except (OSError, json.JSONDecodeError, KeyError):
|
|
774
|
+
pass
|
|
775
|
+
try:
|
|
776
|
+
view = json.loads((newest["dir"] / "view.json").read_text())
|
|
777
|
+
for panel in view.get("panels", []):
|
|
778
|
+
if panel.get("source") == "model_output":
|
|
779
|
+
fields = [
|
|
780
|
+
{"name": f["key"], "type": "text", "description": ""}
|
|
781
|
+
for f in panel.get("fields", [])
|
|
782
|
+
]
|
|
783
|
+
except (OSError, json.JSONDecodeError):
|
|
784
|
+
pass
|
|
785
|
+
return {
|
|
786
|
+
"prompt": prompt,
|
|
787
|
+
"fields": fields,
|
|
788
|
+
"project": project,
|
|
789
|
+
"source": endpoint.get("source"),
|
|
790
|
+
"base_url": endpoint.get("base_url"),
|
|
791
|
+
"api_key": endpoint.get("api_key") or "EMPTY",
|
|
792
|
+
"model": endpoint.get("model"),
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
|
|
626
796
|
def serve(
|
|
627
797
|
data_dir: Path,
|
|
628
798
|
port: int = DEFAULT_PORT,
|
|
@@ -630,6 +800,8 @@ def serve(
|
|
|
630
800
|
host: str = "127.0.0.1",
|
|
631
801
|
db_path: Path | None = None,
|
|
632
802
|
allow_empty: bool = False,
|
|
803
|
+
workshop: Path | str | None = None,
|
|
804
|
+
endpoint: dict | None = None,
|
|
633
805
|
) -> None:
|
|
634
806
|
"""Serve the review UI over ``data_dir``.
|
|
635
807
|
|
|
@@ -648,11 +820,20 @@ def serve(
|
|
|
648
820
|
f"or pass a dataset directory: paratext review <dir>"
|
|
649
821
|
)
|
|
650
822
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
651
|
-
Handler.
|
|
823
|
+
Handler.base_data_dir = data_dir
|
|
652
824
|
# Default the annotation store to <data_dir>/annotations.db; --db can point it
|
|
653
825
|
# elsewhere (e.g. an existing DB when swapping in for another review app).
|
|
654
|
-
Handler.
|
|
826
|
+
Handler.base_store = Store(
|
|
827
|
+
default_db_path(data_dir, Path(db_path).resolve() if db_path else None)
|
|
828
|
+
)
|
|
655
829
|
datasets = discover_datasets(data_dir)
|
|
830
|
+
# Workshop mode: every browser gets its own workspace, seeded with the rounds
|
|
831
|
+
# shipped here so an attendee's own runs sit beside the worked example.
|
|
832
|
+
Handler.sessions = (
|
|
833
|
+
Sessions(Path(workshop).resolve(), examples=[d["dir"] for d in datasets])
|
|
834
|
+
if workshop
|
|
835
|
+
else None
|
|
836
|
+
)
|
|
656
837
|
httpd = ThreadingHTTPServer((host, port), Handler)
|
|
657
838
|
# When bound to all interfaces there's no single canonical URL; show
|
|
658
839
|
# localhost for clicking and note the bind address.
|
|
@@ -663,6 +844,12 @@ def serve(
|
|
|
663
844
|
print(
|
|
664
845
|
f" data dir: {data_dir} ({len(datasets)} dataset(s): {names})"
|
|
665
846
|
)
|
|
847
|
+
if workshop:
|
|
848
|
+
Handler.workshop_defaults = _workshop_defaults(datasets, endpoint or {})
|
|
849
|
+
d = Handler.workshop_defaults
|
|
850
|
+
print(f" workshop mode: per-session workspaces under {Handler.sessions.root}")
|
|
851
|
+
print(f" endpoint {d.get('model') or '(no model set)'} at {d.get('base_url')}")
|
|
852
|
+
print(f" source {d.get('source') or '(none — runs disabled)'}")
|
|
666
853
|
if open_browser:
|
|
667
854
|
try:
|
|
668
855
|
webbrowser.open(url)
|