@utilix-tech/sdk 0.5.0 → 0.7.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 +22 -6
- package/dist/{chunk-WWKPLYEP.js → chunk-5CKOJXFU.js} +249 -2
- package/dist/{chunk-YBBYFAOV.js → chunk-F57WMKZO.js} +69 -1
- package/dist/{chunk-FL53T24A.js → chunk-GEFVMGZR.js} +2 -8
- package/dist/index.cjs +318 -9
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/media-D66WnthC.d.cts +107 -0
- package/dist/media-D66WnthC.d.ts +107 -0
- package/dist/{text-CI7JAl7F.d.cts → text-Bbx-tZ43.d.cts} +23 -2
- package/dist/{text-CI7JAl7F.d.ts → text-Bbx-tZ43.d.ts} +23 -2
- package/dist/tools/hashing.cjs +2 -8
- package/dist/tools/hashing.js +1 -1
- package/dist/tools/media.cjs +247 -0
- package/dist/tools/media.d.cts +1 -1
- package/dist/tools/media.d.ts +1 -1
- package/dist/tools/media.js +1 -1
- package/dist/tools/text.cjs +68 -0
- package/dist/tools/text.d.cts +1 -1
- package/dist/tools/text.d.ts +1 -1
- package/dist/tools/text.js +1 -1
- package/package.json +3 -3
- package/dist/media-D-K5aZnq.d.cts +0 -53
- package/dist/media-D-K5aZnq.d.ts +0 -53
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utilix-tech/sdk
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**526 developer utility functions for Node.js: runs locally, no API key required.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@utilix-tech/sdk)
|
|
6
6
|
[](https://www.npmjs.com/package/@utilix-tech/sdk)
|
|
@@ -127,7 +127,7 @@ generateHtpasswdFile([{ username: "alice", password: "secret" }]);
|
|
|
127
127
|
|
|
128
128
|
```ts
|
|
129
129
|
import { convertCase, slugify, countWords, generateWords, generateParagraphs,
|
|
130
|
-
escapeString, htmlToMarkdown, applyOps, detectPassiveVoice } from "@utilix-tech/sdk/text";
|
|
130
|
+
escapeString, htmlToMarkdown, applyOps, detectPassiveVoice, scoreReadability } from "@utilix-tech/sdk/text";
|
|
131
131
|
|
|
132
132
|
convertCase("hello world", "camelCase"); // "helloWorld"
|
|
133
133
|
convertCase("hello world", "PascalCase"); // "HelloWorld"
|
|
@@ -148,6 +148,10 @@ htmlToMarkdown("<h1>Hello</h1><p>World</p>");
|
|
|
148
148
|
|
|
149
149
|
// Flag passive-voice sentences, e.g. "was written", "were approved by the team"
|
|
150
150
|
detectPassiveVoice("The report was reviewed by the committee. They approved it.");
|
|
151
|
+
|
|
152
|
+
// Flesch Reading Ease, Flesch-Kincaid Grade Level, and Gunning Fog Index
|
|
153
|
+
scoreReadability("The cat sat on the mat. It was a sunny day.");
|
|
154
|
+
// { fleschReadingEase: 109, fleschKincaidGrade: -0.6, gunningFog: 2.2, readingLevel: "Very easy (5th grade)", ... }
|
|
151
155
|
```
|
|
152
156
|
|
|
153
157
|
---
|
|
@@ -416,10 +420,10 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
|
|
|
416
420
|
|
|
417
421
|
---
|
|
418
422
|
|
|
419
|
-
### `/media`: Image Header Parsing
|
|
423
|
+
### `/media`: Image Header Parsing, EXIF, PDF & WAV Metadata
|
|
420
424
|
|
|
421
425
|
```ts
|
|
422
|
-
import { readImageInfo, readPdfMetadata } from "@utilix-tech/sdk/media";
|
|
426
|
+
import { readImageInfo, readExifData, readPdfMetadata, readWavInfo } from "@utilix-tech/sdk/media";
|
|
423
427
|
|
|
424
428
|
// Reads format, dimensions, bit depth, and alpha channel straight from
|
|
425
429
|
// file bytes: no decoding, no canvas, no image library.
|
|
@@ -427,14 +431,26 @@ const bytes = await fs.promises.readFile("photo.png");
|
|
|
427
431
|
readImageInfo(new Uint8Array(bytes));
|
|
428
432
|
// { format: "png", width: 1920, height: 1080, bitDepth: 8, colorType: "rgba", hasAlpha: true }
|
|
429
433
|
|
|
434
|
+
// Reads camera make/model, orientation, timestamps, exposure settings, and
|
|
435
|
+
// GPS coordinates directly from a JPEG's EXIF data. JPEG only.
|
|
436
|
+
const jpegBytes = await fs.promises.readFile("photo.jpg");
|
|
437
|
+
readExifData(new Uint8Array(jpegBytes));
|
|
438
|
+
// { make: "Canon", model: "EOS R5", orientation: 1, exposureTime: "1/125", fNumber: 2.8, ... }
|
|
439
|
+
|
|
430
440
|
// Reads title, author, dates, page count, and encryption flag from a PDF's
|
|
431
441
|
// trailer/Info dictionary: no pdf.js or other PDF library required.
|
|
432
442
|
const pdfBytes = await fs.promises.readFile("report.pdf");
|
|
433
443
|
readPdfMetadata(new Uint8Array(pdfBytes));
|
|
434
444
|
// { version: "1.7", pageCount: 12, title: "Q3 Report", author: "Jane Doe", ... }
|
|
445
|
+
|
|
446
|
+
// Reads sample rate, channels, bit depth, and duration from a WAV file's
|
|
447
|
+
// RIFF/fmt/data chunk headers: no audio library required.
|
|
448
|
+
const wavBytes = await fs.promises.readFile("recording.wav");
|
|
449
|
+
readWavInfo(new Uint8Array(wavBytes));
|
|
450
|
+
// { audioFormat: 1, audioFormatLabel: "PCM", channels: 2, sampleRate: 44100, bitsPerSample: 16, durationSeconds: 12.4, ... }
|
|
435
451
|
```
|
|
436
452
|
|
|
437
|
-
|
|
453
|
+
`readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only.
|
|
438
454
|
|
|
439
455
|
---
|
|
440
456
|
|
|
@@ -457,7 +473,7 @@ Supports PNG, JPEG, GIF, WebP, and BMP.
|
|
|
457
473
|
| `@utilix-tech/sdk/css` | CSS gradient, box-shadow, border-radius, keyframe animation generators |
|
|
458
474
|
| `@utilix-tech/sdk/misc` | SVG optimize/sanitize, file size format, Unicode inspector |
|
|
459
475
|
| `@utilix-tech/sdk/ai_agent` | Token estimate/trim, chunk text, extract URLs/JSON/keywords, sanitize HTML, flatten/merge JSON, dedupe lines, validate schema, PII/secret/injection detect |
|
|
460
|
-
| `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes (PNG, JPEG, GIF, WebP, BMP); PDF metadata reading |
|
|
476
|
+
| `@utilix-tech/sdk/media` | Image format & dimension reading from raw bytes (PNG, JPEG, GIF, WebP, BMP); PDF metadata reading; WAV audio info |
|
|
461
477
|
|
|
462
478
|
---
|
|
463
479
|
|
|
@@ -3,8 +3,10 @@ import { __export } from './chunk-MLKGABMK.js';
|
|
|
3
3
|
// src/tools/media.ts
|
|
4
4
|
var media_exports = {};
|
|
5
5
|
__export(media_exports, {
|
|
6
|
+
readExifData: () => readExifData,
|
|
6
7
|
readImageInfo: () => readImageInfo,
|
|
7
|
-
readPdfMetadata: () => readPdfMetadata
|
|
8
|
+
readPdfMetadata: () => readPdfMetadata,
|
|
9
|
+
readWavInfo: () => readWavInfo
|
|
8
10
|
});
|
|
9
11
|
function readU32BE(bytes, offset) {
|
|
10
12
|
return (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
|
|
@@ -131,6 +133,195 @@ function readImageInfo(bytes) {
|
|
|
131
133
|
}
|
|
132
134
|
return { error: "Unrecognized image format. Supported formats: PNG, JPEG, GIF, WebP, BMP" };
|
|
133
135
|
}
|
|
136
|
+
var ORIENTATION_LABELS = {
|
|
137
|
+
1: "Normal",
|
|
138
|
+
2: "Mirrored horizontal",
|
|
139
|
+
3: "Rotated 180\xB0",
|
|
140
|
+
4: "Mirrored vertical",
|
|
141
|
+
5: "Mirrored horizontal, rotated 90\xB0 CCW",
|
|
142
|
+
6: "Rotated 90\xB0 CW",
|
|
143
|
+
7: "Mirrored horizontal, rotated 90\xB0 CW",
|
|
144
|
+
8: "Rotated 90\xB0 CCW"
|
|
145
|
+
};
|
|
146
|
+
function tu16(r, o) {
|
|
147
|
+
return r.view.getUint16(o, r.little);
|
|
148
|
+
}
|
|
149
|
+
function tu32(r, o) {
|
|
150
|
+
return r.view.getUint32(o, r.little);
|
|
151
|
+
}
|
|
152
|
+
function readIfdEntries(r, ifdOffset) {
|
|
153
|
+
if (ifdOffset < 0 || ifdOffset + 2 > r.bytes.length) return null;
|
|
154
|
+
const numEntries = tu16(r, ifdOffset);
|
|
155
|
+
const entries = [];
|
|
156
|
+
for (let i = 0; i < numEntries; i++) {
|
|
157
|
+
const entryOffset = ifdOffset + 2 + i * 12;
|
|
158
|
+
if (entryOffset + 12 > r.bytes.length) break;
|
|
159
|
+
entries.push({
|
|
160
|
+
tag: tu16(r, entryOffset),
|
|
161
|
+
type: tu16(r, entryOffset + 2),
|
|
162
|
+
count: tu32(r, entryOffset + 4),
|
|
163
|
+
valueOffsetField: entryOffset + 8
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return { entries };
|
|
167
|
+
}
|
|
168
|
+
function findEntry(entries, tag) {
|
|
169
|
+
return entries.find((e) => e.tag === tag);
|
|
170
|
+
}
|
|
171
|
+
function readAscii(r, entry) {
|
|
172
|
+
const size = entry.count;
|
|
173
|
+
const inline = size <= 4;
|
|
174
|
+
const dataOffset = inline ? entry.valueOffsetField : r.tiffStart + tu32(r, entry.valueOffsetField);
|
|
175
|
+
if (dataOffset < 0 || dataOffset + size > r.bytes.length) return null;
|
|
176
|
+
let out = "";
|
|
177
|
+
for (let i = 0; i < size; i++) {
|
|
178
|
+
const c = r.bytes[dataOffset + i];
|
|
179
|
+
if (c === 0) break;
|
|
180
|
+
out += String.fromCharCode(c);
|
|
181
|
+
}
|
|
182
|
+
const trimmed = out.trim();
|
|
183
|
+
return trimmed || null;
|
|
184
|
+
}
|
|
185
|
+
function readRational(r, entry) {
|
|
186
|
+
const dataOffset = r.tiffStart + tu32(r, entry.valueOffsetField);
|
|
187
|
+
if (dataOffset < 0 || dataOffset + 8 > r.bytes.length) return null;
|
|
188
|
+
return { num: tu32(r, dataOffset), den: tu32(r, dataOffset + 4) };
|
|
189
|
+
}
|
|
190
|
+
function readGpsCoordinate(r, entries, valueTag, refTag) {
|
|
191
|
+
const valueEntry = findEntry(entries, valueTag);
|
|
192
|
+
if (!valueEntry) return null;
|
|
193
|
+
const dataOffset = r.tiffStart + tu32(r, valueEntry.valueOffsetField);
|
|
194
|
+
if (dataOffset < 0 || dataOffset + 24 > r.bytes.length) return null;
|
|
195
|
+
const deg = tu32(r, dataOffset) / (tu32(r, dataOffset + 4) || 1);
|
|
196
|
+
const min = tu32(r, dataOffset + 8) / (tu32(r, dataOffset + 12) || 1);
|
|
197
|
+
const sec = tu32(r, dataOffset + 16) / (tu32(r, dataOffset + 20) || 1);
|
|
198
|
+
let decimal = deg + min / 60 + sec / 3600;
|
|
199
|
+
const refEntry = findEntry(entries, refTag);
|
|
200
|
+
const ref = refEntry ? readAscii(r, refEntry) : null;
|
|
201
|
+
if (ref === "S" || ref === "W") decimal = -decimal;
|
|
202
|
+
return Math.round(decimal * 1e6) / 1e6;
|
|
203
|
+
}
|
|
204
|
+
function gcd(a, b) {
|
|
205
|
+
return b === 0 ? a : gcd(b, a % b);
|
|
206
|
+
}
|
|
207
|
+
function round2(n) {
|
|
208
|
+
return Math.round(n * 100) / 100;
|
|
209
|
+
}
|
|
210
|
+
function formatExposureTime(num, den) {
|
|
211
|
+
if (num === 0) return "0s";
|
|
212
|
+
if (num >= den) return `${round2(num / den)}s`;
|
|
213
|
+
const divisor = gcd(num, den) || 1;
|
|
214
|
+
const n = num / divisor;
|
|
215
|
+
const d = den / divisor;
|
|
216
|
+
return `1/${Math.round(d / n)}`;
|
|
217
|
+
}
|
|
218
|
+
function readExifData(bytes) {
|
|
219
|
+
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) {
|
|
220
|
+
return { error: "Not a JPEG file (EXIF is only read from JPEG files)" };
|
|
221
|
+
}
|
|
222
|
+
let offset = 2;
|
|
223
|
+
let exifStart = -1;
|
|
224
|
+
while (offset + 4 <= bytes.length) {
|
|
225
|
+
if (bytes[offset] !== 255) {
|
|
226
|
+
offset++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const marker = bytes[offset + 1];
|
|
230
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
231
|
+
offset += 2;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (marker === 217 || marker === 218) break;
|
|
235
|
+
const segmentLength = bytes[offset + 2] << 8 | bytes[offset + 3];
|
|
236
|
+
if (marker === 225) {
|
|
237
|
+
const idOffset = offset + 4;
|
|
238
|
+
if (idOffset + 6 <= bytes.length && bytes[idOffset] === 69 && bytes[idOffset + 1] === 120 && bytes[idOffset + 2] === 105 && bytes[idOffset + 3] === 102 && bytes[idOffset + 4] === 0 && bytes[idOffset + 5] === 0) {
|
|
239
|
+
exifStart = idOffset + 6;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
offset += 2 + segmentLength;
|
|
244
|
+
}
|
|
245
|
+
if (exifStart === -1) return { error: "No EXIF segment found in this JPEG" };
|
|
246
|
+
if (exifStart + 8 > bytes.length) return { error: "Truncated EXIF segment" };
|
|
247
|
+
const byteOrder = String.fromCharCode(bytes[exifStart], bytes[exifStart + 1]);
|
|
248
|
+
if (byteOrder !== "II" && byteOrder !== "MM") return { error: "Invalid TIFF byte order marker in EXIF data" };
|
|
249
|
+
const little = byteOrder === "II";
|
|
250
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
251
|
+
const r = { bytes, view, little, tiffStart: exifStart };
|
|
252
|
+
if (tu16(r, exifStart + 2) !== 42) return { error: "Invalid TIFF magic number in EXIF data" };
|
|
253
|
+
const ifd0Offset = exifStart + tu32(r, exifStart + 4);
|
|
254
|
+
const ifd0 = readIfdEntries(r, ifd0Offset);
|
|
255
|
+
if (!ifd0) return { error: "Could not read IFD0 from EXIF data" };
|
|
256
|
+
const result = { tagCount: ifd0.entries.length };
|
|
257
|
+
const makeEntry = findEntry(ifd0.entries, 271);
|
|
258
|
+
if (makeEntry) {
|
|
259
|
+
const v = readAscii(r, makeEntry);
|
|
260
|
+
if (v) result.make = v;
|
|
261
|
+
}
|
|
262
|
+
const modelEntry = findEntry(ifd0.entries, 272);
|
|
263
|
+
if (modelEntry) {
|
|
264
|
+
const v = readAscii(r, modelEntry);
|
|
265
|
+
if (v) result.model = v;
|
|
266
|
+
}
|
|
267
|
+
const softwareEntry = findEntry(ifd0.entries, 305);
|
|
268
|
+
if (softwareEntry) {
|
|
269
|
+
const v = readAscii(r, softwareEntry);
|
|
270
|
+
if (v) result.software = v;
|
|
271
|
+
}
|
|
272
|
+
const dateTimeEntry = findEntry(ifd0.entries, 306);
|
|
273
|
+
if (dateTimeEntry) {
|
|
274
|
+
const v = readAscii(r, dateTimeEntry);
|
|
275
|
+
if (v) result.dateTime = v;
|
|
276
|
+
}
|
|
277
|
+
const orientEntry = findEntry(ifd0.entries, 274);
|
|
278
|
+
if (orientEntry) {
|
|
279
|
+
const v = tu16(r, orientEntry.valueOffsetField);
|
|
280
|
+
result.orientation = v;
|
|
281
|
+
result.orientationLabel = ORIENTATION_LABELS[v] ?? "Unknown";
|
|
282
|
+
}
|
|
283
|
+
const exifIfdPtr = findEntry(ifd0.entries, 34665);
|
|
284
|
+
if (exifIfdPtr) {
|
|
285
|
+
const subOffset = exifStart + tu32(r, exifIfdPtr.valueOffsetField);
|
|
286
|
+
const sub = readIfdEntries(r, subOffset);
|
|
287
|
+
if (sub) {
|
|
288
|
+
const dtoEntry = findEntry(sub.entries, 36867);
|
|
289
|
+
if (dtoEntry) {
|
|
290
|
+
const v = readAscii(r, dtoEntry);
|
|
291
|
+
if (v) result.dateTimeOriginal = v;
|
|
292
|
+
}
|
|
293
|
+
const expEntry = findEntry(sub.entries, 33434);
|
|
294
|
+
if (expEntry) {
|
|
295
|
+
const rat = readRational(r, expEntry);
|
|
296
|
+
if (rat && rat.den !== 0) result.exposureTime = formatExposureTime(rat.num, rat.den);
|
|
297
|
+
}
|
|
298
|
+
const fEntry = findEntry(sub.entries, 33437);
|
|
299
|
+
if (fEntry) {
|
|
300
|
+
const rat = readRational(r, fEntry);
|
|
301
|
+
if (rat && rat.den !== 0) result.fNumber = round2(rat.num / rat.den);
|
|
302
|
+
}
|
|
303
|
+
const isoEntry = findEntry(sub.entries, 34855);
|
|
304
|
+
if (isoEntry) result.isoSpeed = tu16(r, isoEntry.valueOffsetField);
|
|
305
|
+
const focalEntry = findEntry(sub.entries, 37386);
|
|
306
|
+
if (focalEntry) {
|
|
307
|
+
const rat = readRational(r, focalEntry);
|
|
308
|
+
if (rat && rat.den !== 0) result.focalLength = round2(rat.num / rat.den);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const gpsIfdPtr = findEntry(ifd0.entries, 34853);
|
|
313
|
+
if (gpsIfdPtr) {
|
|
314
|
+
const gpsOffset = exifStart + tu32(r, gpsIfdPtr.valueOffsetField);
|
|
315
|
+
const gps = readIfdEntries(r, gpsOffset);
|
|
316
|
+
if (gps) {
|
|
317
|
+
const lat = readGpsCoordinate(r, gps.entries, 2, 1);
|
|
318
|
+
const lon = readGpsCoordinate(r, gps.entries, 4, 3);
|
|
319
|
+
if (lat !== null) result.gpsLatitude = lat;
|
|
320
|
+
if (lon !== null) result.gpsLongitude = lon;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
134
325
|
function bytesToLatin1(bytes) {
|
|
135
326
|
let out = "";
|
|
136
327
|
const CHUNK = 32768;
|
|
@@ -273,5 +464,61 @@ function readPdfMetadata(bytes) {
|
|
|
273
464
|
encrypted
|
|
274
465
|
};
|
|
275
466
|
}
|
|
467
|
+
var AUDIO_FORMAT_LABELS = {
|
|
468
|
+
1: "PCM",
|
|
469
|
+
3: "IEEE float",
|
|
470
|
+
6: "A-law",
|
|
471
|
+
7: "mu-law",
|
|
472
|
+
65534: "Extensible"
|
|
473
|
+
};
|
|
474
|
+
function readAscii4(bytes, offset) {
|
|
475
|
+
return String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
|
|
476
|
+
}
|
|
477
|
+
function readWavInfo(bytes) {
|
|
478
|
+
if (bytes.length < 12) return { error: "File is too small to be a valid WAV file" };
|
|
479
|
+
if (readAscii4(bytes, 0) !== "RIFF") return { error: 'Not a RIFF file (missing "RIFF" header)' };
|
|
480
|
+
if (readAscii4(bytes, 8) !== "WAVE") return { error: 'Not a WAVE file (missing "WAVE" format marker)' };
|
|
481
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
482
|
+
const fileSize = view.getUint32(4, true) + 8;
|
|
483
|
+
let offset = 12;
|
|
484
|
+
let fmt = null;
|
|
485
|
+
let dataSize = null;
|
|
486
|
+
while (offset + 8 <= bytes.length) {
|
|
487
|
+
const chunkId = readAscii4(bytes, offset);
|
|
488
|
+
const chunkSize = view.getUint32(offset + 4, true);
|
|
489
|
+
const dataOffset = offset + 8;
|
|
490
|
+
if (chunkId === "fmt ") {
|
|
491
|
+
if (dataOffset + 16 > bytes.length) return { error: 'Truncated "fmt " chunk' };
|
|
492
|
+
fmt = {
|
|
493
|
+
audioFormat: view.getUint16(dataOffset, true),
|
|
494
|
+
channels: view.getUint16(dataOffset + 2, true),
|
|
495
|
+
sampleRate: view.getUint32(dataOffset + 4, true),
|
|
496
|
+
byteRate: view.getUint32(dataOffset + 8, true),
|
|
497
|
+
blockAlign: view.getUint16(dataOffset + 12, true),
|
|
498
|
+
bitsPerSample: view.getUint16(dataOffset + 14, true)
|
|
499
|
+
};
|
|
500
|
+
} else if (chunkId === "data") {
|
|
501
|
+
dataSize = Math.min(chunkSize, bytes.length - dataOffset);
|
|
502
|
+
}
|
|
503
|
+
offset = dataOffset + chunkSize + chunkSize % 2;
|
|
504
|
+
if (fmt && dataSize !== null) break;
|
|
505
|
+
}
|
|
506
|
+
if (!fmt) return { error: 'No "fmt " chunk found in this WAV file' };
|
|
507
|
+
if (dataSize === null) return { error: 'No "data" chunk found in this WAV file' };
|
|
508
|
+
if (fmt.byteRate <= 0) return { error: 'Invalid byte rate in "fmt " chunk' };
|
|
509
|
+
const durationSeconds = Math.round(dataSize / fmt.byteRate * 1e3) / 1e3;
|
|
510
|
+
return {
|
|
511
|
+
audioFormat: fmt.audioFormat,
|
|
512
|
+
audioFormatLabel: AUDIO_FORMAT_LABELS[fmt.audioFormat] ?? `Unknown (0x${fmt.audioFormat.toString(16)})`,
|
|
513
|
+
channels: fmt.channels,
|
|
514
|
+
sampleRate: fmt.sampleRate,
|
|
515
|
+
byteRate: fmt.byteRate,
|
|
516
|
+
blockAlign: fmt.blockAlign,
|
|
517
|
+
bitsPerSample: fmt.bitsPerSample,
|
|
518
|
+
dataSize,
|
|
519
|
+
durationSeconds,
|
|
520
|
+
fileSize
|
|
521
|
+
};
|
|
522
|
+
}
|
|
276
523
|
|
|
277
|
-
export { media_exports, readImageInfo, readPdfMetadata };
|
|
524
|
+
export { media_exports, readExifData, readImageInfo, readPdfMetadata, readWavInfo };
|
|
@@ -51,6 +51,7 @@ __export(text_exports, {
|
|
|
51
51
|
readabilityScore: () => readabilityScore,
|
|
52
52
|
renderMarkdown: () => renderMarkdown,
|
|
53
53
|
romanToNumber: () => romanToNumber,
|
|
54
|
+
scoreReadability: () => scoreReadability,
|
|
54
55
|
sentenceCount: () => sentenceCount,
|
|
55
56
|
slugify: () => slugify,
|
|
56
57
|
slugifyAll: () => slugifyAll,
|
|
@@ -2389,5 +2390,72 @@ function detectPassiveVoice(text) {
|
|
|
2389
2390
|
const passivePercent = totalSentences > 0 ? Math.round(passiveSentences / totalSentences * 1e3) / 10 : 0;
|
|
2390
2391
|
return { sentences: analyzed, totalSentences, passiveSentences, passivePercent };
|
|
2391
2392
|
}
|
|
2393
|
+
function countSyllables(word) {
|
|
2394
|
+
const w = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
2395
|
+
if (w.length === 0) return 0;
|
|
2396
|
+
if (w.length <= 3) return 1;
|
|
2397
|
+
let count = 0;
|
|
2398
|
+
let prevWasVowel = false;
|
|
2399
|
+
for (const ch of w) {
|
|
2400
|
+
const isVowel = "aeiouy".includes(ch);
|
|
2401
|
+
if (isVowel && !prevWasVowel) count++;
|
|
2402
|
+
prevWasVowel = isVowel;
|
|
2403
|
+
}
|
|
2404
|
+
if (w.endsWith("e") && !w.endsWith("le") && count > 1) count--;
|
|
2405
|
+
if (w.endsWith("le") && w.length > 2 && !"aeiouy".includes(w[w.length - 3])) count++;
|
|
2406
|
+
return Math.max(count, 1);
|
|
2407
|
+
}
|
|
2408
|
+
function readabilityGradeLabel(fleschReadingEase) {
|
|
2409
|
+
if (fleschReadingEase >= 90) return "Very easy (5th grade)";
|
|
2410
|
+
if (fleschReadingEase >= 80) return "Easy (6th grade)";
|
|
2411
|
+
if (fleschReadingEase >= 70) return "Fairly easy (7th grade)";
|
|
2412
|
+
if (fleschReadingEase >= 60) return "Standard (8th-9th grade)";
|
|
2413
|
+
if (fleschReadingEase >= 50) return "Fairly difficult (10th-12th grade)";
|
|
2414
|
+
if (fleschReadingEase >= 30) return "Difficult (college)";
|
|
2415
|
+
return "Very difficult (college graduate)";
|
|
2416
|
+
}
|
|
2417
|
+
function round1(n) {
|
|
2418
|
+
return Math.round(n * 10) / 10;
|
|
2419
|
+
}
|
|
2420
|
+
function scoreReadability(text) {
|
|
2421
|
+
const sentences = splitIntoSentences(text);
|
|
2422
|
+
const words = text.match(/[a-zA-Z']+/g) ?? [];
|
|
2423
|
+
const sentenceCount2 = Math.max(sentences.length, 1);
|
|
2424
|
+
const wordCount = words.length;
|
|
2425
|
+
const syllableCounts = words.map(countSyllables);
|
|
2426
|
+
const syllableCount2 = syllableCounts.reduce((a, b) => a + b, 0);
|
|
2427
|
+
const complexWordCount = syllableCounts.filter((c) => c >= 3).length;
|
|
2428
|
+
if (wordCount === 0) {
|
|
2429
|
+
return {
|
|
2430
|
+
wordCount: 0,
|
|
2431
|
+
sentenceCount: 0,
|
|
2432
|
+
syllableCount: 0,
|
|
2433
|
+
complexWordCount: 0,
|
|
2434
|
+
avgWordsPerSentence: 0,
|
|
2435
|
+
avgSyllablesPerWord: 0,
|
|
2436
|
+
fleschReadingEase: 0,
|
|
2437
|
+
fleschKincaidGrade: 0,
|
|
2438
|
+
gunningFog: 0,
|
|
2439
|
+
readingLevel: "No text to score"
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
const avgWordsPerSentence = wordCount / sentenceCount2;
|
|
2443
|
+
const avgSyllablesPerWord = syllableCount2 / wordCount;
|
|
2444
|
+
const fleschReadingEase = round1(206.835 - 1.015 * avgWordsPerSentence - 84.6 * avgSyllablesPerWord);
|
|
2445
|
+
const fleschKincaidGrade = round1(0.39 * avgWordsPerSentence + 11.8 * avgSyllablesPerWord - 15.59);
|
|
2446
|
+
const gunningFog = round1(0.4 * (avgWordsPerSentence + 100 * (complexWordCount / wordCount)));
|
|
2447
|
+
return {
|
|
2448
|
+
wordCount,
|
|
2449
|
+
sentenceCount: sentences.length,
|
|
2450
|
+
syllableCount: syllableCount2,
|
|
2451
|
+
complexWordCount,
|
|
2452
|
+
avgWordsPerSentence: round1(avgWordsPerSentence),
|
|
2453
|
+
avgSyllablesPerWord: round1(avgSyllablesPerWord),
|
|
2454
|
+
fleschReadingEase,
|
|
2455
|
+
fleschKincaidGrade,
|
|
2456
|
+
gunningFog,
|
|
2457
|
+
readingLevel: readabilityGradeLabel(fleschReadingEase)
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2392
2460
|
|
|
2393
|
-
export { CASE_FORMATS, DEFAULT_CONVERT_OPTIONS, ESCAPE_MODES, LINE_OPS, MORSE_CODE, addBorder, analyzeText, applyOp, applyOps, centerText, convertAll, convertCase, countWords, csvToHtml, csvToMarkdown, detectDelimiter, detectPassiveVoice, diffLines, diffLinesSimple, diffStats, escapeAll, escapeString, extractHeadings, formatReadingTime, generate, generateDiff, generateHtmlTable, generateMarkdownTable, generateParagraphs, generateSentences, generateWords, getAvailableFonts, getMorseForChar, getMorseTiming, htmlToMarkdown, isValidMorse, markdownToHtml, morseToText, numberToOrdinal, numberToRoman, numberToWords, ordinalWords, parseDiff, parseTableData, readabilityScore, renderMarkdown, romanToNumber, sentenceCount, slugify, slugifyAll, syllableCount, textToAscii, textToMorse, text_exports, unescapeString, wordFrequency };
|
|
2461
|
+
export { CASE_FORMATS, DEFAULT_CONVERT_OPTIONS, ESCAPE_MODES, LINE_OPS, MORSE_CODE, addBorder, analyzeText, applyOp, applyOps, centerText, convertAll, convertCase, countWords, csvToHtml, csvToMarkdown, detectDelimiter, detectPassiveVoice, diffLines, diffLinesSimple, diffStats, escapeAll, escapeString, extractHeadings, formatReadingTime, generate, generateDiff, generateHtmlTable, generateMarkdownTable, generateParagraphs, generateSentences, generateWords, getAvailableFonts, getMorseForChar, getMorseTiming, htmlToMarkdown, isValidMorse, markdownToHtml, morseToText, numberToOrdinal, numberToRoman, numberToWords, ordinalWords, parseDiff, parseTableData, readabilityScore, renderMarkdown, romanToNumber, scoreReadability, sentenceCount, slugify, slugifyAll, syllableCount, textToAscii, textToMorse, text_exports, unescapeString, wordFrequency };
|
|
@@ -189,13 +189,7 @@ function generateMd5Hash(password) {
|
|
|
189
189
|
return "$apr1$" + md5(password);
|
|
190
190
|
}
|
|
191
191
|
function generateBcryptHash(password, rounds = 10) {
|
|
192
|
-
|
|
193
|
-
let salt = "";
|
|
194
|
-
for (let i = 0; i < 22; i++) salt += chars[Math.floor(Math.random() * chars.length)];
|
|
195
|
-
const roundStr = rounds.toString().padStart(2, "0");
|
|
196
|
-
let hash = "";
|
|
197
|
-
for (let i = 0; i < 31; i++) hash += chars[Math.floor(Math.random() * chars.length)];
|
|
198
|
-
return `$2b$${roundStr}$${salt}${hash}`;
|
|
192
|
+
return bcrypt.hashSync(password, rounds);
|
|
199
193
|
}
|
|
200
194
|
function generatePlain(password) {
|
|
201
195
|
return password;
|
|
@@ -204,7 +198,7 @@ function createEntry(username, password, algorithm = "bcrypt") {
|
|
|
204
198
|
let hashedPassword;
|
|
205
199
|
switch (algorithm) {
|
|
206
200
|
case "bcrypt":
|
|
207
|
-
hashedPassword = generateBcryptHash();
|
|
201
|
+
hashedPassword = generateBcryptHash(password);
|
|
208
202
|
break;
|
|
209
203
|
case "sha1":
|
|
210
204
|
hashedPassword = generateSha1Hash(password);
|