calliope-ts 0.0.2 → 0.0.3

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 (159) hide show
  1. package/README.md +3 -13
  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/clio/caesura.ts +145 -0
  130. package/src/clio/depfix.ts +88 -0
  131. package/src/clio/display.ts +1042 -0
  132. package/src/clio/engine.ts +38 -0
  133. package/src/clio/parser.ts +845 -0
  134. package/src/clio/phonological.ts +849 -0
  135. package/src/clio/phrasestress.ts +108 -0
  136. package/src/clio/pipeline.ts +154 -0
  137. package/src/clio/rhyme.ts +740 -0
  138. package/src/clio/scandroid.ts +434 -0
  139. package/src/clio/scansion.ts +1130 -0
  140. package/src/clio/semantics.ts +134 -0
  141. package/src/clio/stress.ts +1731 -0
  142. package/src/clio/tagfix.ts +104 -0
  143. package/src/display.ts +321 -137
  144. package/src/engine.ts +22 -0
  145. package/src/index.ts +176 -32
  146. package/src/parser.ts +346 -682
  147. package/src/phonological.ts +72 -4
  148. package/src/phrasestress.ts +108 -0
  149. package/src/rhyme.ts +428 -8
  150. package/src/scansion.ts +136 -15
  151. package/src/semantics.ts +134 -0
  152. package/src/stress.ts +794 -64
  153. package/src/types.ts +39 -4
  154. package/tests/DataForHayesLinesOnly.txt +364 -0
  155. package/tests/DataForHayesStressSymbolsRevised.txt +728 -0
  156. package/tests/basic.test.ts +443 -37
  157. package/tests/bench-hayes.mjs +72 -0
  158. package/tests/wagner-stress.test.ts +188 -0
  159. package/vitest.config.ts +0 -15
@@ -596,9 +596,33 @@ function quickSyllableCount(s: string): number {
596
596
  return Math.max(1, groups);
597
597
  }
598
598
 
599
- export function syllabifyWord(word: string, syllableCount: number, vowelLengths?: VowelLength[], morphSuffix?: string): string[] {
599
+ export function syllabifyWord(word: string, syllableCount: number, vowelLengths?: VowelLength[], morphSuffix?: string, morphPrefix?: string): string[] {
600
600
  if (syllableCount <= 1) return [word];
601
601
 
602
+ // Morpheme-aware prefix peel: when OOV stress assignment validated a productive
603
+ // prefix ("dis"), split it off as the first syllable(s) so the stem keeps its
604
+ // spelling (dis·il·lu·sions, not di·sil·lu·sions — the Maximal Onset principle
605
+ // would otherwise pull the prefix's final consonant into the next syllable).
606
+ // The prefix's own syllable count is estimated orthographically; the stem gets
607
+ // the remaining syllables.
608
+ if (morphPrefix && syllableCount >= 2
609
+ && word.toLowerCase().startsWith(morphPrefix)
610
+ && word.length > morphPrefix.length + 1) {
611
+ const prefixSylls = quickSyllableCount(morphPrefix);
612
+ if (prefixSylls < syllableCount) {
613
+ const prefixChunk = word.slice(0, morphPrefix.length);
614
+ const stemChunk = word.slice(morphPrefix.length);
615
+ const stemChunks = syllabifyWord(stemChunk, syllableCount - prefixSylls,
616
+ vowelLengths ? vowelLengths.slice(prefixSylls) : undefined, morphSuffix);
617
+ // If the prefix is polysyllabic, syllabify it on its own; monosyllabic → as-is.
618
+ const prefixChunks = prefixSylls > 1
619
+ ? syllabifyWord(prefixChunk, prefixSylls)
620
+ : [prefixChunk];
621
+ const result = [...prefixChunks, ...stemChunks];
622
+ if (result.length === syllableCount) return result;
623
+ }
624
+ }
625
+
602
626
  // Lexical compound boundary (someone → some·one, not so·meone). Only when the
603
627
  // constituents' own syllable counts add up to the requested total.
604
628
  {
@@ -653,8 +677,14 @@ export function syllabifyWord(word: string, syllableCount: number, vowelLengths?
653
677
  const lower = cleanWord.toLowerCase();
654
678
  const n = lower.length;
655
679
 
656
- // Common English consonant digraphs
657
- const DIGRAPHS = new Set(['ch', 'sh', 'th', 'wh', 'ph', 'gh', 'ck', 'ng', 'wr', 'kn', 'gn']);
680
+ // Common English consonant digraphs. "kn"/"gn"/"wr" are EXCLUDED: they are
681
+ // digraphs only WORD-INITIALLY (where the first consonant is silent "know",
682
+ // "gnaw", "write"), and the digraph set is used only for MEDIAL boundary
683
+ // placement. In medial position ("frankness") "kn" is two pronounced
684
+ // consonants: /k/ closes the first syllable (frank), /n/ opens the second
685
+ // (ness) — treating it as a medial digraph sends both to the next syllable
686
+ // ("fran-kness"), which is wrong.
687
+ const DIGRAPHS = new Set(['ch', 'sh', 'th', 'wh', 'ph', 'gh', 'ck', 'ng']);
658
688
  // Digraphs that commonly end syllables (codas)
659
689
  const CODA_DIGRAPHS = new Set(['ch', 'sh', 'ck', 'ng', 'th']);
660
690
  // "Muta cum liquida": an obstruent + liquid/glide that, between vowels, stays
@@ -679,6 +709,17 @@ export function syllabifyWord(word: string, syllableCount: number, vowelLengths?
679
709
  // (want·ed, embed·ded), so those are excluded.
680
710
  const endsSilentEd = n >= 3 && lower.endsWith('ed')
681
711
  && !VOWEL_CHARS.has(lower[n - 3]) && lower[n - 3] !== 't' && lower[n - 3] !== 'd';
712
+ // Non-syllabic inflectional "-es": the 'e' in a final "…Xes" (X a consonant
713
+ // other than a vowel — so "goes"/"shoes" are excluded, where 'e' is part of a
714
+ // vowel digraph) is silent when the nucleus count exceeds the CMU syllable
715
+ // count. This is the VCe pattern extended to plurals/3sg: the 'e' makes the
716
+ // preceding vowel long and is not itself pronounced (receives = re·CEIVES,
717
+ // makes = MAKES, writes = WRITES). When the stem ends in a sibilant
718
+ // (bus→buses, bush→bushes) the 'e' IS syllabic, but in that case the nucleus
719
+ // count already matches the syllable count and this check never fires — the
720
+ // surplus-nucleus guard makes it safe.
721
+ const endsSilentEs = n >= 3 && lower.endsWith('es')
722
+ && !VOWEL_CHARS.has(lower[n - 3]);
682
723
 
683
724
  interface Nucleus { start: number; end: number }
684
725
  const nuclei: Nucleus[] = [];
@@ -707,6 +748,33 @@ export function syllabifyWord(word: string, syllableCount: number, vowelLengths?
707
748
  const last = nuclei[nuclei.length - 1];
708
749
  if (last.start === n - 2 && last.end === n - 1) nuclei.pop();
709
750
  }
751
+ // Same for non-syllabic "-es": drop the silent 'e' when there's a surplus
752
+ // (receives = 3 nuclei → 2 syllables → drop → re·ceives, not recei·ves).
753
+ if (nuclei.length > syllableCount && endsSilentEs) {
754
+ const last = nuclei[nuclei.length - 1];
755
+ if (last.start === n - 2 && last.end === n - 1) nuclei.pop();
756
+ }
757
+
758
+ // Vowel hiatus: if we have TOO FEW nuclei, a multi-vowel nucleus (≥2 vowel
759
+ // characters scanned as one group) is a HIATUS — two syllabic vowels in
760
+ // adjacent syllables — not a diphthong. Split the rightmost multi-vowel
761
+ // nucleus, peeling off its last vowel char as a new nucleus, until nuclei
762
+ // match the syllable count. English hiatus tends word-ward ("-ia", "-eo"),
763
+ // so rightmost-first is the right priority. This respects the authoritative
764
+ // CMU syllable count ("hysterias" = 4: hys·te·ri·as, not 3) and prevents the
765
+ // crude fallback chunker from producing nonsensical splits like "hy·ST·eri·as"
766
+ // (treating "ST" as a syllable nucleus).
767
+ while (nuclei.length < syllableCount) {
768
+ let splitIdx = -1;
769
+ for (let j = nuclei.length - 1; j >= 0; j--) {
770
+ if (nuclei[j].end - nuclei[j].start >= 2) { splitIdx = j; break; }
771
+ }
772
+ if (splitIdx < 0) break; // no multi-vowel nucleus left
773
+ const nuc = nuclei[splitIdx];
774
+ nuclei.splice(splitIdx, 1,
775
+ { start: nuc.start, end: nuc.end - 1 },
776
+ { start: nuc.end - 1, end: nuc.end });
777
+ }
710
778
 
711
779
  while (nuclei.length > syllableCount && nuclei.length > 1) {
712
780
  let minConsonants = Infinity;
@@ -830,7 +898,7 @@ export function flattenDisplayEntries(words: ClsWord[]): SyllableDisplayEntry[]
830
898
  for (const w of words) {
831
899
  if (isPunctuation(w.lexicalClass)) continue;
832
900
  const sylCount = w.syllables.length;
833
- const chunks = syllabifyWord(w.word, sylCount, syllableVowelLengths(w.syllables), w.morphSuffix);
901
+ const chunks = syllabifyWord(w.word, sylCount, syllableVowelLengths(w.syllables), w.morphSuffix, w.morphPrefix);
834
902
  for (let si = 0; si < sylCount; si++) {
835
903
  result.push({
836
904
  wordText: w.word,
@@ -0,0 +1,108 @@
1
+ // phrasestress.ts — McAleese's Phrase-Stress phase (previously SKIPPED).
2
+ //
3
+ // The pipeline used to map lexical stress straight to relative stress (plus a
4
+ // single "+1 on the rightmost content word"), leaving `ClsWord.phraseStress`
5
+ // a vestigial field stuck at 0. This module restores the genuine phase that
6
+ // sits BETWEEN lexical and relative stress in McAleese's procedure (thesis
7
+ // Appendix A; Chomsky & Halle's Nuclear + Compound Stress Rules):
8
+ //
9
+ // 1. every word starts at 1;
10
+ // 2. the Compound Stress Rule pins a compound's subordinate element at the
11
+ // floor while its principal stays in the ramp;
12
+ // 3. the recursive Nuclear Stress Rule ramps the principal stress of each
13
+ // successive *stressed* word left-to-right, so prominence rises to the
14
+ // phrase's nuclear (rightmost) peak.
15
+ //
16
+ // For the right-branching structure of English declaratives the recursive NSR
17
+ // reduces to a monotone ramp over the stressed words — function words and
18
+ // compound-subordinates pinned at 1. NOTE the convention: here 1 is the FLOOR
19
+ // and the ramp climbs RIGHTWARD to the nuclear peak (the "If hairs…" example
20
+ // below). McAleese's "Mary ate sweet ice cream" worked example uses the
21
+ // OPPOSITE (SPE) numbering — 1 = PRIMARY/strongest, weakened outward — so his
22
+ // ICE cream (primary on "ice") is reproduced here as ice=PEAK, cream=floor
23
+ // (the compound's HEAD "cream" is the subordinate, pinned at 1):
24
+ //
25
+ // "Mary ate sweet ice cream" -> Mary2 ate3 sweet4 ice5 cream1 (ICE cream: head subordinate)
26
+ // "If hairs be wires, black wires grow on her head" -> 1 2 3 4 5 6 7 1 1 8
27
+ //
28
+ // (Marked / left-branching structures are approximated by the same ramp; a
29
+ // constituent-tree-driven refinement over `sentence.nodes` is a flagged
30
+ // follow-up. This module reads only POS / content flags and surface order,
31
+ // and writes only the previously-unused phraseStress field — purely additive.)
32
+
33
+ import { ClsWord } from './types.js';
34
+ import { isPunctuation } from './parser.js';
35
+ import { compoundStressSide } from './stress.js';
36
+
37
+ /** Sentence-final punctuation resets the nuclear ramp (the NSR is
38
+ * sentence-bounded). A comma / colon / dash does NOT — the ramp runs across
39
+ * an internal intonational break (cf. "If hairs…, black wires…" → 1234…5,678). */
40
+ const SENTENCE_FINAL = new Set(['.', '!', '?', '…']);
41
+
42
+ /** Common-/proper-noun POS tags, gating which compounds floor an element. */
43
+ const NOUN_TAGS = new Set(['NN', 'NNS', 'NNP', 'NNPS']);
44
+
45
+ /**
46
+ * Proclitic POS tags pinned at the phrase-stress floor: the NSR ramp applies to
47
+ * *stressed words* (nouns, adjectives, adverbs, and VERBS — including the
48
+ * copula/auxiliary "be" and modals, which bear lexical stress even though they
49
+ * reduce in the contour), while pure clitics carry none. Verbs are NOT here,
50
+ * so McAleese's "if hairs BE wires…" keeps `be` in the ramp (=3). A word the
51
+ * earlier pipeline promoted to content (a phrasal-verb particle, a focus
52
+ * demonstrative — `isContent === true`) is un-pinned and ramps.
53
+ */
54
+ const PINNED_POS = new Set([
55
+ 'IN', 'TO', 'DT', 'CC', 'PRP', 'PRP$', 'WP', 'WP$', 'WDT', 'EX', 'POS',
56
+ ]);
57
+
58
+ /**
59
+ * Populate `word.phraseStress` for every word of a parsed sentence.
60
+ */
61
+ export function computePhraseStress(words: ClsWord[]): void {
62
+ const subordinate = compoundSubordinates(words);
63
+ let ramp = 1; // running nuclear ramp; the floor for pinned words is 1
64
+ for (const w of words) {
65
+ if (isPunctuation(w.lexicalClass)) {
66
+ w.phraseStress = 0;
67
+ if (SENTENCE_FINAL.has(w.word)) ramp = 1;
68
+ continue;
69
+ }
70
+ // Pinned at the floor: a proclitic function word bears no nuclear ramp, and
71
+ // a compound's subordinate element is demoted by the Compound Stress Rule.
72
+ const isClitic = PINNED_POS.has(w.lexicalClass) && !w.isContent;
73
+ if (isClitic || subordinate.has(w)) {
74
+ w.phraseStress = 1;
75
+ continue;
76
+ }
77
+ w.phraseStress = ++ramp;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Identify the subordinate (pinned) element of each surface-adjacent compound,
83
+ * delegating the direction to `compoundStressSide` (the shared rule, so phrase
84
+ * stress and the lexical compound pass cannot disagree).
85
+ * - fore-stress ('left') → the HEAD (right) element is subordinate: SEA·shore,
86
+ * ICE cream, KITCHen table — the Compound Stress Rule default for N+N;
87
+ * - right-stress ('right') → the MODIFIER (left) is subordinate: apple PIE,
88
+ * and proper-name pairs (New YORK);
89
+ * - Adjective+noun ("sweet cream") is phrasal, not a compound (`side` is
90
+ * 'right' there too, so the modifier "sweet" is pinned and "cream" ramps —
91
+ * i.e. end-stress, as desired).
92
+ */
93
+ function compoundSubordinates(words: ClsWord[]): Set<ClsWord> {
94
+ const subs = new Set<ClsWord>();
95
+ for (let i = 0; i + 1 < words.length; i++) {
96
+ const a = words[i], b = words[i + 1];
97
+ if (isPunctuation(a.lexicalClass) || isPunctuation(b.lexicalClass)) continue;
98
+ if (!a.isContent || !b.isContent) continue;
99
+ const side = compoundStressSide(a.word, a.lexicalClass, b.word, b.lexicalClass);
100
+ // Floor an element only for a genuine N+N compound; an Adj+N ("sweet ice",
101
+ // "red car") is phrasal — its end-stress emerges from the ramp itself, so we
102
+ // pin nothing and let the adjective ramp (McAleese: sweet=4, above cream).
103
+ const bothNouns = NOUN_TAGS.has(a.lexicalClass) && NOUN_TAGS.has(b.lexicalClass);
104
+ if (side === 'left') subs.add(b); // fore-stress compound: head subordinate (ICE cream)
105
+ else if (side === 'right' && bothNouns) subs.add(a); // right-stress N+N: modifier subordinate (apple PIE)
106
+ }
107
+ return subs;
108
+ }