audioexport 0.1.4__tar.gz → 0.1.7__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.
Files changed (45) hide show
  1. {audioexport-0.1.4 → audioexport-0.1.7}/PKG-INFO +22 -12
  2. {audioexport-0.1.4 → audioexport-0.1.7}/README.md +21 -11
  3. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/__init__.py +10 -1
  4. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/_version.py +3 -3
  5. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/api.py +10 -1
  6. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/bundle.py +167 -11
  7. audioexport-0.1.7/audioexport/bundle_inspection.py +700 -0
  8. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/cli.py +93 -21
  9. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/document.py +30 -3
  10. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/pipeline.py +34 -4
  11. audioexport-0.1.7/audioexport/variants.py +339 -0
  12. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport.egg-info/SOURCES.txt +2 -0
  13. {audioexport-0.1.4 → audioexport-0.1.7}/docs/changelog.md +29 -1
  14. {audioexport-0.1.4 → audioexport-0.1.7}/docs/index.md +18 -10
  15. {audioexport-0.1.4 → audioexport-0.1.7}/pyproject.toml +5 -0
  16. {audioexport-0.1.4 → audioexport-0.1.7}/LICENSE +0 -0
  17. {audioexport-0.1.4 → audioexport-0.1.7}/MANIFEST.in +0 -0
  18. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/__main__.py +0 -0
  19. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/_workspace.py +0 -0
  20. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/audiobook.py +0 -0
  21. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/audiobook_profile.py +0 -0
  22. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/book.py +0 -0
  23. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/bundle_contract.py +0 -0
  24. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/chapters.py +0 -0
  25. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/errors.py +0 -0
  26. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/fftools.py +0 -0
  27. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/formats.py +0 -0
  28. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/inputs.py +0 -0
  29. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/metadata.py +0 -0
  30. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/preflight.py +0 -0
  31. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/profile.py +0 -0
  32. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/py.typed +0 -0
  33. {audioexport-0.1.4 → audioexport-0.1.7}/audioexport/validation.py +0 -0
  34. {audioexport-0.1.4 → audioexport-0.1.7}/docs/Makefile +0 -0
  35. {audioexport-0.1.4 → audioexport-0.1.7}/docs/conf.py +0 -0
  36. {audioexport-0.1.4 → audioexport-0.1.7}/docs/make.bat +0 -0
  37. {audioexport-0.1.4 → audioexport-0.1.7}/docs/make.py +0 -0
  38. {audioexport-0.1.4 → audioexport-0.1.7}/docs/requirements.txt +0 -0
  39. {audioexport-0.1.4 → audioexport-0.1.7}/examples/audiobook.toml +0 -0
  40. {audioexport-0.1.4 → audioexport-0.1.7}/examples/chapters.json +0 -0
  41. {audioexport-0.1.4 → audioexport-0.1.7}/examples/chapters.txt +0 -0
  42. {audioexport-0.1.4 → audioexport-0.1.7}/examples/create_demo_wav.py +0 -0
  43. {audioexport-0.1.4 → audioexport-0.1.7}/examples/create_ssmd_bundle_demo.py +0 -0
  44. {audioexport-0.1.4 → audioexport-0.1.7}/examples/export.toml +0 -0
  45. {audioexport-0.1.4 → audioexport-0.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: audioexport
3
- Version: 0.1.4
3
+ Version: 0.1.7
4
4
  Summary: Standalone FFmpeg audio encoder with metadata, chapter, cover and reproducible export manifests
5
5
  Author: audioexport contributors
6
6
  License-Expression: Apache-2.0
@@ -164,26 +164,36 @@ audioexport bundle export "My Book.ssmd-bundle" --variant "$VARIANT" \
164
164
 
165
165
  audioexport bundle export story.ssmd-bundle --variant "$VARIANT" \
166
166
  --output story.m4a --title "Story" --artist "An Author"
167
+
168
+ # Inspect the AudioCompose export-stage inventory (BUNDLE defaults to .).
169
+ audioexport bundle variants
170
+
171
+ # Export every discovered variant in deterministic name order.
172
+ audioexport bundle export story.ssmd-bundle --format mp3
173
+ # A batch can relocate its variant-separated outputs.
174
+ audioexport bundle export story.ssmd-bundle --format mp3 --output-root /srv/audio-exports
167
175
  ```
168
176
 
169
- AudioExport verifies the selected AudioCompose report, output paths, WAV checksums, and audio geometry before exporting. For a book, `--chapters` selects manifest chapters (`all` by default), output defaults to M4B, `--format` accepts only `m4b`, and audiobook profiles remain supported. For a document, only `--chapters all` is valid; `--format` or the output filename extension selects the final format, and `--timeline` supplies an optional generic chapter timeline. `--author` and `--artist` are aliases for the appropriate author/artist metadata field. Kind-specific options that have no meaning for the selected manifest are rejected rather than ignored.
177
+ `audioexport bundle variants [BUNDLE]` is a read-only AudioExport-stage inventory: it inspects immediate variant directories and compose reports only, does not hash WAVs or create output directories, and reports malformed candidates without hiding other variants. Use `--json` for the `audioexport-bundle-variants` envelope or `--audiocompose-root PATH` to select another stage root. Full render/compose pipeline status remains AudioCompose's concern.
178
+
179
+ AudioExport verifies selected reports, report-relative output paths, WAV checksums, and audio geometry before encoding. Current AudioCompose document reports store geometry in `[output_geometry]`; AudioExport also accepts legacy flat reports, but rejects contradictory mixed layouts. Books use verified M4B assembly, support chapter selection and audiobook profiles, and accept only `--format m4b` (M4B is the default). Documents support only `--chapters all`; explicit single-variant export accepts `--format` or infers the format from `--output`, while all-variant document export requires `--format`. `--timeline` supplies an optional generic document chapter timeline.
170
180
 
171
- Default output names strip only `.ssmd-bundle`: books use `audioexport/<variant>/<name>.m4b`, while documents use `audioexport/<variant>/<name>.<format>`. `--output-root` relocates these defaults; `--output` selects an explicit destination. Book metadata precedence is manifest, then audiobook profile, then explicit CLI/API values. BCP 47 language tags such as `en-US` are normalized to ISO 639-2/T track codes such as `eng` for M4A/M4B.
181
+ Single-variant default names strip only `.ssmd-bundle`: books use `audioexport/<variant>/<name>.m4b`, and documents use `audioexport/<variant>/<name>.<format>`. When `--variant` is omitted, export processes every immediate AudioCompose variant in deterministic name order and keeps outputs separated as `audioexport/<variant>/<name>.<format>` (books use `.m4b`). `--output-root` relocates single-variant defaults or, in batch mode, the whole variant-separated tree. A single `--output FILE` requires an explicit `--variant`; it is rejected in batch mode before encoding. Batch export is fail-fast and does not remove outputs already completed before a later variant fails. `--author` and `--artist` are aliases for the appropriate metadata field; options unsupported for the detected bundle kind fail explicitly.
172
182
 
173
- The JSON result uses one versioned envelope with `format: "audioexport-bundle-result"`, `version: 1`, `kind`, and a kind-specific `result` object. Human output begins with `AudioExport bundle export` and reports the selected kind, workspace, variant, and output.
183
+ The explicit single-variant JSON result remains `audioexport-bundle-result` with version 1 and a `result` object. Human single-variant output begins with `AudioExport bundle export` and reports kind, workspace, variant, output, and the selected chapter count or composed document item. All-variant export uses `audioexport-bundle-batch-result`; human output lists each variant and its output. Book metadata precedence is manifest, then audiobook profile, then explicit CLI/API values. BCP 47 language tags such as `en-US` normalize to ISO 639-2/T track codes such as `eng` for M4A/M4B.
174
184
 
175
- Python callers use the same manifest-dispatched facade:
185
+ Python callers can inspect and export variants through the same public API:
176
186
 
177
187
  ```python
178
- from audioexport import export_bundle
188
+ from audioexport import export_bundle, export_bundle_all, list_bundle_variants
179
189
 
180
- book_result = export_bundle("My Book.ssmd-bundle", variant=variant, chapters="5,7")
181
- document_result = export_bundle("story.ssmd-bundle", variant=variant, format="mp3")
182
- print(book_result.kind, book_result.output)
183
- print(document_result.kind, document_result.output)
190
+ inventory = list_bundle_variants("story.ssmd-bundle")
191
+ single = export_bundle("story.ssmd-bundle", variant=variant, format="mp3")
192
+ batch = export_bundle_all("story.ssmd-bundle", format="mp3")
193
+ print(inventory.to_dict(), single.output, batch.outputs)
184
194
  ```
185
195
 
186
- `BundleExportResult.value` contains the specialized book or document result; `BundleExportResult.output` gives the final artifact path. The facade is available from both `audioexport` and `audioexport.api`.
196
+ `BundleExportResult.value` contains the specialized book or document result, and its `output` property gives the artifact path. It remains the result type for explicit single-variant export. `BundleExportBatchResult` contains the ordered per-variant results and outputs. These APIs and `BundleVariantInfo`/`BundleVariantsResult` are available from both `audioexport` and `audioexport.api`.
187
197
  OGG defaults to Vorbis quality-based encoding (`-qscale:a 4`) to avoid low-rate/mono bitrate incompatibilities. `--bitrate` accepts values such as `192k`, `192000`, or `1M`; whitespace is trimmed. A bare integer is interpreted as **bits per second**, so use `192k` (or `192000`), not `192`, for a nominal 192 kb/s rate. WAV/FLAC don't accept bitrate. WAV output is always PCM16 conversion, not a byte-for-byte passthrough; lossy codecs and even PCM output can vary with FFmpeg versions, which are included in the export identity.
188
198
 
189
199
  ## Python API
@@ -217,7 +227,7 @@ print(probe("exported/book.m4b")["chapters"])
217
227
 
218
228
  You may import the same public functions from `audioexport.api` as well as from `audioexport`.
219
229
 
220
- The public errors are `AudioExportError` and subclasses `InvalidExportError`, `ToolNotFoundError`, `EncodingError`, and `VerificationError`, with a stable machine-readable `.code` value. `encode()` returns an immutable `ExportResult`; `build_audiobook()` returns an `AudiobookResult`; `export_bundle()` provides one manifest-dispatched entry point for verified book and document bundles and returns `BundleExportResult`. The facade and result types are available from both `audioexport` and `audioexport.api`.
230
+ The public errors are `AudioExportError` and subclasses `InvalidExportError`, `ToolNotFoundError`, `EncodingError`, and `VerificationError`, with a stable machine-readable `.code` value. `encode()` returns an immutable `ExportResult`; `build_audiobook()` returns an `AudiobookResult`; `export_bundle()` provides one manifest-dispatched entry point for verified book and document bundles and returns `BundleExportResult`; `export_bundle_all()` batches all variants, and `list_bundle_variants()` provides read-only inventory. The batch and inventory result types are available from both `audioexport` and `audioexport.api`.
221
231
 
222
232
  ## Export profile
223
233
 
@@ -131,26 +131,36 @@ audioexport bundle export "My Book.ssmd-bundle" --variant "$VARIANT" \
131
131
 
132
132
  audioexport bundle export story.ssmd-bundle --variant "$VARIANT" \
133
133
  --output story.m4a --title "Story" --artist "An Author"
134
+
135
+ # Inspect the AudioCompose export-stage inventory (BUNDLE defaults to .).
136
+ audioexport bundle variants
137
+
138
+ # Export every discovered variant in deterministic name order.
139
+ audioexport bundle export story.ssmd-bundle --format mp3
140
+ # A batch can relocate its variant-separated outputs.
141
+ audioexport bundle export story.ssmd-bundle --format mp3 --output-root /srv/audio-exports
134
142
  ```
135
143
 
136
- AudioExport verifies the selected AudioCompose report, output paths, WAV checksums, and audio geometry before exporting. For a book, `--chapters` selects manifest chapters (`all` by default), output defaults to M4B, `--format` accepts only `m4b`, and audiobook profiles remain supported. For a document, only `--chapters all` is valid; `--format` or the output filename extension selects the final format, and `--timeline` supplies an optional generic chapter timeline. `--author` and `--artist` are aliases for the appropriate author/artist metadata field. Kind-specific options that have no meaning for the selected manifest are rejected rather than ignored.
144
+ `audioexport bundle variants [BUNDLE]` is a read-only AudioExport-stage inventory: it inspects immediate variant directories and compose reports only, does not hash WAVs or create output directories, and reports malformed candidates without hiding other variants. Use `--json` for the `audioexport-bundle-variants` envelope or `--audiocompose-root PATH` to select another stage root. Full render/compose pipeline status remains AudioCompose's concern.
145
+
146
+ AudioExport verifies selected reports, report-relative output paths, WAV checksums, and audio geometry before encoding. Current AudioCompose document reports store geometry in `[output_geometry]`; AudioExport also accepts legacy flat reports, but rejects contradictory mixed layouts. Books use verified M4B assembly, support chapter selection and audiobook profiles, and accept only `--format m4b` (M4B is the default). Documents support only `--chapters all`; explicit single-variant export accepts `--format` or infers the format from `--output`, while all-variant document export requires `--format`. `--timeline` supplies an optional generic document chapter timeline.
137
147
 
138
- Default output names strip only `.ssmd-bundle`: books use `audioexport/<variant>/<name>.m4b`, while documents use `audioexport/<variant>/<name>.<format>`. `--output-root` relocates these defaults; `--output` selects an explicit destination. Book metadata precedence is manifest, then audiobook profile, then explicit CLI/API values. BCP 47 language tags such as `en-US` are normalized to ISO 639-2/T track codes such as `eng` for M4A/M4B.
148
+ Single-variant default names strip only `.ssmd-bundle`: books use `audioexport/<variant>/<name>.m4b`, and documents use `audioexport/<variant>/<name>.<format>`. When `--variant` is omitted, export processes every immediate AudioCompose variant in deterministic name order and keeps outputs separated as `audioexport/<variant>/<name>.<format>` (books use `.m4b`). `--output-root` relocates single-variant defaults or, in batch mode, the whole variant-separated tree. A single `--output FILE` requires an explicit `--variant`; it is rejected in batch mode before encoding. Batch export is fail-fast and does not remove outputs already completed before a later variant fails. `--author` and `--artist` are aliases for the appropriate metadata field; options unsupported for the detected bundle kind fail explicitly.
139
149
 
140
- The JSON result uses one versioned envelope with `format: "audioexport-bundle-result"`, `version: 1`, `kind`, and a kind-specific `result` object. Human output begins with `AudioExport bundle export` and reports the selected kind, workspace, variant, and output.
150
+ The explicit single-variant JSON result remains `audioexport-bundle-result` with version 1 and a `result` object. Human single-variant output begins with `AudioExport bundle export` and reports kind, workspace, variant, output, and the selected chapter count or composed document item. All-variant export uses `audioexport-bundle-batch-result`; human output lists each variant and its output. Book metadata precedence is manifest, then audiobook profile, then explicit CLI/API values. BCP 47 language tags such as `en-US` normalize to ISO 639-2/T track codes such as `eng` for M4A/M4B.
141
151
 
142
- Python callers use the same manifest-dispatched facade:
152
+ Python callers can inspect and export variants through the same public API:
143
153
 
144
154
  ```python
145
- from audioexport import export_bundle
155
+ from audioexport import export_bundle, export_bundle_all, list_bundle_variants
146
156
 
147
- book_result = export_bundle("My Book.ssmd-bundle", variant=variant, chapters="5,7")
148
- document_result = export_bundle("story.ssmd-bundle", variant=variant, format="mp3")
149
- print(book_result.kind, book_result.output)
150
- print(document_result.kind, document_result.output)
157
+ inventory = list_bundle_variants("story.ssmd-bundle")
158
+ single = export_bundle("story.ssmd-bundle", variant=variant, format="mp3")
159
+ batch = export_bundle_all("story.ssmd-bundle", format="mp3")
160
+ print(inventory.to_dict(), single.output, batch.outputs)
151
161
  ```
152
162
 
153
- `BundleExportResult.value` contains the specialized book or document result; `BundleExportResult.output` gives the final artifact path. The facade is available from both `audioexport` and `audioexport.api`.
163
+ `BundleExportResult.value` contains the specialized book or document result, and its `output` property gives the artifact path. It remains the result type for explicit single-variant export. `BundleExportBatchResult` contains the ordered per-variant results and outputs. These APIs and `BundleVariantInfo`/`BundleVariantsResult` are available from both `audioexport` and `audioexport.api`.
154
164
  OGG defaults to Vorbis quality-based encoding (`-qscale:a 4`) to avoid low-rate/mono bitrate incompatibilities. `--bitrate` accepts values such as `192k`, `192000`, or `1M`; whitespace is trimmed. A bare integer is interpreted as **bits per second**, so use `192k` (or `192000`), not `192`, for a nominal 192 kb/s rate. WAV/FLAC don't accept bitrate. WAV output is always PCM16 conversion, not a byte-for-byte passthrough; lossy codecs and even PCM output can vary with FFmpeg versions, which are included in the export identity.
155
165
 
156
166
  ## Python API
@@ -184,7 +194,7 @@ print(probe("exported/book.m4b")["chapters"])
184
194
 
185
195
  You may import the same public functions from `audioexport.api` as well as from `audioexport`.
186
196
 
187
- The public errors are `AudioExportError` and subclasses `InvalidExportError`, `ToolNotFoundError`, `EncodingError`, and `VerificationError`, with a stable machine-readable `.code` value. `encode()` returns an immutable `ExportResult`; `build_audiobook()` returns an `AudiobookResult`; `export_bundle()` provides one manifest-dispatched entry point for verified book and document bundles and returns `BundleExportResult`. The facade and result types are available from both `audioexport` and `audioexport.api`.
197
+ The public errors are `AudioExportError` and subclasses `InvalidExportError`, `ToolNotFoundError`, `EncodingError`, and `VerificationError`, with a stable machine-readable `.code` value. `encode()` returns an immutable `ExportResult`; `build_audiobook()` returns an `AudiobookResult`; `export_bundle()` provides one manifest-dispatched entry point for verified book and document bundles and returns `BundleExportResult`; `export_bundle_all()` batches all variants, and `list_bundle_variants()` provides read-only inventory. The batch and inventory result types are available from both `audioexport` and `audioexport.api`.
188
198
 
189
199
  ## Export profile
190
200
 
@@ -13,7 +13,8 @@ from .book import (
13
13
  export_book,
14
14
  resolve_book_export,
15
15
  )
16
- from .bundle import BundleExportResult, export_bundle
16
+ from .bundle import BundleExportBatchResult, BundleExportResult, export_bundle, export_bundle_all
17
+ from .bundle_inspection import BundleExportInspection, inspect_bundle_export
17
18
  from .chapters import Chapter, load_chapters
18
19
  from .document import (
19
20
  ComposedDocument,
@@ -35,6 +36,7 @@ from .metadata import AudiobookMetadata
35
36
  from .pipeline import ExportResult, encode, run_profile
36
37
  from .preflight import preflight_profile
37
38
  from .profile import ExportProfile, OutputSpec, ResolvedOutput, load_profile, resolve_output
39
+ from .variants import BundleVariantInfo, BundleVariantsResult, list_bundle_variants
38
40
 
39
41
  try:
40
42
  __version__ = version("audioexport")
@@ -51,7 +53,11 @@ __all__ = [
51
53
  "AudiobookTrack",
52
54
  "BookExportContext",
53
55
  "BookExportResult",
56
+ "BundleExportBatchResult",
57
+ "BundleExportInspection",
54
58
  "BundleExportResult",
59
+ "BundleVariantInfo",
60
+ "BundleVariantsResult",
55
61
  "Chapter",
56
62
  "ComposedChapter",
57
63
  "ComposedDocument",
@@ -71,7 +77,10 @@ __all__ = [
71
77
  "encode",
72
78
  "export_book",
73
79
  "export_bundle",
80
+ "export_bundle_all",
74
81
  "export_document",
82
+ "inspect_bundle_export",
83
+ "list_bundle_variants",
75
84
  "load_audiobook_profile",
76
85
  "load_chapters",
77
86
  "load_profile",
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.4'
22
- __version_tuple__ = version_tuple = (0, 1, 4)
21
+ __version__ = version = '0.1.7'
22
+ __version_tuple__ = version_tuple = (0, 1, 7)
23
23
 
24
- __commit_id__ = commit_id = 'gb9fbcb9c0'
24
+ __commit_id__ = commit_id = 'g0852cf5d3'
@@ -11,7 +11,8 @@ from .book import (
11
11
  export_book,
12
12
  resolve_book_export,
13
13
  )
14
- from .bundle import BundleExportResult, export_bundle
14
+ from .bundle import BundleExportBatchResult, BundleExportResult, export_bundle, export_bundle_all
15
+ from .bundle_inspection import BundleExportInspection, inspect_bundle_export
15
16
  from .chapters import Chapter, load_chapters
16
17
  from .document import (
17
18
  ComposedDocument,
@@ -33,6 +34,7 @@ from .metadata import AudiobookMetadata
33
34
  from .pipeline import ExportResult, encode, run_profile
34
35
  from .preflight import preflight_profile
35
36
  from .profile import ExportProfile, OutputSpec, ResolvedOutput, load_profile, resolve_output
37
+ from .variants import BundleVariantInfo, BundleVariantsResult, list_bundle_variants
36
38
 
37
39
  __all__ = [
38
40
  "FORMATS",
@@ -44,7 +46,11 @@ __all__ = [
44
46
  "AudiobookTrack",
45
47
  "BookExportContext",
46
48
  "BookExportResult",
49
+ "BundleExportBatchResult",
50
+ "BundleExportInspection",
47
51
  "BundleExportResult",
52
+ "BundleVariantInfo",
53
+ "BundleVariantsResult",
48
54
  "Chapter",
49
55
  "ComposedChapter",
50
56
  "ComposedDocument",
@@ -63,7 +69,10 @@ __all__ = [
63
69
  "encode",
64
70
  "export_book",
65
71
  "export_bundle",
72
+ "export_bundle_all",
66
73
  "export_document",
74
+ "inspect_bundle_export",
75
+ "list_bundle_variants",
67
76
  "load_audiobook_profile",
68
77
  "load_chapters",
69
78
  "load_profile",
@@ -7,12 +7,14 @@ from dataclasses import dataclass
7
7
  from pathlib import Path
8
8
  from typing import Any
9
9
 
10
+ from .audiobook_profile import load_audiobook_profile
10
11
  from .book import BookExportResult, export_book
11
12
  from .bundle_contract import BundleKind, load_bundle_identity
12
13
  from .document import DocumentExportResult, export_document
13
14
  from .errors import InvalidExportError
14
15
  from .formats import normalize_format
15
16
  from .metadata import AudiobookMetadata
17
+ from .variants import BundleVariantsResult, list_bundle_variants
16
18
 
17
19
 
18
20
  @dataclass(frozen=True, slots=True)
@@ -60,6 +62,29 @@ class BundleExportResult:
60
62
  }
61
63
 
62
64
 
65
+ @dataclass(frozen=True, slots=True)
66
+ class BundleExportBatchResult:
67
+ """Results from exporting every discovered variant of one bundle."""
68
+
69
+ kind: BundleKind
70
+ workspace: Path
71
+ results: tuple[BundleExportResult, ...]
72
+
73
+ @property
74
+ def outputs(self) -> tuple[Path, ...]:
75
+ return tuple(result.output for result in self.results)
76
+
77
+ def to_dict(self) -> dict[str, Any]:
78
+ return {
79
+ "format": "audioexport-bundle-batch-result",
80
+ "version": 1,
81
+ "kind": self.kind,
82
+ "workspace": str(self.workspace),
83
+ "count": len(self.results),
84
+ "results": [result.to_dict()["result"] for result in self.results],
85
+ }
86
+
87
+
63
88
  def _option_error(message: str) -> InvalidExportError:
64
89
  return InvalidExportError(message, code="audioexport.bundle_option_invalid")
65
90
 
@@ -91,6 +116,49 @@ def _metadata_for_kind(
91
116
  return values
92
117
 
93
118
 
119
+ def _validate_options_for_kind(
120
+ *,
121
+ kind: BundleKind,
122
+ chapters: str,
123
+ output: str | Path | None,
124
+ format: str | None,
125
+ profile: str | Path | None,
126
+ sample_rate: int | None,
127
+ channels: int | None,
128
+ timeline: str | Path | None,
129
+ metadata: AudiobookMetadata | Mapping[str, str] | None,
130
+ batch: bool,
131
+ ) -> AudiobookMetadata | dict[str, str] | None:
132
+ """Validate shared kind restrictions before dispatch or batch iteration."""
133
+ if batch and output is not None:
134
+ raise _option_error(
135
+ "--output requires --variant; use --output-root when exporting all variants"
136
+ )
137
+
138
+ normalized_metadata = _metadata_for_kind(metadata, kind=kind)
139
+ if kind == "book":
140
+ if format is not None and normalize_format(format) != "m4b":
141
+ raise _option_error("book bundles support only --format m4b")
142
+ if timeline is not None:
143
+ raise _option_error("--timeline is not supported for book bundles")
144
+ return normalized_metadata
145
+
146
+ if chapters != "all":
147
+ raise _option_error("document bundles support only --chapters all")
148
+ if profile is not None:
149
+ raise _option_error("--profile is supported only for book bundles")
150
+ if sample_rate is not None or channels is not None:
151
+ raise _option_error("--sample-rate and --channels are supported only for book bundles")
152
+ if batch:
153
+ if format is None:
154
+ raise InvalidExportError(
155
+ "document export requires a format when exporting all variants",
156
+ code="audioexport.document_format_required",
157
+ )
158
+ normalize_format(format)
159
+ return normalized_metadata
160
+
161
+
94
162
  def export_bundle(
95
163
  bundle: str | Path,
96
164
  *,
@@ -113,14 +181,21 @@ def export_bundle(
113
181
  ) -> BundleExportResult:
114
182
  """Export one canonical bundle, selecting the specialized path from its manifest."""
115
183
  identity = load_bundle_identity(bundle)
116
- normalized_metadata = _metadata_for_kind(metadata, kind=identity.kind)
184
+ normalized_metadata = _validate_options_for_kind(
185
+ kind=identity.kind,
186
+ chapters=chapters,
187
+ output=output,
188
+ format=format,
189
+ profile=profile,
190
+ sample_rate=sample_rate,
191
+ channels=channels,
192
+ timeline=timeline,
193
+ metadata=metadata,
194
+ batch=False,
195
+ )
117
196
 
118
197
  value: BookExportResult | DocumentExportResult
119
198
  if identity.kind == "book":
120
- if format is not None and normalize_format(format) != "m4b":
121
- raise _option_error("book bundles support only --format m4b")
122
- if timeline is not None:
123
- raise _option_error("--timeline is not supported for book bundles")
124
199
  value = export_book(
125
200
  identity.root,
126
201
  variant=variant,
@@ -140,12 +215,6 @@ def export_bundle(
140
215
  )
141
216
  return BundleExportResult(kind="book", value=value)
142
217
 
143
- if chapters != "all":
144
- raise _option_error("document bundles support only --chapters all")
145
- if profile is not None:
146
- raise _option_error("--profile is supported only for book bundles")
147
- if sample_rate is not None or channels is not None:
148
- raise _option_error("--sample-rate and --channels are supported only for book bundles")
149
218
  if isinstance(normalized_metadata, AudiobookMetadata):
150
219
  raise _option_error("document bundle metadata must be a string mapping")
151
220
  value = export_document(
@@ -164,3 +233,90 @@ def export_bundle(
164
233
  ffprobe=ffprobe,
165
234
  )
166
235
  return BundleExportResult(kind="document", value=value)
236
+
237
+
238
+ def export_bundle_all(
239
+ bundle: str | Path,
240
+ *,
241
+ chapters: str = "all",
242
+ output: str | Path | None = None,
243
+ format: str | None = None,
244
+ profile: str | Path | None = None,
245
+ bitrate: str | int | None = None,
246
+ sample_rate: int | None = None,
247
+ channels: int | None = None,
248
+ metadata: AudiobookMetadata | Mapping[str, str] | None = None,
249
+ cover: str | Path | None = None,
250
+ timeline: str | Path | None = None,
251
+ audiocompose_root: str | Path | None = None,
252
+ output_root: str | Path | None = None,
253
+ force: bool = False,
254
+ ffmpeg: str | Path | None = None,
255
+ ffprobe: str | Path | None = None,
256
+ ) -> BundleExportBatchResult:
257
+ """Export every discovered AudioCompose variant in deterministic name order."""
258
+ identity = load_bundle_identity(bundle)
259
+ _validate_options_for_kind(
260
+ kind=identity.kind,
261
+ chapters=chapters,
262
+ output=output,
263
+ format=format,
264
+ profile=profile,
265
+ sample_rate=sample_rate,
266
+ channels=channels,
267
+ timeline=timeline,
268
+ metadata=metadata,
269
+ batch=True,
270
+ )
271
+
272
+ profile_output_name: str | None = None
273
+ if identity.kind == "book" and profile is not None:
274
+ profile_output_name = load_audiobook_profile(profile).output.name
275
+ discovered: BundleVariantsResult = list_bundle_variants(
276
+ identity.root,
277
+ audiocompose_root=audiocompose_root,
278
+ )
279
+ if not discovered.variants:
280
+ raise InvalidExportError(
281
+ "no AudioCompose variants were found",
282
+ code="audioexport.bundle_no_variants",
283
+ )
284
+
285
+ results: list[BundleExportResult] = []
286
+ for item in sorted(discovered.variants, key=lambda variant_info: variant_info.name):
287
+ batch_output = output
288
+ if profile_output_name is not None:
289
+ base_output = (
290
+ Path(output_root).expanduser()
291
+ if output_root is not None
292
+ else identity.root / "audioexport"
293
+ )
294
+ if not base_output.is_absolute():
295
+ base_output = Path.cwd() / base_output
296
+ batch_output = base_output.absolute() / item.name / profile_output_name
297
+ results.append(
298
+ export_bundle(
299
+ identity.root,
300
+ variant=item.name,
301
+ chapters=chapters,
302
+ output=batch_output,
303
+ format=format,
304
+ profile=profile,
305
+ bitrate=bitrate,
306
+ sample_rate=sample_rate,
307
+ channels=channels,
308
+ metadata=metadata,
309
+ cover=cover,
310
+ timeline=timeline,
311
+ audiocompose_root=audiocompose_root,
312
+ output_root=output_root,
313
+ force=force,
314
+ ffmpeg=ffmpeg,
315
+ ffprobe=ffprobe,
316
+ )
317
+ )
318
+ return BundleExportBatchResult(
319
+ kind=identity.kind,
320
+ workspace=identity.root,
321
+ results=tuple(results),
322
+ )