Architecture
A high-level tour of how Prosodic 3 is put together. For the linguistic theory and the parsing internals, see Metrical parsing and Phrasal stress; for exhaustive design notes, see CLAUDE.md in the source tree.
DataFrame-first design
The source of truth for a text is a flat, syllable-level DataFrame (_syll_df) — one row per syllable, per pronunciation variant, with columns for stress, weight, strong/weak metrical role, function-word status, and (with syntax=True) phrasal prominence. The vectorized parser works entirely from this frame, in numpy, without building any objects.
You can inspect it directly:
import prosodic
sonnet = prosodic.Text("Shall I compare thee to a summer's day?")
sonnet.df # the syllable-level DataFrame
sonnet.parse()
sonnet.parsed_df # per-syllable parse results across the whole textThe entity hierarchy
Linguistic objects form a lazily-constructed tree — nothing below the text is built until you access it:
TextModel → Stanza → Line → WordToken → WordType → WordForm → Syllable → Phoneme
- TextModel — the root container, created with
prosodic.Text(...). - Line — the unit of metrical parsing;
line.parse()andline.best_parse. - WordToken — a token in the text, wrapping a WordType (canonical form) whose WordForm children are pronunciation variants.
- WordForm → Syllable → Phoneme — a specific pronunciation with IPA, stress, and weight.
Because construction is lazy, batch workflows that only need parse results (via text.parse() and text.parsed_df) never pay for the ~hundreds of thousands of entity objects a full tree would allocate.
Two parse paths
There are two ways parsing happens, and it matters which one you are in:
- DataFrame path (
text.parse()) — works entirely from_syll_df; parse slots hold lightweightSyllDatastand-ins with no parent chain. Fastest; used for batch processing,text.parsed_df, andtext.save(). - Entity path (
parse_batch(text.lines, meter)) — the same tensors, but parse slots hold realSyllableentities with parent chains, so you can walk from a syllable back to its word. Needed for HTML rendering and the web app.
Both paths share the same exhaustive vectorized evaluator; see Metrical parsing for how it works.
Parsing pipeline
TextModel.__init__tokenizes the text, phonemizes each unique word (CMU dictionary, with espeak TTS fallback), and builds_syll_df.text.parse()groups syllables by line, evaluates every candidate scansion’s constraint violations in numpy, and prunes by harmonic bounding (on GPU via torch when available).- Results are stored by line number and attached to
Lineentities lazily on first access oftext.lines.
Learning constraint weights
Meter.fit(text, "wswswswsws") (or meter.fit_annotations(data)) learns constraint weights from data with a Maximum-Entropy / Harmonic-Grammar model (prosodic/parsing/maxent.py). Weights can be split by syllable position (zones) so positional sensitivity — line-initial inversion, say — transfers to parsing unseen text. Details in Metrical parsing.
Poem-level analysis
prosodic/analysis/ (a port of the standalone poesy package) computes higher-order form statistics, surfaced as TextModel properties: text.meter_type, text.line_scheme, text.rhyme_scheme, text.is_sonnet, and the tabular text.summary().
Beyond the library
- Web app — FastAPI + SvelteKit; see Web app.
- Remote client —
prosodic.set_server(...)returns proxy objects that duck-type the local API, so downstream code works unchanged against a Prosodic server. Onlyrequestsis required locally. - Languages — English (CMU + espeak) and Finnish (rule-based) ship today; the language layer is designed to be extended.