|
| 1 | +# annotationpro-format |
| 2 | + |
| 3 | +Python library for reading and writing **Annotation PRO** `.antx` files (XML annotation format used by the Annotation PRO application). |
| 4 | + |
| 5 | +## About ANTX files |
| 6 | + |
| 7 | +ANTX files are XML documents that store speech or audio annotations: layers, segments (with labels and time bounds), and project configuration. They are produced and consumed by [Annotation PRO](https://annotationpro.org/). |
| 8 | + |
| 9 | +**Time in ANTX is stored in samples**, not in seconds. Each annotation file has a **samplerate** (e.g. 44100 Hz) in its configuration. Segment `start` and `duration` in the file are sample counts. To work in seconds you can use the samplerate from the annotation and the helper functions provided by this library (see below). |
| 10 | + |
| 11 | +## Requirements |
| 12 | + |
| 13 | +- Python 3.9+ |
| 14 | + |
| 15 | +## Installation |
| 16 | + |
| 17 | +From PyPI: |
| 18 | + |
| 19 | +```bash |
| 20 | +pip install annotationpro-format |
| 21 | +``` |
| 22 | + |
| 23 | +From source (development): |
| 24 | + |
| 25 | +```bash |
| 26 | +pip install -e . |
| 27 | +``` |
| 28 | + |
| 29 | +## Usage |
| 30 | + |
| 31 | +### Reading an ANTX file |
| 32 | + |
| 33 | +```python |
| 34 | +from annotationpro_format import deserialize_annotation |
| 35 | + |
| 36 | +with open("project.antx", "r", encoding="utf-8") as f: |
| 37 | + xml_content = f.read() |
| 38 | + |
| 39 | +annotation = deserialize_annotation(xml_content) |
| 40 | +samplerate = int(annotation.configuration["Samplerate"]) |
| 41 | + |
| 42 | +for layer in annotation.layers: |
| 43 | + print(f"Layer: {layer.name}") |
| 44 | + for seg in layer.segments: |
| 45 | + print(f" {seg.start} – {seg.duration} (samples) – {seg.label}") |
| 46 | +``` |
| 47 | + |
| 48 | +### Writing an ANTX file (using samples) |
| 49 | + |
| 50 | +Segment `start` and `duration` are stored in samples. You can set them directly when you work in sample counts: |
| 51 | + |
| 52 | +```python |
| 53 | +from annotationpro_format import Annotation, Layer, Segment, serialize_annotation |
| 54 | + |
| 55 | +annotation = Annotation(samplerate=44100) |
| 56 | +layer = Layer("Transcription") |
| 57 | +# start=0, duration=66150 means 0 to 1.5 seconds at 44100 Hz |
| 58 | +layer.segments.append(Segment("hello", start=0.0, duration=66150.0)) |
| 59 | +annotation.layers.append(layer) |
| 60 | + |
| 61 | +xml_str = serialize_annotation(annotation) |
| 62 | +with open("output.antx", "w", encoding="utf-8") as f: |
| 63 | + f.write(xml_str) |
| 64 | +``` |
| 65 | + |
| 66 | +### Working with time in seconds (helpers) |
| 67 | + |
| 68 | +To avoid manual sample math, use the seconds helpers. They use the annotation’s samplerate to convert between seconds and samples. |
| 69 | + |
| 70 | +**Writing segments with start/duration in seconds:** |
| 71 | + |
| 72 | +```python |
| 73 | +from annotationpro_format import ( |
| 74 | + Annotation, |
| 75 | + Layer, |
| 76 | + Segment, |
| 77 | + serialize_annotation, |
| 78 | + set_segment_start_seconds, |
| 79 | + set_segment_duration_seconds, |
| 80 | +) |
| 81 | + |
| 82 | +annotation = Annotation(samplerate=44100) |
| 83 | +layer = Layer("Transcription") |
| 84 | + |
| 85 | +# Create segment (start/duration in samples will be set by helpers) |
| 86 | +seg = Segment("hello", start=0.0, duration=0.0) |
| 87 | +set_segment_start_seconds(annotation, seg, 0.0) # 0 seconds |
| 88 | +set_segment_duration_seconds(annotation, seg, 1.5) # 1.5 seconds |
| 89 | +layer.segments.append(seg) |
| 90 | + |
| 91 | +# Another segment: from 2.5 s to 4.0 s (duration 1.5 s) |
| 92 | +seg2 = Segment("world", start=0.0, duration=0.0) |
| 93 | +set_segment_start_seconds(annotation, seg2, 2.5) |
| 94 | +set_segment_duration_seconds(annotation, seg2, 1.5) |
| 95 | +layer.segments.append(seg2) |
| 96 | + |
| 97 | +annotation.layers.append(layer) |
| 98 | +with open("output.antx", "w", encoding="utf-8") as f: |
| 99 | + f.write(serialize_annotation(annotation)) |
| 100 | +``` |
| 101 | + |
| 102 | +**Reading segment times in seconds:** |
| 103 | + |
| 104 | +```python |
| 105 | +from annotationpro_format import ( |
| 106 | + deserialize_annotation, |
| 107 | + get_segment_start_seconds, |
| 108 | + get_segment_duration_seconds, |
| 109 | +) |
| 110 | + |
| 111 | +with open("project.antx", "r", encoding="utf-8") as f: |
| 112 | + annotation = deserialize_annotation(f.read()) |
| 113 | + |
| 114 | +for layer in annotation.layers: |
| 115 | + for seg in layer.segments: |
| 116 | + start_sec = get_segment_start_seconds(annotation, seg) |
| 117 | + duration_sec = get_segment_duration_seconds(annotation, seg) |
| 118 | + print(f"{start_sec:.2f}s – {start_sec + duration_sec:.2f}s: {seg.label}") |
| 119 | +``` |
| 120 | + |
| 121 | +**Low-level conversion (seconds ↔ samples):** |
| 122 | + |
| 123 | +```python |
| 124 | +from annotationpro_format import seconds_to_samples, samples_to_seconds |
| 125 | + |
| 126 | +samplerate = 44100 |
| 127 | +samples = seconds_to_samples(1.5, samplerate) # 66150.0 |
| 128 | +seconds = samples_to_seconds(66150, samplerate) # 1.5 |
| 129 | +``` |
| 130 | + |
| 131 | +## Model overview |
| 132 | + |
| 133 | +- **Annotation** – Root object: `configuration` (dict, includes `Samplerate`), `layers` (list). |
| 134 | +- **Layer** – One annotation layer: `name`, `id`, `segments` (list-like `SegmentCollection`), plus display options (colors, height, etc.). |
| 135 | +- **Segment** – One segment: `label`, `start`, `duration` (in **samples**), and optional fields (`feature`, `language`, `group`, etc.). |
| 136 | +- **SegmentCollection** – List of segments for a layer; setting `id_layer` on the collection is handled when you append segments. |
| 137 | +- **AudioFile** – Model for audio file references (not used in the current serialization). |
| 138 | + |
| 139 | +## Publishing to PyPI (for maintainers) |
| 140 | + |
| 141 | +Publishing the library to PyPI is **fully automated** via GitHub Actions. |
| 142 | + |
| 143 | +- A release is published when a **tag** matching `vX.Y.Z` is **pushed to GitHub**, e.g.: |
| 144 | + |
| 145 | + ```bash |
| 146 | + git tag v0.1.1 |
| 147 | + git push origin v0.1.1 |
| 148 | + ``` |
| 149 | + |
| 150 | +- Before creating the tag, **bump the version** in `pyproject.toml`: |
| 151 | + |
| 152 | + ```toml |
| 153 | + [project] |
| 154 | + version = "0.1.1" |
| 155 | + ``` |
| 156 | + |
| 157 | +- The **tag name** (e.g. `v0.1.1`) and the version in `pyproject.toml` (e.g. `0.1.1`) **must match**. |
| 158 | + |
| 159 | +When the tag is pushed: |
| 160 | + |
| 161 | +1. The workflow in `.github/workflows/publish-pypi.yml` runs. |
| 162 | +2. It checks out the code at that tag. |
| 163 | +3. It builds the package with `python -m build` (sdist + wheel). |
| 164 | +4. It uploads the artifacts in `dist/` to PyPI using `twine upload dist/*` and the `PYPI_API_TOKEN` secret configured in the repository. |
| 165 | + |
| 166 | +If the workflow succeeds, the new version is immediately available on PyPI as `annotationpro-format`. |
| 167 | + |
| 168 | +## License |
| 169 | + |
| 170 | +MIT – see [LICENSE](LICENSE). |
0 commit comments