@ugfoundation/swaralipi-js 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.
Files changed (51) hide show
  1. package/LICENSE +347 -0
  2. package/README.md +257 -0
  3. package/dist/batch-NF2Q2CFS.js +47 -0
  4. package/dist/batch-NF2Q2CFS.js.map +1 -0
  5. package/dist/chunk-C6O7DYU5.js +115 -0
  6. package/dist/chunk-C6O7DYU5.js.map +1 -0
  7. package/dist/chunk-GWXI2HJA.js +14 -0
  8. package/dist/chunk-GWXI2HJA.js.map +1 -0
  9. package/dist/chunk-N3NNWAOW.js +593 -0
  10. package/dist/chunk-N3NNWAOW.js.map +1 -0
  11. package/dist/cli.cjs +913 -0
  12. package/dist/cli.cjs.map +1 -0
  13. package/dist/cli.d.cts +1 -0
  14. package/dist/cli.d.ts +1 -0
  15. package/dist/cli.js +91 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/convert/index.cjs +720 -0
  18. package/dist/convert/index.cjs.map +1 -0
  19. package/dist/convert/index.d.cts +270 -0
  20. package/dist/convert/index.d.ts +270 -0
  21. package/dist/convert/index.js +5 -0
  22. package/dist/convert/index.js.map +1 -0
  23. package/dist/index.cjs +143 -0
  24. package/dist/index.cjs.map +1 -0
  25. package/dist/index.d.cts +4 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/player/index.cjs +775 -0
  30. package/dist/player/index.cjs.map +1 -0
  31. package/dist/player/index.d.cts +285 -0
  32. package/dist/player/index.d.ts +285 -0
  33. package/dist/player/index.js +743 -0
  34. package/dist/player/index.js.map +1 -0
  35. package/dist/schema-gxhG45OK.d.cts +467 -0
  36. package/dist/schema-gxhG45OK.d.ts +467 -0
  37. package/dist/spec/index.cjs +143 -0
  38. package/dist/spec/index.cjs.map +1 -0
  39. package/dist/spec/index.d.cts +70 -0
  40. package/dist/spec/index.d.ts +70 -0
  41. package/dist/spec/index.js +4 -0
  42. package/dist/spec/index.js.map +1 -0
  43. package/dist/taals-DguYW0wf.d.cts +60 -0
  44. package/dist/taals-DguYW0wf.d.ts +60 -0
  45. package/dist/viewer/index.cjs +998 -0
  46. package/dist/viewer/index.cjs.map +1 -0
  47. package/dist/viewer/index.d.cts +285 -0
  48. package/dist/viewer/index.d.ts +285 -0
  49. package/dist/viewer/index.js +814 -0
  50. package/dist/viewer/index.js.map +1 -0
  51. package/package.json +93 -0
package/dist/cli.cjs ADDED
@@ -0,0 +1,913 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var zod = require('zod');
5
+ var promises = require('fs/promises');
6
+ var path = require('path');
7
+ var util = require('util');
8
+
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __esm = (fn, res) => function __init() {
12
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
13
+ };
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var octaveSchema, svaraLetterSchema, svaraSchema, microtoneSchema, barSchema, bracketSchema, noteSchema, rowSchema, partSchema, taalSchema, metaSchema, swaralipiSchema;
19
+ var init_schema = __esm({
20
+ "src/spec/schema.ts"() {
21
+ octaveSchema = zod.z.enum([
22
+ "ati-mandra",
23
+ // very low
24
+ "mandra",
25
+ // low (dot below)
26
+ "madhyam",
27
+ // middle
28
+ "taar",
29
+ // high (ref/dot above)
30
+ "ati-taar"
31
+ // very high
32
+ ]);
33
+ svaraLetterSchema = zod.z.enum(["s", "r", "g", "m", "p", "d", "n"]);
34
+ svaraSchema = zod.z.tuple([octaveSchema, svaraLetterSchema]);
35
+ microtoneSchema = zod.z.enum(["alpakomal", "atikomal"]);
36
+ barSchema = zod.z.enum(["dari", "single", "double", "quad"]);
37
+ bracketSchema = zod.z.object({
38
+ kind: zod.z.enum(["repeat", "skip", "alt"]),
39
+ edge: zod.z.enum(["open", "close"])
40
+ });
41
+ noteSchema = zod.z.lazy(
42
+ () => zod.z.object({
43
+ svara: svaraSchema.optional(),
44
+ komal: zod.z.boolean().optional(),
45
+ tivra: zod.z.boolean().optional(),
46
+ microtone: microtoneSchema.optional(),
47
+ durationMatra: zod.z.number().positive().optional(),
48
+ sustain: zod.z.boolean().optional(),
49
+ rest: zod.z.boolean().optional(),
50
+ kan: svaraSchema.optional(),
51
+ meend: zod.z.enum(["start", "end"]).optional(),
52
+ bar: barSchema.optional(),
53
+ brackets: zod.z.array(bracketSchema).optional(),
54
+ lyric: zod.z.string().optional(),
55
+ raw: zod.z.string().optional(),
56
+ above: zod.z.array(noteSchema).optional(),
57
+ below: zod.z.array(noteSchema).optional()
58
+ }).refine(
59
+ (n) => n.svara !== void 0 || n.rest === true || n.sustain === true || n.bar !== void 0 || n.raw !== void 0 || n.brackets !== void 0 && n.brackets.length > 0 || n.above !== void 0 && n.above.length > 0 || n.below !== void 0 && n.below.length > 0 || n.lyric !== void 0,
60
+ {
61
+ message: "note must carry one of: svara, rest, sustain, bar, raw, lyric, brackets, or a non-empty above/below"
62
+ }
63
+ )
64
+ );
65
+ rowSchema = zod.z.object({
66
+ /** Optional line label (e.g. a tuk/section marker). */
67
+ label: zod.z.string().optional(),
68
+ notes: zod.z.array(noteSchema)
69
+ });
70
+ partSchema = zod.z.object({
71
+ name: zod.z.string().optional(),
72
+ rows: zod.z.array(rowSchema)
73
+ });
74
+ taalSchema = zod.z.object({
75
+ /** Player sample key, e.g. `dadra`, `teentaal`. */
76
+ id: zod.z.string(),
77
+ numBeats: zod.z.number().int().positive(),
78
+ /** Vibhag groupings; if given, they sum to numBeats. */
79
+ bhaags: zod.z.array(zod.z.number().int().positive()).optional(),
80
+ /** Sam (cycle start) beat index, 0-based. */
81
+ sam: zod.z.number().int().nonnegative().optional(),
82
+ /** Khaali (wave) beat indices, 0-based. */
83
+ khaali: zod.z.array(zod.z.number().int().nonnegative()).optional()
84
+ }).refine((t) => t.bhaags === void 0 || t.bhaags.reduce((a, b) => a + b, 0) === t.numBeats, {
85
+ message: "bhaags must sum to numBeats"
86
+ });
87
+ metaSchema = zod.z.object({
88
+ title: zod.z.string(),
89
+ /** Cross-reference to a gitabitan.io song content id (filename, immutable). */
90
+ songId: zod.z.string().optional(),
91
+ /** Source NLTR node/song id. */
92
+ nltrId: zod.z.number().int().optional(),
93
+ taal: taalSchema.optional(),
94
+ /**
95
+ * The taal name exactly as printed in the source (e.g. `তালমুক্ত`,
96
+ * `ষষ্ঠী(২/৪)`). Always set by the NLTR converter; `taal` is set only
97
+ * when the name resolves to a known definition.
98
+ */
99
+ taalName: zod.z.string().optional(),
100
+ /** Base pitch for playback, e.g. `C`, `C#`, or the player's `cs` form. */
101
+ tonic: zod.z.string().optional(),
102
+ /** Default playback tempo in matra/min. Player-only — the viewer ignores it.
103
+ * The player adopts it on `load()` (precedence: constructor `bpm` → `setBpm`
104
+ * tweak → this → 90). `null` is the converter's blank placeholder = unset. */
105
+ bpm: zod.z.number().positive().nullable().optional(),
106
+ raag: zod.z.string().optional(),
107
+ /** আবর্তন: number of taal cycles, when the source states it. */
108
+ avartan: zod.z.number().int().positive().optional(),
109
+ /** স্বরলিপিকার (notation author). */
110
+ notationBy: zod.z.string().optional(),
111
+ /** Provenance note or URL. */
112
+ source: zod.z.string().optional()
113
+ });
114
+ swaralipiSchema = zod.z.object({
115
+ /** Spec version this document targets (see SPEC_VERSION in ./index). */
116
+ version: zod.z.string(),
117
+ meta: metaSchema,
118
+ parts: zod.z.array(partSchema)
119
+ });
120
+ }
121
+ });
122
+
123
+ // src/spec/taals.ts
124
+ var TAALS;
125
+ var init_taals = __esm({
126
+ "src/spec/taals.ts"() {
127
+ TAALS = {
128
+ dadra: { id: "dadra", numBeats: 6, bhaags: [3, 3], sam: 0, khaali: [3] },
129
+ kehrwa: { id: "kehrwa", numBeats: 8, bhaags: [4, 4], sam: 0, khaali: [4] },
130
+ rupak: { id: "rupak", numBeats: 7, bhaags: [3, 2, 2], sam: 0, khaali: [0] },
131
+ jhaptaal: { id: "jhaptaal", numBeats: 10, bhaags: [2, 3, 2, 3], sam: 0, khaali: [5] },
132
+ ektaal: { id: "ektaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0, khaali: [2, 6] },
133
+ adachautaal: { id: "adachautaal", numBeats: 14, sam: 0 },
134
+ teentaal: { id: "teentaal", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] }
135
+ };
136
+ }
137
+ });
138
+
139
+ // src/spec/index.ts
140
+ var SPEC_VERSION;
141
+ var init_spec = __esm({
142
+ "src/spec/index.ts"() {
143
+ init_schema();
144
+ init_taals();
145
+ SPEC_VERSION = "0.1.0";
146
+ }
147
+ });
148
+
149
+ // src/convert/codec.ts
150
+ function decodeBarRun(body) {
151
+ const m = body.match(BAR_RUN_RE);
152
+ if (!m || !/[lLA]/.test(m[2])) return void 0;
153
+ const notes = [];
154
+ if (m[1]) notes.push({ sustain: true });
155
+ const run = m[2];
156
+ let i = 0;
157
+ while (i < run.length) {
158
+ const ch = run[i];
159
+ if (ch === "l" || ch === "L") {
160
+ let j = i;
161
+ while (j < run.length && run[j] === ch) j++;
162
+ const strokes = (j - i) * (ch === "L" ? 2 : 1);
163
+ notes.push({ bar: strokes >= 4 ? "quad" : strokes >= 2 ? "double" : "single" });
164
+ i = j;
165
+ } else if (ch === "A") {
166
+ notes.push({ bar: "dari" });
167
+ i++;
168
+ } else {
169
+ notes.push({ brackets: [{ kind: "alt", edge: ch === "[" ? "open" : "close" }] });
170
+ i++;
171
+ }
172
+ }
173
+ return notes;
174
+ }
175
+ function octaveOf(mark) {
176
+ if (mark === "f" || mark === "F") return "taar";
177
+ if (mark === "h" || mark === "H") return "mandra";
178
+ return "madhyam";
179
+ }
180
+ function decodeChunk(chunk) {
181
+ const opens = [];
182
+ const closes = [];
183
+ let body = chunk;
184
+ while (body.length > 0) {
185
+ const kind = OPEN_BRACKETS[body[0]];
186
+ if (kind === void 0) break;
187
+ opens.push(kind);
188
+ body = body.slice(1);
189
+ }
190
+ while (body.length > 0) {
191
+ const kind = CLOSE_BRACKETS[body[body.length - 1]];
192
+ if (kind === void 0) break;
193
+ closes.unshift(kind);
194
+ body = body.slice(0, -1);
195
+ }
196
+ const finish = (notes, kind) => {
197
+ if (notes.length === 0) notes.push({});
198
+ const first = notes[0];
199
+ const last = notes[notes.length - 1];
200
+ if (opens.length > 0) first.brackets = [...opens.map((kind_) => ({ kind: kind_, edge: "open" })), ...first.brackets ?? []];
201
+ if (closes.length > 0) last.brackets = [...last.brackets ?? [], ...closes.map((kind_) => ({ kind: kind_, edge: "close" }))];
202
+ return { notes, kind };
203
+ };
204
+ if (body === "") return finish([{}], "decoded");
205
+ const barRun = decodeBarRun(body);
206
+ if (barRun) return finish(barRun, "decoded");
207
+ const structural = STRUCTURALS[body];
208
+ if (structural) return finish([structural()], "decoded");
209
+ if (KNOWN_RAW_RE.some((re) => re.test(body))) return finish([{}], "known");
210
+ const m = body.match(BODY_RE);
211
+ if (m) {
212
+ const [, , , hyphen, , kanLetter, kanOct, unitsStr, aakar, postOct, , trailKan, trailKanOct] = m;
213
+ const attachKan = (note, letter, oct) => {
214
+ const def = LETTERS[letter.toLowerCase()];
215
+ note.kan = [octaveOf(oct?.toLowerCase()), def.letter];
216
+ };
217
+ if (unitsStr) {
218
+ const units = unitsStr.match(/[srgmpqndtkuv][fh]?/g) ?? [];
219
+ const notes = units.map((unit) => {
220
+ const def = LETTERS[unit[0]];
221
+ const note = { svara: [octaveOf(unit[1]), def.letter] };
222
+ if (def.komal) note.komal = true;
223
+ if (def.tivra) note.tivra = true;
224
+ if (units.length > 1) note.durationMatra = 1 / units.length;
225
+ return note;
226
+ });
227
+ const last = notes[notes.length - 1];
228
+ if (postOct && last.svara[0] === "madhyam") {
229
+ last.svara = [octaveOf(postOct), last.svara[1]];
230
+ }
231
+ if (hyphen) notes[0].sustain = true;
232
+ if (kanLetter) attachKan(notes[0], kanLetter, kanOct);
233
+ if (trailKan && last.kan === void 0) attachKan(last, trailKan, trailKanOct);
234
+ return finish(notes, "decoded");
235
+ }
236
+ if (aakar) {
237
+ const note = hyphen ? { sustain: true } : { rest: true };
238
+ if (trailKan) attachKan(note, trailKan, trailKanOct);
239
+ else if (kanLetter) attachKan(note, kanLetter, kanOct);
240
+ return finish([note], "decoded");
241
+ }
242
+ if (hyphen && !kanLetter && !trailKan) {
243
+ return finish([{ sustain: true }], "decoded");
244
+ }
245
+ }
246
+ return finish([{}], "unknown");
247
+ }
248
+ function decodeCell(text) {
249
+ const chunks = text.split(/\s+/).filter((c) => c.length > 0);
250
+ const out = { notes: [], unknownTokens: [], knownRawTokens: [], decoded: false };
251
+ for (const chunk of chunks) {
252
+ const { notes, kind } = decodeChunk(chunk);
253
+ notes[0].raw = chunk;
254
+ if (kind === "decoded") out.decoded = true;
255
+ else if (kind === "known") out.knownRawTokens.push(chunk);
256
+ else out.unknownTokens.push(chunk);
257
+ out.notes.push(...notes);
258
+ }
259
+ return out;
260
+ }
261
+ var LETTERS, STRUCTURALS, BAR_RUN_RE, KNOWN_RAW_RE, OPEN_BRACKETS, CLOSE_BRACKETS, BODY_RE;
262
+ var init_codec = __esm({
263
+ "src/convert/codec.ts"() {
264
+ LETTERS = {
265
+ s: { letter: "s" },
266
+ r: { letter: "r" },
267
+ g: { letter: "g" },
268
+ m: { letter: "m" },
269
+ p: { letter: "p" },
270
+ q: { letter: "d" },
271
+ // ধ — shuddha dha
272
+ n: { letter: "n" },
273
+ d: { letter: "d", komal: true },
274
+ // দ
275
+ t: { letter: "g", komal: true },
276
+ // জ্ঞ
277
+ k: { letter: "m", tivra: true },
278
+ // ক্ষ
279
+ u: { letter: "n", komal: true },
280
+ // ণ
281
+ v: { letter: "r", komal: true }
282
+ // ঋ
283
+ };
284
+ STRUCTURALS = {
285
+ "%": () => ({ svara: ["madhyam", "s"] }),
286
+ // precomposed সা
287
+ "&": () => ({ svara: ["madhyam", "n"] })
288
+ // precomposed নি
289
+ };
290
+ BAR_RUN_RE = /^(-)?([lLA[\]]+)$/;
291
+ KNOWN_RAW_RE = [
292
+ /^[wxyjEJ$<>=_]+$/,
293
+ // arc & line drawing segments
294
+ /^\|+$/,
295
+ // thin yugal-dari stop mark
296
+ /^[0-9!@#]{1,2}[fF]?:?$/,
297
+ // beat-number labels incl. 10–16 (sam tick = f/F, e.g. 1f / 1F / 2:)
298
+ /^[oOe`+,.;:iI~^*bcBCWXYFHZz'"?]$/
299
+ // dots, ticks, duration marks, legacy forms
300
+ ];
301
+ OPEN_BRACKETS = {
302
+ "{": "repeat",
303
+ "(": "skip",
304
+ "[": "alt"
305
+ };
306
+ CLOSE_BRACKETS = {
307
+ "}": "repeat",
308
+ ")": "skip",
309
+ "]": "alt"
310
+ };
311
+ BODY_RE = /^([0-9!@][fF]?)?([zj]*)(-)?([i:])?(?:([SRGMPQNDTKUV])([fFhH])?)?((?:[srgmpqndtkuv][fh]?[i:]?)+)?(a)?([fh])?([i:])?(?:([SRGMPQNDTKUV])([fFhH])?)?([0-9!@][fF]?)?(-)?$/;
312
+ }
313
+ });
314
+
315
+ // src/convert/grid.ts
316
+ function buildSystems(page) {
317
+ const warnings = [];
318
+ const byIndex = /* @__PURE__ */ new Map();
319
+ let styleMismatches = 0;
320
+ for (const cell of page.cells) {
321
+ if (cell.text === "") continue;
322
+ const role = ROLES[cell.row % 4];
323
+ const expected = role === "lyric" ? "style2" : "style1";
324
+ if (cell.style !== expected) styleMismatches++;
325
+ const index = Math.floor(cell.row / 4);
326
+ let system = byIndex.get(index);
327
+ if (!system) {
328
+ system = {
329
+ index,
330
+ above: /* @__PURE__ */ new Map(),
331
+ notation: /* @__PURE__ */ new Map(),
332
+ below: /* @__PURE__ */ new Map(),
333
+ lyric: /* @__PURE__ */ new Map(),
334
+ maxCol: 0
335
+ };
336
+ byIndex.set(index, system);
337
+ }
338
+ system[role].set(cell.col, cell.text);
339
+ if (cell.col > system.maxCol) system.maxCol = cell.col;
340
+ }
341
+ if (styleMismatches > 0) {
342
+ warnings.push(`${styleMismatches} cell(s) had an unexpected style class for their row role`);
343
+ }
344
+ const systems = [...byIndex.values()].sort((a, b) => a.index - b.index);
345
+ return { systems, warnings };
346
+ }
347
+ var ROLES;
348
+ var init_grid = __esm({
349
+ "src/convert/grid.ts"() {
350
+ ROLES = ["above", "notation", "below", "lyric"];
351
+ }
352
+ });
353
+
354
+ // src/convert/taal-map.ts
355
+ function bnInt(s) {
356
+ if (s === void 0) return void 0;
357
+ const ascii = s.trim().replace(/[০-৯]/g, (d) => BN_DIGIT_MAP[d] ?? d);
358
+ return /^\d+$/.test(ascii) ? Number(ascii) : void 0;
359
+ }
360
+ function deriveGeometry(systems) {
361
+ let numCols = 0;
362
+ for (const system of systems) {
363
+ if (system.notation.size === 0) continue;
364
+ const cols = [...system.notation.keys()].sort((a, b) => a - b);
365
+ numCols = Math.max(numCols, cols.length);
366
+ const barCols = cols.filter((c) => BAR_TOKENS.has(system.notation.get(c)));
367
+ if (barCols.length < 2) continue;
368
+ const from = barCols[0];
369
+ const to = barCols[1];
370
+ const inner = cols.filter((c) => c > from && c < to);
371
+ if (inner.length === 0) continue;
372
+ const bhaags = [];
373
+ let run = 0;
374
+ for (const c of inner) {
375
+ if (system.notation.get(c) === DARI_TOKEN) {
376
+ if (run > 0) bhaags.push(run);
377
+ run = 0;
378
+ } else {
379
+ run++;
380
+ }
381
+ }
382
+ if (run > 0) bhaags.push(run);
383
+ const numBeats = bhaags.reduce((a, b) => a + b, 0);
384
+ if (numBeats === 0) continue;
385
+ return { numCols: cols.length, numBeats, bhaags: bhaags.length > 1 ? bhaags : void 0 };
386
+ }
387
+ return numCols > 0 ? { numCols } : void 0;
388
+ }
389
+ function resolveTaal(nameRaw, avartanRaw, geometry) {
390
+ const warnings = [];
391
+ const avartan = bnInt(avartanRaw);
392
+ const taalName = nameRaw?.trim() || void 0;
393
+ if (!taalName || taalName === TAAL_FREE) {
394
+ return { taalName, avartan, matched: "none", warnings };
395
+ }
396
+ const parenMatch = taalName.match(/^(.*?)\s*\(([০-৯\d/\s]+)\)\s*$/);
397
+ const base = (parenMatch ? parenMatch[1] : taalName).trim();
398
+ let hintBhaags;
399
+ if (parenMatch) {
400
+ const parts = parenMatch[2].split("/").map((p) => bnInt(p));
401
+ if (parts.every((p) => p !== void 0) && parts.length > 0) {
402
+ hintBhaags = parts;
403
+ }
404
+ }
405
+ const akhandaMatch = base.match(/^অখন্ড\s+([০-৯\d]+)\s*মাত্রা$/);
406
+ let taal;
407
+ let matched = "none";
408
+ const mappedId = BN_TAAL_MAP[base];
409
+ if (mappedId) {
410
+ taal = { ...TAALS[mappedId] };
411
+ matched = "taals";
412
+ } else if (BN_TAAL_DEFS[base]) {
413
+ taal = { ...BN_TAAL_DEFS[base] };
414
+ matched = "table";
415
+ } else if (akhandaMatch) {
416
+ const beats = bnInt(akhandaMatch[1]);
417
+ if (beats) {
418
+ taal = { id: `akhanda-${beats}`, numBeats: beats, sam: 0 };
419
+ matched = "table";
420
+ }
421
+ }
422
+ const hintBeats = hintBhaags?.reduce((a, b) => a + b, 0);
423
+ if (taal) {
424
+ if (hintBeats !== void 0 && hintBeats !== taal.numBeats) {
425
+ warnings.push(
426
+ `taal ${taal.id}: name declares ${hintBeats} beats (${hintBhaags.join("/")}), table says ${taal.numBeats} \u2014 using the name's`
427
+ );
428
+ taal = { id: taal.id, numBeats: hintBeats, sam: 0 };
429
+ if (hintBhaags.length > 1) taal.bhaags = hintBhaags;
430
+ } else if (hintBhaags && hintBhaags.length > 1) {
431
+ const tableBhaags = taal.bhaags?.join("/");
432
+ if (tableBhaags && tableBhaags !== hintBhaags.join("/")) {
433
+ warnings.push(
434
+ `taal ${taal.id}: name declares bhaags ${hintBhaags.join("/")}, table says ${tableBhaags} \u2014 using the name's`
435
+ );
436
+ const { khaali: _khaali, ...rest } = taal;
437
+ taal = { ...rest, bhaags: hintBhaags };
438
+ }
439
+ }
440
+ } else if (hintBeats !== void 0 || geometry?.numBeats !== void 0) {
441
+ const evidenceBeats = hintBeats ?? geometry?.numBeats;
442
+ const evidenceBhaags = hintBhaags ?? geometry?.bhaags;
443
+ taal = { id: BN_TAAL_IDS[base] ?? base, numBeats: evidenceBeats, sam: 0 };
444
+ if (evidenceBhaags && evidenceBhaags.length > 1) taal.bhaags = evidenceBhaags;
445
+ matched = "geometry";
446
+ } else {
447
+ warnings.push(`unresolved taal name: ${taalName}`);
448
+ }
449
+ if (taal && geometry?.numBeats !== void 0 && geometry.numBeats !== taal.numBeats) {
450
+ warnings.push(
451
+ `grid geometry says ${geometry.numBeats} beats per cycle but taal ${taal.id} has ${taal.numBeats}`
452
+ );
453
+ }
454
+ return { taal, taalName, avartan, matched, warnings };
455
+ }
456
+ var BN_DIGIT_MAP, BN_TAAL_MAP, BN_TAAL_DEFS, BN_TAAL_IDS, TAAL_FREE, BAR_TOKENS, DARI_TOKEN;
457
+ var init_taal_map = __esm({
458
+ "src/convert/taal-map.ts"() {
459
+ init_spec();
460
+ BN_DIGIT_MAP = {
461
+ "\u09E6": "0",
462
+ "\u09E7": "1",
463
+ "\u09E8": "2",
464
+ "\u09E9": "3",
465
+ "\u09EA": "4",
466
+ "\u09EB": "5",
467
+ "\u09EC": "6",
468
+ "\u09ED": "7",
469
+ "\u09EE": "8",
470
+ "\u09EF": "9"
471
+ };
472
+ BN_TAAL_MAP = {
473
+ \u09A6\u09BE\u09A6\u09B0\u09BE: "dadra",
474
+ \u0995\u09BE\u09B9\u09BE\u09B0\u09AC\u09BE: "kehrwa",
475
+ \u098F\u0995\u09A4\u09BE\u09B2\u09BE: "ektaal",
476
+ \u098F\u0995\u09A4\u09BE\u09B2: "ektaal",
477
+ \u09A4\u09BF\u09A8\u09A4\u09BE\u09B2\u09BE: "teentaal",
478
+ \u09A4\u09CD\u09B0\u09BF\u09A4\u09BE\u09B2: "teentaal",
479
+ \u099D\u09BE\u0981\u09AA\u09A4\u09BE\u09B2: "jhaptaal",
480
+ \u099D\u09BE\u0981\u09AA: "jhaptaal",
481
+ \u09B0\u09C1\u09AA\u0995: "rupak",
482
+ \u09B0\u09C2\u09AA\u0995: "rupak",
483
+ \u0986\u09A1\u09BC\u09BE\u099A\u09CC\u09A4\u09BE\u09B2: "adachautaal"
484
+ };
485
+ BN_TAAL_DEFS = {
486
+ \u09A4\u09C7\u0993\u09A1\u09BC\u09BE: { id: "teora", numBeats: 7, bhaags: [3, 2, 2], sam: 0 },
487
+ \u09A4\u09C7\u0993\u09B0\u09BE: { id: "teora", numBeats: 7, bhaags: [3, 2, 2], sam: 0 },
488
+ \u099D\u09AE\u09CD\u09AA\u0995: { id: "jhampak", numBeats: 5, bhaags: [3, 2], sam: 0 },
489
+ \u09B7\u09B7\u09CD\u09A0\u09C0: { id: "shashthi", numBeats: 6, bhaags: [2, 4], sam: 0 },
490
+ \u09B0\u09C1\u09AA\u0995\u09A1\u09BC\u09BE: { id: "rupakda", numBeats: 8, bhaags: [3, 2, 3], sam: 0 },
491
+ \u09B0\u09C2\u09AA\u0995\u09A1\u09BC\u09BE: { id: "rupakda", numBeats: 8, bhaags: [3, 2, 3], sam: 0 },
492
+ \u09A8\u09AC\u09A4\u09BE\u09B2: { id: "nabataal", numBeats: 9, bhaags: [5, 4], sam: 0 },
493
+ \u098F\u0995\u09BE\u09A6\u09B6\u09C0: { id: "ekadashi", numBeats: 11, bhaags: [3, 4, 4], sam: 0 },
494
+ \u09A8\u09AC\u09AA\u099E\u09CD\u099A: { id: "nabapancha", numBeats: 18, bhaags: [2, 4, 4, 4, 4], sam: 0 },
495
+ \u09A7\u09BE\u09AE\u09BE\u09B0: { id: "dhamar", numBeats: 14, bhaags: [3, 2, 2, 3, 4], sam: 0 },
496
+ \u099A\u09CC\u09A4\u09BE\u09B2: { id: "choutaal", numBeats: 12, bhaags: [2, 2, 2, 2, 2, 2], sam: 0 },
497
+ \u09B8\u09C1\u09B0\u09AB\u09BE\u0981\u0995: { id: "surphank", numBeats: 10, sam: 0 }
498
+ };
499
+ BN_TAAL_IDS = {
500
+ \u0995\u09BE\u0993\u09AF\u09BC\u09BE\u09B2\u09BF: "kawali",
501
+ \u0996\u09C7\u09AE\u099F\u09BE: "khemta",
502
+ \u0986\u09A1\u09BC\u09BE\u09A0\u09C7\u0995\u09BE: "araatheka",
503
+ \u0995\u09BE\u09B6\u09CD\u09AE\u09C0\u09B0\u09BF: "kashmiri",
504
+ "\u09AF\u09CE": "yat",
505
+ \u09AE\u09A7\u09CD\u09AF\u09AE\u09BE\u09A8: "madhyaman",
506
+ \u0985\u09B0\u09CD\u09A6\u09CD\u09A7\u099D\u09BE\u0981\u09AA: "ardhajhanp",
507
+ \u0985\u09B0\u09CD\u09A7\u099D\u09BE\u0981\u09AA: "ardhajhanp",
508
+ "\u09AC\u09BF\u09B2\u09AE\u09CD\u09AC\u09BF\u09A4 \u09A4\u09CD\u09B0\u09BF\u09A4\u09BE\u09B2": "vilambit-teentaal"
509
+ };
510
+ TAAL_FREE = "\u09A4\u09BE\u09B2\u09AE\u09C1\u0995\u09CD\u09A4";
511
+ BAR_TOKENS = /* @__PURE__ */ new Set(["l", "ll", "lll", "llll", "L", "LL", "|"]);
512
+ DARI_TOKEN = "A";
513
+ }
514
+ });
515
+
516
+ // src/convert/assemble.ts
517
+ function splitLyric(text) {
518
+ return [...segmenter.segment(text)].map((s) => s.segment);
519
+ }
520
+ function assemble(page, opts = {}) {
521
+ const { systems, warnings } = buildSystems(page);
522
+ const geometry = deriveGeometry(systems);
523
+ const resolved = resolveTaal(page.header.taalNameRaw, page.header.avartanRaw, geometry);
524
+ warnings.push(...resolved.warnings);
525
+ const report = {
526
+ nltrId: opts.nltrId,
527
+ title: page.header.title ?? "",
528
+ taalName: resolved.taalName,
529
+ taalId: resolved.taal?.id,
530
+ taalMatched: resolved.matched,
531
+ systems: 0,
532
+ notationCells: 0,
533
+ lyricCells: 0,
534
+ annotationCells: 0,
535
+ decodedCells: 0,
536
+ knownRawCells: 0,
537
+ unknownCells: 0,
538
+ unknownTokens: {},
539
+ lyricAlignMismatches: 0,
540
+ warnings
541
+ };
542
+ const countUnknown = (tokens) => {
543
+ for (const t of tokens) report.unknownTokens[t] = (report.unknownTokens[t] ?? 0) + 1;
544
+ };
545
+ const rows = [];
546
+ for (const system of systems) {
547
+ const notes = [];
548
+ for (let col = 0; col <= system.maxCol; col++) {
549
+ const notationText = system.notation.get(col);
550
+ const lyricText = system.lyric.get(col);
551
+ const aboveText = system.above.get(col);
552
+ const belowText = system.below.get(col);
553
+ if (notationText === void 0 && lyricText === void 0 && aboveText === void 0 && belowText === void 0) {
554
+ continue;
555
+ }
556
+ let cellNotes;
557
+ if (notationText !== void 0) {
558
+ report.notationCells++;
559
+ const decode = decodeCell(notationText);
560
+ countUnknown(decode.unknownTokens);
561
+ if (decode.unknownTokens.length > 0) report.unknownCells++;
562
+ else if (decode.decoded) report.decodedCells++;
563
+ else report.knownRawCells++;
564
+ cellNotes = decode.notes;
565
+ } else {
566
+ cellNotes = [{}];
567
+ }
568
+ if (lyricText !== void 0) {
569
+ report.lyricCells++;
570
+ attachLyric(cellNotes, lyricText, report);
571
+ }
572
+ if (aboveText !== void 0) {
573
+ report.annotationCells++;
574
+ const decode = decodeCell(aboveText);
575
+ countUnknown(decode.unknownTokens);
576
+ cellNotes[0].above = decode.notes;
577
+ }
578
+ if (belowText !== void 0) {
579
+ report.annotationCells++;
580
+ const decode = decodeCell(belowText);
581
+ countUnknown(decode.unknownTokens);
582
+ cellNotes[0].below = decode.notes;
583
+ }
584
+ notes.push(...cellNotes);
585
+ }
586
+ if (notes.length > 0) {
587
+ rows.push({ notes });
588
+ report.systems++;
589
+ }
590
+ }
591
+ const meta = { title: page.header.title ?? `NLTR ${opts.nltrId ?? "song"}` };
592
+ if (opts.nltrId !== void 0) meta.nltrId = opts.nltrId;
593
+ if (resolved.taal) meta.taal = resolved.taal;
594
+ if (resolved.taalName) meta.taalName = resolved.taalName;
595
+ if (resolved.avartan !== void 0) meta.avartan = resolved.avartan;
596
+ const source = opts.source ?? (opts.nltrId !== void 0 ? `https://rabindra-rachanabali.nltr.org/node/${opts.nltrId}` : page.header.printId !== void 0 ? `https://rabindra-rachanabali.nltr.org/node/${page.header.printId}` : void 0);
597
+ if (source) meta.source = source;
598
+ const doc = swaralipiSchema.parse({ version: SPEC_VERSION, meta, parts: [{ rows }] });
599
+ return { doc, report };
600
+ }
601
+ function attachLyric(cellNotes, lyricText, report) {
602
+ if (cellNotes.length > 1) {
603
+ const clusters = splitLyric(lyricText);
604
+ if (clusters.length === cellNotes.length) {
605
+ clusters.forEach((cluster, i) => {
606
+ cellNotes[i].lyric = cluster;
607
+ });
608
+ return;
609
+ }
610
+ report.lyricAlignMismatches++;
611
+ }
612
+ cellNotes[0].lyric = lyricText;
613
+ }
614
+ var segmenter;
615
+ var init_assemble = __esm({
616
+ "src/convert/assemble.ts"() {
617
+ init_spec();
618
+ init_codec();
619
+ init_grid();
620
+ init_taal_map();
621
+ segmenter = new Intl.Segmenter("bn", { granularity: "grapheme" });
622
+ }
623
+ });
624
+
625
+ // src/convert/extract.ts
626
+ function decodeEntities(s) {
627
+ return s.replace(/&(?:#x([0-9a-fA-F]+)|#(\d+)|([a-zA-Z]+));/g, (m, hex, dec, name) => {
628
+ if (hex) return String.fromCodePoint(parseInt(hex, 16));
629
+ if (dec) return String.fromCodePoint(parseInt(dec, 10));
630
+ return NAMED_ENTITIES[name] ?? m;
631
+ });
632
+ }
633
+ function normalizeCellText(s) {
634
+ return decodeEntities(s).replace(/\p{Cf}/gu, "").replace(/[\s ]+/gu, " ").trim().normalize("NFC");
635
+ }
636
+ function extractPage(html) {
637
+ const header = {};
638
+ for (const m of html.matchAll(META_ROW_RE)) {
639
+ const label = normalizeCellText(m[1]);
640
+ const value = normalizeCellText(m[2]);
641
+ if (label === "\u09A8\u09BE\u09AE") header.title = value;
642
+ else if (label === "\u09A4\u09BE\u09B2") header.taalNameRaw = value;
643
+ else if (label === "\u0986\u09AC\u09B0\u09CD\u09A4\u09A8") header.avartanRaw = value;
644
+ }
645
+ const printMatch = html.match(/\/print\/(\d+)/);
646
+ if (printMatch) header.printId = Number(printMatch[1]);
647
+ const cells = [];
648
+ let maxRow = 0;
649
+ for (const m of html.matchAll(CELL_RE)) {
650
+ const row = Number(m[1]);
651
+ const col = Number(m[2]);
652
+ cells.push({ row, col, style: m[3], text: normalizeCellText(m[4]) });
653
+ if (row > maxRow) maxRow = row;
654
+ }
655
+ const tdCount = (html.match(/<td width='auto' id=/g) ?? []).length;
656
+ if (cells.length !== tdCount) {
657
+ throw new ExtractError(
658
+ `grid cell mismatch: matched ${cells.length} cells but found ${tdCount} '<td width=' openers`
659
+ );
660
+ }
661
+ if (cells.length === 0) throw new ExtractError("no swaralipi grid cells found");
662
+ if (header.title === void 0) throw new ExtractError("no \u09A8\u09BE\u09AE metadata row found");
663
+ return { header, cells, maxRow };
664
+ }
665
+ var ExtractError, NAMED_ENTITIES, CELL_RE, META_ROW_RE;
666
+ var init_extract = __esm({
667
+ "src/convert/extract.ts"() {
668
+ ExtractError = class extends Error {
669
+ constructor(message) {
670
+ super(message);
671
+ this.name = "ExtractError";
672
+ }
673
+ };
674
+ NAMED_ENTITIES = {
675
+ nbsp: "\xA0",
676
+ amp: "&",
677
+ lt: "<",
678
+ gt: ">",
679
+ quot: '"',
680
+ apos: "'"
681
+ };
682
+ CELL_RE = /<td width='auto' id=["']?(\d+)\.(\d+)["']?\s*>\s*<span class='(style\d)'><div align='(?:center|left|right)'>([\s\S]*?)<\/div><\/span><\/td>/g;
683
+ META_ROW_RE = /<tr><td><div align='left'><span class='style3'>([^<]*)<\/span><\/div><\/td><td><div align='left'><span class='style3'>:<\/span><\/div><\/td><td><div align='left'><span class='style3'>([^<]*)<\/span><\/div><\/td><\/tr>/g;
684
+ }
685
+ });
686
+
687
+ // src/convert/report.ts
688
+ function aggregate(reports, failures) {
689
+ const out = {
690
+ files: reports.length + failures.length,
691
+ ok: reports.length,
692
+ failed: failures.length,
693
+ failures,
694
+ systems: 0,
695
+ notationCells: 0,
696
+ lyricCells: 0,
697
+ annotationCells: 0,
698
+ decodedCells: 0,
699
+ knownRawCells: 0,
700
+ unknownCells: 0,
701
+ decodeRate: 0,
702
+ unknownRate: 0,
703
+ unknownTokens: {},
704
+ lyricAlignMismatches: 0,
705
+ taals: {},
706
+ warningCounts: {}
707
+ };
708
+ const unknown = /* @__PURE__ */ new Map();
709
+ for (const r of reports) {
710
+ out.systems += r.systems;
711
+ out.notationCells += r.notationCells;
712
+ out.lyricCells += r.lyricCells;
713
+ out.annotationCells += r.annotationCells;
714
+ out.decodedCells += r.decodedCells;
715
+ out.knownRawCells += r.knownRawCells;
716
+ out.unknownCells += r.unknownCells;
717
+ out.lyricAlignMismatches += r.lyricAlignMismatches;
718
+ for (const [tok, n] of Object.entries(r.unknownTokens)) {
719
+ unknown.set(tok, (unknown.get(tok) ?? 0) + n);
720
+ }
721
+ const taalKey = r.taalName ?? "<none>";
722
+ const taalEntry = out.taals[taalKey] ??= { count: 0, taalId: r.taalId, matched: r.taalMatched };
723
+ taalEntry.count++;
724
+ for (const w of r.warnings) {
725
+ const shape = w.replace(/\d+/g, "N");
726
+ out.warningCounts[shape] = (out.warningCounts[shape] ?? 0) + 1;
727
+ }
728
+ }
729
+ out.unknownTokens = Object.fromEntries([...unknown.entries()].sort((a, b) => b[1] - a[1]));
730
+ if (out.notationCells > 0) {
731
+ out.decodeRate = out.decodedCells / out.notationCells;
732
+ out.unknownRate = out.unknownCells / out.notationCells;
733
+ }
734
+ return out;
735
+ }
736
+ function formatSummary(r) {
737
+ const pct = (x) => `${(x * 100).toFixed(2)}%`;
738
+ const lines = [
739
+ `files: ${r.ok}/${r.files} converted${r.failed ? ` (${r.failed} FAILED)` : ""}`,
740
+ `systems: ${r.systems} notation cells: ${r.notationCells} lyric cells: ${r.lyricCells} annotation cells: ${r.annotationCells}`,
741
+ `decoded: ${r.decodedCells} (${pct(r.decodeRate)}) known-raw: ${r.knownRawCells} unknown: ${r.unknownCells} (${pct(r.unknownRate)})`,
742
+ `lyric alignment fallbacks: ${r.lyricAlignMismatches}`
743
+ ];
744
+ const unknowns = Object.entries(r.unknownTokens);
745
+ if (unknowns.length > 0) {
746
+ lines.push(`unknown tokens (${unknowns.length} distinct, top 20):`);
747
+ for (const [tok, n] of unknowns.slice(0, 20)) lines.push(` ${String(n).padStart(6)} ${JSON.stringify(tok)}`);
748
+ }
749
+ const taalLines = Object.entries(r.taals).sort((a, b) => b[1].count - a[1].count);
750
+ lines.push(`taals (${taalLines.length} distinct):`);
751
+ for (const [name, info] of taalLines.slice(0, 15)) {
752
+ lines.push(` ${String(info.count).padStart(5)} ${name} \u2192 ${info.taalId ?? "\u2014"} [${info.matched}]`);
753
+ }
754
+ if (r.failed > 0) {
755
+ lines.push("failures:");
756
+ for (const f of r.failures.slice(0, 10)) lines.push(` ${f.file}: ${f.error}`);
757
+ }
758
+ return lines.join("\n");
759
+ }
760
+ var init_report = __esm({
761
+ "src/convert/report.ts"() {
762
+ }
763
+ });
764
+
765
+ // src/convert/index.ts
766
+ function convertNltrHtml(html, opts = {}) {
767
+ return assemble(extractPage(html), opts);
768
+ }
769
+ var init_convert = __esm({
770
+ "src/convert/index.ts"() {
771
+ init_assemble();
772
+ init_extract();
773
+ }
774
+ });
775
+
776
+ // src/convert/batch.ts
777
+ var batch_exports = {};
778
+ __export(batch_exports, {
779
+ runConvert: () => runConvert
780
+ });
781
+ async function runConvert(input, opts = {}) {
782
+ const log = opts.log ?? ((line) => process.stderr.write(line + "\n"));
783
+ const stats = await promises.stat(input);
784
+ const files = stats.isDirectory() ? (await promises.readdir(input)).filter((f) => f.endsWith(".html")).sort((a, b) => (nltrIdOf(a) ?? 0) - (nltrIdOf(b) ?? 0) || a.localeCompare(b)).map((f) => path.join(input, f)) : [input];
785
+ const isDir = stats.isDirectory();
786
+ if (isDir && !opts.out) throw new Error("directory input requires -o <outDir>");
787
+ if (isDir && opts.out) await promises.mkdir(opts.out, { recursive: true });
788
+ const reports = [];
789
+ const failures = [];
790
+ for (const file of files) {
791
+ try {
792
+ const html = await promises.readFile(file, "utf8");
793
+ const { doc, report: report2 } = convertNltrHtml(html, { nltrId: nltrIdOf(file) });
794
+ reports.push(report2);
795
+ const json = serialize(doc, opts.compact);
796
+ if (isDir) {
797
+ const name = `${nltrIdOf(file) ?? path.basename(file, ".html")}.json`;
798
+ await promises.writeFile(path.join(opts.out, name), json);
799
+ } else if (opts.out) {
800
+ await promises.writeFile(opts.out, json);
801
+ } else {
802
+ process.stdout.write(json);
803
+ }
804
+ } catch (err) {
805
+ failures.push({ file: path.basename(file), error: err instanceof Error ? err.message : String(err) });
806
+ }
807
+ }
808
+ const report = aggregate(reports, failures);
809
+ if (opts.reportPath) await promises.writeFile(opts.reportPath, serialize(report, false));
810
+ if (!opts.quiet) log(formatSummary(report));
811
+ return { exitCode: failures.length > 0 ? 1 : 0, report };
812
+ }
813
+ var nltrIdOf, serialize;
814
+ var init_batch = __esm({
815
+ "src/convert/batch.ts"() {
816
+ init_convert();
817
+ init_report();
818
+ nltrIdOf = (file) => {
819
+ const m = path.basename(file).match(/^(\d+)\.html$/);
820
+ return m ? Number(m[1]) : void 0;
821
+ };
822
+ serialize = (doc, compact) => (compact ? JSON.stringify(doc) : JSON.stringify(doc, null, 2)) + "\n";
823
+ }
824
+ });
825
+
826
+ // src/cli.ts
827
+ init_spec();
828
+ var VERSION = "0.1.0";
829
+ var HELP = `swaralipi \u2014 Rabindrasangeet swaralipi toolkit (v${VERSION}, spec v${SPEC_VERSION})
830
+
831
+ Usage:
832
+ swaralipi convert <html|dir> [options] NLTR HTML \u2192 spec JSON
833
+
834
+ Options:
835
+ -o, --out <path> output file (single input) or directory (dir input)
836
+ --report <path> write the aggregate decode report JSON here
837
+ --compact minified JSON output (default: pretty, 2-space)
838
+ --quiet suppress the summary on stderr
839
+ -v, --version print version
840
+ -h, --help print this help
841
+
842
+ Examples:
843
+ swaralipi convert 4270.html # JSON to stdout
844
+ swaralipi convert nltr/html -o out/ --report out/_report.json
845
+
846
+ Spec + types are available programmatically:
847
+ import { parseSwaralipi } from '@ugfoundation/swaralipi-js/spec';
848
+ import { convertNltrHtml } from '@ugfoundation/swaralipi-js/convert'; // Node-only
849
+ `;
850
+ async function main(argv) {
851
+ let parsed;
852
+ try {
853
+ parsed = util.parseArgs({
854
+ args: argv.slice(2),
855
+ allowPositionals: true,
856
+ options: {
857
+ out: { type: "string", short: "o" },
858
+ report: { type: "string" },
859
+ compact: { type: "boolean" },
860
+ quiet: { type: "boolean" },
861
+ version: { type: "boolean", short: "v" },
862
+ help: { type: "boolean", short: "h" }
863
+ }
864
+ });
865
+ } catch (err) {
866
+ console.error(err instanceof Error ? err.message : String(err));
867
+ console.error(HELP);
868
+ return 2;
869
+ }
870
+ const { values, positionals } = parsed;
871
+ if (values.version) {
872
+ console.log(VERSION);
873
+ return 0;
874
+ }
875
+ if (values.help || positionals.length === 0) {
876
+ console.log(HELP);
877
+ return 0;
878
+ }
879
+ const [cmd, input] = positionals;
880
+ if (cmd !== "convert") {
881
+ console.error(`Unknown command: ${cmd}
882
+ `);
883
+ console.error(HELP);
884
+ return 1;
885
+ }
886
+ if (!input) {
887
+ console.error("convert: missing <html|dir> input\n");
888
+ console.error(HELP);
889
+ return 2;
890
+ }
891
+ const { runConvert: runConvert2 } = await Promise.resolve().then(() => (init_batch(), batch_exports));
892
+ try {
893
+ const { exitCode } = await runConvert2(input, {
894
+ out: values.out,
895
+ reportPath: values.report,
896
+ compact: values.compact,
897
+ quiet: values.quiet
898
+ });
899
+ return exitCode;
900
+ } catch (err) {
901
+ console.error(`convert: ${err instanceof Error ? err.message : String(err)}`);
902
+ return 2;
903
+ }
904
+ }
905
+ main(process.argv).then(
906
+ (code) => process.exit(code),
907
+ (err) => {
908
+ console.error(err);
909
+ process.exit(2);
910
+ }
911
+ );
912
+ //# sourceMappingURL=cli.cjs.map
913
+ //# sourceMappingURL=cli.cjs.map