utterplan 0.5.0__tar.gz → 0.5.1__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.5.0 → utterplan-0.5.1}/PKG-INFO +1 -1
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/architecture.md +6 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/changelog.md +12 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/coordinate-spaces.md +4 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/debugging.md +2 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/format.md +6 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/python-api.md +6 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/goldens/multilingual_preparation.json +2 -2
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/goldens/ssmd_break.json +1 -1
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_cli.py +6 -4
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_cli_batch.py +8 -4
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_cli_ssmdbook.py +39 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_consumer_contract.py +5 -2
- utterplan-0.5.1/tests/test_flow_compilation.py +220 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_linguistics.py +3 -16
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_planner.py +27 -11
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_renderability_repair.py +23 -28
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_segment_topology.py +32 -7
- utterplan-0.5.1/tests/test_segmentation_adapter.py +203 -0
- utterplan-0.5.1/tests/test_segmentation_recovery.py +114 -0
- utterplan-0.5.1/tests/test_sentence_topology.py +361 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_ssmd_09_parser.py +10 -1
- utterplan-0.5.1/tests/test_token_topology.py +73 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/_version.py +3 -3
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/flow_projection.py +41 -11
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/planner.py +255 -186
- utterplan-0.5.1/utterplan/segmentation.py +847 -0
- utterplan-0.5.1/utterplan/token_topology.py +102 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan.egg-info/PKG-INFO +1 -1
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan.egg-info/SOURCES.txt +6 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan.egg-info/scm_file_list.json +17 -0
- utterplan-0.5.1/utterplan.egg-info/scm_version.json +8 -0
- utterplan-0.5.0/tests/test_flow_compilation.py +0 -97
- utterplan-0.5.0/utterplan.egg-info/scm_version.json +0 -8
- {utterplan-0.5.0 → utterplan-0.5.1}/.codecrate.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/.github/workflows/codecov.yml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/.github/workflows/pre-commit.yml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/.github/workflows/python-publish.yml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/.github/workflows/tests.yml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/.gitignore +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/.pre-commit-config.yaml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/AGENTS.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/LICENSE +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/MANIFEST.in +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/NOTICE +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/README.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/Makefile +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/cli.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/conf.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/consumer-guide.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/getting-started.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/index.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/make.bat +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/phase-a-completion.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/pykokoro-integration.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/requirements.txt +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/docs/ssmdbook.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/examples/basic.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/examples/chapter.ssmd +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/examples/compile_file.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/examples/inspect_plan.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/examples/multilingual.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/examples/ssmd_example.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/pyproject.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/setup.cfg +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/spec/schemas/v1.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/spec/schemas/v2.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/spec/schemas/v3.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/spec/schemas/v4.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/spec/utterplan.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/__init__.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/compiler_helpers.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/fixtures/canonical_consumer_contract.expected.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/fixtures/canonical_consumer_contract.ssmd +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/fixtures/ssmd_09_comprehensive.ssmd +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/golden/basic_en.utterplan.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/golden/directives.utterplan.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/golden/markers.utterplan.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/golden/multilingual.utterplan.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/golden/parenthetical.utterplan.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/golden/spokenform_offsets.utterplan.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/golden/ssmd_09_comprehensive.utterplan.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/golden/ssmd_breaks.utterplan.toml +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/__init__.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/cases.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/fixtures/v4/basic_en.utterplan.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/fixtures/v4/directives.utterplan.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/fixtures/v4/markers.utterplan.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/fixtures/v4/multilingual.utterplan.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/fixtures/v4/parenthetical.utterplan.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/fixtures/v4/spokenform_offsets.utterplan.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/fixtures/v4/ssmd_09_comprehensive.utterplan.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/fixtures/v4/ssmd_breaks.utterplan.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/goldens/spokenform_numbers.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/goldens/ssmd_language_detection.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/goldens/ssmd_marker.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/goldens/ssmd_pronunciation.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/test_legacy_json_import.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/migration/test_semantic_migration_regressions.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/README.md +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v1/basic.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v1/directives.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v1/markers.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v1/multilingual.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v1/parenthetical.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v1/spokenform_offsets.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v1/ssmd_breaks.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v2/directives.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v3/multilingual.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/schema_history/v3/ssmd_09_comprehensive.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_atomic_io.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_batch.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_boundaries.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_compiler.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_consumer_readiness.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_coordinates.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_directives.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_explain.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_fake_renderer.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_format.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_golden.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_identity_units.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_import_boundaries.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_language.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_migration_framework.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_migration_v3_to_v4.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_migration_v4_to_v5.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_pauses.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_preparation.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_progress.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_public_identity.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_renderability.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_schema_history.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_schema_registry.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_schema_strict.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_semantic_boundaries.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_ssmdbook.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_toml_format.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/tests/test_version.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/__init__.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/__main__.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/atomic_io.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/attempt_serialization.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/attempts.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/batch.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/cli.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/codecs/__init__.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/codecs/v4_toml.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/codecs/v5_toml.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/compiler.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/config.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/diagnostics.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/directives.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/exceptions.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/explain.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/format.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/hashing.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/language.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/linguistics.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/migration.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/migrations/__init__.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/migrations/registry.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/migrations/v1_to_v2.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/migrations/v2_to_v3.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/migrations/v3_to_v4.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/migrations/v4_to_v5.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/model.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/parsers.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/pauses.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/preparation.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/progress.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/py.typed +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/renderability.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/schema_registry.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/schemas/v1.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/schemas/v2.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/schemas/v3.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/schemas/v4.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/serialization.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/spacy_models.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/spans.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/ssmdbook.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/stages/__init__.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/stages/document/__init__.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/stages/document/plain.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/stages/document/ssmd.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/stages/preparation/__init__.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/stages/preparation/identity.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/stages/preparation/spokenform.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/stages/protocols.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/toml_codec.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/trace_codec.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/units.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/utterplan.schema.json +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan/versioning.py +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan.egg-info/dependency_links.txt +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan.egg-info/entry_points.txt +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan.egg-info/requires.txt +0 -0
- {utterplan-0.5.0 → utterplan-0.5.1}/utterplan.egg-info/top_level.txt +0 -0
|
@@ -28,6 +28,12 @@ The dependency direction is from SSMD, spokenform, and phrasplit into UtterPlan,
|
|
|
28
28
|
|
|
29
29
|
`FlowPlan` is the current v5 executable contract: ordered `FlowUnit` objects each own ordered `FlowSegment` records. Segments include prepared text, effective language, local tokens, semantic pause intents, directives, markers, and optional heading metadata. `DocumentInfo` retains compact portable document information; `LinguisticProvenance` identifies providers without retaining their documents. UtterPlan ends before G2P. Engine/model selection, synthesis, and audio composition happen downstream.
|
|
30
30
|
|
|
31
|
+
## Compiler-owned segmentation topology
|
|
32
|
+
|
|
33
|
+
Segmentation has four distinct layers: parser-owned paragraph events define paragraph regions; PhraseSplit supplies advisory sentence-boundary proposals; UtterPlan validates those proposals and owns canonical paragraph, sentence, and private sentence-part spans in prepared spoken-text coordinates; renderer-required language, directive, pause, marker, and media cuts subdivide a part into final segments. Positive-width atomic media spans protect their interiors from automatic sentence/part boundaries; zero-width media events remain renderer operations, not parts. The public v5 `FlowPlan` contains only the resulting compact ordered flow—there is no serialized sentence-parts table.
|
|
34
|
+
|
|
35
|
+
PhraseSplit failure or an invalid proposal batch falls back to deterministic, conservative terminal scanning. Unsafe automatic cuts inside provider-aware lexical content are merged or dropped before projection; the final flow-projection token-edge check remains defense in depth. Exact surface and non-whitespace coverage checks run before and after applicable repairs. This normalization is independent of `renderability_mode`: strict and repair compilation use the same topology, while renderability mode governs only the existing punctuation-repair policy. See the [coordinate-space contract](coordinate-spaces) for offset ownership.
|
|
36
|
+
|
|
31
37
|
## SSMD and preparation
|
|
32
38
|
|
|
33
39
|
The document parser calls SSMD with `dialect="0.9"` for every SSMD source path, including unversioned fragments selected with `document_format="ssmd"`. UtterPlan does not parse SSMD 0.8 or migrate source. Older documents must first be converted with `ssmd migrate FILE --to 0.9`. The `utterplan migrate` command imports serialized legacy plans and applies schema migrations only.
|
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.1] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Improved sentence topology with validated fallback recovery and token-edge safeguards
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed fallback token projection and sentence-boundary repair for punctuation-separated text
|
|
16
|
+
|
|
17
|
+
<!-- generated by releaseledger -->
|
|
18
|
+
|
|
7
19
|
## [0.5.0] - 2026-10-09
|
|
8
20
|
|
|
9
21
|
### Added
|
|
@@ -19,4 +19,8 @@ Trace coordinates use Python string character boundaries (Unicode code points),
|
|
|
19
19
|
- `structural_to_spoken` maps structural character boundaries to spoken boundaries; `spoken_to_structural` maps spoken boundaries back to structural boundaries.
|
|
20
20
|
- `PreparationTraceUnit.source_start` / `source_end` address structural text. `PreparationChange.source_start` / `source_end` use structural coordinates; `output_start` / `output_end` use prepared spoken coordinates.
|
|
21
21
|
|
|
22
|
+
## Compiler-only segmentation coordinates
|
|
23
|
+
|
|
24
|
+
Paragraph, sentence, and sentence-part spans use half-open Python-character offsets into the prepared global `spoken_text`. PhraseSplit offsets are run-local and are translated to global spoken coordinates exactly once before validation. Renderer segments retain the exact `spoken_text[start:end]` slice; language-run edges and other required renderer cuts do not change their owning paragraph, sentence, or part index. These spans are compiler topology, not fields in the v5 flow. Segmentation diagnostics map their spoken ranges back to exact source coordinates through the compiler's source map.
|
|
25
|
+
|
|
22
26
|
Diagnostic source offsets use the exact original-source coordinate space. Diagnostic `line` and `column` values are 1-based; columns count Python characters from the start of a line. The trace retains all these maps and source evidence. The executable FlowPlan intentionally does not.
|
|
@@ -17,6 +17,8 @@ utterplan inspect-trace chapter.trace.toml --preparation --boundaries
|
|
|
17
17
|
|
|
18
18
|
Safe punctuation-only repair is the default during compilation. If a semantic blocker prevents repair, the error includes source location, prepared fragment, spoken context, blocked repair reason, and a concrete next action; UtterPlan never guesses symbol pronunciation. Use `--renderability strict` to see safe repair opportunities without applying them. Batch compilation reports each repair and continues with later sources.
|
|
19
19
|
|
|
20
|
+
Sentence segmentation is a compiler-integrity step, not a renderability repair. PhraseSplit is advisory: dependency failure or a malformed/partial proposal batch produces a `segmentation.phrasplit_*` diagnostic and triggers conservative fallback. Unsafe automatic token-edge candidates are dropped or merged before projection; exact text and non-whitespace coverage are checked in both strict and repair modes. Inspect compiler diagnostics with `compile --trace FILE` and `inspect-trace FILE --boundaries`; sentence-part topology itself is intentionally not persisted in v5.
|
|
21
|
+
|
|
20
22
|
Use `inspect --tokens` to inspect local `TokenView` values. Fallback analysis may leave POS, tag, and morphology unavailable. `explain --details` shows token spans relative to the owning segment and its flow hash. Token spans always slice `segment.text`; they are not offsets into the original source or another segment.
|
|
21
23
|
|
|
22
24
|
If spoken wording is wrong, inspect `FlowSegment.text`. If effective language is wrong, inspect the segment and `FlowPlan.linguistics`. If a pause seems missing, distinguish the semantic `PauseIntent` on the segment edge from the consumer's engine-specific timing policy. Exact authored timed breaks remain explicit; semantic strengths do not prescribe a duration.
|
|
@@ -24,6 +24,12 @@ text = "Hello world."
|
|
|
24
24
|
token.span = [[0, 5], [6, 12]]
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
## Compiler-only segmentation topology
|
|
28
|
+
|
|
29
|
+
The compiler validates PhraseSplit proposals and owns paragraph, sentence, and sentence-part spans in spoken-text coordinates. Semicolon/context-safe colon boundaries and optional clause/parenthetical proposals may create renderer cuts; language, directive, pause, marker, and media subdivisions retain the owning sentence-part identity internally. If proposals are malformed or incomplete, deterministic fallback is used and non-whitespace text remains covered exactly.
|
|
30
|
+
|
|
31
|
+
Sentence parts and their indices are deliberately absent from this v5 format. The `flow` contains only executable renderer segments and remains the compact public contract; no schema field, FlowPlan/FlowUnit/FlowSegment shape, or migration rule records private topology. Schema v5 is unchanged.
|
|
32
|
+
|
|
27
33
|
## Executable flow
|
|
28
34
|
|
|
29
35
|
Each ordered `flow` entry is a `FlowUnit` with a stable local `hash` and one or more `segment` entries. A segment contains already-prepared speech `text`, an optional language override, local token columns, optional semantic `pause_before` / `pause_after` intents, typed renderer-neutral directives, markers, and an optional heading level. Language defaults to the root `language`; `unit` identifies paragraph or sentence grouping. Consumers render segments in the stored order and do not need compiler lookup tables or segment-ID joins.
|
|
@@ -12,6 +12,12 @@ The Python defaults are deliberately spaCy-free and repair-first: `PlannerConfig
|
|
|
12
12
|
For an explicit contextual-G2P configuration, use `LinguisticsConfig(use_spacy=True, spacy_model="en_core_web_sm", require_spacy=True)`. The resulting plan records final pass-B token provenance in `linguistic_runs`; no provider document is retained.
|
|
13
13
|
spaCy enrichment is opt-in through the CLI's `--spacy auto` policy or an explicit `LinguisticsConfig` with a compatible local model. It may provide richer tokenization, POS tags, lemmas, and tags, but UtterPlan never downloads a model implicitly.
|
|
14
14
|
|
|
15
|
+
## Sentence segmentation and private topology
|
|
16
|
+
|
|
17
|
+
PhraseSplit is an advisory sentence-boundary provider, not an owner of compiler topology. UtterPlan validates each whole proposal batch, maps accepted run-local offsets once into prepared spoken-text coordinates, then recomputes paragraph and sentence indices from parser paragraph events and accepted boundaries. If PhraseSplit fails or returns malformed/incomplete coverage, deterministic conservative fallback preserves the text and emits diagnostics. Automatic cuts that divide provider-aware lexical content are discarded or merged; the final flow-projection token-edge check remains active.
|
|
18
|
+
|
|
19
|
+
Semicolon and context-safe colon separators, plus optional trusted clause/parenthetical candidates, form private sentence parts. Renderer-required language, directive, pause, marker, and media cuts subdivide those parts without changing their indices. Exact surface and non-whitespace coverage validation runs in strict and repair renderability modes alike. Parts are not a public field: only the compact ordered v5 `flow` is serialized, and `unit="sentence"` groups by the recomputed sentence topology.
|
|
20
|
+
|
|
15
21
|
## Canonical single-document compiler
|
|
16
22
|
|
|
17
23
|
`compile_document` is the shared semantic interpretation boundary for independent
|
|
@@ -220,7 +220,7 @@ def test_compile_strict_renderability_reports_source_and_writes_no_plan(
|
|
|
220
220
|
assert "Automatic repair: safe;" in captured.err
|
|
221
221
|
|
|
222
222
|
|
|
223
|
-
def
|
|
223
|
+
def test_compile_repair_cli_blocks_unattachable_punctuation_without_loss(
|
|
224
224
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
225
225
|
) -> None:
|
|
226
226
|
source = tmp_path / "repair.txt"
|
|
@@ -244,9 +244,11 @@ def test_compile_repair_cli_reports_guaranteed_renderability(
|
|
|
244
244
|
)
|
|
245
245
|
|
|
246
246
|
captured = capsys.readouterr()
|
|
247
|
-
assert result ==
|
|
248
|
-
assert
|
|
249
|
-
assert "
|
|
247
|
+
assert result == 1
|
|
248
|
+
assert not output.exists()
|
|
249
|
+
assert "isolated punctuation became its own speech segment" in captured.err
|
|
250
|
+
assert "Prepared fragment: '.'" in captured.err
|
|
251
|
+
assert "Automatic repair: safe; remove the isolated punctuation-only segment." in captured.err
|
|
250
252
|
|
|
251
253
|
|
|
252
254
|
def test_top_level_version(capsys: pytest.CaptureFixture[str]) -> None:
|
|
@@ -237,7 +237,7 @@ def test_compile_many_protects_existing_output_and_force_replaces_it(
|
|
|
237
237
|
)
|
|
238
238
|
|
|
239
239
|
|
|
240
|
-
def
|
|
240
|
+
def test_compile_many_repair_and_strict_modes_preserve_unattachable_punctuation(
|
|
241
241
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
242
242
|
) -> None:
|
|
243
243
|
source = _write(tmp_path / "chapter.txt", "Hello.\n\n.\n\nWorld.")
|
|
@@ -257,9 +257,13 @@ def test_compile_many_default_repair_and_strict_mode_are_both_observable(
|
|
|
257
257
|
]
|
|
258
258
|
)
|
|
259
259
|
repair_output = capsys.readouterr()
|
|
260
|
-
assert repair_result ==
|
|
261
|
-
assert "
|
|
262
|
-
assert "
|
|
260
|
+
assert repair_result == 1
|
|
261
|
+
assert "isolated punctuation became its own speech segment" in repair_output.err
|
|
262
|
+
assert "Prepared fragment: '.'" in repair_output.err
|
|
263
|
+
assert (
|
|
264
|
+
"Automatic repair: safe; remove the isolated punctuation-only segment." in repair_output.err
|
|
265
|
+
)
|
|
266
|
+
assert not (repair_dir / "chapter.utterplan.toml").exists()
|
|
263
267
|
|
|
264
268
|
strict_dir = tmp_path / "strict"
|
|
265
269
|
strict_result = main(
|
|
@@ -429,3 +429,42 @@ def test_report_path_collision_with_plan_output_is_preflighted(
|
|
|
429
429
|
assert result == 2
|
|
430
430
|
assert "report path collides with plan output" in captured.err
|
|
431
431
|
assert not output_dir.exists()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_compile_book_localizes_fallback_boundaries_with_default_settings(
|
|
435
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
436
|
+
) -> None:
|
|
437
|
+
root = tmp_path / "fallback-boundaries.ssmdbook"
|
|
438
|
+
_write_book(
|
|
439
|
+
root,
|
|
440
|
+
(5, 6),
|
|
441
|
+
contents={
|
|
442
|
+
5: _ssmd('[alpha]{emphasis="strong"}—beta'),
|
|
443
|
+
6: _ssmd('alpha—[beta]{emphasis="strong"}'),
|
|
444
|
+
},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
result = main(["compile-book", str(root), "--chapters", "5-6"])
|
|
448
|
+
capsys.readouterr()
|
|
449
|
+
|
|
450
|
+
assert result == 0
|
|
451
|
+
output_dir = root / "utterplan"
|
|
452
|
+
report = _report(output_dir / "compile-report.toml")
|
|
453
|
+
assert report["requested"] == 2
|
|
454
|
+
assert report["written"] == 2
|
|
455
|
+
assert report["failed"] == 0
|
|
456
|
+
assert report["skipped"] == 0
|
|
457
|
+
assert report["complete"] is True
|
|
458
|
+
|
|
459
|
+
for number, expected_surface in ((5, "alpha"), (6, "beta")):
|
|
460
|
+
plan_path = output_dir / f"chapter-{number:04d}.utterplan.toml"
|
|
461
|
+
assert plan_path.exists()
|
|
462
|
+
plan = FlowPlan.load(plan_path)
|
|
463
|
+
assert plan.linguistics[0].provider == "fallback"
|
|
464
|
+
surfaces = {
|
|
465
|
+
segment.text[token.start : token.end]
|
|
466
|
+
for unit in plan.flow
|
|
467
|
+
for segment in unit.segments
|
|
468
|
+
for token in segment.tokens
|
|
469
|
+
}
|
|
470
|
+
assert expected_surface in surfaces
|
|
@@ -128,8 +128,11 @@ def test_multilingual_and_sentence_units_remain_in_render_order() -> None:
|
|
|
128
128
|
|
|
129
129
|
rendered = _fake_renderer(plan)
|
|
130
130
|
assert [item[1] for item in rendered] == ["en-us", "fr"]
|
|
131
|
-
assert len(plan.flow) ==
|
|
132
|
-
assert
|
|
131
|
+
assert len(plan.flow) == 2
|
|
132
|
+
assert [[segment.text for segment in unit.segments] for unit in plan.flow] == [
|
|
133
|
+
["One."],
|
|
134
|
+
["Bonjour."],
|
|
135
|
+
]
|
|
133
136
|
assert_public_consumer_contract(plan)
|
|
134
137
|
|
|
135
138
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from utterplan import (
|
|
10
|
+
FlowPlan,
|
|
11
|
+
LinguisticsConfig,
|
|
12
|
+
PauseConfig,
|
|
13
|
+
PlannerConfig,
|
|
14
|
+
UtterancePlanner,
|
|
15
|
+
compile_document,
|
|
16
|
+
)
|
|
17
|
+
from utterplan.exceptions import PlanValidationError
|
|
18
|
+
from utterplan.flow_projection import _token_providers, _tokens_for_segment
|
|
19
|
+
from utterplan.migrations.v4_to_v5 import migrate_v4_to_v5
|
|
20
|
+
from utterplan.model import LinguisticRun, TokenAnnotation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _segments(plan: FlowPlan):
|
|
24
|
+
return tuple(segment for unit in plan.flow for segment in unit.segments)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_fresh_compile_returns_current_v5_local_flow() -> None:
|
|
28
|
+
text = 'Hello [world]{emphasis="strong"}.'
|
|
29
|
+
config = PlannerConfig(language="en-us", document_format="ssmd", text_preparation="identity")
|
|
30
|
+
result = compile_document(text, input_format="ssmd", config=config)
|
|
31
|
+
planner = UtterancePlanner(config)
|
|
32
|
+
try:
|
|
33
|
+
planned = planner.plan(text)
|
|
34
|
+
finally:
|
|
35
|
+
planner.close()
|
|
36
|
+
|
|
37
|
+
assert isinstance(result.plan, FlowPlan)
|
|
38
|
+
assert result.plan.schema_version == 5
|
|
39
|
+
assert planned == result.plan
|
|
40
|
+
payload = result.plan.to_toml()
|
|
41
|
+
assert payload == result.plan.to_toml()
|
|
42
|
+
assert FlowPlan.from_toml(payload) == result.plan
|
|
43
|
+
assert not hasattr(result.plan, "tokens")
|
|
44
|
+
assert not hasattr(result.plan, "segments")
|
|
45
|
+
assert any(segment.directives.emphasis is not None for segment in _segments(result.plan))
|
|
46
|
+
for segment in _segments(result.plan):
|
|
47
|
+
assert all(segment.text[token.start : token.end] for token in segment.tokens)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize(
|
|
51
|
+
"fixture_name",
|
|
52
|
+
("basic_en", "ssmd_breaks", "markers", "multilingual", "parenthetical", "spokenform_offsets"),
|
|
53
|
+
)
|
|
54
|
+
def test_fresh_compilation_converges_with_v4_migration_on_unambiguous_input(
|
|
55
|
+
fixture_name: str,
|
|
56
|
+
) -> None:
|
|
57
|
+
fixture = Path(__file__).parent / f"migration/fixtures/v4/{fixture_name}.utterplan.json"
|
|
58
|
+
v4_plan = json.loads(fixture.read_text(encoding="utf-8"))
|
|
59
|
+
old_config = v4_plan["config"]
|
|
60
|
+
old_pauses = old_config.get("pauses", {})
|
|
61
|
+
config = PlannerConfig(
|
|
62
|
+
language=old_config["language"],
|
|
63
|
+
document_format=old_config["document_format"],
|
|
64
|
+
unit=old_config.get("unit", "paragraph"),
|
|
65
|
+
text_preparation=old_config.get("text_preparation", "spokenform"),
|
|
66
|
+
pauses=PauseConfig(
|
|
67
|
+
mode=old_pauses.get("mode", "tts"),
|
|
68
|
+
enabled=old_pauses.get("enabled", True),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
fresh = compile_document(
|
|
72
|
+
v4_plan["source"]["text"],
|
|
73
|
+
input_format=old_config["document_format"],
|
|
74
|
+
config=config,
|
|
75
|
+
).plan
|
|
76
|
+
migrated = FlowPlan.from_dict(migrate_v4_to_v5(v4_plan))
|
|
77
|
+
|
|
78
|
+
if fixture_name == "multilingual":
|
|
79
|
+
# The compiler now preserves the inter-language whitespace on the right-hand
|
|
80
|
+
# renderer segment instead of dropping it at the run boundary.
|
|
81
|
+
assert fresh.flow != migrated.flow
|
|
82
|
+
assert [[segment.text for segment in unit.segments] for unit in fresh.flow] == [
|
|
83
|
+
["Hello", " Bonjour."]
|
|
84
|
+
]
|
|
85
|
+
assert fresh.document == migrated.document
|
|
86
|
+
assert fresh.linguistics == migrated.linguistics
|
|
87
|
+
assert fresh.language == migrated.language
|
|
88
|
+
assert fresh.unit == migrated.unit
|
|
89
|
+
else:
|
|
90
|
+
assert fresh.plan_id == migrated.plan_id
|
|
91
|
+
assert fresh.flow == migrated.flow
|
|
92
|
+
assert fresh.document == migrated.document
|
|
93
|
+
assert fresh.linguistics == migrated.linguistics
|
|
94
|
+
assert fresh.language == migrated.language
|
|
95
|
+
assert fresh.unit == migrated.unit
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_fresh_compile_localizes_a_token_cut_by_ssmd_semantics() -> None:
|
|
99
|
+
fixture = Path(__file__).parent / "migration/fixtures/v4/ssmd_09_comprehensive.utterplan.json"
|
|
100
|
+
source = json.loads(fixture.read_text(encoding="utf-8"))["source"]["text"]
|
|
101
|
+
result = compile_document(
|
|
102
|
+
source,
|
|
103
|
+
input_format="ssmd",
|
|
104
|
+
config=PlannerConfig(
|
|
105
|
+
language="sr-Latn",
|
|
106
|
+
document_format="ssmd",
|
|
107
|
+
text_preparation="identity",
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
segments = _segments(result.plan)
|
|
112
|
+
water_segment = next(segment for segment in segments if segment.text == "H2O")
|
|
113
|
+
assert any(segment.text == ", and" for segment in segments)
|
|
114
|
+
assert any(
|
|
115
|
+
token.start == 0 and token.end == 3 and water_segment.text[token.start : token.end] == "H2O"
|
|
116
|
+
for token in water_segment.tokens
|
|
117
|
+
)
|
|
118
|
+
for segment in segments:
|
|
119
|
+
for token in segment.tokens:
|
|
120
|
+
assert segment.text[token.start : token.end]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _project_partial_token(
|
|
124
|
+
text: str,
|
|
125
|
+
start: int,
|
|
126
|
+
end: int,
|
|
127
|
+
*,
|
|
128
|
+
provider: str = "fallback",
|
|
129
|
+
lemma: str | None = None,
|
|
130
|
+
):
|
|
131
|
+
token = TokenAnnotation(0, len(text), text, lemma=lemma)
|
|
132
|
+
plan = SimpleNamespace(
|
|
133
|
+
texts=SimpleNamespace(spoken=text),
|
|
134
|
+
tokens=(token,),
|
|
135
|
+
linguistic_runs=(LinguisticRun("language-0001", provider, 0, 1),),
|
|
136
|
+
)
|
|
137
|
+
segment = SimpleNamespace(
|
|
138
|
+
id="seg-000123",
|
|
139
|
+
language="en-US",
|
|
140
|
+
spoken_start=start,
|
|
141
|
+
spoken_end=end,
|
|
142
|
+
token_indices=(0,),
|
|
143
|
+
)
|
|
144
|
+
return _tokens_for_segment(
|
|
145
|
+
plan,
|
|
146
|
+
segment,
|
|
147
|
+
_token_providers(plan),
|
|
148
|
+
"$.flow[0].segments[0].tokens",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@pytest.mark.parametrize(
|
|
153
|
+
("body", "expected_surface"),
|
|
154
|
+
(
|
|
155
|
+
('[alpha]{emphasis="strong"}—beta', "alpha"),
|
|
156
|
+
('alpha—[beta]{emphasis="strong"}', "beta"),
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
def test_fresh_compile_localizes_fallback_tokens_at_internal_punctuation(
|
|
160
|
+
body: str, expected_surface: str
|
|
161
|
+
) -> None:
|
|
162
|
+
source = f'---\nssmd_version: "0.9"\nlanguage: en-US\n---\n{body}\n'
|
|
163
|
+
result = compile_document(
|
|
164
|
+
source,
|
|
165
|
+
input_format="ssmd",
|
|
166
|
+
config=PlannerConfig(
|
|
167
|
+
language="en-US",
|
|
168
|
+
document_format="ssmd",
|
|
169
|
+
text_preparation="identity",
|
|
170
|
+
linguistics=LinguisticsConfig(use_spacy=False),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert result.plan.linguistics[0].provider == "fallback"
|
|
175
|
+
segments = _segments(result.plan)
|
|
176
|
+
assert any(
|
|
177
|
+
segment.text[token.start : token.end] == expected_surface
|
|
178
|
+
for segment in segments
|
|
179
|
+
for token in segment.tokens
|
|
180
|
+
)
|
|
181
|
+
for segment in segments:
|
|
182
|
+
assert all(segment.text[token.start : token.end] for token in segment.tokens)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pytest.mark.parametrize(
|
|
186
|
+
("start", "end", "split_side"),
|
|
187
|
+
((0, 3, "end"), (2, 5, "start")),
|
|
188
|
+
)
|
|
189
|
+
def test_fallback_provider_rejects_true_alphanumeric_splits_with_coordinates(
|
|
190
|
+
start: int, end: int, split_side: str
|
|
191
|
+
) -> None:
|
|
192
|
+
with pytest.raises(PlanValidationError) as exc_info:
|
|
193
|
+
_project_partial_token("hello", start, end, lemma="hello")
|
|
194
|
+
|
|
195
|
+
error = exc_info.value
|
|
196
|
+
assert error.code == "token.segment_split"
|
|
197
|
+
assert error.path == "$.flow[0].segments[0].tokens"
|
|
198
|
+
assert f"segment seg-000123 [{start}:{end}]" in str(error)
|
|
199
|
+
assert f"token 0 [0:5] at its {split_side}" in str(error)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@pytest.mark.parametrize("provider", ("spacy", "unknown"))
|
|
203
|
+
def test_nonfallback_provider_keeps_punctuation_connected_partial_tokens_strict(
|
|
204
|
+
provider: str,
|
|
205
|
+
) -> None:
|
|
206
|
+
with pytest.raises(PlanValidationError) as exc_info:
|
|
207
|
+
_project_partial_token("alpha—beta", 0, 5, provider=provider, lemma="alpha—beta")
|
|
208
|
+
|
|
209
|
+
assert exc_info.value.code == "token.segment_split"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_fallback_partial_default_lemmas_are_casefolded_per_fragment() -> None:
|
|
213
|
+
text = "Straße—Bahn"
|
|
214
|
+
default_lemma = text.lower()
|
|
215
|
+
|
|
216
|
+
left = _project_partial_token(text, 0, 6, lemma=default_lemma)
|
|
217
|
+
right = _project_partial_token(text, 7, 11, lemma=default_lemma)
|
|
218
|
+
|
|
219
|
+
assert [token.lemma for token in left] == ["strasse"]
|
|
220
|
+
assert [token.lemma for token in right] == ["bahn"]
|
|
@@ -13,11 +13,9 @@ from utterplan import (
|
|
|
13
13
|
PlannerConfig,
|
|
14
14
|
UtterancePlan,
|
|
15
15
|
)
|
|
16
|
-
from utterplan.exceptions import PlanningError
|
|
17
16
|
from utterplan.explain import format_explanation
|
|
18
17
|
from utterplan.language import LanguageRun
|
|
19
18
|
from utterplan.linguistics import LinguisticResourcePool, analyze_run_analyses
|
|
20
|
-
from utterplan.planner import _split_run
|
|
21
19
|
|
|
22
20
|
|
|
23
21
|
class FakeProviderDoc(list):
|
|
@@ -209,7 +207,7 @@ def test_clausal_boundaries_reuse_provider_document(monkeypatch):
|
|
|
209
207
|
assert len(docs) == 1
|
|
210
208
|
assert detected == [(source, docs[-1])]
|
|
211
209
|
boundary = next(item for item in plan.boundaries if item.kind == "clausal_comma")
|
|
212
|
-
assert boundary.position == source.index(",")
|
|
210
|
+
assert boundary.position == source.index(",") + 2
|
|
213
211
|
semantic = next(item for item in plan.semantic_boundaries if item.kind == "clause")
|
|
214
212
|
assert semantic.position == source.index(",") + 2
|
|
215
213
|
assert semantic.attrs["detector_start"] == source.index(",")
|
|
@@ -274,8 +272,8 @@ def test_semantic_clause_is_independent_of_pause_activation(monkeypatch):
|
|
|
274
272
|
assert [(item.position, item.kind, item.id) for item in clauses] == [
|
|
275
273
|
(source.index(",") + 2, "clause", "semantic-boundary-000000")
|
|
276
274
|
] * 3
|
|
277
|
-
assert len(plans[0].segments) ==
|
|
278
|
-
assert plans[0].segments[0].
|
|
275
|
+
assert len(plans[0].segments) == 2
|
|
276
|
+
assert plans[0].segments[0].spoken_end == clauses[0].position
|
|
279
277
|
assert not any(
|
|
280
278
|
segment.pause_before is not None or segment.pause_after is not None
|
|
281
279
|
for segment in plans[0].segments
|
|
@@ -331,17 +329,6 @@ def test_fallback_does_not_attempt_clausal_boundary_detection(monkeypatch):
|
|
|
331
329
|
assert not any(item.kind == "clause" for item in plan.semantic_boundaries)
|
|
332
330
|
|
|
333
331
|
|
|
334
|
-
def test_segmentation_type_error_is_not_a_whole_document_fallback(monkeypatch):
|
|
335
|
-
import phrasplit
|
|
336
|
-
|
|
337
|
-
def broken_split(*args, **kwargs):
|
|
338
|
-
raise TypeError("invalid nlp integration")
|
|
339
|
-
|
|
340
|
-
monkeypatch.setattr(phrasplit, "split_with_offsets", broken_split)
|
|
341
|
-
with pytest.raises(PlanningError, match="sentence segmentation integration failed"):
|
|
342
|
-
_split_run("One sentence. Two sentences.", "en-us")
|
|
343
|
-
|
|
344
|
-
|
|
345
332
|
def _fake_spacy_plan(
|
|
346
333
|
monkeypatch, *, tag: str = "VBP", morph: str = "Tense=Pres|VerbForm=Fin"
|
|
347
334
|
) -> UtterancePlan:
|
|
@@ -2,6 +2,7 @@ import pytest
|
|
|
2
2
|
|
|
3
3
|
from tests.compiler_helpers import CompilerTestPlanner as UtterancePlanner
|
|
4
4
|
from utterplan import PauseConfig, PauseIntent, PlannerConfig, PlanningError
|
|
5
|
+
from utterplan.exceptions import PlanValidationError
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def test_plain_sentences_paragraphs_and_pauses():
|
|
@@ -16,6 +17,28 @@ def test_plain_sentences_paragraphs_and_pauses():
|
|
|
16
17
|
assert plan.segments[1].pause_after == PauseIntent("paragraph")
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
def test_sentence_part_edges_split_renderer_segments_and_auto_pause_at_part_boundary():
|
|
21
|
+
source = "Before; after."
|
|
22
|
+
plans = [
|
|
23
|
+
UtterancePlanner(
|
|
24
|
+
PlannerConfig(
|
|
25
|
+
language="en-us",
|
|
26
|
+
text_preparation="identity",
|
|
27
|
+
pauses=PauseConfig(mode=mode, enabled=enabled),
|
|
28
|
+
)
|
|
29
|
+
).plan(source)
|
|
30
|
+
for mode, enabled in (("tts", True), ("auto", True), ("auto", False))
|
|
31
|
+
]
|
|
32
|
+
expected_texts = ["Before; ", "after."]
|
|
33
|
+
for plan in plans:
|
|
34
|
+
assert [segment.text for segment in plan.segments] == expected_texts
|
|
35
|
+
assert [segment.clause for segment in plan.segments] == [0, 1]
|
|
36
|
+
assert plan.semantic_boundaries[0].position == len("Before; ")
|
|
37
|
+
assert plans[0].segments[0].pause_after is None
|
|
38
|
+
assert plans[1].segments[0].pause_after == PauseIntent("clause")
|
|
39
|
+
assert plans[2].segments[0].pause_after is None
|
|
40
|
+
|
|
41
|
+
|
|
19
42
|
def test_ssmd_preparation_and_explicit_directives():
|
|
20
43
|
plan = UtterancePlanner(PlannerConfig(language="en-us", document_format="ssmd")).plan(
|
|
21
44
|
'[tomato]{ph="təˈmeɪtoʊ"}'
|
|
@@ -97,7 +120,7 @@ def test_unrepresentable_audio_topology_is_rejected(source: str, message: str) -
|
|
|
97
120
|
planner.plan(source)
|
|
98
121
|
|
|
99
122
|
|
|
100
|
-
def
|
|
123
|
+
def test_adjacent_audio_annotations_report_required_token_conflict() -> None:
|
|
101
124
|
planner = UtterancePlanner(
|
|
102
125
|
PlannerConfig(
|
|
103
126
|
language="en-us",
|
|
@@ -105,18 +128,11 @@ def test_adjacent_audio_annotations_are_distinct_and_supported() -> None:
|
|
|
105
128
|
text_preparation="identity",
|
|
106
129
|
)
|
|
107
130
|
)
|
|
108
|
-
plan = planner.plan('[first]{src="same.wav"}[second]{src="same.wav"}')
|
|
109
|
-
audio_segments = [segment for segment in plan.segments if segment.directives.audio is not None]
|
|
110
131
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
second_audio = audio_segments[1].directives.audio
|
|
132
|
+
with pytest.raises(PlanValidationError) as exc_info:
|
|
133
|
+
planner.plan('[first]{src="same.wav"}[second]{src="same.wav"}')
|
|
114
134
|
|
|
115
|
-
assert
|
|
116
|
-
assert second_audio is not None
|
|
117
|
-
assert first_audio.src == second_audio.src
|
|
118
|
-
assert audio_segments[0].id != audio_segments[1].id
|
|
119
|
-
assert audio_segments[0].spoken_start < audio_segments[1].spoken_start
|
|
135
|
+
assert exc_info.value.code == "segmentation.required_cut_conflict"
|
|
120
136
|
|
|
121
137
|
|
|
122
138
|
def test_audio_fallback_is_atomic_across_sentence_segmentation() -> None:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import replace
|
|
4
|
+
from typing import Literal
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
@@ -72,26 +73,20 @@ def _assess(
|
|
|
72
73
|
return replace(issue, repair_assessment=assessment)
|
|
73
74
|
|
|
74
75
|
|
|
75
|
-
def
|
|
76
|
+
def test_repair_mode_preserves_isolated_punctuation_instead_of_dropping_content() -> None:
|
|
76
77
|
config = PlannerConfig(language="en-US", text_preparation="identity")
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
planner = UtterancePlanner(config)
|
|
79
|
+
attempt = planner.compile_attempt("Hello.\n\n.\n\nWorld.")
|
|
80
|
+
repeated = UtterancePlanner(config).compile_attempt("Hello.\n\n.\n\nWorld.")
|
|
79
81
|
|
|
80
82
|
assert config.renderability_mode == "repair"
|
|
81
|
-
assert
|
|
82
|
-
assert repeated.
|
|
83
|
-
assert [segment.text for segment in
|
|
84
|
-
assert
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"guaranteed": True,
|
|
89
|
-
}
|
|
90
|
-
repairs = [item for item in plan.diagnostics if item.code == "planning.renderability.repaired"]
|
|
91
|
-
assert len(repairs) == 1
|
|
92
|
-
assert "by removing the punctuation-only segment" in repairs[0].message
|
|
93
|
-
assert repairs[0].line == 3
|
|
94
|
-
assert plan.texts.spoken == "Hello.\n\n.\n\nWorld."
|
|
83
|
+
assert attempt.status == "blocked"
|
|
84
|
+
assert repeated.attempt_id == attempt.attempt_id
|
|
85
|
+
assert [segment.text for segment in attempt.candidate.segments] == ["Hello.", ".", "World."]
|
|
86
|
+
assert attempt.candidate.document_metadata["planning"]["renderability"]["guaranteed"] is False
|
|
87
|
+
assert not any(item.code == "planning.renderability.repaired" for item in attempt.diagnostics)
|
|
88
|
+
assert attempt.renderability.issues[0].line == 3
|
|
89
|
+
assert attempt.candidate.texts.spoken == "Hello.\n\n.\n\nWorld."
|
|
95
90
|
|
|
96
91
|
|
|
97
92
|
def test_default_repairs_exact_comma_space_with_isolated_planner_topology(
|
|
@@ -362,18 +357,18 @@ def test_strict_attempt_retains_blocked_candidate_and_compile_still_raises() ->
|
|
|
362
357
|
assert error.value.issues == attempt.renderability.issues
|
|
363
358
|
|
|
364
359
|
|
|
365
|
-
def
|
|
360
|
+
def test_unattachable_punctuation_remains_in_a_blocked_repair_attempt() -> None:
|
|
366
361
|
config = PlannerConfig(language="en-US", text_preparation="identity")
|
|
367
362
|
attempt = UtterancePlanner(config).compile_attempt("Hello.\n\n.\n\nWorld.")
|
|
368
363
|
|
|
369
|
-
assert attempt.status == "
|
|
370
|
-
assert attempt.ok
|
|
371
|
-
assert
|
|
372
|
-
assert attempt.
|
|
373
|
-
assert attempt.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
364
|
+
assert attempt.status == "blocked"
|
|
365
|
+
assert not attempt.ok
|
|
366
|
+
assert attempt.repairs == ()
|
|
367
|
+
assert attempt.renderability.issues[0].repair_assessment is not None
|
|
368
|
+
assert attempt.renderability.issues[0].repair_assessment.safe
|
|
369
|
+
assert [segment.text for segment in attempt.candidate.segments] == ["Hello.", ".", "World."]
|
|
370
|
+
with pytest.raises(ValueError, match="blocked planning-attempt draft"):
|
|
371
|
+
attempt.candidate.to_plan()
|
|
377
372
|
|
|
378
373
|
|
|
379
374
|
def test_unsafe_repair_attempt_is_blocked_and_draft_structure_is_checked() -> None:
|
|
@@ -405,12 +400,12 @@ def test_unsafe_repair_attempt_is_blocked_and_draft_structure_is_checked() -> No
|
|
|
405
400
|
("source", "mode", "expected_status"),
|
|
406
401
|
[
|
|
407
402
|
("Ready.", "strict", "renderable"),
|
|
408
|
-
("Hello.\n\n.\n\nWorld.", "repair", "
|
|
403
|
+
("Hello.\n\n.\n\nWorld.", "repair", "blocked"),
|
|
409
404
|
("€", "strict", "blocked"),
|
|
410
405
|
],
|
|
411
406
|
)
|
|
412
407
|
def test_planning_attempt_toml_roundtrips_all_outcomes_and_stays_noncanonical(
|
|
413
|
-
source: str, mode:
|
|
408
|
+
source: str, mode: Literal["strict", "repair"], expected_status: str
|
|
414
409
|
) -> None:
|
|
415
410
|
config = PlannerConfig(
|
|
416
411
|
language="en-US",
|