calliope-ts 0.0.1 → 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 +101 -205
- 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 +178 -27
- 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 +180 -31
- 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 +1102 -0
- package/tests/bench-hayes.mjs +72 -0
- package/tests/epg64.meter.train.txt +8139 -0
- package/tests/litlab-sample-2016.txt +1738 -0
- package/tests/prosodic.meter.train.txt +13192 -0
- package/tests/wagner-stress.test.ts +188 -0
- package/tsconfig.json +29 -0
- package/vitest.config.d.ts +2 -0
- package/vitest.config.js +14 -0
- package/.claude/settings.local.json +0 -8
- package/.opencode/skills/calliope-ts/SKILL.md +0 -74
package/dist/caesura.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// caesura.ts — Caesura placement, shared by the display and rhyme layers.
|
|
2
|
+
//
|
|
3
|
+
// A caesura is the line's medial pause. Two kinds are distinguished:
|
|
4
|
+
// • 'hard' — an overt break at an Intonational-Unit boundary (comma, dash,
|
|
5
|
+
// colon, semicolon …): the punctuation projection.
|
|
6
|
+
// • 'soft' — a single INFERRED medial caesura for a punctuation-free line
|
|
7
|
+
// (Kiparsky 1975, via McAleese: "phonological phrasing determines the
|
|
8
|
+
// location of caesurae in verse").
|
|
9
|
+
//
|
|
10
|
+
// Extracted from display.ts (2026-06-13) so the rhyme layer can find the words
|
|
11
|
+
// that immediately precede caesurae (for pre-caesural internal-rhyme detection)
|
|
12
|
+
// without importing the display module. Pure analysis — no colour/chalk here.
|
|
13
|
+
import { isPunctuation } from './parser.js';
|
|
14
|
+
import { computeBoundaries } from './calliope/boundaries.js';
|
|
15
|
+
/** Foot-boundary syllable indices from a scansion string (cumulative syllable
|
|
16
|
+
* count after each foot; silent beats '-' are not syllables). */
|
|
17
|
+
function footBoundarySet(scansion) {
|
|
18
|
+
const set = new Set();
|
|
19
|
+
let c = 0;
|
|
20
|
+
for (const foot of scansion.split('|')) {
|
|
21
|
+
for (const ch of foot)
|
|
22
|
+
if ('xwnms'.includes(ch))
|
|
23
|
+
c++;
|
|
24
|
+
set.add(c);
|
|
25
|
+
}
|
|
26
|
+
return set;
|
|
27
|
+
}
|
|
28
|
+
// A new phonological/syntactic phrase opens at these POS tags: prepositions &
|
|
29
|
+
// subordinators (IN), infinitival "to" (TO), coordinators (CC), wh/relativizers,
|
|
30
|
+
// verb-particles (RP), and the predicate's verb/modal. The major medial caesura
|
|
31
|
+
// of a line falls immediately BEFORE such a word — far more reliably read off the
|
|
32
|
+
// (robust) POS tags than off FinNLP's (noisy) phonological-phrase grouping, which
|
|
33
|
+
// mis-bracketed e.g. "The epic | feast…". Determiners/articles are excluded (they
|
|
34
|
+
// continue a phrase a preposition already opened: "as | one empty bag").
|
|
35
|
+
const PHRASE_ONSET_POS = new Set([
|
|
36
|
+
'IN', 'TO', 'CC', 'WDT', 'WP', 'WP$', 'WRB', 'RP',
|
|
37
|
+
'VB', 'VBD', 'VBG', 'VBN', 'VBP', 'VBZ', 'MD',
|
|
38
|
+
]);
|
|
39
|
+
// Directional/spatial adverbs that open a phrase ("snowed-in OUT of many routes",
|
|
40
|
+
// "drifting DOWN to sleep"). FinNLP often tags these RB rather than RP/IN, so the
|
|
41
|
+
// POS set alone misses them; a small curated lemma list recovers them with low
|
|
42
|
+
// over-fire risk (a generic RB like "very"/"quickly" is NOT a phrase onset).
|
|
43
|
+
const DIRECTIONAL_ONSET = new Set([
|
|
44
|
+
'out', 'in', 'up', 'down', 'off', 'away', 'back', 'forth', 'over',
|
|
45
|
+
'around', 'along', 'through', 'apart', 'aside', 'onward', 'onwards',
|
|
46
|
+
]);
|
|
47
|
+
/** True if a word opens a new phonological/syntactic phrase (a caesura candidate). */
|
|
48
|
+
function isPhraseOnset(w) {
|
|
49
|
+
if (PHRASE_ONSET_POS.has(w.lexicalClass))
|
|
50
|
+
return true;
|
|
51
|
+
return w.lexicalClass === 'RB' && DIRECTIONAL_ONSET.has(w.word.toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Caesura positions for a line, keyed by the syllable index AFTER which the pause
|
|
55
|
+
* falls (= number of syllables to its left).
|
|
56
|
+
* • 'hard' — an overt break at an Intonational-Unit boundary.
|
|
57
|
+
* • 'soft' — a single INFERRED medial caesura for a punctuation-free line.
|
|
58
|
+
* Candidates are the boundaries just before a phrase/clause onset
|
|
59
|
+
* (PHRASE_ONSET_POS); the one nearest the line's midpoint that lies in the
|
|
60
|
+
* central third AND coincides with a foot boundary wins — so it is medial,
|
|
61
|
+
* never mid-foot, and consistent across structurally-parallel lines. Read in
|
|
62
|
+
* LINEAR order (robust to clitic-group reordering); needs a line of ≥ 8
|
|
63
|
+
* syllables.
|
|
64
|
+
*/
|
|
65
|
+
export function computeCaesurae(words, ius, scansion) {
|
|
66
|
+
const caes = new Map();
|
|
67
|
+
// Graded boundary strength (Wagner Ch.4–5) keyed by syllable index, so each
|
|
68
|
+
// caesura carries the strength of its underlying ϕ/ι break — used both to COLOUR
|
|
69
|
+
// the mark and to gate strong-ϕ hard caesurae on a line-relative threshold.
|
|
70
|
+
const bounds = computeBoundaries(words, ius);
|
|
71
|
+
const strengthAt = new Map();
|
|
72
|
+
for (const b of bounds.phi) {
|
|
73
|
+
const prev = strengthAt.get(b.syllableIndex) ?? 0;
|
|
74
|
+
if (b.strength > prev)
|
|
75
|
+
strengthAt.set(b.syllableIndex, b.strength);
|
|
76
|
+
}
|
|
77
|
+
const strOf = (syl) => strengthAt.get(syl) ?? 0;
|
|
78
|
+
const setHard = (syl) => caes.set(syl, { kind: 'hard', strength: Math.max(0.6, strOf(syl)) });
|
|
79
|
+
// Caesurae must sit on FOOT BOUNDARIES (when the scansion is known). The line's
|
|
80
|
+
// metrical feet ARE the scansion's own segmentation, so a break that falls mid-
|
|
81
|
+
// foot has no metrical reality — it merely fragments a foot (often into a lone
|
|
82
|
+
// monosyllable: "But ‖ Oh ‖ ye…") and it made the reading view disagree with the
|
|
83
|
+
// detailed "Feet:" view, which already marks foot-edge breaks only. Punctuation
|
|
84
|
+
// is a caesura CANDIDATE, never an override: an IU boundary that does not land on
|
|
85
|
+
// a foot edge is dropped (the soft-caesura fallback below then offers one medial,
|
|
86
|
+
// foot-aligned break if the line is long enough).
|
|
87
|
+
const footEdges = scansion ? footBoundarySet(scansion) : null;
|
|
88
|
+
const iuOf = new Map();
|
|
89
|
+
for (let i = 0; i < ius.length; i++) {
|
|
90
|
+
for (const pp of ius[i].phonologicalPhrases) {
|
|
91
|
+
for (const cg of pp.cliticGroups)
|
|
92
|
+
for (const tok of cg.tokens)
|
|
93
|
+
iuOf.set(tok, i);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
let cum = 0;
|
|
97
|
+
let prevIu;
|
|
98
|
+
let prevWasContentful = false;
|
|
99
|
+
const onsetPositions = [];
|
|
100
|
+
for (const w of words) {
|
|
101
|
+
if (isPunctuation(w.lexicalClass) || w.syllables.length === 0) {
|
|
102
|
+
// A COMMA is an overt medial pause — the canonical caesura marker — but in
|
|
103
|
+
// the Match-Theory hierarchy it is only a ϕ (minor) break, NOT an ι boundary
|
|
104
|
+
// (dash/colon/semicolon DO project an ι, so those are caught by the IU test
|
|
105
|
+
// below). So detect a comma here directly: a comma after a contentful word,
|
|
106
|
+
// landing on a foot edge, is a hard caesura. (Without this, the comma→ϕ
|
|
107
|
+
// reclassification silently dropped every comma caesura, taking pre-caesural
|
|
108
|
+
// and caesural internal-rhyme detection with it.)
|
|
109
|
+
if (prevWasContentful && (w.lexicalClass === ',' || w.word === ',')
|
|
110
|
+
&& (!footEdges || footEdges.has(cum))) {
|
|
111
|
+
setHard(cum);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const iu = iuOf.get(w);
|
|
116
|
+
if (prevIu !== undefined && iu !== undefined && iu !== prevIu
|
|
117
|
+
&& (!footEdges || footEdges.has(cum))) {
|
|
118
|
+
setHard(cum); // IU boundary landing on a foot edge → hard caesura
|
|
119
|
+
}
|
|
120
|
+
// A phrase-onset word that is NOT the line's first word opens a candidate
|
|
121
|
+
// caesura immediately before it.
|
|
122
|
+
if (prevWasContentful && isPhraseOnset(w))
|
|
123
|
+
onsetPositions.push(cum);
|
|
124
|
+
cum += w.syllables.length;
|
|
125
|
+
prevIu = iu;
|
|
126
|
+
prevWasContentful = true;
|
|
127
|
+
}
|
|
128
|
+
const total = cum;
|
|
129
|
+
// STRONG-ϕ hard caesura (Wagner Ch.5): a ϕ boundary whose NSBR-scaled strength
|
|
130
|
+
// clears a line-relative threshold — a major medial break after a long branching
|
|
131
|
+
// phrase — is a hard caesura even without punctuation, provided it lands on a foot
|
|
132
|
+
// edge and is medial. Threshold is relative (strength is already line-normalised),
|
|
133
|
+
// so a comma after a short phrase does NOT trigger one.
|
|
134
|
+
if (total >= 8) {
|
|
135
|
+
const lo = Math.max(2, Math.ceil(total / 3));
|
|
136
|
+
const hi = Math.floor((2 * total) / 3);
|
|
137
|
+
for (const b of bounds.phi) {
|
|
138
|
+
const c = b.syllableIndex;
|
|
139
|
+
if (c < lo || c > hi)
|
|
140
|
+
continue;
|
|
141
|
+
if (footEdges && !footEdges.has(c))
|
|
142
|
+
continue;
|
|
143
|
+
if (caes.has(c))
|
|
144
|
+
continue;
|
|
145
|
+
if (b.strength >= 0.75)
|
|
146
|
+
caes.set(c, { kind: 'hard', strength: b.strength });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Infer ONE medial caesura only when the line carries no overt (hard) break.
|
|
150
|
+
// Prefer the STRONGEST candidate boundary, tie-broken by nearness to the midpoint.
|
|
151
|
+
if (caes.size === 0 && total >= 8 && onsetPositions.length > 0) {
|
|
152
|
+
const mid = total / 2;
|
|
153
|
+
const lo = Math.max(2, Math.ceil(total / 3));
|
|
154
|
+
const hi = Math.floor((2 * total) / 3);
|
|
155
|
+
let best = -1, bestStr = -1, bestDist = Infinity;
|
|
156
|
+
for (const c of onsetPositions) {
|
|
157
|
+
if (c < lo || c > hi)
|
|
158
|
+
continue; // medial third only
|
|
159
|
+
if (footEdges && !footEdges.has(c))
|
|
160
|
+
continue; // align to a foot boundary
|
|
161
|
+
const str = strOf(c);
|
|
162
|
+
const d = Math.abs(c - mid);
|
|
163
|
+
if (str > bestStr + 1e-6 || (Math.abs(str - bestStr) <= 1e-6 && d < bestDist)) {
|
|
164
|
+
bestStr = str;
|
|
165
|
+
bestDist = d;
|
|
166
|
+
best = c;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (best > 0)
|
|
170
|
+
caes.set(best, { kind: 'soft', strength: Math.max(0.25, bestStr) });
|
|
171
|
+
}
|
|
172
|
+
// A caesura is a MEDIAL pause: a line-terminal comma/IU close lands at the last
|
|
173
|
+
// syllable (cum == total) and is the line break's own boundary, not a caesura.
|
|
174
|
+
// Dropping it here keeps every consumer consistent — the detailed view used to
|
|
175
|
+
// print a spurious trailing '‖' the reading view (correctly) never showed, and
|
|
176
|
+
// the rhyme layer would have double-counted the end word as "pre-caesural".
|
|
177
|
+
caes.delete(total);
|
|
178
|
+
return caes;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The words that immediately PRECEDE a caesura in a line — i.e. each word whose
|
|
182
|
+
* cumulative syllable count lands exactly on a caesura position. Used by the
|
|
183
|
+
* rhyme layer for pre-caesural internal-rhyme detection. Returned in linear
|
|
184
|
+
* (reading) order; the caesura kind is paired so callers can weight hard vs
|
|
185
|
+
* inferred breaks if they wish.
|
|
186
|
+
*/
|
|
187
|
+
export function preCaesuralWords(words, ius, scansion) {
|
|
188
|
+
const caes = computeCaesurae(words, ius, scansion);
|
|
189
|
+
if (caes.size === 0)
|
|
190
|
+
return [];
|
|
191
|
+
const out = [];
|
|
192
|
+
let cum = 0;
|
|
193
|
+
for (const w of words) {
|
|
194
|
+
if (isPunctuation(w.lexicalClass) || w.syllables.length === 0)
|
|
195
|
+
continue;
|
|
196
|
+
cum += w.syllables.length;
|
|
197
|
+
const info = caes.get(cum);
|
|
198
|
+
if (info)
|
|
199
|
+
out.push({ word: w, kind: info.kind });
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ClsWord, IntonationalUnit } from '../types.js';
|
|
2
|
+
export interface BoundaryInfo {
|
|
3
|
+
level: 'kappa' | 'phi' | 'iota';
|
|
4
|
+
strength: number;
|
|
5
|
+
raw: number;
|
|
6
|
+
syllableIndex: number;
|
|
7
|
+
}
|
|
8
|
+
export interface LineBoundaries {
|
|
9
|
+
/** ϕ boundaries in document order: phi[k] is the boundary OPENING the k-th ϕ.
|
|
10
|
+
* phi[0] is the line's left edge (strength 0 — not a real internal break). */
|
|
11
|
+
phi: BoundaryInfo[];
|
|
12
|
+
/** ι boundaries in document order, same convention. */
|
|
13
|
+
iota: BoundaryInfo[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Compute graded boundary strength for every ϕ and ι boundary in a line.
|
|
17
|
+
*/
|
|
18
|
+
export declare function computeBoundaries(words: ClsWord[], ius: IntonationalUnit[]): LineBoundaries;
|
|
19
|
+
//# sourceMappingURL=boundaries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundaries.d.ts","sourceRoot":"","sources":["../../src/calliope/boundaries.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B;mFAC+E;IAC/E,GAAG,EAAE,YAAY,EAAE,CAAC;IACpB,uDAAuD;IACvD,IAAI,EAAE,YAAY,EAAE,CAAC;CACtB;AAuFD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE,GAAG,cAAc,CAyD3F"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// calliope/boundaries.ts — graded prosodic boundary strength (Wagner 2005 Ch. 4–5).
|
|
2
|
+
//
|
|
3
|
+
// The labelled κ/ϕ/ι hierarchy is the SKELETON; this module adds the relational
|
|
4
|
+
// FLESH Wagner argues the grid actually encodes — a graded boundary strength rather
|
|
5
|
+
// than a categorical label. Each ϕ/ι boundary gets a strength scaled RELATIVE to the
|
|
6
|
+
// line's other boundaries (the NSBR, Normalized Scopally-determined Boundary Rank,
|
|
7
|
+
// Ch. 5 §5.3): the strongest boundary in the line is 1.0, the rest fall below it.
|
|
8
|
+
//
|
|
9
|
+
// The raw strength of a boundary between left unit A and right unit B combines:
|
|
10
|
+
// • the categorical level (ι ≫ ϕ ≫ κ) — the SBR base rank;
|
|
11
|
+
// • punctuation coincidence — a comma/dash/colon at the boundary makes it stronger;
|
|
12
|
+
// • the LENGTH of the preceding constituent (a boundary after a long, branching
|
|
13
|
+
// phrase is stronger — Wagner Ch. 5 look-back);
|
|
14
|
+
// • clause separation — a boundary between subject|predicate or main|subordinate
|
|
15
|
+
// clause (their dependency LCA is at/near the root) is stronger;
|
|
16
|
+
// • associative coordination — a boundary internal to a same-coordinator series is
|
|
17
|
+
// WEAKER (equal-rank, flat).
|
|
18
|
+
//
|
|
19
|
+
// Strength feeds (a) the colored bracket rendering (display.ts) and (b) caesura
|
|
20
|
+
// placement (caesura.ts): a hard caesura needs a boundary whose strength clears a
|
|
21
|
+
// line-relative threshold, not an absolute one.
|
|
22
|
+
function isPunct(w) {
|
|
23
|
+
return /^[^A-Za-z0-9]+$/.test(w.lexicalClass) || w.syllables.length === 0;
|
|
24
|
+
}
|
|
25
|
+
/** Dependency depth of a word (number of governors up to the root). */
|
|
26
|
+
function depthOf(w, memo) {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
let d = 0;
|
|
29
|
+
let cur = w;
|
|
30
|
+
while (cur) {
|
|
31
|
+
if (memo.has(cur)) {
|
|
32
|
+
d += memo.get(cur);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (seen.has(cur))
|
|
36
|
+
break; // cycle guard
|
|
37
|
+
seen.add(cur);
|
|
38
|
+
const g = cur.dependency?.governor;
|
|
39
|
+
if (!g || g === cur || isPunct(g))
|
|
40
|
+
break;
|
|
41
|
+
d++;
|
|
42
|
+
cur = g;
|
|
43
|
+
}
|
|
44
|
+
memo.set(w, d);
|
|
45
|
+
return d;
|
|
46
|
+
}
|
|
47
|
+
/** The syntactic head of a ϕ — the lowest-depth (closest to root) content token. */
|
|
48
|
+
function phraseHead(tokens, memo) {
|
|
49
|
+
let best = null;
|
|
50
|
+
let bestD = Infinity;
|
|
51
|
+
for (const t of tokens) {
|
|
52
|
+
if (isPunct(t))
|
|
53
|
+
continue;
|
|
54
|
+
const d = depthOf(t, memo);
|
|
55
|
+
if (d < bestD) {
|
|
56
|
+
bestD = d;
|
|
57
|
+
best = t;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return best;
|
|
61
|
+
}
|
|
62
|
+
/** Lowest-common-ancestor depth of two words in the dependency tree. */
|
|
63
|
+
function lcaDepth(a, b, memo) {
|
|
64
|
+
const anc = new Map();
|
|
65
|
+
let cur = a;
|
|
66
|
+
let d = 0;
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
while (cur && !seen.has(cur)) {
|
|
69
|
+
anc.set(cur, d++);
|
|
70
|
+
seen.add(cur);
|
|
71
|
+
const g = cur.dependency?.governor;
|
|
72
|
+
if (!g || g === cur || isPunct(g))
|
|
73
|
+
break;
|
|
74
|
+
cur = g;
|
|
75
|
+
}
|
|
76
|
+
cur = b;
|
|
77
|
+
const seen2 = new Set();
|
|
78
|
+
while (cur && !seen2.has(cur)) {
|
|
79
|
+
if (anc.has(cur))
|
|
80
|
+
return depthOf(cur, memo);
|
|
81
|
+
seen2.add(cur);
|
|
82
|
+
const g = cur.dependency?.governor;
|
|
83
|
+
if (!g || g === cur || isPunct(g))
|
|
84
|
+
break;
|
|
85
|
+
cur = g;
|
|
86
|
+
}
|
|
87
|
+
return 0; // disjoint subtrees / different roots → treat as top-level (depth 0)
|
|
88
|
+
}
|
|
89
|
+
/** Is there a comma / dash / terminal punctuation token between A and B in surface
|
|
90
|
+
* order, and is it a strong (ι-class) one? */
|
|
91
|
+
function punctBetween(words, a, b) {
|
|
92
|
+
let comma = false, strong = false;
|
|
93
|
+
for (const w of words) {
|
|
94
|
+
if (w.absoluteIndex <= a.absoluteIndex || w.absoluteIndex >= b.absoluteIndex)
|
|
95
|
+
continue;
|
|
96
|
+
if (!isPunct(w))
|
|
97
|
+
continue;
|
|
98
|
+
if (w.word === ',' || w.lexicalClass === ',')
|
|
99
|
+
comma = true;
|
|
100
|
+
if (/^[.!?:;…]$/.test(w.word) || /^[.!?:;-]$/.test(w.lexicalClass) ||
|
|
101
|
+
w.lexicalClass === '-LRB-' || w.lexicalClass === '-RRB-')
|
|
102
|
+
strong = true;
|
|
103
|
+
}
|
|
104
|
+
return { comma, strong };
|
|
105
|
+
}
|
|
106
|
+
function sylCount(tokens) {
|
|
107
|
+
let n = 0;
|
|
108
|
+
for (const t of tokens)
|
|
109
|
+
n += t.syllables.length;
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
/** Flatten the ϕ of an ι into token lists. */
|
|
113
|
+
function phiTokenLists(iu) {
|
|
114
|
+
return iu.phonologicalPhrases.map(pp => pp.cliticGroups.flatMap(cg => cg.tokens).filter(t => !isPunct(t)));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Compute graded boundary strength for every ϕ and ι boundary in a line.
|
|
118
|
+
*/
|
|
119
|
+
export function computeBoundaries(words, ius) {
|
|
120
|
+
const memo = new Map();
|
|
121
|
+
const maxDepth = Math.max(1, ...words.filter(w => !isPunct(w)).map(w => depthOf(w, memo)));
|
|
122
|
+
// Flatten ϕ across all ι in document order, remembering each ϕ's ι index.
|
|
123
|
+
const flatPhi = [];
|
|
124
|
+
ius.forEach((iu, iuIdx) => {
|
|
125
|
+
phiTokenLists(iu).forEach((tokens, ppIdx) => {
|
|
126
|
+
if (tokens.length)
|
|
127
|
+
flatPhi.push({ tokens, iuIdx, ppIdx });
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
let cumSyl = 0;
|
|
131
|
+
const phi = [];
|
|
132
|
+
const iota = [];
|
|
133
|
+
for (let k = 0; k < flatPhi.length; k++) {
|
|
134
|
+
const cur = flatPhi[k];
|
|
135
|
+
const prev = k > 0 ? flatPhi[k - 1] : null;
|
|
136
|
+
const isIotaBoundary = !!prev && cur.iuIdx !== prev.iuIdx;
|
|
137
|
+
let raw = 0;
|
|
138
|
+
if (prev) {
|
|
139
|
+
const aHead = phraseHead(prev.tokens, memo); // left phrase head
|
|
140
|
+
const bHead = phraseHead(cur.tokens, memo); // right phrase head
|
|
141
|
+
const aLast = prev.tokens[prev.tokens.length - 1];
|
|
142
|
+
const bFirst = cur.tokens[0];
|
|
143
|
+
// base rank by level
|
|
144
|
+
raw += isIotaBoundary ? 3.0 : 1.0;
|
|
145
|
+
// punctuation coincidence
|
|
146
|
+
const p = punctBetween(words, aLast, bFirst);
|
|
147
|
+
if (p.strong)
|
|
148
|
+
raw += 2.0;
|
|
149
|
+
else if (p.comma)
|
|
150
|
+
raw += 1.2;
|
|
151
|
+
// length of the preceding constituent (look-back): longer → stronger
|
|
152
|
+
raw += Math.min(1.5, sylCount(prev.tokens) / 6);
|
|
153
|
+
// clause separation: a shallow dependency LCA (near the root) → strong
|
|
154
|
+
if (aHead && bHead) {
|
|
155
|
+
const lca = lcaDepth(aHead, bHead, memo);
|
|
156
|
+
raw += 1.2 * (1 - Math.min(1, lca / maxDepth)); // shallow LCA → +clause bonus
|
|
157
|
+
}
|
|
158
|
+
// associative coordination weakening: same coordinator across the boundary
|
|
159
|
+
if (startsCoordinator(cur.tokens) && !p.comma && !p.strong)
|
|
160
|
+
raw -= 0.4;
|
|
161
|
+
}
|
|
162
|
+
const info = {
|
|
163
|
+
level: isIotaBoundary ? 'iota' : 'phi',
|
|
164
|
+
strength: 0, // filled after normalisation
|
|
165
|
+
raw: Math.max(0, raw),
|
|
166
|
+
syllableIndex: cumSyl,
|
|
167
|
+
};
|
|
168
|
+
phi.push(info);
|
|
169
|
+
if (isIotaBoundary)
|
|
170
|
+
iota.push(info);
|
|
171
|
+
cumSyl += sylCount(cur.tokens);
|
|
172
|
+
}
|
|
173
|
+
// NSBR normalisation: scale to the strongest boundary in the line (0..1).
|
|
174
|
+
const maxRaw = Math.max(0, ...phi.map(b => b.raw));
|
|
175
|
+
for (const b of phi)
|
|
176
|
+
b.strength = maxRaw > 0 ? b.raw / maxRaw : 0;
|
|
177
|
+
return { phi, iota };
|
|
178
|
+
}
|
|
179
|
+
function startsCoordinator(tokens) {
|
|
180
|
+
const t = tokens[0];
|
|
181
|
+
return !!t && (t.lexicalClass === 'CC' || t.canonicalRel === 'CC');
|
|
182
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ClsSentence, ClsWord } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run the cyclic Compound + Nuclear Stress Rules (Wagner-extended) over the
|
|
4
|
+
* sentence's dependency tree and write each word's `phraseStress` (1 = strongest).
|
|
5
|
+
*/
|
|
6
|
+
export declare function computePhraseStress(sent: ClsSentence): void;
|
|
7
|
+
/**
|
|
8
|
+
* Partition the sentence's words into ϕ-domains over the dependency tree.
|
|
9
|
+
*/
|
|
10
|
+
export declare function computePhiDomains(sent: ClsSentence): Map<ClsWord, number>;
|
|
11
|
+
//# sourceMappingURL=bracketing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bracketing.d.ts","sourceRoot":"","sources":["../../src/calliope/bracketing.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AA4HnD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAmF3D;AA4GD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CA6BzE"}
|