utterplan 0.6.9__tar.gz → 0.6.12__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.
- {utterplan-0.6.9 → utterplan-0.6.12}/.github/workflows/python-publish.yml +1 -1
- {utterplan-0.6.9 → utterplan-0.6.12}/.github/workflows/tests.yml +2 -2
- {utterplan-0.6.9 → utterplan-0.6.12}/PKG-INFO +4 -2
- {utterplan-0.6.9 → utterplan-0.6.12}/README.md +2 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/architecture.md +15 -1
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/changelog.md +16 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/consumer-guide.md +2 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/format.md +2 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/pykokoro-integration.md +2 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/python-api.md +6 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/pyproject.toml +1 -1
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_bundle_document_render.py +14 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_cli_bundle_render_book.py +14 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_identity_units.py +61 -1
- utterplan-0.6.12/tests/test_prepared_semantics.py +188 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_renderplan_dependency_contract.py +1 -1
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_renderplan_export.py +30 -2
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_renderplan_v7_contract.py +46 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_replacement_provenance.py +23 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_ssmd_substitution_preparation.py +176 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_toml_format.py +39 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/_version.py +3 -3
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/flow_projection.py +87 -11
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/planner.py +19 -1
- utterplan-0.6.12/utterplan/prepared_semantics.py +196 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan.egg-info/PKG-INFO +4 -2
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan.egg-info/SOURCES.txt +2 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan.egg-info/requires.txt +1 -1
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan.egg-info/scm_file_list.json +13 -0
- utterplan-0.6.12/utterplan.egg-info/scm_version.json +8 -0
- utterplan-0.6.9/utterplan.egg-info/scm_version.json +0 -8
- {utterplan-0.6.9 → utterplan-0.6.12}/.codecrate.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/.github/workflows/codecov.yml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/.github/workflows/pre-commit.yml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/.gitignore +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/.pre-commit-config.yaml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/AGENTS.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/LICENSE +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/MANIFEST.in +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/NOTICE +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/Makefile +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/bundles.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/cli.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/conf.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/coordinate-spaces.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/debugging.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/getting-started.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/index.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/make.bat +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/phase-a-completion.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/docs/requirements.txt +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/examples/basic.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/examples/chapter.ssmd +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/examples/compile_file.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/examples/inspect_plan.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/examples/multilingual.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/examples/ssmd_example.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/setup.cfg +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/spec/schemas/v1.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/spec/schemas/v2.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/spec/schemas/v3.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/spec/schemas/v4.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/spec/utterplan.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/__init__.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/compiler_helpers.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/fixtures/canonical_consumer_contract.expected.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/fixtures/canonical_consumer_contract.ssmd +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/fixtures/document-bundle-v1/story.ssmd-bundle/document.ssmd.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/fixtures/document-bundle-v1/story.ssmd-bundle/manifest.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/fixtures/ssmd_09_comprehensive.ssmd +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/golden/basic_en.utterplan.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/golden/directives.utterplan.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/golden/markers.utterplan.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/golden/multilingual.utterplan.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/golden/parenthetical.utterplan.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/golden/spokenform_offsets.utterplan.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/golden/ssmd_09_comprehensive.utterplan.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/golden/ssmd_breaks.utterplan.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/__init__.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/cases.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/fixtures/v4/basic_en.utterplan.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/fixtures/v4/directives.utterplan.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/fixtures/v4/markers.utterplan.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/fixtures/v4/multilingual.utterplan.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/fixtures/v4/parenthetical.utterplan.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/fixtures/v4/spokenform_offsets.utterplan.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/fixtures/v4/ssmd_09_comprehensive.utterplan.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/fixtures/v4/ssmd_breaks.utterplan.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/goldens/multilingual_preparation.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/goldens/spokenform_numbers.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/goldens/ssmd_break.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/goldens/ssmd_language_detection.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/goldens/ssmd_marker.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/goldens/ssmd_pronunciation.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/test_legacy_json_import.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/migration/test_semantic_migration_regressions.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/README.md +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v1/basic.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v1/directives.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v1/markers.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v1/multilingual.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v1/parenthetical.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v1/spokenform_offsets.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v1/ssmd_breaks.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v2/directives.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v3/multilingual.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v3/ssmd_09_comprehensive.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/schema_history/v5/canonical_consumer_contract.toml +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_atomic_io.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_batch.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_boundaries.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_bundle_book_replan.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_bundle_book_services.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_bundle_document_compile.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_bundle_replan.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_bundle_role_inventory.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_bundle_services.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_cli.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_cli_batch.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_cli_bundle_compile_book.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_cli_bundle_document.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_cli_bundle_render_document.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_cli_bundle_replan_book.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_compiler.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_consumer_contract.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_consumer_readiness.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_coordinates.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_dependency_architecture.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_directives.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_explain.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_fake_renderer.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_flow_compilation.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_format.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_golden.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_import_boundaries.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_language.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_linguistics.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_migration_framework.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_migration_v3_to_v4.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_migration_v4_to_v5.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_pauses.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_planner.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_preparation.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_preview.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_progress.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_public_identity.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_renderability.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_renderability_repair.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_renderplan_batch.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_renderplan_replan.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_role_coverage.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_schema_history.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_schema_registry.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_schema_strict.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_schema_v6_source.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_schema_v7.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_segment_topology.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_segmentation_adapter.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_segmentation_recovery.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_semantic_boundaries.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_sentence_topology.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_source_input.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_ssmd_09_parser.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_ssmd_bundle.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_ssmd_bundle_book.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_ssmd_bundle_document.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_token_topology.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_ttsready_preparation_adapter.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/tests/test_version.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/__init__.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/__main__.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/_book_replan.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/_book_service.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/_document_service.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/atomic_io.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/attempt_serialization.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/attempts.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/batch.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/boundaries.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/bundle.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/bundle_replan.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/bundle_role_inventory.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/cli.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/codecs/__init__.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/codecs/v4_toml.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/codecs/v5_toml.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/codecs/v6_toml.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/codecs/v7_toml.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/compiler.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/config.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/diagnostics.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/directives.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/exceptions.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/explain.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/format.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/hashing.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/language.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/linguistics.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/migration.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/migrations/__init__.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/migrations/registry.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/migrations/v1_to_v2.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/migrations/v2_to_v3.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/migrations/v3_to_v4.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/migrations/v4_to_v5.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/migrations/v5_to_v6.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/migrations/v6_to_v7.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/model.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/parsers.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/pauses.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/preparation.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/preview.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/progress.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/py.typed +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/renderability.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/renderplan_batch.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/renderplan_export.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/renderplan_grouping.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/renderplan_projection.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/renderplan_replan.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/renderplan_workspace.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/renderprofile_io.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/role_coverage.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/schema_registry.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/schemas/v1.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/schemas/v2.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/schemas/v3.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/schemas/v4.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/segmentation.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/serialization.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/source_input.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/spacy_models.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/spans.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/ssmd_bundle.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/stages/__init__.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/stages/document/__init__.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/stages/document/plain.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/stages/document/ssmd.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/stages/preparation/__init__.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/stages/preparation/identity.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/stages/preparation/ssmd.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/stages/preparation/ttsready.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/stages/protocols.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/token_topology.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/toml_codec.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/trace_codec.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/units.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/utterplan.schema.json +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/versioning.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan/workspace_paths.py +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan.egg-info/dependency_links.txt +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan.egg-info/entry_points.txt +0 -0
- {utterplan-0.6.9 → utterplan-0.6.12}/utterplan.egg-info/top_level.txt +0 -0
|
@@ -82,7 +82,7 @@ jobs:
|
|
|
82
82
|
assert has_range(requirements, "phrasplit", "0.3.9", "0.4")
|
|
83
83
|
assert has_range(requirements, "ssmd", "0.9.3", "0.10")
|
|
84
84
|
assert has_range(requirements, "ttsready", "0.2.3", "0.3")
|
|
85
|
-
assert has_range(requirements, "renderplan", "0.
|
|
85
|
+
assert has_range(requirements, "renderplan", "0.2.0", "0.3")
|
|
86
86
|
names = set(archive.namelist())
|
|
87
87
|
expected = {
|
|
88
88
|
"utterplan/py.typed",
|
|
@@ -51,7 +51,7 @@ jobs:
|
|
|
51
51
|
"ssmd==0.9.3" \
|
|
52
52
|
"ttsready==0.2.3" \
|
|
53
53
|
"spokenform==0.4.5" \
|
|
54
|
-
"renderplan==0.
|
|
54
|
+
"renderplan==0.2.0" \
|
|
55
55
|
"typing_extensions==4.0.0" \
|
|
56
56
|
"tomlkit==0.13.0"
|
|
57
57
|
python -m pip install --no-deps -e .
|
|
@@ -120,7 +120,7 @@ jobs:
|
|
|
120
120
|
"phrasplit": ("0.3.9", "0.4"),
|
|
121
121
|
"ssmd": ("0.9.3", "0.10"),
|
|
122
122
|
"ttsready": ("0.2.3", "0.3"),
|
|
123
|
-
"renderplan": ("0.
|
|
123
|
+
"renderplan": ("0.2.0", "0.3"),
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
def check_metadata(contents):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: utterplan
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.12
|
|
4
4
|
Summary: Engine-independent text-to-speech planning compiler and interchange format
|
|
5
5
|
Author-email: Holger Nahrstaedt <nahrstaedt@gmail.com>
|
|
6
6
|
License: Apache License
|
|
@@ -227,7 +227,7 @@ Requires-Dist: ssmd<0.10,>=0.9.3
|
|
|
227
227
|
Requires-Dist: ttsready<0.3,>=0.2.3
|
|
228
228
|
Requires-Dist: typing_extensions>=4.0
|
|
229
229
|
Requires-Dist: tomlkit<1,>=0.13
|
|
230
|
-
Requires-Dist: renderplan<0.
|
|
230
|
+
Requires-Dist: renderplan<0.3,>=0.2.0
|
|
231
231
|
Provides-Extra: dev
|
|
232
232
|
Requires-Dist: build; extra == "dev"
|
|
233
233
|
Requires-Dist: jsonschema; extra == "dev"
|
|
@@ -408,6 +408,8 @@ engine-backed profiles, then preflights and renders the resulting RenderPlans.
|
|
|
408
408
|
UtterPlan's single-file `render-plan` command remains the low-level projection and
|
|
409
409
|
deterministic replan primitive.
|
|
410
410
|
|
|
411
|
+
Character-spelling segments keep exact identity in their portable token lemmas: a single alphanumeric token under `say_as = {interpret_as = "characters"}` has the exact token surface as its lemma, even when a linguistic provider reports a context-sensitive lowercase lemma. Ordinary lexical lemmas retain compact casefold-default encoding. Newly generated RenderPlans use schema v2 and store each used role's concrete configuration once in a top-level `[bindings.<role>]` table. The RenderPlan codec restores the complete `SpeechItem.binding` mapping on load; this changes disk layout, not the consumer API.
|
|
412
|
+
|
|
411
413
|
### Troubleshooting RenderProfile identity mismatch
|
|
412
414
|
|
|
413
415
|
A RenderProfile `profile_id` is a semantic content identity validated by RenderPlan; it is not a hash of the TOML file bytes. If the profile's identity-bearing semantics change without refreshing its ID, UtterPlan rejects it before creating RenderPlan artifacts. Regenerate or re-save the complete profile with VoiceRender, then validate it before rendering:
|
|
@@ -164,6 +164,8 @@ engine-backed profiles, then preflights and renders the resulting RenderPlans.
|
|
|
164
164
|
UtterPlan's single-file `render-plan` command remains the low-level projection and
|
|
165
165
|
deterministic replan primitive.
|
|
166
166
|
|
|
167
|
+
Character-spelling segments keep exact identity in their portable token lemmas: a single alphanumeric token under `say_as = {interpret_as = "characters"}` has the exact token surface as its lemma, even when a linguistic provider reports a context-sensitive lowercase lemma. Ordinary lexical lemmas retain compact casefold-default encoding. Newly generated RenderPlans use schema v2 and store each used role's concrete configuration once in a top-level `[bindings.<role>]` table. The RenderPlan codec restores the complete `SpeechItem.binding` mapping on load; this changes disk layout, not the consumer API.
|
|
168
|
+
|
|
167
169
|
### Troubleshooting RenderProfile identity mismatch
|
|
168
170
|
|
|
169
171
|
A RenderProfile `profile_id` is a semantic content identity validated by RenderPlan; it is not a hash of the TOML file bytes. If the profile's identity-bearing semantics change without refreshing its ID, UtterPlan rejects it before creating RenderPlan artifacts. Regenerate or re-save the complete profile with VoiceRender, then validate it before rendering:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Architecture Documentation"
|
|
3
|
-
version:
|
|
3
|
+
version: 6
|
|
4
4
|
generator: "archledger 0.4.0"
|
|
5
5
|
arc42_template_version: "9.0-EN"
|
|
6
6
|
---
|
|
@@ -237,6 +237,14 @@ Supporting tactics:
|
|
|
237
237
|
evidence live in an optional versioned trace TOML that never affects plan
|
|
238
238
|
identity or executable content.
|
|
239
239
|
|
|
240
|
+
## Prepared character-spelling semantics
|
|
241
|
+
|
|
242
|
+
After written-to-spoken preparation and before segmentation, UtterPlan inspects verified replacement records for the exact character-spelling transformation: an ASCII uppercase-letter/digit core of at least two characters becomes the same characters joined by single spaces, optionally retaining an identical `'s` or `’s` suffix. This derives a compiler-owned `say_as=characters` annotation over only the prepared character core. For example, `[ART]{sub=\"A R T\"}` keeps prepared text `A R T`; `[ART's]{sub=\"A R T's\"}` keeps `A R T's` and marks only `A R T`.
|
|
243
|
+
|
|
244
|
+
The derivation is portable executable intent, not a renderer-specific spelling rewrite. Prepared text never becomes `A- R- T`; a renderer adapter may choose that frontend form. Existing semantic-edge segmentation produces the exact controlled span, while RenderPlan sentence grouping retains the full sentence as one normal speech request with a request-local control. This feature adds neither a sentence split nor an automatic pause.
|
|
245
|
+
|
|
246
|
+
Authored `say-as` semantics take precedence, and the consumed SSMD `sub` annotation remains consumed. The derived directive uses existing schema-v7 `SayAsDirective` serialization and identity hashing; replacement records remain diagnostic provenance and are not synthesis input. No schema version or migration changes are required.
|
|
247
|
+
|
|
240
248
|
## Strategy Items
|
|
241
249
|
|
|
242
250
|
<!-- archledger: no accepted records for this section yet -->
|
|
@@ -281,6 +289,8 @@ from `manifest.json`, normalizes item selection, and enforces path containment.
|
|
|
281
289
|
services; private book and document helpers retain kind-specific persistence and
|
|
282
290
|
report behavior. The generic source loader remains limited to standalone files.
|
|
283
291
|
|
|
292
|
+
Preparation semantics include the compiler-only `prepared_semantics.py` pass. It consumes verified preparation replacement records after text preparation and before language-run/segment construction, appending deterministic `say-as=characters` annotations without changing prepared text, replacement provenance, boundaries, source maps, or consumed substitutions. It reuses existing directive resolution, semantic-edge segmentation, FlowPlan v7 serialization/identity, and RenderPlan request-local control projection.
|
|
293
|
+
|
|
284
294
|
<!-- archledger: no accepted records for this section yet -->
|
|
285
295
|
|
|
286
296
|
# Runtime View
|
|
@@ -479,6 +489,10 @@ semantic blockers. Blocked planning outcomes may be reviewed through the separat
|
|
|
479
489
|
`utterplan.planning-attempt.v1` inspect-only artifact; such candidates remain
|
|
480
490
|
drafts and never become canonical plans.
|
|
481
491
|
|
|
492
|
+
### Character spelling: text, intent, and realization
|
|
493
|
+
|
|
494
|
+
Character spelling keeps four concerns separate: source spelling (for example, `ART`), prepared spoken text (`A R T`), portable executable intent (`say_as=characters` over the exact prepared core), and downstream renderer realization. The matcher uses only verified replacement metadata and does not guess from arbitrary uppercase words or normalization changes. Possessive suffixes remain outside the semantic range. Replacement provenance is excluded from executable identity; the derived directive affects FlowUnit and FlowPlan identity. UtterPlan never persists a frontend-specific hyphen form or adds a pause for this feature.
|
|
495
|
+
|
|
482
496
|
<!-- archledger: no accepted records for this section yet -->
|
|
483
497
|
|
|
484
498
|
# Architecture Decisions
|
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [v0.6.12] - Unreleased
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Preserved exact character spelling and adopted RenderPlan v2 binding catalogs
|
|
12
|
+
|
|
13
|
+
<!-- generated by releaseledger -->
|
|
14
|
+
|
|
15
|
+
## [v0.6.10] - 2026-10-28
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added portable character-spelling semantics for verified substitutions
|
|
20
|
+
|
|
21
|
+
<!-- generated by releaseledger -->
|
|
22
|
+
|
|
7
23
|
## [v0.6.9] - 2026-10-27
|
|
8
24
|
|
|
9
25
|
### Added
|
|
@@ -42,6 +42,8 @@ In Python, `PlannerConfig.document_format` defaults to `"plain"`; select `"ssmd"
|
|
|
42
42
|
Token offsets are half-open Python-character ranges into the owning `segment.text`. Do not use them to slice document text or add offsets from another segment. Consumers preserve stored segment order but decide downstream chunk boundaries; a segment is not necessarily one TTS call.
|
|
43
43
|
Token `pos`, `tag`, `lemma`, and `morph` describe that exact prepared text and are the authoritative linguistic context, not model-token IDs or counts. Consumers may pass them through without retokenizing or rerunning spaCy. Optional replacements are provenance only: their output spans are segment-local, source values are original spellings, and they do not alter hashes independently of the source digest and are not synthesis inputs.
|
|
44
44
|
|
|
45
|
+
For `say_as.interpret_as = "characters"`, a single alphanumeric spelling token keeps its exact surface as the portable lemma (`T`, not a provider's contextual `t`); POS, tag, and morphology remain provider facts. The ordinary casefold-default encoding is unchanged. If the plan is projected and saved as RenderPlan v2, role bindings are interned in top-level `[bindings.<role>]` tables; `renderplan.codec.load_plan()` restores each complete `SpeechItem.binding` mapping for consumers.
|
|
46
|
+
|
|
45
47
|
```python
|
|
46
48
|
from utterplan import FlowPlan
|
|
47
49
|
|
|
@@ -55,6 +55,8 @@ Pause intents represent authored `none`, strength values (`x-weak`, `weak`, `med
|
|
|
55
55
|
|
|
56
56
|
`plan_id` identifies canonical executable semantics and, in schema v7, the SHA-256 digest of exact raw source when available; it does not hash TOML bytes. The plan-level `hash_schema` is `utterplan-flow-v3`; canonical boundary opportunities and authored pause intents participate in executable identity and flow-unit hashes. Compact replacement provenance is excluded. Producer identity and formatting do not alter executable identity. Plans contain no phonemes, model token IDs, models, sessions, renderer configuration, or audio.
|
|
57
57
|
|
|
58
|
+
For a segment with `say_as = {interpret_as = "characters"}`, each single alphanumeric token's portable lemma is its exact surface (for example, `T` remains `T` even if a provider reports `t`). This semantic normalization occurs before v7 sparse lemma encoding; ordinary lexical lemmas still default to the case-folded surface, and provider POS, tag, and morphology are retained. When projected separately, RenderPlan v2 stores concrete role bindings in one top-level `[bindings.<role>]` table per used role rather than repeating them in speech entries. Loading with the RenderPlan codec still exposes the complete mapping as `SpeechItem.binding`.
|
|
59
|
+
|
|
58
60
|
## TOML API and optional compiler trace
|
|
59
61
|
|
|
60
62
|
`FlowPlan.to_toml()`, `FlowPlan.from_toml()`, `FlowPlan.save()`, and `FlowPlan.load()` are the persistence APIs. `save()` writes atomically. Normal plan loading and the `validate`, `inspect`, and `explain` commands accept TOML; JSON is never auto-detected as a current plan format. Import a historical plan explicitly with `utterplan migrate`; schema-v6 data cannot be promoted to v7 without recompiling the original source, so migration may report `migration.recompile_required` instead of writing a file.
|
|
@@ -38,6 +38,8 @@ for unit in plan.flow:
|
|
|
38
38
|
|
|
39
39
|
Use the v7 linguistic token fields (not model-token IDs or counts) as authoritative context: pass available `pos`, `tag`, `lemma`, and `morph` through unchanged rather than rerunning spaCy or inferring tags. Default `auto` uses installed compatible local models, preferring `sm`, `md`, `lg`, `trf`, and safely falls back if unavailable; model packages are never downloaded. When fields are unavailable, follow the adapter's documented non-contextual fallback.
|
|
40
40
|
|
|
41
|
+
Character spelling is an explicit semantic exception to contextual lemmatization: within `say_as.interpret_as = "characters"`, each single alphanumeric token's portable lemma is its exact surface (so `T` remains uppercase even if the provider reports `t`). POS, tag, and morphology are not rewritten, and ordinary lexical lemmas keep the compact casefold default. On the separate RenderPlan path, schema-v2 files store role bindings once in a plan-level catalog; loading them still supplies the full `SpeechItem.binding` to the consumer.
|
|
42
|
+
|
|
41
43
|
## Pause behavior
|
|
42
44
|
|
|
43
45
|
Schema v7 carries safe boundary opportunities separately from authored pause intents and exact timed breaks; neither selects a renderer activation policy or fixes a chunk. PyKokoro may choose its own chunking and native, automatic, or manual pause realization. A `FlowSegment` is not necessarily one TTS call, and renderer/acoustic variance remains outside UtterPlan and plan identity.
|
|
@@ -329,6 +329,12 @@ print(segment.directives.emphasis)
|
|
|
329
329
|
|
|
330
330
|
The `document` record retains compact portable document information. Effective directives and pause intents live on their owning flow segments. Compiler-only source spans and preparation coordinate maps remain in the optional trace. See the [coordinate-space contract](coordinate-spaces) and [consumer guide](consumer-guide) for renderer responsibilities.
|
|
331
331
|
|
|
332
|
+
### Character-spelling substitutions
|
|
333
|
+
|
|
334
|
+
When a verified preparation replacement is exactly an ASCII uppercase-letter/digit sequence expanded into its individual characters separated by single spaces, UtterPlan derives `say_as=characters` for that prepared core. For example, `[ART]{sub="A R T"}` keeps prepared text `A R T`; `[ART's]{sub="A R T's"}` keeps `A R T's` and covers only `A R T`. This applies to exact SSMD substitutions and other preparation backends' verified replacement records, including TTSReady. It does not guess from arbitrary acronyms, case changes, normalization, punctuation, or altered spacing. An authored `say-as` takes precedence.
|
|
335
|
+
|
|
336
|
+
The derived directive is executable semantic intent and participates in FlowPlan identity. Replacement provenance remains diagnostic only. With sentence-level RenderPlan grouping, the full sentence stays one speech request and the request-local `say_as` control covers only the spelled core. UtterPlan never rewrites prepared text to frontend-specific forms such as `A- R- T` and does not add an automatic pause for this feature; an engine adapter owns any renderer-specific realization.
|
|
337
|
+
|
|
332
338
|
## Flow segments and local tokens
|
|
333
339
|
|
|
334
340
|
A `FlowPlan` stores ordered `FlowUnit` objects. Each unit owns its ordered segments, so consumers do not join plan-global IDs. Token offsets use Python-character coordinates local to `segment.text`:
|
|
@@ -47,6 +47,12 @@ def test_render_document_bundle_projects_canonical_flowplan_without_book_index(
|
|
|
47
47
|
assert result.renderplan_path.name == "document.renderplan.toml"
|
|
48
48
|
plan = renderplan_codec.load_plan(result.renderplan_path)
|
|
49
49
|
plan.validate_against_profile(profile)
|
|
50
|
+
serialized = result.renderplan_path.read_text(encoding="utf-8")
|
|
51
|
+
assert plan.schema_version == 2
|
|
52
|
+
assert serialized.count("[bindings.narrator]") == 1
|
|
53
|
+
assert "[flow.binding]" not in serialized
|
|
54
|
+
assert plan.plan_id == plan.computed_id()
|
|
55
|
+
assert all(speech.binding == dict(profile.roles[speech.role].binding) for speech in plan.speech)
|
|
50
56
|
flow = FlowPlan.load(result.source_plan_path)
|
|
51
57
|
assert result.variant.startswith("narration--")
|
|
52
58
|
assert result.snapshot_path.is_file()
|
|
@@ -119,3 +125,11 @@ def test_render_document_bundle_returns_multi_role_coverage_and_bindings(tmp_pat
|
|
|
119
125
|
plan = renderplan_codec.load_plan(result.renderplan_path)
|
|
120
126
|
assert {speech.role for speech in plan.speech} == {"host", "guest", "narrator"}
|
|
121
127
|
assert {speech.binding["engine"] for speech in plan.speech} == {"kokoro", "kitten", "piper"}
|
|
128
|
+
serialized = result.renderplan_path.read_text(encoding="utf-8")
|
|
129
|
+
for role in ("narrator", "host", "guest"):
|
|
130
|
+
assert serialized.count(f"[bindings.{role}]") == 1
|
|
131
|
+
assert "[flow.binding]" not in serialized
|
|
132
|
+
assert plan.schema_version == 2
|
|
133
|
+
assert plan.plan_id == plan.computed_id()
|
|
134
|
+
assert all(speech.binding == dict(profile.roles[speech.role].binding) for speech in plan.speech)
|
|
135
|
+
plan.validate_against_profile(profile)
|
|
@@ -319,6 +319,20 @@ def test_render_book_reports_multi_role_coverage(tmp_path: Path, capsys) -> None
|
|
|
319
319
|
"narrator",
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
for chapter, plan in zip(collection.chapters, plans, strict=True):
|
|
323
|
+
serialized = (variant / chapter.renderplan).read_text(encoding="utf-8")
|
|
324
|
+
roles = {speech.role for speech in plan.speech}
|
|
325
|
+
assert len(roles) == 1
|
|
326
|
+
for role in roles:
|
|
327
|
+
assert serialized.count(f"[bindings.{role}]") == 1
|
|
328
|
+
assert "[flow.binding]" not in serialized
|
|
329
|
+
assert plan.schema_version == 2
|
|
330
|
+
assert plan.plan_id == plan.computed_id()
|
|
331
|
+
assert all(
|
|
332
|
+
speech.binding == dict(profile.roles[speech.role].binding) for speech in plan.speech
|
|
333
|
+
)
|
|
334
|
+
plan.validate_against_profile(profile)
|
|
335
|
+
|
|
322
336
|
|
|
323
337
|
def test_render_book_accumulates_successful_chapters_and_replaces_latest_report(
|
|
324
338
|
tmp_path: Path, capsys
|
|
@@ -2,7 +2,7 @@ from dataclasses import replace
|
|
|
2
2
|
from types import SimpleNamespace
|
|
3
3
|
|
|
4
4
|
from tests.compiler_helpers import CompilerTestPlanner as UtterancePlanner
|
|
5
|
-
from utterplan import PlannerConfig, SemanticBoundary
|
|
5
|
+
from utterplan import FlowUnit, LinguisticsConfig, PlannerConfig, SemanticBoundary, compile_document
|
|
6
6
|
from utterplan.hashing import (
|
|
7
7
|
LEGACY_UNIT_HASH_SCHEMA,
|
|
8
8
|
UNIT_HASH_SCHEMA,
|
|
@@ -182,3 +182,63 @@ def test_serialized_and_runtime_unit_hash_payloads_match() -> None:
|
|
|
182
182
|
migrated_payload = unit_hash_payload_from_serialized(serialized_unit, serialized_plan)
|
|
183
183
|
|
|
184
184
|
assert migrated_payload == runtime_payload
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_derived_say_as_changes_identity_and_v7_roundtrips_without_hashing_provenance() -> None:
|
|
188
|
+
source = 'Because someone called [ART]{sub="A R T"} finally.'
|
|
189
|
+
plan = compile_document(
|
|
190
|
+
source,
|
|
191
|
+
input_format="ssmd",
|
|
192
|
+
config=PlannerConfig(
|
|
193
|
+
language="en-us",
|
|
194
|
+
document_format="ssmd",
|
|
195
|
+
linguistics=LinguisticsConfig(use_spacy=False),
|
|
196
|
+
),
|
|
197
|
+
).plan
|
|
198
|
+
assert "".join(segment.text for unit in plan.flow for segment in unit.segments) == (
|
|
199
|
+
"Because someone called A R T finally."
|
|
200
|
+
)
|
|
201
|
+
spelled_ref = next(
|
|
202
|
+
(unit_index, segment_index, segment)
|
|
203
|
+
for unit_index, unit in enumerate(plan.flow)
|
|
204
|
+
for segment_index, segment in enumerate(unit.segments)
|
|
205
|
+
if segment.text == "A R T"
|
|
206
|
+
)
|
|
207
|
+
unit_index, segment_index, spelled = spelled_ref
|
|
208
|
+
assert spelled.directives.say_as is not None
|
|
209
|
+
assert spelled.directives.say_as.interpret_as == "characters"
|
|
210
|
+
|
|
211
|
+
without_units = []
|
|
212
|
+
for unit in plan.flow:
|
|
213
|
+
without_units.append(
|
|
214
|
+
FlowUnit(
|
|
215
|
+
tuple(
|
|
216
|
+
replace(
|
|
217
|
+
segment,
|
|
218
|
+
directives=replace(segment.directives, say_as=None),
|
|
219
|
+
)
|
|
220
|
+
for segment in unit.segments
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
without_say_as = replace(plan, flow=tuple(without_units), plan_id="")
|
|
225
|
+
assert without_say_as.flow[unit_index].segments[segment_index].text == spelled.text
|
|
226
|
+
assert without_say_as.flow[unit_index].content_hash != plan.flow[unit_index].content_hash
|
|
227
|
+
assert without_say_as.plan_id != plan.plan_id
|
|
228
|
+
|
|
229
|
+
replacement = spelled.replacements[0]
|
|
230
|
+
provenance_only = replace(replacement, source="different diagnostic spelling")
|
|
231
|
+
changed_segments = list(plan.flow[unit_index].segments)
|
|
232
|
+
changed_segments[segment_index] = replace(spelled, replacements=(provenance_only,))
|
|
233
|
+
changed_units = list(plan.flow)
|
|
234
|
+
changed_units[unit_index] = FlowUnit(tuple(changed_segments))
|
|
235
|
+
provenance_changed = replace(plan, flow=tuple(changed_units), plan_id="")
|
|
236
|
+
assert provenance_changed.flow[unit_index].content_hash == plan.flow[unit_index].content_hash
|
|
237
|
+
assert provenance_changed.plan_id == plan.plan_id
|
|
238
|
+
|
|
239
|
+
restored = type(plan).from_toml(plan.to_toml())
|
|
240
|
+
assert restored == plan
|
|
241
|
+
assert restored.schema_version == 7
|
|
242
|
+
assert restored.flow[unit_index].segments[segment_index].directives.say_as == (
|
|
243
|
+
spelled.directives.say_as
|
|
244
|
+
)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from utterplan.model import AnnotationSpan, BoundaryEvent, TextPreparationInfo
|
|
9
|
+
from utterplan.preparation import PreparedText
|
|
10
|
+
from utterplan.prepared_semantics import (
|
|
11
|
+
CharacterSpellingMatch,
|
|
12
|
+
detect_character_spelling,
|
|
13
|
+
infer_prepared_semantics,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class _MappedSource:
|
|
19
|
+
source_length: int
|
|
20
|
+
output_length: int
|
|
21
|
+
source_start: int
|
|
22
|
+
source_end: int
|
|
23
|
+
output_start: int
|
|
24
|
+
output_end: int
|
|
25
|
+
|
|
26
|
+
def map_source_span(self, start: int, end: int) -> tuple[int, int]:
|
|
27
|
+
if (start, end) == (self.source_start, self.source_end):
|
|
28
|
+
return self.output_start, self.output_end
|
|
29
|
+
return start, end
|
|
30
|
+
|
|
31
|
+
def map_output_span(self, start: int, end: int) -> tuple[int, int]:
|
|
32
|
+
if (start, end) == (self.output_start, self.output_end):
|
|
33
|
+
return self.source_start, self.source_end
|
|
34
|
+
return start, end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _replacement(source: str, replacement: str, *, start: int = 0) -> PreparedText:
|
|
38
|
+
output_start = start + 10
|
|
39
|
+
source_map = _MappedSource(
|
|
40
|
+
source_length=start + len(source) + 5,
|
|
41
|
+
output_length=output_start + len(replacement) + 5,
|
|
42
|
+
source_start=start,
|
|
43
|
+
source_end=start + len(source),
|
|
44
|
+
output_start=output_start,
|
|
45
|
+
output_end=output_start + len(replacement),
|
|
46
|
+
)
|
|
47
|
+
spoken = " " * output_start + replacement + " " * 5
|
|
48
|
+
record: dict[str, Any] = {
|
|
49
|
+
"source_start": start,
|
|
50
|
+
"source_end": start + len(source),
|
|
51
|
+
"output_start": output_start,
|
|
52
|
+
"output_end": output_start + len(replacement),
|
|
53
|
+
"source": source,
|
|
54
|
+
"replacement": replacement,
|
|
55
|
+
"kind": "ssmd-sub",
|
|
56
|
+
"rule": "ssmd.sub",
|
|
57
|
+
"annotation_id": "sub-1",
|
|
58
|
+
}
|
|
59
|
+
source_annotation = AnnotationSpan(
|
|
60
|
+
id="sub-1",
|
|
61
|
+
kind="directive",
|
|
62
|
+
attrs={"sub": replacement},
|
|
63
|
+
structural_start=start,
|
|
64
|
+
structural_end=start + len(source),
|
|
65
|
+
spoken_start=output_start,
|
|
66
|
+
spoken_end=output_start + len(replacement),
|
|
67
|
+
source_start=100 + start,
|
|
68
|
+
source_end=100 + start + len(source),
|
|
69
|
+
source_node_id="source-node-1",
|
|
70
|
+
)
|
|
71
|
+
return PreparedText(
|
|
72
|
+
spoken_text=spoken,
|
|
73
|
+
info=TextPreparationInfo("ssmd", None, replacements=(record,)),
|
|
74
|
+
annotations=(source_annotation,),
|
|
75
|
+
boundaries=(BoundaryEvent("b-1", 0, "sentence"),),
|
|
76
|
+
source_map=source_map,
|
|
77
|
+
consumed_substitutions=("sub-1",),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize(
|
|
82
|
+
("source", "replacement", "expected"),
|
|
83
|
+
[
|
|
84
|
+
("ART", "A R T", CharacterSpellingMatch(0, 3, 0, 5)),
|
|
85
|
+
("AI", "A I", CharacterSpellingMatch(0, 2, 0, 3)),
|
|
86
|
+
("USA", "U S A", CharacterSpellingMatch(0, 3, 0, 5)),
|
|
87
|
+
("R2D2", "R 2 D 2", CharacterSpellingMatch(0, 4, 0, 7)),
|
|
88
|
+
("ART's", "A R T's", CharacterSpellingMatch(0, 3, 0, 5)),
|
|
89
|
+
("ART’s", "A R T’s", CharacterSpellingMatch(0, 3, 0, 5)),
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
def test_detect_exact_character_spelling(
|
|
93
|
+
source: str, replacement: str, expected: CharacterSpellingMatch
|
|
94
|
+
) -> None:
|
|
95
|
+
assert detect_character_spelling(source, replacement) == expected
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize(
|
|
99
|
+
("source", "replacement"),
|
|
100
|
+
[
|
|
101
|
+
("A", "A"),
|
|
102
|
+
("NASA", "nasa"),
|
|
103
|
+
("NASA", "N A S A agency"),
|
|
104
|
+
("Dr.", "Doctor"),
|
|
105
|
+
("kg", "kilograms"),
|
|
106
|
+
("ART", "A-R-T"),
|
|
107
|
+
("ABC", "A B C D"),
|
|
108
|
+
("AB", "A B"),
|
|
109
|
+
("A&B", "A and B"),
|
|
110
|
+
("ARTs", "A R T s"),
|
|
111
|
+
("ART's", "A R T’s"),
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
def test_rejects_non_exact_character_spelling(source: str, replacement: str) -> None:
|
|
115
|
+
assert detect_character_spelling(source, replacement) is None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_inference_adds_immutable_core_only_deterministic_annotation() -> None:
|
|
119
|
+
prepared = _replacement("ART's", "A R T's", start=4)
|
|
120
|
+
result = infer_prepared_semantics(prepared)
|
|
121
|
+
derived = result.annotations[-1]
|
|
122
|
+
|
|
123
|
+
assert result.spoken_text == prepared.spoken_text
|
|
124
|
+
assert result.info is prepared.info
|
|
125
|
+
assert result.info.replacements == prepared.info.replacements
|
|
126
|
+
assert result.boundaries is prepared.boundaries
|
|
127
|
+
assert result.source_map is prepared.source_map
|
|
128
|
+
assert result.consumed_substitutions is prepared.consumed_substitutions
|
|
129
|
+
assert derived.id == "derived.say-as.characters.4.7.14.19"
|
|
130
|
+
assert derived.kind == "directive"
|
|
131
|
+
assert derived.attrs == {
|
|
132
|
+
"tag": "say-as",
|
|
133
|
+
"as": "characters",
|
|
134
|
+
"origin": "derived-character-spelling",
|
|
135
|
+
}
|
|
136
|
+
assert (derived.structural_start, derived.structural_end) == (4, 7)
|
|
137
|
+
assert (derived.spoken_start, derived.spoken_end) == (14, 19)
|
|
138
|
+
assert prepared.spoken_text[derived.spoken_start : derived.spoken_end] == "A R T"
|
|
139
|
+
assert (derived.source_start, derived.source_end) == (104, 109)
|
|
140
|
+
assert derived.source_node_id == "source-node-1"
|
|
141
|
+
assert infer_prepared_semantics(result).annotations == result.annotations
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_authored_say_as_prevents_derived_overlap_and_wins() -> None:
|
|
145
|
+
prepared = _replacement("ART", "A R T")
|
|
146
|
+
authored = AnnotationSpan(
|
|
147
|
+
id="authored-say-as",
|
|
148
|
+
kind="directive",
|
|
149
|
+
attrs={"tag": "say-as", "as": "initialism"},
|
|
150
|
+
structural_start=0,
|
|
151
|
+
structural_end=3,
|
|
152
|
+
spoken_start=10,
|
|
153
|
+
spoken_end=15,
|
|
154
|
+
)
|
|
155
|
+
prepared = PreparedText(
|
|
156
|
+
prepared.spoken_text,
|
|
157
|
+
prepared.info,
|
|
158
|
+
(*prepared.annotations, authored),
|
|
159
|
+
prepared.boundaries,
|
|
160
|
+
prepared.source_map,
|
|
161
|
+
prepared.consumed_substitutions,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
result = infer_prepared_semantics(prepared)
|
|
165
|
+
|
|
166
|
+
assert result is prepared
|
|
167
|
+
assert [item.id for item in result.annotations].count("authored-say-as") == 1
|
|
168
|
+
assert all(
|
|
169
|
+
item.attrs.get("origin") != "derived-character-spelling" for item in result.annotations
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_unverified_replacement_is_ignored() -> None:
|
|
174
|
+
prepared = _replacement("ART", "A R T")
|
|
175
|
+
bad = dict(
|
|
176
|
+
prepared.info.replacements[0], output_end=prepared.info.replacements[0]["output_end"] - 1
|
|
177
|
+
)
|
|
178
|
+
info = TextPreparationInfo("ssmd", None, replacements=(bad,))
|
|
179
|
+
unverified = PreparedText(
|
|
180
|
+
prepared.spoken_text,
|
|
181
|
+
info,
|
|
182
|
+
prepared.annotations,
|
|
183
|
+
prepared.boundaries,
|
|
184
|
+
prepared.source_map,
|
|
185
|
+
prepared.consumed_substitutions,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
assert infer_prepared_semantics(unverified) is unverified
|
|
@@ -29,4 +29,4 @@ def test_renderplan_collection_api_minimum_is_declared_as_runtime_dependency() -
|
|
|
29
29
|
project = tomlkit.parse(
|
|
30
30
|
(Path(__file__).resolve().parents[1] / "pyproject.toml").read_text(encoding="utf-8")
|
|
31
31
|
).unwrap()
|
|
32
|
-
assert "renderplan>=0.
|
|
32
|
+
assert "renderplan>=0.2.0,<0.3" in project["project"]["dependencies"]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from dataclasses import replace
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import pytest
|
|
@@ -91,7 +92,7 @@ def _preflight(plan, profile) -> PreflightReport:
|
|
|
91
92
|
).with_identity()
|
|
92
93
|
|
|
93
94
|
|
|
94
|
-
def test_public_build_is_deterministic_and_rejects_incomplete_replan_inputs() -> None:
|
|
95
|
+
def test_public_build_is_deterministic_and_rejects_incomplete_replan_inputs(tmp_path: Path) -> None:
|
|
95
96
|
source = _source()
|
|
96
97
|
profile = _profile()
|
|
97
98
|
|
|
@@ -103,6 +104,23 @@ def test_public_build_is_deterministic_and_rejects_incomplete_replan_inputs() ->
|
|
|
103
104
|
assert first.speech[0].text == "one two three."
|
|
104
105
|
assert first.speech[0].source[0].locator == "f000000.s000"
|
|
105
106
|
assert first.speech[0].wav == "000001.wav"
|
|
107
|
+
renderplan_path = tmp_path / "single-role.renderplan.toml"
|
|
108
|
+
renderplan_codec.save_plan(first, renderplan_path)
|
|
109
|
+
serialized = renderplan_path.read_text(encoding="utf-8")
|
|
110
|
+
loaded = renderplan_codec.load_plan(renderplan_path)
|
|
111
|
+
assert first.schema_version == 2
|
|
112
|
+
assert serialized.count("[bindings.narrator]") == 1
|
|
113
|
+
assert "[flow.binding]" not in serialized
|
|
114
|
+
assert loaded.plan_id == first.plan_id == first.computed_id()
|
|
115
|
+
assert all(
|
|
116
|
+
speech.binding == dict(profile.roles[speech.role].binding) for speech in loaded.speech
|
|
117
|
+
)
|
|
118
|
+
loaded.validate_against_profile(profile)
|
|
119
|
+
|
|
120
|
+
changed_role = replace(profile.roles["narrator"], binding={"engine": "changed"})
|
|
121
|
+
changed_profile = replace(profile, roles={"narrator": changed_role}, profile_id=None)
|
|
122
|
+
changed_plan = build_render_plan(source, changed_profile)
|
|
123
|
+
assert changed_plan.speech[0].segment_id != first.speech[0].segment_id
|
|
106
124
|
try:
|
|
107
125
|
build_render_plan(source, profile, preflight=_preflight(first, profile))
|
|
108
126
|
except RenderPlanProjectionError as error:
|
|
@@ -314,7 +332,8 @@ def test_render_plan_json_reports_role_coverage_metadata(tmp_path: Path, capsys)
|
|
|
314
332
|
profile_path = tmp_path / "cast.renderprofile.toml"
|
|
315
333
|
output_path = tmp_path / "roles.renderplan.toml"
|
|
316
334
|
_multi_role_source().save(source_path)
|
|
317
|
-
|
|
335
|
+
profile = _multi_role_profile()
|
|
336
|
+
renderplan_codec.save_profile(profile, profile_path)
|
|
318
337
|
|
|
319
338
|
assert (
|
|
320
339
|
main(
|
|
@@ -341,6 +360,15 @@ def test_render_plan_json_reports_role_coverage_metadata(tmp_path: Path, capsys)
|
|
|
341
360
|
}
|
|
342
361
|
assert output_path.is_file()
|
|
343
362
|
|
|
363
|
+
plan = renderplan_codec.load_plan(output_path)
|
|
364
|
+
serialized = output_path.read_text(encoding="utf-8")
|
|
365
|
+
for role in ("narrator", "host", "guest"):
|
|
366
|
+
assert serialized.count(f"[bindings.{role}]") == 1
|
|
367
|
+
assert "[flow.binding]" not in serialized
|
|
368
|
+
assert plan.schema_version == 2
|
|
369
|
+
assert plan.plan_id == plan.computed_id()
|
|
370
|
+
assert all(speech.binding == dict(profile.roles[speech.role].binding) for speech in plan.speech)
|
|
371
|
+
plan.validate_against_profile(profile)
|
|
344
372
|
human_output_path = tmp_path / "roles-human.renderplan.toml"
|
|
345
373
|
assert (
|
|
346
374
|
main(
|
|
@@ -22,10 +22,13 @@ from utterplan import (
|
|
|
22
22
|
FlowUnit,
|
|
23
23
|
LegacyFlowPlan,
|
|
24
24
|
PauseIntent,
|
|
25
|
+
PlannerConfig,
|
|
25
26
|
ProsodyDirective,
|
|
26
27
|
SegmentDirectives,
|
|
27
28
|
TokenView,
|
|
28
29
|
VoiceDirective,
|
|
30
|
+
build_render_plan,
|
|
31
|
+
compile_document,
|
|
29
32
|
)
|
|
30
33
|
from utterplan.hashing import V5_FLOW_HASH_SCHEMA, V6_FLOW_HASH_SCHEMA
|
|
31
34
|
from utterplan.renderplan_grouping import group_initial_plan
|
|
@@ -554,3 +557,46 @@ def test_model_token_capacity_is_not_interpreted_by_utterplan() -> None:
|
|
|
554
557
|
with pytest.raises(RenderPlanProjectionError) as error:
|
|
555
558
|
_group(plan, profile)
|
|
556
559
|
assert error.value.code == "renderplan.capacity.metric"
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@pytest.mark.parametrize(
|
|
563
|
+
("source", "expected"),
|
|
564
|
+
[
|
|
565
|
+
(
|
|
566
|
+
"Because someone called [ART's]{sub=\"A R T's\"} finally.",
|
|
567
|
+
"Because someone called A R T's finally.",
|
|
568
|
+
),
|
|
569
|
+
(
|
|
570
|
+
'Because someone called [ART’s]{sub="A R T’s"} finally.',
|
|
571
|
+
"Because someone called A R T’s finally.",
|
|
572
|
+
),
|
|
573
|
+
],
|
|
574
|
+
)
|
|
575
|
+
def test_character_spelling_projects_one_sentence_with_exact_request_control(
|
|
576
|
+
source: str, expected: str
|
|
577
|
+
) -> None:
|
|
578
|
+
compiled = compile_document(
|
|
579
|
+
source,
|
|
580
|
+
input_format="ssmd",
|
|
581
|
+
config=PlannerConfig(language="en-us", document_format="ssmd"),
|
|
582
|
+
)
|
|
583
|
+
rendered = build_render_plan(compiled.plan, _profile(pauses={"sentence": 120}))
|
|
584
|
+
speech = [item for item in rendered.flow if isinstance(item, SpeechItem)]
|
|
585
|
+
|
|
586
|
+
assert len(speech) == 1
|
|
587
|
+
assert speech[0].planning_level == "sentence"
|
|
588
|
+
assert speech[0].text == expected
|
|
589
|
+
tokens = speech[0].tokens
|
|
590
|
+
assert tokens is not None
|
|
591
|
+
t_index = next(
|
|
592
|
+
index for index, (start, end) in enumerate(tokens.span) if speech[0].text[start:end] == "T"
|
|
593
|
+
)
|
|
594
|
+
assert dict(tokens.lemma)[t_index] == "T"
|
|
595
|
+
assert "A- R- T" not in speech[0].text
|
|
596
|
+
assert not any(isinstance(item, PauseItem) for item in rendered.flow)
|
|
597
|
+
say_as = [control for control in speech[0].controls if control.name == "say_as"]
|
|
598
|
+
assert len(say_as) == 1
|
|
599
|
+
control = say_as[0]
|
|
600
|
+
start = expected.index("A R T")
|
|
601
|
+
assert (control.start, control.end) == (start, start + len("A R T"))
|
|
602
|
+
assert dict(control.params) == {"interpret_as": "characters"}
|