calliope-ts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.opencode/skills/calliope-ts/SKILL.md +74 -0
  3. package/LICENSE +201 -0
  4. package/README.md +485 -0
  5. package/dist/depfix.d.ts +13 -0
  6. package/dist/depfix.d.ts.map +1 -0
  7. package/dist/depfix.js +84 -0
  8. package/dist/display.d.ts +38 -0
  9. package/dist/display.d.ts.map +1 -0
  10. package/dist/display.js +890 -0
  11. package/dist/index.d.ts +50 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +504 -0
  14. package/dist/parser.d.ts +10 -0
  15. package/dist/parser.d.ts.map +1 -0
  16. package/dist/parser.js +688 -0
  17. package/dist/phonological.d.ts +41 -0
  18. package/dist/phonological.d.ts.map +1 -0
  19. package/dist/phonological.js +788 -0
  20. package/dist/rhyme.d.ts +26 -0
  21. package/dist/rhyme.d.ts.map +1 -0
  22. package/dist/rhyme.js +340 -0
  23. package/dist/scandroid.d.ts +17 -0
  24. package/dist/scandroid.d.ts.map +1 -0
  25. package/dist/scandroid.js +435 -0
  26. package/dist/scansion.d.ts +37 -0
  27. package/dist/scansion.d.ts.map +1 -0
  28. package/dist/scansion.js +1007 -0
  29. package/dist/scansion_debug.js +586 -0
  30. package/dist/stress.d.ts +32 -0
  31. package/dist/stress.d.ts.map +1 -0
  32. package/dist/stress.js +1372 -0
  33. package/dist/tagfix.d.ts +6 -0
  34. package/dist/tagfix.d.ts.map +1 -0
  35. package/dist/tagfix.js +101 -0
  36. package/dist/types.d.ts +173 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +4 -0
  39. package/package.json +62 -0
  40. package/src/depfix.ts +88 -0
  41. package/src/display.ts +954 -0
  42. package/src/index.ts +541 -0
  43. package/src/parser.ts +837 -0
  44. package/src/phonological.ts +849 -0
  45. package/src/rhyme.ts +328 -0
  46. package/src/scandroid.ts +434 -0
  47. package/src/scansion.ts +1053 -0
  48. package/src/stress.ts +1381 -0
  49. package/src/tagfix.ts +104 -0
  50. package/src/types.ts +230 -0
package/README.md ADDED
@@ -0,0 +1,485 @@
1
+ # Calliope TS
2
+
3
+ **Calliope TS** is a phonological poetry-scansion toolkit for TypeScript / Node.js.
4
+ Give it a poem (or a single line of verse) and it will tell you — and *show*
5
+ you — how the poem sounds: which syllables carry stress and how much, where the
6
+ metrical beats fall, what meter each line is written in, how certain that
7
+ reading is, what the rhyme scheme is, and what poetic form the stanzas add up
8
+ to.
9
+
10
+ It is not a syllable-counter with a rulebook bolted on. Calliope TS scans verse
11
+ the way a trained reader does: it first works out how the words would actually
12
+ be *spoken* — dictionary stress, phrase rhythm, sentence intonation — and only
13
+ then asks which metrical pattern that spoken contour fits best.
14
+
15
+ ```
16
+ $ calliope-ts --reading exile.txt
17
+
18
+ Reading View — stress gradient over the original text
19
+
20
+ He happens to be a French poet, that thin, ← every syllable colour-coded
21
+ book-carrying man with a bristly gray chin; by its stress level
22
+ ...
23
+
24
+ Stress Maps & Meter — top-3 fit scores per line
25
+ S1L1 nsw|xwx|msw ‖ xs amphibrachic tetrameter (amph 1.20 · iamb 1.14 ·
26
+ anap 1.13) A
27
+ S1L2 m|-mww|sxx|mwm|s amphibrachic tetrameter ≈ continuity; standalone:
28
+ dactylic tetrameter A(perfect)
29
+ ...
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Contents
35
+
36
+ - [Installation](#installation)
37
+ - [Quick start (CLI)](#quick-start-cli)
38
+ - [Reading the output](#reading-the-output)
39
+ - [How it works: the scansion pipeline](#how-it-works-the-scansion-pipeline)
40
+ - [Programmatic usage (API)](#programmatic-usage-api)
41
+ - [Examples](#examples)
42
+ - [Background and lineage](#background-and-lineage)
43
+ - [Scope, strengths, and honest limitations](#scope-strengths-and-honest-limitations)
44
+ - [Development](#development)
45
+ - [License and credits](#license-and-credits)
46
+
47
+ ---
48
+
49
+ ## Installation
50
+
51
+ Requires **Node.js ≥ 20** (≥ 22 recommended — one optional analysis stage, the
52
+ dependency-tree repair, needs Node's `require(esm)` support and degrades
53
+ gracefully without it).
54
+
55
+ ```bash
56
+ # As a global CLI tool
57
+ npm install -g calliope-ts
58
+
59
+ # Or as a library in your project
60
+ npm install calliope-ts
61
+ ```
62
+
63
+ From a clone of the repository:
64
+
65
+ ```bash
66
+ npm install
67
+ npm run build # compiles src/ → dist/
68
+ npm test # vitest suite
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Quick start (CLI)
74
+
75
+ ```bash
76
+ # 1. Scan a line directly
77
+ calliope-ts "Shall I compare thee to a summer's day?"
78
+
79
+ # 2. Scan a whole poem from a file (blank lines separate stanzas)
80
+ calliope-ts poem.txt
81
+
82
+ # 3. The Reading View — the poem itself, colour-coded by stress (recommended
83
+ # for whole poems; add -r or --reading)
84
+ calliope-ts --reading poem.txt
85
+
86
+ # 4. Pipe text in
87
+ cat poem.txt | calliope-ts --reading
88
+
89
+ # 5. Interactive menu (run with no arguments in a terminal)
90
+ calliope-ts
91
+ ```
92
+
93
+ The interactive menu offers: multi-line paste-and-scan (reading view),
94
+ single-line detailed analysis, line-by-line analysis, file input in either
95
+ view, and a **legend** explaining every symbol and colour.
96
+
97
+ There are two display modes:
98
+
99
+ | Mode | Flag | What you get |
100
+ |---|---|---|
101
+ | **Detailed view** | *(default)* | Per line: the tagged text, the phrase structure, lexical and relative stress maps, phonological bracketing, the foot-by-foot scansion, the dependency tree, and a summary (meter, fit %, scansion string). |
102
+ | **Reading view** | `--reading` / `-r` | The poem in its original formatting with every syllable coloured by stress, followed by one compact line per verse-line: stress map, meter, top-3 candidate scores, rhyme-scheme letter, and any rhythm/continuity notes. |
103
+
104
+ ---
105
+
106
+ ## Reading the output
107
+
108
+ ### The five stress levels
109
+
110
+ Calliope TS grades every syllable on a five-tier *relative* scale (this is the
111
+ core representation everything else is built on):
112
+
113
+ | Symbol | Name | Typical bearer |
114
+ |---|---|---|
115
+ | `x` | zero-provision | maximally reduced function words: *the, a, of, and, to* |
116
+ | `w` | weak | unstressed syllables of content words; unreduced function words (*be, just, here*) |
117
+ | `n` | low | lightly stressed syllables; pronouns and modals with citation stress (*he, might*) |
118
+ | `m` | moderate | secondary stresses; stressed syllables demoted by a neighbouring stronger one |
119
+ | `s` | strong | primary stresses, phrase peaks, line-final nuclei |
120
+
121
+ A line's **stress map** is simply its syllables in order, e.g.
122
+ `xs|wxm|wxs|xws` — with `|` marking foot boundaries, `‖` a strong caesura
123
+ (major punctuation pause), `¦` a lighter phrase break, and `-` a "silent beat"
124
+ inside a stress clash.
125
+
126
+ ### Meter lines
127
+
128
+ ```
129
+ S1L2 m|-mww|sxx|mwm|s amphibrachic tetrameter (dact 1.21 · amph 1.17 ·
130
+ anap 1.16) ≈ continuity; standalone: dactylic tetrameter A(perfect)
131
+ ```
132
+
133
+ - **`amphibrachic tetrameter`** — the line's meter: foot type + number of feet.
134
+ - **`(dact 1.21 · amph 1.17 · anap 1.16)`** — the top three candidate meters
135
+ with their raw fit scores. The named meter need not be the numerically
136
+ first candidate: ties between sibling meters are resolved by principled
137
+ criteria (word integrity, caesura alignment, stanza context — see below).
138
+ - **`≈ continuity; standalone: dactylic tetrameter`** — this line, taken
139
+ alone, fits dactylic a hair better; but the stanza is amphibrachic and the
140
+ line fits amphibrachic nearly as well, so the continuity reading is
141
+ promoted to the base reading and the standalone result is kept as a note.
142
+ - **`↔ aligns w/ stanza …`** — a weaker version of the same: the line stays
143
+ with its own meter but is flagged as compatible with the stanza's.
144
+ - **`♪ 4-beat accentual`** / `♪ 3-ictus dolnik` — a *rhythm note*: the stanza
145
+ is not accentual-syllabic at all but keeps a constant count of strong beats
146
+ with varying syllable counts (see "Beyond classical meters" below).
147
+ - **`A`, `B(perfect)`, `·`** — the rhyme-scheme letter for the line's end
148
+ word, with the rhyme type when the line rhymes with an earlier one
149
+ (`perfect`, `rich`, `family`, `assonant`, `consonant`, `augmented`,
150
+ `diminished`, `wrenched`, `eye`, `identical`). `·` = unrhymed.
151
+ - **`❡ ballad stanza (ABCB, 4·3)`** — a stanza-level *form* verdict (shown in
152
+ the stanza header): ballad stanza, blank verse, couplet, limerick,
153
+ Shakespearean/Petrarchan sonnet, terza rima, etc.
154
+ - **Certainty / fit %** (detailed view) — the share of the line realised by
155
+ clean, unsubstituted feet, tempered by phrase-edge agreement. A perfectly
156
+ regular line reads 100%; real verse usually lands between 50 and 90.
157
+
158
+ ---
159
+
160
+ ## How it works: the scansion pipeline
161
+
162
+ Most automatic scanners pattern-match syllable counts against meter templates.
163
+ Calliope TS instead follows the **phonological scansion** method: derive the
164
+ line's natural spoken prominence first, then fit meters to it. The pipeline
165
+ has eight stages.
166
+
167
+ **1. Parsing (who does what in the sentence).**
168
+ The line is tokenized, part-of-speech tagged, and dependency-parsed (which
169
+ word grammatically governs which) using the FinNLP family of libraries
170
+ (`lexed`, `en-pos`, `en-parse`). Two correction layers sit inside this stage,
171
+ because verse breaks taggers in predictable ways: a *tag-repair* pass fixes
172
+ systematic errors before the dependency tree is built (archaic forms like
173
+ *thou/thy/doth/wherefore*, the pronoun *I*, perfect-tense participles like
174
+ *had quit*), and a *tree-repair* pass (built on the
175
+ [depedits](https://www.npmjs.com/package/depedits) rule engine) fixes
176
+ systematic attachment errors after it (e.g. noun compounds parsed as double
177
+ objects). Hyphenated compounds and contractions (*we'll*, *don't*, archaic
178
+ *fix'd*) are re-merged into single metrical words.
179
+
180
+ **2. Lexical stress (how each word is pronounced).**
181
+ Every word is looked up in an augmented CMU pronouncing dictionary
182
+ ([nounsing-pro](https://www.npmjs.com/package/nounsing-pro)): syllable count,
183
+ stress pattern (primary / secondary / unstressed), syllable weights, and
184
+ vowel quantities. Words not in the dictionary go through a morphological
185
+ fallback (strip a productive suffix, look up the stem, restore) and finally a
186
+ quantity-sensitive English Stress Rule. Poetic elisions are honoured:
187
+ *heav'n* is one syllable, *o'er* one, *th'expense* two, *'tis/'twas* reduce,
188
+ and archaic *-'d* / *-'st* forms (*fix'd*, *stopp'st*) keep their elided
189
+ syllable counts.
190
+
191
+ **3. The prosodic hierarchy (how the line phrases).**
192
+ Words are grouped the way speech groups them: each content word attracts its
193
+ function-word satellites into a **clitic group** (CP); clitic groups join into
194
+ **phonological phrases** (PP) along the syntactic dependencies; phrases join
195
+ into **intonational units** (IU) bounded by major punctuation. Crucially, the
196
+ **line is the scansion domain**: a verse line containing several grammatical
197
+ sentences is still ONE metrical unit (the internal full stops become strong
198
+ caesurae), and quotation marks are *not* treated as prosodic breaks.
199
+
200
+ **4. Phrase-level stress rules.**
201
+ Three classic rules adjust the dictionary stresses in context: the **compound
202
+ rule** (left element stressed: *CITY hall*), the **nuclear stress rule** (the
203
+ last content word of an intonational unit gets the phrase's peak), and a set
204
+ of **clash resolutions** (two adjacent strong syllables cannot both keep full
205
+ prominence — one yields, chosen by syntactic direction). The result is mapped
206
+ onto the five-tier `x w n m s` scale.
207
+
208
+ **5. Key stresses, "beginnings free, endings strict."**
209
+ Following McAleese's central insight, the stresses at the *right edges* of
210
+ prosodic units are the reliable ones — speakers may start a phrase loosely
211
+ but they land its ending. Each unit contributes its right-edge "key stress"
212
+ with a weight (intonational unit > phrase > long word > clitic group), and
213
+ meters that place beats on those key stresses are rewarded.
214
+
215
+ **6. Meter fitting (dynamic programming).**
216
+ For each of seven candidate meters — iambic, trochaic, anapestic, dactylic,
217
+ **amphibrachic**, bacchic, spondaic — the engine finds the optimal division of
218
+ the line into feet, using that meter's full inventory of legitimate
219
+ variations: inversions at line-start or after a caesura, pyrrhic and spondaic
220
+ substitutions, catalexis (truncated final foot), anacrusis (extrametrical
221
+ upbeat), feminine endings, acephalous openings. Each syllable is scored
222
+ against the metrical position it lands in (a strong syllable in a beat slot
223
+ is ideal; a reduced clitic forced onto a beat is the cardinal violation), and
224
+ an **Attridge promotion** rule lets a weak syllable carry a beat when it is
225
+ flanked by even weaker ones (*"happens to BE a French poet"*) — which is how
226
+ real readers actually deliver such lines.
227
+
228
+ **7. Arbitration and context.**
229
+ The winning meter is not just the top raw score:
230
+ - **Ternary siblings** (anapest / amphibrach / dactyl) frequently fit a line
231
+ with the *identical* beat pattern — then the name is decided by which
232
+ division avoids splitting words across feet and aligns with the line's
233
+ pauses, not by score noise.
234
+ - **Stanza consensus**: each stanza's dominant meter is identified (for
235
+ ternary verse, by the stanza's *anacrusis profile* — how many slack
236
+ syllables open the lines, following Russian metrics). A line that fits the
237
+ dominant meter nearly as well as its own standalone winner **adopts the
238
+ dominant meter** as its base reading, with the standalone reading preserved
239
+ as a note. Metrical continuity outranks a hair of fit score, exactly as it
240
+ does for a human scander.
241
+
242
+ **8. Beyond classical meters.**
243
+ If a stanza's syllable counts vary while its strong-beat counts stay constant
244
+ — and no classical meter dominates — the stanza is read as **accentual verse**
245
+ (`4-beat accentual`), or more precisely as a **dolnik** (1–2 slack syllables
246
+ between beats) or **taktovik** (1–3), the categories of Russian verse theory
247
+ that also fit much English song, ballad, and modernist verse. Free verse is
248
+ recognised both line-by-line and stanza-wide. Independently, a **rhyme
249
+ layer** classifies every line-end pair (perfect / rich / family / slant /
250
+ eye / … rhymes, masculine / feminine / dactylic), detects the rhyme scheme,
251
+ and a **form layer** names what the stanzas amount to: ballad stanza (rhyme
252
+ scheme *and* alternating 4·3 beats), blank verse (unrhymed iambic
253
+ pentameter), couplets, quatrains, limerick, rhyme royal, Shakespearean or
254
+ Petrarchan sonnet, terza rima.
255
+
256
+ ---
257
+
258
+ ## Programmatic usage (API)
259
+
260
+ ```ts
261
+ import {
262
+ analyzeStanzas, // poem text → LineResult[][] (per stanza, per line)
263
+ analyzeText, // poem text → LineResult[] (flat)
264
+ analyzeReadingDocument, // poem text → ReadingStanza[] (keeps verbatim lines)
265
+ } from 'calliope-ts';
266
+ ```
267
+
268
+ ### Scan a poem
269
+
270
+ ```ts
271
+ import { analyzeStanzas } from 'calliope-ts';
272
+
273
+ const poem = `He happens to be a French poet, that thin,
274
+ book-carrying man with a bristly gray chin;
275
+ you meet him whenever you go`;
276
+
277
+ const stanzas = analyzeStanzas(poem);
278
+
279
+ for (const stanza of stanzas) {
280
+ for (const line of stanza) {
281
+ const d = line.phonologicalScansion;
282
+ console.log(d.meter); // "amphibrachic tetrameter"
283
+ console.log(d.scansion); // "nsw|xwx|msw|xs"
284
+ console.log(d.footCount); // 4
285
+ console.log(d.certainty); // 0–100
286
+ console.log(d.ranking); // [{ meter: 'amphibrachic', score: 1.20 }, …]
287
+ console.log(d.standaloneMeter);// set when stanza continuity renamed the line
288
+ console.log(d.rhythmNote); // "4-beat accentual", "3-ictus dolnik", …
289
+ console.log(d.rhyme); // { endWord, letter: 'A', type: 'perfect', … }
290
+ console.log(d.formNote); // "ballad stanza (ABCB, 4·3)", "blank verse", …
291
+ }
292
+ }
293
+ ```
294
+
295
+ ### Inspect the linguistic analysis
296
+
297
+ Each `LineResult` also carries the full intermediate analysis:
298
+
299
+ ```ts
300
+ const line = stanzas[0][0];
301
+
302
+ // Words with POS tags, content/function status, and per-syllable stress
303
+ for (const w of line.sentence.words) {
304
+ console.log(w.word, w.lexicalClass, w.isContent,
305
+ w.syllables.map(s => s.relativeStress).join(''));
306
+ }
307
+
308
+ // The dependency tree
309
+ for (const dep of line.sentence.dependencies) {
310
+ console.log(`${dep.dependentName} ←${dep.dependentType}← ${dep.governorName}`);
311
+ }
312
+
313
+ // The prosodic hierarchy: IU → PP → clitic groups
314
+ console.log(line.phonologicalHierarchy);
315
+
316
+ // The weighted key stresses extracted from unit right-edges
317
+ console.log(line.keyStresses);
318
+ ```
319
+
320
+ ### Rhyme utilities
321
+
322
+ The rhyme classifier is exported on its own:
323
+
324
+ ```ts
325
+ import { classifyRhymePair, detectScheme } from 'calliope-ts/dist/rhyme.js';
326
+
327
+ classifyRhymePair('grace', 'face');
328
+ // → { type: 'perfect', structure: 'masculine' }
329
+
330
+ classifyRhymePair('picky', 'tricky');
331
+ // → { type: 'perfect', structure: 'feminine' }
332
+
333
+ detectScheme(['Mariner', 'three', 'eye', 'me']).map(r => r.letter).join('');
334
+ // → "·A·A" (i.e. ABCB with unrhymed lines marked ·)
335
+ ```
336
+
337
+ ### Key result types
338
+
339
+ ```ts
340
+ interface PhonologicalScansionDetail {
341
+ meter: string; // "iambic pentameter"
342
+ meterName: MetreName | 'free verse';
343
+ footCount: number;
344
+ scansion: string; // "xs|wxm|wxs|xws"
345
+ certainty: number; // 0–100
346
+ ranking?: MeterScore[]; // all candidate meters, best first
347
+ consensusMeter?: string; // "aligns with stanza X" annotation
348
+ standaloneMeter?: string; // pre-continuity-rename reading
349
+ rhythmNote?: string; // dolnik / taktovik / accentual verdicts
350
+ rhyme?: { endWord: string; letter: string; type?: string; matchedLine?: number };
351
+ formNote?: string; // stanza/poem form verdict
352
+ // … plus the raw weighted-score fields
353
+ }
354
+ ```
355
+
356
+ Lower-level functions (`parseDocument`, `assignLexicalStress`,
357
+ `buildPhonologicalHierarchy`, `scoreMeters`, …) are exported from their
358
+ modules under `calliope-ts/dist/*` for users who want to run or modify
359
+ individual pipeline stages. The optional Scandroid comparison engines
360
+ (Charles Hartman's "Corral the Weird" and "Maximize the Normal") are exported
361
+ from `calliope-ts/dist/scandroid.js`.
362
+
363
+ ---
364
+
365
+ ## Examples
366
+
367
+ **A Shakespeare sonnet** (`calliope-ts --reading sonnet130.txt`) — every line
368
+ identified as iambic pentameter; the scheme letters spell ABAB CDCD EFEF GG;
369
+ the stanza header reads `❡ Shakespearean Sonnet`.
370
+
371
+ **A ballad quatrain:**
372
+
373
+ ```
374
+ It is an ancient Mariner,
375
+ And he stoppeth one of three.
376
+ "By thy long grey beard and glittering eye,
377
+ Now wherefore stopp'st thou me?
378
+ ```
379
+
380
+ → lines of iambic tetrameter / trimeter, scheme `·A·A`, and the form verdict
381
+ `❡ ballad stanza (ABCB, 4·3)` — the rhyme scheme *and* the alternating
382
+ 4-beat/3-beat design both check out.
383
+
384
+ **Accentual verse** (Wyatt, *They flee from me*) — no classical meter
385
+ dominates and syllable counts vary, but every line carries four strong beats:
386
+ each line is annotated `♪ 4-beat accentual`.
387
+
388
+ **Amphibrachic verse** (Nabokov wrote his poem "Exile" as a demonstration
389
+ of English amphibrachs — the `x S x` foot): stanza consensus reads the poem's
390
+ constant one-syllable anacrusis as amphibrachic, names near-tie lines
391
+ accordingly with `≈ continuity` notes, and reports the `aabccb` rhyme
392
+ envelope.
393
+
394
+ **Blank verse** (Frost, *Mending Wall*) — unrhymed lines, dominant iambic
395
+ pentameter: `❡ blank verse`.
396
+
397
+ ---
398
+
399
+ ## Background and lineage
400
+
401
+ **Calliope (2008).** The method implemented here originates with Gareth
402
+ McAleese's *Calliope*, developed for his M.Sc. at the Open University
403
+ (*"Improving Scansion with Syntax: an Investigation into the Effectiveness of
404
+ a Syntactic Analysis of Poetry by Computer using Phonological Scansion
405
+ Theory"*, Technical Report 2007/26, submitted 2008). McAleese's central idea:
406
+ scansion should be computed from a line's *phonology* — the prosodic
407
+ hierarchy and the stresses at the right edges of its units ("beginnings free,
408
+ endings strict") — rather than from orthographic pattern-matching. Calliope TS
409
+ re-implements that architecture on a modern Node.js stack and extends it.
410
+
411
+ **Scandroid (1996/2005).** Charles O. Hartman's Scandroid, a classic
412
+ foot-by-foot scanner (GNU GPL), is included as an optional comparison engine:
413
+ its "Corral the Weird" and "Maximize the Normal" algorithms can be run side
414
+ by side with the phonological scansion.
415
+
416
+ **Extensions beyond McAleese.** The five-tier stress gradient with a
417
+ zero-provision level; a unified dynamic-programming meter fitter with full
418
+ substitution inventories (and first-class **amphibrach** support); beat
419
+ promotion after Derek Attridge's beat/offbeat analysis; the dolnik/taktovik
420
+ classification from Russian verse theory (Gasparov); stanza-consensus and
421
+ anacrusis-profile naming; the rhyme and form layers; and the parse-correction
422
+ layers for verse-specific tagging.
423
+
424
+ **Data.** Lexical phonology comes from the augmented CMU dictionary published
425
+ as `nounsing-pro` (syllabification, stress transcription, syllable structure,
426
+ morphological stress classes). Parsing uses the FinNLP family. Dependency
427
+ repairs use `depedits`, a TypeScript port of the DepEdit rule engine.
428
+
429
+ Calliope TS is developed by **Aleksey Calvin** /
430
+ [SilverAgePoets.com](https://www.SilverAgePoets.com), with particular care for
431
+ verse traditions that standard English-centric tools marginalize: ternary
432
+ meters, Russian Silver-Age forms in translation, song lyrics, and accentual
433
+ verse.
434
+
435
+ ---
436
+
437
+ ## Scope, strengths, and honest limitations
438
+
439
+ **Strengths.** Line-level stress mapping is the core competence — the Reading
440
+ View is usable as a reading aid in itself. Ternary meters (anapest /
441
+ amphibrach / dactyl) are first-class citizens, not afterthoughts. Accentual
442
+ and dolnik verse get a real classification instead of a "free verse" shrug.
443
+ Whole-poem context (stanza consensus, rhyme, form) informs line verdicts the
444
+ way it does for human readers.
445
+
446
+ **Limitations to know about.**
447
+ - English only (the dictionary and parser are English; other languages will
448
+ produce nonsense rather than errors).
449
+ - Stress-doublet words (*rebel*, *content*, names like *Hugo*) are read with
450
+ their dictionary stress; a rhyme-driven correction is on the roadmap.
451
+ - Rare or foreign proper names fall back to rule-based stress guesses.
452
+ - The form layer recognises the common stanza forms; it does not yet attempt
453
+ villanelle/pantoum repetition checking or syllabic forms.
454
+ - Meter identification on single lines without context is genuinely harder
455
+ than with stanza context — when possible, scan whole poems.
456
+
457
+ ---
458
+
459
+ ## Development
460
+
461
+ ```bash
462
+ npm run build # tsc → dist/
463
+ npm test # vitest (73 tests: pipeline, stress, meters, rhyme, forms)
464
+ npm run dev # ts-node
465
+
466
+ # Benchmark harnesses (require the annotated corpora in tests/)
467
+ node trials/mcaleese_benchmark.mjs # McAleese's own trial poems + expert keys
468
+ node trials/corpus_benchmark.mjs # litlab / prosodic / epg64 meter corpora
469
+ ```
470
+
471
+ The repository's `AGENTS.md` carries a dated engineering log of every
472
+ substantive change with its rationale and verification.
473
+
474
+ ---
475
+
476
+ ## License and credits
477
+
478
+ Apache-2.0. © Aleksey Calvin Tsukanov / SilverAgePoets.com.
479
+
480
+ Methodological debts: Gareth McAleese (Calliope, the phonological scansion
481
+ procedure); Charles O. Hartman (Scandroid); Derek Attridge (beat/offbeat
482
+ rhythm and the English dolnik); M. L. Gasparov (dolnik/taktovik taxonomy);
483
+ Bruce Hayes, Paul Kiparsky, and the generative-metrics tradition (stress
484
+ theory, syllable quantity, extrametricality); the CMU Pronouncing Dictionary
485
+ and its `nounsing-pro` augmentation.
@@ -0,0 +1,13 @@
1
+ interface FinDepNode {
2
+ label: string;
3
+ type: string;
4
+ parent: number;
5
+ }
6
+ /**
7
+ * Repair systematic en-parse attachment errors. Returns a new deps array
8
+ * (same shape as en-parse's `toArray` output); on any failure returns the
9
+ * input unchanged.
10
+ */
11
+ export declare function applyDepFixes(tokens: string[], tags: string[], deps: FinDepNode[]): FinDepNode[];
12
+ export {};
13
+ //# sourceMappingURL=depfix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"depfix.d.ts","sourceRoot":"","sources":["../src/depfix.ts"],"names":[],"mappings":"AAiBA,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAqCD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAwBhG"}
package/dist/depfix.js ADDED
@@ -0,0 +1,84 @@
1
+ // depfix.ts — Post-parse dependency repair via DepEdit rules (the `depedits`
2
+ // npm package, the maintainer's TypeScript port of DepEdit).
3
+ //
4
+ // Runs AFTER en-parse, complementing the pre-parse tag corrections in
5
+ // tagfix.ts: tagfix repairs what the tagger got wrong before the tree is
6
+ // built; this pass repairs systematic attachment errors en-parse makes even
7
+ // with correct tags. Rules are written in DepEdit's declarative format
8
+ // (definitions ⟶ relations ⟶ actions, tab-separated) over en-parse's own
9
+ // label space (DOBJ/NSUBJ/DEP/…), so the round-trip is lossless and every
10
+ // rule is independently testable.
11
+ //
12
+ // The rule set is deliberately small and evidence-based — each rule cites the
13
+ // observed failure it corrects. `depedits` is ESM-only; it is loaded lazily
14
+ // and failures degrade gracefully (the unrepaired parse is still a parse).
15
+ import { createRequire } from 'module';
16
+ // Observed failure (probe: "I had quit the programming paradigm"): en-parse
17
+ // attaches BOTH nouns of a noun compound to the verb as parallel objects
18
+ // ("programming ←DOBJ← quit", "paradigm ←DOBJ← quit"), and leaves the
19
+ // determiner dangling on the first noun as generic DEP. The repairs:
20
+ // 1. Two adjacent common nouns sharing a governor with the same object
21
+ // relation → the first is a compound modifier (AMOD) of the second.
22
+ // 2. A determiner left as DEP on a noun that has become a modifier →
23
+ // re-attach it as DET to that noun's head (the true NP head).
24
+ const CALLIOPE_DEP_FIXES = [
25
+ 'xpos=/NNS?/&func=/DOBJ|IOBJ/;xpos=/NNS?/&func=/DOBJ|IOBJ/;xpos=/VB.*/\t#3>#1;#3>#2;#1.#2\t#2>#1;#1:func=AMOD',
26
+ 'xpos=/DT/&func=/DEP|EXT/;xpos=/NNS?/&func=/AMOD/;xpos=/NNS?.*/\t#2>#1;#3>#2\t#3>#1;#1:func=DET',
27
+ ].join('\n');
28
+ let engine;
29
+ function loadEngine() {
30
+ if (engine !== undefined)
31
+ return engine;
32
+ try {
33
+ // This package compiles to ESM, where bare `require` does not exist, and
34
+ // the parse path is synchronous, so dynamic import() is not an option:
35
+ // createRequire gives a sync loader, and since `depedits` is itself
36
+ // ESM-only this resolves via Node's require(esm) (≥20.17 / ≥22.12). On
37
+ // older runtimes it throws and the repair pass degrades to a no-op (the
38
+ // unrepaired parse is still a parse).
39
+ const req = createRequire(import.meta.url);
40
+ const { DepEditEngine } = req('depedits');
41
+ const e = new DepEditEngine();
42
+ e.loadIniString(CALLIOPE_DEP_FIXES);
43
+ engine = e;
44
+ }
45
+ catch {
46
+ engine = null;
47
+ }
48
+ return engine;
49
+ }
50
+ /**
51
+ * Repair systematic en-parse attachment errors. Returns a new deps array
52
+ * (same shape as en-parse's `toArray` output); on any failure returns the
53
+ * input unchanged.
54
+ */
55
+ export function applyDepFixes(tokens, tags, deps) {
56
+ const e = loadEngine();
57
+ if (!e || tokens.length === 0 || deps.length !== tokens.length)
58
+ return deps;
59
+ try {
60
+ const conllu = tokens.map((tok, i) => {
61
+ const head = deps[i].parent >= 0 ? deps[i].parent + 1 : 0;
62
+ const safe = tok.replace(/\s/g, '_') || '_';
63
+ return `${i + 1}\t${safe}\t${safe}\t_\t${tags[i] || '_'}\t_\t${head}\t${deps[i].label || 'DEP'}\t_\t_`;
64
+ }).join('\n') + '\n\n';
65
+ const out = e.process(conllu);
66
+ const fixed = deps.map(d => ({ ...d }));
67
+ for (const row of out.split('\n')) {
68
+ const cols = row.split('\t');
69
+ if (cols.length < 10)
70
+ continue;
71
+ const idx = parseInt(cols[0], 10) - 1;
72
+ if (!(idx >= 0 && idx < fixed.length))
73
+ continue;
74
+ const head = parseInt(cols[6], 10);
75
+ fixed[idx].parent = Number.isFinite(head) ? head - 1 : fixed[idx].parent;
76
+ if (cols[7] && cols[7] !== '_')
77
+ fixed[idx].label = cols[7];
78
+ }
79
+ return fixed;
80
+ }
81
+ catch {
82
+ return deps;
83
+ }
84
+ }
@@ -0,0 +1,38 @@
1
+ import { ClsWord, LineResult } from './types.js';
2
+ export declare function renderUnifiedDisplay(result: LineResult, rawLine?: string): string;
3
+ export declare function renderLegend(): string;
4
+ /**
5
+ * The long-form legend triggered from the main menu's "Display Legend" option:
6
+ * the compact legend PLUS full Penn POS-tag and grammatical-dependency glossaries.
7
+ * (These glossaries are deliberately NOT part of the compact in-output legend.)
8
+ */
9
+ export declare function renderFullLegend(): string;
10
+ /** One input line with its (1+) parsed sentence results. */
11
+ export interface ReadingLine {
12
+ raw: string;
13
+ results: LineResult[];
14
+ }
15
+ /** A stanza: a run of consecutive non-blank input lines. */
16
+ export interface ReadingStanza {
17
+ lines: ReadingLine[];
18
+ }
19
+ /**
20
+ * Project per-syllable stress colours back onto the original line, preserving
21
+ * capitalisation, punctuation, spacing and any extrametrical fragments the
22
+ * pipeline dropped (e.g. possessive "'s"). Word-like tokens are coloured;
23
+ * everything between them (spaces, punctuation, dashes) is emitted verbatim.
24
+ *
25
+ * Alignment is tolerant: it matches each token to the next parsed word by
26
+ * normalised form (equal, or token starts with the word — handling "cat's"),
27
+ * with a small look-ahead resync so a stray/unsyllabified token never derails
28
+ * the rest of the line. No original character is ever dropped.
29
+ */
30
+ export declare function projectStressOntoLine(rawLine: string, words: ClsWord[]): string;
31
+ /**
32
+ * Reading view: the poem itself in its original formatting, each syllable
33
+ * coloured by 4-tier relative stress, followed by a same-structure block of
34
+ * per-line stress maps + meter (with top-3 fit scores). This is the whole
35
+ * output for this mode — not the full per-line analytic dump.
36
+ */
37
+ export declare function renderReadingView(stanzas: ReadingStanza[]): string;
38
+ //# sourceMappingURL=display.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display.d.ts","sourceRoot":"","sources":["../src/display.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,OAAO,EAMP,UAAU,EAGX,MAAM,YAAY,CAAC;AAqJpB,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAkPjF;AAMD,wBAAgB,YAAY,IAAI,MAAM,CAUrC;AAuHD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAsBzC;AAqCD,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAuC/E;AAmMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,CAsDlE"}