calliope-ts 0.0.4 → 0.1.0

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 (183) hide show
  1. package/README.md +69 -3
  2. package/dist/calliope/bracketing.d.ts.map +1 -1
  3. package/dist/calliope/bracketing.js +59 -5
  4. package/dist/calliope/deps.d.ts +5 -1
  5. package/dist/calliope/deps.d.ts.map +1 -1
  6. package/dist/calliope/deps.js +53 -3
  7. package/dist/calliope/engine.d.ts.map +1 -1
  8. package/dist/calliope/engine.js +6 -1
  9. package/dist/calliope/relstress.d.ts.map +1 -1
  10. package/dist/calliope/relstress.js +411 -15
  11. package/dist/calliope/syntax.d.ts +33 -0
  12. package/dist/calliope/syntax.d.ts.map +1 -1
  13. package/dist/calliope/syntax.js +199 -0
  14. package/dist/data/en_common_words.json +1 -0
  15. package/dist/display.d.ts +15 -1
  16. package/dist/display.d.ts.map +1 -1
  17. package/dist/display.js +112 -10
  18. package/dist/fabbhalle.d.ts +54 -0
  19. package/dist/fabbhalle.d.ts.map +1 -0
  20. package/dist/fabbhalle.js +404 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +86 -33
  23. package/dist/phonological.d.ts.map +1 -1
  24. package/dist/phonological.js +7 -1
  25. package/dist/rewriteEn.d.ts +14 -0
  26. package/dist/rewriteEn.d.ts.map +1 -0
  27. package/dist/rewriteEn.js +392 -0
  28. package/dist/rhyme.d.ts +35 -3
  29. package/dist/rhyme.d.ts.map +1 -1
  30. package/dist/rhyme.js +176 -11
  31. package/dist/russian/accentuator.d.ts +31 -0
  32. package/dist/russian/accentuator.d.ts.map +1 -0
  33. package/dist/russian/accentuator.js +546 -0
  34. package/dist/russian/collocations.d.ts +7 -0
  35. package/dist/russian/collocations.d.ts.map +1 -0
  36. package/dist/russian/collocations.js +38 -0
  37. package/dist/russian/compounds.d.ts +6 -0
  38. package/dist/russian/compounds.d.ts.map +1 -0
  39. package/dist/russian/compounds.js +81 -0
  40. package/dist/russian/download.d.ts +23 -0
  41. package/dist/russian/download.d.ts.map +1 -0
  42. package/dist/russian/download.js +117 -0
  43. package/dist/russian/engine.d.ts +6 -0
  44. package/dist/russian/engine.d.ts.map +1 -0
  45. package/dist/russian/engine.js +1424 -0
  46. package/dist/russian/parser.d.ts +8 -0
  47. package/dist/russian/parser.d.ts.map +1 -0
  48. package/dist/russian/parser.js +92 -0
  49. package/dist/russian/paths.d.ts +55 -0
  50. package/dist/russian/paths.d.ts.map +1 -0
  51. package/dist/russian/paths.js +114 -0
  52. package/dist/russian/rewrite.d.ts +6 -0
  53. package/dist/russian/rewrite.d.ts.map +1 -0
  54. package/dist/russian/rewrite.js +400 -0
  55. package/dist/russian/rhyme.d.ts +18 -0
  56. package/dist/russian/rhyme.d.ts.map +1 -0
  57. package/dist/russian/rhyme.js +230 -0
  58. package/dist/russian/syllabifier.d.ts +14 -0
  59. package/dist/russian/syllabifier.d.ts.map +1 -0
  60. package/dist/russian/syllabifier.js +475 -0
  61. package/dist/russian/types.d.ts +94 -0
  62. package/dist/russian/types.d.ts.map +1 -0
  63. package/dist/russian/types.js +27 -0
  64. package/dist/scandroidNative/constants.d.ts +16 -0
  65. package/dist/scandroidNative/constants.d.ts.map +1 -0
  66. package/dist/scandroidNative/constants.js +60 -0
  67. package/dist/scandroidNative/deduceParameters.d.ts +16 -0
  68. package/dist/scandroidNative/deduceParameters.d.ts.map +1 -0
  69. package/dist/scandroidNative/deduceParameters.js +69 -0
  70. package/dist/scandroidNative/dictionary-data.d.ts +2 -0
  71. package/dist/scandroidNative/dictionary-data.d.ts.map +1 -0
  72. package/dist/scandroidNative/dictionary-data.js +744 -0
  73. package/dist/scandroidNative/dictionary.d.ts +12 -0
  74. package/dist/scandroidNative/dictionary.d.ts.map +1 -0
  75. package/dist/scandroidNative/dictionary.js +50 -0
  76. package/dist/scandroidNative/engine.d.ts +41 -0
  77. package/dist/scandroidNative/engine.d.ts.map +1 -0
  78. package/dist/scandroidNative/engine.js +119 -0
  79. package/dist/scandroidNative/machine.d.ts +93 -0
  80. package/dist/scandroidNative/machine.d.ts.map +1 -0
  81. package/dist/scandroidNative/machine.js +795 -0
  82. package/dist/scandroidNative/positioner.d.ts +32 -0
  83. package/dist/scandroidNative/positioner.d.ts.map +1 -0
  84. package/dist/scandroidNative/positioner.js +208 -0
  85. package/dist/scandroidNative/syllabizer.d.ts +3 -0
  86. package/dist/scandroidNative/syllabizer.d.ts.map +1 -0
  87. package/dist/scandroidNative/syllabizer.js +215 -0
  88. package/dist/scandroidNative/utilities.d.ts +37 -0
  89. package/dist/scandroidNative/utilities.d.ts.map +1 -0
  90. package/dist/scandroidNative/utilities.js +86 -0
  91. package/dist/scansion.d.ts.map +1 -1
  92. package/dist/scansion.js +118 -14
  93. package/dist/stress.d.ts +1 -0
  94. package/dist/stress.d.ts.map +1 -1
  95. package/dist/stress.js +502 -42
  96. package/dist/types.d.ts +3 -0
  97. package/dist/types.d.ts.map +1 -1
  98. package/package.json +27 -6
  99. package/tools/fetch-russian-data.mjs +30 -0
  100. package/webapp/public/app.js +2418 -0
  101. package/webapp/public/assets/GithubLogo.png +0 -0
  102. package/webapp/public/assets/NPM2.png +0 -0
  103. package/webapp/public/assets/fonts/MurenaBold-webfont.woff2 +0 -0
  104. package/webapp/public/assets/fonts/MurenaItalic-webfont.woff2 +0 -0
  105. package/webapp/public/assets/fonts/MurenaRegular-webfont.woff2 +0 -0
  106. package/webapp/public/assets/fonts/Myra_4F_Caps_Bold-webfont.woff2 +0 -0
  107. package/webapp/public/assets/fonts/SinkinSans-700Bold.otf +0 -0
  108. package/webapp/public/assets/fonts/butterflies.ttf +0 -0
  109. package/webapp/public/assets/fonts/midcase_blackline-webfont.woff2 +0 -0
  110. package/webapp/public/assets/fonts/neueral-bold-webfont.woff2 +0 -0
  111. package/webapp/public/assets/fonts/neueral-regular-webfont.woff2 +0 -0
  112. package/webapp/public/assets/fonts/popelka-webfont.woff2 +0 -0
  113. package/webapp/public/assets/fonts/vremenagrotesk-webfont.woff +0 -0
  114. package/webapp/public/assets/fonts/wicky_javick_bold-webfont.woff2 +0 -0
  115. package/webapp/public/assets/logos/Logo_Var_10_Celtic_Knot_SVG.svg +116 -0
  116. package/webapp/public/assets/logos/Logo_Var_11_Graph_SVG.svg +313 -0
  117. package/webapp/public/assets/logos/Logo_Var_12_Limacon_Knot_SVG.svg +3942 -0
  118. package/webapp/public/assets/logos/Logo_Var_13_Crescents_SVG.svg +1030 -0
  119. package/webapp/public/assets/logos/Logo_Var_1_Spiral_SVG.svg +1491 -0
  120. package/webapp/public/assets/logos/Logo_Var_2_Tunnel_SVG.svg +12 -0
  121. package/webapp/public/assets/logos/Logo_Var_3_Triskelion_SVG.svg +10 -0
  122. package/webapp/public/assets/logos/Logo_Var_4_Quinquetra_PNG.png +0 -0
  123. package/webapp/public/assets/logos/Logo_Var_5_Penrose_Pentagram_SVG.svg +37 -0
  124. package/webapp/public/assets/logos/Logo_Var_6_Triskelion2_SVG.svg +10 -0
  125. package/webapp/public/assets/logos/Logo_Var_7_SixfoldSpirals_SVG.svg +20 -0
  126. package/webapp/public/assets/logos/Logo_Var_8_Radial_Labyrinth_SVG.svg +163 -0
  127. package/webapp/public/assets/logos/Logo_Var_9_Radial_Labyrinth_2_SVG.svg +106 -0
  128. package/webapp/public/index.html +316 -0
  129. package/webapp/public/styles.css +1114 -0
  130. package/webapp/server.mjs +680 -0
  131. package/src/caesura.ts +0 -201
  132. package/src/calliope/boundaries.ts +0 -190
  133. package/src/calliope/bracketing.ts +0 -390
  134. package/src/calliope/deps.ts +0 -160
  135. package/src/calliope/engine.ts +0 -77
  136. package/src/calliope/feats.ts +0 -46
  137. package/src/calliope/names.ts +0 -44
  138. package/src/calliope/postag.ts +0 -253
  139. package/src/calliope/prosodic.ts +0 -262
  140. package/src/calliope/relstress.ts +0 -645
  141. package/src/calliope/stressrules.ts +0 -147
  142. package/src/calliope/syntax.ts +0 -218
  143. package/src/calliope/udpos.ts +0 -152
  144. package/src/calliope_src_contents.md +0 -19049
  145. package/src/clio/caesura.ts +0 -145
  146. package/src/clio/depfix.ts +0 -88
  147. package/src/clio/display.ts +0 -1042
  148. package/src/clio/engine.ts +0 -38
  149. package/src/clio/parser.ts +0 -845
  150. package/src/clio/phonological.ts +0 -849
  151. package/src/clio/phrasestress.ts +0 -108
  152. package/src/clio/pipeline.ts +0 -154
  153. package/src/clio/rhyme.ts +0 -740
  154. package/src/clio/scandroid.ts +0 -434
  155. package/src/clio/scansion.ts +0 -1130
  156. package/src/clio/semantics.ts +0 -134
  157. package/src/clio/stress.ts +0 -1731
  158. package/src/clio/tagfix.ts +0 -104
  159. package/src/depfix.ts +0 -88
  160. package/src/display.ts +0 -1138
  161. package/src/engine.ts +0 -22
  162. package/src/index.ts +0 -690
  163. package/src/parser.ts +0 -501
  164. package/src/phonological.ts +0 -917
  165. package/src/phrasestress.ts +0 -108
  166. package/src/rhyme.ts +0 -748
  167. package/src/scandroid.ts +0 -434
  168. package/src/scansion.ts +0 -1174
  169. package/src/semantics.ts +0 -134
  170. package/src/stress.ts +0 -2111
  171. package/src/tagfix.ts +0 -104
  172. package/src/types.ts +0 -265
  173. package/tests/DataForHayesLinesOnly.txt +0 -364
  174. package/tests/DataForHayesStressSymbolsRevised.txt +0 -728
  175. package/tests/basic.test.ts +0 -1102
  176. package/tests/bench-hayes.mjs +0 -72
  177. package/tests/epg64.meter.train.txt +0 -8139
  178. package/tests/litlab-sample-2016.txt +0 -1738
  179. package/tests/prosodic.meter.train.txt +0 -13192
  180. package/tests/wagner-stress.test.ts +0 -188
  181. package/tsconfig.json +0 -29
  182. package/vitest.config.d.ts +0 -2
  183. package/vitest.config.js +0 -14
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Calliope TS 0.0.4
1
+ # Calliope TS 0.0.7
2
2
 
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.
3
+ **Calliope TS** is a bilingual (English & Russian) 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. The English pipeline is the primary and most fully documented engine; a parallel Russian pipeline (accentology + SynTagRus dependency parsing) is described under [Russian support](#russian-support).
4
4
 
5
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.
6
6
 
@@ -24,6 +24,7 @@ Stress Maps & Meter — top-3 candidate meters per line
24
24
  ## Contents
25
25
 
26
26
  - [Installation](#installation)
27
+ - [Russian support](#russian-support)
27
28
  - [Quick start (CLI)](#quick-start-cli)
28
29
  - [Reading the output](#reading-the-output)
29
30
  - [What "phonological scansion" means](#what-phonological-scansion-means)
@@ -61,6 +62,31 @@ npm test # vitest suite
61
62
 
62
63
  ---
63
64
 
65
+ ## Russian support
66
+
67
+ Calliope's Russian pipeline (accentology + ёфикация + SynTagRus dependency parsing + Russian rhyme/meter analysis) runs on a set of large data assets — the SynTagRus UDPipe model plus stress dictionaries and neural-accentuator weights (~122 MB total). To keep the npm package small, **these assets are not bundled in the published package**; the English engine has no such dependency and works immediately after install.
68
+
69
+ The Russian assets are fetched **automatically on first Russian analysis** and written into the toolkit's `russian/data` folder, after which they are reused offline. You can also pre-fetch them explicitly:
70
+
71
+ ```bash
72
+ npx calliope-fetch-russian # from an installed package (global or local)
73
+ npm run fetch-russian # from a clone of this repository
74
+ ```
75
+
76
+ Configuration (all optional):
77
+
78
+ | Variable | Effect |
79
+ |---|---|
80
+ | `CALLIOPE_RUSSIAN_DATA` | Absolute path to a directory holding the Russian data files (skips download; use a pre-populated location). |
81
+ | `CALLIOPE_RUSSIAN_DATA_URL` | Base URL to fetch the assets from (defaults to the project's HuggingFace space). |
82
+ | `CALLIOPE_RUSSIAN_UDPIPE_MODEL` | Absolute path to just the `.udpipe` model file. |
83
+
84
+ Asset resolution searches, in order: `$CALLIOPE_RUSSIAN_DATA` → `data/` beside the running module (`src/russian/data` or `dist/russian/data`) → the sibling `src/russian/data` when running compiled code from `dist/` (the Docker / HuggingFace layout) → cwd-relative fallbacks. This means a source checkout, a compiled `dist/`, a Docker/HF deployment, and an npm-installed dependency all locate the data without any manual copying.
85
+
86
+ The Russian assets — dictionaries, methodology, and the SynTagRus UDPipe model — are redistributed here under the **MIT License** of Ilya Koziev's RussianPoetryScansionTool distribution, which is how they reached this project and which permits commercial use. A CC BY-NC-SA 4.0 reference file is also retained for the SynTagRus model, reflecting the terms under which it is distributed elsewhere; see [License and credits](#license-and-credits).
87
+
88
+ ---
89
+
64
90
  ## Quick start (CLI)
65
91
 
66
92
  ```bash
@@ -98,6 +124,36 @@ There are two display modes:
98
124
 
99
125
  ---
100
126
 
127
+ ## The web app (browser interface)
128
+
129
+ Everything the CLI prints — and several instruments beyond it — is also available
130
+ as a local web app with two visual skins (a bright-on-black **Terminal** display
131
+ and a parchment **Manuscript** display, switchable in ⚙ Settings):
132
+
133
+ ```bash
134
+ # from a clone or an installed package
135
+ npm run web # then open http://localhost:4321
136
+
137
+ # or, installed globally / via npx
138
+ calliope-web
139
+ PORT=8080 calliope-web # any port you like
140
+ ```
141
+
142
+ The app serves four instruments in one place:
143
+
144
+ | Tab | What it does |
145
+ |---|---|
146
+ | **Scansion** | The full reading view: every syllable tinted by stress, per-line meter tags with fit %, rhyme letters, a per-line detailed scan (feet, syllable anatomy, prosodic phrasing, key stresses, dependency arcs, Scandroid comparison), poem synopsis, phonopoetics, and rich word/syllable hover-profiles backed by Nounsing Pro. A **⧉ Copy stress map** button exports the whole poem's stress keys as CLI-style plain text. |
147
+ | **Rhyme Forge** | Word dossiers from the augmented lexicon: rhymes (strict or by syllable count), rime phonemes, metrical feet inside words, and Meter Match — cast a stress contour (`010`) and summon every word that carries it. |
148
+ | **Rewrites** | Nounsing Pro's transmutations: stress-skeleton, phoneme-echo, or rhyme-swap rewrites with POS-fidelity and word-frequency (Zipf floor) controls. |
149
+ | **Syntax** | The UDPipe dependency parse (with DepEdit-TS repairs) as arc diagrams and a glossed register, line-by-line or fused into one continuous text. |
150
+
151
+ The web app has **no extra dependencies** — it is a single zero-dependency Node
152
+ server (`webapp/server.mjs`) importing the same `dist/` build the CLI uses, plus
153
+ a vanilla-JS front-end. It only talks to `localhost`; nothing leaves your machine.
154
+
155
+ ---
156
+
101
157
  ## Reading the output
102
158
 
103
159
  ### The five stress levels
@@ -360,7 +416,7 @@ Calliope TS is developed by **Aleksey Calvin** / [SilverAgePoets.com](https://ww
360
416
  ---
361
417
 
362
418
  **Limitations to know about.**
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).
419
+ - Two languages are supported: **English** (the primary engine — its dictionary and phonological rules) and **Russian** (a parallel accentological engine, see [Russian support](#russian-support)). Other languages will produce nonsense rather than errors, since the UDPipe backend parses them but the prosodic rules are language-specific.
364
420
  - Stress-doublet words (*rebel*, *content*, names like *Hugo*) are read with their dictionary stress; a correction is on the roadmap.
365
421
  - Rare or foreign proper names fall back to rule-based stress guesses.
366
422
 
@@ -400,4 +456,14 @@ Methodological and conceptual debts:
400
456
  - 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
457
  - Milan Straka and the UDPipe project (for the robust neural parsing architecture now driving the core mechanics),
402
458
  - 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>
459
+
460
+ ### The Russian pipeline
461
+
462
+ Calliope's Russian side owes an especial and foundational debt to **Ilya Koziev** and the **"Natural Language Processing in Russian"** group, whose [RussianPoetryScansionTool](https://github.com/Koziev/RussianPoetryScansionTool) served as the principal blueprint for much (though not all) of Calliope's Russian accentology and scansion methodology. Koziev's tool is generously distributed under the **MIT License** (which permits commercial use), and it is through it that the **SynTagRus UDPipe model** first came to this project. With deep respect and gratitude for their excellent work, any questions concerning the SynTagRus model or the underlying Russian-scansion methodology are most appropriately directed to Ilya Koziev and the NLP-in-Russian group.
463
+
464
+ The Russian data assets bundle their license files, mirrored in the project's [HuggingFace deployment](https://huggingface.co/spaces/AlekseyCalvin/cts/tree/main/src/russian/data):
465
+ - `src/russian/data/LICENSE.txt` — the **MIT** license of the RussianPoetryScansionTool / NLP-in-Russian group. This is the license under which the Russian assets used here — dictionaries, methodology, and the SynTagRus UDPipe model — were obtained and are redistributed. It permits commercial use.
466
+ - `src/russian/data/LICENSE_UDpipe_SynTagRus.txt` — a **CC BY-NC-SA 4.0** reference file, retained because SynTagRus/UDPipe models are distributed under those terms in other (upstream) channels. It is kept for transparency around that residual ambiguity; it does not govern the copy bundled here, which came via the MIT distribution above.
467
+
468
+ **A note on commercial use:** the Calliope-TS code (Apache-2.0) and the Russian assets as obtained here (MIT, via Koziev's distribution) permit commercial use. Because SynTagRus/UDPipe models are elsewhere distributed under CC BY-NC-SA 4.0 (non-commercial), anyone with strict compliance requirements may wish to independently confirm the provenance of their model file or substitute one whose licensing they control (point `CALLIOPE_RUSSIAN_UDPIPE_MODEL` at it). The **SynTagRus** treebank and the **UDPipe** framework are the work of Milan Straka et al. (Charles University / LINDAT); the Russian stress dictionaries and neural accentuator derive from the RussianPoetryScansionTool corpus.
403
469
  *Gareth McAleese: If you're reading this, please do email me!
@@ -1 +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"}
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;AAiJnD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAsG3D;AAyHD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CA6BzE"}
@@ -36,6 +36,8 @@
36
36
  // Output is per-word `phraseStress` (1 = strongest, punctuation = 0): the structural
37
37
  // prominence the relativiser then reads PER PHONOLOGICAL PHRASE to place beats.
38
38
  import { isPunct, isVerb, verbHasArgChild, isUnaccusativeOrPassive, isLowLocative, isFrameSetting, isLightNominalHead, } from './syntax.js';
39
+ import { compoundStressSide } from '../stress.js';
40
+ import { isMarginalModalUse, isSemiModalHaveTo } from './syntax.js';
39
41
  const NOUN = /^(NN|NNS|NNP|NNPS)$/;
40
42
  /** A head-dependent combination is a COMPOUND (Compound Stress Rule → primary LEFT)
41
43
  * when the parse labels it NOMD, or when a common-/proper-noun modifier sits
@@ -46,6 +48,12 @@ function isCompound(dep, head) {
46
48
  return true;
47
49
  if ((dep.canonicalRel ?? '') === 'EXT')
48
50
  return false;
51
+ // A POSSESSIVE noun modifier ("love's sway", "the belfies' bee-buzz" —
52
+ // nmod:poss, canonical CASE) is a syntactic PHRASE, not a compound: the NSR
53
+ // right-stresses it (love's SWAY). Without this the N+N surface-adjacency
54
+ // fallback swallowed possessives and fore-stressed them.
55
+ if ((dep.canonicalRel ?? '') === 'CASE')
56
+ return false;
49
57
  return NOUN.test(dep.lexicalClass) && NOUN.test(head.lexicalClass)
50
58
  && Math.abs(dep.absoluteIndex - head.absoluteIndex) === 1;
51
59
  }
@@ -88,9 +96,17 @@ function combineDecision(dep, head, children, words) {
88
96
  const depBefore = dep.absoluteIndex < head.absoluteIndex;
89
97
  const rightWins = depBefore ? 'head' : 'dep'; // NSR rightmost
90
98
  const leftWins = depBefore ? 'dep' : 'head'; // CSR leftmost
91
- // Compound Stress Rule first (handles the backwards-parsed N+N the tagger mislabels).
92
- if (isCompound(dep, head))
93
- return leftWins;
99
+ // Compound Stress Rule first (handles the backwards-parsed N+N the tagger
100
+ // mislabels) — but through compoundStressSide, the SINGLE source of truth the
101
+ // lexical pass and scenario rules already share, so the curated right-stress
102
+ // heads hold here too: a temporal/product head keeps phrasal end-stress
103
+ // ("a wondrous Moscow DAY", "apple PIE") — those are N+N by POS yet phrasal
104
+ // by prosody, and fore-stressing them handed the φ nuclear to the modifier.
105
+ if (isCompound(dep, head)) {
106
+ const [w1, w2] = depBefore ? [dep, head] : [head, dep];
107
+ const side = compoundStressSide(w1.word, w1.lexicalClass, w2.word, w2.lexicalClass);
108
+ return side === 'right' ? rightWins : leftWins;
109
+ }
94
110
  if (rel === 'EXT')
95
111
  return rightWins; // proper-name head (New YORK)
96
112
  switch (rel) {
@@ -126,8 +142,13 @@ function combineDecision(dep, head, children, words) {
126
142
  case 'AMOD':
127
143
  return depBefore ? 'match' : 'head'; // pre-modifier → own domain; post → subordinate
128
144
  case 'ADVMOD':
129
- if (/^JJ/.test(head.lexicalClass))
130
- return 'head'; // degree adv ("very TALL") integrates
145
+ // A degree/measure adverb premodifying an ADJECTIVE or another ADVERB
146
+ // integrates with its head the head is the argument and keeps the
147
+ // accent ("very TALL", "far aWAY", "so MUCH"): adverb-over-adverb
148
+ // premodification is essentially always degree, unlike VP-adverbs,
149
+ // which keep their own accentual domain.
150
+ if (/^(JJ|RB)/.test(head.lexicalClass))
151
+ return 'head';
131
152
  return depBefore ? 'match' : 'head'; // pre-adv → own domain; post → subordinate
132
153
  case 'ACL':
133
154
  case 'ADVCL':
@@ -260,6 +281,25 @@ export function computePhraseStress(sent) {
260
281
  // Accent-level clash reduction (Wagner §6.4.1): runs BEFORE the syllable-level
261
282
  // resolveStressClashes (relstress.ts) and is additional to it. See below.
262
283
  reduceAccentClashes(words);
284
+ // Newman's generalization (1946; Wagner §2.4.1): "nuclear stress is perceived
285
+ // on the LAST of the grid marks that project highest." MATCH cycles
286
+ // legitimately co-designate sisters (coordination, the specifier restriction),
287
+ // so the cyclic levels can tie at the top — but the utterance NUCLEAR is
288
+ // unique: the final co-equal keeps the top rank and earlier co-equals are
289
+ // perceived one step lower (Mary(2) ate sweet ICE(1) cream — the subject
290
+ // matched the branching VP, yet the ear resolves rightward). One demotion
291
+ // step only; the rest of the gradient is untouched, so per-φ beat selection
292
+ // (which reads the LOCAL minimum) is unaffected.
293
+ {
294
+ const ranked = words.filter(w => (w.phraseStress || 0) > 0);
295
+ if (ranked.length > 1) {
296
+ const min = Math.min(...ranked.map(w => w.phraseStress));
297
+ const atMin = ranked.filter(w => w.phraseStress === min)
298
+ .sort((a, b) => a.absoluteIndex - b.absoluteIndex);
299
+ for (let i = 0; i < atMin.length - 1; i++)
300
+ atMin[i].phraseStress = min + 1;
301
+ }
302
+ }
263
303
  }
264
304
  /** Accent-level clash reduction (Wagner §6.4.1). Operates on which CONTENT words
265
305
  * bear a phrasal accent (a local prominence), NOT on syllable stress. A run of
@@ -330,6 +370,20 @@ function phiProjects(dep, head, children, words) {
330
370
  switch (dep.canonicalRel ?? '') {
331
371
  case 'CCOMP':
332
372
  case 'XCOMP':
373
+ // A BARE-infinitive complement of a marginal modal ("dare [he] aspire",
374
+ // "dare seize the fire") is one prosodic verb cluster with its functor —
375
+ // the modal reduces and the infinitive holds the beat, so splitting them
376
+ // into separate φs would strand the reduced modal as a beat-less φ (which
377
+ // the anchor fallback would then wrongly re-promote).
378
+ if (isMarginalModalUse(head, words))
379
+ return false;
380
+ // The TO-infinitive complement of semi-modal "have to" is likewise one
381
+ // prosodic verb cluster with its reduced functor ("do I have to bless"):
382
+ // splitting φ between "have" and "to bless" strands the reduced "have",
383
+ // which the anchor fallback then wrongly re-promotes to the φ nuclear.
384
+ if (isSemiModalHaveTo(head, words))
385
+ return false;
386
+ return true;
333
387
  case 'ADVCL':
334
388
  case 'CONJ':
335
389
  return true;
@@ -1,4 +1,8 @@
1
1
  import { ClsSentence } from '../types.js';
2
- /** Populate `canonicalRel` for every word, then apply label-only repairs. */
2
+ /** Populate `canonicalRel` for every word, then apply label-only repairs.
3
+ * One HEAD-CHANGING repair (fixPossessiveApposition) runs first, before the
4
+ * labels are computed — it is the single exception to this file's additive
5
+ * contract, and like the POS corrections of Stage F1 it mutates only the
6
+ * Calliope view of the parse (Clio never runs this pass). */
3
7
  export declare function normalizeDeps(sent: ClsSentence): void;
4
8
  //# sourceMappingURL=deps.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../src/calliope/deps.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,WAAW,EAAW,MAAM,aAAa,CAAC;AAkBnD,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAIrD"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../src/calliope/deps.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,WAAW,EAAW,MAAM,aAAa,CAAC;AAkBnD;;;;8DAI8D;AAC9D,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAKrD"}
@@ -26,18 +26,68 @@ function gov(w) {
26
26
  function preHead(w, head) {
27
27
  return w.absoluteIndex + 1 === head.absoluteIndex;
28
28
  }
29
- /** Populate `canonicalRel` for every word, then apply label-only repairs. */
29
+ /** Populate `canonicalRel` for every word, then apply label-only repairs.
30
+ * One HEAD-CHANGING repair (fixPossessiveApposition) runs first, before the
31
+ * labels are computed — it is the single exception to this file's additive
32
+ * contract, and like the POS corrections of Stage F1 it mutates only the
33
+ * Calliope view of the parse (Clio never runs this pass). */
30
34
  export function normalizeDeps(sent) {
35
+ fixPossessiveApposition(sent.words);
31
36
  for (const w of sent.words)
32
37
  w.canonicalRel = canonical(w);
33
38
  fixDitransitive(sent.words);
34
39
  inferPrenominalModifiers(sent.words);
35
40
  }
36
- /** Reliable structural relations whose label adjacency must NOT override. */
41
+ /** A POSSESSIVE-marked noun cannot head a following noun as its APPOSITION —
42
+ * a possessor is a dependent by definition. UDPipe parses Shakespeare's
43
+ * "…more red than her lips' red" with red₂ ←appos← lips' (and lips' ←obl←
44
+ * red₁), inverting the real structure ("the red of her lips"): the deep
45
+ * demotion this hands red₂ wrecked the line's iambic grid. Repair: the
46
+ * appositive noun takes over the possessive's relation and governor; the
47
+ * possessive becomes its poss dependent — which the NSR right-stresses
48
+ * (her lips' RED, like love's SWAY). Gated on the OVERT possessive mark
49
+ * (trailing 's or s'), so genuine appositions ("my brother the doctor")
50
+ * never match. */
51
+ function fixPossessiveApposition(words) {
52
+ for (const b of words) {
53
+ if (rawRel(b) !== 'appos')
54
+ continue;
55
+ if (!NOUN.test(b.lexicalClass))
56
+ continue;
57
+ const a = gov(b);
58
+ if (!a || !NOUN.test(a.lexicalClass))
59
+ continue;
60
+ if (a.absoluteIndex >= b.absoluteIndex)
61
+ continue; // possessive precedes
62
+ if (!/['’]s$|s['’]$/.test(a.word))
63
+ continue; // overt possessive mark
64
+ const aGov = gov(a);
65
+ if (!aGov || aGov === b || !a.dependency || !b.dependency)
66
+ continue;
67
+ // B inherits A's governor and relation; A becomes B's possessive.
68
+ b.dependency.governor = aGov;
69
+ b.dependency.dependentType = a.dependency.dependentType;
70
+ a.dependency.governor = b;
71
+ a.dependency.dependentType = 'nmod:poss';
72
+ // A's case marker ("than") re-points to B, the true oblique head.
73
+ for (const c of words) {
74
+ if (c !== a && c !== b && gov(c) === a && c.dependency &&
75
+ (rawRel(c) === 'case' || c.lexicalClass === 'IN' || c.lexicalClass === 'TO')) {
76
+ c.dependency.governor = b;
77
+ }
78
+ }
79
+ }
80
+ }
81
+ /** Reliable structural relations whose label adjacency must NOT override.
82
+ * CASE is here for the POSSESSIVE noun ("her lips' red" — nmod:poss →
83
+ * CASE): a possessive NNS immediately precedes its noun head, and letting
84
+ * surface adjacency relabel it NOMD hands it to the Compound Stress Rule,
85
+ * fore-stressing what the NSR must right-stress (her lips' RED, love's
86
+ * SWAY). */
37
87
  const STRUCTURAL = new Set([
38
88
  'ROOT', 'NSUBJ', 'NSUBJPASS', 'DOBJ', 'IOBJ', 'OBL', 'AUX', 'AUXPASS',
39
89
  'CCOMP', 'XCOMP', 'ADVCL', 'ADVMOD', 'AMOD', 'ACL', 'CC', 'CONJ', 'EXPL',
40
- 'INTJ', 'DISCOURSE', 'VPRT', 'COMPMARK', 'ADVMARK', 'EXT',
90
+ 'INTJ', 'DISCOURSE', 'VPRT', 'COMPMARK', 'ADVMARK', 'EXT', 'CASE',
41
91
  ]);
42
92
  /**
43
93
  * Pre-head modifier inference by SURFACE ADJACENCY — independent of en-parse's
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/calliope/engine.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA0D7C,eAAO,MAAM,cAAc,EAAE,aAG5B,CAAC"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/calliope/engine.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA+D7C,eAAO,MAAM,cAAc,EAAE,aAG5B,CAAC"}
@@ -10,7 +10,7 @@
10
10
  // replace the body with: canonical deps (Stage 1) → Match-Theory hierarchy
11
11
  // (Stage 2) → Scenario A–O stress (Stage 3) → phrase-stress ramp + relativise
12
12
  // (Stage 4). The legacy path remains untouched in `src/clio/engine.ts`.
13
- import { assignLexicalStress, applySurfacePostProcessing, detectDisplayPrefixes, applyStressShift } from '../stress.js';
13
+ import { assignLexicalStress, applySurfacePostProcessing, detectDisplayPrefixes, applyStressShift, applyRhythmicPeakPlacement } from '../stress.js';
14
14
  import { correctPosWithLexicon } from './postag.js';
15
15
  import { tagNames } from './names.js';
16
16
  import { parseFeats } from './feats.js';
@@ -55,6 +55,11 @@ function analyzeSentenceCalliope(sent) {
55
55
  // Rhythm Rule clash). Runs after hierarchy (needs PP-initial info) but before
56
56
  // phrase stress (so the shifted peak flows into the NSR ramp).
57
57
  applyStressShift(sent.words, ius);
58
+ // Rhythm-Rule peak placement for bistable disyllables (contourless
59
+ // shift-likely words like "into"; fused negatives like "cannot"): the peak
60
+ // is selected by grid alternation with the neighbouring lexical stresses,
61
+ // before phrase stress reads the contour.
62
+ applyRhythmicPeakPlacement(sent.words);
58
63
  computePhraseStress(sent);
59
64
  computeRelativeStress(sent.words, ius);
60
65
  // Surface-order post-processing passes shared with the Clio engine: compound
@@ -1 +1 @@
1
- {"version":3,"file":"relstress.d.ts","sourceRoot":"","sources":["../../src/calliope/relstress.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAyB,MAAM,aAAa,CAAC;AAmkB/E,uEAAuE;AACvE,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAkCrF"}
1
+ {"version":3,"file":"relstress.d.ts","sourceRoot":"","sources":["../../src/calliope/relstress.ts"],"names":[],"mappings":"AA8BA,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAyB,MAAM,aAAa,CAAC;AAq7B/E,uEAAuE;AACvE,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAkCrF"}