Metadata-Version: 2.4
Name: aup-parser
Version: 0.10.2
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Topic :: Multimedia :: Sound/Audio
Summary: Audacity .aup3 parser with Rust core and Python interface
Keywords: audacity,aup3,audio,parser,rust,pyo3
Author: aup-parser contributors
License: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# aup-parser

`aup-parser` is an Audacity `.aup3` parser package with a Rust core and typed Python interface.

## Highlights

- Rust core for binary XML and sampleblocks analysis
- Class-based Python API (`AUPParser`) with `TypedDict` return types
- Optional audio export helper

## Package Layout

- `src/`: Rust core (`PyO3` extension)
- `python/aup_parser/`: Python API, types, exports
- `tests/`: API/parity tests and golden fixtures

## Build (local dev)

```bash
cd /Users/joonholee/deeply/aup3-parser
python3 -m pip install --user --break-system-packages maturin
python3 -m maturin build --release -i python3
python3 -m pip install --user --break-system-packages --force-reinstall target/wheels/aup_parser-*.whl
```

## Python API (Class-only)

```python
from aup_parser import AUPParser

parser = AUPParser("tests/test.aup3")
essential = parser.parse(profile="essential")
full = parser.parse(profile="full")
raw = parser.inspect_core(profile="full")  # low-level diagnostics payload
```

`AUPParser.parse()` signature:

```python
def parse(
    self,
    *,
    profile: Literal["essential", "full"] = "essential",
    audio_output_path: str | pathlib.Path | None = None,
    sample_rate: int = 48000,
    output_naming: Literal["channel_index", "track_name"] = "channel_index",
) -> ParseResultTD: ...
```

- `audio_output_path`: `None`이 아니면 WAV를 export
  - 싱글 채널 프로젝트: 단일 WAV 파일 출력
  - 멀티 채널 프로젝트: `audio_output_path`를 폴더로 사용하여 채널별 WAV 파일 생성
  - 멀티 채널 + 파일처럼 보이는 경로(`out.wav`) 입력 시: 자동으로 `out_channels/` 폴더로 정규화
  - 타임라인 복원: clip metadata(`offset`, `trimLeft`, `trimRight`)를 우선 적용하고, 불가능한 경우 `waveblock.start` 기준으로 배치
  - gap 구간은 zero-fill, 겹침 구간은 later block overwrite 규칙 사용
- `sample_rate`: `audio_output_path` export 시 사용할 샘플레이트
- `output_naming`: 멀티 채널 파일 이름 전략
  - `channel_index`: `channel_00.wav`, `channel_01.wav`, ...
  - `track_name`: 트랙명을 안전한 파일명으로 변환해 사용(유니코드 보존, 중복 시 suffix 추가)

## Parse Result (Essential)

`parse(profile="essential")` 반환 타입:

```python
{
  "schema_version": "2",
  "project": {...},
  "audio": {...},
  "exports": {...}  # optional
}
```

### `project` fields

| Field                             | Type                                               | Meaning                               |
| --------------------------------- | -------------------------------------------------- | ------------------------------------- |
| `project_path`                    | `str`                                              | `.aup3` 절대/정규화 경로              |
| `project_file_name`               | `str \| None`                                      | `.aup3` 파일명 (예: `session.aup3`)   |
| `project_file_stem`               | `str \| None`                                      | 확장자 제외 파일명 (예: `session`)    |
| `project_extension`               | `str`                                              | 확장자 (기본 `aup3`)                  |
| `project_size_bytes`              | `int \| None`                                      | 프로젝트 파일 크기                    |
| `original_audio_file_name`        | `str \| None`                                      | 원본 오디오 파일명 추정값 (가능할 때) |
| `original_audio_file_name_source` | `"embedded_name" \| "track_name" \| "unavailable"` | 추정 출처                             |

`original_audio_file_name` 주의:

- Audacity `.aup3`는 원본 입력 파일명을 항상 보존하지 않습니다.
- 따라서 확정값이 아닐 수 있으며, 추정 출처는 `original_audio_file_name_source`로 확인하세요.

### `audio` fields

| Field                | Type                          | Meaning                    |
| -------------------- | ----------------------------- | -------------------------- |
| `sample_rate_hz`     | `float \| int \| str \| None` | 프로젝트 샘플레이트        |
| `duration_seconds`   | `float \| None`               | 전체 재생 길이(초)         |
| `total_samples`      | `int \| None`                 | 전체 샘플 수               |
| `total_sample_bytes` | `int \| None`                 | 전체 PCM 바이트 수         |
| `channel_indices`    | `list[int]`                   | 채널 인덱스 목록           |
| `channel_count`      | `int`                         | 채널 개수                  |
| `track_count`        | `int \| None`                 | wavetrack 개수             |
| `clip_count`         | `int \| None`                 | waveclip 개수              |
| `block_count`        | `int \| None`                 | sample block 개수          |
| `sample_format`      | `SampleFormatInfoTD \| None`  | 샘플 포맷 정보             |
| `tracks`             | `list[TrackSummaryTD]`        | 트랙 요약 리스트           |
| `clips`              | `list[ClipSummaryTD]`         | 클립 요약 리스트           |
| `timeline`           | `TimelineSummaryTD`           | 블록 사용/누락/미사용 정보 |
| `exactness_notes`    | `list[str]`                   | 정확도 관련 메모           |

### `audio.sample_format`

| Field                | Type  | Meaning                        |
| -------------------- | ----- | ------------------------------ |
| `raw`                | `int` | Audacity raw sampleformat 정수 |
| `sample_width_bytes` | `int` | 샘플 폭 (바이트)               |
| `encoding_id`        | `int` | 인코딩 식별자                  |

### `audio.tracks[]` (`TrackSummaryTD`)

| Field              | Type            | Meaning               |
| ------------------ | --------------- | --------------------- |
| `channel_index`    | `int \| None`   | 채널 인덱스           |
| `name`             | `str \| None`   | 트랙 이름             |
| `clip_count`       | `int \| None`   | 트랙 내 클립 수       |
| `mute`             | `bool \| None`  | mute 상태             |
| `solo`             | `bool \| None`  | solo 상태             |
| `gain`             | `float \| None` | 게인                  |
| `pan`              | `float \| None` | 팬                    |
| `sampleformat_raw` | `int \| None`   | 트랙 raw sampleformat |

### `audio.clips[]` (`ClipSummaryTD`)

| Field                    | Type            | Meaning           |
| ------------------------ | --------------- | ----------------- |
| `name`                   | `str \| None`   | 클립 이름         |
| `offset_seconds`         | `float \| None` | 클립 오프셋(초)   |
| `trim_left_seconds`      | `float \| None` | 좌측 trim(초)     |
| `trim_right_seconds`     | `float \| None` | 우측 trim(초)     |
| `raw_audio_tempo`        | `float \| None` | 원시 템포 메타    |
| `stretch_ratio`          | `float \| None` | stretch 비율      |
| `sample_count`           | `int \| None`   | 클립 샘플 수      |
| `max_block_sample_count` | `int \| None`   | 블록 최대 샘플 수 |

### `audio.timeline` (`TimelineSummaryTD`)

| Field               | Type        | Meaning                                   |
| ------------------- | ----------- | ----------------------------------------- |
| `used_block_ids`    | `list[int]` | 타임라인에서 참조한 block IDs             |
| `missing_block_ids` | `list[int]` | 타임라인엔 있으나 sampleblocks엔 없는 IDs |
| `unused_block_ids`  | `list[int]` | sampleblocks엔 있으나 타임라인에 없는 IDs |

## Parse Result (Full)

`parse(profile="full")`은 Essential 결과에 `diagnostics`를 추가합니다.

### `diagnostics` fields

| Field             | Type                | Meaning                     |
| ----------------- | ------------------- | --------------------------- |
| `sqlite`          | `dict[str, object]` | SQLite 개요 정보            |
| `project_payload` | `ProjectPayloadTD`  | 파싱된 프로젝트 payload raw |
| `audio_blocks`    | `AudioBlocksInfoTD` | sampleblocks 집계 raw       |

## Exports fields

`exports.audio_output` 필드:

| Field                       | Type                                      | Meaning               |
| --------------------------- | ----------------------------------------- | --------------------- |
| `mode`                      | `"single_file" \| "multi_channel_folder"` | 출력 모드             |
| `output_naming`             | `"channel_index" \| "track_name"`         | 파일명 전략           |
| `requested_output_path`     | `str`                                     | 입력된 경로           |
| `output_path`               | `str`                                     | 실제 출력 경로        |
| `path_was_normalized`       | `bool`                                    | 경로 정규화 여부      |
| `channel_count`             | `int`                                     | 출력 채널 수          |
| `files`                     | `list[AudioOutputFileTD]`                 | 출력 파일 목록        |
| `sample_rate`               | `int`                                     | export 샘플레이트     |
| `sample_width_bytes`        | `int`                                     | export 샘플 폭        |
| `sampleformat`              | `SampleFormatInfoTD`                      | export sampleformat   |
| `total_written_audio_bytes` | `int`                                     | 총 출력 바이트 수     |
| `note`                      | `str`                                     | 복원/정규화/주의 사항 |

## inspect_core() (Raw)

`inspect_core()`는 low-level payload를 그대로 반환합니다.

- 용도: 디버깅, parity 검증, 내부 분석
- 안정 API 용도는 `parse()` 사용 권장

## Compatibility Notes

- Python 3.11~3.14
- `abi3-py311` build strategy
- CI validates install + runtime parse on all combinations:
  - Linux/macOS/Windows
  - Python 3.11, 3.12, 3.13, 3.14
- Release wheels are built for:
  - Linux (`ubuntu-latest`)
  - Windows (`windows-latest`)
  - macOS Apple Silicon (`macos-15`)
  - macOS Intel (`macos-15-intel`)
- If a wheel is unavailable for your environment, `pip` falls back to sdist build and requires a Rust toolchain.

## Release

GitHub Actions workflow (`.github/workflows/release.yml`) runs:

1. Matrix tests on PRs
2. Wheel/sdist build on version tags (`v*`)
3. Publish `dist/*` to GitHub Release assets
4. Publish `dist/*` to GitHub Packages (GHCR OCI artifact)
5. Optional PyPI publish (only when `UV_PUBLISH_TOKEN` secret is set)

### PyPI Token Setup

1. Create a PyPI API token (`__token__`) on PyPI account settings.
2. In GitHub repo settings, add:
   - `Settings > Secrets and variables > Actions > New repository secret`
   - Name: `UV_PUBLISH_TOKEN`
   - Value: `pypi-...` token string
3. Push a release tag (`vX.Y.Z`). The `publish-pypi` job uploads `dist/*` via:
   - `uv publish --trusted-publishing never dist/*`

### GitHub Distribution

- Release assets: `https://github.com/<owner>/<repo>/releases/tag/vX.Y.Z`
- GitHub Packages (GHCR): `ghcr.io/<owner>/aup-parser:vX.Y.Z` and `ghcr.io/<owner>/aup-parser:X.Y.Z`
- GHCR pull example:
  - `oras pull ghcr.io/<owner>/aup-parser:vX.Y.Z`
  - pulled wheel/sdist files can then be installed with `pip install <wheel-file>`

