calliope-ts 0.0.1
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/.claude/settings.local.json +8 -0
- package/.opencode/skills/calliope-ts/SKILL.md +74 -0
- package/LICENSE +201 -0
- package/README.md +485 -0
- package/dist/depfix.d.ts +13 -0
- package/dist/depfix.d.ts.map +1 -0
- package/dist/depfix.js +84 -0
- package/dist/display.d.ts +38 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +890 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +504 -0
- package/dist/parser.d.ts +10 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +688 -0
- package/dist/phonological.d.ts +41 -0
- package/dist/phonological.d.ts.map +1 -0
- package/dist/phonological.js +788 -0
- package/dist/rhyme.d.ts +26 -0
- package/dist/rhyme.d.ts.map +1 -0
- package/dist/rhyme.js +340 -0
- package/dist/scandroid.d.ts +17 -0
- package/dist/scandroid.d.ts.map +1 -0
- package/dist/scandroid.js +435 -0
- package/dist/scansion.d.ts +37 -0
- package/dist/scansion.d.ts.map +1 -0
- package/dist/scansion.js +1007 -0
- package/dist/scansion_debug.js +586 -0
- package/dist/stress.d.ts +32 -0
- package/dist/stress.d.ts.map +1 -0
- package/dist/stress.js +1372 -0
- package/dist/tagfix.d.ts +6 -0
- package/dist/tagfix.d.ts.map +1 -0
- package/dist/tagfix.js +101 -0
- package/dist/types.d.ts +173 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +62 -0
- package/src/depfix.ts +88 -0
- package/src/display.ts +954 -0
- package/src/index.ts +541 -0
- package/src/parser.ts +837 -0
- package/src/phonological.ts +849 -0
- package/src/rhyme.ts +328 -0
- package/src/scandroid.ts +434 -0
- package/src/scansion.ts +1053 -0
- package/src/stress.ts +1381 -0
- package/src/tagfix.ts +104 -0
- package/src/types.ts +230 -0
package/README.md
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# Calliope TS
|
|
2
|
+
|
|
3
|
+
**Calliope TS** is a phonological poetry-scansion toolkit for TypeScript / Node.js.
|
|
4
|
+
Give it a poem (or a single line of verse) and it will tell you — and *show*
|
|
5
|
+
you — how the poem sounds: which syllables carry stress and how much, where the
|
|
6
|
+
metrical beats fall, what meter each line is written in, how certain that
|
|
7
|
+
reading is, what the rhyme scheme is, and what poetic form the stanzas add up
|
|
8
|
+
to.
|
|
9
|
+
|
|
10
|
+
It is not a syllable-counter with a rulebook bolted on. Calliope TS scans verse
|
|
11
|
+
the way a trained reader does: it first works out how the words would actually
|
|
12
|
+
be *spoken* — dictionary stress, phrase rhythm, sentence intonation — and only
|
|
13
|
+
then asks which metrical pattern that spoken contour fits best.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
$ calliope-ts --reading exile.txt
|
|
17
|
+
|
|
18
|
+
Reading View — stress gradient over the original text
|
|
19
|
+
|
|
20
|
+
He happens to be a French poet, that thin, ← every syllable colour-coded
|
|
21
|
+
book-carrying man with a bristly gray chin; by its stress level
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
Stress Maps & Meter — top-3 fit scores per line
|
|
25
|
+
S1L1 nsw|xwx|msw ‖ xs amphibrachic tetrameter (amph 1.20 · iamb 1.14 ·
|
|
26
|
+
anap 1.13) A
|
|
27
|
+
S1L2 m|-mww|sxx|mwm|s amphibrachic tetrameter ≈ continuity; standalone:
|
|
28
|
+
dactylic tetrameter A(perfect)
|
|
29
|
+
...
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Contents
|
|
35
|
+
|
|
36
|
+
- [Installation](#installation)
|
|
37
|
+
- [Quick start (CLI)](#quick-start-cli)
|
|
38
|
+
- [Reading the output](#reading-the-output)
|
|
39
|
+
- [How it works: the scansion pipeline](#how-it-works-the-scansion-pipeline)
|
|
40
|
+
- [Programmatic usage (API)](#programmatic-usage-api)
|
|
41
|
+
- [Examples](#examples)
|
|
42
|
+
- [Background and lineage](#background-and-lineage)
|
|
43
|
+
- [Scope, strengths, and honest limitations](#scope-strengths-and-honest-limitations)
|
|
44
|
+
- [Development](#development)
|
|
45
|
+
- [License and credits](#license-and-credits)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
Requires **Node.js ≥ 20** (≥ 22 recommended — one optional analysis stage, the
|
|
52
|
+
dependency-tree repair, needs Node's `require(esm)` support and degrades
|
|
53
|
+
gracefully without it).
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# As a global CLI tool
|
|
57
|
+
npm install -g calliope-ts
|
|
58
|
+
|
|
59
|
+
# Or as a library in your project
|
|
60
|
+
npm install calliope-ts
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
From a clone of the repository:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run build # compiles src/ → dist/
|
|
68
|
+
npm test # vitest suite
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick start (CLI)
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# 1. Scan a line directly
|
|
77
|
+
calliope-ts "Shall I compare thee to a summer's day?"
|
|
78
|
+
|
|
79
|
+
# 2. Scan a whole poem from a file (blank lines separate stanzas)
|
|
80
|
+
calliope-ts poem.txt
|
|
81
|
+
|
|
82
|
+
# 3. The Reading View — the poem itself, colour-coded by stress (recommended
|
|
83
|
+
# for whole poems; add -r or --reading)
|
|
84
|
+
calliope-ts --reading poem.txt
|
|
85
|
+
|
|
86
|
+
# 4. Pipe text in
|
|
87
|
+
cat poem.txt | calliope-ts --reading
|
|
88
|
+
|
|
89
|
+
# 5. Interactive menu (run with no arguments in a terminal)
|
|
90
|
+
calliope-ts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The interactive menu offers: multi-line paste-and-scan (reading view),
|
|
94
|
+
single-line detailed analysis, line-by-line analysis, file input in either
|
|
95
|
+
view, and a **legend** explaining every symbol and colour.
|
|
96
|
+
|
|
97
|
+
There are two display modes:
|
|
98
|
+
|
|
99
|
+
| Mode | Flag | What you get |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| **Detailed view** | *(default)* | Per line: the tagged text, the phrase structure, lexical and relative stress maps, phonological bracketing, the foot-by-foot scansion, the dependency tree, and a summary (meter, fit %, scansion string). |
|
|
102
|
+
| **Reading view** | `--reading` / `-r` | The poem in its original formatting with every syllable coloured by stress, followed by one compact line per verse-line: stress map, meter, top-3 candidate scores, rhyme-scheme letter, and any rhythm/continuity notes. |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Reading the output
|
|
107
|
+
|
|
108
|
+
### The five stress levels
|
|
109
|
+
|
|
110
|
+
Calliope TS grades every syllable on a five-tier *relative* scale (this is the
|
|
111
|
+
core representation everything else is built on):
|
|
112
|
+
|
|
113
|
+
| Symbol | Name | Typical bearer |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `x` | zero-provision | maximally reduced function words: *the, a, of, and, to* |
|
|
116
|
+
| `w` | weak | unstressed syllables of content words; unreduced function words (*be, just, here*) |
|
|
117
|
+
| `n` | low | lightly stressed syllables; pronouns and modals with citation stress (*he, might*) |
|
|
118
|
+
| `m` | moderate | secondary stresses; stressed syllables demoted by a neighbouring stronger one |
|
|
119
|
+
| `s` | strong | primary stresses, phrase peaks, line-final nuclei |
|
|
120
|
+
|
|
121
|
+
A line's **stress map** is simply its syllables in order, e.g.
|
|
122
|
+
`xs|wxm|wxs|xws` — with `|` marking foot boundaries, `‖` a strong caesura
|
|
123
|
+
(major punctuation pause), `¦` a lighter phrase break, and `-` a "silent beat"
|
|
124
|
+
inside a stress clash.
|
|
125
|
+
|
|
126
|
+
### Meter lines
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
S1L2 m|-mww|sxx|mwm|s amphibrachic tetrameter (dact 1.21 · amph 1.17 ·
|
|
130
|
+
anap 1.16) ≈ continuity; standalone: dactylic tetrameter A(perfect)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **`amphibrachic tetrameter`** — the line's meter: foot type + number of feet.
|
|
134
|
+
- **`(dact 1.21 · amph 1.17 · anap 1.16)`** — the top three candidate meters
|
|
135
|
+
with their raw fit scores. The named meter need not be the numerically
|
|
136
|
+
first candidate: ties between sibling meters are resolved by principled
|
|
137
|
+
criteria (word integrity, caesura alignment, stanza context — see below).
|
|
138
|
+
- **`≈ continuity; standalone: dactylic tetrameter`** — this line, taken
|
|
139
|
+
alone, fits dactylic a hair better; but the stanza is amphibrachic and the
|
|
140
|
+
line fits amphibrachic nearly as well, so the continuity reading is
|
|
141
|
+
promoted to the base reading and the standalone result is kept as a note.
|
|
142
|
+
- **`↔ aligns w/ stanza …`** — a weaker version of the same: the line stays
|
|
143
|
+
with its own meter but is flagged as compatible with the stanza's.
|
|
144
|
+
- **`♪ 4-beat accentual`** / `♪ 3-ictus dolnik` — a *rhythm note*: the stanza
|
|
145
|
+
is not accentual-syllabic at all but keeps a constant count of strong beats
|
|
146
|
+
with varying syllable counts (see "Beyond classical meters" below).
|
|
147
|
+
- **`A`, `B(perfect)`, `·`** — the rhyme-scheme letter for the line's end
|
|
148
|
+
word, with the rhyme type when the line rhymes with an earlier one
|
|
149
|
+
(`perfect`, `rich`, `family`, `assonant`, `consonant`, `augmented`,
|
|
150
|
+
`diminished`, `wrenched`, `eye`, `identical`). `·` = unrhymed.
|
|
151
|
+
- **`❡ ballad stanza (ABCB, 4·3)`** — a stanza-level *form* verdict (shown in
|
|
152
|
+
the stanza header): ballad stanza, blank verse, couplet, limerick,
|
|
153
|
+
Shakespearean/Petrarchan sonnet, terza rima, etc.
|
|
154
|
+
- **Certainty / fit %** (detailed view) — the share of the line realised by
|
|
155
|
+
clean, unsubstituted feet, tempered by phrase-edge agreement. A perfectly
|
|
156
|
+
regular line reads 100%; real verse usually lands between 50 and 90.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## How it works: the scansion pipeline
|
|
161
|
+
|
|
162
|
+
Most automatic scanners pattern-match syllable counts against meter templates.
|
|
163
|
+
Calliope TS instead follows the **phonological scansion** method: derive the
|
|
164
|
+
line's natural spoken prominence first, then fit meters to it. The pipeline
|
|
165
|
+
has eight stages.
|
|
166
|
+
|
|
167
|
+
**1. Parsing (who does what in the sentence).**
|
|
168
|
+
The line is tokenized, part-of-speech tagged, and dependency-parsed (which
|
|
169
|
+
word grammatically governs which) using the FinNLP family of libraries
|
|
170
|
+
(`lexed`, `en-pos`, `en-parse`). Two correction layers sit inside this stage,
|
|
171
|
+
because verse breaks taggers in predictable ways: a *tag-repair* pass fixes
|
|
172
|
+
systematic errors before the dependency tree is built (archaic forms like
|
|
173
|
+
*thou/thy/doth/wherefore*, the pronoun *I*, perfect-tense participles like
|
|
174
|
+
*had quit*), and a *tree-repair* pass (built on the
|
|
175
|
+
[depedits](https://www.npmjs.com/package/depedits) rule engine) fixes
|
|
176
|
+
systematic attachment errors after it (e.g. noun compounds parsed as double
|
|
177
|
+
objects). Hyphenated compounds and contractions (*we'll*, *don't*, archaic
|
|
178
|
+
*fix'd*) are re-merged into single metrical words.
|
|
179
|
+
|
|
180
|
+
**2. Lexical stress (how each word is pronounced).**
|
|
181
|
+
Every word is looked up in an augmented CMU pronouncing dictionary
|
|
182
|
+
([nounsing-pro](https://www.npmjs.com/package/nounsing-pro)): syllable count,
|
|
183
|
+
stress pattern (primary / secondary / unstressed), syllable weights, and
|
|
184
|
+
vowel quantities. Words not in the dictionary go through a morphological
|
|
185
|
+
fallback (strip a productive suffix, look up the stem, restore) and finally a
|
|
186
|
+
quantity-sensitive English Stress Rule. Poetic elisions are honoured:
|
|
187
|
+
*heav'n* is one syllable, *o'er* one, *th'expense* two, *'tis/'twas* reduce,
|
|
188
|
+
and archaic *-'d* / *-'st* forms (*fix'd*, *stopp'st*) keep their elided
|
|
189
|
+
syllable counts.
|
|
190
|
+
|
|
191
|
+
**3. The prosodic hierarchy (how the line phrases).**
|
|
192
|
+
Words are grouped the way speech groups them: each content word attracts its
|
|
193
|
+
function-word satellites into a **clitic group** (CP); clitic groups join into
|
|
194
|
+
**phonological phrases** (PP) along the syntactic dependencies; phrases join
|
|
195
|
+
into **intonational units** (IU) bounded by major punctuation. Crucially, the
|
|
196
|
+
**line is the scansion domain**: a verse line containing several grammatical
|
|
197
|
+
sentences is still ONE metrical unit (the internal full stops become strong
|
|
198
|
+
caesurae), and quotation marks are *not* treated as prosodic breaks.
|
|
199
|
+
|
|
200
|
+
**4. Phrase-level stress rules.**
|
|
201
|
+
Three classic rules adjust the dictionary stresses in context: the **compound
|
|
202
|
+
rule** (left element stressed: *CITY hall*), the **nuclear stress rule** (the
|
|
203
|
+
last content word of an intonational unit gets the phrase's peak), and a set
|
|
204
|
+
of **clash resolutions** (two adjacent strong syllables cannot both keep full
|
|
205
|
+
prominence — one yields, chosen by syntactic direction). The result is mapped
|
|
206
|
+
onto the five-tier `x w n m s` scale.
|
|
207
|
+
|
|
208
|
+
**5. Key stresses, "beginnings free, endings strict."**
|
|
209
|
+
Following McAleese's central insight, the stresses at the *right edges* of
|
|
210
|
+
prosodic units are the reliable ones — speakers may start a phrase loosely
|
|
211
|
+
but they land its ending. Each unit contributes its right-edge "key stress"
|
|
212
|
+
with a weight (intonational unit > phrase > long word > clitic group), and
|
|
213
|
+
meters that place beats on those key stresses are rewarded.
|
|
214
|
+
|
|
215
|
+
**6. Meter fitting (dynamic programming).**
|
|
216
|
+
For each of seven candidate meters — iambic, trochaic, anapestic, dactylic,
|
|
217
|
+
**amphibrachic**, bacchic, spondaic — the engine finds the optimal division of
|
|
218
|
+
the line into feet, using that meter's full inventory of legitimate
|
|
219
|
+
variations: inversions at line-start or after a caesura, pyrrhic and spondaic
|
|
220
|
+
substitutions, catalexis (truncated final foot), anacrusis (extrametrical
|
|
221
|
+
upbeat), feminine endings, acephalous openings. Each syllable is scored
|
|
222
|
+
against the metrical position it lands in (a strong syllable in a beat slot
|
|
223
|
+
is ideal; a reduced clitic forced onto a beat is the cardinal violation), and
|
|
224
|
+
an **Attridge promotion** rule lets a weak syllable carry a beat when it is
|
|
225
|
+
flanked by even weaker ones (*"happens to BE a French poet"*) — which is how
|
|
226
|
+
real readers actually deliver such lines.
|
|
227
|
+
|
|
228
|
+
**7. Arbitration and context.**
|
|
229
|
+
The winning meter is not just the top raw score:
|
|
230
|
+
- **Ternary siblings** (anapest / amphibrach / dactyl) frequently fit a line
|
|
231
|
+
with the *identical* beat pattern — then the name is decided by which
|
|
232
|
+
division avoids splitting words across feet and aligns with the line's
|
|
233
|
+
pauses, not by score noise.
|
|
234
|
+
- **Stanza consensus**: each stanza's dominant meter is identified (for
|
|
235
|
+
ternary verse, by the stanza's *anacrusis profile* — how many slack
|
|
236
|
+
syllables open the lines, following Russian metrics). A line that fits the
|
|
237
|
+
dominant meter nearly as well as its own standalone winner **adopts the
|
|
238
|
+
dominant meter** as its base reading, with the standalone reading preserved
|
|
239
|
+
as a note. Metrical continuity outranks a hair of fit score, exactly as it
|
|
240
|
+
does for a human scander.
|
|
241
|
+
|
|
242
|
+
**8. Beyond classical meters.**
|
|
243
|
+
If a stanza's syllable counts vary while its strong-beat counts stay constant
|
|
244
|
+
— and no classical meter dominates — the stanza is read as **accentual verse**
|
|
245
|
+
(`4-beat accentual`), or more precisely as a **dolnik** (1–2 slack syllables
|
|
246
|
+
between beats) or **taktovik** (1–3), the categories of Russian verse theory
|
|
247
|
+
that also fit much English song, ballad, and modernist verse. Free verse is
|
|
248
|
+
recognised both line-by-line and stanza-wide. Independently, a **rhyme
|
|
249
|
+
layer** classifies every line-end pair (perfect / rich / family / slant /
|
|
250
|
+
eye / … rhymes, masculine / feminine / dactylic), detects the rhyme scheme,
|
|
251
|
+
and a **form layer** names what the stanzas amount to: ballad stanza (rhyme
|
|
252
|
+
scheme *and* alternating 4·3 beats), blank verse (unrhymed iambic
|
|
253
|
+
pentameter), couplets, quatrains, limerick, rhyme royal, Shakespearean or
|
|
254
|
+
Petrarchan sonnet, terza rima.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Programmatic usage (API)
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import {
|
|
262
|
+
analyzeStanzas, // poem text → LineResult[][] (per stanza, per line)
|
|
263
|
+
analyzeText, // poem text → LineResult[] (flat)
|
|
264
|
+
analyzeReadingDocument, // poem text → ReadingStanza[] (keeps verbatim lines)
|
|
265
|
+
} from 'calliope-ts';
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Scan a poem
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { analyzeStanzas } from 'calliope-ts';
|
|
272
|
+
|
|
273
|
+
const poem = `He happens to be a French poet, that thin,
|
|
274
|
+
book-carrying man with a bristly gray chin;
|
|
275
|
+
you meet him whenever you go`;
|
|
276
|
+
|
|
277
|
+
const stanzas = analyzeStanzas(poem);
|
|
278
|
+
|
|
279
|
+
for (const stanza of stanzas) {
|
|
280
|
+
for (const line of stanza) {
|
|
281
|
+
const d = line.phonologicalScansion;
|
|
282
|
+
console.log(d.meter); // "amphibrachic tetrameter"
|
|
283
|
+
console.log(d.scansion); // "nsw|xwx|msw|xs"
|
|
284
|
+
console.log(d.footCount); // 4
|
|
285
|
+
console.log(d.certainty); // 0–100
|
|
286
|
+
console.log(d.ranking); // [{ meter: 'amphibrachic', score: 1.20 }, …]
|
|
287
|
+
console.log(d.standaloneMeter);// set when stanza continuity renamed the line
|
|
288
|
+
console.log(d.rhythmNote); // "4-beat accentual", "3-ictus dolnik", …
|
|
289
|
+
console.log(d.rhyme); // { endWord, letter: 'A', type: 'perfect', … }
|
|
290
|
+
console.log(d.formNote); // "ballad stanza (ABCB, 4·3)", "blank verse", …
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Inspect the linguistic analysis
|
|
296
|
+
|
|
297
|
+
Each `LineResult` also carries the full intermediate analysis:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
const line = stanzas[0][0];
|
|
301
|
+
|
|
302
|
+
// Words with POS tags, content/function status, and per-syllable stress
|
|
303
|
+
for (const w of line.sentence.words) {
|
|
304
|
+
console.log(w.word, w.lexicalClass, w.isContent,
|
|
305
|
+
w.syllables.map(s => s.relativeStress).join(''));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// The dependency tree
|
|
309
|
+
for (const dep of line.sentence.dependencies) {
|
|
310
|
+
console.log(`${dep.dependentName} ←${dep.dependentType}← ${dep.governorName}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// The prosodic hierarchy: IU → PP → clitic groups
|
|
314
|
+
console.log(line.phonologicalHierarchy);
|
|
315
|
+
|
|
316
|
+
// The weighted key stresses extracted from unit right-edges
|
|
317
|
+
console.log(line.keyStresses);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Rhyme utilities
|
|
321
|
+
|
|
322
|
+
The rhyme classifier is exported on its own:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { classifyRhymePair, detectScheme } from 'calliope-ts/dist/rhyme.js';
|
|
326
|
+
|
|
327
|
+
classifyRhymePair('grace', 'face');
|
|
328
|
+
// → { type: 'perfect', structure: 'masculine' }
|
|
329
|
+
|
|
330
|
+
classifyRhymePair('picky', 'tricky');
|
|
331
|
+
// → { type: 'perfect', structure: 'feminine' }
|
|
332
|
+
|
|
333
|
+
detectScheme(['Mariner', 'three', 'eye', 'me']).map(r => r.letter).join('');
|
|
334
|
+
// → "·A·A" (i.e. ABCB with unrhymed lines marked ·)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Key result types
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
interface PhonologicalScansionDetail {
|
|
341
|
+
meter: string; // "iambic pentameter"
|
|
342
|
+
meterName: MetreName | 'free verse';
|
|
343
|
+
footCount: number;
|
|
344
|
+
scansion: string; // "xs|wxm|wxs|xws"
|
|
345
|
+
certainty: number; // 0–100
|
|
346
|
+
ranking?: MeterScore[]; // all candidate meters, best first
|
|
347
|
+
consensusMeter?: string; // "aligns with stanza X" annotation
|
|
348
|
+
standaloneMeter?: string; // pre-continuity-rename reading
|
|
349
|
+
rhythmNote?: string; // dolnik / taktovik / accentual verdicts
|
|
350
|
+
rhyme?: { endWord: string; letter: string; type?: string; matchedLine?: number };
|
|
351
|
+
formNote?: string; // stanza/poem form verdict
|
|
352
|
+
// … plus the raw weighted-score fields
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Lower-level functions (`parseDocument`, `assignLexicalStress`,
|
|
357
|
+
`buildPhonologicalHierarchy`, `scoreMeters`, …) are exported from their
|
|
358
|
+
modules under `calliope-ts/dist/*` for users who want to run or modify
|
|
359
|
+
individual pipeline stages. The optional Scandroid comparison engines
|
|
360
|
+
(Charles Hartman's "Corral the Weird" and "Maximize the Normal") are exported
|
|
361
|
+
from `calliope-ts/dist/scandroid.js`.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## Examples
|
|
366
|
+
|
|
367
|
+
**A Shakespeare sonnet** (`calliope-ts --reading sonnet130.txt`) — every line
|
|
368
|
+
identified as iambic pentameter; the scheme letters spell ABAB CDCD EFEF GG;
|
|
369
|
+
the stanza header reads `❡ Shakespearean Sonnet`.
|
|
370
|
+
|
|
371
|
+
**A ballad quatrain:**
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
It is an ancient Mariner,
|
|
375
|
+
And he stoppeth one of three.
|
|
376
|
+
"By thy long grey beard and glittering eye,
|
|
377
|
+
Now wherefore stopp'st thou me?
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
→ lines of iambic tetrameter / trimeter, scheme `·A·A`, and the form verdict
|
|
381
|
+
`❡ ballad stanza (ABCB, 4·3)` — the rhyme scheme *and* the alternating
|
|
382
|
+
4-beat/3-beat design both check out.
|
|
383
|
+
|
|
384
|
+
**Accentual verse** (Wyatt, *They flee from me*) — no classical meter
|
|
385
|
+
dominates and syllable counts vary, but every line carries four strong beats:
|
|
386
|
+
each line is annotated `♪ 4-beat accentual`.
|
|
387
|
+
|
|
388
|
+
**Amphibrachic verse** (Nabokov wrote his poem "Exile" as a demonstration
|
|
389
|
+
of English amphibrachs — the `x S x` foot): stanza consensus reads the poem's
|
|
390
|
+
constant one-syllable anacrusis as amphibrachic, names near-tie lines
|
|
391
|
+
accordingly with `≈ continuity` notes, and reports the `aabccb` rhyme
|
|
392
|
+
envelope.
|
|
393
|
+
|
|
394
|
+
**Blank verse** (Frost, *Mending Wall*) — unrhymed lines, dominant iambic
|
|
395
|
+
pentameter: `❡ blank verse`.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Background and lineage
|
|
400
|
+
|
|
401
|
+
**Calliope (2008).** The method implemented here originates with Gareth
|
|
402
|
+
McAleese's *Calliope*, developed for his M.Sc. at the Open University
|
|
403
|
+
(*"Improving Scansion with Syntax: an Investigation into the Effectiveness of
|
|
404
|
+
a Syntactic Analysis of Poetry by Computer using Phonological Scansion
|
|
405
|
+
Theory"*, Technical Report 2007/26, submitted 2008). McAleese's central idea:
|
|
406
|
+
scansion should be computed from a line's *phonology* — the prosodic
|
|
407
|
+
hierarchy and the stresses at the right edges of its units ("beginnings free,
|
|
408
|
+
endings strict") — rather than from orthographic pattern-matching. Calliope TS
|
|
409
|
+
re-implements that architecture on a modern Node.js stack and extends it.
|
|
410
|
+
|
|
411
|
+
**Scandroid (1996/2005).** Charles O. Hartman's Scandroid, a classic
|
|
412
|
+
foot-by-foot scanner (GNU GPL), is included as an optional comparison engine:
|
|
413
|
+
its "Corral the Weird" and "Maximize the Normal" algorithms can be run side
|
|
414
|
+
by side with the phonological scansion.
|
|
415
|
+
|
|
416
|
+
**Extensions beyond McAleese.** The five-tier stress gradient with a
|
|
417
|
+
zero-provision level; a unified dynamic-programming meter fitter with full
|
|
418
|
+
substitution inventories (and first-class **amphibrach** support); beat
|
|
419
|
+
promotion after Derek Attridge's beat/offbeat analysis; the dolnik/taktovik
|
|
420
|
+
classification from Russian verse theory (Gasparov); stanza-consensus and
|
|
421
|
+
anacrusis-profile naming; the rhyme and form layers; and the parse-correction
|
|
422
|
+
layers for verse-specific tagging.
|
|
423
|
+
|
|
424
|
+
**Data.** Lexical phonology comes from the augmented CMU dictionary published
|
|
425
|
+
as `nounsing-pro` (syllabification, stress transcription, syllable structure,
|
|
426
|
+
morphological stress classes). Parsing uses the FinNLP family. Dependency
|
|
427
|
+
repairs use `depedits`, a TypeScript port of the DepEdit rule engine.
|
|
428
|
+
|
|
429
|
+
Calliope TS is developed by **Aleksey Calvin** /
|
|
430
|
+
[SilverAgePoets.com](https://www.SilverAgePoets.com), with particular care for
|
|
431
|
+
verse traditions that standard English-centric tools marginalize: ternary
|
|
432
|
+
meters, Russian Silver-Age forms in translation, song lyrics, and accentual
|
|
433
|
+
verse.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Scope, strengths, and honest limitations
|
|
438
|
+
|
|
439
|
+
**Strengths.** Line-level stress mapping is the core competence — the Reading
|
|
440
|
+
View is usable as a reading aid in itself. Ternary meters (anapest /
|
|
441
|
+
amphibrach / dactyl) are first-class citizens, not afterthoughts. Accentual
|
|
442
|
+
and dolnik verse get a real classification instead of a "free verse" shrug.
|
|
443
|
+
Whole-poem context (stanza consensus, rhyme, form) informs line verdicts the
|
|
444
|
+
way it does for human readers.
|
|
445
|
+
|
|
446
|
+
**Limitations to know about.**
|
|
447
|
+
- English only (the dictionary and parser are English; other languages will
|
|
448
|
+
produce nonsense rather than errors).
|
|
449
|
+
- Stress-doublet words (*rebel*, *content*, names like *Hugo*) are read with
|
|
450
|
+
their dictionary stress; a rhyme-driven correction is on the roadmap.
|
|
451
|
+
- Rare or foreign proper names fall back to rule-based stress guesses.
|
|
452
|
+
- The form layer recognises the common stanza forms; it does not yet attempt
|
|
453
|
+
villanelle/pantoum repetition checking or syllabic forms.
|
|
454
|
+
- Meter identification on single lines without context is genuinely harder
|
|
455
|
+
than with stanza context — when possible, scan whole poems.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Development
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
npm run build # tsc → dist/
|
|
463
|
+
npm test # vitest (73 tests: pipeline, stress, meters, rhyme, forms)
|
|
464
|
+
npm run dev # ts-node
|
|
465
|
+
|
|
466
|
+
# Benchmark harnesses (require the annotated corpora in tests/)
|
|
467
|
+
node trials/mcaleese_benchmark.mjs # McAleese's own trial poems + expert keys
|
|
468
|
+
node trials/corpus_benchmark.mjs # litlab / prosodic / epg64 meter corpora
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The repository's `AGENTS.md` carries a dated engineering log of every
|
|
472
|
+
substantive change with its rationale and verification.
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## License and credits
|
|
477
|
+
|
|
478
|
+
Apache-2.0. © Aleksey Calvin Tsukanov / SilverAgePoets.com.
|
|
479
|
+
|
|
480
|
+
Methodological debts: Gareth McAleese (Calliope, the phonological scansion
|
|
481
|
+
procedure); Charles O. Hartman (Scandroid); Derek Attridge (beat/offbeat
|
|
482
|
+
rhythm and the English dolnik); M. L. Gasparov (dolnik/taktovik taxonomy);
|
|
483
|
+
Bruce Hayes, Paul Kiparsky, and the generative-metrics tradition (stress
|
|
484
|
+
theory, syllable quantity, extrametricality); the CMU Pronouncing Dictionary
|
|
485
|
+
and its `nounsing-pro` augmentation.
|
package/dist/depfix.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface FinDepNode {
|
|
2
|
+
label: string;
|
|
3
|
+
type: string;
|
|
4
|
+
parent: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Repair systematic en-parse attachment errors. Returns a new deps array
|
|
8
|
+
* (same shape as en-parse's `toArray` output); on any failure returns the
|
|
9
|
+
* input unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyDepFixes(tokens: string[], tags: string[], deps: FinDepNode[]): FinDepNode[];
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=depfix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"depfix.d.ts","sourceRoot":"","sources":["../src/depfix.ts"],"names":[],"mappings":"AAiBA,UAAU,UAAU;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAqCD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAwBhG"}
|
package/dist/depfix.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// depfix.ts — Post-parse dependency repair via DepEdit rules (the `depedits`
|
|
2
|
+
// npm package, the maintainer's TypeScript port of DepEdit).
|
|
3
|
+
//
|
|
4
|
+
// Runs AFTER en-parse, complementing the pre-parse tag corrections in
|
|
5
|
+
// tagfix.ts: tagfix repairs what the tagger got wrong before the tree is
|
|
6
|
+
// built; this pass repairs systematic attachment errors en-parse makes even
|
|
7
|
+
// with correct tags. Rules are written in DepEdit's declarative format
|
|
8
|
+
// (definitions ⟶ relations ⟶ actions, tab-separated) over en-parse's own
|
|
9
|
+
// label space (DOBJ/NSUBJ/DEP/…), so the round-trip is lossless and every
|
|
10
|
+
// rule is independently testable.
|
|
11
|
+
//
|
|
12
|
+
// The rule set is deliberately small and evidence-based — each rule cites the
|
|
13
|
+
// observed failure it corrects. `depedits` is ESM-only; it is loaded lazily
|
|
14
|
+
// and failures degrade gracefully (the unrepaired parse is still a parse).
|
|
15
|
+
import { createRequire } from 'module';
|
|
16
|
+
// Observed failure (probe: "I had quit the programming paradigm"): en-parse
|
|
17
|
+
// attaches BOTH nouns of a noun compound to the verb as parallel objects
|
|
18
|
+
// ("programming ←DOBJ← quit", "paradigm ←DOBJ← quit"), and leaves the
|
|
19
|
+
// determiner dangling on the first noun as generic DEP. The repairs:
|
|
20
|
+
// 1. Two adjacent common nouns sharing a governor with the same object
|
|
21
|
+
// relation → the first is a compound modifier (AMOD) of the second.
|
|
22
|
+
// 2. A determiner left as DEP on a noun that has become a modifier →
|
|
23
|
+
// re-attach it as DET to that noun's head (the true NP head).
|
|
24
|
+
const CALLIOPE_DEP_FIXES = [
|
|
25
|
+
'xpos=/NNS?/&func=/DOBJ|IOBJ/;xpos=/NNS?/&func=/DOBJ|IOBJ/;xpos=/VB.*/\t#3>#1;#3>#2;#1.#2\t#2>#1;#1:func=AMOD',
|
|
26
|
+
'xpos=/DT/&func=/DEP|EXT/;xpos=/NNS?/&func=/AMOD/;xpos=/NNS?.*/\t#2>#1;#3>#2\t#3>#1;#1:func=DET',
|
|
27
|
+
].join('\n');
|
|
28
|
+
let engine;
|
|
29
|
+
function loadEngine() {
|
|
30
|
+
if (engine !== undefined)
|
|
31
|
+
return engine;
|
|
32
|
+
try {
|
|
33
|
+
// This package compiles to ESM, where bare `require` does not exist, and
|
|
34
|
+
// the parse path is synchronous, so dynamic import() is not an option:
|
|
35
|
+
// createRequire gives a sync loader, and since `depedits` is itself
|
|
36
|
+
// ESM-only this resolves via Node's require(esm) (≥20.17 / ≥22.12). On
|
|
37
|
+
// older runtimes it throws and the repair pass degrades to a no-op (the
|
|
38
|
+
// unrepaired parse is still a parse).
|
|
39
|
+
const req = createRequire(import.meta.url);
|
|
40
|
+
const { DepEditEngine } = req('depedits');
|
|
41
|
+
const e = new DepEditEngine();
|
|
42
|
+
e.loadIniString(CALLIOPE_DEP_FIXES);
|
|
43
|
+
engine = e;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
engine = null;
|
|
47
|
+
}
|
|
48
|
+
return engine;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Repair systematic en-parse attachment errors. Returns a new deps array
|
|
52
|
+
* (same shape as en-parse's `toArray` output); on any failure returns the
|
|
53
|
+
* input unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export function applyDepFixes(tokens, tags, deps) {
|
|
56
|
+
const e = loadEngine();
|
|
57
|
+
if (!e || tokens.length === 0 || deps.length !== tokens.length)
|
|
58
|
+
return deps;
|
|
59
|
+
try {
|
|
60
|
+
const conllu = tokens.map((tok, i) => {
|
|
61
|
+
const head = deps[i].parent >= 0 ? deps[i].parent + 1 : 0;
|
|
62
|
+
const safe = tok.replace(/\s/g, '_') || '_';
|
|
63
|
+
return `${i + 1}\t${safe}\t${safe}\t_\t${tags[i] || '_'}\t_\t${head}\t${deps[i].label || 'DEP'}\t_\t_`;
|
|
64
|
+
}).join('\n') + '\n\n';
|
|
65
|
+
const out = e.process(conllu);
|
|
66
|
+
const fixed = deps.map(d => ({ ...d }));
|
|
67
|
+
for (const row of out.split('\n')) {
|
|
68
|
+
const cols = row.split('\t');
|
|
69
|
+
if (cols.length < 10)
|
|
70
|
+
continue;
|
|
71
|
+
const idx = parseInt(cols[0], 10) - 1;
|
|
72
|
+
if (!(idx >= 0 && idx < fixed.length))
|
|
73
|
+
continue;
|
|
74
|
+
const head = parseInt(cols[6], 10);
|
|
75
|
+
fixed[idx].parent = Number.isFinite(head) ? head - 1 : fixed[idx].parent;
|
|
76
|
+
if (cols[7] && cols[7] !== '_')
|
|
77
|
+
fixed[idx].label = cols[7];
|
|
78
|
+
}
|
|
79
|
+
return fixed;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return deps;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ClsWord, LineResult } from './types.js';
|
|
2
|
+
export declare function renderUnifiedDisplay(result: LineResult, rawLine?: string): string;
|
|
3
|
+
export declare function renderLegend(): string;
|
|
4
|
+
/**
|
|
5
|
+
* The long-form legend triggered from the main menu's "Display Legend" option:
|
|
6
|
+
* the compact legend PLUS full Penn POS-tag and grammatical-dependency glossaries.
|
|
7
|
+
* (These glossaries are deliberately NOT part of the compact in-output legend.)
|
|
8
|
+
*/
|
|
9
|
+
export declare function renderFullLegend(): string;
|
|
10
|
+
/** One input line with its (1+) parsed sentence results. */
|
|
11
|
+
export interface ReadingLine {
|
|
12
|
+
raw: string;
|
|
13
|
+
results: LineResult[];
|
|
14
|
+
}
|
|
15
|
+
/** A stanza: a run of consecutive non-blank input lines. */
|
|
16
|
+
export interface ReadingStanza {
|
|
17
|
+
lines: ReadingLine[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Project per-syllable stress colours back onto the original line, preserving
|
|
21
|
+
* capitalisation, punctuation, spacing and any extrametrical fragments the
|
|
22
|
+
* pipeline dropped (e.g. possessive "'s"). Word-like tokens are coloured;
|
|
23
|
+
* everything between them (spaces, punctuation, dashes) is emitted verbatim.
|
|
24
|
+
*
|
|
25
|
+
* Alignment is tolerant: it matches each token to the next parsed word by
|
|
26
|
+
* normalised form (equal, or token starts with the word — handling "cat's"),
|
|
27
|
+
* with a small look-ahead resync so a stray/unsyllabified token never derails
|
|
28
|
+
* the rest of the line. No original character is ever dropped.
|
|
29
|
+
*/
|
|
30
|
+
export declare function projectStressOntoLine(rawLine: string, words: ClsWord[]): string;
|
|
31
|
+
/**
|
|
32
|
+
* Reading view: the poem itself in its original formatting, each syllable
|
|
33
|
+
* coloured by 4-tier relative stress, followed by a same-structure block of
|
|
34
|
+
* per-line stress maps + meter (with top-3 fit scores). This is the whole
|
|
35
|
+
* output for this mode — not the full per-line analytic dump.
|
|
36
|
+
*/
|
|
37
|
+
export declare function renderReadingView(stanzas: ReadingStanza[]): string;
|
|
38
|
+
//# sourceMappingURL=display.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"display.d.ts","sourceRoot":"","sources":["../src/display.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,OAAO,EAMP,UAAU,EAGX,MAAM,YAAY,CAAC;AAqJpB,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAkPjF;AAMD,wBAAgB,YAAY,IAAI,MAAM,CAUrC;AAuHD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAsBzC;AAqCD,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAuC/E;AAmMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,CAsDlE"}
|