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
package/README.md
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
# Calliope TS
|
|
2
2
|
|
|
3
|
-
**Calliope TS** is a phonological poetry
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Hand to this muse a song or verse,
|
|
7
|
-
And she might show you how it curves,
|
|
8
|
-
Which syllables hold weight in time,
|
|
9
|
-
How much of it, and where a rhyme,
|
|
10
|
-
Which meter dances through each line,
|
|
11
|
-
And where should land each beat
|
|
12
|
-
Your voice might drawl, or music drawn...
|
|
13
|
-
She even knows poetic form!
|
|
14
|
-
|
|
15
|
-
The premise of this toolkit is that rhythm, and therefore also meter, is much more reliably *recovered from pronunciation* than from text as such. What?! But there's no Text-to-Speech here?! (Sorry!) A lapse? A paradox? Nah. Instead, Calliope parses a stanza or verse, reproducing roughly how an attentive (but silent) reader might do so: first, working out how the words are likely to be said aloud — deduced by following contextual patterns, cohering a syntax, wielding phonotactics, evading constraints, gathering prosody, accounting for the ways phrases clump or bounce, for how a voice might subtly perk over clause-ends, and all the rest of it; and only thereafter scanning for what sort of rhythms a phrase such as that might be spectrally reaching for, or settling into. The scholars dwelling on Generative Metrics, a liminal little field somewhere in-between linguistics, phonetics, and literary studies, call this sort of approach **phonological scansion**, and the bulk of this README is about what that means and how the toolkit puts it to work.
|
|
3
|
+
**Calliope TS** is a phonological poetry-scansion toolkit for TypeScript / Node.js. Hand it a poem (or a single line), and it will tell you — and *show* you — how that verse actually moves: which syllables carry weight and how much, where the beats fall, what meter each line is in and how confidently, what rhymes with what, and what form the stanzas add up to.
|
|
4
|
+
|
|
5
|
+
The premise is that meter is something you *recover from pronunciation*, not something you stamp onto spelling. So Calliope reads a line roughly the way an attentive reader does: it first works out how the words would be said aloud — dictionary stress, the way phrases clump, where the voice peaks at a clause-end — and only then looks for the regular beat that saying settles into. This approach has a name in the linguistics literature, **phonological scansion**, and the bulk of this README is about what that means and how the toolkit puts it to work.
|
|
16
6
|
|
|
17
7
|
```
|
|
18
8
|
$ calliope-ts --reading exile.txt
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ClsWord, IntonationalUnit } from './types.js';
|
|
2
|
+
export type CaesuraKind = 'hard' | 'soft';
|
|
3
|
+
/** A caesura with its graded boundary strength (0..1, NSBR-scaled), so the display
|
|
4
|
+
* can colour the mark by how strong the underlying prosodic break is. */
|
|
5
|
+
export interface CaesuraInfo {
|
|
6
|
+
kind: CaesuraKind;
|
|
7
|
+
strength: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Caesura positions for a line, keyed by the syllable index AFTER which the pause
|
|
11
|
+
* falls (= number of syllables to its left).
|
|
12
|
+
* • 'hard' — an overt break at an Intonational-Unit boundary.
|
|
13
|
+
* • 'soft' — a single INFERRED medial caesura for a punctuation-free line.
|
|
14
|
+
* Candidates are the boundaries just before a phrase/clause onset
|
|
15
|
+
* (PHRASE_ONSET_POS); the one nearest the line's midpoint that lies in the
|
|
16
|
+
* central third AND coincides with a foot boundary wins — so it is medial,
|
|
17
|
+
* never mid-foot, and consistent across structurally-parallel lines. Read in
|
|
18
|
+
* LINEAR order (robust to clitic-group reordering); needs a line of ≥ 8
|
|
19
|
+
* syllables.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeCaesurae(words: ClsWord[], ius: IntonationalUnit[], scansion?: string): Map<number, CaesuraInfo>;
|
|
22
|
+
/**
|
|
23
|
+
* The words that immediately PRECEDE a caesura in a line — i.e. each word whose
|
|
24
|
+
* cumulative syllable count lands exactly on a caesura position. Used by the
|
|
25
|
+
* rhyme layer for pre-caesural internal-rhyme detection. Returned in linear
|
|
26
|
+
* (reading) order; the caesura kind is paired so callers can weight hard vs
|
|
27
|
+
* inferred breaks if they wish.
|
|
28
|
+
*/
|
|
29
|
+
export declare function preCaesuralWords(words: ClsWord[], ius: IntonationalUnit[], scansion?: string): {
|
|
30
|
+
word: ClsWord;
|
|
31
|
+
kind: CaesuraKind;
|
|
32
|
+
}[];
|
|
33
|
+
//# sourceMappingURL=caesura.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caesura.d.ts","sourceRoot":"","sources":["../src/caesura.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAIvD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1C;0EAC0E;AAC1E,MAAM,WAAW,WAAW;IAAG,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;CAAE;AAyCrE;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAuGtH;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,gBAAgB,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAC3D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,EAAE,CAYxC"}
|
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"}
|