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
package/src/tagfix.ts ADDED
@@ -0,0 +1,104 @@
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
+
15
+ /** Zero-derived irregular past participles that en-pos tags NN/VBP after a
16
+ * have-auxiliary ("had quit", "has put", "have read"). Only forms whose
17
+ * participle is identical to the base/noun spelling — the -en/-ed forms tag
18
+ * fine on their own. */
19
+ const ZERO_PARTICIPLES = new Set([
20
+ 'quit', 'put', 'set', 'cut', 'hit', 'let', 'shut', 'cast', 'cost', 'hurt',
21
+ 'burst', 'split', 'spread', 'bet', 'wed', 'read', 'rid', 'shed', 'thrust',
22
+ 'slit', 'bid', 'broadcast', 'upset', 'sunburst',
23
+ ]);
24
+
25
+ const HAVE_FORMS = new Set(['have', 'has', 'had', 'having', "'ve", "'d"]);
26
+
27
+ /** Archaic / Early-Modern-English forms en-pos has no lexicon entries for —
28
+ * ubiquitous in the verse this toolkit exists to scan. */
29
+ const ARCHAIC_TAGS: Record<string, string> = {
30
+ thou: 'PRP', thee: 'PRP', ye: 'PRP',
31
+ thy: 'PRP$', thine: 'PRP$',
32
+ art: 'VBP', wert: 'VBD', wast: 'VBD',
33
+ doth: 'VBZ', hath: 'VBZ', dost: 'VBZ', hast: 'VBZ', saith: 'VBZ',
34
+ didst: 'VBD', hadst: 'VBD', wouldst: 'MD', couldst: 'MD', shouldst: 'MD',
35
+ shalt: 'MD', wilt: 'MD', canst: 'MD', mayst: 'MD', 'mightst': 'MD',
36
+ wherefore: 'WRB', whither: 'WRB', whence: 'WRB',
37
+ hither: 'RB', thither: 'RB', yon: 'JJ', yonder: 'RB',
38
+ ere: 'IN', oft: 'RB', anon: 'RB',
39
+ };
40
+
41
+ /**
42
+ * Correct a sentence's tags in place-safe fashion (returns a new array).
43
+ * `tokens` and `tags` are the en-pos outputs, index-aligned.
44
+ */
45
+ export function correctTags(tokens: string[], tags: string[]): string[] {
46
+ const out = tags.slice();
47
+ for (let i = 0; i < tokens.length; i++) {
48
+ const w = tokens[i].toLowerCase();
49
+
50
+ // 1. The pronoun "I". en-norm lowercases sentence-initial "I" → "i",
51
+ // which en-pos then reads as a foreign word / letter name (FW).
52
+ if (w === 'i' && out[i] === 'FW') out[i] = 'PRP';
53
+
54
+ // 2. Archaic forms (thou/thy/doth/shalt/wherefore…): en-pos guesses
55
+ // NN/JJ/FW for these, wrecking both stress class and the parse.
56
+ // Guard "art": only when a pronoun precedes ("thou art"), since the
57
+ // noun reading ("the art of…") is the modern default.
58
+ const archaic = ARCHAIC_TAGS[w];
59
+ if (archaic && !/^(NNP|NNPS)$/.test(out[i])) {
60
+ if (w === 'art') {
61
+ const prev = i > 0 ? tokens[i - 1].toLowerCase() : '';
62
+ if (prev === 'thou' || prev === 'ye' || prev === 'you') out[i] = 'VBP';
63
+ } else {
64
+ out[i] = archaic;
65
+ }
66
+ }
67
+
68
+ // 3. Perfect-tense zero participles: have-form + ("quit"/"put"/"read"…)
69
+ // tagged as NN/VBP/VBD → VBN, so en-parse builds the verb chain
70
+ // instead of treating the participle as a direct-object noun
71
+ // ("I had quit the programming paradigm"). An intervening adverb
72
+ // ("had just quit") is allowed.
73
+ if (ZERO_PARTICIPLES.has(w) && /^(NN|NNS|VBP|VBD|VB)$/.test(out[i])) {
74
+ const prev1 = i > 0 ? tokens[i - 1].toLowerCase() : '';
75
+ const prev2 = i > 1 ? tokens[i - 2].toLowerCase() : '';
76
+ const prev1IsAdv = i > 0 && /^RB/.test(out[i - 1]);
77
+ if (HAVE_FORMS.has(prev1) || (prev1IsAdv && HAVE_FORMS.has(prev2))) {
78
+ out[i] = 'VBN';
79
+ }
80
+ }
81
+
82
+ // 4. Impossible gerunds: a VBG tag on a token that does not end in
83
+ // -ing/-in' cannot be a gerund/present participle — it is an en-pos
84
+ // lexicon glitch. The right tag depends on context: before a noun it
85
+ // is a noun modifier ("wisdom"/VBG teeth → NN); after a subject
86
+ // pronoun it is a finite verb ("as they bicycle/VBG through" → VBP,
87
+ // which keeps "through" a phrasal particle in the parse). With no
88
+ // deciding context, leave the tag alone (en-parse treats VBG
89
+ // verb-ishly, the safer default).
90
+ if (out[i] === 'VBG' && !/in[g'’]?$/.test(w)) {
91
+ const prevTag = i > 0 ? out[i - 1] : '';
92
+ const nextTag = i + 1 < tokens.length ? out[i + 1] : '';
93
+ if (/^NNS?$/.test(nextTag)) out[i] = 'NN';
94
+ else if (prevTag === 'PRP') out[i] = 'VBP';
95
+ }
96
+
97
+ // 5. Vocative "O" ("O wild West Wind"): en-pos gives NNP/JJ; it is an
98
+ // interjection (and must not become a content word with a beat by
99
+ // default). Only the bare capital O — "o'er" etc. are handled by the
100
+ // aphaeresis lexicon in stress.ts.
101
+ if (tokens[i] === 'O' && i + 1 < tokens.length && out[i] !== 'UH') out[i] = 'UH';
102
+ }
103
+ return out;
104
+ }
package/src/types.ts ADDED
@@ -0,0 +1,230 @@
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
+
5
+ /**
6
+ * Stress levels in McAleese’s relative system, in ascending order
7
+ * `x < w < n < m < s`. `x` is the "Zero Provision" tier (Hayes; Lerdahl &
8
+ * Jackendoff 1983): a level *weaker than a stressless overt syllable*, borne by
9
+ * maximally-reduced clitics (the/a/of/and…) and unfilled positions.
10
+ */
11
+ export type StressLevel = 'x' | 'w' | 'n' | 'm' | 's';
12
+
13
+ /** A single syllable within a word, with phonetic and stress information */
14
+ export interface Syllable {
15
+ text: string; // the orthographic syllable (or entire word if unsplit)
16
+ phones: string; // CMU phonetic transcription (per-syllable ARPAbet)
17
+ weight?: 'H' | 'L'; // syllable weight (Heavy/Light)
18
+ stress: number; // numeric stress from CMU; modified by compound + nuclear rules
19
+ lexicalStress?: number; // stress before nuclear rule; used for relative mapping / meter detection
20
+ relativeStress?: StressLevel; // assigned after phrase‑stress rules and relative adjustment
21
+ extrametrical?: 'morphological' | 'light_noun' | 'derivational'; // extrametricality classification
22
+ }
23
+
24
+ /** Represents a word in the dependency‑parse graph */
25
+ export interface ClsWord {
26
+ index: number; // 1‑based index in the sentence (matching Antelope’s XML)
27
+ lexicalClass: string; // POS tag, e.g., 'VBD', 'NN', 'PRP'
28
+ lexicalDetails: string; // additional morphological info (empty if none)
29
+ lexicalPlural: boolean; // true if plural
30
+ position: string; // textual position (not always used)
31
+ word: string; // the surface form of the word
32
+ absoluteIndex: number; // 0‑based index among all words in the text
33
+ isContent: boolean;
34
+ // extended properties
35
+ syllables: Syllable[]; // array of syllables for the word
36
+ morphSuffix?: string; // productive suffix split off for OOV stress (e.g. 'est'); guides display syllabification
37
+ phraseStress: number; // numeric phrase‑level stress after Nuclear Stress Rule
38
+ dependency?: ClsDependency; // back‑reference to dependency edge (if any)
39
+ node?: ClsNode; // back‑reference to the constituent node (if any)
40
+ }
41
+
42
+ /** A typed dependency edge between two words (as in Antelope’s XML) */
43
+ export interface ClsDependency {
44
+ index: number; // 1‑based dependency index
45
+ governorIndex: number; // word index of the governor (head)
46
+ dependentIndex: number; // word index of the dependent
47
+ dependentType: string; // type label: 'aux','nsubj','dobj','prep','det','poss','possessive','pobj',…
48
+ governorName: string; // surface form of the governor
49
+ dependentName: string; // surface form of the dependent
50
+ governor: ClsWord; // reference to the governor word object
51
+ dependent: ClsWord; // reference to the dependent word object
52
+ }
53
+
54
+ /** A constituent node in the parse tree (mirrors Figure 14) */
55
+ export interface ClsNode {
56
+ index: string; // node identifier, e.g., '1', '1.2'
57
+ nodeName: string; // label (e.g., 'SQ', 'NP', 'VP', 'PP', or a word index)
58
+ parent: ClsNode | null; // parent node, null for root
59
+ contains: (ClsNode | ClsWord)[]; // children: either sub‑nodes or words
60
+ }
61
+
62
+ /** A single parsed sentence */
63
+ export interface ClsSentence {
64
+ index: number; // sentence number (1‑based)
65
+ nodes: ClsNode | null; // root node of the parse tree
66
+ dependencies: ClsDependency[]; // all dependency edges in this sentence
67
+ words: ClsWord[]; // word objects in order
68
+ xml: string; // serialised XML representation (optional)
69
+ }
70
+
71
+ /** Top‑level document containing parsed sentences */
72
+ export interface ClsDocument {
73
+ sentences: ClsSentence[]; // list of parsed sentences
74
+ xml: string; // full XML document (optional)
75
+ }
76
+
77
+ // ─── Phonological Hierarchy (CP, PP, IU) ────────────────────────────
78
+
79
+ /** A clitic group: one content word plus its attached function words */
80
+ export interface CliticGroup {
81
+ tokens: ClsWord[];
82
+ }
83
+
84
+ /** A phonological phrase: one or more clitic groups related syntactically */
85
+ export interface PhonologicalPhrase {
86
+ cliticGroups: CliticGroup[];
87
+ }
88
+
89
+ /** An intonational unit: one or more phonological phrases bounded by punctuation/line‑end */
90
+ export interface IntonationalUnit {
91
+ phonologicalPhrases: PhonologicalPhrase[];
92
+ }
93
+
94
+ // ─── Metre‑ and scansion‑related types ─────────────────────────────
95
+
96
+ /** Recognised metre names */
97
+ export type MetreName =
98
+ | 'iambic'
99
+ | 'trochaic'
100
+ | 'spondaic'
101
+ | 'pyrrhic'
102
+ | 'anapestic'
103
+ | 'dactylic'
104
+ | 'amphibrachic'
105
+ | 'bacchic';
106
+
107
+ /** Definition of a metre candidate: its foot shape and syllable count per foot */
108
+ export interface MetreCandidate {
109
+ name: MetreName;
110
+ foot: string; // e.g., 'ws', 'sw', 'wws'
111
+ syllableCount: number; // number of syllables in one foot (2 or 3)
112
+ }
113
+
114
+ /** A key‑stress pattern extracted from one unit of the phonological hierarchy */
115
+ export interface KeyStress {
116
+ unitType: 'PW' | 'CP' | 'PP' | 'IU'; // type of prosodic unit
117
+ pattern: string; // stress pattern, e.g., 'ws', 'sw', 'wsw'
118
+ weight: number; // importance weight (1–3) as per McAleese’s scoring
119
+ positions: number[]; // global syllable indices involved in this key stress
120
+
121
+ }
122
+
123
+ /** Result of scansion for a single line */
124
+ export interface ScansionResult {
125
+ meter: MetreName | 'free verse'; // identified metre, or free verse if below threshold
126
+ scansion: string; // the foot‑delimited scansion string, e.g., "ws|ws|ws|ws|ws"
127
+ certainty: number; // percentage of maximum possible weighted score
128
+ weightScore: number; // actual accumulated weight
129
+ maxPossibleWeight: number; // theoretical maximum weight for the line
130
+ algorithm?: string; // optional, to distinguish Phonological vs Scandroid results
131
+ }
132
+
133
+ /** One candidate meter's overall fit score (internal composite, not a probability) */
134
+ export interface MeterScore {
135
+ meter: MetreName | 'free verse';
136
+ score: number; // scoreMeters' finalScore — a relative fit score, higher = better
137
+ }
138
+
139
+ /** Detailed phonological scansion for a single line */
140
+ export interface PhonologicalScansionDetail {
141
+ all: string; // hierarchical string, e.g. "<{[nm/ws\n]}mn/sw\]m]}>"
142
+ keyStresses: string; // key‑stress string, e.g. "<{[xx/ws\n]}xx/sw\]m]}>"
143
+ meter: string; // e.g. "iambic pentameter"
144
+ meterName: MetreName | 'free verse'; // enum value for tests
145
+ footCount: number; // e.g. 5
146
+ summary: string; // e.g. "IU=1 PP=1 PW=1" counts per metre direction
147
+ scansion: string; // foot‑separated, e.g. "xx/ws|nx/xs/wm"
148
+ certainty: number; // 0‑100
149
+ weightScore: number;
150
+ maxPossibleWeight: number;
151
+ ranking?: MeterScore[]; // candidate meters ranked by fit score (best first); optional
152
+ consensusMeter?: string; // set by applyStanzaConsensus when this line's standalone meter
153
+ // diverges from, yet closely fits, the stanza's dominant meter.
154
+ // The continuity-rename pass (index.ts) then normally CONVERTS
155
+ // this annotation: the line adopts the dominant meter as its BASE
156
+ // reading (meter/scansion/footCount/certainty re-fitted under it),
157
+ // consensusMeter is cleared, and standaloneMeter records the
158
+ // numerically-best standalone reading. consensusMeter survives
159
+ // only when the forced re-fit fails.
160
+ standaloneMeter?: string; // the line's numerically best standalone meter (e.g. "dactylic
161
+ // tetrameter"), kept when stanza/poem continuity renamed the base
162
+ // reading to the dominant meter.
163
+ rhythmNote?: string; // non-classical rhythm classification (Russian-metrics taxonomy):
164
+ // "4-ictus dolnik", "3-ictus taktovik", "4-beat accentual",
165
+ // "alternating 4·3-ictus accentual" — set at stanza level when
166
+ // beat counts are regular but syllable counts vary and no
167
+ // classical meter dominates; or per-line to refine a free-verse
168
+ // reading. NOT a form verdict: "ballad" etc. belong to the
169
+ // rhyme-aware form layer (rhyme.ts).
170
+ rhyme?: { // end-rhyme annotation (rhyme.ts), LYRICAL-compatible typology
171
+ endWord: string;
172
+ letter: string; // scheme letter 'A'/'B'/…, or '·' when unrhymed
173
+ type?: string; // perfect/rich/family/assonant/consonant/augmented/
174
+ // diminished/wrenched/eye/identical
175
+ matchedLine?: number; // 0-based stanza line this one rhymes with
176
+ };
177
+ formNote?: string; // stanza/poem FORM verdict (rhyme-aware): "ballad stanza
178
+ // (ABCB, 4·3)", "blank verse", "Shakespearean Sonnet",
179
+ // "terza rima (ABA BCB CDC…)", "limerick"…
180
+ }
181
+
182
+ /** Complete per‑line result from the pipeline */
183
+ export interface LineResult {
184
+ sentence: ClsSentence; // parsed sentence with words, dependencies, nodes
185
+ phonologicalHierarchy: IntonationalUnit[]; // CP/PP/IU structure
186
+ keyStresses: KeyStress[]; // extracted key patterns with weights
187
+ phonologicalScansion: PhonologicalScansionDetail; // scansion via phonological scoring
188
+ scandroidCorral?: ScansionResult; // optional, scansion via Scandroid's 'Corral the Weird'
189
+ scandroidMaximise?: ScansionResult; // optional, scansion via Scandroid's 'Maximise the Normal'
190
+ }
191
+
192
+ // ─── Display formatting types ──────────────────────────────────────
193
+
194
+ /** Per-syllable information for display rendering */
195
+ export interface SyllableDisplayEntry {
196
+ wordText: string;
197
+ sylText: string; // orthographic text of this syllable
198
+ sylIndex: number; // 0‑based index within the word
199
+ sylCount: number; // total syllables in the word
200
+ relativeStress: StressLevel;
201
+ globalIndex: number;
202
+ wordIndex: number; // 0‑based index among non-punctuation words
203
+ }
204
+
205
+ /** A single foot in the display, mapping scansion pattern to its syllables */
206
+ export interface FootDisplayEntry {
207
+ footIndex: number;
208
+ footPattern: string; // raw foot pattern from scansion, e.g., 'ws', '-ms'
209
+ syllables: SyllableDisplayEntry[];
210
+ }
211
+
212
+ /** All formatted display representations for a single line */
213
+ export interface FormattedDisplay {
214
+ originalText: string;
215
+ diacriticText: string;
216
+ uppercaseText: string;
217
+ ansiText: string;
218
+ sylColoredText: string;
219
+ footAligned: string;
220
+ syllableBreakdown: string;
221
+ }
222
+
223
+ /** Options controlling display formatting */
224
+ export interface DisplayOptions {
225
+ ansi: boolean;
226
+ diacritics: boolean;
227
+ footAligned: boolean;
228
+ verbose: boolean;
229
+ phrasal: boolean;
230
+ }