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/dist/rhyme.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { LineResult, StressLevel } from './types.js';
2
- export type RhymeTypeName = 'identical' | 'rich' | 'perfect' | 'family' | 'assonant' | 'consonant' | 'augmented' | 'diminished' | 'wrenched' | 'eye';
2
+ export type RhymeTypeName = 'identical' | 'rich' | 'perfect' | 'family' | 'assonant' | 'consonant' | 'augmented' | 'diminished' | 'grammatical' | 'mosaic' | 'wrenched' | 'eye';
3
3
  export interface RhymePair {
4
4
  type: RhymeTypeName;
5
5
  /** masculine (stress on final syllable) / feminine (penult) / dactylic (antepenult) */
@@ -7,6 +7,34 @@ export interface RhymePair {
7
7
  }
8
8
  /** Classify the rhyme relation between two line-end words (or null). */
9
9
  export declare function classifyRhymePair(a: string, b: string): RhymePair | null;
10
+ /** A BEAT-BEARING end-rhyme between two line-end words — the bar the stanza
11
+ * rhyme-fellow pass (index.ts markRhymeFellows) requires before a weak
12
+ * line-final monosyllable may inherit a beat from its rhyme partner. Beyond
13
+ * the STRONG tier (perfect/rich/family) it admits AUGMENTED and DIMINISHED
14
+ * pairs: their stressed vowel is identical and they differ by ONE terminal
15
+ * consonant, so the correspondence is still carried audibly by the final
16
+ * beat ("me"/"dream", "sea"/"seed"). Identical repetition is excluded
17
+ * (epistrophe, not a prominence template), as are the looser slant tiers
18
+ * (assonant/consonant/grammatical/wrenched/eye) — a hunch is not a beat. */
19
+ export declare function isBeatTransferRhyme(a: string, b: string): boolean;
20
+ /**
21
+ * MOSAIC (multi-word) end-rhyme — Byron's "intellectual / hen-pecked you all";
22
+ * the maintainer's "TENderly / SLENder — SEE?". One line's POLYSYLLABIC rhyme
23
+ * part (its final word's last stressed vowel + everything after, ≥2 syllables)
24
+ * is covered by a SEQUENCE of words at the other line's end, the rhyme domain
25
+ * re-segmented across the word boundary. Detection is nucleus-aligned:
26
+ * • the covering span opens on a STRESSED vowel exactly n syllables from the
27
+ * line end (n = the anchor's rhyme-part syllable count) and must CROSS a
28
+ * word boundary (a one-word cover is ordinary rhyme, handled elsewhere);
29
+ * • every nucleus must match its opposite number;
30
+ * • the consonant runs between nuclei must match, except AT MOST ONE run may
31
+ * differ (by substitution or one phone of length) — the re-segmentation
32
+ * fudge that makes tender-LY ~ slender-SEE (L→S) and ~ slender-BE (L→B)
33
+ * ring true, while "tenderly / slender seed" (two differing runs) fails.
34
+ * This is inherently beyond a one-word-at-a-time rhyme dictionary (Nounsing);
35
+ * only the line-holding layer can see it.
36
+ */
37
+ export declare function classifyMosaicPair(tailA: string[], tailB: string[]): RhymePair | null;
10
38
  export interface LineRhyme {
11
39
  endWord: string;
12
40
  letter: string;
@@ -14,8 +42,12 @@ export interface LineRhyme {
14
42
  matchedLine?: number;
15
43
  }
16
44
  /** Detect a stanza's rhyme scheme from its line-end words. Strong rhymes
17
- * bind; slant-tier rhymes bind only when no strong candidate exists. */
18
- export declare function detectScheme(endWords: string[]): LineRhyme[];
45
+ * bind; slant-tier rhymes bind only when no strong candidate exists. When
46
+ * `endTails` (each line's last few words) is supplied, a MOSAIC multi-word
47
+ * rhyme is tried wherever the single-word pair yields nothing strong — the
48
+ * mosaic verdict outranks a looser single-word slant reading ("tenderly /
49
+ * slender — see?" is mosaic, not a wrenched hunch on tenderly/see). */
50
+ export declare function detectScheme(endWords: string[], endTails?: string[][]): LineRhyme[];
19
51
  /**
20
52
  * Annotate rhyme letters/types (`detail.rhyme`) and form verdicts
21
53
  * (`detail.formNote`) across a poem. Stanza forms are per-stanza; a poem-level
@@ -1 +1 @@
1
- {"version":3,"file":"rhyme.d.ts","sourceRoot":"","sources":["../src/rhyme.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAW,UAAU,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;AAK1F,MAAM,MAAM,aAAa,GACrB,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAC3C,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,GACrD,UAAU,GAAG,KAAK,CAAC;AAEvB,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,aAAa,CAAC;IACpB,uFAAuF;IACvF,SAAS,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,UAAU,CAAC;CACnD;AA6DD,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAkDxE;AAQD,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;yEACyE;AACzE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CA6C5D;AA6MD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,IAAI,CAkB/D;AAsCD,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IAAG,OAAO,EAAE,MAAM,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,KAAK,GAAG,UAAU,GAAG,MAAM,CAAC;IAClC,SAAS,EAAE,WAAW,CAAC;CACxB;AACD,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,QAAQ,EAAE,CAAC;IAChB,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IACnD,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnE;AAED;0EAC0E;AAC1E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,YAAY,CAsIzE;AAgCD,mEAAmE;AACnE,MAAM,WAAW,WAAW;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;CAAE;AA2B9D;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,WAAW,EAAE,CA0DpE"}
1
+ {"version":3,"file":"rhyme.d.ts","sourceRoot":"","sources":["../src/rhyme.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAW,UAAU,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;AAK1F,MAAM,MAAM,aAAa,GACrB,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAC3C,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,GACrD,aAAa,GAAG,QAAQ,GACxB,UAAU,GAAG,KAAK,CAAC;AAEvB,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,aAAa,CAAC;IACpB,uFAAuF;IACvF,SAAS,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,UAAU,CAAC;CACnD;AA6DD,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CA8ExE;AASD;;;;;;;;6EAQ6E;AAC7E,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAIjE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,IAAI,CAErF;AAwED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;wEAKwE;AACxE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,SAAS,EAAE,CAiDnF;AAmND;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,IAAI,CAkB/D;AAsCD,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IAAG,OAAO,EAAE,MAAM,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,KAAK,GAAG,UAAU,GAAG,MAAM,CAAC;IAClC,SAAS,EAAE,WAAW,CAAC;CACxB;AACD,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,QAAQ,EAAE,CAAC;IAChB,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IACnD,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnE;AAED;0EAC0E;AAC1E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,YAAY,CAsIzE;AAgCD,mEAAmE;AACnE,MAAM,WAAW,WAAW;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;CAAE;AA2B9D;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,WAAW,EAAE,CA0DpE"}
package/dist/rhyme.js CHANGED
@@ -120,15 +120,42 @@ export function classifyRhymePair(a, b) {
120
120
  if (vowelSame) {
121
121
  // Same stressed vowel. Matching-length codas whose consonants pair up
122
122
  // within one phonetic family (wet/deck, dame/grain) → family rhyme.
123
- if (codaA.length === codaB.length && codaA.length > 0
123
+ // The segments must ALSO be the same length overall — the coda test
124
+ // filters vowels out, so without the length gate an extra tail syllable
125
+ // vanishes and fun/funny reads "family" (it is grammatical, below).
126
+ if (na.length === nb.length && codaA.length === codaB.length && codaA.length > 0
124
127
  && codaA.every((p, i) => p === codaB[i] || sameFamily(p, codaB[i]))) {
125
128
  return { type: 'family', structure };
126
129
  }
127
- // One extra trailing consonant on the second/first word (bray/brave).
128
- if (codaA.length + 1 === codaB.length && codaA.every((p, i) => p === codaB[i]))
130
+ // AUGMENTED / DIMINISHED (poemshape's Dickinson taxonomy): the rhyme is
131
+ // "extended by a consonant" ONE extra TERMINAL consonant on one side,
132
+ // compared over the FULL rhyme segments, vowels included (bray/brave,
133
+ // grow/sown, stained/rain; manner/manners feminine). The previous
134
+ // coda-only comparison filtered the tail's unstressed VOWELS away before
135
+ // comparing, so a whole extension syllable could masquerade as a terminal
136
+ // consonant (pun/running "augmented", floating/float "diminished") — the
137
+ // same corruption the maintainer traced in Nounsing's search lineage.
138
+ const extraTerminalConsonant = (shorter, longer) => longer.length === shorter.length + 1 &&
139
+ shorter.every((p, i) => p === longer[i]) &&
140
+ !isVowelPhone(longer[longer.length - 1]);
141
+ if (extraTerminalConsonant(na, nb))
129
142
  return { type: 'augmented', structure };
130
- if (codaB.length + 1 === codaA.length && codaB.every((p, i) => p === codaA[i]))
143
+ if (extraTerminalConsonant(nb, na))
131
144
  return { type: 'diminished', structure };
145
+ // GRAMMATICAL rhyme: matching stressed vowel at the roots, distinct
146
+ // INFLECTIONAL tails (pun/running, fun/funny, float/floating). One full
147
+ // segment is a proper prefix of the other, the excess is SYLLABIC
148
+ // (contains a vowel — a bare terminal consonant was aug/dim above), and
149
+ // the longer word's spelling ends in a closed inflectional suffix.
150
+ const shorterFirst = na.length <= nb.length;
151
+ const segS = shorterFirst ? na : nb;
152
+ const segL = shorterFirst ? nb : na;
153
+ const wordL = shorterFirst ? wb : wa;
154
+ if (segL.length > segS.length && segS.every((p, i) => p === segL[i])
155
+ && segL.slice(segS.length).some(p => isVowelPhone(p))
156
+ && GRAMMATICAL_TAILS.some(t => wordL.endsWith(t))) {
157
+ return { type: 'grammatical', structure };
158
+ }
132
159
  return { type: 'assonant', structure };
133
160
  }
134
161
  if (codaEq && codaA.length > 0)
@@ -136,20 +163,153 @@ export function classifyRhymePair(a, b) {
136
163
  const t = orthographicTier(a, b, pa, pb);
137
164
  return t ? { type: t } : null;
138
165
  }
166
+ /** Closed inflectional/light-derivational tails for the GRAMMATICAL rhyme
167
+ * test (spelling-level; the phone-level prefix condition does the real work). */
168
+ const GRAMMATICAL_TAILS = [
169
+ 'ings', 'ing', "in'", 'ied', 'ies', 'ed', 'es', 'ers', 'er', 'est', 'eth',
170
+ 'ly', 'y', 's', "'d", "'st",
171
+ ];
172
+ /** A BEAT-BEARING end-rhyme between two line-end words — the bar the stanza
173
+ * rhyme-fellow pass (index.ts markRhymeFellows) requires before a weak
174
+ * line-final monosyllable may inherit a beat from its rhyme partner. Beyond
175
+ * the STRONG tier (perfect/rich/family) it admits AUGMENTED and DIMINISHED
176
+ * pairs: their stressed vowel is identical and they differ by ONE terminal
177
+ * consonant, so the correspondence is still carried audibly by the final
178
+ * beat ("me"/"dream", "sea"/"seed"). Identical repetition is excluded
179
+ * (epistrophe, not a prominence template), as are the looser slant tiers
180
+ * (assonant/consonant/grammatical/wrenched/eye) — a hunch is not a beat. */
181
+ export function isBeatTransferRhyme(a, b) {
182
+ const r = classifyRhymePair(a, b);
183
+ if (!r || r.type === 'identical')
184
+ return false;
185
+ return STRONG.has(r.type) || r.type === 'augmented' || r.type === 'diminished';
186
+ }
187
+ /**
188
+ * MOSAIC (multi-word) end-rhyme — Byron's "intellectual / hen-pecked you all";
189
+ * the maintainer's "TENderly / SLENder — SEE?". One line's POLYSYLLABIC rhyme
190
+ * part (its final word's last stressed vowel + everything after, ≥2 syllables)
191
+ * is covered by a SEQUENCE of words at the other line's end, the rhyme domain
192
+ * re-segmented across the word boundary. Detection is nucleus-aligned:
193
+ * • the covering span opens on a STRESSED vowel exactly n syllables from the
194
+ * line end (n = the anchor's rhyme-part syllable count) and must CROSS a
195
+ * word boundary (a one-word cover is ordinary rhyme, handled elsewhere);
196
+ * • every nucleus must match its opposite number;
197
+ * • the consonant runs between nuclei must match, except AT MOST ONE run may
198
+ * differ (by substitution or one phone of length) — the re-segmentation
199
+ * fudge that makes tender-LY ~ slender-SEE (L→S) and ~ slender-BE (L→B)
200
+ * ring true, while "tenderly / slender seed" (two differing runs) fails.
201
+ * This is inherently beyond a one-word-at-a-time rhyme dictionary (Nounsing);
202
+ * only the line-holding layer can see it.
203
+ */
204
+ export function classifyMosaicPair(tailA, tailB) {
205
+ return mosaicDirected(tailA, tailB) ?? mosaicDirected(tailB, tailA);
206
+ }
207
+ function mosaicDirected(anchorTail, coverTail) {
208
+ if (anchorTail.length === 0 || coverTail.length < 2)
209
+ return null;
210
+ const anchorWord = anchorTail[anchorTail.length - 1];
211
+ const pa = phonesOf(anchorWord);
212
+ if (!pa)
213
+ return null;
214
+ const ia = lastStressedIdx(pa);
215
+ if (ia < 0)
216
+ return null;
217
+ const ra = pa.slice(ia);
218
+ const nA = ra.filter(p => isVowelPhone(p)).length;
219
+ if (nA < 2)
220
+ return null; // needs a feminine/dactylic anchor
221
+ const perWord = [];
222
+ for (const w of coverTail) {
223
+ const p = phonesOf(w);
224
+ if (!p)
225
+ return null;
226
+ perWord.push(p);
227
+ }
228
+ const combined = perWord.flat();
229
+ const lastWordStart = combined.length - perWord[perWord.length - 1].length;
230
+ // The covering span opens at the vowel sitting nA nuclei from the line end.
231
+ let seen = 0;
232
+ let p0 = -1;
233
+ for (let i = combined.length - 1; i >= 0; i--) {
234
+ if (isVowelPhone(combined[i])) {
235
+ seen++;
236
+ if (seen === nA) {
237
+ p0 = i;
238
+ break;
239
+ }
240
+ }
241
+ }
242
+ if (p0 < 0)
243
+ return null;
244
+ if (!/[12]$/.test(combined[p0]))
245
+ return null; // span opens on a stressed vowel
246
+ if (p0 >= lastWordStart)
247
+ return null; // must cross a word boundary
248
+ const rb = combined.slice(p0);
249
+ // Nucleus-by-nucleus alignment with the one-run fudge.
250
+ const decompose = (seg) => {
251
+ const nuclei = [];
252
+ const runs = [];
253
+ let cur = [];
254
+ for (const p of seg) {
255
+ if (isVowelPhone(p)) {
256
+ if (nuclei.length)
257
+ runs.push(cur);
258
+ cur = [];
259
+ nuclei.push(base(p));
260
+ }
261
+ else {
262
+ cur.push(base(p));
263
+ }
264
+ }
265
+ runs.push(cur); // final coda
266
+ return { nuclei, runs };
267
+ };
268
+ const A = decompose(ra);
269
+ const B = decompose(rb);
270
+ if (A.nuclei.length !== B.nuclei.length)
271
+ return null;
272
+ if (A.nuclei.some((v, i) => v !== B.nuclei[i]))
273
+ return null;
274
+ let fudged = 0;
275
+ for (let i = 0; i < A.runs.length; i++) {
276
+ const x = A.runs[i];
277
+ const y = B.runs[i];
278
+ if (x.length === y.length && x.every((p, k) => p === y[k]))
279
+ continue;
280
+ if (Math.abs(x.length - y.length) <= 1) {
281
+ fudged++;
282
+ continue;
283
+ }
284
+ return null;
285
+ }
286
+ if (fudged > 1)
287
+ return null;
288
+ return { type: 'mosaic', structure: nA === 2 ? 'feminine' : 'dactylic' };
289
+ }
139
290
  // Strength tiers for scheme detection.
140
291
  const STRONG = new Set(['identical', 'rich', 'perfect', 'family']);
141
- const SLANT = new Set(['assonant', 'consonant', 'augmented', 'diminished', 'wrenched', 'eye']);
292
+ const SLANT = new Set(['assonant', 'consonant', 'augmented', 'diminished', 'grammatical', 'mosaic', 'wrenched', 'eye']);
142
293
  // Full rhyme only — the stricter bar a pre-caesural INTERNAL rhyme must clear.
143
294
  const STRICT_INTERNAL = new Set(['identical', 'rich', 'perfect']);
144
295
  /** Detect a stanza's rhyme scheme from its line-end words. Strong rhymes
145
- * bind; slant-tier rhymes bind only when no strong candidate exists. */
146
- export function detectScheme(endWords) {
296
+ * bind; slant-tier rhymes bind only when no strong candidate exists. When
297
+ * `endTails` (each line's last few words) is supplied, a MOSAIC multi-word
298
+ * rhyme is tried wherever the single-word pair yields nothing strong — the
299
+ * mosaic verdict outranks a looser single-word slant reading ("tenderly /
300
+ * slender — see?" is mosaic, not a wrenched hunch on tenderly/see). */
301
+ export function detectScheme(endWords, endTails) {
147
302
  const out = [];
148
303
  let nextLetter = 0;
149
304
  for (let i = 0; i < endWords.length; i++) {
150
305
  let best = null;
151
306
  for (let j = i - 1; j >= 0; j--) {
152
- const pair = classifyRhymePair(endWords[j], endWords[i]);
307
+ let pair = classifyRhymePair(endWords[j], endWords[i]);
308
+ if ((!pair || !STRONG.has(pair.type)) && endTails?.[j]?.length && endTails?.[i]?.length) {
309
+ const m = classifyMosaicPair(endTails[j], endTails[i]);
310
+ if (m)
311
+ pair = m;
312
+ }
153
313
  if (!pair)
154
314
  continue;
155
315
  if (STRONG.has(pair.type)) {
@@ -327,6 +487,11 @@ function lineEndWord(line) {
327
487
  const ws = lineWords(line);
328
488
  return ws.length ? surfaceOf(ws[ws.length - 1]) : '';
329
489
  }
490
+ /** The line's last few syllable-bearing words — the material a MOSAIC
491
+ * multi-word rhyme may re-segment across (classifyMosaicPair). */
492
+ function lineEndTail(line, maxWords = 4) {
493
+ return lineWords(line).slice(-maxWords).map(surfaceOf);
494
+ }
330
495
  // ─── INTERNAL (PRE-CAESURAL) RHYME — additive layer ─────────────────
331
496
  //
332
497
  // The per-stanza END-rhyme scheme (detectScheme) is the primary, UNTOUCHED
@@ -444,7 +609,7 @@ export function applyRhymeAndForm(stanzas) {
444
609
  // stanza) — that is what couplet/quatrain/sonnet patterns are defined over.
445
610
  const analyzed = stanzas.map(lines => {
446
611
  const details = lines.map(l => l.phonologicalScansion);
447
- const rhymes = detectScheme(lines.map(lineEndWord));
612
+ const rhymes = detectScheme(lines.map(lineEndWord), lines.map(l => lineEndTail(l)));
448
613
  return { lines, details, rhymes };
449
614
  });
450
615
  for (const { details, rhymes } of analyzed) {
@@ -514,7 +679,7 @@ export function analyzePhonopoetics(stanzas) {
514
679
  };
515
680
  // ── Phase 1: END rhymes (poem-wide) ──
516
681
  const endWords = PLs.map(pl => lineEndWord(pl.line));
517
- const es = detectScheme(endWords);
682
+ const es = detectScheme(endWords, PLs.map(pl => lineEndTail(pl.line)));
518
683
  const endScheme = canonicalScheme(es);
519
684
  const byLetter = new Map();
520
685
  es.forEach((r, i) => { if (r.letter !== '·') {
@@ -740,7 +905,7 @@ export function summarizePoem(stanzas) {
740
905
  // ── Form — the form layer's verdict(s) + registry-scheme alignment. (The
741
906
  // detailed rhyme listing now lives in the Phonopoetics section; the synopsis
742
907
  // carries only the canonical End-Rhyme Scheme, below.) ──
743
- const endRhymes = N >= 2 ? detectScheme(lines.map(lineEndWord)) : [];
908
+ const endRhymes = N >= 2 ? detectScheme(lines.map(lineEndWord), lines.map(l => lineEndTail(l))) : [];
744
909
  const endScheme = endRhymes.length ? canonicalScheme(endRhymes) : '';
745
910
  const hasEndRhyme = endRhymes.some(r => r.letter !== '·');
746
911
  const forms = [...new Set(details.map(d => d.formNote).filter((x) => !!x))];
@@ -0,0 +1,31 @@
1
+ interface AccentsData {
2
+ wordAccents: Map<string, number>;
3
+ ambiguousAccents: Record<string, Record<string, string[]>>;
4
+ ambiguousAccents2: Record<string, number[]>;
5
+ yoWords: Record<string, string>;
6
+ yoByGram: Record<string, any>;
7
+ secondaryStress: Record<string, number[]>;
8
+ rhymedWords: Set<[string, string]>;
9
+ rhymingDict: Record<string, string[]>;
10
+ derivationData: any;
11
+ }
12
+ declare function loadJson<T>(filename: string): T;
13
+ /** Load a JSON data file (used by other modules). */
14
+ export { loadJson };
15
+ export interface AccentResult {
16
+ stressPos: number;
17
+ secondaryStress: number[] | null;
18
+ ambiguous: boolean;
19
+ allStressPositions: number[];
20
+ }
21
+ /** Apply ёфикация: replace 'е' with 'ё' where appropriate. */
22
+ export declare function yoficate(word: string, feats?: Record<string, string>, upos?: string): string;
23
+ /** Get all accentuations for a word (multi-variant, like original get_accents). */
24
+ export declare function getAccents(word: string, feats?: Record<string, string>, upos?: string): AccentResult[];
25
+ /** Get stress position for a word, using dictionary + neural MLP fallback. */
26
+ export declare function getAccent(word: string, feats?: Record<string, string>, upos?: string): AccentResult;
27
+ /** Render a word with stress mark (U+0301 for primary, U+0300 for secondary). */
28
+ export declare function renderStressedWord(word: string, accent: AccentResult): string;
29
+ /** Get the data handle (for rhyme module). */
30
+ export declare function getAccentsData(): AccentsData;
31
+ //# sourceMappingURL=accentuator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accentuator.d.ts","sourceRoot":"","sources":["../../src/russian/accentuator.ts"],"names":[],"mappings":"AAuBA,UAAU,WAAW;IACnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IAC3D,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1C,WAAW,EAAE,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,cAAc,EAAE,GAAG,CAAC;CACrB;AAID,iBAAS,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CAGxC;AAED,qDAAqD;AACrD,OAAO,EAAE,QAAQ,EAAE,CAAC;AAuMpB,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,8DAA8D;AAC9D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CA+ChG;AAwCD,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,YAAY,EAAE,CA8E1G;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,YAAY,CAqHvG;AAsED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,CAe7E;AAED,8CAA8C;AAC9C,wBAAgB,cAAc,IAAI,WAAW,CAE5C"}