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.
Files changed (50) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.opencode/skills/calliope-ts/SKILL.md +74 -0
  3. package/LICENSE +201 -0
  4. package/README.md +485 -0
  5. package/dist/depfix.d.ts +13 -0
  6. package/dist/depfix.d.ts.map +1 -0
  7. package/dist/depfix.js +84 -0
  8. package/dist/display.d.ts +38 -0
  9. package/dist/display.d.ts.map +1 -0
  10. package/dist/display.js +890 -0
  11. package/dist/index.d.ts +50 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +504 -0
  14. package/dist/parser.d.ts +10 -0
  15. package/dist/parser.d.ts.map +1 -0
  16. package/dist/parser.js +688 -0
  17. package/dist/phonological.d.ts +41 -0
  18. package/dist/phonological.d.ts.map +1 -0
  19. package/dist/phonological.js +788 -0
  20. package/dist/rhyme.d.ts +26 -0
  21. package/dist/rhyme.d.ts.map +1 -0
  22. package/dist/rhyme.js +340 -0
  23. package/dist/scandroid.d.ts +17 -0
  24. package/dist/scandroid.d.ts.map +1 -0
  25. package/dist/scandroid.js +435 -0
  26. package/dist/scansion.d.ts +37 -0
  27. package/dist/scansion.d.ts.map +1 -0
  28. package/dist/scansion.js +1007 -0
  29. package/dist/scansion_debug.js +586 -0
  30. package/dist/stress.d.ts +32 -0
  31. package/dist/stress.d.ts.map +1 -0
  32. package/dist/stress.js +1372 -0
  33. package/dist/tagfix.d.ts +6 -0
  34. package/dist/tagfix.d.ts.map +1 -0
  35. package/dist/tagfix.js +101 -0
  36. package/dist/types.d.ts +173 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +4 -0
  39. package/package.json +62 -0
  40. package/src/depfix.ts +88 -0
  41. package/src/display.ts +954 -0
  42. package/src/index.ts +541 -0
  43. package/src/parser.ts +837 -0
  44. package/src/phonological.ts +849 -0
  45. package/src/rhyme.ts +328 -0
  46. package/src/scandroid.ts +434 -0
  47. package/src/scansion.ts +1053 -0
  48. package/src/stress.ts +1381 -0
  49. package/src/tagfix.ts +104 -0
  50. package/src/types.ts +230 -0
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Correct a sentence's tags in place-safe fashion (returns a new array).
3
+ * `tokens` and `tags` are the en-pos outputs, index-aligned.
4
+ */
5
+ export declare function correctTags(tokens: string[], tags: string[]): string[];
6
+ //# sourceMappingURL=tagfix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tagfix.d.ts","sourceRoot":"","sources":["../src/tagfix.ts"],"names":[],"mappings":"AAwCA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CA2DtE"}
package/dist/tagfix.js ADDED
@@ -0,0 +1,101 @@
1
+ // tagfix.ts — Pre-parse POS-tag correction layer.
2
+ //
3
+ // FinNLP's en-pos tagger is structurally sound but carries a small tail of
4
+ // SYSTEMATIC tag errors that matter enormously for verse analysis, because a
5
+ // wrong tag flips a word's content/function status (→ its stress tier) and
6
+ // derails the en-parse dependency tree built from the tags. This pass runs
7
+ // BETWEEN en-pos and en-parse (see parseDocument in parser.ts), so corrected
8
+ // tags repair both the tagging AND the resulting dependency structure — a
9
+ // post-hoc fix of the parse could never do that.
10
+ //
11
+ // Every rule below targets an error class actually observed in this repo's
12
+ // trials; rules are deliberately narrow (anti-gaming: each must be justified
13
+ // by the error it fixes, not by benchmark deltas).
14
+ /** Zero-derived irregular past participles that en-pos tags NN/VBP after a
15
+ * have-auxiliary ("had quit", "has put", "have read"). Only forms whose
16
+ * participle is identical to the base/noun spelling — the -en/-ed forms tag
17
+ * fine on their own. */
18
+ const ZERO_PARTICIPLES = new Set([
19
+ 'quit', 'put', 'set', 'cut', 'hit', 'let', 'shut', 'cast', 'cost', 'hurt',
20
+ 'burst', 'split', 'spread', 'bet', 'wed', 'read', 'rid', 'shed', 'thrust',
21
+ 'slit', 'bid', 'broadcast', 'upset', 'sunburst',
22
+ ]);
23
+ const HAVE_FORMS = new Set(['have', 'has', 'had', 'having', "'ve", "'d"]);
24
+ /** Archaic / Early-Modern-English forms en-pos has no lexicon entries for —
25
+ * ubiquitous in the verse this toolkit exists to scan. */
26
+ const ARCHAIC_TAGS = {
27
+ thou: 'PRP', thee: 'PRP', ye: 'PRP',
28
+ thy: 'PRP$', thine: 'PRP$',
29
+ art: 'VBP', wert: 'VBD', wast: 'VBD',
30
+ doth: 'VBZ', hath: 'VBZ', dost: 'VBZ', hast: 'VBZ', saith: 'VBZ',
31
+ didst: 'VBD', hadst: 'VBD', wouldst: 'MD', couldst: 'MD', shouldst: 'MD',
32
+ shalt: 'MD', wilt: 'MD', canst: 'MD', mayst: 'MD', 'mightst': 'MD',
33
+ wherefore: 'WRB', whither: 'WRB', whence: 'WRB',
34
+ hither: 'RB', thither: 'RB', yon: 'JJ', yonder: 'RB',
35
+ ere: 'IN', oft: 'RB', anon: 'RB',
36
+ };
37
+ /**
38
+ * Correct a sentence's tags in place-safe fashion (returns a new array).
39
+ * `tokens` and `tags` are the en-pos outputs, index-aligned.
40
+ */
41
+ export function correctTags(tokens, tags) {
42
+ const out = tags.slice();
43
+ for (let i = 0; i < tokens.length; i++) {
44
+ const w = tokens[i].toLowerCase();
45
+ // 1. The pronoun "I". en-norm lowercases sentence-initial "I" → "i",
46
+ // which en-pos then reads as a foreign word / letter name (FW).
47
+ if (w === 'i' && out[i] === 'FW')
48
+ out[i] = 'PRP';
49
+ // 2. Archaic forms (thou/thy/doth/shalt/wherefore…): en-pos guesses
50
+ // NN/JJ/FW for these, wrecking both stress class and the parse.
51
+ // Guard "art": only when a pronoun precedes ("thou art"), since the
52
+ // noun reading ("the art of…") is the modern default.
53
+ const archaic = ARCHAIC_TAGS[w];
54
+ if (archaic && !/^(NNP|NNPS)$/.test(out[i])) {
55
+ if (w === 'art') {
56
+ const prev = i > 0 ? tokens[i - 1].toLowerCase() : '';
57
+ if (prev === 'thou' || prev === 'ye' || prev === 'you')
58
+ out[i] = 'VBP';
59
+ }
60
+ else {
61
+ out[i] = archaic;
62
+ }
63
+ }
64
+ // 3. Perfect-tense zero participles: have-form + ("quit"/"put"/"read"…)
65
+ // tagged as NN/VBP/VBD → VBN, so en-parse builds the verb chain
66
+ // instead of treating the participle as a direct-object noun
67
+ // ("I had quit the programming paradigm"). An intervening adverb
68
+ // ("had just quit") is allowed.
69
+ if (ZERO_PARTICIPLES.has(w) && /^(NN|NNS|VBP|VBD|VB)$/.test(out[i])) {
70
+ const prev1 = i > 0 ? tokens[i - 1].toLowerCase() : '';
71
+ const prev2 = i > 1 ? tokens[i - 2].toLowerCase() : '';
72
+ const prev1IsAdv = i > 0 && /^RB/.test(out[i - 1]);
73
+ if (HAVE_FORMS.has(prev1) || (prev1IsAdv && HAVE_FORMS.has(prev2))) {
74
+ out[i] = 'VBN';
75
+ }
76
+ }
77
+ // 4. Impossible gerunds: a VBG tag on a token that does not end in
78
+ // -ing/-in' cannot be a gerund/present participle — it is an en-pos
79
+ // lexicon glitch. The right tag depends on context: before a noun it
80
+ // is a noun modifier ("wisdom"/VBG teeth → NN); after a subject
81
+ // pronoun it is a finite verb ("as they bicycle/VBG through" → VBP,
82
+ // which keeps "through" a phrasal particle in the parse). With no
83
+ // deciding context, leave the tag alone (en-parse treats VBG
84
+ // verb-ishly, the safer default).
85
+ if (out[i] === 'VBG' && !/in[g'’]?$/.test(w)) {
86
+ const prevTag = i > 0 ? out[i - 1] : '';
87
+ const nextTag = i + 1 < tokens.length ? out[i + 1] : '';
88
+ if (/^NNS?$/.test(nextTag))
89
+ out[i] = 'NN';
90
+ else if (prevTag === 'PRP')
91
+ out[i] = 'VBP';
92
+ }
93
+ // 5. Vocative "O" ("O wild West Wind"): en-pos gives NNP/JJ; it is an
94
+ // interjection (and must not become a content word with a beat by
95
+ // default). Only the bare capital O — "o'er" etc. are handled by the
96
+ // aphaeresis lexicon in stress.ts.
97
+ if (tokens[i] === 'O' && i + 1 < tokens.length && out[i] !== 'UH')
98
+ out[i] = 'UH';
99
+ }
100
+ return out;
101
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Stress levels in McAleese’s relative system, in ascending order
3
+ * `x < w < n < m < s`. `x` is the "Zero Provision" tier (Hayes; Lerdahl &
4
+ * Jackendoff 1983): a level *weaker than a stressless overt syllable*, borne by
5
+ * maximally-reduced clitics (the/a/of/and…) and unfilled positions.
6
+ */
7
+ export type StressLevel = 'x' | 'w' | 'n' | 'm' | 's';
8
+ /** A single syllable within a word, with phonetic and stress information */
9
+ export interface Syllable {
10
+ text: string;
11
+ phones: string;
12
+ weight?: 'H' | 'L';
13
+ stress: number;
14
+ lexicalStress?: number;
15
+ relativeStress?: StressLevel;
16
+ extrametrical?: 'morphological' | 'light_noun' | 'derivational';
17
+ }
18
+ /** Represents a word in the dependency‑parse graph */
19
+ export interface ClsWord {
20
+ index: number;
21
+ lexicalClass: string;
22
+ lexicalDetails: string;
23
+ lexicalPlural: boolean;
24
+ position: string;
25
+ word: string;
26
+ absoluteIndex: number;
27
+ isContent: boolean;
28
+ syllables: Syllable[];
29
+ morphSuffix?: string;
30
+ phraseStress: number;
31
+ dependency?: ClsDependency;
32
+ node?: ClsNode;
33
+ }
34
+ /** A typed dependency edge between two words (as in Antelope’s XML) */
35
+ export interface ClsDependency {
36
+ index: number;
37
+ governorIndex: number;
38
+ dependentIndex: number;
39
+ dependentType: string;
40
+ governorName: string;
41
+ dependentName: string;
42
+ governor: ClsWord;
43
+ dependent: ClsWord;
44
+ }
45
+ /** A constituent node in the parse tree (mirrors Figure 14) */
46
+ export interface ClsNode {
47
+ index: string;
48
+ nodeName: string;
49
+ parent: ClsNode | null;
50
+ contains: (ClsNode | ClsWord)[];
51
+ }
52
+ /** A single parsed sentence */
53
+ export interface ClsSentence {
54
+ index: number;
55
+ nodes: ClsNode | null;
56
+ dependencies: ClsDependency[];
57
+ words: ClsWord[];
58
+ xml: string;
59
+ }
60
+ /** Top‑level document containing parsed sentences */
61
+ export interface ClsDocument {
62
+ sentences: ClsSentence[];
63
+ xml: string;
64
+ }
65
+ /** A clitic group: one content word plus its attached function words */
66
+ export interface CliticGroup {
67
+ tokens: ClsWord[];
68
+ }
69
+ /** A phonological phrase: one or more clitic groups related syntactically */
70
+ export interface PhonologicalPhrase {
71
+ cliticGroups: CliticGroup[];
72
+ }
73
+ /** An intonational unit: one or more phonological phrases bounded by punctuation/line‑end */
74
+ export interface IntonationalUnit {
75
+ phonologicalPhrases: PhonologicalPhrase[];
76
+ }
77
+ /** Recognised metre names */
78
+ export type MetreName = 'iambic' | 'trochaic' | 'spondaic' | 'pyrrhic' | 'anapestic' | 'dactylic' | 'amphibrachic' | 'bacchic';
79
+ /** Definition of a metre candidate: its foot shape and syllable count per foot */
80
+ export interface MetreCandidate {
81
+ name: MetreName;
82
+ foot: string;
83
+ syllableCount: number;
84
+ }
85
+ /** A key‑stress pattern extracted from one unit of the phonological hierarchy */
86
+ export interface KeyStress {
87
+ unitType: 'PW' | 'CP' | 'PP' | 'IU';
88
+ pattern: string;
89
+ weight: number;
90
+ positions: number[];
91
+ }
92
+ /** Result of scansion for a single line */
93
+ export interface ScansionResult {
94
+ meter: MetreName | 'free verse';
95
+ scansion: string;
96
+ certainty: number;
97
+ weightScore: number;
98
+ maxPossibleWeight: number;
99
+ algorithm?: string;
100
+ }
101
+ /** One candidate meter's overall fit score (internal composite, not a probability) */
102
+ export interface MeterScore {
103
+ meter: MetreName | 'free verse';
104
+ score: number;
105
+ }
106
+ /** Detailed phonological scansion for a single line */
107
+ export interface PhonologicalScansionDetail {
108
+ all: string;
109
+ keyStresses: string;
110
+ meter: string;
111
+ meterName: MetreName | 'free verse';
112
+ footCount: number;
113
+ summary: string;
114
+ scansion: string;
115
+ certainty: number;
116
+ weightScore: number;
117
+ maxPossibleWeight: number;
118
+ ranking?: MeterScore[];
119
+ consensusMeter?: string;
120
+ standaloneMeter?: string;
121
+ rhythmNote?: string;
122
+ rhyme?: {
123
+ endWord: string;
124
+ letter: string;
125
+ type?: string;
126
+ matchedLine?: number;
127
+ };
128
+ formNote?: string;
129
+ }
130
+ /** Complete per‑line result from the pipeline */
131
+ export interface LineResult {
132
+ sentence: ClsSentence;
133
+ phonologicalHierarchy: IntonationalUnit[];
134
+ keyStresses: KeyStress[];
135
+ phonologicalScansion: PhonologicalScansionDetail;
136
+ scandroidCorral?: ScansionResult;
137
+ scandroidMaximise?: ScansionResult;
138
+ }
139
+ /** Per-syllable information for display rendering */
140
+ export interface SyllableDisplayEntry {
141
+ wordText: string;
142
+ sylText: string;
143
+ sylIndex: number;
144
+ sylCount: number;
145
+ relativeStress: StressLevel;
146
+ globalIndex: number;
147
+ wordIndex: number;
148
+ }
149
+ /** A single foot in the display, mapping scansion pattern to its syllables */
150
+ export interface FootDisplayEntry {
151
+ footIndex: number;
152
+ footPattern: string;
153
+ syllables: SyllableDisplayEntry[];
154
+ }
155
+ /** All formatted display representations for a single line */
156
+ export interface FormattedDisplay {
157
+ originalText: string;
158
+ diacriticText: string;
159
+ uppercaseText: string;
160
+ ansiText: string;
161
+ sylColoredText: string;
162
+ footAligned: string;
163
+ syllableBreakdown: string;
164
+ }
165
+ /** Options controlling display formatting */
166
+ export interface DisplayOptions {
167
+ ansi: boolean;
168
+ diacritics: boolean;
169
+ footAligned: boolean;
170
+ verbose: boolean;
171
+ phrasal: boolean;
172
+ }
173
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ // types.ts — Complete type declarations for Calliope_TS pipeline
2
+ // Reflects McAleese’s class diagrams (Figure 14) and additional phonological /
3
+ // metrical types required by stress, phonological hierarchy, scansion, and Scandroid modules.
4
+ export {};
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "calliope-ts",
3
+ "version": "0.0.1",
4
+ "description": "A phonological poetry scansion framework in TypeScript, attempting to retrace and extend the works of G. McAleese (Calliope, 2008) and the makers of Scandroid (2005)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "calliope-ts": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "ts-node --esm src/index.ts",
14
+ "test": "vitest run",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "nlp",
19
+ "scansion",
20
+ "meter",
21
+ "metre",
22
+ "analysis",
23
+ "poetics",
24
+ "verse",
25
+ "poetic",
26
+ "literature",
27
+ "prosody",
28
+ "prosodic",
29
+ "scandroid",
30
+ "typescript",
31
+ "javascript",
32
+ "syntax",
33
+ "morphology",
34
+ "calliope",
35
+ "calliope_ts",
36
+ "poetry",
37
+ "silveragepoets",
38
+ "parser",
39
+ "ud"
40
+ ],
41
+ "author": "Aleksey Calvin Tsukanov",
42
+ "license": "apache-2.0",
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "dependencies": {
47
+ "depedits": "^0.0.1",
48
+ "en-norm": "0.0.2",
49
+ "en-parse": "^1.1.0",
50
+ "en-pos": "^1.0.13",
51
+ "finnlp": "^1.0.0",
52
+ "lexed": "^1.0.10",
53
+ "nounsing-pro": "0.0.8"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^22.14.0",
57
+ "@types/prompts": "^2.4.9",
58
+ "ts-node": "^10.9.0",
59
+ "typescript": "~5.8.2",
60
+ "vitest": "^2.1.0"
61
+ }
62
+ }
package/src/depfix.ts ADDED
@@ -0,0 +1,88 @@
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
+
16
+ import { createRequire } from 'module';
17
+
18
+ interface FinDepNode {
19
+ label: string; // dependency label, e.g. "NSUBJ", "ROOT"
20
+ type: string; // phrase type, e.g. "NP", "VP"
21
+ parent: number; // 0-based index of governor token; -1 for root
22
+ }
23
+
24
+ // Observed failure (probe: "I had quit the programming paradigm"): en-parse
25
+ // attaches BOTH nouns of a noun compound to the verb as parallel objects
26
+ // ("programming ←DOBJ← quit", "paradigm ←DOBJ← quit"), and leaves the
27
+ // determiner dangling on the first noun as generic DEP. The repairs:
28
+ // 1. Two adjacent common nouns sharing a governor with the same object
29
+ // relation → the first is a compound modifier (AMOD) of the second.
30
+ // 2. A determiner left as DEP on a noun that has become a modifier →
31
+ // re-attach it as DET to that noun's head (the true NP head).
32
+ const CALLIOPE_DEP_FIXES = [
33
+ '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',
34
+ 'xpos=/DT/&func=/DEP|EXT/;xpos=/NNS?/&func=/AMOD/;xpos=/NNS?.*/\t#2>#1;#3>#2\t#3>#1;#1:func=DET',
35
+ ].join('\n');
36
+
37
+ let engine: { process(conllu: string): string } | null | undefined;
38
+
39
+ function loadEngine(): typeof engine {
40
+ if (engine !== undefined) return engine;
41
+ try {
42
+ // This package compiles to ESM, where bare `require` does not exist, and
43
+ // the parse path is synchronous, so dynamic import() is not an option:
44
+ // createRequire gives a sync loader, and since `depedits` is itself
45
+ // ESM-only this resolves via Node's require(esm) (≥20.17 / ≥22.12). On
46
+ // older runtimes it throws and the repair pass degrades to a no-op (the
47
+ // unrepaired parse is still a parse).
48
+ const req = createRequire(import.meta.url);
49
+ const { DepEditEngine } = req('depedits');
50
+ const e = new DepEditEngine();
51
+ e.loadIniString(CALLIOPE_DEP_FIXES);
52
+ engine = e;
53
+ } catch {
54
+ engine = null;
55
+ }
56
+ return engine;
57
+ }
58
+
59
+ /**
60
+ * Repair systematic en-parse attachment errors. Returns a new deps array
61
+ * (same shape as en-parse's `toArray` output); on any failure returns the
62
+ * input unchanged.
63
+ */
64
+ export function applyDepFixes(tokens: string[], tags: string[], deps: FinDepNode[]): FinDepNode[] {
65
+ const e = loadEngine();
66
+ if (!e || tokens.length === 0 || deps.length !== tokens.length) return deps;
67
+ try {
68
+ const conllu = tokens.map((tok, i) => {
69
+ const head = deps[i].parent >= 0 ? deps[i].parent + 1 : 0;
70
+ const safe = tok.replace(/\s/g, '_') || '_';
71
+ return `${i + 1}\t${safe}\t${safe}\t_\t${tags[i] || '_'}\t_\t${head}\t${deps[i].label || 'DEP'}\t_\t_`;
72
+ }).join('\n') + '\n\n';
73
+ const out = e.process(conllu);
74
+ const fixed: FinDepNode[] = deps.map(d => ({ ...d }));
75
+ for (const row of out.split('\n')) {
76
+ const cols = row.split('\t');
77
+ if (cols.length < 10) continue;
78
+ const idx = parseInt(cols[0], 10) - 1;
79
+ if (!(idx >= 0 && idx < fixed.length)) continue;
80
+ const head = parseInt(cols[6], 10);
81
+ fixed[idx].parent = Number.isFinite(head) ? head - 1 : fixed[idx].parent;
82
+ if (cols[7] && cols[7] !== '_') fixed[idx].label = cols[7];
83
+ }
84
+ return fixed;
85
+ } catch {
86
+ return deps;
87
+ }
88
+ }