echogarden 3.0.5 → 3.2.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 (107) hide show
  1. package/data/lexicons/heteronyms.en.json +2 -2
  2. package/data/lexicons/words.en.json +120 -0
  3. package/dist/alignment/SemanticTextAlignment.js +2 -2
  4. package/dist/alignment/SemanticTextAlignment.js.map +1 -1
  5. package/dist/api/Alignment.js +2 -2
  6. package/dist/api/Alignment.js.map +1 -1
  7. package/dist/api/Synthesis.js +2 -2
  8. package/dist/api/Synthesis.js.map +1 -1
  9. package/dist/encodings/Ascii.d.ts +1 -3
  10. package/dist/encodings/Ascii.d.ts.map +1 -1
  11. package/dist/encodings/Ascii.js +13 -9
  12. package/dist/encodings/Ascii.js.map +1 -1
  13. package/dist/encodings/Base64.d.ts.map +1 -1
  14. package/dist/encodings/Base64.js +9 -0
  15. package/dist/encodings/Base64.js.map +1 -1
  16. package/dist/encodings/Hex.d.ts.map +1 -1
  17. package/dist/encodings/Hex.js +11 -2
  18. package/dist/encodings/Hex.js.map +1 -1
  19. package/dist/encodings/LEB128.d.ts +1 -15
  20. package/dist/encodings/LEB128.d.ts.map +1 -1
  21. package/dist/encodings/LEB128.js +1 -247
  22. package/dist/encodings/LEB128.js.map +1 -1
  23. package/dist/encodings/Utf16.d.ts +1 -3
  24. package/dist/encodings/Utf16.d.ts.map +1 -1
  25. package/dist/encodings/Utf16.js +13 -9
  26. package/dist/encodings/Utf16.js.map +1 -1
  27. package/dist/encodings/Utf32.d.ts.map +1 -1
  28. package/dist/encodings/Utf32.js +6 -0
  29. package/dist/encodings/Utf32.js.map +1 -1
  30. package/dist/encodings/Utf8.d.ts +9 -5
  31. package/dist/encodings/Utf8.d.ts.map +1 -1
  32. package/dist/encodings/Utf8.js +36 -78
  33. package/dist/encodings/Utf8.js.map +1 -1
  34. package/dist/nlp/Segmentation.d.ts +2 -8
  35. package/dist/nlp/Segmentation.d.ts.map +1 -1
  36. package/dist/nlp/Segmentation.js +42 -19
  37. package/dist/nlp/Segmentation.js.map +1 -1
  38. package/dist/nlp/TextNormalizer.d.ts.map +1 -1
  39. package/dist/nlp/TextNormalizer.js +29 -14
  40. package/dist/nlp/TextNormalizer.js.map +1 -1
  41. package/dist/recognition/AmazonTranscribeSTT.js +3 -3
  42. package/dist/recognition/AmazonTranscribeSTT.js.map +1 -1
  43. package/dist/synthesis/AzureCognitiveServicesTTS.js +1 -1
  44. package/dist/synthesis/AzureCognitiveServicesTTS.js.map +1 -1
  45. package/dist/synthesis/EspeakTTS.d.ts.map +1 -1
  46. package/dist/synthesis/EspeakTTS.js +15 -8
  47. package/dist/synthesis/EspeakTTS.js.map +1 -1
  48. package/dist/synthesis/KokoroTTS.d.ts.map +1 -1
  49. package/dist/synthesis/KokoroTTS.js +0 -5
  50. package/dist/synthesis/KokoroTTS.js.map +1 -1
  51. package/dist/synthesis/MicrosoftEdgeTTS.js +1 -1
  52. package/dist/synthesis/MicrosoftEdgeTTS.js.map +1 -1
  53. package/dist/synthesis/VitsTTS.d.ts.map +1 -1
  54. package/dist/synthesis/VitsTTS.js +0 -5
  55. package/dist/synthesis/VitsTTS.js.map +1 -1
  56. package/dist/utilities/FileSystem.d.ts +12 -7
  57. package/dist/utilities/FileSystem.d.ts.map +1 -1
  58. package/dist/utilities/FileSystem.js +196 -79
  59. package/dist/utilities/FileSystem.js.map +1 -1
  60. package/dist/utilities/FileWriter.d.ts +1 -0
  61. package/dist/utilities/FileWriter.d.ts.map +1 -1
  62. package/dist/utilities/FileWriter.js +8 -1
  63. package/dist/utilities/FileWriter.js.map +1 -1
  64. package/dist/utilities/ObjectUtilities.d.ts.map +1 -1
  65. package/dist/utilities/ObjectUtilities.js +15 -13
  66. package/dist/utilities/ObjectUtilities.js.map +1 -1
  67. package/dist/utilities/PackageManager.js +2 -2
  68. package/dist/utilities/PackageManager.js.map +1 -1
  69. package/dist/utilities/PathUtilities.d.ts +1 -0
  70. package/dist/utilities/PathUtilities.d.ts.map +1 -1
  71. package/dist/utilities/PathUtilities.js +18 -0
  72. package/dist/utilities/PathUtilities.js.map +1 -1
  73. package/dist/utilities/StringUtilities.d.ts.map +1 -1
  74. package/dist/utilities/StringUtilities.js.map +1 -1
  75. package/dist/utilities/Timeline.js +2 -2
  76. package/dist/utilities/Timeline.js.map +1 -1
  77. package/dist/utilities/WikipediaReader.js +5 -5
  78. package/dist/utilities/WikipediaReader.js.map +1 -1
  79. package/package.json +13 -12
  80. package/src/alignment/SemanticTextAlignment.ts +2 -2
  81. package/src/api/Alignment.ts +2 -2
  82. package/src/api/Synthesis.ts +2 -2
  83. package/src/encodings/Ascii.ts +14 -10
  84. package/src/encodings/Base64.ts +9 -0
  85. package/src/encodings/Hex.ts +11 -2
  86. package/src/encodings/Utf16.ts +14 -10
  87. package/src/encodings/Utf32.ts +6 -0
  88. package/src/encodings/Utf8.ts +45 -82
  89. package/src/nlp/Segmentation.ts +53 -19
  90. package/src/nlp/TextNormalizer.ts +40 -16
  91. package/src/recognition/AmazonTranscribeSTT.ts +3 -3
  92. package/src/synthesis/AzureCognitiveServicesTTS.ts +1 -1
  93. package/src/synthesis/EspeakTTS.ts +17 -9
  94. package/src/synthesis/KokoroTTS.ts +0 -6
  95. package/src/synthesis/MicrosoftEdgeTTS.ts +1 -1
  96. package/src/synthesis/VitsTTS.ts +0 -6
  97. package/src/utilities/FileSystem.ts +224 -85
  98. package/src/utilities/FileWriter.ts +11 -1
  99. package/src/utilities/ObjectUtilities.ts +19 -15
  100. package/src/utilities/PackageManager.ts +2 -2
  101. package/src/utilities/PathUtilities.ts +19 -0
  102. package/src/utilities/StringUtilities.ts +0 -2
  103. package/src/utilities/Timeline.ts +2 -2
  104. package/src/utilities/WikipediaReader.ts +5 -5
  105. package/src/encodings/HtmlEscape.ts +0 -39
  106. package/src/encodings/LEB128.ts +0 -362
  107. package/src/utilities/StringBuilder.ts +0 -45
@@ -1,6 +1,5 @@
1
1
  import * as fsExtra from 'fs-extra/esm'
2
2
  import gracefulFS from 'graceful-fs'
3
- import * as os from 'node:os'
4
3
 
5
4
  import { promisify } from 'node:util'
6
5
  import { getRandomHexString, parseJson, stringifyAndFormatJson } from './Utilities.js'
@@ -8,7 +7,7 @@ import { appName } from '../api/Common.js'
8
7
  import { getAppTempDir, getDirName, joinPath, normalizePath, parsePath } from './PathUtilities.js'
9
8
 
10
9
  import { createDynamicUint8Array } from '../data-structures/DynamicTypedArray.js'
11
- import { ChunkedUtf8Decoder } from '../encodings/Utf8.js'
10
+ import { ChunkedUtf8Decoder, ChunkedUtf8Encoder } from '../encodings/Utf8.js'
12
11
  import { FileWriter } from './FileWriter.js'
13
12
  import { FileReader } from './FileReader.js'
14
13
 
@@ -26,9 +25,10 @@ export const access = promisify(gracefulFS.access)
26
25
 
27
26
  export const readdir = promisify(gracefulFS.readdir)
28
27
  export const copyFile = promisify(gracefulFS.copyFile)
28
+ export const rename = promisify(gracefulFS.rename)
29
+ export const fsync = promisify(gracefulFS.fsync)
29
30
 
30
31
  export const remove = fsExtra.remove
31
- export const copy = fsExtra.copy
32
32
 
33
33
  ///////////////////////////////////////////////////////////////////////////////////////////
34
34
  // File read operations
@@ -41,32 +41,35 @@ export async function readFileAsBinary(filePath: string) {
41
41
 
42
42
  const fileReader = new FileReader(filePath)
43
43
  const buffer = new Uint8Array(chunkSize)
44
- const result = createDynamicUint8Array(fileSize)
44
+ const fileContent = createDynamicUint8Array(fileSize)
45
45
 
46
46
  while (!fileReader.isFinished) {
47
47
  const chunk = await fileReader.readChunk(buffer)
48
48
 
49
- result.addArray(chunk)
49
+ fileContent.addArray(chunk)
50
50
  }
51
51
 
52
- return result.toTypedArray()
52
+ return fileContent.toTypedArray()
53
53
  }
54
54
 
55
55
  export async function readFileAsUtf8(filePath: string) {
56
- const chunkSize = 2 ** 20
56
+ const maxChunkSize = 2 ** 20
57
57
 
58
58
  const fileReader = new FileReader(filePath)
59
- const buffer = new Uint8Array(chunkSize)
59
+ const buffer = new Uint8Array(maxChunkSize)
60
+
61
+ const chunkedUtf8Decoder = new ChunkedUtf8Decoder()
60
62
 
61
- const result = new ChunkedUtf8Decoder()
63
+ let fileContent = ''
62
64
 
63
65
  while (!fileReader.isFinished) {
64
- const chunk = await fileReader.readChunk(buffer)
66
+ const utf8Chunk = await fileReader.readChunk(buffer)
65
67
 
66
- result.writeChunk(chunk)
68
+ const stringChunk = chunkedUtf8Decoder.writeChunk(utf8Chunk)
69
+ fileContent += stringChunk
67
70
  }
68
71
 
69
- return result.toString()
72
+ return fileContent
70
73
  }
71
74
 
72
75
  export async function readAndParseJsonFile(jsonFilePath: string, useJson5 = false) {
@@ -78,17 +81,17 @@ export async function readAndParseJsonFile(jsonFilePath: string, useJson5 = fals
78
81
  ///////////////////////////////////////////////////////////////////////////////////////////
79
82
  // File write operations
80
83
  ///////////////////////////////////////////////////////////////////////////////////////////
81
- export async function writeFile(filePath: string, content: Uint8Array | string) {
84
+ export async function writeFile(filePath: string, content: Uint8Array | string, options?: WriteFileOptions) {
82
85
  if (content instanceof Uint8Array) {
83
- return writeBinaryFile(filePath, content)
86
+ return writeBinaryFile(filePath, content, options)
84
87
  } else if (typeof content === 'string') {
85
- return writeUtf8File(filePath, content)
88
+ return writeUtf8File(filePath, content, options)
86
89
  } else {
87
- throw new Error(`Content can only be a Uint8Array or string`)
90
+ throw new Error(`Content can only be a Uint8Array or string.`)
88
91
  }
89
92
  }
90
93
 
91
- export async function writeBinaryFile(filePath: string, content: Uint8Array) {
94
+ export async function writeBinaryFile(filePath: string, content: Uint8Array, options?: WriteFileOptions) {
92
95
  const maxChunkSize = 2 ** 20
93
96
 
94
97
  const fileDir = getDirName(filePath)
@@ -97,24 +100,28 @@ export async function writeBinaryFile(filePath: string, content: Uint8Array) {
97
100
 
98
101
  const fileWriter = new FileWriter(filePath)
99
102
 
100
- let readOffset = 0
103
+ try {
104
+ // The do-while body runs at least once, even for empty content, so the
105
+ // file is always created (or truncated) via FileWriter's first write.
106
+ let readOffset = 0
101
107
 
102
- while (true) {
103
- const chunk = content.subarray(readOffset, readOffset + maxChunkSize)
108
+ do {
109
+ const chunk = content.subarray(readOffset, readOffset + maxChunkSize)
104
110
 
105
- if (chunk.length === 0) {
106
- break
107
- }
111
+ readOffset += chunk.length
108
112
 
109
- await fileWriter.write(chunk)
113
+ await fileWriter.write(chunk)
114
+ } while (readOffset < content.length)
110
115
 
111
- readOffset += chunk.length
116
+ if (options?.fsync === true) {
117
+ await fileWriter.fsync()
118
+ }
119
+ } finally {
120
+ await fileWriter.dispose()
112
121
  }
113
-
114
- await fileWriter.dispose()
115
122
  }
116
123
 
117
- export async function writeUtf8File(filePath: string, content: string) {
124
+ export async function writeUtf8File(filePath: string, content: string, options?: WriteFileOptions) {
118
125
  const maxChunkSize = 2 ** 20
119
126
 
120
127
  const fileDir = getDirName(filePath)
@@ -123,25 +130,36 @@ export async function writeUtf8File(filePath: string, content: string) {
123
130
 
124
131
  const fileWriter = new FileWriter(filePath)
125
132
 
126
- const textEncoder = new TextEncoder()
133
+ const chunkedUtf8Encoder = new ChunkedUtf8Encoder()
127
134
 
128
- let readOffset = 0
135
+ try {
136
+ // The do-while body runs at least once, even for empty content, so the
137
+ // file is always created (or truncated) via FileWriter's first write.
138
+ let readOffset = 0
129
139
 
130
- while (true) {
131
- const stringChunk = content.substring(readOffset, readOffset + maxChunkSize)
140
+ do {
141
+ const stringChunk = content.substring(readOffset, readOffset + maxChunkSize)
142
+
143
+ readOffset += stringChunk.length
132
144
 
133
- const chunk = textEncoder.encode(stringChunk)
145
+ const utf8Chunk = chunkedUtf8Encoder.writeChunk(stringChunk)
134
146
 
135
- await fileWriter.write(chunk)
147
+ await fileWriter.write(utf8Chunk)
148
+ } while (readOffset < content.length)
136
149
 
137
- readOffset += stringChunk.length
150
+ const finalChunk = chunkedUtf8Encoder.finalize()
138
151
 
139
- if (stringChunk.length === 0) {
140
- break
152
+ if (finalChunk.length > 0) {
153
+ await fileWriter.write(finalChunk)
141
154
  }
142
- }
143
155
 
144
- await fileWriter.dispose()
156
+ if (options?.fsync === true) {
157
+ await fileWriter.fsync()
158
+ }
159
+ } finally {
160
+ // Always release the file handle, even if an error interrupted the write.
161
+ await fileWriter.dispose()
162
+ }
145
163
  }
146
164
 
147
165
  export async function writeJsonFile(filePath: string, content: any, useJson5 = false) {
@@ -151,12 +169,25 @@ export async function writeJsonFile(filePath: string, content: any, useJson5 = f
151
169
  }
152
170
 
153
171
  export async function writeFileSafe(filePath: string, content: Uint8Array | string) {
154
- const tempDir = getAppTempDir(appName)
172
+ const destDir = getDirName(filePath)
173
+
174
+ const tempDir = await getTemporaryDirectory(destDir)
155
175
  const tempFilePath = joinPath(tempDir, `${getRandomHexString(16)}.partial`)
156
176
 
157
- await writeFile(tempFilePath, content)
177
+ try {
178
+ await writeFile(tempFilePath, content, { fsync: true })
179
+
180
+ await move(tempFilePath, filePath)
181
+ } catch (e) {
182
+ // Best-effort cleanup so failed writes don't leave orphaned .partial files behind.
183
+ try {
184
+ await remove(tempFilePath)
185
+ } catch {
186
+ // Ignore cleanup errors so the original error is not masked.
187
+ }
158
188
 
159
- await move(tempFilePath, filePath)
189
+ throw e
190
+ }
160
191
  }
161
192
 
162
193
  ///////////////////////////////////////////////////////////////////////////////////////////
@@ -176,19 +207,6 @@ export async function ensureDir(dirPath: string) {
176
207
  }
177
208
  }
178
209
 
179
- export async function testDirectoryIsWritable(dir: string) {
180
- const testFileName = joinPath(dir, getRandomHexString(16))
181
-
182
- try {
183
- await fsExtra.createFile(testFileName)
184
- await remove(testFileName)
185
- } catch (e) {
186
- return false
187
- }
188
-
189
- return true
190
- }
191
-
192
210
  export async function readDirRecursive(dir: string, pathFilter?: (filePath: string) => boolean) {
193
211
  if (!(await stat(dir)).isDirectory()) {
194
212
  throw new Error(`'${dir}' is not a directory`)
@@ -220,33 +238,20 @@ export async function readDirRecursive(dir: string, pathFilter?: (filePath: stri
220
238
  return result
221
239
  }
222
240
 
223
- export function getAppDataDir(appName: string) {
224
- let dataDir: string
225
-
226
- const platform = process.platform
227
- const homeDir = os.homedir()
228
-
229
- if (platform == 'win32') {
230
- dataDir = joinPath(homeDir, 'AppData', 'Local', appName)
231
- } else if (platform == 'darwin') {
232
- dataDir = joinPath(homeDir, 'Library', 'Application Support', appName)
233
- } else if (platform == 'linux') {
234
- dataDir = joinPath(homeDir, '.local', 'share', appName)
235
- } else {
236
- throw new Error(`Unsupport platform ${platform}`)
237
- }
238
-
239
- return dataDir
240
- }
241
-
242
241
  ///////////////////////////////////////////////////////////////////////////////////////////
243
- // Copy operations
242
+ // Copy and move operations
244
243
  ///////////////////////////////////////////////////////////////////////////////////////////
245
244
  export async function move(source: string, dest: string) {
246
245
  source = normalizePath(source)
247
246
  dest = normalizePath(dest)
248
247
 
249
- if (existsSync(dest)) {
248
+ const destStats = existsSync(dest) ? await stat(dest) : undefined
249
+
250
+ if (destStats?.isDirectory()) {
251
+ throw new Error(`The destination path '${dest}' exists and is a directory. A file cannot be moved over a directory.`)
252
+ }
253
+
254
+ if (destStats) {
250
255
  const destPathExistsAndIsWritable = await existsAndIsWritable(dest)
251
256
 
252
257
  if (!destPathExistsAndIsWritable) {
@@ -254,29 +259,88 @@ export async function move(source: string, dest: string) {
254
259
  }
255
260
  } else {
256
261
  const destDir = parsePath(dest).dir
257
- const destDirIsWritable = await testDirectoryIsWritable(destDir)
258
262
 
259
- if (!destDirIsWritable) {
260
- throw new Error(`The directory ${destDir} is not writable. There may be a permissions issue.`)
263
+ await ensureDir(destDir)
264
+ }
265
+
266
+ try {
267
+ // fs.rename() replaces an existing destination file in a single step on
268
+ // POSIX and on Windows (where libuv uses MoveFileExW with
269
+ // MOVEFILE_REPLACE_EXISTING, the atomic replace flag available to Node).
270
+ await rename(source, dest)
271
+ } catch (e) {
272
+ const error = e as NodeJS.ErrnoException
273
+
274
+ if (error.code === 'EXDEV') {
275
+ // The source and destination are on different volumes, where an
276
+ // atomic rename is impossible; fall back to a copy and delete.
277
+ await copyFile(source, dest)
278
+ await tryFsyncFile(dest)
279
+ await remove(source)
280
+ } else {
281
+ throw error
261
282
  }
262
283
  }
263
284
 
264
- return fsExtra.move(source, dest, { overwrite: true })
285
+ // The rename is now durable: flush the destination directory so the name
286
+ // change survives a crash (best effort, see the helper).
287
+ await tryFsyncDirectory(parsePath(dest).dir)
265
288
  }
266
289
 
267
290
  ///////////////////////////////////////////////////////////////////////////////////////////
268
- // Misc operations
291
+ // Synchronization operations
269
292
  ///////////////////////////////////////////////////////////////////////////////////////////
270
- export async function existsAndIsWritable(targetPath: string) {
293
+ export async function tryFsyncFile(filePath: string) {
294
+ let fileHandle: number | undefined
295
+
296
+ let succeeded = false
297
+
271
298
  try {
272
- await access(targetPath, gracefulFS.constants.W_OK);
299
+ fileHandle = await open(filePath, 'r+')
300
+
301
+ await fsync(fileHandle)
302
+
303
+ succeeded = true
273
304
  } catch {
274
- return false
305
+ } finally {
306
+ if (fileHandle !== undefined) {
307
+ try {
308
+ await close(fileHandle)
309
+ } catch {
310
+ }
311
+ }
275
312
  }
276
313
 
277
- return true
314
+ return succeeded
315
+ }
316
+
317
+ export async function tryFsyncDirectory(dirPath: string) {
318
+ let dirHandle: number | undefined
319
+
320
+ let succeeded = false
321
+
322
+ try {
323
+ dirHandle = await open(dirPath, 'r')
324
+
325
+ await fsync(dirHandle)
326
+
327
+ succeeded = true
328
+ } catch {
329
+ } finally {
330
+ if (dirHandle !== undefined) {
331
+ try {
332
+ await close(dirHandle)
333
+ } catch {
334
+ }
335
+ }
336
+ }
337
+
338
+ return succeeded
278
339
  }
279
340
 
341
+ ///////////////////////////////////////////////////////////////////////////////////////////
342
+ // Misc operations
343
+ ///////////////////////////////////////////////////////////////////////////////////////////
280
344
  export async function chmodRecursive(rootPath: string, newMode: number) {
281
345
  const rootPathStat = await stat(rootPath)
282
346
 
@@ -323,3 +387,78 @@ export async function computeFileSha256Hex(filePath: string) {
323
387
 
324
388
  return result
325
389
  }
390
+
391
+ export async function existsAndIsWritable(targetPath: string) {
392
+ try {
393
+ await access(targetPath, gracefulFS.constants.W_OK);
394
+ } catch {
395
+ return false
396
+ }
397
+
398
+ return true
399
+ }
400
+
401
+ // The OS temporary directory keeps temporary files away from the caller's
402
+ // directories, and the OS itself eventually cleans it up. It is used when it is
403
+ // on the same volume as the destination, since then the final rename is atomic.
404
+ //
405
+ // When it is on a different volume, the temporary file is written next to the
406
+ // destination instead: an atomic rename across volumes is impossible and would
407
+ // have to fall back to a copy and delete.
408
+ async function getTemporaryDirectory(destDir: string) {
409
+ const osTempDir = getAppTempDir(appName)
410
+
411
+ await ensureDir(osTempDir)
412
+
413
+ if (await areOnSameDevice(osTempDir, destDir)) {
414
+ return osTempDir
415
+ } else {
416
+ return destDir
417
+ }
418
+ }
419
+
420
+ async function areOnSameDevice(firstPath: string, secondPath: string) {
421
+ const firstDevice = await getDeviceOfExistingPath(firstPath)
422
+ const secondDevice = await getDeviceOfExistingPath(secondPath)
423
+
424
+ return firstDevice !== undefined && firstDevice === secondDevice
425
+ }
426
+
427
+ // Resolves the device of the closest existing ancestor directory of a target
428
+ // path, since the path itself may not exist yet. The device identifies the
429
+ // volume a path lives on (on Windows it is the drive number).
430
+ async function getDeviceOfExistingPath(targetPath: string) {
431
+ let currentPath = normalizePath(targetPath)
432
+
433
+ while (true) {
434
+ try {
435
+ const targetStats = await stat(currentPath)
436
+
437
+ return targetStats.dev
438
+ } catch (e) {
439
+ const error = e as NodeJS.ErrnoException
440
+
441
+ if (error.code !== 'ENOENT') {
442
+ // If the path cannot be inspected, treat the volumes as different;
443
+ // falling back to the destination directory always remains safe.
444
+ return undefined
445
+ }
446
+
447
+ const parentPath = getDirName(currentPath)
448
+
449
+ if (parentPath === currentPath) {
450
+ return undefined
451
+ }
452
+
453
+ currentPath = parentPath
454
+ }
455
+ }
456
+ }
457
+
458
+ ///////////////////////////////////////////////////////////////////////////////////////////
459
+ // Types
460
+ ///////////////////////////////////////////////////////////////////////////////////////////
461
+
462
+ interface WriteFileOptions {
463
+ fsync?: boolean
464
+ }
@@ -1,4 +1,4 @@
1
- import { write, open, close } from "./FileSystem.js"
1
+ import { write, open, close, fsync } from './FileSystem.js'
2
2
 
3
3
  export class FileWriter {
4
4
  private fileHandle?: number
@@ -45,6 +45,16 @@ export class FileWriter {
45
45
  this.fileHandle = await open(this.filePath, 'w')
46
46
  }
47
47
 
48
+ async fsync() {
49
+ if (this.isDisposed) {
50
+ throw new Error(`FileWriter has been disposed`)
51
+ }
52
+
53
+ await this.openIfNeeded()
54
+
55
+ await fsync(this.fileHandle!)
56
+ }
57
+
48
58
  async dispose() {
49
59
  if (this.isDisposed) {
50
60
  return
@@ -1,11 +1,12 @@
1
1
  export function extendDeep(base: any, extension: any): any {
2
2
  const baseClone = deepClone(base)
3
3
 
4
- if (isPlainObject(base) && extension === undefined) {
4
+ if (extension === undefined) {
5
5
  return baseClone
6
6
  }
7
7
 
8
8
  const extensionClone = deepClone(extension)
9
+
9
10
  if (!isPlainObject(base) || !isPlainObject(extension)) {
10
11
  return extensionClone
11
12
  }
@@ -21,15 +22,15 @@ export function extendDeep(base: any, extension: any): any {
21
22
  return baseClone
22
23
  }
23
24
 
24
- export function shallowClone<T>(val: T) {
25
- return clone(val, false)
25
+ export function shallowClone<T>(val: T): T {
26
+ return clone(val, false, new Set())
26
27
  }
27
28
 
28
- export function deepClone<T>(val: T) {
29
- return clone(val, true)
29
+ export function deepClone<T>(val: T): T {
30
+ return clone(val, true, new Set())
30
31
  }
31
32
 
32
- function clone<T>(val: T, deep = true, seenObjects: any[] = []): T {
33
+ function clone<T>(val: T, deep = true, seenObjects: Set<any>): T {
33
34
  if (val === undefined || val === null || typeof val !== 'object') {
34
35
  return val
35
36
  }
@@ -39,11 +40,11 @@ function clone<T>(val: T, deep = true, seenObjects: any[] = []): T {
39
40
 
40
41
  switch (prototypeIdentifier) {
41
42
  case '[object Array]': {
42
- if (seenObjects.includes(obj)) {
43
+ if (seenObjects.has(obj)) {
43
44
  throw new Error('deepClone: encountered a cyclic object')
44
45
  }
45
46
 
46
- seenObjects.push(obj)
47
+ seenObjects.add(obj)
47
48
 
48
49
  const clonedArray = new Array(obj.length)
49
50
 
@@ -55,7 +56,7 @@ function clone<T>(val: T, deep = true, seenObjects: any[] = []): T {
55
56
  }
56
57
  }
57
58
 
58
- seenObjects.pop()
59
+ seenObjects.delete(obj)
59
60
 
60
61
  return clonedArray as any
61
62
  }
@@ -149,7 +150,10 @@ function clone<T>(val: T, deep = true, seenObjects: any[] = []): T {
149
150
  }
150
151
 
151
152
  case '[object RegExp]': {
152
- return obj
153
+ const clonedRegExp = new RegExp(obj.source, obj.flags)
154
+ clonedRegExp.lastIndex = obj.lastIndex
155
+
156
+ return clonedRegExp as any
153
157
  }
154
158
 
155
159
  case '[object Function]': {
@@ -157,11 +161,11 @@ function clone<T>(val: T, deep = true, seenObjects: any[] = []): T {
157
161
  }
158
162
 
159
163
  case '[object Object]': {
160
- if (seenObjects.includes(obj)) {
161
- throw new Error('deepClone: encountered a cyclic object')
164
+ if (seenObjects.has(obj)) {
165
+ throw new Error('deepClone: encountered a cyclic object.')
162
166
  }
163
167
 
164
- seenObjects.push(obj)
168
+ seenObjects.add(obj)
165
169
 
166
170
  const clonedObj: any = {}
167
171
 
@@ -177,13 +181,13 @@ function clone<T>(val: T, deep = true, seenObjects: any[] = []): T {
177
181
  }
178
182
  }
179
183
 
180
- seenObjects.pop()
184
+ seenObjects.delete(obj)
181
185
 
182
186
  return clonedObj
183
187
  }
184
188
 
185
189
  default: {
186
- throw new Error(`Cloning of type ${prototypeIdentifier} is not supported`)
190
+ throw new Error(`Cloning of type ${prototypeIdentifier} is not supported.`)
187
191
  }
188
192
  }
189
193
  }
@@ -1,7 +1,7 @@
1
1
  import { downloadAndExtractTarball, FileDownloaderCallbacks } from './FileDownloader.js'
2
- import { getAppDataDir, ensureDir, existsSync, remove } from './FileSystem.js'
2
+ import { ensureDir, existsSync, remove } from './FileSystem.js'
3
3
  import { appName } from '../api/Common.js'
4
- import { getAppTempDir, joinPath } from './PathUtilities.js'
4
+ import { getAppDataDir, getAppTempDir, joinPath } from './PathUtilities.js'
5
5
  import { getGlobalOption } from '../api/GlobalOptions.js'
6
6
  import { EasierHttpRequestConfig } from 'easier-http-request'
7
7
 
@@ -41,6 +41,25 @@ export function getAppTempDir(appName: string) {
41
41
  return tempDir
42
42
  }
43
43
 
44
+ export function getAppDataDir(appName: string) {
45
+ let dataDir: string
46
+
47
+ const platform = process.platform
48
+ const homeDir = os.homedir()
49
+
50
+ if (platform == 'win32') {
51
+ dataDir = joinPath(homeDir, 'AppData', 'Local', appName)
52
+ } else if (platform == 'darwin') {
53
+ dataDir = joinPath(homeDir, 'Library', 'Application Support', appName)
54
+ } else if (platform == 'linux') {
55
+ dataDir = joinPath(homeDir, '.local', 'share', appName)
56
+ } else {
57
+ throw new Error(`Unsupport platform ${platform}`)
58
+ }
59
+
60
+ return dataDir
61
+ }
62
+
44
63
  export function joinPath(...paths: string[]) {
45
64
  return NodePath.join(...paths)
46
65
  }
@@ -1,5 +1,3 @@
1
- import { TypedArray } from '../typings/TypedArray.js'
2
-
3
1
  export function includesAnyOf(str: string, substrings: string[]) {
4
2
  return indexOfAnyOf(str, substrings) >= 0
5
3
  }
@@ -1,6 +1,6 @@
1
1
  import { addMissingPunctuationWordsToWordSequence, segmentWordSequence, WordSequence } from '@echogarden/text-segmentation'
2
2
  import { ParagraphBreakType, WhitespaceProcessing } from '../api/Common.js'
3
- import { applyWhitespaceProcessing, isWord, isWordOrEmojiOrSymbolWord, splitToParagraphs } from '../nlp/Segmentation.js'
3
+ import { applyWhitespaceProcessing, includesWordCharacter, isWordOrEmojiOrSymbolWord, splitToParagraphs } from '../nlp/Segmentation.js'
4
4
  import { deepClone } from './ObjectUtilities.js'
5
5
  import { getUTF32Chars } from './StringUtilities.js'
6
6
  import { roundToDigits } from './Utilities.js'
@@ -213,7 +213,7 @@ function replaceSentenceEndersWithinWordsWithMaskingCharacter(transcript: string
213
213
  for (const wordEntry of wordTimeline) {
214
214
  const wordText = wordEntry.text
215
215
 
216
- if (!isWord(wordText)) {
216
+ if (!includesWordCharacter(wordText)) {
217
217
  continue
218
218
  }
219
219
 
@@ -1,10 +1,10 @@
1
1
  import { OperationCallbacks } from '../api/Common.js'
2
- import { splitToParagraphs, wordCharacterRegExp } from '../nlp/Segmentation.js'
2
+ import { isWordOrEmojiOrSymbolWord, splitToParagraphs } from '../nlp/Segmentation.js'
3
3
 
4
4
  export async function fetchAndParseWikipediaArticle(articleName: string, language: string, callbacks: OperationCallbacks) {
5
- const { default: wtf } = await import('wtf_wikipedia')
5
+ const { default: wtfWikipedia } = await import('wtf_wikipedia')
6
6
 
7
- const document = await wtf.fetch(articleName, language)
7
+ const document = await wtfWikipedia.fetch(articleName, language)
8
8
 
9
9
  if (!document) {
10
10
  throw new Error('Error fetching Wikipedia article')
@@ -16,7 +16,7 @@ export async function fetchAndParseWikipediaArticle(articleName: string, languag
16
16
  for (const section of sections) {
17
17
  const sectionTitle = section.title()
18
18
 
19
- if (wordCharacterRegExp.test(sectionTitle)) {
19
+ if (isWordOrEmojiOrSymbolWord(sectionTitle)) {
20
20
  sectionsText.push(sectionTitle)
21
21
  }
22
22
 
@@ -25,7 +25,7 @@ export async function fetchAndParseWikipediaArticle(articleName: string, languag
25
25
  for (const paragraph of sectionParagraphs) {
26
26
  const paragraphText = paragraph
27
27
 
28
- if (wordCharacterRegExp.test(paragraphText)) {
28
+ if (isWordOrEmojiOrSymbolWord(paragraphText)) {
29
29
  sectionsText.push(paragraphText)
30
30
  }
31
31
  }