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 text

The 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() and line.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

  1. TextModel.__init__ tokenizes the text, phonemizes each unique word (CMU dictionary, with espeak TTS fallback), and builds _syll_df.
  2. 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).
  3. Results are stored by line number and attached to Line entities lazily on first access of text.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 clientprosodic.set_server(...) returns proxy objects that duck-type the local API, so downstream code works unchanged against a Prosodic server. Only requests is required locally.
  • Languages — English (CMU + espeak) and Finnish (rule-based) ship today; the language layer is designed to be extended.