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.
One parse path
All parsing goes through a single vectorized path working entirely from _syll_df: text.parse() batches whole texts (parse_batch_from_df), and single lines, lineparts, and arbitrary token lists route through parse_units_from_df, which scopes the parent frame by word_num — so line.best_parse and text.parse() always agree. Parse slots hold lightweight SyllData stand-ins; rendering and the web app bridge from a slot to its WordToken via SyllData.word_num rather than a parent chain. (A second, entity-based parser existed through v3.9 and was retired; the one construction that still builds entity slots is the manual Parse(line, "wsws") reference constructor.) See Metrical parsing for how the evaluator 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.