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.

Two parse paths

There are two ways parsing happens, and it matters which one you are in:

  1. DataFrame path (text.parse()) — works entirely from _syll_df; parse slots hold lightweight SyllData stand-ins with no parent chain. Fastest; used for batch processing, text.parsed_df, and text.save().
  2. Entity path (parse_batch(text.lines, meter)) — the same tensors, but parse slots hold real Syllable entities 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

  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.