Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!--
SPDX-FileCopyrightText: 2026 Mikkel Bergmann
SPDX-License-Identifier: CC-BY-4.0
-->

# Changelog

All notable changes to the Podscript specification are recorded here. The spec follows
[Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`, SPEC §20): a change that
alters the meaning of an existing valid script or any golden conformance fixture is
**major**; additive, backward-compatible features are **minor**; clarifications are
**patch**. Scripts declare the lowest version whose features they use (§1.2).

## 0.2.0 — 2026-07-03

### Added
- **Prosody model (SPEC §5.5).** A `prosody` control for speaking delivery — the continuous
attributes `rate` (tempo multiplier), `pitch` (semitone shift), and `volume` (vocal-intensity
multiplier) — as a per-voice baseline object (§2.3) and a per-line `cue` clause (§5), merged
**per attribute** onto the IR clip (§11.1) and omitted at natural defaults. The section maps
the remaining prosodic dimensions onto existing constructs (`*emphasis*`, breaks, sentence
punctuation, `(direction)`).
- New `SEMITONES` token (§1.5); AST `Voice.prosody`, `Prosody` interface, and `prosody` clause
variant (§10); IR-clip `prosody` (§11).
- Prosody is **advisory** synthesis guidance — a deliberate exception to fail-closed (§15.4):
core vocabulary, but engines drop attributes they cannot honour, degrading expressiveness not
mix correctness. It joins the synthesis cache key (§14.3) and does **not** affect
timing/resolution.
- Conformance fixtures `conformance/parse/prosody` and `conformance/resolve/prosody`; runnable
example `examples/prosody.podscript`.

### Changed
- Adopted three-part semantic-version stamps (`MAJOR.MINOR.PATCH`; `PATCH` optional, defaults
to `0`, §1.2). Normalized every bundled example and conformance-fixture stamp from `0.1` to
`0.2.0` so the repository uses one consistent current version — a one-time pre-1.0 housekeeping
step. Two-part `0.1` stamps remain valid input (`0.1` ≡ `0.1.0`).

### Compatibility
- The prosody feature is additive and backward-compatible at the language level: `prosody` is
omitted when absent, and a `0.1` script parses unchanged under a 0.2.0 engine. The version
*stamps* in the bundled examples and golden fixtures were re-stamped to `0.2.0` as part of the
semver normalization above — a housekeeping change to the stamp only, not a semantic change to
any script's meaning.

## 0.1.0 — 2026-06-24

### Added
- Initial public draft: lexical grammar; `meta` / `voices` / `assets` / `pronounce` headers;
speech lines with typed inline content (emphasis, breaks, pronunciation); clauses (anchors,
`gain`, `fade`, `crossfade`, `duck-under`, `at`, loop); the relational timing model and speech
spine; the resolved IR schema; the gain model; determinism and the render manifest;
extensibility (namespaced extensions, fail-closed); security; and transcript export.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ music beds, sound effects, fades, and ducking — that compiles deterministicall
finished audio file.

```podscript
podscript: "0.1"
podscript: "0.2.0"

voices:
sam: { voice: Adam, preset: host }
Expand Down Expand Up @@ -69,7 +69,7 @@ and any renderer. Source → IR → audio.

## The specification

[`docs/SPEC.md`](docs/SPEC.md) is the normative v0.1 specification: lexical grammar,
[`docs/SPEC.md`](docs/SPEC.md) is the normative v0.2.0 specification: lexical grammar,
speakers and voices, cues and directions, the gain / duck / fade model, the relational
timing model, the IR schema, determinism, extensibility, security, and transcript export.
This README is informative; where they differ, the spec wins. Domain terms (bed, duck,
Expand All @@ -91,7 +91,9 @@ provider-dependent and reproducible only when pinned via a render manifest.
## Examples

[`examples/`](examples/) holds runnable sample scripts, including
[`cold_open.podscript`](examples/cold_open.podscript).
[`cold_open.podscript`](examples/cold_open.podscript) and
[`prosody.podscript`](examples/prosody.podscript) (controlling delivery — rate, pitch,
volume, emphasis).

## Implementations

Expand Down
2 changes: 2 additions & 0 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ renderer) and MUST pass every fixture for each class it claims.
| `resolve` | `full_episode` | the above plus: continuous cross-scene timeline, bed placement at the playhead, bed lifetime across scenes, two instances of one asset, `duck-under speech` (declarative), and a cross-scene `crossfade` paired with a forward-anchored (`at signoff.end`) fade-out |
| `parse` | `pronounce` | `pronounce:` header lexicon (quoted multi-word term), inline `{say "X" as "Y"}` pronunciation node alongside emphasis/break (§2.5, §3.1) |
| `resolve` | `pronounce` | `pronounce` map copied to the IR root; speech `content` (incl. the inline `pronounce` node) preserved verbatim with original spelling; timing unaffected |
| `parse` | `prosody` | voice-level `prosody: { rate, pitch }` baseline object and per-line `prosody rate/pitch/volume` clauses in the speech `cue` (§2.3, §5.5); pitch parsed from semitones (`-1st` → `-1`); a voice without `prosody` omits it |
| `resolve` | `prosody` | per-attribute effective-prosody merge onto the clip (line clause overrides voice baseline attribute-by-attribute; attributes at their natural default and the empty object omitted); prosody is synthesis metadata and does not perturb `start`/`dur`/`end` |

The semantic questions the multi-scene episode raised — multi-scene timing, bed
lifetime/placement, cross-scene crossfade, duck envelope, and how to add automation
Expand Down
2 changes: 1 addition & 1 deletion conformance/parse/cold_open/expected.ast.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"podscript": "0.1",
"podscript": "0.2.0",
"meta": {
"title": "Episode 12 — The Thing",
"lufs": -16,
Expand Down
2 changes: 1 addition & 1 deletion conformance/parse/cold_open/input.podscript
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
podscript: "0.1"
podscript: "0.2.0"

meta:
title: "Episode 12 — The Thing"
Expand Down
2 changes: 1 addition & 1 deletion conformance/parse/pronounce/expected.ast.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"podscript": "0.1",
"podscript": "0.2.0",
"meta": {},
"requires": [],
"voices": {
Expand Down
2 changes: 1 addition & 1 deletion conformance/parse/pronounce/input.podscript
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
podscript: "0.1"
podscript: "0.2.0"

voices:
sam: { voice: Adam, preset: host }
Expand Down
90 changes: 90 additions & 0 deletions conformance/parse/prosody/expected.ast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"podscript": "0.2.0",
"meta": {},
"requires": [],
"voices": {
"sam": {
"voice": "Adam",
"preset": "host",
"prosody": {
"rate": 1.1,
"pitch": -1
}
},
"alex": {
"voice": "Rachel",
"preset": "warm-host"
}
},
"assets": {},
"scenes": [
{
"kind": "scene",
"name": "chat",
"body": [
{
"kind": "speech",
"speaker": "sam",
"content": [
{
"kind": "text",
"value": "This is my normal quicker cadence."
}
]
},
{
"kind": "speech",
"speaker": "sam",
"cue": [
{
"kind": "prosody",
"rate": 1.35,
"pitch": 3,
"volume": 1.2
}
],
"content": [
{
"kind": "text",
"value": "Wait, it actually "
},
{
"kind": "emphasis",
"value": "worked"
},
{
"kind": "text",
"value": "!"
}
]
},
{
"kind": "speech",
"speaker": "alex",
"cue": [
{
"kind": "prosody",
"rate": 0.9
}
],
"content": [
{
"kind": "text",
"value": "Slow and steady over here."
}
]
},
{
"kind": "speech",
"speaker": "alex",
"content": [
{
"kind": "text",
"value": "And back to normal."
}
]
}
]
}
]
}
11 changes: 11 additions & 0 deletions conformance/parse/prosody/input.podscript
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
podscript: "0.2.0"

voices:
sam: { voice: Adam, preset: host, prosody: { rate: 1.1, pitch: -1st } }
alex: { voice: Rachel, preset: warm-host }

scene chat:
sam: This is my normal quicker cadence.
sam [prosody rate 1.35 pitch +3st volume 1.2]: Wait, it actually *worked*!
alex [prosody rate 0.9]: Slow and steady over here.
alex: And back to normal.
2 changes: 1 addition & 1 deletion conformance/resolve/cold_open/expected.ir.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"podscript": "0.1",
"podscript": "0.2.0",
"meta": {
"title": "Episode 12 — The Thing",
"lufs": -16,
Expand Down
2 changes: 1 addition & 1 deletion conformance/resolve/cold_open/input.podscript
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
podscript: "0.1"
podscript: "0.2.0"

meta:
title: "Episode 12 — The Thing"
Expand Down
2 changes: 1 addition & 1 deletion conformance/resolve/full_episode/expected.ir.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"podscript": "0.1",
"podscript": "0.2.0",
"meta": {
"title": "Episode 12 — The Thing",
"lufs": -16,
Expand Down
2 changes: 1 addition & 1 deletion conformance/resolve/full_episode/input.podscript
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
podscript: "0.1"
podscript: "0.2.0"

# Full multi-scene episode: exercises continuous cross-scene timeline,
# bed lifetime across scenes, duck-under-speech, and a cross-scene crossfade
Expand Down
2 changes: 1 addition & 1 deletion conformance/resolve/pronounce/expected.ir.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"podscript": "0.1",
"podscript": "0.2.0",
"meta": {},
"pronounce": {
"Seibold": "SY-bold",
Expand Down
2 changes: 1 addition & 1 deletion conformance/resolve/pronounce/input.podscript
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
podscript: "0.1"
podscript: "0.2.0"

voices:
sam: { voice: Adam, preset: host }
Expand Down
10 changes: 10 additions & 0 deletions conformance/resolve/prosody/durations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"_comment": "Durations supplied to the resolver, in milliseconds, standing in for TTS + ffprobe. Speech keyed by clip id; assets by asset id. Prosody does not affect resolution, so these are the measured clip lengths regardless of any rate/pitch/volume.",
"clips": {
"c1": 3000,
"c2": 1800,
"c3": 2600,
"c4": 1500
},
"assets": {}
}
88 changes: 88 additions & 0 deletions conformance/resolve/prosody/expected.ir.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"podscript": "0.2.0",
"meta": {},
"clips": [
{
"id": "c1",
"track": "speech",
"voice": "sam",
"preset": "host",
"prosody": {
"rate": 1.1,
"pitch": -1
},
"start": 0.0,
"dur": 3.0,
"end": 3.0,
"content": [
{
"kind": "text",
"value": "This is my normal quicker cadence."
}
]
},
{
"id": "c2",
"track": "speech",
"voice": "sam",
"preset": "host",
"prosody": {
"rate": 1.35,
"pitch": 3,
"volume": 1.2
},
"start": 3.25,
"dur": 1.8,
"end": 5.05,
"content": [
{
"kind": "text",
"value": "Wait, it actually "
},
{
"kind": "emphasis",
"value": "worked"
},
{
"kind": "text",
"value": "!"
}
]
},
{
"id": "c3",
"track": "speech",
"voice": "alex",
"preset": "warm-host",
"prosody": {
"rate": 0.9
},
"start": 5.3,
"dur": 2.6,
"end": 7.9,
"content": [
{
"kind": "text",
"value": "Slow and steady over here."
}
]
},
{
"id": "c4",
"track": "speech",
"voice": "alex",
"preset": "warm-host",
"start": 8.15,
"dur": 1.5,
"end": 9.65,
"content": [
{
"kind": "text",
"value": "And back to normal."
}
]
}
],
"beds": [],
"oneshots": []
}
11 changes: 11 additions & 0 deletions conformance/resolve/prosody/input.podscript
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
podscript: "0.2.0"

voices:
sam: { voice: Adam, preset: host, prosody: { rate: 1.1, pitch: -1st } }
alex: { voice: Rachel, preset: warm-host }

scene chat:
sam: This is my normal quicker cadence.
sam [prosody rate 1.35 pitch +3st volume 1.2]: Wait, it actually *worked*!
alex [prosody rate 0.9]: Slow and steady over here.
alex: And back to normal.
Loading