audioexport 0.1.0__tar.gz → 0.1.2__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.2/PKG-INFO +350 -0
  2. audioexport-0.1.2/README.md +317 -0
  3. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/__init__.py +27 -1
  4. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/_version.py +3 -3
  5. audioexport-0.1.2/audioexport/api.py +62 -0
  6. audioexport-0.1.2/audioexport/audiobook.py +829 -0
  7. audioexport-0.1.2/audioexport/audiobook_profile.py +271 -0
  8. audioexport-0.1.2/audioexport/book.py +780 -0
  9. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/chapters.py +66 -0
  10. audioexport-0.1.2/audioexport/cli.py +409 -0
  11. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/fftools.py +34 -12
  12. audioexport-0.1.2/audioexport/inputs.py +151 -0
  13. audioexport-0.1.2/audioexport/metadata.py +126 -0
  14. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/pipeline.py +27 -112
  15. audioexport-0.1.2/audioexport/preflight.py +77 -0
  16. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/profile.py +83 -44
  17. audioexport-0.1.2/audioexport/validation.py +63 -0
  18. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport.egg-info/SOURCES.txt +16 -0
  19. audioexport-0.1.2/docs/Makefile +32 -0
  20. audioexport-0.1.2/docs/changelog.md +44 -0
  21. audioexport-0.1.2/docs/conf.py +100 -0
  22. audioexport-0.1.2/docs/index.md +187 -0
  23. audioexport-0.1.2/docs/make.bat +113 -0
  24. audioexport-0.1.2/docs/make.py +89 -0
  25. {audioexport-0.1.0 → audioexport-0.1.2}/docs/readio-migration.md +36 -12
  26. audioexport-0.1.2/docs/requirements.txt +5 -0
  27. audioexport-0.1.2/examples/audiobook.toml +18 -0
  28. audioexport-0.1.2/examples/chapters.txt +1 -0
  29. audioexport-0.1.2/examples/create_ssmdbook_demo.py +83 -0
  30. {audioexport-0.1.0 → audioexport-0.1.2}/pyproject.toml +6 -2
  31. audioexport-0.1.0/PKG-INFO +0 -226
  32. audioexport-0.1.0/README.md +0 -196
  33. audioexport-0.1.0/audioexport/api.py +0 -36
  34. audioexport-0.1.0/audioexport/cli.py +0 -141
  35. audioexport-0.1.0/docs/changelog.md +0 -15
  36. {audioexport-0.1.0 → audioexport-0.1.2}/LICENSE +0 -0
  37. {audioexport-0.1.0 → audioexport-0.1.2}/MANIFEST.in +0 -0
  38. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/__main__.py +0 -0
  39. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/errors.py +0 -0
  40. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/formats.py +0 -0
  41. {audioexport-0.1.0 → audioexport-0.1.2}/audioexport/py.typed +0 -0
  42. {audioexport-0.1.0 → audioexport-0.1.2}/examples/chapters.json +0 -0
  43. {audioexport-0.1.0 → audioexport-0.1.2}/examples/create_demo_wav.py +0 -0
  44. {audioexport-0.1.0 → audioexport-0.1.2}/examples/export.toml +0 -0
  45. {audioexport-0.1.0 → audioexport-0.1.2}/setup.cfg +0 -0
@@ -0,0 +1,350 @@
1
+ Metadata-Version: 2.4
2
+ Name: audioexport
3
+ Version: 0.1.2
4
+ Summary: Standalone FFmpeg audio encoder with metadata, chapter, cover and reproducible export manifests
5
+ Author: audioexport contributors
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/buchwandler/audioexport
8
+ Project-URL: Repository, https://github.com/buchwandler/audioexport
9
+ Keywords: ffmpeg,audio,mp3,m4a,flac,m4b,opus
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Multimedia :: Sound/Audio
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: tomli>=2; python_version < "3.11"
25
+ Requires-Dist: langcodes>=3.3
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8; extra == "dev"
28
+ Requires-Dist: ruff>=0.6; extra == "dev"
29
+ Requires-Dist: build>=1; extra == "dev"
30
+ Requires-Dist: mypy>=1.10; extra == "dev"
31
+ Requires-Dist: twine>=5; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # audioexport
35
+
36
+ An independent Python library and command-line interface for exporting audio through **FFmpeg**. This is the first extraction step from Readio's output/encoding subsystem, but **audioexport does not import or install Readio, AudioCompose, UtterPlan, or synthesis engines**.
37
+
38
+ Supported formats: WAV (PCM16), FLAC, MP3, M4A (AAC), M4B (AAC audiobook), OGG (Vorbis), and Opus. Handles tagged metadata, embedded JPEG/PNG cover art for MP3/M4A/M4B, JSON chapter timelines for M4A/M4B, standalone ordered multi-track audiobook assembly, FFprobe verification, artifact identities and safe reuse.
39
+
40
+ ## Install
41
+
42
+ Requirements: **Python 3.10+** and external **FFmpeg + FFprobe** executables available on `PATH`. Install a build containing the encoders you need: `pcm_s16le`, `flac`, `libmp3lame`, `aac`, `libvorbis` (or native `vorbis`), and/or `libopus`.
43
+
44
+ Platform notes:
45
+
46
+ - Ubuntu/Debian: `sudo apt-get install ffmpeg`.
47
+ - macOS: `brew install ffmpeg`.
48
+ - Windows: install a current FFmpeg build and add its `bin` directory to `PATH`.
49
+
50
+ ```bash
51
+ python -m pip install audioexport
52
+ python -m audioexport --version
53
+ # Or install from a checkout:
54
+ python -m pip install -e .
55
+ audioexport doctor --json
56
+ ```
57
+
58
+ `doctor` reports package/tool versions and encoder availability per format. Its existing `ready` key means FFmpeg and FFprobe are present; `all_formats_ready` and each `formats` entry report encoder capability and a reason when unavailable. The additive `features` entries report `m4b_encode`, `m4b_audiobook`, and `m4b_stream_copy` availability; missing optional capabilities do not make `ready` false. `run_profile()` checks the encoders selected by the profile before writing outputs.
59
+
60
+ Base Python dependencies are `langcodes>=3.3` on all supported versions and `tomli>=2` on Python 3.10. FFmpeg/FFprobe are external executables; AudioExport does not install or import Readio, AudioCompose, UtterPlan, VoiceRender, soundfile, or NumPy.
61
+
62
+ ## CLI quickstart
63
+
64
+ ```bash
65
+ # Creates demo.wav in the current directory (or use your own audio file):
66
+ python examples/create_demo_wav.py
67
+ # Any FFmpeg-decodable audio source is accepted.
68
+ audioexport encode demo.wav --format mp3 -o output.mp3 --bitrate 192k
69
+ audioexport encode demo.wav --format flac -o output.flac
70
+ audioexport encode demo.wav --format m4a -o output.m4a --title "My recording" --artist "An author"
71
+ audioexport encode demo.wav --format ogg -o output.ogg
72
+ audioexport encode demo.wav --format opus -o output.opus
73
+
74
+ audioexport inspect output.mp3 --json
75
+ audioexport doctor --json
76
+
77
+ # For M4B chapters; use your own cover.jpg or omit --cover:
78
+ audioexport encode demo.wav --format m4b -o mybook.m4b \
79
+ --title "My Book" --artist "An Author" \
80
+ --timeline examples/chapters.json --cover cover.jpg --bitrate 192k
81
+
82
+ # Export multiple formats at once using an editable profile:
83
+ audioexport run demo.wav --profile examples/export.toml --out-dir exported/ --json
84
+ ```
85
+
86
+ ## Standalone audiobook assembly
87
+
88
+ `encode` converts one already-prepared audio source to a format; it does not merge a collection of source tracks. Use the separate `audiobook` workflow when starting from ordered tracks or a directory:
89
+
90
+ ```bash
91
+ # Direct directory input: immediate audio files only, natural filename ordering.
92
+ audioexport audiobook book-tracks/ -o book.m4b \
93
+ --title "My Book" --author "Author Name" --album "My Book" --cover cover.jpg
94
+
95
+ # Or provide an explicit order; source title tags become chapter titles when present.
96
+ audioexport audiobook 01-intro.mp3 02-chapter-one.flac 03-chapter-two.wav \
97
+ -o book.m4b --bitrate 96k --language eng
98
+
99
+ # Explicit chapter starts from chapters.txt or the existing JSON/Readio timeline format.
100
+ audioexport audiobook book-tracks/ -o book.m4b --chapters-file chapters.txt
101
+ ```
102
+
103
+ Each source track becomes one chapter by default. Chapter starts use normalized segment durations; titles prefer an explicit `AudiobookTrack` title, then the source title tag, then the filename stem. Tracks are serially normalized to common AAC settings, concatenated with stream copy, and final-muxed without a second audio encode. Sample rate and channel count default from the first track; only mono/stereo targets are supported. The audiobook command defaults to the FFmpeg-compatible `media_type=2` tag. `--author` (or its `--artist` alias) writes the player-compatible `artist` tag; `--album-artist`, `--writer`, and `--long-description` map to `album_artist`, `composer`, and `synopsis`. `--metadata KEY=VALUE` adds or overrides an FFmpeg tag.
104
+
105
+ For one directory input, the CLI discovers `cover.jpg`, then `cover.jpeg`, then `cover.png`, plus `chapters.txt`, when explicit resources were not supplied. Discovered names appear in command output and the audiobook manifest. Use `--no-auto-sidecars` to disable this behavior. The library API does not scan sibling files unless called with `discover_sidecars=True`.
106
+
107
+ Python callers can use the same assembly workflow without invoking the CLI:
108
+
109
+ ```python
110
+ from audioexport import AudiobookMetadata, AudiobookTrack, build_audiobook
111
+
112
+ result = build_audiobook(
113
+ [
114
+ AudiobookTrack("01-intro.mp3", title="Introduction"),
115
+ "02-chapter-one.flac",
116
+ ],
117
+ "book.m4b",
118
+ metadata=AudiobookMetadata(title="My Book", author="Author Name"),
119
+ cover="cover.jpg",
120
+ )
121
+ print(result.track_count, result.chapter_count, result.reused)
122
+ ```
123
+
124
+ A separate `audioexport.audiobook.v1` profile describes ordered inputs and explicit resources. All relative paths resolve from the profile file; no glob or implicit directory expansion is used in TOML. `chapters_file` is a top-level resource path, while `[chapters]` selects its mode:
125
+
126
+ ```toml
127
+ schema = "audioexport.audiobook.v1"
128
+ output = "My Book.m4b"
129
+ inputs = ["01 Intro.mp3", "02 Chapter One.flac"]
130
+ cover = "cover.jpg"
131
+ chapters_file = "chapters.txt"
132
+
133
+ [audio]
134
+ bitrate = "96k"
135
+ sample_rate = 44100
136
+ channels = 2
137
+
138
+ [metadata]
139
+ title = "My Book"
140
+ author = "Author Name"
141
+ album = "My Book"
142
+ genre = "Audiobook"
143
+ language = "eng"
144
+
145
+ [chapters]
146
+ mode = "explicit" # tracks | explicit | none
147
+ # title_source = "tag" # tag (falls back to filename) | filename
148
+ ```
149
+
150
+ Run it with `audioexport audiobook --profile audiobook.toml`. CLI `--output`, metadata, audio settings, cover, and `--chapters-file` override profile values; the TOML input order remains authoritative. `mode = "tracks"` (the default) creates one chapter per track; `mode = "explicit"` requires `chapters_file`; `mode = "none"` disables chapters. Existing `encode(..., format="m4b", timeline=...)` remains the right choice for Readio's prepared master and project timeline.
151
+
152
+ ## Native `.ssmdbook` pipeline
153
+
154
+ Use `export-book` when AudioCompose has already written a variant in a standard `.ssmdbook` workspace. It is the native book-workspace adapter; `audioexport audiobook` remains the generic standalone track/directory assembler, and `encode` remains for one prepared source with a timeline.
155
+
156
+ ```bash
157
+ audiocompose compose-book . --variant "$VARIANT"
158
+ audioexport export-book . --variant "$VARIANT"
159
+ ```
160
+
161
+ `export-book` reads `manifest.json` and `audiocompose/<variant>/compose-report.toml`, accepts the producer's singular `[[item]]` tables (and legacy `[[items]]` tables), validates selected entries and WAV checksums/geometry with bare or `sha256:`-prefixed digests, then delegates final M4B assembly and verification to AudioExport. It does not run AudioCompose, repair or synthesize chapters, or use Readio-specific state. Chapter selection accepts `all` (the default), a source number, an inclusive range, or comma-separated source numbers/ranges, for example `--chapters 5,7,11-13`. Selection follows manifest chapter order.
162
+
163
+ For M4A/M4B, language metadata is written on the audio track using ISO 639-2/T. BCP 47 tags such as `en-US` are normalized to their three-letter track code (`eng`).
164
+
165
+ By default, output is written to `audioexport/<variant>/<workspace-name-without-.ssmdbook>.m4b` in the workspace. `--output` may override this; when an audiobook profile is supplied, its output is used unless `--output` is specified.
166
+
167
+ Metadata precedence is **manifest metadata < audiobook profile metadata < explicit CLI/API metadata**. Audio settings use **AudioExport defaults < audiobook profile < explicit CLI/API flags**. Thus explicit metadata fields replace matching profile/manifest values, profile values replace manifest defaults, and unset values continue to fall through. Explicit cover and audio flags override corresponding profile settings. The profile's inputs and chapter configuration do not replace the variant chapters selected from the compose report.
168
+
169
+ If `--output` is not set for `encode`, the output filename defaults to the input stem and chosen format. To avoid destroying the input, encoding input and output to the same real path is always rejected.
170
+
171
+ Without `--force`, an untracked, symlink, or manually modified output is never overwritten. A previously generated output is updated when its sidecar and checksum still match; re-running identical options returns `reused: true` without re-encoding. `--force` authorizes replacing an unknown or modified regular file, but symlink outputs and sidecars are always rejected. Writes are staged with their manifest; a failed manifest commit restores the last good pair when possible, and an incomplete recovery reports the preserved backup path. A per-target process lock rejects concurrent writers with `audioexport.output_busy`; remove a stale lock only after confirming no writer is active.
172
+
173
+ 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.
174
+
175
+ ## Python API
176
+
177
+ ```python
178
+ import audioexport
179
+ from pathlib import Path
180
+ from audioexport import Chapter, encode, run_profile, doctor, probe
181
+
182
+ result = encode(
183
+ Path("master.wav"),
184
+ Path("exported/book.m4b"),
185
+ format="m4b", bitrate="192k",
186
+ metadata={"title": "My Book", "artist": "Author Name"},
187
+ cover=Path("cover.jpg"),
188
+ chapters=[
189
+ Chapter("Introduction", start_ms=0),
190
+ Chapter("Chapter One", start_ms=60_000),
191
+ ],
192
+ )
193
+ print(result.to_dict())
194
+
195
+ for other in run_profile("master.wav", "examples/export.toml", "exported"):
196
+ print(other.path, other.reused)
197
+
198
+ print(audioexport.__version__)
199
+ diagnostics = doctor()
200
+ print(diagnostics["ready"], diagnostics["all_formats_ready"])
201
+ print(probe("exported/book.m4b")["chapters"])
202
+ ```
203
+
204
+ You may import the same public functions from `audioexport.api` as well as from `audioexport`.
205
+
206
+ 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_book()` and `resolve_book_export()` provide verified composed-book export through `BookExportContext`, `BookExportResult`, and `ComposedChapter`. These symbols, like the other public functions, are available from both `audioexport` and `audioexport.api`.
207
+
208
+ ## Export profile
209
+
210
+ [`examples/export.toml`](examples/export.toml) is a complete seven-format example. The `audioexport.profile.v1` schema keeps profile-wide `cover`, `timeline`, and `[metadata]`, while each `[[outputs]]` row may add `use_cover` and `use_chapters`:
211
+
212
+ ```toml
213
+ schema = "audioexport.profile.v1"
214
+ timeline = "chapters.json" # resolved relative to this TOML
215
+ # cover = "cover.jpg" # optional; uncomment and provide the file
216
+
217
+ [metadata]
218
+ title = "My audiobook"
219
+ artist = "An author"
220
+ album = "My audiobook"
221
+
222
+ [[outputs]]
223
+ format = "mp3"
224
+ bitrate = "192k"
225
+ use_cover = false
226
+ use_chapters = false
227
+
228
+ [[outputs]]
229
+ format = "flac"
230
+ use_cover = false
231
+ use_chapters = false
232
+
233
+ [[outputs]]
234
+ format = "m4a"
235
+ bitrate = "192k"
236
+ use_chapters = true
237
+
238
+ [[outputs]]
239
+ format = "m4b"
240
+ bitrate = "192k"
241
+ use_chapters = true
242
+
243
+ [[outputs]]
244
+ format = "ogg"
245
+
246
+ [[outputs]]
247
+ format = "opus"
248
+ bitrate = "96k"
249
+
250
+ [[outputs]]
251
+ format = "wav"
252
+ ```
253
+
254
+ When a selector is omitted (`None`), configured global artwork is attached automatically only to MP3/M4A/M4B, and chapters only to M4A/M4B; unsupported formats skip those global resources. `true` requires the resource and a capable format; `false` suppresses it. For example, to enable artwork, set a valid top-level `cover` and `use_cover = true` on the MP3/M4A/M4B rows. Direct `encode()` calls remain strict: unsupported cover/chapter arguments are errors.
255
+
256
+ `run_profile()` validates every row, resource, capability, and concrete filename collision before the first encode, then returns results in TOML order. Resource paths are relative to the profile file. Explicit `filename` values are literal simple filenames with the matching extension; no template expansion or nested paths are supported. If omitted, each filename uses the input stem. Profile-wide metadata is inherited by every output; there is no per-output metadata override.
257
+
258
+ ## Profile resolution and read-only preflight API
259
+
260
+ The `audioexport.profile.v1` `use_cover` and `use_chapters` fields select profile-wide resources for each output. An omitted selector (`None`) uses a configured resource only when the format supports it; `false` suppresses it; `true` requires both a configured resource and a supporting format. Automatic selection is:
261
+
262
+ | Formats | Cover by default | Chapters by default |
263
+ | -------------------- | ------------------ | ------------------- |
264
+ | WAV, FLAC, OGG, Opus | No | No |
265
+ | MP3 | Yes, if configured | No |
266
+ | M4A, M4B | Yes, if configured | Yes, if configured |
267
+
268
+ `ResolvedOutput` is an immutable description containing normalized format, literal filename, effective normalized bitrate, and selected cover/timeline paths. Explicit filenames are simple filenames with a matching extension and are never interpolated; only an omitted filename uses the supplied source stem. `resolve_output(profile, spec, source_stem)` is a pure operation on an already-loaded profile and does not probe media or read the TOML again.
269
+
270
+ Use `preflight_profile()` when the complete profile must be proven ready before the first output write. It returns the resolved outputs in TOML order, checks the source, metadata, selected resources and chapters, FFmpeg/FFprobe, and only the requested encoders. It does not accept an output directory and creates no outputs, directories, manifests, or temporary files. An unused global resource (for example, a missing cover on a FLAC-only profile) is not validated.
271
+
272
+ ```python
273
+ from pathlib import Path
274
+ from audioexport import encode, load_profile, preflight_profile
275
+
276
+ source = Path("master.wav")
277
+ profile = load_profile("export.toml") # Load once.
278
+ resolved_outputs = preflight_profile(profile, source)
279
+
280
+ for resolved in resolved_outputs:
281
+ target = Path("exports") / resolved.filename
282
+ # A consumer such as Readio performs its target/ownership/overwrite checks here.
283
+ result = encode(
284
+ source,
285
+ target,
286
+ format=resolved.format,
287
+ bitrate=resolved.bitrate,
288
+ metadata=profile.metadata,
289
+ cover=resolved.cover,
290
+ timeline=resolved.timeline,
291
+ )
292
+ ```
293
+
294
+ A consumer may instead call `resolve_output()` for a single selected spec; use full-profile preflight when the operation requires every configured output validated. Preflight is not an ownership authorization: Readio remains responsible for project locks and freshness, destination selection and collision/ownership checks, overwrite authorization, state/index updates, result types, and partial-batch bookkeeping. Call `encode()` only after those consumer-owned checks pass. `ResolvedOutput`, `resolve_output`, and `preflight_profile` are available from both `audioexport` and `audioexport.api`.
295
+
296
+ ## Chapters/timeline
297
+
298
+ Use an independent JSON file such as [`examples/chapters.json`](examples/chapters.json):
299
+
300
+ ```json
301
+ {
302
+ "chapters": [
303
+ { "title": "Intro", "start_ms": 0 },
304
+ { "title": "Part One", "start_ms": 60000 }
305
+ ]
306
+ }
307
+ ```
308
+
309
+ `end_ms` is optional; without it, chapter ends are the next chapter start or the audio duration. Chapter offsets must be strictly increasing and within the audio file. FFmpeg metadata escaping is handled internally. Embedded chapter markers are currently supported for **M4A and M4B**.
310
+
311
+ For an incremental Readio migration, the same `--timeline` API also reads **Readio's `readio.composition-timeline` JSON** with `sample_rate` and chapter `start_sample`. FFmetadata is constructed with its original sample timebase for precision, with generic millisecond chapter fields available for non-Readio workflows.
312
+
313
+ ## Integrity and versioning
314
+
315
+ `encode()` and `run_profile()` outputs have a sibling sidecar, for example `output.mp3.audioexport.json`, holding `audioexport.manifest.v1`, a SHA-256 export identity, source/output and consumed-resource digests, tool versions, normalized options, chapter mapping, and the probed output summary. `build_audiobook()` uses the same sidecar path with `audioexport.audiobook-manifest.v1`, recording ordered tracks, consumed resources, normalized settings, resolved chapter structure, tools, and verification. Generic export identity is **input-content + relevant options/resources + tools**, not output location or TOML formatting. The original sources are never modified.
316
+
317
+ Encoding uses argument-vector subprocess calls (`shell=False`) and stages both audio and manifest before committing. The output lock serializes same-target writers across processes; a competing call receives a typed busy error. If a sidecar commit fails, the previous output/manifest are restored when possible. A recovery failure retains its backup and refuses to treat a mismatched sidecar as current; inspect the reported recovery path before retrying. FFprobe validation checks the single audio stream, codec, duration, requested tags/chapters, and cover codec/disposition.
318
+
319
+ Version is dynamic through `setuptools-scm`: `pyproject.toml` has no static `version` field, and Git tags determine wheel/sdist metadata, `audioexport.__version__`, and CLI output. The project supports Python 3.10+ and retains the flat `audioexport/` layout. Unversioned archives use a development fallback; a fallback build must never be treated as final `0.1.2`.
320
+
321
+ For a release, first commit the code and wait for the complete Linux/Windows/macOS, Python 3.10–3.14 CI matrix, including its Sphinx warning-as-error documentation build. Then create and push the intended version tag (for example `v0.1.2`). Manually dispatch the GitHub Actions workflow named **Release** with that existing tag. It reruns the full supported-Python test matrix, checks the tag against wheel/sdist metadata, inspects package contents, installs the wheel outside the checkout, and checks CLI/API version parity. Publishing uses the configured `PYPI_API_TOKEN` secret after approval in the `pypi` GitHub environment; repository administrators must ensure the token and environment approval rules are configured. Do not publish from an untagged/fallback build.
322
+
323
+ ## Readio migration
324
+
325
+ **Readio is deliberately not patched by this project.** Its existing export commands continue using their current code until an opt-in bridge is tested. A future integration must call AudioExport's Python library API; it must never launch the `audioexport` CLI as a subprocess. See [`docs/readio-migration.md`](docs/readio-migration.md) for the compatibility boundary, integration order, and rollback rules. Readio continues owning project locks, path defaults, staleness, indexed state, and orchestration.
326
+
327
+ ## Tests
328
+
329
+ ```bash
330
+ python -m pip install -e '.[dev]'
331
+ python -m pytest -q
332
+ ruff check .
333
+ ruff format --check .
334
+ mypy audioexport
335
+ python -m compileall -q audioexport
336
+ python -m build
337
+ python -m twine check dist/*
338
+ python scripts/check_package_artifacts.py
339
+ audioexport doctor --json
340
+ ```
341
+
342
+ Tests cover seven real formats in one profile, mixed-format audiobook assembly, 100-track ordering/chapters, audiobook cache invalidation, M4A/M4B chapters and covers, Readio sample timelines, malformed probes, encoder availability, transactional failure recovery, no-clobber behavior, and concurrent writers. Normal CI runs Python 3.10–3.14 on Linux, Windows, and macOS, and checks tests, lint, formatting, typing, build metadata, Twine, package contents, and isolated wheel installation. CI also builds the Sphinx documentation on Linux/Python 3.13 with warnings treated as errors. The manually dispatched **Release** workflow retests the full supported Python range before packaging and API-token publishing. Cross-platform encoder availability depends on the FFmpeg build installed by each runner.
343
+
344
+ ## Limitations (MVP)
345
+
346
+ No audio mastering, TTS, audio mixing, streaming PCM API, FFmpeg filter graphs, network service, or GUI. No automatic conversion of Readio project configs, legacy export cache, or Audiocompose timeline semantics other than chapter starts. Encoder output may vary across FFmpeg versions; the cache key includes tool versions. Chapter verification has a small tolerance for FFmpeg container timebase rounding. Standalone encoding does not modify Readio projects.
347
+
348
+ ## License
349
+
350
+ Apache-2.0. The FFmpeg executable is a separate third-party program distributed under its own terms.