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,28 @@
|
|
|
1
|
+
import { ClsWord } from '../types.js';
|
|
2
|
+
type Side = 'left' | 'right';
|
|
3
|
+
/** Which element of an adjacent (modifier w1, head-noun w2) structure carries
|
|
4
|
+
* primary stress, per Scenarios A/B/C. */
|
|
5
|
+
export declare function scenarioModifierSide(w1: ClsWord, w2: ClsWord): Side;
|
|
6
|
+
/**
|
|
7
|
+
* Apply the nominal-modifier stress scenarios over a sentence's words. Mirrors
|
|
8
|
+
* the legacy `applyCompoundStress` iteration (adjacent content words, noun head)
|
|
9
|
+
* but decides via the canonical relations / name flags rather than POS alone, so
|
|
10
|
+
* the proper-modifier and lexicalised cases come out right. Replaces
|
|
11
|
+
* `applyCompoundStress` in the Calliope engine.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyScenarioStress(words: ClsWord[]): void;
|
|
14
|
+
/**
|
|
15
|
+
* Make the LEFT-stress scenario decision DURABLE in the relative-stress contour.
|
|
16
|
+
*
|
|
17
|
+
* The lexical compound pass (`applyScenarioStress`) sets the modifier primary, but
|
|
18
|
+
* the nuclear pass + relativiser re-promote the rightmost word, so a *non-curated*
|
|
19
|
+
* fore-stressed compound (SLATE roof, REAGAN era, WALL Street, HOT dog) would still
|
|
20
|
+
* surface as end-stressed. The legacy `resolveCompoundForestress` repairs this only
|
|
21
|
+
* for the curated `isLeftStressedPair` set; this Calliope post-pass — run AFTER
|
|
22
|
+
* `assignRelativeStresses` — re-asserts the same demote-only fore-stress for EVERY
|
|
23
|
+
* scenario-left pair: the modifier rises to the pair's max prominence, the head is
|
|
24
|
+
* demoted one rung below (never raised), which also keeps the no-equal-clash rule.
|
|
25
|
+
*/
|
|
26
|
+
export declare function enforceScenarioStress(words: ClsWord[]): void;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=stressrules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stressrules.d.ts","sourceRoot":"","sources":["../../src/calliope/stressrules.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AA8BtC,KAAK,IAAI,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7B;2CAC2C;AAC3C,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,IAAI,CAqBnE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAgB1D;AAeD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAkB5D"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// calliope/stressrules.ts — Scenario A–O relation-keyed stress (the maintainer's
|
|
2
|
+
// authoritative spec, each rule cross-checked against external linguistics).
|
|
3
|
+
// Operates over POS + word.canonicalRel + the person/place name flags.
|
|
4
|
+
//
|
|
5
|
+
// THIS FILE so far implements the nominal-modifier scenarios — the most directly
|
|
6
|
+
// testable, and where the canonical substrate first pays off:
|
|
7
|
+
// A N+N noun-adjunct (NOMD) → LEFT (SLATE roof, BIRD nest, MSNBC news,
|
|
8
|
+
// REAGAN era) — the ~⅔ baseline;
|
|
9
|
+
// food/temporal heads stay RIGHT.
|
|
10
|
+
// B Adj+N (AMOD) → RIGHT (a green ROOF); a LEXICALISED Adj+N
|
|
11
|
+
// compound fore-stresses (HOT dog).
|
|
12
|
+
// C Proper NAME (NNP+NNP) → RIGHT/head (New YORK, Bay BRIDGE) EXCEPT a
|
|
13
|
+
// de-accenting classifier head ("Street"
|
|
14
|
+
// → WALL Street).
|
|
15
|
+
// M is subsumed here (proper-name extension → rightmost head).
|
|
16
|
+
// Verbal/clausal/override scenarios (D–L, N, O) are added next.
|
|
17
|
+
//
|
|
18
|
+
// External-theory notes (binding): N+N left is only a ~⅔ baseline (Kunter & Plag),
|
|
19
|
+
// so the curated right-stress heads are first-class; lexicalised Adj+N fore-stress
|
|
20
|
+
// is a documented lexicalisation marker, not merely contrast; "Street" de-accents.
|
|
21
|
+
import { setPrimaryStress, isRightStressedHead } from '../stress.js';
|
|
22
|
+
const NOUN = /^(NN|NNS|NNP|NNPS)$/;
|
|
23
|
+
const PROPER = /^(NNP|NNPS)$/;
|
|
24
|
+
const ADJ = /^JJ/;
|
|
25
|
+
function bare(w) {
|
|
26
|
+
return w.word.toLowerCase().replace(/[^a-z]/g, '');
|
|
27
|
+
}
|
|
28
|
+
function isNoun(w) { return NOUN.test(w.lexicalClass); }
|
|
29
|
+
function isProper(w) { return PROPER.test(w.lexicalClass); }
|
|
30
|
+
function isAdjMod(w) { return ADJ.test(w.lexicalClass) || w.canonicalRel === 'AMOD'; }
|
|
31
|
+
// Scenario C exception: proper-name heads that de-accent the name LEFT. "Street"
|
|
32
|
+
// is the well-documented case (WALL Street, STATE Street) vs right-stressing
|
|
33
|
+
// Avenue/Road/Square/Bridge (Fifth AVENUE) — see the plan's external cross-check.
|
|
34
|
+
const PROPER_NAME_LEFT_HEADS = new Set(['street']);
|
|
35
|
+
// Scenario B exception: lexicalised Adj+N compounds that fore-stress despite the
|
|
36
|
+
// AMOD right-stress baseline (the stress shift is the lexicalisation marker:
|
|
37
|
+
// "HOT dog" the food vs "hot DOG" a dog that is hot). Small, curated, defensible.
|
|
38
|
+
const LEXICALIZED_FORESTRESS = new Set([
|
|
39
|
+
'hot dog', 'hot dogs', 'high school', 'high schools', 'white house',
|
|
40
|
+
'black board', 'blackboard', 'green house', 'blue blood', 'hot rod',
|
|
41
|
+
]);
|
|
42
|
+
function isLexicalizedForestress(a, b) {
|
|
43
|
+
return LEXICALIZED_FORESTRESS.has(`${bare(a)} ${bare(b)}`);
|
|
44
|
+
}
|
|
45
|
+
/** Which element of an adjacent (modifier w1, head-noun w2) structure carries
|
|
46
|
+
* primary stress, per Scenarios A/B/C. */
|
|
47
|
+
export function scenarioModifierSide(w1, w2) {
|
|
48
|
+
// Participial/gerundial HYPHENATED compound modifier (book-carrying, law-abiding,
|
|
49
|
+
// well-known) is adjectival/phrasal, NOT a noun adjunct — so it end-stresses, the
|
|
50
|
+
// head keeping prominence: "a book-carrying MAN", "a law-abiding CITIZEN". (The
|
|
51
|
+
// table's VBN/VBG-participle → end-stress; only a single-word -ing GERUND like
|
|
52
|
+
// WAITING room fore-stresses, and those are not hyphenated.)
|
|
53
|
+
if (/-/.test(w1.word) && /(ing|ed)$/i.test(bare(w1)))
|
|
54
|
+
return 'right';
|
|
55
|
+
// C — proper NAME (both proper nouns): head/right, classifier "Street" → left.
|
|
56
|
+
if (isProper(w1) && isProper(w2)) {
|
|
57
|
+
return PROPER_NAME_LEFT_HEADS.has(bare(w2)) ? 'left' : 'right';
|
|
58
|
+
}
|
|
59
|
+
// B — Adj+N: right (compositional); lexicalised Adj+N compound → left.
|
|
60
|
+
if (isAdjMod(w1)) {
|
|
61
|
+
return isLexicalizedForestress(w1, w2) ? 'left' : 'right';
|
|
62
|
+
}
|
|
63
|
+
// A — N+N noun-adjunct (incl. proper-modifier + common head, REAGAN era / MSNBC
|
|
64
|
+
// news): left by default; food/temporal right-stress heads (apple PIE) → right.
|
|
65
|
+
if (isNoun(w1) && isNoun(w2)) {
|
|
66
|
+
return isRightStressedHead(w2.word) ? 'right' : 'left';
|
|
67
|
+
}
|
|
68
|
+
return 'right';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Apply the nominal-modifier stress scenarios over a sentence's words. Mirrors
|
|
72
|
+
* the legacy `applyCompoundStress` iteration (adjacent content words, noun head)
|
|
73
|
+
* but decides via the canonical relations / name flags rather than POS alone, so
|
|
74
|
+
* the proper-modifier and lexicalised cases come out right. Replaces
|
|
75
|
+
* `applyCompoundStress` in the Calliope engine.
|
|
76
|
+
*/
|
|
77
|
+
export function applyScenarioStress(words) {
|
|
78
|
+
const content = words.filter(w => w.isContent);
|
|
79
|
+
for (let i = 0; i + 1 < content.length; i++) {
|
|
80
|
+
const w1 = content[i];
|
|
81
|
+
const w2 = content[i + 1];
|
|
82
|
+
if (w1.absoluteIndex + 1 !== w2.absoluteIndex)
|
|
83
|
+
continue; // surface-adjacent only
|
|
84
|
+
if (!isNoun(w2))
|
|
85
|
+
continue; // head of the structure is a noun
|
|
86
|
+
if (!isNoun(w1) && !isAdjMod(w1))
|
|
87
|
+
continue; // modifier is a noun or adjective
|
|
88
|
+
if (scenarioModifierSide(w1, w2) === 'left') {
|
|
89
|
+
setPrimaryStress(w1, 2);
|
|
90
|
+
setPrimaryStress(w2, 1);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
setPrimaryStress(w1, 1);
|
|
94
|
+
setPrimaryStress(w2, 2);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const RANK = { x: 0, w: 1, n: 2, m: 3, s: 4 };
|
|
99
|
+
const LEVELS = ['x', 'w', 'n', 'm', 's'];
|
|
100
|
+
function peakSyll(w) {
|
|
101
|
+
let best = null;
|
|
102
|
+
let bestR = -1;
|
|
103
|
+
for (const s of w.syllables) {
|
|
104
|
+
const r = RANK[s.relativeStress ?? 'w'];
|
|
105
|
+
if (r >= bestR) {
|
|
106
|
+
bestR = r;
|
|
107
|
+
best = s;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return best;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Make the LEFT-stress scenario decision DURABLE in the relative-stress contour.
|
|
114
|
+
*
|
|
115
|
+
* The lexical compound pass (`applyScenarioStress`) sets the modifier primary, but
|
|
116
|
+
* the nuclear pass + relativiser re-promote the rightmost word, so a *non-curated*
|
|
117
|
+
* fore-stressed compound (SLATE roof, REAGAN era, WALL Street, HOT dog) would still
|
|
118
|
+
* surface as end-stressed. The legacy `resolveCompoundForestress` repairs this only
|
|
119
|
+
* for the curated `isLeftStressedPair` set; this Calliope post-pass — run AFTER
|
|
120
|
+
* `assignRelativeStresses` — re-asserts the same demote-only fore-stress for EVERY
|
|
121
|
+
* scenario-left pair: the modifier rises to the pair's max prominence, the head is
|
|
122
|
+
* demoted one rung below (never raised), which also keeps the no-equal-clash rule.
|
|
123
|
+
*/
|
|
124
|
+
export function enforceScenarioStress(words) {
|
|
125
|
+
const content = words.filter(w => w.isContent);
|
|
126
|
+
for (let i = 0; i + 1 < content.length; i++) {
|
|
127
|
+
const w1 = content[i];
|
|
128
|
+
const w2 = content[i + 1];
|
|
129
|
+
if (w1.absoluteIndex + 1 !== w2.absoluteIndex)
|
|
130
|
+
continue;
|
|
131
|
+
if (!isNoun(w2))
|
|
132
|
+
continue;
|
|
133
|
+
if (!isNoun(w1) && !isAdjMod(w1))
|
|
134
|
+
continue;
|
|
135
|
+
if (scenarioModifierSide(w1, w2) !== 'left')
|
|
136
|
+
continue;
|
|
137
|
+
const s1 = peakSyll(w1);
|
|
138
|
+
const s2 = peakSyll(w2);
|
|
139
|
+
if (!s1 || !s2)
|
|
140
|
+
continue;
|
|
141
|
+
const r1 = RANK[s1.relativeStress ?? 'w'];
|
|
142
|
+
const r2 = RANK[s2.relativeStress ?? 'w'];
|
|
143
|
+
const hi = Math.max(r1, r2);
|
|
144
|
+
s1.relativeStress = LEVELS[hi]; // modifier ≥ both
|
|
145
|
+
s2.relativeStress = LEVELS[Math.min(r2, Math.max(0, hi - 1))]; // demote head only
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ClsWord } from '../types.js';
|
|
2
|
+
export declare function isPunct(w: ClsWord): boolean;
|
|
3
|
+
export declare function isNoun(w: ClsWord): boolean;
|
|
4
|
+
export declare function isVerb(w: ClsWord): boolean;
|
|
5
|
+
/** A PRONOUN — inherently given (Wagner §7.2.3): a personal/possessive pronoun by
|
|
6
|
+
* POS, or any word the parse marks PronType=Prs. Demonstratives (this/that),
|
|
7
|
+
* wh-words (Rel/Int), negative (nobody/nothing) and indefinite-as-focus pronouns
|
|
8
|
+
* are NOT treated as inherently given here. */
|
|
9
|
+
export declare function isPronoun(w: ClsWord): boolean;
|
|
10
|
+
export declare function isInherentlyGiven(w: ClsWord): boolean;
|
|
11
|
+
/** A LIGHT nominal head — a personal pronoun or an inherently-given indefinite —
|
|
12
|
+
* whose post-nominal PP modifier cannot be hosted inside the head's own group and
|
|
13
|
+
* so needs its OWN ϕ (the phrasing break the ear hears after "Something …"). */
|
|
14
|
+
export declare function isLightNominalHead(w: ClsWord): boolean;
|
|
15
|
+
export declare function verbHasArgChild(verb: ClsWord, children: Map<number, ClsWord[]>): boolean;
|
|
16
|
+
export declare function isUnaccusativeOrPassive(subj: ClsWord, verb: ClsWord): boolean;
|
|
17
|
+
/** The preposition lemma governing an oblique noun: its `case`/`CASE` dependent. */
|
|
18
|
+
export declare function prepLemmaOf(obl: ClsWord, words: ClsWord[]): string | null;
|
|
19
|
+
/** An oblique ARGUMENT (goal/source/recipient) — can integrate with the verb so
|
|
20
|
+
* the verb is subordinated and the oblique NP bears the single accent. */
|
|
21
|
+
export declare function isObliqueArgument(obl: ClsWord, words: ClsWord[]): boolean;
|
|
22
|
+
/** A LOW LOCATIVE / temporal adjunct (Larson 2005): a VP-final place/time oblique
|
|
23
|
+
* that gets its OWN accent regardless of position — NOT a functor of the VP. */
|
|
24
|
+
export declare function isLowLocative(obl: ClsWord, head: ClsWord, words?: ClsWord[]): boolean;
|
|
25
|
+
/** A FRAME-SETTING locative (Wagner §6.5.1): a sentence-initial temporal/spatial
|
|
26
|
+
* oblique that frames the proposition — IS a functor, gets its own domain. */
|
|
27
|
+
export declare function isFrameSetting(obl: ClsWord, words: ClsWord[]): boolean;
|
|
28
|
+
/** A TRANSITIVE function word governs a nominal complement (Wagner §6.5.5: only
|
|
29
|
+
* complement-taking functors have weak/stressless allomorphs). An INTRANSITIVE
|
|
30
|
+
* or STRANDED preposition/particle keeps stress — returns false. */
|
|
31
|
+
export declare function isTransitiveFunctionWord(w: ClsWord, words: ClsWord[]): boolean;
|
|
32
|
+
export type FunctorSide = 'dep' | 'head' | 'match';
|
|
33
|
+
/** Which sister of a (dep, head) UD edge is the FUNCTOR — or 'match' for an
|
|
34
|
+
* associative pairing (Wagner §2.2.2). This is the relation-keyed mapping in
|
|
35
|
+
* the plan's Gap-2 table: for some relations the GOVERNOR is the functor
|
|
36
|
+
* (DOBJ/OBL/CCOMP), for others the DEPENDENT is (AMOD/CASE/AUX/DET/ADVMOD). */
|
|
37
|
+
export declare function functorOf(dep: ClsWord): FunctorSide;
|
|
38
|
+
//# sourceMappingURL=syntax.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syntax.d.ts","sourceRoot":"","sources":["../../src/calliope/syntax.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAQtC,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAE3C;AACD,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAsC;AACjF,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAsC;AAGjF;;;gDAGgD;AAChD,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAG7C;AAaD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAIrD;AAED;;iFAEiF;AACjF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAEtD;AAMD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,GAC9C,OAAO,CAQT;AASD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAI7E;AAmBD,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,GAAG,IAAI,CAQzE;AAED;2EAC2E;AAC3E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAKzE;AAED;iFACiF;AACjF,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAYrF;AAED;+EAC+E;AAC/E,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAStE;AAkBD;;qEAEqE;AACrE,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAqB9E;AAGD,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAEnD;;;gFAGgF;AAChF,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,CAcnD"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// calliope/syntax.ts — Wagner/Krifka structural predicates shared by the cyclic
|
|
2
|
+
// stress (bracketing.ts) and the relativiser (relstress.ts).
|
|
3
|
+
//
|
|
4
|
+
// These encode the functor/argument geometry of Wagner (2005) Ch. 6 and the
|
|
5
|
+
// argument/adjunct/locative typology of Krifka (2001) §4.5 / Wagner §6.5.1, read
|
|
6
|
+
// off the canonical UD relations + Penn POS + morphological FEATS. No semantic
|
|
7
|
+
// role labeller is available, so the obl subtyping and transitivity tests are
|
|
8
|
+
// heuristic (preposition lemma + surface position + complement presence); every
|
|
9
|
+
// classification is approximate and, where uncertain, defaults to the reading
|
|
10
|
+
// that preserves prior behaviour (integration / transitive).
|
|
11
|
+
import { feat } from './feats.js';
|
|
12
|
+
const NOUN = /^(NN|NNS|NNP|NNPS)$/;
|
|
13
|
+
const PROPER = /^(NNP|NNPS)$/;
|
|
14
|
+
const VERB = /^VB/;
|
|
15
|
+
const PRON = /^(PRP|PRP\$)$/;
|
|
16
|
+
export function isPunct(w) {
|
|
17
|
+
return /^[^A-Za-z0-9]+$/.test(w.lexicalClass) || w.syllables.length === 0;
|
|
18
|
+
}
|
|
19
|
+
export function isNoun(w) { return NOUN.test(w.lexicalClass); }
|
|
20
|
+
export function isVerb(w) { return VERB.test(w.lexicalClass); }
|
|
21
|
+
function bare(w) { return w.word.toLowerCase().replace(/['’]/g, ''); }
|
|
22
|
+
/** A PRONOUN — inherently given (Wagner §7.2.3): a personal/possessive pronoun by
|
|
23
|
+
* POS, or any word the parse marks PronType=Prs. Demonstratives (this/that),
|
|
24
|
+
* wh-words (Rel/Int), negative (nobody/nothing) and indefinite-as-focus pronouns
|
|
25
|
+
* are NOT treated as inherently given here. */
|
|
26
|
+
export function isPronoun(w) {
|
|
27
|
+
if (PRON.test(w.lexicalClass))
|
|
28
|
+
return true;
|
|
29
|
+
return feat(w, 'PronType') === 'Prs';
|
|
30
|
+
}
|
|
31
|
+
/** A POSITIVE indefinite pronoun — inherently given (Wagner §7.2.3): its
|
|
32
|
+
* existential presupposition is trivially satisfied in any context, so it is
|
|
33
|
+
* subordinated by default and yields the utterance nuclear to a heavier sister
|
|
34
|
+
* ("Something for the modern STAGE"). Only the SOME-/ANY- existential series:
|
|
35
|
+
* NEGATIVE (nothing/nobody) and temporal/emphatic words are deliberately excluded
|
|
36
|
+
* — those are FOCAL, not given (Eliot's "Nothing", McCartney's "Yesterday"), and
|
|
37
|
+
* per the maintainer's directive must keep their prominence. UDPipe routinely
|
|
38
|
+
* mis-tags these as NN, so detection is by lemma (+ PronType guard). */
|
|
39
|
+
const INDEFINITE_GIVEN = new Set([
|
|
40
|
+
'something', 'someone', 'somebody', 'somewhat', 'somewhere', 'someplace',
|
|
41
|
+
]);
|
|
42
|
+
export function isInherentlyGiven(w) {
|
|
43
|
+
if (!INDEFINITE_GIVEN.has(bare(w)))
|
|
44
|
+
return false;
|
|
45
|
+
const pt = feat(w, 'PronType');
|
|
46
|
+
return pt !== 'Neg'; // never a negative pronoun
|
|
47
|
+
}
|
|
48
|
+
/** A LIGHT nominal head — a personal pronoun or an inherently-given indefinite —
|
|
49
|
+
* whose post-nominal PP modifier cannot be hosted inside the head's own group and
|
|
50
|
+
* so needs its OWN ϕ (the phrasing break the ear hears after "Something …"). */
|
|
51
|
+
export function isLightNominalHead(w) {
|
|
52
|
+
return isPronoun(w) || isInherentlyGiven(w);
|
|
53
|
+
}
|
|
54
|
+
/** A child relation that makes a verb BRANCH (carry an internal argument) — the
|
|
55
|
+
* specifier-restriction trigger (Wagner §6.3.2): a transitive/clausal VP whose
|
|
56
|
+
* branchingness blocks subordination of its subject. */
|
|
57
|
+
const ARG_CHILD = new Set(['DOBJ', 'IOBJ', 'OBJ', 'OBL', 'CCOMP', 'XCOMP']);
|
|
58
|
+
export function verbHasArgChild(verb, children) {
|
|
59
|
+
return (children.get(verb.absoluteIndex) ?? []).some(c => {
|
|
60
|
+
const rel = c.canonicalRel ?? '';
|
|
61
|
+
if (!ARG_CHILD.has(rel))
|
|
62
|
+
return false;
|
|
63
|
+
// A low locative oblique is an adjunct, not a branching argument.
|
|
64
|
+
if (rel === 'OBL' && isLowLocative(c, verb))
|
|
65
|
+
return false;
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** An unaccusative / passive subject is an UNDERLYING OBJECT, so it can bear the
|
|
70
|
+
* nuclear accent (Krifka §4.5.2): NSUBJPASS, or Voice=Pass on the verb, or a
|
|
71
|
+
* small set of unaccusative intransitives. */
|
|
72
|
+
const UNACCUSATIVE = new Set([
|
|
73
|
+
'arrive', 'come', 'go', 'fall', 'rise', 'appear', 'die', 'happen', 'remain',
|
|
74
|
+
'emerge', 'occur', 'vanish', 'grow', 'sink', 'flow', 'melt', 'wake',
|
|
75
|
+
]);
|
|
76
|
+
export function isUnaccusativeOrPassive(subj, verb) {
|
|
77
|
+
if ((subj.canonicalRel ?? '') === 'NSUBJPASS')
|
|
78
|
+
return true;
|
|
79
|
+
if (feat(verb, 'Voice') === 'Pass')
|
|
80
|
+
return true;
|
|
81
|
+
return UNACCUSATIVE.has(bare(verb).replace(/(ed|s|ing)$/, ''));
|
|
82
|
+
}
|
|
83
|
+
// ─── oblique typology (Wagner §6.5.1 / Larson 2005 / Krifka §4.5.1) ──────────
|
|
84
|
+
const GOAL_PREPS = new Set(['to', 'into', 'onto', 'toward', 'towards', 'unto']);
|
|
85
|
+
const SOURCE_PREPS = new Set(['from', 'out']);
|
|
86
|
+
const ADJUNCT_PREPS = new Set([
|
|
87
|
+
'in', 'on', 'at', 'by', 'for', 'during', 'throughout', 'within', 'amid',
|
|
88
|
+
'amidst', 'among', 'amongst', 'beneath', 'beside', 'over', 'under', 'above',
|
|
89
|
+
]);
|
|
90
|
+
const FRAME_PREPS = new Set([
|
|
91
|
+
'in', 'on', 'at', 'by', 'during', 'after', 'before', 'since', 'until', 'upon',
|
|
92
|
+
]);
|
|
93
|
+
const TIME_NOUNS = new Set([
|
|
94
|
+
'hour', 'hours', 'day', 'days', 'week', 'weeks', 'month', 'months', 'year',
|
|
95
|
+
'years', 'minute', 'minutes', 'moment', 'moments', 'century', 'centuries',
|
|
96
|
+
'morning', 'evening', 'night', 'nights', 'spring', 'summer', 'autumn', 'winter',
|
|
97
|
+
'dawn', 'dusk', 'noon', 'midnight', 'season', 'seasons',
|
|
98
|
+
]);
|
|
99
|
+
/** The preposition lemma governing an oblique noun: its `case`/`CASE` dependent. */
|
|
100
|
+
export function prepLemmaOf(obl, words) {
|
|
101
|
+
for (const w of words) {
|
|
102
|
+
if (w.dependency?.governor === obl && (w.canonicalRel === 'CASE' ||
|
|
103
|
+
w.lexicalClass === 'IN' || w.lexicalClass === 'TO')) {
|
|
104
|
+
return bare(w);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/** An oblique ARGUMENT (goal/source/recipient) — can integrate with the verb so
|
|
110
|
+
* the verb is subordinated and the oblique NP bears the single accent. */
|
|
111
|
+
export function isObliqueArgument(obl, words) {
|
|
112
|
+
const p = prepLemmaOf(obl, words);
|
|
113
|
+
if (!p)
|
|
114
|
+
return false;
|
|
115
|
+
if (GOAL_PREPS.has(p) || SOURCE_PREPS.has(p))
|
|
116
|
+
return true;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/** A LOW LOCATIVE / temporal adjunct (Larson 2005): a VP-final place/time oblique
|
|
120
|
+
* that gets its OWN accent regardless of position — NOT a functor of the VP. */
|
|
121
|
+
export function isLowLocative(obl, head, words) {
|
|
122
|
+
// Detect by preposition + noun semantics; if we have no word list, fall back to
|
|
123
|
+
// the noun being a time word.
|
|
124
|
+
if (words) {
|
|
125
|
+
const p = prepLemmaOf(obl, words);
|
|
126
|
+
if (p && ADJUNCT_PREPS.has(p)) {
|
|
127
|
+
// VP-final: the oblique follows its verbal head.
|
|
128
|
+
if (obl.absoluteIndex > head.absoluteIndex)
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (TIME_NOUNS.has(bare(obl)))
|
|
133
|
+
return true;
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
/** A FRAME-SETTING locative (Wagner §6.5.1): a sentence-initial temporal/spatial
|
|
137
|
+
* oblique that frames the proposition — IS a functor, gets its own domain. */
|
|
138
|
+
export function isFrameSetting(obl, words) {
|
|
139
|
+
const p = prepLemmaOf(obl, words);
|
|
140
|
+
if (!p || !FRAME_PREPS.has(p))
|
|
141
|
+
return false;
|
|
142
|
+
// sentence-initial: the preposition is (near) the first content token.
|
|
143
|
+
let firstContent = -1;
|
|
144
|
+
for (let i = 0; i < words.length; i++) {
|
|
145
|
+
if (!isPunct(words[i])) {
|
|
146
|
+
firstContent = words[i].absoluteIndex;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return obl.absoluteIndex <= firstContent + 2;
|
|
151
|
+
}
|
|
152
|
+
// ─── function-word transitivity (Wagner §6.5.5) ──────────────────────────────
|
|
153
|
+
const PARTICLE_LEMMAS = new Set([
|
|
154
|
+
'in', 'out', 'up', 'down', 'off', 'away', 'back', 'forth', 'over', 'around',
|
|
155
|
+
'along', 'through', 'apart', 'aside', 'onward', 'about',
|
|
156
|
+
]);
|
|
157
|
+
/** Last non-punct token of the clause at/after `w`? (the canonical strand site). */
|
|
158
|
+
function isClauseFinal(w, words) {
|
|
159
|
+
const idx = words.findIndex(x => x === w);
|
|
160
|
+
if (idx < 0)
|
|
161
|
+
return false;
|
|
162
|
+
for (let k = idx + 1; k < words.length; k++) {
|
|
163
|
+
if (!isPunct(words[k]))
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
/** A TRANSITIVE function word governs a nominal complement (Wagner §6.5.5: only
|
|
169
|
+
* complement-taking functors have weak/stressless allomorphs). An INTRANSITIVE
|
|
170
|
+
* or STRANDED preposition/particle keeps stress — returns false. */
|
|
171
|
+
export function isTransitiveFunctionWord(w, words) {
|
|
172
|
+
const pos = w.lexicalClass;
|
|
173
|
+
const rel = w.canonicalRel ?? '';
|
|
174
|
+
if (pos === 'RP' || rel === 'VPRT')
|
|
175
|
+
return false; // particles are intransitive
|
|
176
|
+
if (pos === 'IN' || pos === 'TO' || rel === 'CASE') {
|
|
177
|
+
// Does it govern a noun (have a nominal complement)? In UD the preposition is
|
|
178
|
+
// a CASE dependent of its noun, so check both directions.
|
|
179
|
+
const gov = w.dependency?.governor;
|
|
180
|
+
if (gov && !isPunct(gov) && NOUN.test(gov.lexicalClass) &&
|
|
181
|
+
gov.absoluteIndex > w.absoluteIndex) {
|
|
182
|
+
return true; // prep → noun complement to its right
|
|
183
|
+
}
|
|
184
|
+
const hasNominalDep = words.some(d => d.dependency?.governor === w &&
|
|
185
|
+
(NOUN.test(d.lexicalClass) || d.canonicalRel === 'OBL' || d.canonicalRel === 'DOBJ'));
|
|
186
|
+
if (hasNominalDep)
|
|
187
|
+
return true;
|
|
188
|
+
// Clause-final adverbial particle/preposition with no complement → stranded.
|
|
189
|
+
if (PARTICLE_LEMMAS.has(bare(w)) && isClauseFinal(w, words))
|
|
190
|
+
return false;
|
|
191
|
+
if (isClauseFinal(w, words))
|
|
192
|
+
return false; // stranded
|
|
193
|
+
return true; // default: transitive
|
|
194
|
+
}
|
|
195
|
+
return true; // det/aux/cc/etc.: transitive
|
|
196
|
+
}
|
|
197
|
+
/** Which sister of a (dep, head) UD edge is the FUNCTOR — or 'match' for an
|
|
198
|
+
* associative pairing (Wagner §2.2.2). This is the relation-keyed mapping in
|
|
199
|
+
* the plan's Gap-2 table: for some relations the GOVERNOR is the functor
|
|
200
|
+
* (DOBJ/OBL/CCOMP), for others the DEPENDENT is (AMOD/CASE/AUX/DET/ADVMOD). */
|
|
201
|
+
export function functorOf(dep) {
|
|
202
|
+
switch (dep.canonicalRel ?? '') {
|
|
203
|
+
case 'DOBJ':
|
|
204
|
+
case 'IOBJ':
|
|
205
|
+
case 'OBJ':
|
|
206
|
+
case 'OBL':
|
|
207
|
+
case 'CCOMP':
|
|
208
|
+
case 'XCOMP':
|
|
209
|
+
case 'NSUBJ':
|
|
210
|
+
case 'NSUBJPASS':
|
|
211
|
+
return 'head'; // governor (verb/VP) is functor
|
|
212
|
+
case 'AMOD':
|
|
213
|
+
case 'ADVMOD':
|
|
214
|
+
case 'ACL':
|
|
215
|
+
case 'ADVCL':
|
|
216
|
+
case 'CASE':
|
|
217
|
+
case 'AUX':
|
|
218
|
+
case 'AUXPASS':
|
|
219
|
+
case 'DET':
|
|
220
|
+
case 'NUMMOD':
|
|
221
|
+
case 'CC':
|
|
222
|
+
case 'COMPMARK':
|
|
223
|
+
case 'ADVMARK':
|
|
224
|
+
case 'EXPL':
|
|
225
|
+
case 'VPRT':
|
|
226
|
+
return 'dep'; // dependent is functor
|
|
227
|
+
case 'CONJ':
|
|
228
|
+
case 'DISCOURSE':
|
|
229
|
+
case 'INTJ':
|
|
230
|
+
return 'match'; // associative / own accent
|
|
231
|
+
default:
|
|
232
|
+
return 'head';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { UDWord } from 'udpipe-node';
|
|
2
|
+
/** True if `xpos` is a Penn tag the pipeline consumes directly (EWT/GUM). */
|
|
3
|
+
export declare function isPennTag(xpos: string | undefined): boolean;
|
|
4
|
+
/** The Penn tag for a token: the raw XPOS when it is already Penn (EWT/GUM),
|
|
5
|
+
* otherwise derived from UPOS+FEATS (LinES/ParTUT, or a missing XPOS). */
|
|
6
|
+
export declare function pennTagOf(w: UDWord): string;
|
|
7
|
+
/**
|
|
8
|
+
* Convert one UDPipe token to a Penn Treebank tag from UPOS + FEATS (+ lemma /
|
|
9
|
+
* deprel for the cases morphology alone can't settle). Falls back to the raw XPOS
|
|
10
|
+
* only when UPOS is absent.
|
|
11
|
+
*/
|
|
12
|
+
export declare function udToPenn(w: UDWord): string;
|
|
13
|
+
//# sourceMappingURL=udpos.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"udpos.d.ts","sourceRoot":"","sources":["../../src/calliope/udpos.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAa1C,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE3D;AAED;2EAC2E;AAC3E,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG3C;AAaD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAiG1C"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// calliope/udpos.ts — UNIVERSAL POS → Penn Treebank conversion.
|
|
2
|
+
//
|
|
3
|
+
// WHY: the phonological pipeline keys on Penn XPOS (NN/VBZ/JJ/DT/IN…), but
|
|
4
|
+
// UDPipe's XPOS column is treebank-specific and INCONSISTENT across models —
|
|
5
|
+
// EWT/GUM emit Penn tags, but LinES emits its own morphological tagset
|
|
6
|
+
// (DEF/SG-NOM/PL-NOM/ING/REL…) and ParTUT an Italian-derived one (RD/S/V/E/A/PD…),
|
|
7
|
+
// neither of which the downstream understands. UPOS and FEATS, by contrast, are
|
|
8
|
+
// the Universal Dependencies standard and are consistent (and more accurate:
|
|
9
|
+
// ~94% UPOS vs ~93% XPOS) across ALL four models. So we DERIVE the Penn tag the
|
|
10
|
+
// pipeline needs from UPOS + morphological FEATS, making the parser model-agnostic
|
|
11
|
+
// AND giving us morphology en-parse never had (Number, Tense, Degree, PronType,
|
|
12
|
+
// VerbForm) to make finer, more reliable distinctions.
|
|
13
|
+
// The Penn Treebank tag set the downstream understands. EWT/GUM emit these
|
|
14
|
+
// directly (and reliably — a lexicalised PDT like "all" stays PDT even when the
|
|
15
|
+
// parse mislabels its relation); LinES/ParTUT do NOT, so for those we derive the
|
|
16
|
+
// Penn tag from UPOS+FEATS instead.
|
|
17
|
+
const PENN_TAGS = new Set([
|
|
18
|
+
'NN', 'NNS', 'NNP', 'NNPS', 'JJ', 'JJR', 'JJS',
|
|
19
|
+
'VB', 'VBD', 'VBG', 'VBN', 'VBP', 'VBZ', 'MD',
|
|
20
|
+
'RB', 'RBR', 'RBS', 'PRP', 'PRP$', 'WP', 'WP$', 'WDT', 'WRB',
|
|
21
|
+
'DT', 'PDT', 'CD', 'IN', 'TO', 'CC', 'RP', 'EX', 'POS', 'UH', 'FW', 'SYM', 'LS',
|
|
22
|
+
]);
|
|
23
|
+
/** True if `xpos` is a Penn tag the pipeline consumes directly (EWT/GUM). */
|
|
24
|
+
export function isPennTag(xpos) {
|
|
25
|
+
return !!xpos && PENN_TAGS.has(xpos);
|
|
26
|
+
}
|
|
27
|
+
/** The Penn tag for a token: the raw XPOS when it is already Penn (EWT/GUM),
|
|
28
|
+
* otherwise derived from UPOS+FEATS (LinES/ParTUT, or a missing XPOS). */
|
|
29
|
+
export function pennTagOf(w) {
|
|
30
|
+
if (isPennTag(w.xpos))
|
|
31
|
+
return w.xpos;
|
|
32
|
+
return udToPenn(w);
|
|
33
|
+
}
|
|
34
|
+
const MODAL_LEMMAS = new Set([
|
|
35
|
+
'can', 'could', 'will', 'would', 'shall', 'should', 'may', 'might', 'must',
|
|
36
|
+
'ought', "'ll", "'d", 'wilt', 'shalt', 'canst', 'wouldst', 'shouldst', 'couldst',
|
|
37
|
+
]);
|
|
38
|
+
// Pre-determiners ("ALL the books", "BOTH his hands", "such a day", "HALF the night").
|
|
39
|
+
const PREDET_LEMMAS = new Set(['all', 'both', 'half', 'such', 'quite', 'many']);
|
|
40
|
+
function feat(w, k) {
|
|
41
|
+
return w.featsMap ? w.featsMap[k] : undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert one UDPipe token to a Penn Treebank tag from UPOS + FEATS (+ lemma /
|
|
45
|
+
* deprel for the cases morphology alone can't settle). Falls back to the raw XPOS
|
|
46
|
+
* only when UPOS is absent.
|
|
47
|
+
*/
|
|
48
|
+
export function udToPenn(w) {
|
|
49
|
+
const upos = w.upos || '';
|
|
50
|
+
const lemma = (w.lemma || w.form || '').toLowerCase();
|
|
51
|
+
const deprel = (w.deprel || '').toLowerCase();
|
|
52
|
+
const num = feat(w, 'Number');
|
|
53
|
+
const degree = feat(w, 'Degree');
|
|
54
|
+
const pron = feat(w, 'PronType');
|
|
55
|
+
const poss = feat(w, 'Poss');
|
|
56
|
+
const vform = feat(w, 'VerbForm');
|
|
57
|
+
const tense = feat(w, 'Tense');
|
|
58
|
+
const person = feat(w, 'Person');
|
|
59
|
+
// Pre-determiner ("ALL the time", "BOTH his hands"): a stress-bearing quantifier
|
|
60
|
+
// (Penn PDT, content), regardless of whether the model calls it DET or PRON. UD
|
|
61
|
+
// marks it with the det:predet relation; the non-Penn models lose that, so back
|
|
62
|
+
// it up with the lemma list.
|
|
63
|
+
if (deprel === 'det:predet')
|
|
64
|
+
return 'PDT';
|
|
65
|
+
switch (upos) {
|
|
66
|
+
case 'NOUN':
|
|
67
|
+
return num === 'Plur' ? 'NNS' : 'NN';
|
|
68
|
+
case 'PROPN':
|
|
69
|
+
return num === 'Plur' ? 'NNPS' : 'NNP';
|
|
70
|
+
case 'ADJ':
|
|
71
|
+
if (degree === 'Cmp')
|
|
72
|
+
return 'JJR';
|
|
73
|
+
if (degree === 'Sup')
|
|
74
|
+
return 'JJS';
|
|
75
|
+
// Ordinal numerals tag JJ in UD but CD-like in Penn; keep JJ (attributive).
|
|
76
|
+
return 'JJ';
|
|
77
|
+
case 'ADV':
|
|
78
|
+
if (pron === 'Int' || pron === 'Rel')
|
|
79
|
+
return 'WRB'; // when/where/why/how
|
|
80
|
+
if (degree === 'Cmp')
|
|
81
|
+
return 'RBR';
|
|
82
|
+
if (degree === 'Sup')
|
|
83
|
+
return 'RBS';
|
|
84
|
+
return 'RB';
|
|
85
|
+
case 'VERB':
|
|
86
|
+
case 'AUX': {
|
|
87
|
+
if (upos === 'AUX' && (feat(w, 'VerbType') === 'Mod' || MODAL_LEMMAS.has(lemma)))
|
|
88
|
+
return 'MD';
|
|
89
|
+
if (vform === 'Ger')
|
|
90
|
+
return 'VBG';
|
|
91
|
+
if (vform === 'Part')
|
|
92
|
+
return tense === 'Past' ? 'VBN' : 'VBG';
|
|
93
|
+
if (vform === 'Inf')
|
|
94
|
+
return 'VB';
|
|
95
|
+
if (vform === 'Fin') {
|
|
96
|
+
if (tense === 'Past')
|
|
97
|
+
return 'VBD';
|
|
98
|
+
if (person === '3' && num === 'Sing')
|
|
99
|
+
return 'VBZ';
|
|
100
|
+
return 'VBP';
|
|
101
|
+
}
|
|
102
|
+
// No VerbForm feature: best-effort by tense/person.
|
|
103
|
+
if (tense === 'Past')
|
|
104
|
+
return 'VBD';
|
|
105
|
+
if (person === '3' && num === 'Sing')
|
|
106
|
+
return 'VBZ';
|
|
107
|
+
return upos === 'AUX' ? 'VBP' : 'VB';
|
|
108
|
+
}
|
|
109
|
+
case 'PRON':
|
|
110
|
+
if (poss === 'Yes')
|
|
111
|
+
return (pron === 'Rel' || pron === 'Int') ? 'WP$' : 'PRP$';
|
|
112
|
+
if (pron === 'Rel' || pron === 'Int')
|
|
113
|
+
return 'WP';
|
|
114
|
+
if (pron === 'Dem')
|
|
115
|
+
return 'DT'; // "this/that" pronominal
|
|
116
|
+
return 'PRP';
|
|
117
|
+
case 'DET':
|
|
118
|
+
if (pron === 'Rel' || pron === 'Int')
|
|
119
|
+
return 'WDT'; // which/that(rel)/what
|
|
120
|
+
if (poss === 'Yes')
|
|
121
|
+
return 'PRP$'; // my/your/their (UD DET)
|
|
122
|
+
if (PREDET_LEMMAS.has(lemma) && deprel === 'det:predet')
|
|
123
|
+
return 'PDT';
|
|
124
|
+
return 'DT'; // articles + demonstratives
|
|
125
|
+
case 'ADP':
|
|
126
|
+
// A particle of a phrasal verb ("came DOWN", "give UP") is stress-bearing RP;
|
|
127
|
+
// an ordinary preposition is the reducible IN. UD marks the particle by the
|
|
128
|
+
// compound:prt relation.
|
|
129
|
+
return deprel === 'compound:prt' ? 'RP' : 'IN';
|
|
130
|
+
case 'SCONJ':
|
|
131
|
+
return 'IN';
|
|
132
|
+
case 'CCONJ':
|
|
133
|
+
return 'CC';
|
|
134
|
+
case 'PART':
|
|
135
|
+
if (lemma === 'to')
|
|
136
|
+
return 'TO';
|
|
137
|
+
if (lemma === "'s" || lemma === '’s' || deprel === 'case')
|
|
138
|
+
return 'POS';
|
|
139
|
+
if (lemma === 'not' || lemma === "n't" || lemma === "n’t")
|
|
140
|
+
return 'RB';
|
|
141
|
+
return 'RB';
|
|
142
|
+
case 'NUM':
|
|
143
|
+
return 'CD';
|
|
144
|
+
case 'INTJ':
|
|
145
|
+
return 'UH';
|
|
146
|
+
case 'SYM':
|
|
147
|
+
return 'SYM';
|
|
148
|
+
case 'X':
|
|
149
|
+
return 'FW';
|
|
150
|
+
case 'PUNCT':
|
|
151
|
+
// EWT/GUM give the punctuation char as XPOS already; otherwise use the form.
|
|
152
|
+
return (w.xpos && /[^A-Za-z0-9]/.test(w.xpos)) ? w.xpos : (w.form || w.xpos || ':');
|
|
153
|
+
default:
|
|
154
|
+
return w.xpos || w.upos || 'NN';
|
|
155
|
+
}
|
|
156
|
+
}
|