calliope-ts 0.0.2 → 0.0.4
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 +32 -20
- 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/calliope_src_contents.md +19049 -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,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 @@
|
|
|
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"}
|