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.
- package/README.md +3 -13
- package/dist/caesura.d.ts +33 -0
- package/dist/caesura.d.ts.map +1 -0
- package/dist/caesura.js +202 -0
- package/dist/calliope/boundaries.d.ts +19 -0
- package/dist/calliope/boundaries.d.ts.map +1 -0
- package/dist/calliope/boundaries.js +182 -0
- package/dist/calliope/bracketing.d.ts +11 -0
- package/dist/calliope/bracketing.d.ts.map +1 -0
- package/dist/calliope/bracketing.js +416 -0
- package/dist/calliope/deps.d.ts +4 -0
- package/dist/calliope/deps.d.ts.map +1 -0
- package/dist/calliope/deps.js +181 -0
- package/dist/calliope/engine.d.ts +3 -0
- package/dist/calliope/engine.d.ts.map +1 -0
- package/dist/calliope/engine.js +71 -0
- package/dist/calliope/feats.d.ts +9 -0
- package/dist/calliope/feats.d.ts.map +1 -0
- package/dist/calliope/feats.js +45 -0
- package/dist/calliope/names.d.ts +7 -0
- package/dist/calliope/names.d.ts.map +1 -0
- package/dist/calliope/names.js +42 -0
- package/dist/calliope/postag.d.ts +8 -0
- package/dist/calliope/postag.d.ts.map +1 -0
- package/dist/calliope/postag.js +250 -0
- package/dist/calliope/prosodic.d.ts +3 -0
- package/dist/calliope/prosodic.d.ts.map +1 -0
- package/dist/calliope/prosodic.js +275 -0
- package/dist/calliope/relstress.d.ts +4 -0
- package/dist/calliope/relstress.d.ts.map +1 -0
- package/dist/calliope/relstress.js +688 -0
- package/dist/calliope/stressrules.d.ts +28 -0
- package/dist/calliope/stressrules.d.ts.map +1 -0
- package/dist/calliope/stressrules.js +147 -0
- package/dist/calliope/syntax.d.ts +38 -0
- package/dist/calliope/syntax.d.ts.map +1 -0
- package/dist/calliope/syntax.js +234 -0
- package/dist/calliope/udpos.d.ts +13 -0
- package/dist/calliope/udpos.d.ts.map +1 -0
- package/dist/calliope/udpos.js +156 -0
- package/dist/clio/caesura.d.ts +27 -0
- package/dist/clio/caesura.d.ts.map +1 -0
- package/dist/clio/caesura.js +148 -0
- package/dist/clio/depfix.d.ts +13 -0
- package/dist/clio/depfix.d.ts.map +1 -0
- package/dist/clio/depfix.js +84 -0
- package/dist/clio/display.d.ts +32 -0
- package/dist/clio/display.d.ts.map +1 -0
- package/dist/clio/display.js +976 -0
- package/dist/clio/engine.d.ts +3 -0
- package/dist/clio/engine.d.ts.map +1 -0
- package/dist/clio/engine.js +28 -0
- package/dist/clio/parser.d.ts +10 -0
- package/dist/clio/parser.d.ts.map +1 -0
- package/dist/clio/parser.js +696 -0
- package/dist/clio/phonological.d.ts +41 -0
- package/dist/clio/phonological.d.ts.map +1 -0
- package/dist/clio/phonological.js +788 -0
- package/dist/clio/phrasestress.d.ts +6 -0
- package/dist/clio/phrasestress.d.ts.map +1 -0
- package/dist/clio/phrasestress.js +106 -0
- package/dist/clio/pipeline.d.ts +11 -0
- package/dist/clio/pipeline.d.ts.map +1 -0
- package/dist/clio/pipeline.js +147 -0
- package/dist/clio/rhyme.d.ts +65 -0
- package/dist/clio/rhyme.d.ts.map +1 -0
- package/dist/clio/rhyme.js +761 -0
- package/dist/clio/scandroid.d.ts +17 -0
- package/dist/clio/scandroid.d.ts.map +1 -0
- package/dist/clio/scandroid.js +435 -0
- package/dist/clio/scansion.d.ts +46 -0
- package/dist/clio/scansion.d.ts.map +1 -0
- package/dist/clio/scansion.js +1086 -0
- package/dist/clio/semantics.d.ts +44 -0
- package/dist/clio/semantics.d.ts.map +1 -0
- package/dist/clio/semantics.js +139 -0
- package/dist/clio/stress.d.ts +83 -0
- package/dist/clio/stress.d.ts.map +1 -0
- package/dist/clio/stress.js +1737 -0
- package/dist/clio/tagfix.d.ts +6 -0
- package/dist/clio/tagfix.d.ts.map +1 -0
- package/dist/clio/tagfix.js +101 -0
- package/dist/display.d.ts +0 -6
- package/dist/display.d.ts.map +1 -1
- package/dist/display.js +322 -141
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +12 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +169 -26
- package/dist/parser.d.ts +3 -3
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +320 -563
- package/dist/phonological.d.ts +1 -1
- package/dist/phonological.d.ts.map +1 -1
- package/dist/phonological.js +73 -4
- package/dist/phrasestress.d.ts +6 -0
- package/dist/phrasestress.d.ts.map +1 -0
- package/dist/phrasestress.js +106 -0
- package/dist/rhyme.d.ts +40 -1
- package/dist/rhyme.d.ts.map +1 -1
- package/dist/rhyme.js +435 -7
- package/dist/scansion.d.ts +9 -0
- package/dist/scansion.d.ts.map +1 -1
- package/dist/scansion.js +145 -18
- package/dist/semantics.d.ts +44 -0
- package/dist/semantics.d.ts.map +1 -0
- package/dist/semantics.js +139 -0
- package/dist/stress.d.ts +127 -4
- package/dist/stress.d.ts.map +1 -1
- package/dist/stress.js +834 -61
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/caesura.ts +201 -0
- package/src/calliope/boundaries.ts +190 -0
- package/src/calliope/bracketing.ts +390 -0
- package/src/calliope/deps.ts +160 -0
- package/src/calliope/engine.ts +77 -0
- package/src/calliope/feats.ts +46 -0
- package/src/calliope/names.ts +44 -0
- package/src/calliope/postag.ts +253 -0
- package/src/calliope/prosodic.ts +262 -0
- package/src/calliope/relstress.ts +645 -0
- package/src/calliope/stressrules.ts +147 -0
- package/src/calliope/syntax.ts +218 -0
- package/src/calliope/udpos.ts +152 -0
- package/src/clio/caesura.ts +145 -0
- package/src/clio/depfix.ts +88 -0
- package/src/clio/display.ts +1042 -0
- package/src/clio/engine.ts +38 -0
- package/src/clio/parser.ts +845 -0
- package/src/clio/phonological.ts +849 -0
- package/src/clio/phrasestress.ts +108 -0
- package/src/clio/pipeline.ts +154 -0
- package/src/clio/rhyme.ts +740 -0
- package/src/clio/scandroid.ts +434 -0
- package/src/clio/scansion.ts +1130 -0
- package/src/clio/semantics.ts +134 -0
- package/src/clio/stress.ts +1731 -0
- package/src/clio/tagfix.ts +104 -0
- package/src/display.ts +321 -137
- package/src/engine.ts +22 -0
- package/src/index.ts +176 -32
- package/src/parser.ts +346 -682
- package/src/phonological.ts +72 -4
- package/src/phrasestress.ts +108 -0
- package/src/rhyme.ts +428 -8
- package/src/scansion.ts +136 -15
- package/src/semantics.ts +134 -0
- package/src/stress.ts +794 -64
- package/src/types.ts +39 -4
- package/tests/DataForHayesLinesOnly.txt +364 -0
- package/tests/DataForHayesStressSymbolsRevised.txt +728 -0
- package/tests/basic.test.ts +443 -37
- package/tests/bench-hayes.mjs +72 -0
- package/tests/wagner-stress.test.ts +188 -0
- package/vitest.config.ts +0 -15
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// calliope/bracketing.ts — cyclic Compound + Nuclear Stress Rules, extended with
|
|
2
|
+
// Wagner (2005) Ch. 6 functor/argument geometry (2026-06-29 Wagner/Krifka rebuild).
|
|
3
|
+
//
|
|
4
|
+
// This is the REAL phrase-stress stage McAleese (after Chomsky & Halle's SPE and
|
|
5
|
+
// Hayes 1984b) prescribes — NOT a left-to-right ramp. It PROJECTS a constituent
|
|
6
|
+
// bracketing from the (UD-normalised) dependency tree, then runs the cyclic stress
|
|
7
|
+
// rules over it, innermost cycle out. The classic SPE convention is:
|
|
8
|
+
//
|
|
9
|
+
// • a COMPOUND combination (N+N / NOMD) → Compound Stress Rule: primary on the LEFT
|
|
10
|
+
// element (SLATE roof, ICE cream);
|
|
11
|
+
// • every other (phrasal) combination → Nuclear Stress Rule: primary on the RIGHT
|
|
12
|
+
// element (the rightmost constituent of a phrase is its nuclear).
|
|
13
|
+
//
|
|
14
|
+
// Wagner refines WHICH sister wins at each cycle (the plan's Gaps 2–8):
|
|
15
|
+
// • Complement Prominence (§6.2.2): the ARGUMENT outranks the FUNCTOR;
|
|
16
|
+
// • the Prosodic Asymmetry (§6.1.3): a functor that FOLLOWS its argument is
|
|
17
|
+
// obligatorily subordinated; one that PRECEDES may be on a par;
|
|
18
|
+
// • the specifier restriction (§6.3.2): a BRANCHING functor (a transitive VP) does
|
|
19
|
+
// not subordinate its subject — the two MATCH (co-nuclei);
|
|
20
|
+
// • the modifier asymmetry (§6.5.1): a modifier that PRECEDES its modifiee gets its
|
|
21
|
+
// OWN accentual domain (both project) — unlike an argument, which integrates;
|
|
22
|
+
// • associative domains (§2.2.2): same-coordinator conjuncts MATCH (flat), not NSR.
|
|
23
|
+
//
|
|
24
|
+
// To express MATCH (two co-equal sisters, neither subordinated) the relational grid is
|
|
25
|
+
// generalised: each constituent carries a SET of designated terminal elements (dtes),
|
|
26
|
+
// and
|
|
27
|
+
//
|
|
28
|
+
// level(w) = 1 + #{ enclosing constituents c : w ∈ c.words ∧ w ∉ c.dtes }
|
|
29
|
+
//
|
|
30
|
+
// A normal NSR/CSR cycle has a singleton dte (the winner's), so the formula still
|
|
31
|
+
// reproduces the canonical derivation
|
|
32
|
+
// [Mary [ate [sweet [ice cream]]]] → Mary 2, ate 3, sweet 4, ice 1, cream 5
|
|
33
|
+
// (Krifka 2001 Table 17); a MATCH cycle co-designates BOTH sisters' dtes, so neither is
|
|
34
|
+
// demoted at that cycle (a branching subject + VP give two co-nuclei).
|
|
35
|
+
//
|
|
36
|
+
// Output is per-word `phraseStress` (1 = strongest, punctuation = 0): the structural
|
|
37
|
+
// prominence the relativiser then reads PER PHONOLOGICAL PHRASE to place beats.
|
|
38
|
+
import { isPunct, isVerb, verbHasArgChild, isUnaccusativeOrPassive, isLowLocative, isFrameSetting, isLightNominalHead, } from './syntax.js';
|
|
39
|
+
const NOUN = /^(NN|NNS|NNP|NNPS)$/;
|
|
40
|
+
/** A head-dependent combination is a COMPOUND (Compound Stress Rule → primary LEFT)
|
|
41
|
+
* when the parse labels it NOMD, or when a common-/proper-noun modifier sits
|
|
42
|
+
* immediately to the left of a noun head — the N+N compound the tagger routinely
|
|
43
|
+
* mislabels AMOD ("ICE cream", "SLATE roof", "BIRD nest"). */
|
|
44
|
+
function isCompound(dep, head) {
|
|
45
|
+
if ((dep.canonicalRel ?? '') === 'NOMD')
|
|
46
|
+
return true;
|
|
47
|
+
if ((dep.canonicalRel ?? '') === 'EXT')
|
|
48
|
+
return false;
|
|
49
|
+
return NOUN.test(dep.lexicalClass) && NOUN.test(head.lexicalClass)
|
|
50
|
+
&& Math.abs(dep.absoluteIndex - head.absoluteIndex) === 1;
|
|
51
|
+
}
|
|
52
|
+
function bare(w) { return w.word.toLowerCase().replace(/['’]/g, ''); }
|
|
53
|
+
/** The coordinator lemma of a conjunct (its `cc` dependent), or null. */
|
|
54
|
+
function coordinatorLemma(conjunct, words) {
|
|
55
|
+
for (const w of words) {
|
|
56
|
+
if (w.dependency?.governor === conjunct &&
|
|
57
|
+
(w.canonicalRel === 'CC' || w.lexicalClass === 'CC')) {
|
|
58
|
+
return bare(w);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** Is the coordination headed by `head` ASSOCIATIVE (all conjuncts share one
|
|
64
|
+
* coordinator: "old and gray and full") rather than ARTICULATED (mixed and/or)?
|
|
65
|
+
* Associative → MATCH (flat, Wagner §2.2.2 / Newman); articulated → NSR. */
|
|
66
|
+
function conjIsAssociative(head, children, words) {
|
|
67
|
+
const conjuncts = (children.get(head.absoluteIndex) ?? [])
|
|
68
|
+
.filter(c => (c.canonicalRel ?? '') === 'CONJ');
|
|
69
|
+
if (conjuncts.length < 1)
|
|
70
|
+
return true;
|
|
71
|
+
const coords = conjuncts.map(c => coordinatorLemma(c, words)).filter(Boolean);
|
|
72
|
+
if (coords.length === 0)
|
|
73
|
+
return true; // no overt coordinator → flat
|
|
74
|
+
return coords.every(c => c === coords[0]);
|
|
75
|
+
}
|
|
76
|
+
/** A child relation that makes a verb BRANCH — carry an internal argument.
|
|
77
|
+
* Used for the specifier-restriction trigger and for distinguishing a bare
|
|
78
|
+
* participle ("little boy lost" — no args, adjective-like) from a participial
|
|
79
|
+
* PHRASE ("gone in the teeth" — has an OBL, a real clause). */
|
|
80
|
+
const ARG_CHILD_LOCAL = new Set(['DOBJ', 'IOBJ', 'OBJ', 'OBL', 'CCOMP', 'XCOMP', 'ADVCL']);
|
|
81
|
+
function hasArgChildren(w, children) {
|
|
82
|
+
return (children.get(w.absoluteIndex) ?? []).some(c => ARG_CHILD_LOCAL.has(c.canonicalRel ?? ''));
|
|
83
|
+
}
|
|
84
|
+
/** Decide which sister of a (dep, head) edge is the constituent's DTE — 'dep',
|
|
85
|
+
* 'head', or 'match' (both co-designated). Encodes Wagner's combination rules. */
|
|
86
|
+
function combineDecision(dep, head, children, words) {
|
|
87
|
+
const rel = dep.canonicalRel ?? '';
|
|
88
|
+
const depBefore = dep.absoluteIndex < head.absoluteIndex;
|
|
89
|
+
const rightWins = depBefore ? 'head' : 'dep'; // NSR rightmost
|
|
90
|
+
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;
|
|
94
|
+
if (rel === 'EXT')
|
|
95
|
+
return rightWins; // proper-name head (New YORK)
|
|
96
|
+
switch (rel) {
|
|
97
|
+
case 'NOMD':
|
|
98
|
+
return leftWins; // CSR: complement (N1) prominent
|
|
99
|
+
case 'DOBJ':
|
|
100
|
+
case 'IOBJ':
|
|
101
|
+
case 'OBJ':
|
|
102
|
+
case 'CCOMP':
|
|
103
|
+
case 'XCOMP':
|
|
104
|
+
return 'dep'; // argument is the DTE
|
|
105
|
+
case 'OBL':
|
|
106
|
+
// A post-nominal / post-adjectival oblique ("a friend OF him", "full OF
|
|
107
|
+
// sleep") is a complement of its NON-verbal head and is subordinated to it
|
|
108
|
+
// (the head stays the local nuclear). Only a VERBAL oblique is an argument
|
|
109
|
+
// / locative of the predicate.
|
|
110
|
+
if (!isVerb(head))
|
|
111
|
+
return 'head';
|
|
112
|
+
if (isLowLocative(dep, head, words))
|
|
113
|
+
return 'match'; // low locative → own accent
|
|
114
|
+
if (isFrameSetting(dep, words))
|
|
115
|
+
return 'match'; // frame-setting → own domain
|
|
116
|
+
return 'dep'; // oblique argument → DTE
|
|
117
|
+
case 'NMOD':
|
|
118
|
+
return 'head'; // post-nominal modifier subordinated
|
|
119
|
+
case 'NSUBJ':
|
|
120
|
+
case 'NSUBJPASS':
|
|
121
|
+
if (verbHasArgChild(head, children))
|
|
122
|
+
return 'match'; // branching VP → specifier restriction
|
|
123
|
+
if (isUnaccusativeOrPassive(dep, head))
|
|
124
|
+
return 'dep'; // underlying object → subject DTE
|
|
125
|
+
return rightWins; // unergative → verb (NSR) DTE
|
|
126
|
+
case 'AMOD':
|
|
127
|
+
return depBefore ? 'match' : 'head'; // pre-modifier → own domain; post → subordinate
|
|
128
|
+
case 'ADVMOD':
|
|
129
|
+
if (/^JJ/.test(head.lexicalClass))
|
|
130
|
+
return 'head'; // degree adv ("very TALL") integrates
|
|
131
|
+
return depBefore ? 'match' : 'head'; // pre-adv → own domain; post → subordinate
|
|
132
|
+
case 'ACL':
|
|
133
|
+
case 'ADVCL':
|
|
134
|
+
// A BARE participial acl (no argument children — "little boy lost",
|
|
135
|
+
// "mission accomplished") is adjective-like: the NSR gives right-stress
|
|
136
|
+
// (the participle is the nuclear: "little boy LOST"). A participial PHRASE
|
|
137
|
+
// ("gone in the teeth") has arguments, so the HEAD NOUN stays the nuclear
|
|
138
|
+
// and the participial phrase is subordinated to it ("old BITCH gone in the
|
|
139
|
+
// teeth").
|
|
140
|
+
if (!hasArgChildren(dep, children))
|
|
141
|
+
return 'dep'; // bare participle → NSR right-stress
|
|
142
|
+
return 'head'; // participial phrase → head noun wins
|
|
143
|
+
case 'CASE':
|
|
144
|
+
return 'head'; // preposition subordinated to its noun
|
|
145
|
+
// (the "OF him" differentiation — prep beat over a given pronoun — is set in
|
|
146
|
+
// relstress.ts, not here, so a function word never becomes the phrase nuclear)
|
|
147
|
+
case 'AUX':
|
|
148
|
+
case 'AUXPASS':
|
|
149
|
+
case 'DET':
|
|
150
|
+
case 'NUMMOD':
|
|
151
|
+
case 'EXPL':
|
|
152
|
+
case 'COMPMARK':
|
|
153
|
+
case 'ADVMARK':
|
|
154
|
+
case 'CC':
|
|
155
|
+
return 'head'; // function-word functor subordinated
|
|
156
|
+
case 'VPRT':
|
|
157
|
+
case 'DISCOURSE':
|
|
158
|
+
case 'INTJ':
|
|
159
|
+
return 'match'; // particle / interjection keep accent
|
|
160
|
+
case 'CONJ':
|
|
161
|
+
return conjIsAssociative(head, children, words) ? 'match' : rightWins;
|
|
162
|
+
default:
|
|
163
|
+
return rightWins; // NSR: rightmost wins
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Run the cyclic Compound + Nuclear Stress Rules (Wagner-extended) over the
|
|
168
|
+
* sentence's dependency tree and write each word's `phraseStress` (1 = strongest).
|
|
169
|
+
*/
|
|
170
|
+
export function computePhraseStress(sent) {
|
|
171
|
+
for (const w of sent.words)
|
|
172
|
+
if (isPunct(w))
|
|
173
|
+
w.phraseStress = 0;
|
|
174
|
+
const words = sent.words.filter(w => !isPunct(w));
|
|
175
|
+
if (words.length === 0)
|
|
176
|
+
return;
|
|
177
|
+
const childrenOf = new Map();
|
|
178
|
+
const roots = [];
|
|
179
|
+
for (const w of words) {
|
|
180
|
+
const gov = w.dependency?.governor;
|
|
181
|
+
if (gov && !isPunct(gov) && gov !== w) {
|
|
182
|
+
const arr = childrenOf.get(gov.absoluteIndex);
|
|
183
|
+
if (arr)
|
|
184
|
+
arr.push(w);
|
|
185
|
+
else
|
|
186
|
+
childrenOf.set(gov.absoluteIndex, [w]);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
roots.push(w);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const cons = [];
|
|
193
|
+
const visited = new Set();
|
|
194
|
+
function combine(left, right, dep, head) {
|
|
195
|
+
const depBefore = dep.absoluteIndex < head.absoluteIndex;
|
|
196
|
+
const depCons = depBefore ? left : right;
|
|
197
|
+
const headCons = depBefore ? right : left;
|
|
198
|
+
const decision = combineDecision(dep, head, childrenOf, words);
|
|
199
|
+
let dtes;
|
|
200
|
+
if (decision === 'match') {
|
|
201
|
+
dtes = [...left.dtes, ...right.dtes];
|
|
202
|
+
// For an ASSOCIATIVE coordination, the conjunct HEAD words are all co-equal
|
|
203
|
+
// accents (old / gray / full), even when a conjunct's own internal nuclear
|
|
204
|
+
// differs ("full of sleep" → sleep) — so co-designate the conjunct heads too,
|
|
205
|
+
// keeping the coordinate accents level instead of letting one sink.
|
|
206
|
+
if ((dep.canonicalRel ?? '') === 'CONJ') {
|
|
207
|
+
if (!dtes.includes(dep))
|
|
208
|
+
dtes.push(dep);
|
|
209
|
+
if (!dtes.includes(head))
|
|
210
|
+
dtes.push(head);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (decision === 'dep') {
|
|
214
|
+
dtes = depCons.dtes;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
dtes = headCons.dtes;
|
|
218
|
+
}
|
|
219
|
+
const c = { words: [...left.words, ...right.words], dtes };
|
|
220
|
+
cons.push(c);
|
|
221
|
+
return c;
|
|
222
|
+
}
|
|
223
|
+
function project(head) {
|
|
224
|
+
visited.add(head);
|
|
225
|
+
let cur = { words: [head], dtes: [head] };
|
|
226
|
+
const kids = (childrenOf.get(head.absoluteIndex) ?? []).filter(k => !visited.has(k));
|
|
227
|
+
const left = kids.filter(k => k.absoluteIndex < head.absoluteIndex)
|
|
228
|
+
.sort((a, b) => b.absoluteIndex - a.absoluteIndex); // closest-first
|
|
229
|
+
const right = kids.filter(k => k.absoluteIndex > head.absoluteIndex)
|
|
230
|
+
.sort((a, b) => a.absoluteIndex - b.absoluteIndex); // closest-first
|
|
231
|
+
for (const d of right)
|
|
232
|
+
cur = combine(cur, project(d), d, head); // head LEFT, dep RIGHT
|
|
233
|
+
for (const d of left)
|
|
234
|
+
cur = combine(project(d), cur, d, head); // dep LEFT, head RIGHT
|
|
235
|
+
return cur;
|
|
236
|
+
}
|
|
237
|
+
roots.sort((a, b) => a.absoluteIndex - b.absoluteIndex);
|
|
238
|
+
let top = null;
|
|
239
|
+
for (const r of roots) {
|
|
240
|
+
if (visited.has(r))
|
|
241
|
+
continue;
|
|
242
|
+
const pc = project(r);
|
|
243
|
+
if (!top) {
|
|
244
|
+
top = pc;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const c = { words: [...top.words, ...pc.words], dtes: pc.dtes }; // NSR: right root wins
|
|
248
|
+
cons.push(c);
|
|
249
|
+
top = c;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// level(w) = 1 + #{ constituents c : w ∈ c.words ∧ w ∉ c.dtes }
|
|
253
|
+
for (const w of words) {
|
|
254
|
+
let demotions = 0;
|
|
255
|
+
for (const c of cons)
|
|
256
|
+
if (!c.dtes.includes(w) && c.words.includes(w))
|
|
257
|
+
demotions++;
|
|
258
|
+
w.phraseStress = 1 + demotions;
|
|
259
|
+
}
|
|
260
|
+
// Accent-level clash reduction (Wagner §6.4.1): runs BEFORE the syllable-level
|
|
261
|
+
// resolveStressClashes (relstress.ts) and is additional to it. See below.
|
|
262
|
+
reduceAccentClashes(words);
|
|
263
|
+
}
|
|
264
|
+
/** Accent-level clash reduction (Wagner §6.4.1). Operates on which CONTENT words
|
|
265
|
+
* bear a phrasal accent (a local prominence), NOT on syllable stress. A run of
|
|
266
|
+
* three-or-more SURFACE-ADJACENT content words (no intervening function word) that
|
|
267
|
+
* share the SAME phraseStress is a clash plateau: the last is the nuclear (never
|
|
268
|
+
* dropped), the first keeps its accent, and medial accents are thinned by the
|
|
269
|
+
* alternation rule (keep every other one). Dropped accents are demoted one rung
|
|
270
|
+
* (phraseStress += 1), increasing contour differentiation without flattening.
|
|
271
|
+
*
|
|
272
|
+
* This is deliberately narrow — it fires only on a true equal-prominence plateau —
|
|
273
|
+
* so ordinary verse, where the cyclic rules already differentiate, is untouched.
|
|
274
|
+
* The syllable-level resolveStressClashes still runs afterward for cross-φ
|
|
275
|
+
* abutments and the compound-pair special case. */
|
|
276
|
+
function reduceAccentClashes(words) {
|
|
277
|
+
const ordered = [...words].sort((a, b) => a.absoluteIndex - b.absoluteIndex);
|
|
278
|
+
let i = 0;
|
|
279
|
+
while (i < ordered.length) {
|
|
280
|
+
if (!ordered[i].isContent) {
|
|
281
|
+
i++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
// Maximal run of surface-adjacent content words (adjacent absolute indices).
|
|
285
|
+
let j = i;
|
|
286
|
+
while (j + 1 < ordered.length && ordered[j + 1].isContent &&
|
|
287
|
+
ordered[j + 1].absoluteIndex === ordered[j].absoluteIndex + 1) {
|
|
288
|
+
j++;
|
|
289
|
+
}
|
|
290
|
+
const run = ordered.slice(i, j + 1);
|
|
291
|
+
if (run.length >= 3) {
|
|
292
|
+
const minPs = Math.min(...run.map(w => w.phraseStress));
|
|
293
|
+
const plateau = run.filter(w => w.phraseStress === minPs);
|
|
294
|
+
// Only thin when MOST of the run sits on the same prominence (a real plateau).
|
|
295
|
+
if (plateau.length >= 3) {
|
|
296
|
+
// keep first and last; drop alternate medial members.
|
|
297
|
+
for (let k = 1; k < plateau.length - 1; k++) {
|
|
298
|
+
if (k % 2 === 1)
|
|
299
|
+
plateau[k].phraseStress = minPs + 1; // drop every other medial accent
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
i = j + 1;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// ─── ϕ-domain derivation (the phonological-phrase grounding) ──────────────
|
|
307
|
+
//
|
|
308
|
+
// The phonological-phrase (ϕ) layer is derived from the SAME dependency
|
|
309
|
+
// constituent structure the cyclic stress rules use above — not from a parallel
|
|
310
|
+
// POS heuristic. Two adjacent words share a ϕ iff the dependency edge between
|
|
311
|
+
// them is ϕ-INTERNAL; a new ϕ opens at every ϕ-PROJECTING edge.
|
|
312
|
+
const NOMINAL = /^(NN|NNS|NNP|NNPS)$/;
|
|
313
|
+
const COPULA = new Set(['am', 'is', 'are', 'was', 'were']);
|
|
314
|
+
function isCopula(w) {
|
|
315
|
+
return COPULA.has(w.word.toLowerCase().replace(/['’]/g, '')) && /^VB/.test(w.lexicalClass);
|
|
316
|
+
}
|
|
317
|
+
/** Does a nominal `dep` BRANCH — carry its own NOMINAL modifier? An ADVERB child
|
|
318
|
+
* does NOT count. */
|
|
319
|
+
function branchesNominally(dep, children) {
|
|
320
|
+
return (children.get(dep.absoluteIndex) ?? []).some(c => {
|
|
321
|
+
if (!c.isContent)
|
|
322
|
+
return false;
|
|
323
|
+
if (/^RB/.test(c.lexicalClass) || (c.canonicalRel ?? '') === 'ADVMOD')
|
|
324
|
+
return false;
|
|
325
|
+
return true;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/** Does `dep`'s subtree open its OWN ϕ relative to its governor `head`? */
|
|
329
|
+
function phiProjects(dep, head, children, words) {
|
|
330
|
+
switch (dep.canonicalRel ?? '') {
|
|
331
|
+
case 'CCOMP':
|
|
332
|
+
case 'XCOMP':
|
|
333
|
+
case 'ADVCL':
|
|
334
|
+
case 'CONJ':
|
|
335
|
+
return true;
|
|
336
|
+
case 'ACL':
|
|
337
|
+
// A BARE participial acl ("little boy lost", "mission accomplished" — no
|
|
338
|
+
// argument children) is adjective-like: it stays WITH its head noun in one
|
|
339
|
+
// ϕ, and the NSR gives right-stress (LOST is the nuclear). A participial
|
|
340
|
+
// PHRASE ("gone in the teeth") has arguments and is a clause: it projects
|
|
341
|
+
// its own ϕ, separate from the head noun.
|
|
342
|
+
return hasArgChildren(dep, children);
|
|
343
|
+
case 'OBL':
|
|
344
|
+
// A verbal oblique is its own ϕ — EXCEPT an oblique whose governor is an
|
|
345
|
+
// ACL (a participle): it is a complement of the participial phrase, not an
|
|
346
|
+
// independent locative, so it stays INSIDE the ACL's ϕ ("gone in the teeth"
|
|
347
|
+
// is one ϕ, not two). This prevents the oblique from being stranded alone
|
|
348
|
+
// while the participle projects separately.
|
|
349
|
+
if ((head.canonicalRel ?? '') === 'ACL')
|
|
350
|
+
return false;
|
|
351
|
+
if (!isVerb(head))
|
|
352
|
+
return isLightNominalHead(head); // post-nominal PP of a LIGHT
|
|
353
|
+
// head ("Something | for the modern stage") projects; of a FULL noun ("a mould
|
|
354
|
+
// in plaster") it does not — see the NMOD case for the same modifier asymmetry.
|
|
355
|
+
if (isLowLocative(dep, head, words) || isFrameSetting(dep, words))
|
|
356
|
+
return true;
|
|
357
|
+
return true; // verbal oblique → own ϕ
|
|
358
|
+
case 'NSUBJ':
|
|
359
|
+
case 'NSUBJPASS':
|
|
360
|
+
return NOMINAL.test(dep.lexicalClass);
|
|
361
|
+
case 'DOBJ':
|
|
362
|
+
case 'IOBJ':
|
|
363
|
+
case 'OBJ':
|
|
364
|
+
if (isCopula(head))
|
|
365
|
+
return false;
|
|
366
|
+
return branchesNominally(dep, children);
|
|
367
|
+
case 'NMOD':
|
|
368
|
+
// A post-nominal PP modifier of a LIGHT head (a pronoun / inherently-given
|
|
369
|
+
// indefinite — "Something | for the modern stage") opens its OWN ϕ: the head
|
|
370
|
+
// is too light to host the modifier, so a real phrasing break falls after it
|
|
371
|
+
// (Wagner §6.5.1). A FULL lexical-noun head keeps its post-nominal PP as an
|
|
372
|
+
// internal κ ("a mould in plaster" stays one ϕ), so this does not over-segment.
|
|
373
|
+
return isLightNominalHead(head);
|
|
374
|
+
default:
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Partition the sentence's words into ϕ-domains over the dependency tree.
|
|
380
|
+
*/
|
|
381
|
+
export function computePhiDomains(sent) {
|
|
382
|
+
const words = sent.words.filter(w => !isPunct(w));
|
|
383
|
+
const children = new Map();
|
|
384
|
+
for (const w of words) {
|
|
385
|
+
const g = w.dependency?.governor;
|
|
386
|
+
if (g && !isPunct(g) && g !== w) {
|
|
387
|
+
const a = children.get(g.absoluteIndex);
|
|
388
|
+
if (a)
|
|
389
|
+
a.push(w);
|
|
390
|
+
else
|
|
391
|
+
children.set(g.absoluteIndex, [w]);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const parent = new Map();
|
|
395
|
+
for (const w of words)
|
|
396
|
+
parent.set(w.absoluteIndex, w.absoluteIndex);
|
|
397
|
+
const find = (x) => {
|
|
398
|
+
while (parent.get(x) !== x) {
|
|
399
|
+
parent.set(x, parent.get(parent.get(x)));
|
|
400
|
+
x = parent.get(x);
|
|
401
|
+
}
|
|
402
|
+
return x;
|
|
403
|
+
};
|
|
404
|
+
const union = (a, b) => { parent.set(find(a), find(b)); };
|
|
405
|
+
for (const w of words) {
|
|
406
|
+
const g = w.dependency?.governor;
|
|
407
|
+
if (!g || isPunct(g) || g === w)
|
|
408
|
+
continue;
|
|
409
|
+
if (!phiProjects(w, g, children, words))
|
|
410
|
+
union(w.absoluteIndex, g.absoluteIndex);
|
|
411
|
+
}
|
|
412
|
+
const dom = new Map();
|
|
413
|
+
for (const w of words)
|
|
414
|
+
dom.set(w, find(w.absoluteIndex));
|
|
415
|
+
return dom;
|
|
416
|
+
}
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// calliope/deps.ts — canonical dependency normalisation for the Calliope engine.
|
|
2
|
+
//
|
|
3
|
+
// en-parse emits a hybrid Stanford/UD label set, partly unreliable. This pass
|
|
4
|
+
// writes a normalised Scenario relation onto `word.canonicalRel`, the label space
|
|
5
|
+
// the Match-Theory prosodic builder (Stage 2) and the Scenario A–O stress rules
|
|
6
|
+
// (Stage 3) read. It is ADDITIVE — it never mutates the raw `dependency`, so the
|
|
7
|
+
// legacy/Clio passes see exactly the same parse as before.
|
|
8
|
+
//
|
|
9
|
+
// Where en-parse is reliable the mapping is a straight relabel; where it is not —
|
|
10
|
+
// the ditransitive DOBJ/IOBJ swap (probed: "gave John a book" → John=dobj,
|
|
11
|
+
// book=iobj, reversed), or N+N compounds it labels generic `dep` — POS and surface
|
|
12
|
+
// adjacency decide. (Head-changing repairs — coordinate re-heading, fronted
|
|
13
|
+
// adverbial re-root, invocations — are handled where the prosodic builder needs
|
|
14
|
+
// them, Stage 2.)
|
|
15
|
+
const NOUN = /^(NN|NNS|NNP|NNPS)$/;
|
|
16
|
+
const PROPER = /^(NNP|NNPS)$/;
|
|
17
|
+
const VERB = /^VB/;
|
|
18
|
+
const ADJ = /^JJ/;
|
|
19
|
+
function rawRel(w) {
|
|
20
|
+
return (w.dependency?.dependentType ?? '').toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
function gov(w) {
|
|
23
|
+
return w.dependency?.governor;
|
|
24
|
+
}
|
|
25
|
+
/** w immediately precedes its head (a pre-head modifier — the N+N / Adj+N frame). */
|
|
26
|
+
function preHead(w, head) {
|
|
27
|
+
return w.absoluteIndex + 1 === head.absoluteIndex;
|
|
28
|
+
}
|
|
29
|
+
/** Populate `canonicalRel` for every word, then apply label-only repairs. */
|
|
30
|
+
export function normalizeDeps(sent) {
|
|
31
|
+
for (const w of sent.words)
|
|
32
|
+
w.canonicalRel = canonical(w);
|
|
33
|
+
fixDitransitive(sent.words);
|
|
34
|
+
inferPrenominalModifiers(sent.words);
|
|
35
|
+
}
|
|
36
|
+
/** Reliable structural relations whose label adjacency must NOT override. */
|
|
37
|
+
const STRUCTURAL = new Set([
|
|
38
|
+
'ROOT', 'NSUBJ', 'NSUBJPASS', 'DOBJ', 'IOBJ', 'OBL', 'AUX', 'AUXPASS',
|
|
39
|
+
'CCOMP', 'XCOMP', 'ADVCL', 'ADVMOD', 'AMOD', 'ACL', 'CC', 'CONJ', 'EXPL',
|
|
40
|
+
'INTJ', 'DISCOURSE', 'VPRT', 'COMPMARK', 'ADVMARK', 'EXT',
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* Pre-head modifier inference by SURFACE ADJACENCY — independent of en-parse's
|
|
44
|
+
* (often unreliable) head links. An attributive adjective immediately before a
|
|
45
|
+
* noun is AMOD; a noun immediately before a noun is a NOMD noun adjunct (or EXT
|
|
46
|
+
* for a proper+proper name span). This is what lets a POS-corrected adjective
|
|
47
|
+
* (Pale/High/Green, demoted from a spurious NNP by `correctPosWithLexicon`) read
|
|
48
|
+
* as the AMOD it is, rather than collapsing to a bare `dep`. Only fills a word
|
|
49
|
+
* whose current label is non-structural, so deliberate relations are preserved.
|
|
50
|
+
*/
|
|
51
|
+
function inferPrenominalModifiers(words) {
|
|
52
|
+
for (let i = 0; i + 1 < words.length; i++) {
|
|
53
|
+
const w = words[i];
|
|
54
|
+
const h = words[i + 1];
|
|
55
|
+
if (w.absoluteIndex + 1 !== h.absoluteIndex)
|
|
56
|
+
continue; // surface-adjacent
|
|
57
|
+
if (!NOUN.test(h.lexicalClass))
|
|
58
|
+
continue; // head is a noun
|
|
59
|
+
if (STRUCTURAL.has(w.canonicalRel ?? ''))
|
|
60
|
+
continue; // keep real relations
|
|
61
|
+
if (ADJ.test(w.lexicalClass)) {
|
|
62
|
+
w.canonicalRel = 'AMOD';
|
|
63
|
+
}
|
|
64
|
+
else if (NOUN.test(w.lexicalClass)) {
|
|
65
|
+
w.canonicalRel = PROPER.test(w.lexicalClass) && PROPER.test(h.lexicalClass) ? 'EXT' : 'NOMD';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function canonical(w) {
|
|
70
|
+
const rel = rawRel(w);
|
|
71
|
+
const pos = w.lexicalClass;
|
|
72
|
+
const g = gov(w);
|
|
73
|
+
const gpos = g?.lexicalClass ?? '';
|
|
74
|
+
switch (rel) {
|
|
75
|
+
case 'root': return 'ROOT';
|
|
76
|
+
case 'nsubj': return 'NSUBJ';
|
|
77
|
+
case 'nsubjpass':
|
|
78
|
+
case 'nsubj:pass': return 'NSUBJPASS';
|
|
79
|
+
case 'csubj': return 'NSUBJ';
|
|
80
|
+
case 'csubjpass':
|
|
81
|
+
case 'csubj:pass': return 'NSUBJPASS';
|
|
82
|
+
case 'dobj':
|
|
83
|
+
case 'obj': return 'DOBJ';
|
|
84
|
+
case 'iobj': return 'IOBJ';
|
|
85
|
+
case 'aux': return 'AUX';
|
|
86
|
+
case 'auxpass':
|
|
87
|
+
case 'aux:pass': return 'AUXPASS';
|
|
88
|
+
// UD oblique nominal (UDPipe emits `obl`; the old path used Stanford `pobj`).
|
|
89
|
+
case 'obl':
|
|
90
|
+
case 'obl:npmod':
|
|
91
|
+
case 'obl:tmod':
|
|
92
|
+
case 'obl:arg': return 'OBL';
|
|
93
|
+
case 'cop': return 'AUX'; // copula behaves prosodically like an auxiliary
|
|
94
|
+
case 'ccomp': return 'CCOMP';
|
|
95
|
+
case 'xcomp': return 'XCOMP';
|
|
96
|
+
case 'advcl': return 'ADVCL';
|
|
97
|
+
case 'advmod': return 'ADVMOD';
|
|
98
|
+
case 'amod': return 'AMOD';
|
|
99
|
+
case 'acl':
|
|
100
|
+
case 'relcl':
|
|
101
|
+
case 'acl:relcl': return 'ACL';
|
|
102
|
+
case 'det':
|
|
103
|
+
case 'predet': return 'DET';
|
|
104
|
+
case 'nummod': return 'NUMMOD';
|
|
105
|
+
case 'cc': return 'CC';
|
|
106
|
+
case 'conj': return 'CONJ';
|
|
107
|
+
case 'expl': return 'EXPL';
|
|
108
|
+
case 'intj': return 'INTJ';
|
|
109
|
+
case 'discourse': return 'DISCOURSE';
|
|
110
|
+
case 'prt':
|
|
111
|
+
case 'compound:prt': return 'VPRT';
|
|
112
|
+
case 'case': return 'CASE';
|
|
113
|
+
case 'poss':
|
|
114
|
+
case 'possessive':
|
|
115
|
+
case 'nmod:poss': return 'CASE';
|
|
116
|
+
case 'prep': return 'CASE'; // the preposition itself cliticises
|
|
117
|
+
case 'pobj': return 'OBL'; // object of a preposition → oblique
|
|
118
|
+
case 'mark': return markType(w); // complementiser vs adverbial subordinator
|
|
119
|
+
case 'nmod':
|
|
120
|
+
if (NOUN.test(pos) && g && NOUN.test(gpos) && preHead(w, g))
|
|
121
|
+
return 'NOMD';
|
|
122
|
+
return 'OBL';
|
|
123
|
+
case 'compound':
|
|
124
|
+
return 'NOMD';
|
|
125
|
+
case 'flat':
|
|
126
|
+
case 'flat:name':
|
|
127
|
+
case 'name':
|
|
128
|
+
return 'EXT';
|
|
129
|
+
}
|
|
130
|
+
// Generic `dep` / unknown: infer from POS + adjacency.
|
|
131
|
+
if (NOUN.test(pos) && g && NOUN.test(gpos) && preHead(w, g)) {
|
|
132
|
+
// A proper-name span (both proper, adjacent) reads as an EXT extension; a
|
|
133
|
+
// common-noun pre-modifier is a NOMD noun adjunct.
|
|
134
|
+
return PROPER.test(pos) && PROPER.test(gpos) ? 'EXT' : 'NOMD';
|
|
135
|
+
}
|
|
136
|
+
if (ADJ.test(pos) && g && NOUN.test(gpos))
|
|
137
|
+
return 'AMOD';
|
|
138
|
+
if (pos === 'RP')
|
|
139
|
+
return 'VPRT';
|
|
140
|
+
if (pos === 'CC')
|
|
141
|
+
return 'CC';
|
|
142
|
+
if (pos === 'DT' || pos === 'PDT')
|
|
143
|
+
return 'DET';
|
|
144
|
+
if (pos === 'CD')
|
|
145
|
+
return 'NUMMOD';
|
|
146
|
+
if (pos === 'IN' || pos === 'TO')
|
|
147
|
+
return 'CASE';
|
|
148
|
+
return rel ? rel.toUpperCase() : 'DEP';
|
|
149
|
+
}
|
|
150
|
+
/** A `mark` heads a complement clause (COMPMARK: to/that) or an adverbial clause
|
|
151
|
+
* (ADVMARK: as/when/because). Decide by the governed clause's own relation. */
|
|
152
|
+
function markType(w) {
|
|
153
|
+
const clauseVerb = gov(w);
|
|
154
|
+
const crel = clauseVerb ? rawRel(clauseVerb) : '';
|
|
155
|
+
return crel === 'advcl' ? 'ADVMARK' : 'COMPMARK';
|
|
156
|
+
}
|
|
157
|
+
/** Ditransitive correction: a verb governing two bare objects N1 (precedes) N2 is
|
|
158
|
+
* often labelled N1=DOBJ N2=IOBJ — reversed. The first post-verbal object is the
|
|
159
|
+
* recipient (IOBJ), the second the theme (DOBJ). */
|
|
160
|
+
function fixDitransitive(words) {
|
|
161
|
+
const byGov = new Map();
|
|
162
|
+
for (const w of words) {
|
|
163
|
+
if (w.canonicalRel !== 'DOBJ' && w.canonicalRel !== 'IOBJ')
|
|
164
|
+
continue;
|
|
165
|
+
const g = gov(w);
|
|
166
|
+
if (!g || !VERB.test(g.lexicalClass))
|
|
167
|
+
continue;
|
|
168
|
+
const list = byGov.get(g);
|
|
169
|
+
if (list)
|
|
170
|
+
list.push(w);
|
|
171
|
+
else
|
|
172
|
+
byGov.set(g, [w]);
|
|
173
|
+
}
|
|
174
|
+
for (const objs of byGov.values()) {
|
|
175
|
+
if (objs.length !== 2)
|
|
176
|
+
continue;
|
|
177
|
+
objs.sort((a, b) => a.absoluteIndex - b.absoluteIndex);
|
|
178
|
+
objs[0].canonicalRel = 'IOBJ';
|
|
179
|
+
objs[1].canonicalRel = 'DOBJ';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +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"}
|