calliope-ts 0.0.2 → 0.0.4

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 (160) hide show
  1. package/README.md +32 -20
  2. package/dist/caesura.d.ts +33 -0
  3. package/dist/caesura.d.ts.map +1 -0
  4. package/dist/caesura.js +202 -0
  5. package/dist/calliope/boundaries.d.ts +19 -0
  6. package/dist/calliope/boundaries.d.ts.map +1 -0
  7. package/dist/calliope/boundaries.js +182 -0
  8. package/dist/calliope/bracketing.d.ts +11 -0
  9. package/dist/calliope/bracketing.d.ts.map +1 -0
  10. package/dist/calliope/bracketing.js +416 -0
  11. package/dist/calliope/deps.d.ts +4 -0
  12. package/dist/calliope/deps.d.ts.map +1 -0
  13. package/dist/calliope/deps.js +181 -0
  14. package/dist/calliope/engine.d.ts +3 -0
  15. package/dist/calliope/engine.d.ts.map +1 -0
  16. package/dist/calliope/engine.js +71 -0
  17. package/dist/calliope/feats.d.ts +9 -0
  18. package/dist/calliope/feats.d.ts.map +1 -0
  19. package/dist/calliope/feats.js +45 -0
  20. package/dist/calliope/names.d.ts +7 -0
  21. package/dist/calliope/names.d.ts.map +1 -0
  22. package/dist/calliope/names.js +42 -0
  23. package/dist/calliope/postag.d.ts +8 -0
  24. package/dist/calliope/postag.d.ts.map +1 -0
  25. package/dist/calliope/postag.js +250 -0
  26. package/dist/calliope/prosodic.d.ts +3 -0
  27. package/dist/calliope/prosodic.d.ts.map +1 -0
  28. package/dist/calliope/prosodic.js +275 -0
  29. package/dist/calliope/relstress.d.ts +4 -0
  30. package/dist/calliope/relstress.d.ts.map +1 -0
  31. package/dist/calliope/relstress.js +688 -0
  32. package/dist/calliope/stressrules.d.ts +28 -0
  33. package/dist/calliope/stressrules.d.ts.map +1 -0
  34. package/dist/calliope/stressrules.js +147 -0
  35. package/dist/calliope/syntax.d.ts +38 -0
  36. package/dist/calliope/syntax.d.ts.map +1 -0
  37. package/dist/calliope/syntax.js +234 -0
  38. package/dist/calliope/udpos.d.ts +13 -0
  39. package/dist/calliope/udpos.d.ts.map +1 -0
  40. package/dist/calliope/udpos.js +156 -0
  41. package/dist/clio/caesura.d.ts +27 -0
  42. package/dist/clio/caesura.d.ts.map +1 -0
  43. package/dist/clio/caesura.js +148 -0
  44. package/dist/clio/depfix.d.ts +13 -0
  45. package/dist/clio/depfix.d.ts.map +1 -0
  46. package/dist/clio/depfix.js +84 -0
  47. package/dist/clio/display.d.ts +32 -0
  48. package/dist/clio/display.d.ts.map +1 -0
  49. package/dist/clio/display.js +976 -0
  50. package/dist/clio/engine.d.ts +3 -0
  51. package/dist/clio/engine.d.ts.map +1 -0
  52. package/dist/clio/engine.js +28 -0
  53. package/dist/clio/parser.d.ts +10 -0
  54. package/dist/clio/parser.d.ts.map +1 -0
  55. package/dist/clio/parser.js +696 -0
  56. package/dist/clio/phonological.d.ts +41 -0
  57. package/dist/clio/phonological.d.ts.map +1 -0
  58. package/dist/clio/phonological.js +788 -0
  59. package/dist/clio/phrasestress.d.ts +6 -0
  60. package/dist/clio/phrasestress.d.ts.map +1 -0
  61. package/dist/clio/phrasestress.js +106 -0
  62. package/dist/clio/pipeline.d.ts +11 -0
  63. package/dist/clio/pipeline.d.ts.map +1 -0
  64. package/dist/clio/pipeline.js +147 -0
  65. package/dist/clio/rhyme.d.ts +65 -0
  66. package/dist/clio/rhyme.d.ts.map +1 -0
  67. package/dist/clio/rhyme.js +761 -0
  68. package/dist/clio/scandroid.d.ts +17 -0
  69. package/dist/clio/scandroid.d.ts.map +1 -0
  70. package/dist/clio/scandroid.js +435 -0
  71. package/dist/clio/scansion.d.ts +46 -0
  72. package/dist/clio/scansion.d.ts.map +1 -0
  73. package/dist/clio/scansion.js +1086 -0
  74. package/dist/clio/semantics.d.ts +44 -0
  75. package/dist/clio/semantics.d.ts.map +1 -0
  76. package/dist/clio/semantics.js +139 -0
  77. package/dist/clio/stress.d.ts +83 -0
  78. package/dist/clio/stress.d.ts.map +1 -0
  79. package/dist/clio/stress.js +1737 -0
  80. package/dist/clio/tagfix.d.ts +6 -0
  81. package/dist/clio/tagfix.d.ts.map +1 -0
  82. package/dist/clio/tagfix.js +101 -0
  83. package/dist/display.d.ts +0 -6
  84. package/dist/display.d.ts.map +1 -1
  85. package/dist/display.js +322 -141
  86. package/dist/engine.d.ts +9 -0
  87. package/dist/engine.d.ts.map +1 -0
  88. package/dist/engine.js +12 -0
  89. package/dist/index.d.ts +4 -3
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +169 -26
  92. package/dist/parser.d.ts +3 -3
  93. package/dist/parser.d.ts.map +1 -1
  94. package/dist/parser.js +320 -563
  95. package/dist/phonological.d.ts +1 -1
  96. package/dist/phonological.d.ts.map +1 -1
  97. package/dist/phonological.js +73 -4
  98. package/dist/phrasestress.d.ts +6 -0
  99. package/dist/phrasestress.d.ts.map +1 -0
  100. package/dist/phrasestress.js +106 -0
  101. package/dist/rhyme.d.ts +40 -1
  102. package/dist/rhyme.d.ts.map +1 -1
  103. package/dist/rhyme.js +435 -7
  104. package/dist/scansion.d.ts +9 -0
  105. package/dist/scansion.d.ts.map +1 -1
  106. package/dist/scansion.js +145 -18
  107. package/dist/semantics.d.ts +44 -0
  108. package/dist/semantics.d.ts.map +1 -0
  109. package/dist/semantics.js +139 -0
  110. package/dist/stress.d.ts +127 -4
  111. package/dist/stress.d.ts.map +1 -1
  112. package/dist/stress.js +834 -61
  113. package/dist/types.d.ts +15 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/package.json +5 -3
  116. package/src/caesura.ts +201 -0
  117. package/src/calliope/boundaries.ts +190 -0
  118. package/src/calliope/bracketing.ts +390 -0
  119. package/src/calliope/deps.ts +160 -0
  120. package/src/calliope/engine.ts +77 -0
  121. package/src/calliope/feats.ts +46 -0
  122. package/src/calliope/names.ts +44 -0
  123. package/src/calliope/postag.ts +253 -0
  124. package/src/calliope/prosodic.ts +262 -0
  125. package/src/calliope/relstress.ts +645 -0
  126. package/src/calliope/stressrules.ts +147 -0
  127. package/src/calliope/syntax.ts +218 -0
  128. package/src/calliope/udpos.ts +152 -0
  129. package/src/calliope_src_contents.md +19049 -0
  130. package/src/clio/caesura.ts +145 -0
  131. package/src/clio/depfix.ts +88 -0
  132. package/src/clio/display.ts +1042 -0
  133. package/src/clio/engine.ts +38 -0
  134. package/src/clio/parser.ts +845 -0
  135. package/src/clio/phonological.ts +849 -0
  136. package/src/clio/phrasestress.ts +108 -0
  137. package/src/clio/pipeline.ts +154 -0
  138. package/src/clio/rhyme.ts +740 -0
  139. package/src/clio/scandroid.ts +434 -0
  140. package/src/clio/scansion.ts +1130 -0
  141. package/src/clio/semantics.ts +134 -0
  142. package/src/clio/stress.ts +1731 -0
  143. package/src/clio/tagfix.ts +104 -0
  144. package/src/display.ts +321 -137
  145. package/src/engine.ts +22 -0
  146. package/src/index.ts +176 -32
  147. package/src/parser.ts +346 -682
  148. package/src/phonological.ts +72 -4
  149. package/src/phrasestress.ts +108 -0
  150. package/src/rhyme.ts +428 -8
  151. package/src/scansion.ts +136 -15
  152. package/src/semantics.ts +134 -0
  153. package/src/stress.ts +794 -64
  154. package/src/types.ts +39 -4
  155. package/tests/DataForHayesLinesOnly.txt +364 -0
  156. package/tests/DataForHayesStressSymbolsRevised.txt +728 -0
  157. package/tests/basic.test.ts +443 -37
  158. package/tests/bench-hayes.mjs +72 -0
  159. package/tests/wagner-stress.test.ts +188 -0
  160. package/vitest.config.ts +0 -15
package/README.md CHANGED
@@ -1,18 +1,8 @@
1
- # Calliope TS
1
+ # Calliope TS 0.0.4
2
2
 
3
- **Calliope TS** is a phonological poetry scansion toolkit for TypeScript / Node.js.
3
+ **Calliope TS** is a phonological poetry-scansion toolkit for TypeScript / Node.js. Hand it a poem (or a single line), and it will tell you — and *show* you — how that verse actually moves: which syllables carry weight and how much, where the beats fall, what meter each line is in and how confidently, what rhymes with what, and what form the stanzas add up to.
4
4
 
5
- Its name, of course, recalls a muse.
6
- Hand to this muse a song or verse,
7
- And she might show you how it curves,
8
- Which syllables hold weight in time,
9
- How much of it, and where a rhyme,
10
- Which meter dances through each line,
11
- And where should land each beat
12
- Your voice might drawl, or music drawn...
13
- She even knows poetic form!
14
-
15
- The premise of this toolkit is that rhythm, and therefore also meter, is much more reliably *recovered from pronunciation* than from text as such. What?! But there's no Text-to-Speech here?! (Sorry!) A lapse? A paradox? Nah. Instead, Calliope parses a stanza or verse, reproducing roughly how an attentive (but silent) reader might do so: first, working out how the words are likely to be said aloud — deduced by following contextual patterns, cohering a syntax, wielding phonotactics, evading constraints, gathering prosody, accounting for the ways phrases clump or bounce, for how a voice might subtly perk over clause-ends, and all the rest of it; and only thereafter scanning for what sort of rhythms a phrase such as that might be spectrally reaching for, or settling into. The scholars dwelling on Generative Metrics, a liminal little field somewhere in-between linguistics, phonetics, and literary studies, call this sort of approach **phonological scansion**, and the bulk of this README is about what that means and how the toolkit puts it to work.
5
+ The premise is that meter is something you *recover from pronunciation*, not something you stamp onto spelling. So Calliope reads a line roughly the way an attentive reader does: it first works out how the words would be said aloud — dictionary stress, the way phrases clump, where the voice peaks at a clause-end — and only then looks for the regular beat that saying settles into. This approach has a name in the linguistics literature, **phonological scansion**, and the bulk of this README is about what that means and how the toolkit puts it to work.
16
6
 
17
7
  ```
18
8
  $ calliope-ts --reading exile.txt
@@ -89,11 +79,15 @@ cat poem.txt | calliope-ts --reading
89
79
 
90
80
  # 5. Interactive menu (run without typing arguments in a terminal each time)
91
81
  calliope-ts
82
+
83
+ # 6. Run the alternative "Clio" parse (powered by the legacy FinNLP suite)
84
+ calliope-ts --clio poem.txt
92
85
  ```
93
86
 
94
87
  The interactive menu offers: multi-line paste-and-scan (reading view),
95
88
  single-line detailed analysis, line-by-line analysis, file input in either
96
- view, and a **legend** explaining every symbol and colour.
89
+ view, a **legend** explaining every symbol and colour, and an option to
90
+ "Ask Clio instead" for the alternative parse.
97
91
 
98
92
  There are two display modes:
99
93
 
@@ -178,7 +172,7 @@ Bruce Hayes himself (2005) and Gareth McAleese (2007/2008) built the first faith
178
172
  The current pipeline has eight stages.
179
173
 
180
174
  **1. Grammatical parsing.**
181
- The line is tokenized, part-of-speech tagged, and dependency-parsed — that is, the engine works out which word grammatically governs which (subject of what verb, object of what preposition) — using the FinNLP family of libraries (`lexed`, `en-pos`, `en-parse`). Two correction layers sit inside this stage,
175
+ The line is tokenized, part-of-speech tagged, and dependency-parsed — that is, the engine works out which word grammatically governs which (subject of what verb, object of what preposition) — using `udpipe-node` (our Node/JS/WASM port of UDPipe), now generating Universal Dependencies (UD) trees and morphological features. (For legacy comparison, the toolkit's built-in "Clio" alternate mechanics persist in using the FinNLP family of libraries (`lexed`, `en-pos`, `en-parse`) instead). A conversion layer seamlessly translates UD tags into the Penn Treebank tags our prosody expects. Two correction layers sit inside this stage,
182
176
  First, because poetry tends to break part-of-speech and grammatical dependency taggers in predictable ways, a *tag-repair* pass fixes systematic errors before the dependency tree is built (this appropriately accounts for rare exotics and awkward/shifty commonplaces alike: from archaic forms like *thou/thy/doth/wherefore*, to the pronoun *I*, to perfect-tense participles like *had quit*).
183
177
  Then we leverage a *tree-repair* pass (using the [depedits](https://www.npmjs.com/package/depedits) rule engine, our TypeScript port of the DepEdit library, originally in Python), which fixes systematic phrasal role attachment errors (e.g. noun compounds parsed as double objects, and the like). Hyphenated compounds and contractions (like *we'll*, *don't*, archaic *fix'd*, etc) are re-merged into single metrical words.
184
178
 
@@ -219,6 +213,10 @@ import {
219
213
  analyzeStanzas, // poem text → LineResult[][] (per stanza, per line)
220
214
  analyzeText, // poem text → LineResult[] (flat)
221
215
  analyzeReadingDocument, // poem text → ReadingStanza[] (keeps verbatim lines)
216
+ // The legacy/alternate "Clio" engine equivalents (using FinNLP):
217
+ analyzeStanzasClio,
218
+ analyzeTextClio,
219
+ analyzeReadingDocumentClio,
222
220
  } from 'calliope-ts';
223
221
  ```
224
222
 
@@ -362,7 +360,7 @@ Calliope TS is developed by **Aleksey Calvin** / [SilverAgePoets.com](https://ww
362
360
  ---
363
361
 
364
362
  **Limitations to know about.**
365
- - English only (the dictionary and parser are English; other languages will produce nonsense rather than errors).
363
+ - English only (the dictionary and phonological rules are English; while the UDPipe backend natively supports multilingual parsing, other languages will produce nonsense rather than errors).
366
364
  - Stress-doublet words (*rebel*, *content*, names like *Hugo*) are read with their dictionary stress; a correction is on the roadmap.
367
365
  - Rare or foreign proper names fall back to rule-based stress guesses.
368
366
 
@@ -384,8 +382,22 @@ node trials/corpus_benchmark.mjs # litlab / prosodic / epg64 meter corpora
384
382
 
385
383
  ## License and credits
386
384
 
387
- Apache-2.0. © Aleksey Calvin Tsukanov / SilverAgePoets.com.
388
- My email: alekseycalvin@gmail.com
389
-
390
- Methodological and conceptual debts: Bruce Hayes (1982/1984/1995/1996 with Abigail Kaun/2005) (the phonological scansion procedure as such, extrametricality insights, text-setting methodologies, MaxEnt OT, and who knows what else), Gareth McAleese (for a single 2008 paper, for detailing the original Calliope implementation, for exhibiting a remarkable field-spanning purview, an uncanny industriousness, and an uncommon – perhaps a tad obsessive – dedication to testing, refining, fusing, and extending all sorts of methodologies in a single-minded pursuit of bringing constraint-based computational scansion far beyond the best documented practices and results at that time; and for so obviously succeeding, if only to seemingly vanish from the field as abruptly and unreservedly as he entered and absorbed it*); Charles O. Hartman (Scandroid); Claire Moore Cantwell (morphological/phonological tagging algorithms), Austin Pursley (implementing finer-grained rhyme-matching heuristics over a corpus), Allison Parrish (Pronouncing-py and being a real life computational poet hero), Derek Attridge (beat/offbeat rhythm theory and insightful writings on the English dolnik); M. L. Gasparov (dolnik/taktovik taxonomy); the compilers of the CMU Pronouncing Dictionary; the makers of Prosodic (Heuser et al, for establishing an admirable state-of-the-art to compare against, differentiate from, and hopefully surpass in due time, in select ways), as well as broader generative-metrics, constraint-based metrics, and OT traditions, including Kiparsky, Prince & Smolensky, Groves, Blumenfeld, Lilja, Chomsky & Halle, Fabb & Halle (rule-based grid scansion theory), Einarsson (Metremic theory), Russom (Universalist metrics), K. M. Ryan (gradient syllable weight), big daddy Jakobson who had once roped the whole world with subtle strings and often hung out with Mayakovsky, and many others.
385
+ Apache-2.0. © Aleksey Calvin Tsukanov / SilverAgePoets.com. <br>
386
+ My email: alekseycalvin@gmail.com <br>
387
+
388
+ Methodological and conceptual debts:
389
+ - Michael Wagner (Prosody and Recursion, MIT, 2005) (for further clarifying the inter-relational nuances of prosody and syntax).
390
+ - Manfred Krifka (2001/2002) (for so poignantly elucidating NSR and CSR beyond SPE).
391
+ - Bruce Hayes (1982/1984/1995/1996 with Abigail Kaun/2005) (the phonological scansion procedure as such, extrametricality insights, text-setting methodologies, MaxEnt OT, and who knows what else);
392
+ - Gareth McAleese (for a single 2008 paper, for detailing the original Calliope implementation, for exhibiting a remarkable field-spanning purview, an uncanny industriousness, and an uncommon – perhaps a tad obsessive – dedication to testing, refining, fusing, and extending all sorts of methodologies in a single-minded pursuit of bringing constraint-based computational scansion far beyond the best documented practices and results at that time; and for so obviously succeeding, if only to seemingly vanish from the field as abruptly and unreservedly as he entered and absorbed it*);
393
+ - Charles O. Hartman (Scandroid);
394
+ - Claire Moore Cantwell (morphological/phonological tagging algorithms),
395
+ - Austin Pursley (implementing finer-grained rhyme-matching heuristics over a corpus),
396
+ - Allison Parrish (Pronouncing-py and being a real life computational poet hero),
397
+ - Derek Attridge (beat/offbeat rhythm theory and insightful writings on the English dolnik);
398
+ - M. L. Gasparov (dolnik/taktovik taxonomy);
399
+ - the compilers of the CMU Pronouncing Dictionary;
400
+ - the makers of Prosodic (Heuser et al, for establishing an admirable state-of-the-art to compare against, differentiate from, and hopefully surpass in due time, in select ways),
401
+ - Milan Straka and the UDPipe project (for the robust neural parsing architecture now driving the core mechanics),
402
+ - as well as broader generative-metrics, constraint-based metrics, and OT traditions, including Kiparsky, Prince & Smolensky, Groves, Blumenfeld, Lilja, Chomsky & Halle, Fabb & Halle (rule-based grid scansion theory), Einarsson (Metremic theory), Russom (Universalist metrics), K. M. Ryan (gradient syllable weight), big daddy Jakobson who had once roped the whole world with subtle strings and often hung out with Mayakovsky, and many others. <br>
391
403
  *Gareth McAleese: If you're reading this, please do email me!
@@ -0,0 +1,33 @@
1
+ import { ClsWord, IntonationalUnit } from './types.js';
2
+ export type CaesuraKind = 'hard' | 'soft';
3
+ /** A caesura with its graded boundary strength (0..1, NSBR-scaled), so the display
4
+ * can colour the mark by how strong the underlying prosodic break is. */
5
+ export interface CaesuraInfo {
6
+ kind: CaesuraKind;
7
+ strength: number;
8
+ }
9
+ /**
10
+ * Caesura positions for a line, keyed by the syllable index AFTER which the pause
11
+ * falls (= number of syllables to its left).
12
+ * • 'hard' — an overt break at an Intonational-Unit boundary.
13
+ * • 'soft' — a single INFERRED medial caesura for a punctuation-free line.
14
+ * Candidates are the boundaries just before a phrase/clause onset
15
+ * (PHRASE_ONSET_POS); the one nearest the line's midpoint that lies in the
16
+ * central third AND coincides with a foot boundary wins — so it is medial,
17
+ * never mid-foot, and consistent across structurally-parallel lines. Read in
18
+ * LINEAR order (robust to clitic-group reordering); needs a line of ≥ 8
19
+ * syllables.
20
+ */
21
+ export declare function computeCaesurae(words: ClsWord[], ius: IntonationalUnit[], scansion?: string): Map<number, CaesuraInfo>;
22
+ /**
23
+ * The words that immediately PRECEDE a caesura in a line — i.e. each word whose
24
+ * cumulative syllable count lands exactly on a caesura position. Used by the
25
+ * rhyme layer for pre-caesural internal-rhyme detection. Returned in linear
26
+ * (reading) order; the caesura kind is paired so callers can weight hard vs
27
+ * inferred breaks if they wish.
28
+ */
29
+ export declare function preCaesuralWords(words: ClsWord[], ius: IntonationalUnit[], scansion?: string): {
30
+ word: ClsWord;
31
+ kind: CaesuraKind;
32
+ }[];
33
+ //# sourceMappingURL=caesura.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caesura.d.ts","sourceRoot":"","sources":["../src/caesura.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAIvD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1C;0EAC0E;AAC1E,MAAM,WAAW,WAAW;IAAG,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;CAAE;AAyCrE;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAuGtH;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAC3D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,EAAE,CAYxC"}
@@ -0,0 +1,202 @@
1
+ // caesura.ts — Caesura placement, shared by the display and rhyme layers.
2
+ //
3
+ // A caesura is the line's medial pause. Two kinds are distinguished:
4
+ // • 'hard' — an overt break at an Intonational-Unit boundary (comma, dash,
5
+ // colon, semicolon …): the punctuation projection.
6
+ // • 'soft' — a single INFERRED medial caesura for a punctuation-free line
7
+ // (Kiparsky 1975, via McAleese: "phonological phrasing determines the
8
+ // location of caesurae in verse").
9
+ //
10
+ // Extracted from display.ts (2026-06-13) so the rhyme layer can find the words
11
+ // that immediately precede caesurae (for pre-caesural internal-rhyme detection)
12
+ // without importing the display module. Pure analysis — no colour/chalk here.
13
+ import { isPunctuation } from './parser.js';
14
+ import { computeBoundaries } from './calliope/boundaries.js';
15
+ /** Foot-boundary syllable indices from a scansion string (cumulative syllable
16
+ * count after each foot; silent beats '-' are not syllables). */
17
+ function footBoundarySet(scansion) {
18
+ const set = new Set();
19
+ let c = 0;
20
+ for (const foot of scansion.split('|')) {
21
+ for (const ch of foot)
22
+ if ('xwnms'.includes(ch))
23
+ c++;
24
+ set.add(c);
25
+ }
26
+ return set;
27
+ }
28
+ // A new phonological/syntactic phrase opens at these POS tags: prepositions &
29
+ // subordinators (IN), infinitival "to" (TO), coordinators (CC), wh/relativizers,
30
+ // verb-particles (RP), and the predicate's verb/modal. The major medial caesura
31
+ // of a line falls immediately BEFORE such a word — far more reliably read off the
32
+ // (robust) POS tags than off FinNLP's (noisy) phonological-phrase grouping, which
33
+ // mis-bracketed e.g. "The epic | feast…". Determiners/articles are excluded (they
34
+ // continue a phrase a preposition already opened: "as | one empty bag").
35
+ const PHRASE_ONSET_POS = new Set([
36
+ 'IN', 'TO', 'CC', 'WDT', 'WP', 'WP$', 'WRB', 'RP',
37
+ 'VB', 'VBD', 'VBG', 'VBN', 'VBP', 'VBZ', 'MD',
38
+ ]);
39
+ // Directional/spatial adverbs that open a phrase ("snowed-in OUT of many routes",
40
+ // "drifting DOWN to sleep"). FinNLP often tags these RB rather than RP/IN, so the
41
+ // POS set alone misses them; a small curated lemma list recovers them with low
42
+ // over-fire risk (a generic RB like "very"/"quickly" is NOT a phrase onset).
43
+ const DIRECTIONAL_ONSET = new Set([
44
+ 'out', 'in', 'up', 'down', 'off', 'away', 'back', 'forth', 'over',
45
+ 'around', 'along', 'through', 'apart', 'aside', 'onward', 'onwards',
46
+ ]);
47
+ /** True if a word opens a new phonological/syntactic phrase (a caesura candidate). */
48
+ function isPhraseOnset(w) {
49
+ if (PHRASE_ONSET_POS.has(w.lexicalClass))
50
+ return true;
51
+ return w.lexicalClass === 'RB' && DIRECTIONAL_ONSET.has(w.word.toLowerCase());
52
+ }
53
+ /**
54
+ * Caesura positions for a line, keyed by the syllable index AFTER which the pause
55
+ * falls (= number of syllables to its left).
56
+ * • 'hard' — an overt break at an Intonational-Unit boundary.
57
+ * • 'soft' — a single INFERRED medial caesura for a punctuation-free line.
58
+ * Candidates are the boundaries just before a phrase/clause onset
59
+ * (PHRASE_ONSET_POS); the one nearest the line's midpoint that lies in the
60
+ * central third AND coincides with a foot boundary wins — so it is medial,
61
+ * never mid-foot, and consistent across structurally-parallel lines. Read in
62
+ * LINEAR order (robust to clitic-group reordering); needs a line of ≥ 8
63
+ * syllables.
64
+ */
65
+ export function computeCaesurae(words, ius, scansion) {
66
+ const caes = new Map();
67
+ // Graded boundary strength (Wagner Ch.4–5) keyed by syllable index, so each
68
+ // caesura carries the strength of its underlying ϕ/ι break — used both to COLOUR
69
+ // the mark and to gate strong-ϕ hard caesurae on a line-relative threshold.
70
+ const bounds = computeBoundaries(words, ius);
71
+ const strengthAt = new Map();
72
+ for (const b of bounds.phi) {
73
+ const prev = strengthAt.get(b.syllableIndex) ?? 0;
74
+ if (b.strength > prev)
75
+ strengthAt.set(b.syllableIndex, b.strength);
76
+ }
77
+ const strOf = (syl) => strengthAt.get(syl) ?? 0;
78
+ const setHard = (syl) => caes.set(syl, { kind: 'hard', strength: Math.max(0.6, strOf(syl)) });
79
+ // Caesurae must sit on FOOT BOUNDARIES (when the scansion is known). The line's
80
+ // metrical feet ARE the scansion's own segmentation, so a break that falls mid-
81
+ // foot has no metrical reality — it merely fragments a foot (often into a lone
82
+ // monosyllable: "But ‖ Oh ‖ ye…") and it made the reading view disagree with the
83
+ // detailed "Feet:" view, which already marks foot-edge breaks only. Punctuation
84
+ // is a caesura CANDIDATE, never an override: an IU boundary that does not land on
85
+ // a foot edge is dropped (the soft-caesura fallback below then offers one medial,
86
+ // foot-aligned break if the line is long enough).
87
+ const footEdges = scansion ? footBoundarySet(scansion) : null;
88
+ const iuOf = new Map();
89
+ for (let i = 0; i < ius.length; i++) {
90
+ for (const pp of ius[i].phonologicalPhrases) {
91
+ for (const cg of pp.cliticGroups)
92
+ for (const tok of cg.tokens)
93
+ iuOf.set(tok, i);
94
+ }
95
+ }
96
+ let cum = 0;
97
+ let prevIu;
98
+ let prevWasContentful = false;
99
+ const onsetPositions = [];
100
+ for (const w of words) {
101
+ if (isPunctuation(w.lexicalClass) || w.syllables.length === 0) {
102
+ // A COMMA is an overt medial pause — the canonical caesura marker — but in
103
+ // the Match-Theory hierarchy it is only a ϕ (minor) break, NOT an ι boundary
104
+ // (dash/colon/semicolon DO project an ι, so those are caught by the IU test
105
+ // below). So detect a comma here directly: a comma after a contentful word,
106
+ // landing on a foot edge, is a hard caesura. (Without this, the comma→ϕ
107
+ // reclassification silently dropped every comma caesura, taking pre-caesural
108
+ // and caesural internal-rhyme detection with it.)
109
+ if (prevWasContentful && (w.lexicalClass === ',' || w.word === ',')
110
+ && (!footEdges || footEdges.has(cum))) {
111
+ setHard(cum);
112
+ }
113
+ continue;
114
+ }
115
+ const iu = iuOf.get(w);
116
+ if (prevIu !== undefined && iu !== undefined && iu !== prevIu
117
+ && (!footEdges || footEdges.has(cum))) {
118
+ setHard(cum); // IU boundary landing on a foot edge → hard caesura
119
+ }
120
+ // A phrase-onset word that is NOT the line's first word opens a candidate
121
+ // caesura immediately before it.
122
+ if (prevWasContentful && isPhraseOnset(w))
123
+ onsetPositions.push(cum);
124
+ cum += w.syllables.length;
125
+ prevIu = iu;
126
+ prevWasContentful = true;
127
+ }
128
+ const total = cum;
129
+ // STRONG-ϕ hard caesura (Wagner Ch.5): a ϕ boundary whose NSBR-scaled strength
130
+ // clears a line-relative threshold — a major medial break after a long branching
131
+ // phrase — is a hard caesura even without punctuation, provided it lands on a foot
132
+ // edge and is medial. Threshold is relative (strength is already line-normalised),
133
+ // so a comma after a short phrase does NOT trigger one.
134
+ if (total >= 8) {
135
+ const lo = Math.max(2, Math.ceil(total / 3));
136
+ const hi = Math.floor((2 * total) / 3);
137
+ for (const b of bounds.phi) {
138
+ const c = b.syllableIndex;
139
+ if (c < lo || c > hi)
140
+ continue;
141
+ if (footEdges && !footEdges.has(c))
142
+ continue;
143
+ if (caes.has(c))
144
+ continue;
145
+ if (b.strength >= 0.75)
146
+ caes.set(c, { kind: 'hard', strength: b.strength });
147
+ }
148
+ }
149
+ // Infer ONE medial caesura only when the line carries no overt (hard) break.
150
+ // Prefer the STRONGEST candidate boundary, tie-broken by nearness to the midpoint.
151
+ if (caes.size === 0 && total >= 8 && onsetPositions.length > 0) {
152
+ const mid = total / 2;
153
+ const lo = Math.max(2, Math.ceil(total / 3));
154
+ const hi = Math.floor((2 * total) / 3);
155
+ let best = -1, bestStr = -1, bestDist = Infinity;
156
+ for (const c of onsetPositions) {
157
+ if (c < lo || c > hi)
158
+ continue; // medial third only
159
+ if (footEdges && !footEdges.has(c))
160
+ continue; // align to a foot boundary
161
+ const str = strOf(c);
162
+ const d = Math.abs(c - mid);
163
+ if (str > bestStr + 1e-6 || (Math.abs(str - bestStr) <= 1e-6 && d < bestDist)) {
164
+ bestStr = str;
165
+ bestDist = d;
166
+ best = c;
167
+ }
168
+ }
169
+ if (best > 0)
170
+ caes.set(best, { kind: 'soft', strength: Math.max(0.25, bestStr) });
171
+ }
172
+ // A caesura is a MEDIAL pause: a line-terminal comma/IU close lands at the last
173
+ // syllable (cum == total) and is the line break's own boundary, not a caesura.
174
+ // Dropping it here keeps every consumer consistent — the detailed view used to
175
+ // print a spurious trailing '‖' the reading view (correctly) never showed, and
176
+ // the rhyme layer would have double-counted the end word as "pre-caesural".
177
+ caes.delete(total);
178
+ return caes;
179
+ }
180
+ /**
181
+ * The words that immediately PRECEDE a caesura in a line — i.e. each word whose
182
+ * cumulative syllable count lands exactly on a caesura position. Used by the
183
+ * rhyme layer for pre-caesural internal-rhyme detection. Returned in linear
184
+ * (reading) order; the caesura kind is paired so callers can weight hard vs
185
+ * inferred breaks if they wish.
186
+ */
187
+ export function preCaesuralWords(words, ius, scansion) {
188
+ const caes = computeCaesurae(words, ius, scansion);
189
+ if (caes.size === 0)
190
+ return [];
191
+ const out = [];
192
+ let cum = 0;
193
+ for (const w of words) {
194
+ if (isPunctuation(w.lexicalClass) || w.syllables.length === 0)
195
+ continue;
196
+ cum += w.syllables.length;
197
+ const info = caes.get(cum);
198
+ if (info)
199
+ out.push({ word: w, kind: info.kind });
200
+ }
201
+ return out;
202
+ }
@@ -0,0 +1,19 @@
1
+ import { ClsWord, IntonationalUnit } from '../types.js';
2
+ export interface BoundaryInfo {
3
+ level: 'kappa' | 'phi' | 'iota';
4
+ strength: number;
5
+ raw: number;
6
+ syllableIndex: number;
7
+ }
8
+ export interface LineBoundaries {
9
+ /** ϕ boundaries in document order: phi[k] is the boundary OPENING the k-th ϕ.
10
+ * phi[0] is the line's left edge (strength 0 — not a real internal break). */
11
+ phi: BoundaryInfo[];
12
+ /** ι boundaries in document order, same convention. */
13
+ iota: BoundaryInfo[];
14
+ }
15
+ /**
16
+ * Compute graded boundary strength for every ϕ and ι boundary in a line.
17
+ */
18
+ export declare function computeBoundaries(words: ClsWord[], ius: IntonationalUnit[]): LineBoundaries;
19
+ //# sourceMappingURL=boundaries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundaries.d.ts","sourceRoot":"","sources":["../../src/calliope/boundaries.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B;mFAC+E;IAC/E,GAAG,EAAE,YAAY,EAAE,CAAC;IACpB,uDAAuD;IACvD,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AAuFD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE,GAAG,cAAc,CAyD3F"}
@@ -0,0 +1,182 @@
1
+ // calliope/boundaries.ts — graded prosodic boundary strength (Wagner 2005 Ch. 4–5).
2
+ //
3
+ // The labelled κ/ϕ/ι hierarchy is the SKELETON; this module adds the relational
4
+ // FLESH Wagner argues the grid actually encodes — a graded boundary strength rather
5
+ // than a categorical label. Each ϕ/ι boundary gets a strength scaled RELATIVE to the
6
+ // line's other boundaries (the NSBR, Normalized Scopally-determined Boundary Rank,
7
+ // Ch. 5 §5.3): the strongest boundary in the line is 1.0, the rest fall below it.
8
+ //
9
+ // The raw strength of a boundary between left unit A and right unit B combines:
10
+ // • the categorical level (ι ≫ ϕ ≫ κ) — the SBR base rank;
11
+ // • punctuation coincidence — a comma/dash/colon at the boundary makes it stronger;
12
+ // • the LENGTH of the preceding constituent (a boundary after a long, branching
13
+ // phrase is stronger — Wagner Ch. 5 look-back);
14
+ // • clause separation — a boundary between subject|predicate or main|subordinate
15
+ // clause (their dependency LCA is at/near the root) is stronger;
16
+ // • associative coordination — a boundary internal to a same-coordinator series is
17
+ // WEAKER (equal-rank, flat).
18
+ //
19
+ // Strength feeds (a) the colored bracket rendering (display.ts) and (b) caesura
20
+ // placement (caesura.ts): a hard caesura needs a boundary whose strength clears a
21
+ // line-relative threshold, not an absolute one.
22
+ function isPunct(w) {
23
+ return /^[^A-Za-z0-9]+$/.test(w.lexicalClass) || w.syllables.length === 0;
24
+ }
25
+ /** Dependency depth of a word (number of governors up to the root). */
26
+ function depthOf(w, memo) {
27
+ const seen = new Set();
28
+ let d = 0;
29
+ let cur = w;
30
+ while (cur) {
31
+ if (memo.has(cur)) {
32
+ d += memo.get(cur);
33
+ break;
34
+ }
35
+ if (seen.has(cur))
36
+ break; // cycle guard
37
+ seen.add(cur);
38
+ const g = cur.dependency?.governor;
39
+ if (!g || g === cur || isPunct(g))
40
+ break;
41
+ d++;
42
+ cur = g;
43
+ }
44
+ memo.set(w, d);
45
+ return d;
46
+ }
47
+ /** The syntactic head of a ϕ — the lowest-depth (closest to root) content token. */
48
+ function phraseHead(tokens, memo) {
49
+ let best = null;
50
+ let bestD = Infinity;
51
+ for (const t of tokens) {
52
+ if (isPunct(t))
53
+ continue;
54
+ const d = depthOf(t, memo);
55
+ if (d < bestD) {
56
+ bestD = d;
57
+ best = t;
58
+ }
59
+ }
60
+ return best;
61
+ }
62
+ /** Lowest-common-ancestor depth of two words in the dependency tree. */
63
+ function lcaDepth(a, b, memo) {
64
+ const anc = new Map();
65
+ let cur = a;
66
+ let d = 0;
67
+ const seen = new Set();
68
+ while (cur && !seen.has(cur)) {
69
+ anc.set(cur, d++);
70
+ seen.add(cur);
71
+ const g = cur.dependency?.governor;
72
+ if (!g || g === cur || isPunct(g))
73
+ break;
74
+ cur = g;
75
+ }
76
+ cur = b;
77
+ const seen2 = new Set();
78
+ while (cur && !seen2.has(cur)) {
79
+ if (anc.has(cur))
80
+ return depthOf(cur, memo);
81
+ seen2.add(cur);
82
+ const g = cur.dependency?.governor;
83
+ if (!g || g === cur || isPunct(g))
84
+ break;
85
+ cur = g;
86
+ }
87
+ return 0; // disjoint subtrees / different roots → treat as top-level (depth 0)
88
+ }
89
+ /** Is there a comma / dash / terminal punctuation token between A and B in surface
90
+ * order, and is it a strong (ι-class) one? */
91
+ function punctBetween(words, a, b) {
92
+ let comma = false, strong = false;
93
+ for (const w of words) {
94
+ if (w.absoluteIndex <= a.absoluteIndex || w.absoluteIndex >= b.absoluteIndex)
95
+ continue;
96
+ if (!isPunct(w))
97
+ continue;
98
+ if (w.word === ',' || w.lexicalClass === ',')
99
+ comma = true;
100
+ if (/^[.!?:;…]$/.test(w.word) || /^[.!?:;-]$/.test(w.lexicalClass) ||
101
+ w.lexicalClass === '-LRB-' || w.lexicalClass === '-RRB-')
102
+ strong = true;
103
+ }
104
+ return { comma, strong };
105
+ }
106
+ function sylCount(tokens) {
107
+ let n = 0;
108
+ for (const t of tokens)
109
+ n += t.syllables.length;
110
+ return n;
111
+ }
112
+ /** Flatten the ϕ of an ι into token lists. */
113
+ function phiTokenLists(iu) {
114
+ return iu.phonologicalPhrases.map(pp => pp.cliticGroups.flatMap(cg => cg.tokens).filter(t => !isPunct(t)));
115
+ }
116
+ /**
117
+ * Compute graded boundary strength for every ϕ and ι boundary in a line.
118
+ */
119
+ export function computeBoundaries(words, ius) {
120
+ const memo = new Map();
121
+ const maxDepth = Math.max(1, ...words.filter(w => !isPunct(w)).map(w => depthOf(w, memo)));
122
+ // Flatten ϕ across all ι in document order, remembering each ϕ's ι index.
123
+ const flatPhi = [];
124
+ ius.forEach((iu, iuIdx) => {
125
+ phiTokenLists(iu).forEach((tokens, ppIdx) => {
126
+ if (tokens.length)
127
+ flatPhi.push({ tokens, iuIdx, ppIdx });
128
+ });
129
+ });
130
+ let cumSyl = 0;
131
+ const phi = [];
132
+ const iota = [];
133
+ for (let k = 0; k < flatPhi.length; k++) {
134
+ const cur = flatPhi[k];
135
+ const prev = k > 0 ? flatPhi[k - 1] : null;
136
+ const isIotaBoundary = !!prev && cur.iuIdx !== prev.iuIdx;
137
+ let raw = 0;
138
+ if (prev) {
139
+ const aHead = phraseHead(prev.tokens, memo); // left phrase head
140
+ const bHead = phraseHead(cur.tokens, memo); // right phrase head
141
+ const aLast = prev.tokens[prev.tokens.length - 1];
142
+ const bFirst = cur.tokens[0];
143
+ // base rank by level
144
+ raw += isIotaBoundary ? 3.0 : 1.0;
145
+ // punctuation coincidence
146
+ const p = punctBetween(words, aLast, bFirst);
147
+ if (p.strong)
148
+ raw += 2.0;
149
+ else if (p.comma)
150
+ raw += 1.2;
151
+ // length of the preceding constituent (look-back): longer → stronger
152
+ raw += Math.min(1.5, sylCount(prev.tokens) / 6);
153
+ // clause separation: a shallow dependency LCA (near the root) → strong
154
+ if (aHead && bHead) {
155
+ const lca = lcaDepth(aHead, bHead, memo);
156
+ raw += 1.2 * (1 - Math.min(1, lca / maxDepth)); // shallow LCA → +clause bonus
157
+ }
158
+ // associative coordination weakening: same coordinator across the boundary
159
+ if (startsCoordinator(cur.tokens) && !p.comma && !p.strong)
160
+ raw -= 0.4;
161
+ }
162
+ const info = {
163
+ level: isIotaBoundary ? 'iota' : 'phi',
164
+ strength: 0, // filled after normalisation
165
+ raw: Math.max(0, raw),
166
+ syllableIndex: cumSyl,
167
+ };
168
+ phi.push(info);
169
+ if (isIotaBoundary)
170
+ iota.push(info);
171
+ cumSyl += sylCount(cur.tokens);
172
+ }
173
+ // NSBR normalisation: scale to the strongest boundary in the line (0..1).
174
+ const maxRaw = Math.max(0, ...phi.map(b => b.raw));
175
+ for (const b of phi)
176
+ b.strength = maxRaw > 0 ? b.raw / maxRaw : 0;
177
+ return { phi, iota };
178
+ }
179
+ function startsCoordinator(tokens) {
180
+ const t = tokens[0];
181
+ return !!t && (t.lexicalClass === 'CC' || t.canonicalRel === 'CC');
182
+ }
@@ -0,0 +1,11 @@
1
+ import { ClsSentence, ClsWord } from '../types.js';
2
+ /**
3
+ * Run the cyclic Compound + Nuclear Stress Rules (Wagner-extended) over the
4
+ * sentence's dependency tree and write each word's `phraseStress` (1 = strongest).
5
+ */
6
+ export declare function computePhraseStress(sent: ClsSentence): void;
7
+ /**
8
+ * Partition the sentence's words into ϕ-domains over the dependency tree.
9
+ */
10
+ export declare function computePhiDomains(sent: ClsSentence): Map<ClsWord, number>;
11
+ //# sourceMappingURL=bracketing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bracketing.d.ts","sourceRoot":"","sources":["../../src/calliope/bracketing.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AA4HnD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAmF3D;AA4GD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CA6BzE"}