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
@@ -0,0 +1,71 @@
1
+ // calliope/engine.ts — the "Calliope" engine: the faithful, default,
2
+ // syntax-driven prosody pipeline. It derives the κ/ϕ/ι prosodic hierarchy and
3
+ // word prominence from a canonical, DepEdit-normalised dependency parse via the
4
+ // Scenario A–O relation-keyed stress rules and the corrected Match-Theory
5
+ // boundary map.
6
+ //
7
+ // Build status: STAGE 0 — the engine seam is in place but the per-sentence
8
+ // sequence is, for now, identical to the legacy ("Clio") one, so the default
9
+ // output is unchanged and the existing tests stay green. Stages 1–4 progressively
10
+ // replace the body with: canonical deps (Stage 1) → Match-Theory hierarchy
11
+ // (Stage 2) → Scenario A–O stress (Stage 3) → phrase-stress ramp + relativise
12
+ // (Stage 4). The legacy path remains untouched in `src/clio/engine.ts`.
13
+ import { assignLexicalStress, applySurfacePostProcessing, detectDisplayPrefixes, applyStressShift } from '../stress.js';
14
+ import { correctPosWithLexicon } from './postag.js';
15
+ import { tagNames } from './names.js';
16
+ import { parseFeats } from './feats.js';
17
+ import { normalizeDeps } from './deps.js';
18
+ import { buildProsodicHierarchy } from './prosodic.js';
19
+ import { computePhraseStress } from './bracketing.js';
20
+ import { computeRelativeStress } from './relstress.js';
21
+ function analyzeSentenceCalliope(sent) {
22
+ // ── Stage F1: reliable parse over the whole utterance (Calliope-only). ──
23
+ // Correct spurious proper-noun tags via en-lexicon (Pale/High → JJ); type real
24
+ // proper nouns as person/place names; normalise en-parse's hybrid relations into
25
+ // the Scenario label space on word.canonicalRel (with surface-adjacency fallback
26
+ // for pre-head modifiers). Mutates the Calliope-only view of the parse; Clio,
27
+ // invoked via --clio, never runs these and keeps its frozen reading.
28
+ correctPosWithLexicon(sent);
29
+ tagNames(sent);
30
+ // Parse UD morphological FEATS (Number/VerbForm/Voice/PronType/Definite/Degree/…)
31
+ // from lexicalDetails onto word.featsMap so the Wagner/Krifka stress + bracketing
32
+ // refinements can read morphology. Must precede normalizeDeps (which may consult it).
33
+ parseFeats(sent);
34
+ normalizeDeps(sent);
35
+ // ── Stress path: lexical → genuine phrase stress → relative, per McAleese E4. ──
36
+ // 1. Lexical stress (syllabification + word contour 0-3).
37
+ // 2. κ/ϕ/ι hierarchy fixed over the whole utterance from the dependency relations.
38
+ // 3. PHRASE STRESS: the genuine cyclic Compound + Nuclear Stress Rules over the
39
+ // dependency tree's constituent bracketing (bracketing.ts) — an integer
40
+ // prominence ranking (1 = strongest utterance nuclear), NOT a ramp.
41
+ // 4. RELATIVE STRESS: the x/w/n/m/s contour DERIVED per φ from that phrase stress
42
+ // (the φ's lowest-phraseStress word is its beat), then clash-resolved. The two
43
+ // layers are separate signals — global integer vs local contour — free to
44
+ // diverge. (Replaces the legacy compound→nuclear→phrase→relativise chain, which
45
+ // Clio still runs.)
46
+ assignLexicalStress(sent.words);
47
+ // Display-only prefix detection: set `morphPrefix` on words whose productive
48
+ // prefix + dictionary stem split would guide the display syllabifier to
49
+ // respect the morpheme boundary (dis·il·lu·sions, un·ed·u·ca·ted). Runs for
50
+ // ALL words (in-vocab AND OOV), never affects stress or meter.
51
+ detectDisplayPrefixes(sent.words);
52
+ const ius = buildProsodicHierarchy(sent);
53
+ // Stress Shift: swap primary↔secondary for words where Nounsing-Pro confirms
54
+ // shiftLikely=true AND the context motivates it (imperative at phrase start or
55
+ // Rhythm Rule clash). Runs after hierarchy (needs PP-initial info) but before
56
+ // phrase stress (so the shifted peak flows into the NSR ramp).
57
+ applyStressShift(sent.words, ius);
58
+ computePhraseStress(sent);
59
+ computeRelativeStress(sent.words, ius);
60
+ // Surface-order post-processing passes shared with the Clio engine: compound
61
+ // forestress, lexicalised collocation forestress, hyphen-seam clash resolution,
62
+ // residual linear clash resolution, and exclaimed-interjection raise. These
63
+ // re-assert forestress on surface-adjacent pairs the hierarchy-order passes may
64
+ // miss (mis-grouped parses) and catch any residual equal-stress clashes.
65
+ applySurfacePostProcessing(sent.words);
66
+ return ius;
67
+ }
68
+ export const calliopeEngine = {
69
+ name: 'calliope',
70
+ analyzeSentence: analyzeSentenceCalliope,
71
+ };
@@ -0,0 +1,9 @@
1
+ import { ClsSentence, ClsWord } from '../types.js';
2
+ /** Populate `word.featsMap` for every word in the sentence from `lexicalDetails`. */
3
+ export declare function parseFeats(sent: ClsSentence): void;
4
+ /** Read one morphological feature, undefined if absent. Lazily parses on demand
5
+ * so callers reached before `parseFeats` still work. */
6
+ export declare function feat(w: ClsWord, key: string): string | undefined;
7
+ /** True when a feature has the given value. */
8
+ export declare function featIs(w: ClsWord, key: string, value: string): boolean;
9
+ //# sourceMappingURL=feats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feats.d.ts","sourceRoot":"","sources":["../../src/calliope/feats.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAgBnD,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAKlD;AAED;yDACyD;AACzD,wBAAgB,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGhE;AAED,+CAA+C;AAC/C,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtE"}
@@ -0,0 +1,45 @@
1
+ // calliope/feats.ts — parse UD morphological FEATS onto ClsWord (Phase 1 enabler).
2
+ //
3
+ // The parser stores the raw FEATS string ("Number=Sing|Person=3|Tense=Pres|
4
+ // VerbForm=Fin") on `word.lexicalDetails`, but no downstream module could read it:
5
+ // `udpos.ts:feat()` only sees the UDPipe-level `UDWord.featsMap` at parse time. This
6
+ // pass parses `lexicalDetails` into `word.featsMap` ONCE, early in the Calliope
7
+ // pipeline, so the stress / bracketing / relativiser modules can key on morphology
8
+ // (VerbForm=Part → participle, Voice=Pass → underlying object subject, PronType=Prs →
9
+ // inherently given, Degree=Cmp → JJR, …). Pure plumbing — it mutates only the
10
+ // additive `featsMap`, never the parse.
11
+ /** Parse a UD FEATS string ("A=b|C=d") into a key→value record. */
12
+ function parseFeatsString(raw) {
13
+ const out = {};
14
+ if (!raw)
15
+ return out;
16
+ for (const pair of raw.split('|')) {
17
+ const eq = pair.indexOf('=');
18
+ if (eq <= 0)
19
+ continue;
20
+ const k = pair.slice(0, eq).trim();
21
+ const v = pair.slice(eq + 1).trim();
22
+ if (k && v)
23
+ out[k] = v;
24
+ }
25
+ return out;
26
+ }
27
+ /** Populate `word.featsMap` for every word in the sentence from `lexicalDetails`. */
28
+ export function parseFeats(sent) {
29
+ for (const w of sent.words) {
30
+ if (w.featsMap)
31
+ continue; // already parsed (idempotent)
32
+ w.featsMap = parseFeatsString(w.lexicalDetails);
33
+ }
34
+ }
35
+ /** Read one morphological feature, undefined if absent. Lazily parses on demand
36
+ * so callers reached before `parseFeats` still work. */
37
+ export function feat(w, key) {
38
+ if (!w.featsMap)
39
+ w.featsMap = parseFeatsString(w.lexicalDetails);
40
+ return w.featsMap[key];
41
+ }
42
+ /** True when a feature has the given value. */
43
+ export function featIs(w, key, value) {
44
+ return feat(w, key) === value;
45
+ }
@@ -0,0 +1,7 @@
1
+ import { ClsSentence } from '../types.js';
2
+ export declare const PERSON: Record<string, number>;
3
+ export declare const PLACE: Record<string, number>;
4
+ export declare function inList(list: Record<string, number>, surface: string): boolean;
5
+ /** Flag proper-noun tokens as person and/or place names (membership only). */
6
+ export declare function tagNames(sent: ClsSentence): void;
7
+ //# sourceMappingURL=names.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../../src/calliope/names.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAa1C,eAAO,MAAM,MAAM,wBAAyB,CAAC;AAC7C,eAAO,MAAM,KAAK,wBAA0B,CAAC;AAG7C,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK7E;AAED,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAMhD"}
@@ -0,0 +1,42 @@
1
+ // calliope/names.ts — proper-NOUN (person) vs proper-NAME (place) typing via the
2
+ // `humannames` and `cities-list` membership lists. These packages are JUST lists
3
+ // ({ "John": 1, … }), so this is a boolean enrichment, not a pipeline: it flags a
4
+ // token as a known person and/or place name to drive Scenario C (proper-name
5
+ // head-stress) and EXT.
6
+ //
7
+ // Lookups are GATED to proper-noun-tagged tokens (NNP/NNPS). Both lists are huge
8
+ // (197k names, 79k cities) and overlap heavily with common words — "Will", "May",
9
+ // "Rose", "Sun", "York", "Reading" are all in them — so flagging an untyped token
10
+ // would be noise. Restricting to NNP(S) keeps the signal honest.
11
+ import { createRequire } from 'module';
12
+ const req = createRequire(import.meta.url);
13
+ function loadList(pkg) {
14
+ try {
15
+ const m = req(pkg);
16
+ return (m && typeof m === 'object' ? m : {});
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ export const PERSON = loadList('humannames');
23
+ export const PLACE = loadList('cities-list');
24
+ const PROPER = /^(NNP|NNPS)$/;
25
+ export function inList(list, surface) {
26
+ const key = surface.replace(/['’].*$/, '').replace(/[^A-Za-z-]/g, '');
27
+ if (!key)
28
+ return false;
29
+ const cap = key[0].toUpperCase() + key.slice(1);
30
+ return !!(list[key] || list[cap]);
31
+ }
32
+ /** Flag proper-noun tokens as person and/or place names (membership only). */
33
+ export function tagNames(sent) {
34
+ for (const w of sent.words) {
35
+ if (!PROPER.test(w.lexicalClass))
36
+ continue;
37
+ if (inList(PERSON, w.word))
38
+ w.isPersonName = true;
39
+ if (inList(PLACE, w.word))
40
+ w.isPlaceName = true;
41
+ }
42
+ }
@@ -0,0 +1,8 @@
1
+ import { ClsSentence } from '../types.js';
2
+ /**
3
+ * Demote spurious proper-noun tags to their true common reading via en-lexicon.
4
+ * Mutates `word.lexicalClass` / `word.isContent` in place for the Calliope engine.
5
+ */
6
+ export declare function correctPosWithLexicon(sent: ClsSentence): void;
7
+ export declare function correctUDPipePos(sent: ClsSentence): void;
8
+ //# sourceMappingURL=postag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postag.d.ts","sourceRoot":"","sources":["../../src/calliope/postag.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,WAAW,EAAW,MAAM,aAAa,CAAC;AA0CnD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAqC7D;AAgDD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CA+FxD"}
@@ -0,0 +1,250 @@
1
+ // calliope/postag.ts — Calliope-only POS correction via en-lexicon.
2
+ //
3
+ // THE BUG THIS FIXES: en-pos tags a capitalised line-initial common word as a
4
+ // proper noun ("Pale rain" → Pale/NNP, "High tide" → High/NNP), purely from the
5
+ // capital. That mis-tag flips content class, derails the dependency parse, and —
6
+ // once relation-keyed stress is reintroduced — fore-stresses an adjective.
7
+ //
8
+ // en-lexicon (a CORE FinNLP module, the POS dictionary that en-pos/en-parse are
9
+ // built on) carries the true multi-role reading keyed on the LOWERCASE form:
10
+ // lexicon['pale'] = "JJ|VBP|NN|VB" (adjective first)
11
+ // lexicon['Pale'] = "NNP|RB" (the capitalised key is the trap)
12
+ // lexicon['high'] = "JJ|NN|RB|RP"
13
+ // so consulting the lowercase entry recovers the real word class.
14
+ //
15
+ // This runs as the FIRST Calliope step — NOT in the shared `tagfix.ts`, because
16
+ // that seam feeds BOTH engines and would un-freeze Clio. It corrects the ClsWord
17
+ // POS in place (Calliope's own pre-pass); Clio, invoked via `--clio`, never calls
18
+ // it and so keeps its frozen reading. (It does not re-run en-parse; the κ/ϕ/ι
19
+ // builder downstream is built to be robust to residual head-attachment errors,
20
+ // and `normalizeDeps` re-derives `canonicalRel` from the corrected POS.)
21
+ //
22
+ // Discipline (anti-gaming): the demotion is gated so it can ONLY fire where the
23
+ // capital is uninformative (sentence/line-initial) and the word is demonstrably a
24
+ // common word, never a known or sequenced proper name.
25
+ import { createRequire } from 'module';
26
+ const req = createRequire(import.meta.url);
27
+ function loadLexicon() {
28
+ try {
29
+ const m = req('en-lexicon');
30
+ return (m && (m.lexicon ?? m));
31
+ }
32
+ catch {
33
+ return {};
34
+ }
35
+ }
36
+ const LEXICON = loadLexicon();
37
+ const PRONOUN_SUBJECT_CONTRACTIONS_LOCAL = new Set([
38
+ "i'm", "i'll", "i've", "i'd",
39
+ "you're", "you'll", "you've", "you'd",
40
+ "he'll", "he'd", "he's", "she'll", "she'd", "she's", "it'll",
41
+ "we're", "we'll", "we've", "we'd",
42
+ "they're", "they'll", "they've", "they'd",
43
+ ]);
44
+ const PROPER = /^(NNP|NNPS)$/;
45
+ const PUNCT = /^[^A-Za-z0-9]+$/;
46
+ // Content POS, matching parser.ts CONTENT_POS (kept local to avoid a cycle).
47
+ const CONTENT = new Set([
48
+ 'NN', 'NNS', 'NNP', 'NNPS', 'JJ', 'JJR', 'JJS',
49
+ 'VB', 'VBD', 'VBG', 'VBN', 'VBP', 'VBZ', 'RB', 'RBR', 'RBS', 'CD',
50
+ ]);
51
+ // Demotion is restricted to the ATTRIBUTIVE/NOMINAL common classes — the exact
52
+ // mis-as-proper class (pale→JJ, slate→NN). A lowercase-primary that is a finite
53
+ // VERB (rose→VBD) is genuinely ambiguous sentence-initially, so we leave NNP
54
+ // rather than risk a wrong verb tag wrecking the parse.
55
+ const DEMOTE_TARGET = /^(JJ|JJR|JJS|NN|NNS)$/;
56
+ function bareLower(w) {
57
+ return w.word.toLowerCase().replace(/[^a-z]/g, '');
58
+ }
59
+ function isPunctTag(tag) {
60
+ return PUNCT.test(tag) || tag === '-LRB-' || tag === '-RRB-';
61
+ }
62
+ /**
63
+ * Demote spurious proper-noun tags to their true common reading via en-lexicon.
64
+ * Mutates `word.lexicalClass` / `word.isContent` in place for the Calliope engine.
65
+ */
66
+ export function correctPosWithLexicon(sent) {
67
+ const words = sent.words;
68
+ // Index of the first non-punctuation word — the one whose capital is forced by
69
+ // sentence/line position and therefore carries no proper-noun evidence.
70
+ let firstContentIdx = -1;
71
+ for (let i = 0; i < words.length; i++) {
72
+ if (!isPunctTag(words[i].lexicalClass)) {
73
+ firstContentIdx = i;
74
+ break;
75
+ }
76
+ }
77
+ for (let i = 0; i < words.length; i++) {
78
+ const w = words[i];
79
+ if (!PROPER.test(w.lexicalClass))
80
+ continue;
81
+ // (a) The lowercase lexicon PRIMARY reading must be an attributive/nominal
82
+ // common word. This — not name-list membership — is the real signal: the
83
+ // huge humannames/cities-list flag almost every capitalised common word
84
+ // ("Pale", "Slate", "Green" are all in them), so membership cannot gate
85
+ // here. Genuinely proper words are protected instead: their lowercase
86
+ // entry is either absent (london, york) or proper-primary (reagan → NNP).
87
+ const entry = LEXICON[bareLower(w)];
88
+ if (!entry)
89
+ continue;
90
+ const primary = entry.split('|')[0];
91
+ if (!DEMOTE_TARGET.test(primary))
92
+ continue;
93
+ // (b) Caps must be uninformative: the token is the sentence/line-initial word,
94
+ // and it is NOT part of a proper-name sequence (a neighbour tagged NNP).
95
+ if (i !== firstContentIdx)
96
+ continue;
97
+ const prev = i > 0 ? words[i - 1] : undefined;
98
+ const next = i + 1 < words.length ? words[i + 1] : undefined;
99
+ const adjacentProper = (prev && PROPER.test(prev.lexicalClass)) || (next && PROPER.test(next.lexicalClass));
100
+ if (adjacentProper)
101
+ continue;
102
+ w.lexicalClass = primary;
103
+ w.isContent = CONTENT.has(primary);
104
+ }
105
+ }
106
+ // ─── UDPipe XPOS correction (the role en-pos + tagfix.ts played pre-UD) ──────
107
+ //
108
+ // UDPipe is trained on running prose and systematically MIS-TAGS terse,
109
+ // decontextualised verse fragments — it has no sentence context to lean on.
110
+ // Observed on the test corpus: "hat"→WP, "Woolen"→NNS, "gray"→VBP, "constantly"
111
+ // →NN, "slate"/"clay"→JJ, "bicycle"→NN. The faithful downstream then scans the
112
+ // garbage. en-pos avoided this because it is lexicon-backed; we restore that by
113
+ // cross-checking UDPipe's XPOS against en-lexicon's multi-role reading (AGENTS.md:
114
+ // "nounsing-pro / lexicon POS to aid FinNLP… cross-check"). HIGH PRECISION — it
115
+ // only overrides a tag the lexicon positively contradicts; an unknown word keeps
116
+ // UDPipe's tag.
117
+ /** Coarse word-class of a Penn tag (N/V/J/R), else the exact tag. */
118
+ function coarseClass(tag) {
119
+ if (/^(NN|NNS|NNP|NNPS)$/.test(tag))
120
+ return 'N';
121
+ if (/^VB/.test(tag))
122
+ return 'V';
123
+ if (/^JJ/.test(tag))
124
+ return 'J';
125
+ if (/^RB/.test(tag))
126
+ return 'R';
127
+ return tag;
128
+ }
129
+ const NOUN_TAG = /^(NN|NNS|NNP|NNPS)$/;
130
+ function retag(w, tag) {
131
+ w.lexicalClass = tag;
132
+ w.isContent = CONTENT.has(tag);
133
+ w.lexicalPlural = tag === 'NNS' || tag === 'NNPS';
134
+ }
135
+ /** Is the immediately-preceding non-punctuation word a subject pronoun? */
136
+ function prevIsSubjectPronoun(words, i) {
137
+ for (let k = i - 1; k >= 0; k--) {
138
+ if (isPunctTag(words[k].lexicalClass))
139
+ continue;
140
+ const rel = (words[k].dependency?.dependentType ?? '').toLowerCase();
141
+ return words[k].lexicalClass === 'PRP' && /nsubj/.test(rel);
142
+ }
143
+ return false;
144
+ }
145
+ /** Next non-punctuation word, or undefined. */
146
+ function nextContentful(words, i) {
147
+ for (let k = i + 1; k < words.length; k++) {
148
+ if (!isPunctTag(words[k].lexicalClass))
149
+ return words[k];
150
+ }
151
+ return undefined;
152
+ }
153
+ export function correctUDPipePos(sent) {
154
+ const words = sent.words;
155
+ for (let i = 0; i < words.length; i++) {
156
+ const w = words[i];
157
+ if (isPunctTag(w.lexicalClass))
158
+ continue;
159
+ // (0) CONTRACTED SUBJECT PRONOUNS: UDPipe/FinNLP often mis-tags
160
+ // "I'll", "I've", "I'd", "I'm" as JJ, NNP, or other content classes,
161
+ // giving them a spurious content beat. Correct them to PRP (personal
162
+ // pronoun) so they floor to 'w' or 'n' like other pronouns.
163
+ const lowerWord = w.word.toLowerCase().replace(/[’]/g, "'");
164
+ if (PRONOUN_SUBJECT_CONTRACTIONS_LOCAL.has(lowerWord)) {
165
+ retag(w, 'PRP');
166
+ continue;
167
+ }
168
+ // (0) POSSESSIVE clitic mis-tagged as a verb: UDPipe tags the genitive "'s"
169
+ // in "laugher's licence" as VBZ (confusing it with the copula "he's" = he
170
+ // is), giving the clitic a spurious content beat. The genitive 's carries
171
+ // a `case` relation onto its possessor noun; the copula does not — so that
172
+ // relation distinguishes them. Retag POS (0 syllables downstream).
173
+ // (0) POSSESSIVE clitic mis-tagged as a verb: UDPipe sometimes tags the
174
+ // genitive "'s" (in "laugher's licence") as VBZ — confusing it with the
175
+ // copula "he's" = he is — giving the clitic a spurious content beat. The
176
+ // genitive 's carries a `case` relation onto its possessor noun; the copula
177
+ // does not — so that relation distinguishes them. Retag POS.
178
+ if ((w.word === "'s" || w.word === "’s") && /^VB/.test(w.lexicalClass)
179
+ && (w.dependency?.dependentType ?? '').toLowerCase() === 'case') {
180
+ retag(w, 'POS');
181
+ continue;
182
+ }
183
+ // The possessive ending itself is never corrected by the lexicon pass below
184
+ // (en-lexicon has a spurious verb entry for the bare letter "s").
185
+ if (w.lexicalClass === 'POS')
186
+ continue;
187
+ const lemma = bareLower(w);
188
+ if (lemma.length <= 1)
189
+ continue; // "s"/"a"/"o" → too ambiguous
190
+ const entry = LEXICON[lemma];
191
+ if (!entry)
192
+ continue; // unknown → trust UDPipe
193
+ const classes = entry.split('|');
194
+ const primary = classes[0];
195
+ const lexCoarse = new Set(classes.map(coarseClass));
196
+ const udCoarse = coarseClass(w.lexicalClass);
197
+ const CONTENT_COARSE = new Set(['N', 'V', 'J', 'R']);
198
+ // (1) PLAUSIBILITY: UDPipe's coarse class is absent from the lexicon entirely
199
+ // (hat→WP, Woolen→NNS, constantly→NN, clay→JJ) → fall back to the lexicon's
200
+ // primary reading. Only fires on a positive contradiction AND only when the
201
+ // correction is TOWARD a content class (never invents a function tag).
202
+ if (!lexCoarse.has(udCoarse) && CONTENT_COARSE.has(coarseClass(primary))
203
+ && coarseClass(primary) !== udCoarse) {
204
+ retag(w, primary);
205
+ continue;
206
+ }
207
+ // (2) ATTRIBUTIVE NOUN mis-tagged JJ: a lexicon-primary noun UDPipe tagged JJ,
208
+ // sitting immediately before a noun, is the modifier of an N+N compound
209
+ // ("SLATE roof", "CLAY jar"), not an adjective. Retag NN so the compound
210
+ // fore-stresses (deps.ts NOMD).
211
+ if (/^JJ/.test(w.lexicalClass) && coarseClass(primary) === 'N') {
212
+ const nxt = nextContentful(words, i);
213
+ if (nxt && NOUN_TAG.test(nxt.lexicalClass) && nxt.absoluteIndex === w.absoluteIndex + 1) {
214
+ retag(w, 'NN');
215
+ continue;
216
+ }
217
+ }
218
+ // (3) NOUN that should be a finite VERB: a lexicon-verb-capable common noun
219
+ // UDPipe tagged NN, immediately preceded by a SUBJECT PRONOUN ("they
220
+ // BICYCLE through", "we PAPER walls"), is the clause's predicate → VBP.
221
+ if (NOUN_TAG.test(w.lexicalClass) && lexCoarse.has('V') && prevIsSubjectPronoun(words, i)) {
222
+ retag(w, 'VBP');
223
+ continue;
224
+ }
225
+ // (4) TEMPORAL ADVERB mis-tagged IN: "before"/"after"/"since"/"once" tagged IN
226
+ // but with no nominal complement before the next clause boundary is an
227
+ // adverb (RB), not a preposition. Penn treebank: IN takes an NP
228
+ // complement ("before the storm"); RB does not ("never before", "long
229
+ // after"). A following verb/pronoun signals a subordinate clause ("before
230
+ // I go") → leave as IN (subordinating conjunction sense).
231
+ const TEMPORAL_ADVS = new Set(['before', 'after', 'since', 'once']);
232
+ if (w.lexicalClass === 'IN' && TEMPORAL_ADVS.has(lemma)) {
233
+ let hasNoun = false, hasClauseStart = false;
234
+ for (let k = i + 1; k < words.length; k++) {
235
+ if (isPunctTag(words[k].lexicalClass))
236
+ break;
237
+ if (NOUN_TAG.test(words[k].lexicalClass)) {
238
+ hasNoun = true;
239
+ break;
240
+ }
241
+ if (/^(VB|VBP|VBD|VBG|VBN|VBZ|PRP)$/.test(words[k].lexicalClass))
242
+ hasClauseStart = true;
243
+ }
244
+ if (!hasNoun && !hasClauseStart) {
245
+ retag(w, 'RB');
246
+ continue;
247
+ }
248
+ }
249
+ }
250
+ }
@@ -0,0 +1,3 @@
1
+ import { ClsSentence, IntonationalUnit } from '../types.js';
2
+ export declare function buildProsodicHierarchy(sent: ClsSentence): IntonationalUnit[];
3
+ //# sourceMappingURL=prosodic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prosodic.d.ts","sourceRoot":"","sources":["../../src/calliope/prosodic.ts"],"names":[],"mappings":"AA0BA,OAAO,EACL,WAAW,EAA4C,gBAAgB,EACxE,MAAM,aAAa,CAAC;AA0LrB,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,GAAG,gBAAgB,EAAE,CA+C5E"}