calliope-ts 0.0.4 → 0.1.0
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 +69 -3
- package/dist/calliope/bracketing.d.ts.map +1 -1
- package/dist/calliope/bracketing.js +59 -5
- package/dist/calliope/deps.d.ts +5 -1
- package/dist/calliope/deps.d.ts.map +1 -1
- package/dist/calliope/deps.js +53 -3
- package/dist/calliope/engine.d.ts.map +1 -1
- package/dist/calliope/engine.js +6 -1
- package/dist/calliope/relstress.d.ts.map +1 -1
- package/dist/calliope/relstress.js +411 -15
- package/dist/calliope/syntax.d.ts +33 -0
- package/dist/calliope/syntax.d.ts.map +1 -1
- package/dist/calliope/syntax.js +199 -0
- package/dist/data/en_common_words.json +1 -0
- package/dist/display.d.ts +15 -1
- package/dist/display.d.ts.map +1 -1
- package/dist/display.js +112 -10
- package/dist/fabbhalle.d.ts +54 -0
- package/dist/fabbhalle.d.ts.map +1 -0
- package/dist/fabbhalle.js +404 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -33
- package/dist/phonological.d.ts.map +1 -1
- package/dist/phonological.js +7 -1
- package/dist/rewriteEn.d.ts +14 -0
- package/dist/rewriteEn.d.ts.map +1 -0
- package/dist/rewriteEn.js +392 -0
- package/dist/rhyme.d.ts +35 -3
- package/dist/rhyme.d.ts.map +1 -1
- package/dist/rhyme.js +176 -11
- package/dist/russian/accentuator.d.ts +31 -0
- package/dist/russian/accentuator.d.ts.map +1 -0
- package/dist/russian/accentuator.js +546 -0
- package/dist/russian/collocations.d.ts +7 -0
- package/dist/russian/collocations.d.ts.map +1 -0
- package/dist/russian/collocations.js +38 -0
- package/dist/russian/compounds.d.ts +6 -0
- package/dist/russian/compounds.d.ts.map +1 -0
- package/dist/russian/compounds.js +81 -0
- package/dist/russian/download.d.ts +23 -0
- package/dist/russian/download.d.ts.map +1 -0
- package/dist/russian/download.js +117 -0
- package/dist/russian/engine.d.ts +6 -0
- package/dist/russian/engine.d.ts.map +1 -0
- package/dist/russian/engine.js +1424 -0
- package/dist/russian/parser.d.ts +8 -0
- package/dist/russian/parser.d.ts.map +1 -0
- package/dist/russian/parser.js +92 -0
- package/dist/russian/paths.d.ts +55 -0
- package/dist/russian/paths.d.ts.map +1 -0
- package/dist/russian/paths.js +114 -0
- package/dist/russian/rewrite.d.ts +6 -0
- package/dist/russian/rewrite.d.ts.map +1 -0
- package/dist/russian/rewrite.js +400 -0
- package/dist/russian/rhyme.d.ts +18 -0
- package/dist/russian/rhyme.d.ts.map +1 -0
- package/dist/russian/rhyme.js +230 -0
- package/dist/russian/syllabifier.d.ts +14 -0
- package/dist/russian/syllabifier.d.ts.map +1 -0
- package/dist/russian/syllabifier.js +475 -0
- package/dist/russian/types.d.ts +94 -0
- package/dist/russian/types.d.ts.map +1 -0
- package/dist/russian/types.js +27 -0
- package/dist/scandroidNative/constants.d.ts +16 -0
- package/dist/scandroidNative/constants.d.ts.map +1 -0
- package/dist/scandroidNative/constants.js +60 -0
- package/dist/scandroidNative/deduceParameters.d.ts +16 -0
- package/dist/scandroidNative/deduceParameters.d.ts.map +1 -0
- package/dist/scandroidNative/deduceParameters.js +69 -0
- package/dist/scandroidNative/dictionary-data.d.ts +2 -0
- package/dist/scandroidNative/dictionary-data.d.ts.map +1 -0
- package/dist/scandroidNative/dictionary-data.js +744 -0
- package/dist/scandroidNative/dictionary.d.ts +12 -0
- package/dist/scandroidNative/dictionary.d.ts.map +1 -0
- package/dist/scandroidNative/dictionary.js +50 -0
- package/dist/scandroidNative/engine.d.ts +41 -0
- package/dist/scandroidNative/engine.d.ts.map +1 -0
- package/dist/scandroidNative/engine.js +119 -0
- package/dist/scandroidNative/machine.d.ts +93 -0
- package/dist/scandroidNative/machine.d.ts.map +1 -0
- package/dist/scandroidNative/machine.js +795 -0
- package/dist/scandroidNative/positioner.d.ts +32 -0
- package/dist/scandroidNative/positioner.d.ts.map +1 -0
- package/dist/scandroidNative/positioner.js +208 -0
- package/dist/scandroidNative/syllabizer.d.ts +3 -0
- package/dist/scandroidNative/syllabizer.d.ts.map +1 -0
- package/dist/scandroidNative/syllabizer.js +215 -0
- package/dist/scandroidNative/utilities.d.ts +37 -0
- package/dist/scandroidNative/utilities.d.ts.map +1 -0
- package/dist/scandroidNative/utilities.js +86 -0
- package/dist/scansion.d.ts.map +1 -1
- package/dist/scansion.js +118 -14
- package/dist/stress.d.ts +1 -0
- package/dist/stress.d.ts.map +1 -1
- package/dist/stress.js +502 -42
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +27 -6
- package/tools/fetch-russian-data.mjs +30 -0
- package/webapp/public/app.js +2418 -0
- package/webapp/public/assets/GithubLogo.png +0 -0
- package/webapp/public/assets/NPM2.png +0 -0
- package/webapp/public/assets/fonts/MurenaBold-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/MurenaItalic-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/MurenaRegular-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/Myra_4F_Caps_Bold-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/SinkinSans-700Bold.otf +0 -0
- package/webapp/public/assets/fonts/butterflies.ttf +0 -0
- package/webapp/public/assets/fonts/midcase_blackline-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/neueral-bold-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/neueral-regular-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/popelka-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/vremenagrotesk-webfont.woff +0 -0
- package/webapp/public/assets/fonts/wicky_javick_bold-webfont.woff2 +0 -0
- package/webapp/public/assets/logos/Logo_Var_10_Celtic_Knot_SVG.svg +116 -0
- package/webapp/public/assets/logos/Logo_Var_11_Graph_SVG.svg +313 -0
- package/webapp/public/assets/logos/Logo_Var_12_Limacon_Knot_SVG.svg +3942 -0
- package/webapp/public/assets/logos/Logo_Var_13_Crescents_SVG.svg +1030 -0
- package/webapp/public/assets/logos/Logo_Var_1_Spiral_SVG.svg +1491 -0
- package/webapp/public/assets/logos/Logo_Var_2_Tunnel_SVG.svg +12 -0
- package/webapp/public/assets/logos/Logo_Var_3_Triskelion_SVG.svg +10 -0
- package/webapp/public/assets/logos/Logo_Var_4_Quinquetra_PNG.png +0 -0
- package/webapp/public/assets/logos/Logo_Var_5_Penrose_Pentagram_SVG.svg +37 -0
- package/webapp/public/assets/logos/Logo_Var_6_Triskelion2_SVG.svg +10 -0
- package/webapp/public/assets/logos/Logo_Var_7_SixfoldSpirals_SVG.svg +20 -0
- package/webapp/public/assets/logos/Logo_Var_8_Radial_Labyrinth_SVG.svg +163 -0
- package/webapp/public/assets/logos/Logo_Var_9_Radial_Labyrinth_2_SVG.svg +106 -0
- package/webapp/public/index.html +316 -0
- package/webapp/public/styles.css +1114 -0
- package/webapp/server.mjs +680 -0
- package/src/caesura.ts +0 -201
- package/src/calliope/boundaries.ts +0 -190
- package/src/calliope/bracketing.ts +0 -390
- package/src/calliope/deps.ts +0 -160
- package/src/calliope/engine.ts +0 -77
- package/src/calliope/feats.ts +0 -46
- package/src/calliope/names.ts +0 -44
- package/src/calliope/postag.ts +0 -253
- package/src/calliope/prosodic.ts +0 -262
- package/src/calliope/relstress.ts +0 -645
- package/src/calliope/stressrules.ts +0 -147
- package/src/calliope/syntax.ts +0 -218
- package/src/calliope/udpos.ts +0 -152
- package/src/calliope_src_contents.md +0 -19049
- package/src/clio/caesura.ts +0 -145
- package/src/clio/depfix.ts +0 -88
- package/src/clio/display.ts +0 -1042
- package/src/clio/engine.ts +0 -38
- package/src/clio/parser.ts +0 -845
- package/src/clio/phonological.ts +0 -849
- package/src/clio/phrasestress.ts +0 -108
- package/src/clio/pipeline.ts +0 -154
- package/src/clio/rhyme.ts +0 -740
- package/src/clio/scandroid.ts +0 -434
- package/src/clio/scansion.ts +0 -1130
- package/src/clio/semantics.ts +0 -134
- package/src/clio/stress.ts +0 -1731
- package/src/clio/tagfix.ts +0 -104
- package/src/depfix.ts +0 -88
- package/src/display.ts +0 -1138
- package/src/engine.ts +0 -22
- package/src/index.ts +0 -690
- package/src/parser.ts +0 -501
- package/src/phonological.ts +0 -917
- package/src/phrasestress.ts +0 -108
- package/src/rhyme.ts +0 -748
- package/src/scandroid.ts +0 -434
- package/src/scansion.ts +0 -1174
- package/src/semantics.ts +0 -134
- package/src/stress.ts +0 -2111
- package/src/tagfix.ts +0 -104
- package/src/types.ts +0 -265
- package/tests/DataForHayesLinesOnly.txt +0 -364
- package/tests/DataForHayesStressSymbolsRevised.txt +0 -728
- package/tests/basic.test.ts +0 -1102
- package/tests/bench-hayes.mjs +0 -72
- package/tests/epg64.meter.train.txt +0 -8139
- package/tests/litlab-sample-2016.txt +0 -1738
- package/tests/prosodic.meter.train.txt +0 -13192
- package/tests/wagner-stress.test.ts +0 -188
- package/tsconfig.json +0 -29
- package/vitest.config.d.ts +0 -2
- package/vitest.config.js +0 -14
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
// accentuator.ts — Russian stress placement.
|
|
2
|
+
// Combines a 3.4M-entry pre-stressed dictionary with a neural MLP (arch=1)
|
|
3
|
+
// for OOV words, plus auxiliary dictionaries for:
|
|
4
|
+
// - morphologically-conditioned ambiguous stress (ambiguous_accents)
|
|
5
|
+
// - multi-stress words (ambiguous_accents2)
|
|
6
|
+
// - ёфикация (yo_words, yo_by_gram)
|
|
7
|
+
// - secondary stress (secondary_stress_dict)
|
|
8
|
+
// - compound word stress (derivation_data)
|
|
9
|
+
//
|
|
10
|
+
// The neural MLP forward pass is implemented in pure TypeScript using
|
|
11
|
+
// Float32Array matrix operations. Weights are loaded from a binary file
|
|
12
|
+
// extracted from the original PyTorch model.
|
|
13
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
14
|
+
import { gunzipSync } from 'node:zlib';
|
|
15
|
+
import { countVowels } from './syllabifier.js';
|
|
16
|
+
import { applyVerbPrefixDerivation } from './compounds.js';
|
|
17
|
+
import { RU_VOWELS } from './types.js';
|
|
18
|
+
import { russianDataFile, requireRussianDataFile } from './paths.js';
|
|
19
|
+
let _data = null;
|
|
20
|
+
function loadJson(filename) {
|
|
21
|
+
const p = requireRussianDataFile(filename);
|
|
22
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
23
|
+
}
|
|
24
|
+
/** Load a JSON data file (used by other modules). */
|
|
25
|
+
export { loadJson };
|
|
26
|
+
function loadAccentsData() {
|
|
27
|
+
if (_data)
|
|
28
|
+
return _data;
|
|
29
|
+
// Load gzipped TSV dictionary
|
|
30
|
+
const tsvPath = russianDataFile('word_accents.tsv.gz');
|
|
31
|
+
const wordAccents = new Map();
|
|
32
|
+
if (tsvPath && existsSync(tsvPath)) {
|
|
33
|
+
const raw = gunzipSync(readFileSync(tsvPath)).toString('utf-8');
|
|
34
|
+
for (const line of raw.split('\n')) {
|
|
35
|
+
if (!line)
|
|
36
|
+
continue;
|
|
37
|
+
const tab = line.indexOf('\t');
|
|
38
|
+
if (tab > 0) {
|
|
39
|
+
wordAccents.set(line.slice(0, tab), Number(line.slice(tab + 1)));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Load JSON dictionaries
|
|
44
|
+
const ambiguousAccents = loadJson('ambiguous_accents.json');
|
|
45
|
+
const ambiguousAccents2 = loadJson('ambiguous_accents2.json');
|
|
46
|
+
const yoWords = loadJson('yo_words.json');
|
|
47
|
+
const yoByGram = loadJson('yo_by_gram.json');
|
|
48
|
+
const secondaryStress = loadJson('secondary_stress.json');
|
|
49
|
+
const rhymedWordsArr = loadJson('rhymed_words.json');
|
|
50
|
+
const rhymingDict = loadJson('rhyming_dict.json');
|
|
51
|
+
const derivationData = loadJson('derivation_data.json');
|
|
52
|
+
const rhymedWords = new Set(rhymedWordsArr.map(p => [p[0], p[1]]));
|
|
53
|
+
_data = {
|
|
54
|
+
wordAccents, ambiguousAccents, ambiguousAccents2,
|
|
55
|
+
yoWords, yoByGram, secondaryStress,
|
|
56
|
+
rhymedWords, rhymingDict, derivationData,
|
|
57
|
+
};
|
|
58
|
+
return _data;
|
|
59
|
+
}
|
|
60
|
+
let _config = null;
|
|
61
|
+
let _weights = null;
|
|
62
|
+
function loadNeuralModel() {
|
|
63
|
+
if (_config && _weights)
|
|
64
|
+
return { config: _config, weights: _weights };
|
|
65
|
+
_config = loadJson('accentuator.json');
|
|
66
|
+
// Load binary weights
|
|
67
|
+
const binPath = requireRussianDataFile('accentuator.bin');
|
|
68
|
+
const buf = readFileSync(binPath);
|
|
69
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
70
|
+
let off = 0;
|
|
71
|
+
// Header: magic(4) + version(4) + num_layers(4)
|
|
72
|
+
off += 4; // skip magic
|
|
73
|
+
dv.getUint32(off, true);
|
|
74
|
+
off += 4; // version
|
|
75
|
+
const numLayers = dv.getUint32(off, true);
|
|
76
|
+
off += 4;
|
|
77
|
+
const layers = {};
|
|
78
|
+
for (let i = 0; i < numLayers; i++) {
|
|
79
|
+
const nameLen = dv.getUint32(off, true);
|
|
80
|
+
off += 4;
|
|
81
|
+
const name = buf.toString('utf-8', off, off + nameLen);
|
|
82
|
+
off += nameLen;
|
|
83
|
+
const numDims = dv.getUint32(off, true);
|
|
84
|
+
off += 4;
|
|
85
|
+
const shape = [];
|
|
86
|
+
for (let d = 0; d < numDims; d++) {
|
|
87
|
+
shape.push(dv.getUint32(off, true));
|
|
88
|
+
off += 4;
|
|
89
|
+
}
|
|
90
|
+
const dataLen = dv.getUint32(off, true);
|
|
91
|
+
off += 4;
|
|
92
|
+
// Copy bytes into a new ArrayBuffer to guarantee 4-byte alignment
|
|
93
|
+
// (the header strings can throw off alignment in the original buffer)
|
|
94
|
+
const dataBuf = new ArrayBuffer(dataLen);
|
|
95
|
+
new Uint8Array(dataBuf).set(buf.subarray(off, off + dataLen));
|
|
96
|
+
const data = new Float32Array(dataBuf);
|
|
97
|
+
off += dataLen;
|
|
98
|
+
layers[name] = { shape, data };
|
|
99
|
+
}
|
|
100
|
+
_weights = {
|
|
101
|
+
embeddingWeight: layers['embedding.weight'].data,
|
|
102
|
+
fc1Weight: layers['fc1.weight'].data,
|
|
103
|
+
fc1Bias: layers['fc1.bias'].data,
|
|
104
|
+
fc2Weight: layers['fc2.weight'].data,
|
|
105
|
+
fc2Bias: layers['fc2.bias'].data,
|
|
106
|
+
fc3Weight: layers['fc3.weight'].data,
|
|
107
|
+
fc3Bias: layers['fc3.bias'].data,
|
|
108
|
+
fc4Weight: layers['fc4.weight'].data,
|
|
109
|
+
fc4Bias: layers['fc4.bias'].data,
|
|
110
|
+
};
|
|
111
|
+
return { config: _config, weights: _weights };
|
|
112
|
+
}
|
|
113
|
+
function relu(x) { return x > 0 ? x : 0; }
|
|
114
|
+
function linear(input, inDim, weight, bias, outDim) {
|
|
115
|
+
const out = new Float32Array(outDim);
|
|
116
|
+
for (let o = 0; o < outDim; o++) {
|
|
117
|
+
let sum = bias[o];
|
|
118
|
+
const wOff = o * inDim;
|
|
119
|
+
for (let i = 0; i < inDim; i++) {
|
|
120
|
+
sum += input[i] * weight[wOff + i];
|
|
121
|
+
}
|
|
122
|
+
out[o] = sum;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function argmax(arr) {
|
|
127
|
+
let max = -Infinity, idx = 0;
|
|
128
|
+
for (let i = 0; i < arr.length; i++) {
|
|
129
|
+
if (arr[i] > max) {
|
|
130
|
+
max = arr[i];
|
|
131
|
+
idx = i;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return idx;
|
|
135
|
+
}
|
|
136
|
+
/** Neural stress prediction: returns 1-based vowel position, or -1. */
|
|
137
|
+
function neuralPredict(word) {
|
|
138
|
+
const { config, weights } = loadNeuralModel();
|
|
139
|
+
const maxLen = config.max_len;
|
|
140
|
+
const embedDim = config.embed_dim;
|
|
141
|
+
const { char2index } = config;
|
|
142
|
+
// Encode: [ + word + ], padded to maxLen
|
|
143
|
+
const inputIds = new Int32Array(maxLen);
|
|
144
|
+
const chars = ('[' + word.toLowerCase() + ']').split('');
|
|
145
|
+
for (let i = 0; i < Math.min(chars.length, maxLen); i++) {
|
|
146
|
+
inputIds[i] = char2index[chars[i]] ?? 0;
|
|
147
|
+
}
|
|
148
|
+
// Embedding lookup → flatten
|
|
149
|
+
const flat = new Float32Array(maxLen * embedDim);
|
|
150
|
+
for (let i = 0; i < maxLen; i++) {
|
|
151
|
+
const row = inputIds[i] * embedDim;
|
|
152
|
+
const off = i * embedDim;
|
|
153
|
+
for (let j = 0; j < embedDim; j++) {
|
|
154
|
+
flat[off + j] = weights.embeddingWeight[row + j];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// FC1 + ReLU
|
|
158
|
+
const h1 = linear(flat, maxLen * embedDim, weights.fc1Weight, weights.fc1Bias, 1984);
|
|
159
|
+
for (let i = 0; i < h1.length; i++)
|
|
160
|
+
h1[i] = relu(h1[i]);
|
|
161
|
+
// FC2 + ReLU
|
|
162
|
+
const h2 = linear(h1, 1984, weights.fc2Weight, weights.fc2Bias, 1984);
|
|
163
|
+
for (let i = 0; i < h2.length; i++)
|
|
164
|
+
h2[i] = relu(h2[i]);
|
|
165
|
+
// FC3 + ReLU
|
|
166
|
+
const h3 = linear(h2, 1984, weights.fc3Weight, weights.fc3Bias, 54);
|
|
167
|
+
for (let i = 0; i < h3.length; i++)
|
|
168
|
+
h3[i] = relu(h3[i]);
|
|
169
|
+
// FC4 (no ReLU, softmax next)
|
|
170
|
+
const logits = linear(h3, 54, weights.fc4Weight, weights.fc4Bias, 27);
|
|
171
|
+
// Argmax → vowel index (0-based, convert to 1-based)
|
|
172
|
+
const predictedIdx = argmax(logits);
|
|
173
|
+
// Map vowel index to actual position in the word
|
|
174
|
+
let nvowels = 0;
|
|
175
|
+
for (let i = 0; i < word.length; i++) {
|
|
176
|
+
if (RU_VOWELS.includes(word[i].toLowerCase())) {
|
|
177
|
+
nvowels++;
|
|
178
|
+
if (nvowels === predictedIdx + 1)
|
|
179
|
+
return nvowels;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return -1;
|
|
183
|
+
}
|
|
184
|
+
/** Apply ёфикация: replace 'е' with 'ё' where appropriate. */
|
|
185
|
+
export function yoficate(word, feats = {}, upos) {
|
|
186
|
+
const data = loadAccentsData();
|
|
187
|
+
const lower = word.toLowerCase();
|
|
188
|
+
// If it's a known ambiguous accent2 word, don't yoficate (matching original logic)
|
|
189
|
+
if (data.ambiguousAccents2[lower]) {
|
|
190
|
+
return word;
|
|
191
|
+
}
|
|
192
|
+
// If there's a strict single yofication, use it
|
|
193
|
+
if (data.yoWords[lower]) {
|
|
194
|
+
return applyYo(word, data.yoWords[lower]);
|
|
195
|
+
}
|
|
196
|
+
// Check gram-specific ёфикация
|
|
197
|
+
if (data.yoByGram[lower]) {
|
|
198
|
+
const gramData = data.yoByGram[lower];
|
|
199
|
+
let bestYoForm = null;
|
|
200
|
+
let bestMatching = -1;
|
|
201
|
+
// Build the tagset the original Python uses: ud_tags + [upos]
|
|
202
|
+
// Here we approximate with feats + upos
|
|
203
|
+
const featSet = new Set();
|
|
204
|
+
if (upos)
|
|
205
|
+
featSet.add(upos);
|
|
206
|
+
for (const [k, v] of Object.entries(feats)) {
|
|
207
|
+
featSet.add(`${k}=${v}`);
|
|
208
|
+
}
|
|
209
|
+
for (const [yoForm, tagsets] of Object.entries(gramData)) {
|
|
210
|
+
for (const tagset of tagsets) {
|
|
211
|
+
let nbMatched = 0;
|
|
212
|
+
for (const tag of tagset.split('|')) {
|
|
213
|
+
if (featSet.has(tag))
|
|
214
|
+
nbMatched++;
|
|
215
|
+
}
|
|
216
|
+
if (nbMatched > bestMatching) {
|
|
217
|
+
bestMatching = nbMatched;
|
|
218
|
+
bestYoForm = yoForm;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (bestYoForm) {
|
|
223
|
+
return applyYo(word, bestYoForm);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return word;
|
|
227
|
+
}
|
|
228
|
+
function applyYo(word, yoForm) {
|
|
229
|
+
// The yoForm is the same word but with 'ё' where 'е' was.
|
|
230
|
+
// Simply find all positions where yoForm has 'ё' and replace in word.
|
|
231
|
+
const result = word.split('');
|
|
232
|
+
const lowerWord = word.toLowerCase();
|
|
233
|
+
const lowerYo = yoForm.toLowerCase();
|
|
234
|
+
// Walk both strings in parallel, matching characters
|
|
235
|
+
let wi = 0, yi = 0;
|
|
236
|
+
while (wi < result.length && yi < yoForm.length) {
|
|
237
|
+
if (lowerWord[wi] === lowerYo[yi]) {
|
|
238
|
+
// Same character — check if yoForm has 'ё' here
|
|
239
|
+
if (yoForm[yi] === 'ё' || yoForm[yi] === 'Ё') {
|
|
240
|
+
// Replace with ё (preserving case)
|
|
241
|
+
if (word[wi] === word[wi].toUpperCase()) {
|
|
242
|
+
result[wi] = 'Ё';
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
result[wi] = 'ё';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
wi++;
|
|
249
|
+
yi++;
|
|
250
|
+
}
|
|
251
|
+
else if (lowerWord[wi] === 'е' && lowerYo[yi] === 'ё') {
|
|
252
|
+
// Direct е→ё replacement
|
|
253
|
+
if (word[wi] === word[wi].toUpperCase()) {
|
|
254
|
+
result[wi] = 'Ё';
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
result[wi] = 'ё';
|
|
258
|
+
}
|
|
259
|
+
wi++;
|
|
260
|
+
yi++;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// Mismatch — skip
|
|
264
|
+
yi++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return result.join('');
|
|
268
|
+
}
|
|
269
|
+
/** Get all accentuations for a word (multi-variant, like original get_accents). */
|
|
270
|
+
export function getAccents(word, feats = {}, upos) {
|
|
271
|
+
const data = loadAccentsData();
|
|
272
|
+
const lower = word.toLowerCase();
|
|
273
|
+
const vowelCount = countVowels(lower);
|
|
274
|
+
if (vowelCount === 0) {
|
|
275
|
+
return [{ stressPos: -1, secondaryStress: null, ambiguous: false, allStressPositions: [] }];
|
|
276
|
+
}
|
|
277
|
+
if (vowelCount === 1) {
|
|
278
|
+
return [{ stressPos: 1, secondaryStress: getSecondaryStress(lower, data), ambiguous: false, allStressPositions: [1] }];
|
|
279
|
+
}
|
|
280
|
+
const secondaryStress = getSecondaryStress(lower, data);
|
|
281
|
+
// ё-stress
|
|
282
|
+
if (lower.includes('ё')) {
|
|
283
|
+
if (data.wordAccents.has(lower)) {
|
|
284
|
+
return [{ stressPos: data.wordAccents.get(lower), secondaryStress, ambiguous: false, allStressPositions: [data.wordAccents.get(lower)] }];
|
|
285
|
+
}
|
|
286
|
+
let nvowels = 0;
|
|
287
|
+
for (const c of lower) {
|
|
288
|
+
if ('уеыаоэёяию'.includes(c)) {
|
|
289
|
+
nvowels++;
|
|
290
|
+
if (c === 'ё') {
|
|
291
|
+
return [{ stressPos: nvowels, secondaryStress, ambiguous: false, allStressPositions: [nvowels] }];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// ambiguous_accents2 — multiple stress positions
|
|
297
|
+
if (data.ambiguousAccents2[lower]) {
|
|
298
|
+
return data.ambiguousAccents2[lower].map(sp => ({ stressPos: sp, secondaryStress, ambiguous: true, allStressPositions: data.ambiguousAccents2[lower] }));
|
|
299
|
+
}
|
|
300
|
+
// word_accents_dict — single stress
|
|
301
|
+
if (data.wordAccents.has(lower)) {
|
|
302
|
+
const sp = data.wordAccents.get(lower);
|
|
303
|
+
return [{ stressPos: sp, secondaryStress, ambiguous: false, allStressPositions: [sp] }];
|
|
304
|
+
}
|
|
305
|
+
// ambiguous_accents — homograph resolution
|
|
306
|
+
if (data.ambiguousAccents[lower]) {
|
|
307
|
+
const forms = data.ambiguousAccents[lower];
|
|
308
|
+
const results = [];
|
|
309
|
+
for (const [stressedForm, tagsets] of Object.entries(forms)) {
|
|
310
|
+
if (matchesFeatures(feats, tagsets, upos)) {
|
|
311
|
+
const sp = stressPosFromForm(stressedForm);
|
|
312
|
+
if (sp > 0)
|
|
313
|
+
results.push({ stressPos: sp, secondaryStress, ambiguous: true, allStressPositions: [sp] });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (results.length === 0) {
|
|
317
|
+
for (const stressedForm of Object.keys(forms)) {
|
|
318
|
+
const sp = stressPosFromForm(stressedForm);
|
|
319
|
+
if (sp > 0)
|
|
320
|
+
results.push({ stressPos: sp, secondaryStress, ambiguous: true, allStressPositions: [sp] });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (results.length > 0)
|
|
324
|
+
return results;
|
|
325
|
+
}
|
|
326
|
+
// Consonant-only abbreviations
|
|
327
|
+
if (/^[бвгджзклмнпрстфхцчшщ]{2,}$/i.test(lower)) {
|
|
328
|
+
return [{ stressPos: lower.length, secondaryStress: null, ambiguous: false, allStressPositions: [lower.length] }];
|
|
329
|
+
}
|
|
330
|
+
// Verb prefix derivation
|
|
331
|
+
if (feats['VerbForm'] || feats['Aspect']) {
|
|
332
|
+
// Basic heuristic to check if it's a verb, or we can just try it
|
|
333
|
+
const verbStress = applyVerbPrefixDerivation(lower, 'VERB');
|
|
334
|
+
if (verbStress && verbStress > 0) {
|
|
335
|
+
return [{ stressPos: verbStress, secondaryStress, ambiguous: false, allStressPositions: [verbStress] }];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Neural MLP fallback
|
|
339
|
+
const predicted = neuralPredict(lower);
|
|
340
|
+
return [{ stressPos: predicted, secondaryStress: null, ambiguous: false, allStressPositions: predicted > 0 ? [predicted] : [] }];
|
|
341
|
+
}
|
|
342
|
+
/** Get stress position for a word, using dictionary + neural MLP fallback. */
|
|
343
|
+
export function getAccent(word, feats = {}, upos) {
|
|
344
|
+
const data = loadAccentsData();
|
|
345
|
+
const lower = word.toLowerCase();
|
|
346
|
+
const vowelCount = countVowels(lower);
|
|
347
|
+
// 0a. No vowels → no stress
|
|
348
|
+
if (vowelCount === 0) {
|
|
349
|
+
return { stressPos: -1, secondaryStress: null, ambiguous: false, allStressPositions: [] };
|
|
350
|
+
}
|
|
351
|
+
// 0b. Single-vowel words → stress on that vowel (universal Russian rule)
|
|
352
|
+
if (vowelCount === 1) {
|
|
353
|
+
return { stressPos: 1, secondaryStress: null, ambiguous: false, allStressPositions: [1] };
|
|
354
|
+
}
|
|
355
|
+
// 0c. ё-stress rule: if word contains ё, check dict first, then ё is always stressed
|
|
356
|
+
if (lower.includes('ё')) {
|
|
357
|
+
if (data.wordAccents.has(lower)) {
|
|
358
|
+
return {
|
|
359
|
+
stressPos: data.wordAccents.get(lower),
|
|
360
|
+
secondaryStress: getSecondaryStress(lower, data),
|
|
361
|
+
ambiguous: false,
|
|
362
|
+
allStressPositions: [data.wordAccents.get(lower)],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
// ё is always stressed in Russian (except ёфикация and derivatives)
|
|
366
|
+
let nvowels = 0;
|
|
367
|
+
for (const c of lower) {
|
|
368
|
+
if ('уеыаоэёяию'.includes(c)) {
|
|
369
|
+
nvowels++;
|
|
370
|
+
if (c === 'ё') {
|
|
371
|
+
return { stressPos: nvowels, secondaryStress: null, ambiguous: false, allStressPositions: [nvowels] };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// 1. Check ambiguous_accents2 first (multiple possible stress positions)
|
|
377
|
+
if (data.ambiguousAccents2[lower]) {
|
|
378
|
+
const positions = data.ambiguousAccents2[lower];
|
|
379
|
+
return {
|
|
380
|
+
stressPos: positions[0],
|
|
381
|
+
secondaryStress: getSecondaryStress(lower, data),
|
|
382
|
+
ambiguous: true,
|
|
383
|
+
allStressPositions: positions,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
// 2. Check ambiguous_accents (morphologically conditioned)
|
|
387
|
+
if (data.ambiguousAccents[lower]) {
|
|
388
|
+
const forms = data.ambiguousAccents[lower];
|
|
389
|
+
for (const [stressedForm, tagsets] of Object.entries(forms)) {
|
|
390
|
+
if (matchesFeatures(feats, tagsets, upos)) {
|
|
391
|
+
const stressPos = stressPosFromForm(stressedForm);
|
|
392
|
+
if (stressPos > 0) {
|
|
393
|
+
return {
|
|
394
|
+
stressPos,
|
|
395
|
+
secondaryStress: getSecondaryStress(lower, data),
|
|
396
|
+
ambiguous: true,
|
|
397
|
+
allStressPositions: [stressPos],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const firstForm = Object.keys(forms)[0];
|
|
403
|
+
const stressPos = stressPosFromForm(firstForm);
|
|
404
|
+
if (stressPos > 0) {
|
|
405
|
+
return {
|
|
406
|
+
stressPos,
|
|
407
|
+
secondaryStress: getSecondaryStress(lower, data),
|
|
408
|
+
ambiguous: true,
|
|
409
|
+
allStressPositions: [stressPos],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// 3. Check main dictionary
|
|
414
|
+
if (data.wordAccents.has(lower)) {
|
|
415
|
+
return {
|
|
416
|
+
stressPos: data.wordAccents.get(lower),
|
|
417
|
+
secondaryStress: getSecondaryStress(lower, data),
|
|
418
|
+
ambiguous: false,
|
|
419
|
+
allStressPositions: [data.wordAccents.get(lower)],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// 4. Consonant-only abbreviations → stress on last "syllable"
|
|
423
|
+
if (/^[бвгджзклмнпрстфхцчшщ]{2,}$/i.test(lower)) {
|
|
424
|
+
return {
|
|
425
|
+
stressPos: lower.length,
|
|
426
|
+
secondaryStress: null,
|
|
427
|
+
ambiguous: false,
|
|
428
|
+
allStressPositions: [lower.length],
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
// Verb prefix derivation
|
|
432
|
+
if (feats['VerbForm'] || feats['Aspect']) {
|
|
433
|
+
const verbStress = applyVerbPrefixDerivation(lower, 'VERB');
|
|
434
|
+
if (verbStress && verbStress > 0) {
|
|
435
|
+
return {
|
|
436
|
+
stressPos: verbStress,
|
|
437
|
+
secondaryStress: null,
|
|
438
|
+
ambiguous: false,
|
|
439
|
+
allStressPositions: [verbStress],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// 5. Neural MLP fallback for OOV words
|
|
444
|
+
const predicted = neuralPredict(lower);
|
|
445
|
+
return {
|
|
446
|
+
stressPos: predicted,
|
|
447
|
+
secondaryStress: null,
|
|
448
|
+
ambiguous: false,
|
|
449
|
+
allStressPositions: predicted > 0 ? [predicted] : [],
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
/** Check if morphological features match any of the tagsets. A tagset
|
|
453
|
+
* component without '=' is a bare UPOS requirement (e.g. 'NOUN|Gender=Neut')
|
|
454
|
+
* — enforced when the caller supplies the token's UPOS, ignored otherwise
|
|
455
|
+
* (legacy lenient behaviour). */
|
|
456
|
+
function matchesFeatures(feats, tagsets, upos) {
|
|
457
|
+
if (!tagsets || tagsets.length === 0)
|
|
458
|
+
return false;
|
|
459
|
+
for (const tagset of tagsets) {
|
|
460
|
+
let allMatch = true;
|
|
461
|
+
for (const pair of tagset.split('|')) {
|
|
462
|
+
const [key, val] = pair.split('=');
|
|
463
|
+
if (val === undefined) {
|
|
464
|
+
if (upos && key !== upos) {
|
|
465
|
+
allMatch = false;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (feats[key] && feats[key] !== val) {
|
|
471
|
+
allMatch = false;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (allMatch)
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
/** Extract stress position from a stressed form (uppercase vowel = stressed). */
|
|
481
|
+
function stressPosFromForm(form) {
|
|
482
|
+
let nvowels = 0;
|
|
483
|
+
for (const c of form) {
|
|
484
|
+
if ('уеыаоэёяию'.includes(c.toLowerCase()))
|
|
485
|
+
nvowels++;
|
|
486
|
+
if ('АЕЁИОУЫЭЮЯ'.includes(c))
|
|
487
|
+
return nvowels;
|
|
488
|
+
}
|
|
489
|
+
return -1;
|
|
490
|
+
}
|
|
491
|
+
/** Get secondary stress for a word. */
|
|
492
|
+
function getSecondaryStress(word, data) {
|
|
493
|
+
if (data.secondaryStress[word]) {
|
|
494
|
+
return data.secondaryStress[word];
|
|
495
|
+
}
|
|
496
|
+
// Check compound word prefixes
|
|
497
|
+
const deriv = data.derivationData;
|
|
498
|
+
if (deriv?.compound2stress && deriv?.compound_prefixes) {
|
|
499
|
+
for (const prefix of deriv.compound_prefixes) {
|
|
500
|
+
if (word.startsWith(prefix) && word.length > prefix.length) {
|
|
501
|
+
const tail = word.slice(prefix.length);
|
|
502
|
+
if (data.wordAccents.has(tail) || data.ambiguousAccents[tail] || deriv.compound_tails?.[tail]) {
|
|
503
|
+
const stressedHead = deriv.compound2stress[prefix];
|
|
504
|
+
if (stressedHead) {
|
|
505
|
+
const secPos = stressPosFromForm(stressedHead);
|
|
506
|
+
if (secPos > 0) {
|
|
507
|
+
const result = new Array(countVowels(word)).fill(0);
|
|
508
|
+
let vCount = 0;
|
|
509
|
+
for (let i = 0; i < prefix.length && vCount < secPos; i++) {
|
|
510
|
+
if (RU_VOWELS.includes(word[i])) {
|
|
511
|
+
vCount++;
|
|
512
|
+
if (vCount === secPos)
|
|
513
|
+
result[vCount - 1] = 2;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
/** Render a word with stress mark (U+0301 for primary, U+0300 for secondary). */
|
|
526
|
+
export function renderStressedWord(word, accent) {
|
|
527
|
+
const out = [];
|
|
528
|
+
let vowelCount = 0;
|
|
529
|
+
for (const c of word) {
|
|
530
|
+
out.push(c);
|
|
531
|
+
if (RU_VOWELS.includes(c.toLowerCase())) {
|
|
532
|
+
vowelCount++;
|
|
533
|
+
if (accent.secondaryStress && accent.secondaryStress[vowelCount - 1] === 2) {
|
|
534
|
+
out.push('\u0300');
|
|
535
|
+
}
|
|
536
|
+
else if (accent.stressPos === vowelCount) {
|
|
537
|
+
out.push('\u0301');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return out.join('');
|
|
542
|
+
}
|
|
543
|
+
/** Get the data handle (for rhyme module). */
|
|
544
|
+
export function getAccentsData() {
|
|
545
|
+
return loadAccentsData();
|
|
546
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Check if a sequence of words matches a collocation override.
|
|
2
|
+
* Returns the stress override for the matched word, or null. */
|
|
3
|
+
export declare function checkCollocation(words: string[], currentIdx: number): {
|
|
4
|
+
wordIndex: number;
|
|
5
|
+
stressPos: number;
|
|
6
|
+
} | null;
|
|
7
|
+
//# sourceMappingURL=collocations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collocations.d.ts","sourceRoot":"","sources":["../../src/russian/collocations.ts"],"names":[],"mappings":"AAuBA;iEACiE;AACjE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,GACjB;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAwBjD"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// collocations.ts — Collocation stress overrides.
|
|
2
|
+
// Some multi-word expressions have non-obvious stress patterns that differ
|
|
3
|
+
// from the individual words' dictionary stress. This module applies a small
|
|
4
|
+
// set of overrides (185 entries from the original tool).
|
|
5
|
+
import { requireRussianDataFile } from './paths.js';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
let _collocations = null;
|
|
8
|
+
function loadCollocations() {
|
|
9
|
+
if (_collocations)
|
|
10
|
+
return _collocations;
|
|
11
|
+
const p = requireRussianDataFile('collocations.json');
|
|
12
|
+
_collocations = JSON.parse(readFileSync(p, 'utf-8'));
|
|
13
|
+
return _collocations;
|
|
14
|
+
}
|
|
15
|
+
/** Check if a sequence of words matches a collocation override.
|
|
16
|
+
* Returns the stress override for the matched word, or null. */
|
|
17
|
+
export function checkCollocation(words, currentIdx) {
|
|
18
|
+
const colls = loadCollocations();
|
|
19
|
+
// Check 2-word collocations
|
|
20
|
+
if (currentIdx > 0) {
|
|
21
|
+
const key = words[currentIdx - 1].toLowerCase() + '|' + words[currentIdx].toLowerCase();
|
|
22
|
+
if (colls[key]) {
|
|
23
|
+
for (const entry of colls[key]) {
|
|
24
|
+
return { wordIndex: entry.stressed_word_index, stressPos: entry.stress_pos };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Check forward
|
|
29
|
+
if (currentIdx < words.length - 1) {
|
|
30
|
+
const key = words[currentIdx].toLowerCase() + '|' + words[currentIdx + 1].toLowerCase();
|
|
31
|
+
if (colls[key]) {
|
|
32
|
+
for (const entry of colls[key]) {
|
|
33
|
+
return { wordIndex: entry.stressed_word_index, stressPos: entry.stress_pos };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Check if a word is a compound and return secondary stress info. */
|
|
2
|
+
export declare function getCompoundSecondaryStress(word: string): number[] | null;
|
|
3
|
+
/** Apply verb prefix derivation: for prefixed verbs, preserve the stress
|
|
4
|
+
* of the original unprefixed verb. */
|
|
5
|
+
export declare function applyVerbPrefixDerivation(word: string, upos: string): number | null;
|
|
6
|
+
//# sourceMappingURL=compounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compounds.d.ts","sourceRoot":"","sources":["../../src/russian/compounds.ts"],"names":[],"mappings":"AASA,sEAAsE;AACtE,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAiDxE;AAWD;uCACuC;AACvC,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAenF"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// compounds.ts — Compound word and secondary stress handling.
|
|
2
|
+
// Russian frequently uses compound words in poetry. The algorithm detects
|
|
3
|
+
// compound prefixes, places secondary stress on the first root, and primary
|
|
4
|
+
// stress on the second root.
|
|
5
|
+
import { getAccentsData } from './accentuator.js';
|
|
6
|
+
import { countVowels } from './syllabifier.js';
|
|
7
|
+
import { RU_VOWELS } from './types.js';
|
|
8
|
+
/** Check if a word is a compound and return secondary stress info. */
|
|
9
|
+
export function getCompoundSecondaryStress(word) {
|
|
10
|
+
const data = getAccentsData();
|
|
11
|
+
const lower = word.toLowerCase();
|
|
12
|
+
// 1. Check explicit secondary stress dictionary
|
|
13
|
+
if (data.secondaryStress[lower]) {
|
|
14
|
+
return data.secondaryStress[lower];
|
|
15
|
+
}
|
|
16
|
+
// 2. Skip if word already has known stress (not compound)
|
|
17
|
+
if (data.wordAccents.has(lower) || data.ambiguousAccents[lower] || data.ambiguousAccents2[lower]) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// 3. Check compound prefixes
|
|
21
|
+
const deriv = data.derivationData;
|
|
22
|
+
if (!deriv?.compound2stress || !deriv?.compound_prefixes)
|
|
23
|
+
return null;
|
|
24
|
+
for (const prefix of deriv.compound_prefixes) {
|
|
25
|
+
if (lower.startsWith(prefix) && lower.length > prefix.length) {
|
|
26
|
+
const tail = lower.slice(prefix.length);
|
|
27
|
+
const isValidTail = data.wordAccents.has(tail) ||
|
|
28
|
+
data.ambiguousAccents[tail] ||
|
|
29
|
+
data.ambiguousAccents2[tail] ||
|
|
30
|
+
deriv.compound_tails?.[tail];
|
|
31
|
+
if (isValidTail) {
|
|
32
|
+
const stressedHead = deriv.compound2stress[prefix];
|
|
33
|
+
if (stressedHead) {
|
|
34
|
+
const secPos = stressPosFromForm(stressedHead);
|
|
35
|
+
if (secPos > 0) {
|
|
36
|
+
const nVowels = countVowels(lower);
|
|
37
|
+
const result = new Array(nVowels).fill(0);
|
|
38
|
+
let vCount = 0;
|
|
39
|
+
for (let i = 0; i < prefix.length && vCount < secPos; i++) {
|
|
40
|
+
if (RU_VOWELS.includes(lower[i])) {
|
|
41
|
+
vCount++;
|
|
42
|
+
if (vCount === secPos)
|
|
43
|
+
result[vCount - 1] = 2;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function stressPosFromForm(form) {
|
|
55
|
+
let nvowels = 0;
|
|
56
|
+
for (const c of form) {
|
|
57
|
+
if ('уеыаоэёяию'.includes(c.toLowerCase()))
|
|
58
|
+
nvowels++;
|
|
59
|
+
if ('АЕЁИОУЫЭЮЯ'.includes(c))
|
|
60
|
+
return nvowels;
|
|
61
|
+
}
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
/** Apply verb prefix derivation: for prefixed verbs, preserve the stress
|
|
65
|
+
* of the original unprefixed verb. */
|
|
66
|
+
export function applyVerbPrefixDerivation(word, upos) {
|
|
67
|
+
if (upos !== 'VERB')
|
|
68
|
+
return null;
|
|
69
|
+
const data = getAccentsData();
|
|
70
|
+
const lower = word.toLowerCase();
|
|
71
|
+
const verbPrefixes = data.derivationData?.verb_prefixes || [];
|
|
72
|
+
for (const prefix of verbPrefixes) {
|
|
73
|
+
if (lower.startsWith(prefix) && lower.length > prefix.length) {
|
|
74
|
+
const stem = lower.slice(prefix.length);
|
|
75
|
+
if (data.wordAccents.has(stem)) {
|
|
76
|
+
return data.wordAccents.get(stem);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|