Pendrokar commited on
Commit
19c8b95
β€’
1 Parent(s): 3d84a14

xVASynth v3 code for English

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitignore +43 -0
  2. LICENSE.md +674 -0
  3. README.md +1 -1
  4. arpabet/custom.json +17 -0
  5. arpabet/xvasynth.json +61 -0
  6. index.html +0 -0
  7. javascript/appLogger.js +39 -0
  8. javascript/arpabet.js +498 -0
  9. javascript/batch.js +1573 -0
  10. javascript/dragdrop_model_install.js +180 -0
  11. javascript/editor.js +2089 -0
  12. javascript/embeddings.js +795 -0
  13. javascript/i18n.js +1070 -0
  14. javascript/nexus.js +983 -0
  15. javascript/outputFiles.js +412 -0
  16. javascript/plugins_manager.js +594 -0
  17. javascript/script.js +1730 -0
  18. javascript/settingsMenu.js +960 -0
  19. javascript/speech2speech.js +379 -0
  20. javascript/style_embeddings.js +335 -0
  21. javascript/textarea.js +580 -0
  22. javascript/totd.js +145 -0
  23. javascript/util.js +740 -0
  24. javascript/workbench.js +497 -0
  25. lib/AbortControllerPolyfill.js +4 -0
  26. lib/OrbitControls.js +1102 -0
  27. lib/Three.min.js +0 -0
  28. lib/Three.sprite.js +1 -0
  29. lib/Three.texture.js +213 -0
  30. lib/TrackballControls.js +778 -0
  31. lib/ffmpeg_normalize/__init__.py +5 -0
  32. lib/ffmpeg_normalize/__main__.py +548 -0
  33. lib/ffmpeg_normalize/_cmd_utils.py +177 -0
  34. lib/ffmpeg_normalize/_errors.py +15 -0
  35. lib/ffmpeg_normalize/_ffmpeg_normalize.py +202 -0
  36. lib/ffmpeg_normalize/_logger.py +83 -0
  37. lib/ffmpeg_normalize/_media_file.py +371 -0
  38. lib/ffmpeg_normalize/_streams.py +344 -0
  39. lib/ffmpeg_normalize/_version.py +1 -0
  40. lib/osutils.js +213 -0
  41. lib/wavesurfer.js +0 -0
  42. lib/xp_error.mp3 +0 -0
  43. main.js +140 -0
  44. package.json +27 -0
  45. patreon.txt +1 -0
  46. plugins.txt +0 -0
  47. plugins/eg_custom_event/frontendPlugin.js +33 -0
  48. plugins/eg_custom_event/main.py +10 -0
  49. plugins/eg_custom_event/plugin.json +28 -0
  50. plugins/test_plugin/custom_event.py +5 -0
.gitignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ xVAEnv
2
+ xVAEnvCPU
3
+ node_modules
4
+ *.wav
5
+ downloads/*
6
+ output/*
7
+ output/*.wav
8
+ output/**/*.wav
9
+ output/*.mp3
10
+ output/**/*.mp3
11
+ output/*.ogg
12
+ output/**/*.ogg
13
+ output/**/*.json
14
+ *.ckpt
15
+ *.todo
16
+ __pycache__/
17
+ .cache/
18
+ *.pyc
19
+ server.log
20
+ server.log.*
21
+ cpython
22
+ xVASynth-win*
23
+ model.rar
24
+ build
25
+ dist
26
+ FASTPITCH_LOADING
27
+ WAVEGLOW_LOADING
28
+ SERVER_STARTING
29
+ DEBUG*
30
+ models
31
+ release-builds
32
+ package-lock.json
33
+ app.log
34
+ batch
35
+ python/wav2vec2/pytorch_model.bin
36
+ python/ffmpeg.exe
37
+ python/xvapitch/base_v1.0.pt
38
+ env*
39
+ embeddings.txt
40
+ *.exe
41
+ *.pt
42
+ *.dll
43
+ *.bin
LICENSE.md ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU General Public License is a free, copyleft license for
11
+ software and other kinds of works.
12
+
13
+ The licenses for most software and other practical works are designed
14
+ to take away your freedom to share and change the works. By contrast,
15
+ the GNU General Public License is intended to guarantee your freedom to
16
+ share and change all versions of a program--to make sure it remains free
17
+ software for all its users. We, the Free Software Foundation, use the
18
+ GNU General Public License for most of our software; it applies also to
19
+ any other work released this way by its authors. You can apply it to
20
+ your programs, too.
21
+
22
+ When we speak of free software, we are referring to freedom, not
23
+ price. Our General Public Licenses are designed to make sure that you
24
+ have the freedom to distribute copies of free software (and charge for
25
+ them if you wish), that you receive source code or can get it if you
26
+ want it, that you can change the software or use pieces of it in new
27
+ free programs, and that you know you can do these things.
28
+
29
+ To protect your rights, we need to prevent others from denying you
30
+ these rights or asking you to surrender the rights. Therefore, you have
31
+ certain responsibilities if you distribute copies of the software, or if
32
+ you modify it: responsibilities to respect the freedom of others.
33
+
34
+ For example, if you distribute copies of such a program, whether
35
+ gratis or for a fee, you must pass on to the recipients the same
36
+ freedoms that you received. You must make sure that they, too, receive
37
+ or can get the source code. And you must show them these terms so they
38
+ know their rights.
39
+
40
+ Developers that use the GNU GPL protect your rights with two steps:
41
+ (1) assert copyright on the software, and (2) offer you this License
42
+ giving you legal permission to copy, distribute and/or modify it.
43
+
44
+ For the developers' and authors' protection, the GPL clearly explains
45
+ that there is no warranty for this free software. For both users' and
46
+ authors' sake, the GPL requires that modified versions be marked as
47
+ changed, so that their problems will not be attributed erroneously to
48
+ authors of previous versions.
49
+
50
+ Some devices are designed to deny users access to install or run
51
+ modified versions of the software inside them, although the manufacturer
52
+ can do so. This is fundamentally incompatible with the aim of
53
+ protecting users' freedom to change the software. The systematic
54
+ pattern of such abuse occurs in the area of products for individuals to
55
+ use, which is precisely where it is most unacceptable. Therefore, we
56
+ have designed this version of the GPL to prohibit the practice for those
57
+ products. If such problems arise substantially in other domains, we
58
+ stand ready to extend this provision to those domains in future versions
59
+ of the GPL, as needed to protect the freedom of users.
60
+
61
+ Finally, every program is threatened constantly by software patents.
62
+ States should not allow patents to restrict development and use of
63
+ software on general-purpose computers, but in those that do, we wish to
64
+ avoid the special danger that patents applied to a free program could
65
+ make it effectively proprietary. To prevent this, the GPL assures that
66
+ patents cannot be used to render the program non-free.
67
+
68
+ The precise terms and conditions for copying, distribution and
69
+ modification follow.
70
+
71
+ TERMS AND CONDITIONS
72
+
73
+ 0. Definitions.
74
+
75
+ "This License" refers to version 3 of the GNU General Public License.
76
+
77
+ "Copyright" also means copyright-like laws that apply to other kinds of
78
+ works, such as semiconductor masks.
79
+
80
+ "The Program" refers to any copyrightable work licensed under this
81
+ License. Each licensee is addressed as "you". "Licensees" and
82
+ "recipients" may be individuals or organizations.
83
+
84
+ To "modify" a work means to copy from or adapt all or part of the work
85
+ in a fashion requiring copyright permission, other than the making of an
86
+ exact copy. The resulting work is called a "modified version" of the
87
+ earlier work or a work "based on" the earlier work.
88
+
89
+ A "covered work" means either the unmodified Program or a work based
90
+ on the Program.
91
+
92
+ To "propagate" a work means to do anything with it that, without
93
+ permission, would make you directly or secondarily liable for
94
+ infringement under applicable copyright law, except executing it on a
95
+ computer or modifying a private copy. Propagation includes copying,
96
+ distribution (with or without modification), making available to the
97
+ public, and in some countries other activities as well.
98
+
99
+ To "convey" a work means any kind of propagation that enables other
100
+ parties to make or receive copies. Mere interaction with a user through
101
+ a computer network, with no transfer of a copy, is not conveying.
102
+
103
+ An interactive user interface displays "Appropriate Legal Notices"
104
+ to the extent that it includes a convenient and prominently visible
105
+ feature that (1) displays an appropriate copyright notice, and (2)
106
+ tells the user that there is no warranty for the work (except to the
107
+ extent that warranties are provided), that licensees may convey the
108
+ work under this License, and how to view a copy of this License. If
109
+ the interface presents a list of user commands or options, such as a
110
+ menu, a prominent item in the list meets this criterion.
111
+
112
+ 1. Source Code.
113
+
114
+ The "source code" for a work means the preferred form of the work
115
+ for making modifications to it. "Object code" means any non-source
116
+ form of a work.
117
+
118
+ A "Standard Interface" means an interface that either is an official
119
+ standard defined by a recognized standards body, or, in the case of
120
+ interfaces specified for a particular programming language, one that
121
+ is widely used among developers working in that language.
122
+
123
+ The "System Libraries" of an executable work include anything, other
124
+ than the work as a whole, that (a) is included in the normal form of
125
+ packaging a Major Component, but which is not part of that Major
126
+ Component, and (b) serves only to enable use of the work with that
127
+ Major Component, or to implement a Standard Interface for which an
128
+ implementation is available to the public in source code form. A
129
+ "Major Component", in this context, means a major essential component
130
+ (kernel, window system, and so on) of the specific operating system
131
+ (if any) on which the executable work runs, or a compiler used to
132
+ produce the work, or an object code interpreter used to run it.
133
+
134
+ The "Corresponding Source" for a work in object code form means all
135
+ the source code needed to generate, install, and (for an executable
136
+ work) run the object code and to modify the work, including scripts to
137
+ control those activities. However, it does not include the work's
138
+ System Libraries, or general-purpose tools or generally available free
139
+ programs which are used unmodified in performing those activities but
140
+ which are not part of the work. For example, Corresponding Source
141
+ includes interface definition files associated with source files for
142
+ the work, and the source code for shared libraries and dynamically
143
+ linked subprograms that the work is specifically designed to require,
144
+ such as by intimate data communication or control flow between those
145
+ subprograms and other parts of the work.
146
+
147
+ The Corresponding Source need not include anything that users
148
+ can regenerate automatically from other parts of the Corresponding
149
+ Source.
150
+
151
+ The Corresponding Source for a work in source code form is that
152
+ same work.
153
+
154
+ 2. Basic Permissions.
155
+
156
+ All rights granted under this License are granted for the term of
157
+ copyright on the Program, and are irrevocable provided the stated
158
+ conditions are met. This License explicitly affirms your unlimited
159
+ permission to run the unmodified Program. The output from running a
160
+ covered work is covered by this License only if the output, given its
161
+ content, constitutes a covered work. This License acknowledges your
162
+ rights of fair use or other equivalent, as provided by copyright law.
163
+
164
+ You may make, run and propagate covered works that you do not
165
+ convey, without conditions so long as your license otherwise remains
166
+ in force. You may convey covered works to others for the sole purpose
167
+ of having them make modifications exclusively for you, or provide you
168
+ with facilities for running those works, provided that you comply with
169
+ the terms of this License in conveying all material for which you do
170
+ not control copyright. Those thus making or running the covered works
171
+ for you must do so exclusively on your behalf, under your direction
172
+ and control, on terms that prohibit them from making any copies of
173
+ your copyrighted material outside their relationship with you.
174
+
175
+ Conveying under any other circumstances is permitted solely under
176
+ the conditions stated below. Sublicensing is not allowed; section 10
177
+ makes it unnecessary.
178
+
179
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180
+
181
+ No covered work shall be deemed part of an effective technological
182
+ measure under any applicable law fulfilling obligations under article
183
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184
+ similar laws prohibiting or restricting circumvention of such
185
+ measures.
186
+
187
+ When you convey a covered work, you waive any legal power to forbid
188
+ circumvention of technological measures to the extent such circumvention
189
+ is effected by exercising rights under this License with respect to
190
+ the covered work, and you disclaim any intention to limit operation or
191
+ modification of the work as a means of enforcing, against the work's
192
+ users, your or third parties' legal rights to forbid circumvention of
193
+ technological measures.
194
+
195
+ 4. Conveying Verbatim Copies.
196
+
197
+ You may convey verbatim copies of the Program's source code as you
198
+ receive it, in any medium, provided that you conspicuously and
199
+ appropriately publish on each copy an appropriate copyright notice;
200
+ keep intact all notices stating that this License and any
201
+ non-permissive terms added in accord with section 7 apply to the code;
202
+ keep intact all notices of the absence of any warranty; and give all
203
+ recipients a copy of this License along with the Program.
204
+
205
+ You may charge any price or no price for each copy that you convey,
206
+ and you may offer support or warranty protection for a fee.
207
+
208
+ 5. Conveying Modified Source Versions.
209
+
210
+ You may convey a work based on the Program, or the modifications to
211
+ produce it from the Program, in the form of source code under the
212
+ terms of section 4, provided that you also meet all of these conditions:
213
+
214
+ a) The work must carry prominent notices stating that you modified
215
+ it, and giving a relevant date.
216
+
217
+ b) The work must carry prominent notices stating that it is
218
+ released under this License and any conditions added under section
219
+ 7. This requirement modifies the requirement in section 4 to
220
+ "keep intact all notices".
221
+
222
+ c) You must license the entire work, as a whole, under this
223
+ License to anyone who comes into possession of a copy. This
224
+ License will therefore apply, along with any applicable section 7
225
+ additional terms, to the whole of the work, and all its parts,
226
+ regardless of how they are packaged. This License gives no
227
+ permission to license the work in any other way, but it does not
228
+ invalidate such permission if you have separately received it.
229
+
230
+ d) If the work has interactive user interfaces, each must display
231
+ Appropriate Legal Notices; however, if the Program has interactive
232
+ interfaces that do not display Appropriate Legal Notices, your
233
+ work need not make them do so.
234
+
235
+ A compilation of a covered work with other separate and independent
236
+ works, which are not by their nature extensions of the covered work,
237
+ and which are not combined with it such as to form a larger program,
238
+ in or on a volume of a storage or distribution medium, is called an
239
+ "aggregate" if the compilation and its resulting copyright are not
240
+ used to limit the access or legal rights of the compilation's users
241
+ beyond what the individual works permit. Inclusion of a covered work
242
+ in an aggregate does not cause this License to apply to the other
243
+ parts of the aggregate.
244
+
245
+ 6. Conveying Non-Source Forms.
246
+
247
+ You may convey a covered work in object code form under the terms
248
+ of sections 4 and 5, provided that you also convey the
249
+ machine-readable Corresponding Source under the terms of this License,
250
+ in one of these ways:
251
+
252
+ a) Convey the object code in, or embodied in, a physical product
253
+ (including a physical distribution medium), accompanied by the
254
+ Corresponding Source fixed on a durable physical medium
255
+ customarily used for software interchange.
256
+
257
+ b) Convey the object code in, or embodied in, a physical product
258
+ (including a physical distribution medium), accompanied by a
259
+ written offer, valid for at least three years and valid for as
260
+ long as you offer spare parts or customer support for that product
261
+ model, to give anyone who possesses the object code either (1) a
262
+ copy of the Corresponding Source for all the software in the
263
+ product that is covered by this License, on a durable physical
264
+ medium customarily used for software interchange, for a price no
265
+ more than your reasonable cost of physically performing this
266
+ conveying of source, or (2) access to copy the
267
+ Corresponding Source from a network server at no charge.
268
+
269
+ c) Convey individual copies of the object code with a copy of the
270
+ written offer to provide the Corresponding Source. This
271
+ alternative is allowed only occasionally and noncommercially, and
272
+ only if you received the object code with such an offer, in accord
273
+ with subsection 6b.
274
+
275
+ d) Convey the object code by offering access from a designated
276
+ place (gratis or for a charge), and offer equivalent access to the
277
+ Corresponding Source in the same way through the same place at no
278
+ further charge. You need not require recipients to copy the
279
+ Corresponding Source along with the object code. If the place to
280
+ copy the object code is a network server, the Corresponding Source
281
+ may be on a different server (operated by you or a third party)
282
+ that supports equivalent copying facilities, provided you maintain
283
+ clear directions next to the object code saying where to find the
284
+ Corresponding Source. Regardless of what server hosts the
285
+ Corresponding Source, you remain obligated to ensure that it is
286
+ available for as long as needed to satisfy these requirements.
287
+
288
+ e) Convey the object code using peer-to-peer transmission, provided
289
+ you inform other peers where the object code and Corresponding
290
+ Source of the work are being offered to the general public at no
291
+ charge under subsection 6d.
292
+
293
+ A separable portion of the object code, whose source code is excluded
294
+ from the Corresponding Source as a System Library, need not be
295
+ included in conveying the object code work.
296
+
297
+ A "User Product" is either (1) a "consumer product", which means any
298
+ tangible personal property which is normally used for personal, family,
299
+ or household purposes, or (2) anything designed or sold for incorporation
300
+ into a dwelling. In determining whether a product is a consumer product,
301
+ doubtful cases shall be resolved in favor of coverage. For a particular
302
+ product received by a particular user, "normally used" refers to a
303
+ typical or common use of that class of product, regardless of the status
304
+ of the particular user or of the way in which the particular user
305
+ actually uses, or expects or is expected to use, the product. A product
306
+ is a consumer product regardless of whether the product has substantial
307
+ commercial, industrial or non-consumer uses, unless such uses represent
308
+ the only significant mode of use of the product.
309
+
310
+ "Installation Information" for a User Product means any methods,
311
+ procedures, authorization keys, or other information required to install
312
+ and execute modified versions of a covered work in that User Product from
313
+ a modified version of its Corresponding Source. The information must
314
+ suffice to ensure that the continued functioning of the modified object
315
+ code is in no case prevented or interfered with solely because
316
+ modification has been made.
317
+
318
+ If you convey an object code work under this section in, or with, or
319
+ specifically for use in, a User Product, and the conveying occurs as
320
+ part of a transaction in which the right of possession and use of the
321
+ User Product is transferred to the recipient in perpetuity or for a
322
+ fixed term (regardless of how the transaction is characterized), the
323
+ Corresponding Source conveyed under this section must be accompanied
324
+ by the Installation Information. But this requirement does not apply
325
+ if neither you nor any third party retains the ability to install
326
+ modified object code on the User Product (for example, the work has
327
+ been installed in ROM).
328
+
329
+ The requirement to provide Installation Information does not include a
330
+ requirement to continue to provide support service, warranty, or updates
331
+ for a work that has been modified or installed by the recipient, or for
332
+ the User Product in which it has been modified or installed. Access to a
333
+ network may be denied when the modification itself materially and
334
+ adversely affects the operation of the network or violates the rules and
335
+ protocols for communication across the network.
336
+
337
+ Corresponding Source conveyed, and Installation Information provided,
338
+ in accord with this section must be in a format that is publicly
339
+ documented (and with an implementation available to the public in
340
+ source code form), and must require no special password or key for
341
+ unpacking, reading or copying.
342
+
343
+ 7. Additional Terms.
344
+
345
+ "Additional permissions" are terms that supplement the terms of this
346
+ License by making exceptions from one or more of its conditions.
347
+ Additional permissions that are applicable to the entire Program shall
348
+ be treated as though they were included in this License, to the extent
349
+ that they are valid under applicable law. If additional permissions
350
+ apply only to part of the Program, that part may be used separately
351
+ under those permissions, but the entire Program remains governed by
352
+ this License without regard to the additional permissions.
353
+
354
+ When you convey a copy of a covered work, you may at your option
355
+ remove any additional permissions from that copy, or from any part of
356
+ it. (Additional permissions may be written to require their own
357
+ removal in certain cases when you modify the work.) You may place
358
+ additional permissions on material, added by you to a covered work,
359
+ for which you have or can give appropriate copyright permission.
360
+
361
+ Notwithstanding any other provision of this License, for material you
362
+ add to a covered work, you may (if authorized by the copyright holders of
363
+ that material) supplement the terms of this License with terms:
364
+
365
+ a) Disclaiming warranty or limiting liability differently from the
366
+ terms of sections 15 and 16 of this License; or
367
+
368
+ b) Requiring preservation of specified reasonable legal notices or
369
+ author attributions in that material or in the Appropriate Legal
370
+ Notices displayed by works containing it; or
371
+
372
+ c) Prohibiting misrepresentation of the origin of that material, or
373
+ requiring that modified versions of such material be marked in
374
+ reasonable ways as different from the original version; or
375
+
376
+ d) Limiting the use for publicity purposes of names of licensors or
377
+ authors of the material; or
378
+
379
+ e) Declining to grant rights under trademark law for use of some
380
+ trade names, trademarks, or service marks; or
381
+
382
+ f) Requiring indemnification of licensors and authors of that
383
+ material by anyone who conveys the material (or modified versions of
384
+ it) with contractual assumptions of liability to the recipient, for
385
+ any liability that these contractual assumptions directly impose on
386
+ those licensors and authors.
387
+
388
+ All other non-permissive additional terms are considered "further
389
+ restrictions" within the meaning of section 10. If the Program as you
390
+ received it, or any part of it, contains a notice stating that it is
391
+ governed by this License along with a term that is a further
392
+ restriction, you may remove that term. If a license document contains
393
+ a further restriction but permits relicensing or conveying under this
394
+ License, you may add to a covered work material governed by the terms
395
+ of that license document, provided that the further restriction does
396
+ not survive such relicensing or conveying.
397
+
398
+ If you add terms to a covered work in accord with this section, you
399
+ must place, in the relevant source files, a statement of the
400
+ additional terms that apply to those files, or a notice indicating
401
+ where to find the applicable terms.
402
+
403
+ Additional terms, permissive or non-permissive, may be stated in the
404
+ form of a separately written license, or stated as exceptions;
405
+ the above requirements apply either way.
406
+
407
+ 8. Termination.
408
+
409
+ You may not propagate or modify a covered work except as expressly
410
+ provided under this License. Any attempt otherwise to propagate or
411
+ modify it is void, and will automatically terminate your rights under
412
+ this License (including any patent licenses granted under the third
413
+ paragraph of section 11).
414
+
415
+ However, if you cease all violation of this License, then your
416
+ license from a particular copyright holder is reinstated (a)
417
+ provisionally, unless and until the copyright holder explicitly and
418
+ finally terminates your license, and (b) permanently, if the copyright
419
+ holder fails to notify you of the violation by some reasonable means
420
+ prior to 60 days after the cessation.
421
+
422
+ Moreover, your license from a particular copyright holder is
423
+ reinstated permanently if the copyright holder notifies you of the
424
+ violation by some reasonable means, this is the first time you have
425
+ received notice of violation of this License (for any work) from that
426
+ copyright holder, and you cure the violation prior to 30 days after
427
+ your receipt of the notice.
428
+
429
+ Termination of your rights under this section does not terminate the
430
+ licenses of parties who have received copies or rights from you under
431
+ this License. If your rights have been terminated and not permanently
432
+ reinstated, you do not qualify to receive new licenses for the same
433
+ material under section 10.
434
+
435
+ 9. Acceptance Not Required for Having Copies.
436
+
437
+ You are not required to accept this License in order to receive or
438
+ run a copy of the Program. Ancillary propagation of a covered work
439
+ occurring solely as a consequence of using peer-to-peer transmission
440
+ to receive a copy likewise does not require acceptance. However,
441
+ nothing other than this License grants you permission to propagate or
442
+ modify any covered work. These actions infringe copyright if you do
443
+ not accept this License. Therefore, by modifying or propagating a
444
+ covered work, you indicate your acceptance of this License to do so.
445
+
446
+ 10. Automatic Licensing of Downstream Recipients.
447
+
448
+ Each time you convey a covered work, the recipient automatically
449
+ receives a license from the original licensors, to run, modify and
450
+ propagate that work, subject to this License. You are not responsible
451
+ for enforcing compliance by third parties with this License.
452
+
453
+ An "entity transaction" is a transaction transferring control of an
454
+ organization, or substantially all assets of one, or subdividing an
455
+ organization, or merging organizations. If propagation of a covered
456
+ work results from an entity transaction, each party to that
457
+ transaction who receives a copy of the work also receives whatever
458
+ licenses to the work the party's predecessor in interest had or could
459
+ give under the previous paragraph, plus a right to possession of the
460
+ Corresponding Source of the work from the predecessor in interest, if
461
+ the predecessor has it or can get it with reasonable efforts.
462
+
463
+ You may not impose any further restrictions on the exercise of the
464
+ rights granted or affirmed under this License. For example, you may
465
+ not impose a license fee, royalty, or other charge for exercise of
466
+ rights granted under this License, and you may not initiate litigation
467
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
468
+ any patent claim is infringed by making, using, selling, offering for
469
+ sale, or importing the Program or any portion of it.
470
+
471
+ 11. Patents.
472
+
473
+ A "contributor" is a copyright holder who authorizes use under this
474
+ License of the Program or a work on which the Program is based. The
475
+ work thus licensed is called the contributor's "contributor version".
476
+
477
+ A contributor's "essential patent claims" are all patent claims
478
+ owned or controlled by the contributor, whether already acquired or
479
+ hereafter acquired, that would be infringed by some manner, permitted
480
+ by this License, of making, using, or selling its contributor version,
481
+ but do not include claims that would be infringed only as a
482
+ consequence of further modification of the contributor version. For
483
+ purposes of this definition, "control" includes the right to grant
484
+ patent sublicenses in a manner consistent with the requirements of
485
+ this License.
486
+
487
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
488
+ patent license under the contributor's essential patent claims, to
489
+ make, use, sell, offer for sale, import and otherwise run, modify and
490
+ propagate the contents of its contributor version.
491
+
492
+ In the following three paragraphs, a "patent license" is any express
493
+ agreement or commitment, however denominated, not to enforce a patent
494
+ (such as an express permission to practice a patent or covenant not to
495
+ sue for patent infringement). To "grant" such a patent license to a
496
+ party means to make such an agreement or commitment not to enforce a
497
+ patent against the party.
498
+
499
+ If you convey a covered work, knowingly relying on a patent license,
500
+ and the Corresponding Source of the work is not available for anyone
501
+ to copy, free of charge and under the terms of this License, through a
502
+ publicly available network server or other readily accessible means,
503
+ then you must either (1) cause the Corresponding Source to be so
504
+ available, or (2) arrange to deprive yourself of the benefit of the
505
+ patent license for this particular work, or (3) arrange, in a manner
506
+ consistent with the requirements of this License, to extend the patent
507
+ license to downstream recipients. "Knowingly relying" means you have
508
+ actual knowledge that, but for the patent license, your conveying the
509
+ covered work in a country, or your recipient's use of the covered work
510
+ in a country, would infringe one or more identifiable patents in that
511
+ country that you have reason to believe are valid.
512
+
513
+ If, pursuant to or in connection with a single transaction or
514
+ arrangement, you convey, or propagate by procuring conveyance of, a
515
+ covered work, and grant a patent license to some of the parties
516
+ receiving the covered work authorizing them to use, propagate, modify
517
+ or convey a specific copy of the covered work, then the patent license
518
+ you grant is automatically extended to all recipients of the covered
519
+ work and works based on it.
520
+
521
+ A patent license is "discriminatory" if it does not include within
522
+ the scope of its coverage, prohibits the exercise of, or is
523
+ conditioned on the non-exercise of one or more of the rights that are
524
+ specifically granted under this License. You may not convey a covered
525
+ work if you are a party to an arrangement with a third party that is
526
+ in the business of distributing software, under which you make payment
527
+ to the third party based on the extent of your activity of conveying
528
+ the work, and under which the third party grants, to any of the
529
+ parties who would receive the covered work from you, a discriminatory
530
+ patent license (a) in connection with copies of the covered work
531
+ conveyed by you (or copies made from those copies), or (b) primarily
532
+ for and in connection with specific products or compilations that
533
+ contain the covered work, unless you entered into that arrangement,
534
+ or that patent license was granted, prior to 28 March 2007.
535
+
536
+ Nothing in this License shall be construed as excluding or limiting
537
+ any implied license or other defenses to infringement that may
538
+ otherwise be available to you under applicable patent law.
539
+
540
+ 12. No Surrender of Others' Freedom.
541
+
542
+ If conditions are imposed on you (whether by court order, agreement or
543
+ otherwise) that contradict the conditions of this License, they do not
544
+ excuse you from the conditions of this License. If you cannot convey a
545
+ covered work so as to satisfy simultaneously your obligations under this
546
+ License and any other pertinent obligations, then as a consequence you may
547
+ not convey it at all. For example, if you agree to terms that obligate you
548
+ to collect a royalty for further conveying from those to whom you convey
549
+ the Program, the only way you could satisfy both those terms and this
550
+ License would be to refrain entirely from conveying the Program.
551
+
552
+ 13. Use with the GNU Affero General Public License.
553
+
554
+ Notwithstanding any other provision of this License, you have
555
+ permission to link or combine any covered work with a work licensed
556
+ under version 3 of the GNU Affero General Public License into a single
557
+ combined work, and to convey the resulting work. The terms of this
558
+ License will continue to apply to the part which is the covered work,
559
+ but the special requirements of the GNU Affero General Public License,
560
+ section 13, concerning interaction through a network will apply to the
561
+ combination as such.
562
+
563
+ 14. Revised Versions of this License.
564
+
565
+ The Free Software Foundation may publish revised and/or new versions of
566
+ the GNU General Public License from time to time. Such new versions will
567
+ be similar in spirit to the present version, but may differ in detail to
568
+ address new problems or concerns.
569
+
570
+ Each version is given a distinguishing version number. If the
571
+ Program specifies that a certain numbered version of the GNU General
572
+ Public License "or any later version" applies to it, you have the
573
+ option of following the terms and conditions either of that numbered
574
+ version or of any later version published by the Free Software
575
+ Foundation. If the Program does not specify a version number of the
576
+ GNU General Public License, you may choose any version ever published
577
+ by the Free Software Foundation.
578
+
579
+ If the Program specifies that a proxy can decide which future
580
+ versions of the GNU General Public License can be used, that proxy's
581
+ public statement of acceptance of a version permanently authorizes you
582
+ to choose that version for the Program.
583
+
584
+ Later license versions may give you additional or different
585
+ permissions. However, no additional obligations are imposed on any
586
+ author or copyright holder as a result of your choosing to follow a
587
+ later version.
588
+
589
+ 15. Disclaimer of Warranty.
590
+
591
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599
+
600
+ 16. Limitation of Liability.
601
+
602
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604
+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606
+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607
+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610
+ SUCH DAMAGES.
611
+
612
+ 17. Interpretation of Sections 15 and 16.
613
+
614
+ If the disclaimer of warranty and limitation of liability provided
615
+ above cannot be given local legal effect according to their terms,
616
+ reviewing courts shall apply local law that most closely approximates
617
+ an absolute waiver of all civil liability in connection with the
618
+ Program, unless a warranty or assumption of liability accompanies a
619
+ copy of the Program in return for a fee.
620
+
621
+ END OF TERMS AND CONDITIONS
622
+
623
+ How to Apply These Terms to Your New Programs
624
+
625
+ If you develop a new program, and you want it to be of the greatest
626
+ possible use to the public, the best way to achieve this is to make it
627
+ free software which everyone can redistribute and change under these terms.
628
+
629
+ To do so, attach the following notices to the program. It is safest
630
+ to attach them to the start of each source file to most effectively
631
+ state the exclusion of warranty; and each file should have at least
632
+ the "copyright" line and a pointer to where the full notice is found.
633
+
634
+ <one line to give the program's name and a brief idea of what it does.>
635
+ Copyright (C) <year> <name of author>
636
+
637
+ This program is free software: you can redistribute it and/or modify
638
+ it under the terms of the GNU General Public License as published by
639
+ the Free Software Foundation, either version 3 of the License, or
640
+ (at your option) any later version.
641
+
642
+ This program is distributed in the hope that it will be useful,
643
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
644
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645
+ GNU General Public License for more details.
646
+
647
+ You should have received a copy of the GNU General Public License
648
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
649
+
650
+ Also add information on how to contact you by electronic and paper mail.
651
+
652
+ If the program does terminal interaction, make it output a short
653
+ notice like this when it starts in an interactive mode:
654
+
655
+ <program> Copyright (C) <year> <name of author>
656
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657
+ This is free software, and you are welcome to redistribute it
658
+ under certain conditions; type `show c' for details.
659
+
660
+ The hypothetical commands `show w' and `show c' should show the appropriate
661
+ parts of the General Public License. Of course, your program's commands
662
+ might be different; for a GUI interface, you would use an "about box".
663
+
664
+ You should also get your employer (if you work as a programmer) or school,
665
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
666
+ For more information on this, and how to apply and follow the GNU GPL, see
667
+ <https://www.gnu.org/licenses/>.
668
+
669
+ The GNU General Public License does not permit incorporating your program
670
+ into proprietary programs. If your program is a subroutine library, you
671
+ may consider it more useful to permit linking proprietary applications with
672
+ the library. If this is what you want to do, use the GNU Lesser General
673
+ Public License instead of this License. But first, please read
674
+ <https://www.gnu.org/licenses/why-not-lgpl.html>.
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: gray
5
  colorTo: gray
6
  sdk: gradio
7
  python_version: 3.9
8
- app_file: app.py
9
  tags:
10
  - tts
11
  - t2s
 
5
  colorTo: gray
6
  sdk: gradio
7
  python_version: 3.9
8
+ app_file: server.py
9
  tags:
10
  - tts
11
  - t2s
arpabet/custom.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Custom Dictionary",
3
+ "description": "Your own custom dictionary",
4
+ "version": "1.0.0",
5
+ "author": "Your name",
6
+ "nexusLink": null,
7
+ "data": {
8
+ "stuff": {
9
+ "enabled": false,
10
+ "arpabet": "S T AA0 F"
11
+ },
12
+ "things": {
13
+ "enabled": false,
14
+ "arpabet": "T HH IH N G S"
15
+ }
16
+ }
17
+ }
arpabet/xvasynth.json ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "xVASynth Terminology",
3
+ "description": "Some technical words specific to xVASynth related tech",
4
+ "version": "1.0.0",
5
+ "author": "Dan Ruta",
6
+ "nexusLink": null,
7
+ "data": {
8
+ "xvasynth": {
9
+ "enabled": true,
10
+ "arpabet": "EH1 G S V EY0 EY0 IH0 S IH0 N TH"
11
+ },
12
+ "model": {
13
+ "enabled": true,
14
+ "arpabet": "M AA1 D AH0 L"
15
+ },
16
+ "hifi": {
17
+ "enabled": true,
18
+ "arpabet": "HH AA0 IH0 F AA0 IH0"
19
+ },
20
+ "fastpitch": {
21
+ "enabled": true,
22
+ "arpabet": "F AE1 S T P IH0 T CH"
23
+ },
24
+ "tacotron": {
25
+ "enabled": true,
26
+ "arpabet": "T AA0 K OW0 T R OW0 N"
27
+ },
28
+ "ai": {
29
+ "enabled": true,
30
+ "arpabet": "EY1 IY0 AA0 IY0"
31
+ },
32
+ "cmudict": {
33
+ "enabled": true,
34
+ "arpabet": "S IY1 IY1 EH0 M Y UW1 D IH1 K T"
35
+ },
36
+ "ffmpeg": {
37
+ "enabled": true,
38
+ "arpabet": "EH1 F } { EH1 F } { EH0 M P EH0 G"
39
+ },
40
+ "vram": {
41
+ "enabled": true,
42
+ "arpabet": "V IY0 R AE1 M"
43
+ },
44
+ "nvidia": {
45
+ "enabled": true,
46
+ "arpabet": "EH0 N V IH1 D IY0 AA1"
47
+ },
48
+ "xvatrainer": {
49
+ "enabled": true,
50
+ "arpabet": "EH1 G S V EY0 IH0 } { T R EY1 N ER0"
51
+ },
52
+ "gpu": {
53
+ "enabled": true,
54
+ "arpabet": "JH IY1 P IY1 Y UW1"
55
+ },
56
+ "video": {
57
+ "enabled": true,
58
+ "arpabet": "V IY0 D IY0 OW0"
59
+ }
60
+ }
61
+ }
index.html ADDED
The diff for this file is too large to render. See raw diff
 
javascript/appLogger.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ const fs = require("fs")
4
+
5
+ class xVAAppLogger {
6
+
7
+ constructor (fileLocation, appVersion) {
8
+ this.lines = []
9
+ this.fileLocation = fileLocation
10
+
11
+ if (fs.existsSync(fileLocation)) {
12
+ const data = fs.readFileSync(fileLocation, "utf8")
13
+ this.lines = data.split("\n")
14
+ }
15
+
16
+ this.prefix = ""
17
+ this.log(`New session - ${appVersion}`)
18
+ }
19
+
20
+ log (message) {
21
+ this.lines.push(`${(new Date()).toJSON().replace("T", "_")} |${this.prefix.length?` [${this.prefix}]:`:""} ${message}`)
22
+
23
+ if (this.lines.length>1000) {
24
+ this.lines = this.lines.slice(this.lines.length-1000, this.lines.length)
25
+ }
26
+
27
+ fs.writeFileSync(this.fileLocation, this.lines.join("\n"), "utf8")
28
+ }
29
+
30
+ print (message) {
31
+ console.log(`${this.prefix.length?` [${this.prefix}]:`:""} ${message}`)
32
+ }
33
+
34
+ setPrefix (prefix) {
35
+ this.prefix = prefix
36
+ }
37
+ }
38
+
39
+ exports.xVAAppLogger = xVAAppLogger
javascript/arpabet.js ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ window.arpabetMenuState = {
4
+ currentDict: undefined,
5
+ dictionaries: {},
6
+ paginationIndex: 0,
7
+ totalPages: 0,
8
+ clickedRecord: undefined,
9
+ skipRefresh: false,
10
+ hasInitialised: false,
11
+ isRefreshing: false,
12
+ hasChangedARPAbet: false
13
+ }
14
+
15
+ window.ARPAbetSymbols = ['AA0', 'AA1', 'AA2', 'AA', 'AE0', 'AE1', 'AE2', 'AE', 'AH0', 'AH1', 'AH2', 'AH',
16
+ 'AO0', 'AO1', 'AO2', 'AO', 'AW0', 'AW1', 'AW2', 'AW', 'AY0', 'AY1', 'AY2', 'AY',
17
+ 'B', 'CH', 'D', 'DH', 'EH0', 'EH1', 'EH2', 'EH', 'ER0', 'ER1', 'ER2', 'ER',
18
+ 'EY0', 'EY1', 'EY2', 'EY', 'F', 'G', 'HH', 'IH0', 'IH1', 'IH2', 'IH', 'IY0', 'IY1',
19
+ 'IY2', 'IY', 'JH', 'K', 'L', 'M', 'N', 'NG', 'OW0', 'OW1', 'OW2', 'OW', 'OY0',
20
+ 'OY1', 'OY2', 'OY', 'P', 'R', 'S', 'SH', 'T', 'TH', 'UH0', 'UH1', 'UH2', 'UH',
21
+ 'UW0', 'UW1', 'UW2', 'UW', 'V', 'W', 'Y', 'Z', 'ZH', ,"}","{", "_",
22
+ "AX", "AXR", "IX", "UX", "DX", "EL", "EM", "EN0", "EN1", "EN2", "EN", "NX", "Q", "WH",
23
+ "RRR", "HR", "OE", "RH", "TS", "RR", "UU", "OO", "KH", "SJ", "HJ", "BR",
24
+
25
+ "A1", "A2", "A3", "A4", "A5", "AI1", "AI2", "AI3", "AI4", "AI5", "AIR2", "AIR3", "AIR4", "AN1", "AN2", "AN3", "AN4", "AN5", "ANG1", "ANG2", "ANG3", "ANG4", "ANG5", "ANGR2", "ANGR3", "ANGR4", "ANR1", "ANR3", "ANR4", "AO1", "AO2", "AO3", "AO4", "AO5", "AOR1", "AOR2", "AOR3", "AOR4", "AOR5", "AR2", "AR3", "AR4", "AR5", "E1", "E2", "E3", "E4", "E5", "EI1", "EI2", "EI3", "EI4", "EI5", "EIR4", "EN1", "EN2", "EN3", "EN4", "EN5", "ENG1", "ENG2", "ENG3", "ENG4", "ENG5", "ENGR1", "ENGR4", "ENR1", "ENR2", "ENR3", "ENR4", "ENR5", "ER1", "ER2", "ER3", "ER4", "ER5", "I1", "I2", "I3", "I4", "I5", "IA1", "IA2", "IA3", "IA4", "IA5", "IAN1", "IAN2", "IAN3", "IAN4", "IAN5", "IANG1", "IANG2", "IANG3", "IANG4", "IANG5", "IANGR2", "IANR1", "IANR2", "IANR3", "IANR4", "IANR5", "IAO1", "IAO2", "IAO3", "IAO4", "IAO5", "IAOR1", "IAOR2", "IAOR3", "IAOR4", "IAR1", "IAR4", "IE1", "IE2", "IE3", "IE4", "IE5", "IN1", "IN2", "IN3", "IN4", "IN5", "ING1", "ING2", "ING3", "ING4", "ING5", "INGR2", "INGR4", "INR1", "INR4", "IONG1", "IONG2", "IONG3", "IONG4", "IONG5", "IR1", "IR3", "IR4", "IU1", "IU2", "IU3", "IU4", "IU5", "IUR1", "IUR2", "O1", "O2", "O3", "O4", "O5", "ONG1", "ONG2", "ONG3", "ONG4", "ONG5", "OR1", "OR2", "OU1", "OU2", "OU3", "OU4", "OU5", "OUR2", "OUR3", "OUR4", "OUR5", "U1", "U2", "U3", "U4", "U5", "UA1", "UA2", "UA3", "UA4", "UA5", "UAI1", "UAI2", "UAI3", "UAI4", "UAIR4", "UAIR5", "UAN1", "UAN2", "UAN3", "UAN4", "UAN5", "UANG1", "UANG2", "UANG3", "UANG4", "UANG5", "UANR1", "UANR2", "UANR3", "UANR4", "UAR1", "UAR2", "UAR4", "UE1", "UE2", "UE3", "UE4", "UE5", "UER2", "UER3", "UI1", "UI2", "UI3", "UI4", "UI5", "UIR1", "UIR2", "UIR3", "UIR4", "UN1", "UN2", "UN3", "UN4", "UN5", "UNR1", "UNR2", "UNR3", "UNR4", "UO1", "UO2", "UO3", "UO4", "UO5", "UOR1", "UOR2", "UOR3", "UOR5", "UR1", "UR2", "UR4", "UR5", "V2", "V3", "V4", "V5", "VE4", "VR3", "WA1", "WA2", "WA3", "WA4", "WA5", "WAI1", "WAI2", "WAI3", "WAI4", "WAN1", "WAN2", "WAN3", "WAN4", "WAN5", "WANG1", "WANG2", "WANG3", "WANG4", "WANG5", "WANGR2", "WANGR4", "WANR2", "WANR4", "WANR5", "WEI1", "WEI2", "WEI3", "WEI4", "WEI5", "WEIR1", "WEIR2", "WEIR3", "WEIR4", "WEIR5", "WEN1", "WEN2", "WEN3", "WEN4", "WEN5", "WENG1", "WENG2", "WENG3", "WENG4", "WENR2", "WO1", "WO2", "WO3", "WO4", "WO5", "WU1", "WU2", "WU3", "WU4", "WU5", "WUR3", "YA1", "YA2", "YA3", "YA4", "YA5", "YAN1", "YAN2", "YAN3", "YAN4", "YANG1", "YANG2", "YANG3", "YANG4", "YANG5", "YANGR4", "YANR3", "YAO1", "YAO2", "YAO3", "YAO4", "YAO5", "YE1", "YE2", "YE3", "YE4", "YE5", "YER4", "YI1", "YI2", "YI3", "YI4", "YI5", "YIN1", "YIN2", "YIN3", "YIN4", "YIN5", "YING1", "YING2", "YING3", "YING4", "YING5", "YINGR1", "YINGR2", "YINGR3", "YIR4", "YO1", "YO3", "YONG1", "YONG2", "YONG3", "YONG4", "YONG5", "YONGR3", "YOU1", "YOU2", "YOU3", "YOU4", "YOU5", "YOUR2", "YOUR3", "YOUR4", "YU1", "YU2", "YU3", "YU4", "YU5", "YUAN1", "YUAN2", "YUAN3", "YUAN4", "YUAN5", "YUANR2", "YUANR4", "YUE1", "YUE2", "YUE4", "YUE5", "YUER4", "YUN1", "YUN2", "YUN3", "YUN4",
26
+
27
+ "@BREATHE_IN", "@BREATHE_OUT", "@LAUGH", "@GIGGLE", "@SIGH", "@COUGH", "@AHEM", "@SNEEZE", "@WHISTLE", "@UGH", "@HMM", "@GASP", "@AAH", "@GRUNT", "@YAWN", "@SNIFF"
28
+
29
+ ]
30
+ // window.ARPAbetSymbols = [
31
+ // 'AA', 'AA0', 'AA1', 'AA2', 'AE', 'AE0', 'AE1', 'AE2', 'AH', 'AH0', 'AH1', 'AH2',
32
+ // 'AO', 'AO0', 'AO1', 'AO2', 'AW', 'AW0', 'AW1', 'AW2', 'AY', 'AY0', 'AY1', 'AY2',
33
+ // 'B', 'CH', 'D', 'DH', 'EH', 'EH0', 'EH1', 'EH2', 'ER', 'ER0', 'ER1', 'ER2', 'EY',
34
+ // 'EY0', 'EY1', 'EY2', 'F', 'G', 'HH', 'IH', 'IH0', 'IH1', 'IH2', 'IY', 'IY0', 'IY1',
35
+ // 'IY2', 'JH', 'K', 'L', 'M', 'N', 'NG', 'OW', 'OW0', 'OW1', 'OW2', 'OY', 'OY0',
36
+ // 'OY1', 'OY2', 'P', 'R', 'S', 'SH', 'T', 'TH', 'UH', 'UH0', 'UH1', 'UH2', 'UW',
37
+ // 'UW0', 'UW1', 'UW2', 'V', 'W', 'Y', 'Z', 'ZH'
38
+ // ,"}","{", "_"
39
+ // ]
40
+
41
+ window.refreshDictionariesList = () => {
42
+
43
+ return new Promise(resolve => {
44
+
45
+ // Don't spam with changes when the menu isn't open
46
+ // if (arpabetModal.parentElement.style.display!="flex" && window.arpabetMenuState.hasInitialised) {
47
+ if (arpabetModal.parentElement.style.display!="flex") {
48
+ return
49
+ }
50
+
51
+ window.arpabetMenuState.hasInitialised = true
52
+ if (window.arpabetMenuState.isRefreshing) {
53
+ resolve()
54
+ return
55
+ }
56
+ window.arpabetMenuState.isRefreshing = true
57
+
58
+ if (window.arpabetMenuState.skipRefresh) {
59
+ resolve()
60
+ return
61
+ }
62
+
63
+ spinnerModal(window.i18n.LOADING_DICTIONARIES)
64
+ window.arpabetMenuState.dictionaries = {}
65
+ arpabet_dicts_list.innerHTML = ""
66
+
67
+ const jsonFiles = fs.readdirSync(`${window.path}/arpabet`).filter(fname => fname.includes(".json"))
68
+
69
+ const readFile = (fileCounter) => {
70
+
71
+ const fname = jsonFiles[fileCounter]
72
+ if (!fname.includes(".json")) {
73
+ if ((fileCounter+1)<jsonFiles.length) {
74
+ readFile(fileCounter+1)
75
+ } else {
76
+ window.arpabetRunSearch()
77
+ window.arpabetMenuState.isRefreshing = false
78
+ closeModal(undefined, arpabetContainer)
79
+ resolve()
80
+ }
81
+ return
82
+ }
83
+ const dictId = fname.replace(".json", "")
84
+
85
+ fs.readFile(`${window.path}/arpabet/${fname}`, "utf8", (err, data) => {
86
+ const jsonData = JSON.parse(data)
87
+
88
+ const dictButton = createElem("button", jsonData.title)
89
+ dictButton.title = jsonData.description
90
+ dictButton.style.background = window.currentGame ? `#${window.currentGame.themeColourPrimary}` : "#aaa"
91
+ arpabet_dicts_list.appendChild(dictButton)
92
+
93
+ window.arpabetMenuState.dictionaries[dictId] = jsonData
94
+
95
+ dictButton.addEventListener("click", ()=>handleDictClick(dictId))
96
+
97
+ if ((fileCounter+1)<jsonFiles.length) {
98
+ readFile(fileCounter+1)
99
+ } else {
100
+ window.arpabetRunSearch()
101
+ window.arpabetMenuState.isRefreshing = false
102
+ closeModal(undefined, arpabetContainer)
103
+ resolve()
104
+ }
105
+ })
106
+ }
107
+ if (jsonFiles.length) {
108
+ readFile(0)
109
+ } else {
110
+ window.arpabetMenuState.isRefreshing = false
111
+ closeModal(undefined, arpabetContainer)
112
+ resolve()
113
+ }
114
+ })
115
+ }
116
+
117
+ window.handleDictClick = (dictId) => {
118
+
119
+ if (window.arpabetMenuState.currentDict==dictId) {
120
+ return
121
+ }
122
+ arpabet_enableall_button.disabled = false
123
+ arpabet_disableall_button.disabled = false
124
+ window.arpabetMenuState.currentDict = dictId
125
+ window.arpabetMenuState.paginationIndex = 0
126
+ window.arpabetMenuState.totalPages = 0
127
+
128
+ arpabet_word_search_input.value = ""
129
+ window.arpabetRunSearch()
130
+ window.refreshDictWordList()
131
+ }
132
+
133
+ window.refreshDictWordList = () => {
134
+
135
+ const dictId = window.arpabetMenuState.currentDict
136
+ arpabetWordsListContainer.innerHTML = ""
137
+
138
+ const wordKeys = Object.keys(window.arpabetMenuState.dictionaries[dictId].filteredData)
139
+ let startIndex = window.arpabetMenuState.paginationIndex*window.userSettings.arpabet_paginationSize
140
+ const endIndex = Math.min(startIndex+window.userSettings.arpabet_paginationSize, wordKeys.length)
141
+
142
+ window.arpabetMenuState.totalPages = Math.ceil(wordKeys.length/window.userSettings.arpabet_paginationSize)
143
+ arpabet_pagination_numbers.innerHTML = window.i18n.PAGINATION_X_OF_Y.replace("_1", window.arpabetMenuState.paginationIndex+1).replace("_2", window.arpabetMenuState.totalPages)
144
+
145
+ for (let i=startIndex; i<endIndex; i++) {
146
+ const data = window.arpabetMenuState.dictionaries[dictId].filteredData[wordKeys[i]]
147
+ const word = wordKeys[i]
148
+
149
+ const rowElem = createElem("div.arpabetRow")
150
+ const ckbx = createElem("input.arpabetRowItem", {type: "checkbox"})
151
+ ckbx.checked = data.enabled
152
+ ckbx.style.marginTop = 0
153
+ ckbx.addEventListener("click", () => {
154
+ window.arpabetMenuState.dictionaries[dictId].data[wordKeys[i]].enabled = ckbx.checked
155
+ window.arpabetMenuState.skipRefresh = true
156
+ window.saveARPAbetDict(dictId)
157
+ window.arpabetMenuState.hasChangedARPAbet = true
158
+ setTimeout(() => window.arpabetMenuState.skipRefresh = false, 1000)
159
+ })
160
+
161
+ const deleteButton = createElem("button.smallButton.arpabetRowItem", window.i18n.DELETE)
162
+ deleteButton.style.background = window.currentGame ? `#${window.currentGame.themeColourPrimary}` : "#aaa"
163
+ deleteButton.addEventListener("click", () => {
164
+ window.confirmModal(window.i18n.ARPABET_CONFIRM_DELETE_WORD.replace("_1", word)).then(response => {
165
+ if (response) {
166
+ setTimeout(() => {
167
+ delete window.arpabetMenuState.dictionaries[dictId].data[word]
168
+ delete window.arpabetMenuState.dictionaries[dictId].filteredData[word]
169
+ window.saveARPAbetDict(dictId)
170
+ window.refreshDictWordList()
171
+ }, 210)
172
+ }
173
+ })
174
+ })
175
+
176
+ const wordElem = createElem("div.arpabetRowItem", word)
177
+ wordElem.title = word
178
+
179
+ const arpabetElem = createElem("div.arpabetRowItem", data.arpabet)
180
+ arpabetElem.title = data.arpabet
181
+
182
+
183
+ rowElem.appendChild(createElem("div.arpabetRowItem", ckbx))
184
+ rowElem.appendChild(createElem("div.arpabetRowItem", deleteButton))
185
+ rowElem.appendChild(wordElem)
186
+ rowElem.appendChild(arpabetElem)
187
+
188
+ rowElem.addEventListener("click", () => {
189
+ window.arpabetMenuState.clickedRecord = {elem: rowElem, word}
190
+ arpabet_word_input.value = word
191
+ arpabet_arpabet_input.value = data.arpabet
192
+ })
193
+
194
+ arpabetWordsListContainer.appendChild(rowElem)
195
+ }
196
+ }
197
+
198
+ window.saveARPAbetDict = (dictId) => {
199
+
200
+ const dataOut = {
201
+ title: window.arpabetMenuState.dictionaries[dictId].title,
202
+ description: window.arpabetMenuState.dictionaries[dictId].description,
203
+ version: window.arpabetMenuState.dictionaries[dictId].version,
204
+ author: window.arpabetMenuState.dictionaries[dictId].author,
205
+ nexusLink: window.arpabetMenuState.dictionaries[dictId].nexusLink,
206
+ data: window.arpabetMenuState.dictionaries[dictId].data
207
+ }
208
+
209
+ doFetch(`http://localhost:8008/updateARPABet`, {
210
+ method: "Post",
211
+ body: JSON.stringify({})
212
+ })//.then(r => r.text()).then(r => {console.log(r)})
213
+
214
+
215
+ fs.writeFileSync(`${window.path}/arpabet/${dictId}.json`, JSON.stringify(dataOut, null, 4))
216
+ }
217
+
218
+
219
+
220
+
221
+ window.refreshARPABETReferenceDisplay = () => {
222
+
223
+ const V2 = [2]
224
+ const V3 = [3]
225
+ const V2_3 = [2,3]
226
+
227
+ const data = [
228
+ // Min model version, symbols, examples
229
+ [V2, "AA0, AA1, AA2", "b<b>al</b>m, b<b>o</b>t, c<b>o</b>t"],
230
+ [V3, "AA, AA0, AA1, AA2", "b<b>al</b>m, b<b>o</b>t, c<b>o</b>t"],
231
+
232
+ [V2, "AE0, AE1, AE2", "b<b>a</b>t, f<b>a</b>st"],
233
+ [V3, "AE, AE0, AE1, AE2", "b<b>a</b>t, f<b>a</b>st"],
234
+
235
+ [V2, "AH0, AH1, AH2", "b<b>u</b>tt"],
236
+ [V3, "AH, AH0, AH1, AH2", "b<b>u</b>tt"],
237
+
238
+ [V2, "AO0, AO1, AO2", "st<b>o</b>ry"],
239
+ [V3, "AO, AO0, AO1, AO2", "st<b>o</b>ry"],
240
+
241
+ [V2, "AW0, AW1, AW2", "b<b>ou</b>t"],
242
+ [V3, "AW, AW0, AW1, AW2", "b<b>ou</b>t"],
243
+
244
+ [V3, "AX", "comm<b>a</b>"],
245
+ [V3, "AXR", "lett<b>er</b>"],
246
+
247
+ [V2, "AY0, AY1, AY2", "b<b>i</b>te"],
248
+ [V3, "AY, AY0, AY1, AY2", "b<b>i</b>te"],
249
+
250
+ [V2_3, "B", "<b>b</b>uy"],
251
+
252
+ [V3, "BR", "B and RRR sounds, together"],
253
+
254
+ [V2_3, "CH", "<b>ch</b>ina"],
255
+ [V2_3, "D", "<b>d</b>ie"],
256
+ [V3, "DX", "bu<b>tt</b>er"],
257
+ [V2_3, "DH", "<b>th</b>y"],
258
+ [V2, "EH0, EH1, EH2", "b<b>e</b>t"],
259
+ [V3, "EH,EH0, EH1, EH2", "b<b>e</b>t"],
260
+
261
+ [V3, "EL", "bott<b>le</b>"],
262
+ [V3, "EM", "rhyth<b>m</b>"],
263
+ [V3, "EN, EN0, EN1, EN2", "butt<b>on</b>"],
264
+
265
+ [V2, "ER0, ER1, ER2", "b<b>i</b>rd"],
266
+ [V3, "ER, ER0, ER1, ER2", "b<b>i</b>rd"],
267
+
268
+ [V2, "EY0, EY1, EY2", "b<b>ai</b>t"],
269
+ [V3, "EY, EY0, EY1, EY2", "b<b>ai</b>t"],
270
+
271
+ [V2_3, "F", "<b>f</b>ight"],
272
+ [V2_3, "G", "<b>g</b>uy"],
273
+ [V2_3, "HH", "<b>h</b>igh"],
274
+ [V3, "HJ", "J sound if mouth was open"],
275
+ [V3, "HR", "hrr sound typical in Arabic"],
276
+ [V2, "IH0, IH1, IH2", "b<b>i</b>t"],
277
+ [V3, "IH, IH0, IH1, IH2", "b<b>i</b>t"],
278
+ [V3, "IX", "ros<b>e</b>s, rabb<b>i</b>t"],
279
+
280
+ [V2, "IY0, IY1, IY2", "b<b>ea</b>t"],
281
+ [V3, "IY, IY0, IY1, IY2", "b<b>ea</b>t"],
282
+
283
+ [V2_3, "JH", "<b>j</b>ive"],
284
+ [V2_3, "K", "<b>k</b>ite"],
285
+ [V3, "KH", "K and H sounds, but together"],
286
+ [V2_3, "L", "<b>l</b>ie"],
287
+ [V2_3, "M", "<b>m</b>y"],
288
+ [V2_3, "N", "<b>n</b>igh"],
289
+ [V2_3, "NG", "si<b>ng</b>"],
290
+ [V3, "NX", "wi<b>nn</b>er"],
291
+
292
+ [V3, "OE", "german m<b>ΓΆ</b>ve, french eu (bl<b>eu</b>)"],
293
+ [V3, "OO", "hard o sound"],
294
+ [V2, "OW0, OW1, OW2", "b<b>oa</b>t"],
295
+ [V3, "OW, OW0, OW1, OW2", "b<b>oa</b>t"],
296
+
297
+ [V2, "OY0, OY1, OY2", "b<b>oy</b>"],
298
+ [V3, "OY, OY0, OY1, OY2", "b<b>oy</b>"],
299
+
300
+ [V2_3, "P", "<b>p</b>ie"],
301
+ [V3, "Q", "(glottal stop) uh<b>-</b>oh)"],
302
+ [V2_3, "R", "<b>r</b>ye"],
303
+ [V3, "RH, RR", "<b>r</b>un"],
304
+ [V3, "RRR", "<strong r>"],
305
+ [V2_3, "S", "<b>s</b>igh"],
306
+ [V3, "SJ", "swedish sj"],
307
+ [V2_3, "SH", "<b>sh</b>y"],
308
+ [V2_3, "T", "<b>t</b>ie"],
309
+ [V2_3, "TH", "<b>th</b>igh"],
310
+ [V3, "TS", "T and S sounds together (eg romanian Θ›)"],
311
+ [V2, "UH0, UH1, UH2", "b<b>oo</b>k"],
312
+ [V3, "UH, UH0, UH1, UH2", "b<b>oo</b>k"],
313
+ [V3, "UU", "german <b>ΓΌ</b>ber"],
314
+
315
+ [V2, "UW0, UW1, UW2", "b<b>oo</b>t"],
316
+ [V3, "UW, UW0, UW1, UW2", "b<b>oo</b>t"],
317
+ [V3, "UX", "d<b>u</b>de"],
318
+ [V3, "WH", "<b>wh</b>at, <b>wh</b>y (w with 'h' sound)"],
319
+
320
+ [V2_3, "V", "<b>v</b>ie"],
321
+ [V2_3, "W", "<b>w</b>ise"],
322
+ [V2_3, "Y", "<b>y</b>acht"],
323
+ [V2_3, "Z", "<b>z</b>oo"],
324
+ [V2_3, "ZH", "plea<b>s</b>ure"],
325
+ ]
326
+ arpabetReferenceList.innerHTML = ""
327
+ data.forEach(item => {
328
+ if (item[0].includes(parseInt(arpabetMenuModelDropdown.value))) {
329
+ const div = createElem("div")
330
+ div.appendChild(createElem("div", item[1]))
331
+
332
+ const exampleDiv = createElem("div")
333
+ exampleDiv.appendChild(createElem("div",item[2]))
334
+ div.appendChild(exampleDiv)
335
+
336
+ arpabetReferenceList.appendChild(div)
337
+ }
338
+ })
339
+ }
340
+ arpabetMenuModelDropdown.value = "3"
341
+ window.refreshARPABETReferenceDisplay()
342
+ arpabetMenuModelDropdown.addEventListener("click", window.refreshARPABETReferenceDisplay)
343
+
344
+
345
+ arpabet_save.addEventListener("click", () => {
346
+ const word = arpabet_word_input.value.trim().toLowerCase()
347
+ const arpabet = arpabet_arpabet_input.value.trim().toUpperCase().replace(/\s{2,}/g, " ")
348
+
349
+ if (!word.length || !arpabet.length) {
350
+ return window.errorModal(window.i18n.ARPABET_ERROR_EMPTY_INPUT)
351
+ }
352
+
353
+ const badSymbols = arpabet.split(" ").filter(symb => !window.ARPAbetSymbols.includes(symb))
354
+ if (badSymbols.length) {
355
+ return window.errorModal(window.i18n.ARPABET_ERROR_BAD_SYMBOLS.replace("_1", badSymbols.join(", ")))
356
+ }
357
+
358
+ const wordKeys = Object.keys(window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data)
359
+
360
+ const doTheRest_updateDict = () => {
361
+ window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data[word] = {enabled: true, arpabet: arpabet}
362
+ window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].filteredData = window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data
363
+
364
+ window.refreshDictWordList()
365
+ window.saveARPAbetDict(window.arpabetMenuState.currentDict)
366
+ }
367
+
368
+
369
+ // Delete the old record
370
+ if (window.arpabetMenuState.clickedRecord && window.arpabetMenuState.clickedRecord.word != word) {
371
+ delete window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data[window.arpabetMenuState.clickedRecord.word]
372
+ }
373
+
374
+ let wordExists = []
375
+ Object.keys(window.arpabetMenuState.dictionaries).forEach(dictName => {
376
+ if (dictName==window.arpabetMenuState.currentDict) {
377
+ return
378
+ }
379
+
380
+ if (Object.keys(window.arpabetMenuState.dictionaries[dictName].data).includes(word)) {
381
+ wordExists.push(dictName)
382
+ }
383
+ })
384
+
385
+ if (wordExists.length) {
386
+ window.confirmModal(window.i18n.ARPABET_CONFIRM_SAME_WORD.replace("_1", word).replace("_2", wordExists.join("<br>"))).then(response => {
387
+ if (response) {
388
+ doTheRest_updateDict()
389
+ }
390
+ })
391
+ } else {
392
+ doTheRest_updateDict()
393
+ }
394
+ })
395
+
396
+ arpabetModal.addEventListener("click", (event) => {
397
+ if (window.arpabetMenuState.clickedRecord && event.target.className!="arpabetRow"&& event.target.className!="arpabetRowItem" && ![arpabet_word_input, arpabet_arpabet_input, arpabet_save, arpabet_prev_btn, arpabet_next_btn].includes(event.target)) {
398
+ window.arpabetMenuState.clickedRecord = undefined
399
+ arpabet_word_input.value = ""
400
+ arpabet_arpabet_input.value = ""
401
+ }
402
+ })
403
+ arpabet_prev_btn.addEventListener("click", () => {
404
+ window.arpabetMenuState.paginationIndex = Math.max(0, window.arpabetMenuState.paginationIndex-1)
405
+ window.refreshDictWordList()
406
+ })
407
+ arpabet_next_btn.addEventListener("click", () => {
408
+ window.arpabetMenuState.paginationIndex = Math.min(window.arpabetMenuState.totalPages-1, window.arpabetMenuState.paginationIndex+1)
409
+ window.refreshDictWordList()
410
+ })
411
+
412
+ window.arpabetRunSearch = () => {
413
+ if (!window.arpabetMenuState.currentDict) {
414
+ return
415
+ }
416
+ window.arpabetMenuState.paginationIndex = 0
417
+ window.arpabetMenuState.totalPages = 0
418
+
419
+ let query = arpabet_word_search_input.value.trim().toLowerCase()
420
+
421
+ if (!query.length) {
422
+ if (arpabet_search_only_enabled.checked) {
423
+ const filteredKeys = Object.keys(window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data).filter(key => window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data[key].enabled)
424
+ window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].filteredData = {}
425
+ filteredKeys.forEach(key => {
426
+ window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].filteredData[key] = window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data[key]
427
+ })
428
+ } else {
429
+ window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].filteredData = window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data
430
+ }
431
+ } else {
432
+ const strictQuery = query.startsWith("\"")
433
+ const filteredKeys = Object.keys(window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data)
434
+ .filter(key => {
435
+ if (strictQuery) {
436
+ query = query.replaceAll("\"", "")
437
+ return key==query && (arpabet_search_only_enabled.checked ? (window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data[key].enabled) : true)
438
+ } else {
439
+ return key.includes(query) && (arpabet_search_only_enabled.checked ? (window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data[key].enabled) : true)
440
+ }
441
+ })
442
+
443
+ window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].filteredData = {}
444
+ filteredKeys.forEach(key => {
445
+ window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].filteredData[key] = window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data[key]
446
+ })
447
+ }
448
+
449
+ window.refreshDictWordList()
450
+ }
451
+ let arpabetSearchInterval
452
+ arpabet_word_search_input.addEventListener("keyup", () => {
453
+ if (arpabetSearchInterval!=null) {
454
+ clearTimeout(arpabetSearchInterval)
455
+ }
456
+ arpabetSearchInterval = setTimeout(window.arpabetRunSearch, 500)
457
+ })
458
+
459
+ arpabet_enableall_button.addEventListener("click", () => {
460
+
461
+ const dictName = window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].title
462
+ window.confirmModal(window.i18n.ARPABET_CONFIRM_ENABLE_ALL.replace("_1", dictName)).then(response => {
463
+ if (response) {
464
+ setTimeout(() => {
465
+ const wordKeys = Object.keys(window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data)
466
+ wordKeys.forEach(word => {
467
+ window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data[word].enabled = true
468
+ })
469
+
470
+ window.saveARPAbetDict(window.arpabetMenuState.currentDict)
471
+ window.arpabetRunSearch()
472
+ }, 210)
473
+ }
474
+ })
475
+ })
476
+
477
+ arpabet_disableall_button.addEventListener("click", () => {
478
+
479
+ const dictName = window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].title
480
+ window.confirmModal(window.i18n.ARPABET_CONFIRM_DISABLE_ALL.replace("_1", dictName)).then(response => {
481
+ if (response) {
482
+ setTimeout(() => {
483
+ const wordKeys = Object.keys(window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data)
484
+ wordKeys.forEach(word => {
485
+ window.arpabetMenuState.dictionaries[window.arpabetMenuState.currentDict].data[word].enabled = false
486
+ })
487
+
488
+ window.saveARPAbetDict(window.arpabetMenuState.currentDict)
489
+ window.arpabetRunSearch()
490
+ }, 210)
491
+ }
492
+ })
493
+ })
494
+ arpabet_search_only_enabled.addEventListener("click", () => window.arpabetRunSearch())
495
+
496
+
497
+
498
+ fs.watch(`${window.path}/arpabet`, {recursive: false, persistent: true}, (eventType, filename) => {console.log(eventType, filename);refreshDictionariesList()})
javascript/batch.js ADDED
@@ -0,0 +1,1573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ const path = require('path')
4
+ const smi = require('node-nvidia-smi')
5
+
6
+ window.batch_state = {
7
+ lines: [],
8
+ fastModeActuallyFinishedTasks: 0,
9
+ fastModeOutputPromises: [],
10
+ lastModel: undefined,
11
+ lastVocoder: undefined,
12
+ lineIndex: 0,
13
+ state: false,
14
+ outPathsChecked: [],
15
+ skippedExisting: 0,
16
+ paginationIndex: 0,
17
+ taskBarPercent: 0,
18
+ startTime: undefined,
19
+ linesDoneSinceStart: 0
20
+ }
21
+
22
+
23
+ // https://stackoverflow.com/questions/1293147/example-javascript-code-to-parse-csv-data
24
+ function CSVToArray( strData, strDelimiter ){
25
+ // Check to see if the delimiter is defined. If not,
26
+ // then default to comma.
27
+ strDelimiter = (strDelimiter || ",");
28
+
29
+ // Create a regular expression to parse the CSV values.
30
+ var objPattern = new RegExp(
31
+ (
32
+ // Delimiters.
33
+ "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
34
+
35
+ // Quoted fields.
36
+ "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
37
+
38
+ // Standard fields.
39
+ "([^\"\\" + strDelimiter + "\\r\\n]*))"
40
+ ),
41
+ "gi"
42
+ );
43
+
44
+ // Create an array to hold our data. Give the array
45
+ // a default empty first row.
46
+ var arrData = [[]];
47
+
48
+ // Create an array to hold our individual pattern
49
+ // matching groups.
50
+ var arrMatches = null;
51
+
52
+ // Keep looping over the regular expression matches
53
+ // until we can no longer find a match.
54
+ while (arrMatches = objPattern.exec( strData )){
55
+
56
+ // Get the delimiter that was found.
57
+ var strMatchedDelimiter = arrMatches[ 1 ];
58
+
59
+ // Check to see if the given delimiter has a length
60
+ // (is not the start of string) and if it matches
61
+ // field delimiter. If id does not, then we know
62
+ // that this delimiter is a row delimiter.
63
+ if (
64
+ strMatchedDelimiter.length &&
65
+ strMatchedDelimiter !== strDelimiter
66
+ ){
67
+ // Since we have reached a new row of data,
68
+ // add an empty row to our data array.
69
+ arrData.push( [] );
70
+ }
71
+
72
+ var strMatchedValue;
73
+
74
+ // Now that we have our delimiter out of the way,
75
+ // let's check to see which kind of value we
76
+ // captured (quoted or unquoted).
77
+ if (arrMatches[ 2 ]){
78
+ // We found a quoted value. When we capture
79
+ // this value, unescape any double quotes.
80
+ strMatchedValue = arrMatches[ 2 ].replace(
81
+ new RegExp( "\"\"", "g" ),
82
+ "\""
83
+ );
84
+ } else {
85
+ // We found a non-quoted value.
86
+ strMatchedValue = arrMatches[ 3 ];
87
+
88
+ }
89
+
90
+ // Now that we have our value string, let's add
91
+ // it to the data array.
92
+ arrData[ arrData.length - 1 ].push( strMatchedValue );
93
+ }
94
+
95
+ // Return the parsed data.
96
+ return( arrData );
97
+ }
98
+ window.CSVToArray = CSVToArray
99
+
100
+ let smiInterval = setInterval(() => {
101
+ try {
102
+ if (window.userSettings.useGPU) {
103
+ smi((err, data) => {
104
+ if (err) {
105
+ console.log("smi error: ", err)
106
+ }
107
+ if (data && data.nvidia_smi_log.cuda_version) {
108
+ let total
109
+ let used
110
+
111
+ if (data.nvidia_smi_log.gpu.length) {
112
+ total = parseInt(data.nvidia_smi_log.gpu[0].fb_memory_usage.total.split(" ")[0])
113
+ used = parseInt(data.nvidia_smi_log.gpu[0].fb_memory_usage.used.split(" ")[0])
114
+ } else {
115
+ total = parseInt(data.nvidia_smi_log.gpu.fb_memory_usage.total.split(" ")[0])
116
+ used = parseInt(data.nvidia_smi_log.gpu.fb_memory_usage.used.split(" ")[0])
117
+ }
118
+ const percent = used/total*100
119
+
120
+ vramUsage.innerHTML = `${(used/1000).toFixed(1)}/${(total/1000).toFixed(1)} GB (${percent.toFixed(2)}%)`
121
+ }
122
+ })
123
+ } else {
124
+ vramUsage.innerHTML = window.i18n.NOT_USING_GPU
125
+ }
126
+ } catch (e) {
127
+ console.log(e)
128
+ window.appLogger.log(e.stack)
129
+ clearInterval(smiInterval)
130
+ }
131
+ }, 1000)
132
+
133
+ batch_instructions_btn.addEventListener("click", () => {
134
+ window.createModal("error", `${window.i18n.BATCH_INSTR1} <a href="https://www.youtube.com/watch?v=PK-m54f84q4" target="_blank">${window.i18n.BATCH_INSTR2}</a> <span>${window.i18n.BATCH_INSTR3}</span>`)
135
+ })
136
+
137
+ batch_generateSample.addEventListener("click", () => {
138
+ // const lines = []
139
+ const csv = ["game_id,voice_id,text,vocoder,out_path,pacing"] // TODO: ffmpeg options
140
+ const games = Object.keys(window.games)
141
+
142
+ if (games.length==0) {
143
+ window.errorModal(window.i18n.BATCH_ERR_NO_VOICES)
144
+ return
145
+ }
146
+
147
+ const sampleText = [
148
+ "Include as many lines of text you wish with one line of data per line of voice to be read out.",
149
+ "Make sure that the required columns (game_id, voice_id, and text) are filled out",
150
+ "The others can be left blank, and the app will figure out some sensible defaults",
151
+ "The valid options for vocoder are one of: quickanddirty, waveglow, waveglowBIG, hifi",
152
+ "If your specified model does not have a bespoke hifi model, it will use the waveglow model, also the default if you leave this blank.",
153
+ "For all the other options, you can leave them blank.",
154
+ "If no output path is specified for a specific voice, the default batch output directory will be used",
155
+ ]
156
+
157
+ sampleText.forEach(line => {
158
+ const game = games[parseInt(Math.random()*games.length)]
159
+ const gameModels = window.games[game].models
160
+ const model = gameModels[parseInt(Math.random()*gameModels.length)].variants[0]
161
+
162
+ console.log("game", game, "model", model)
163
+
164
+ const record = {
165
+ game_id: game,
166
+ voice_id: model.voiceId,
167
+ text: line,
168
+ vocoder: ["quickanddirty","waveglow","waveglowBIG","hifi"][parseInt(Math.random()*4)],
169
+ out_path: "",
170
+ pacing: 1
171
+ }
172
+ // lines.push(record)
173
+ csv.push(Object.values(record).map(v => typeof v =="string" ? `"${v}"` : v).join(","))
174
+ })
175
+
176
+ const out_directory = `${__dirname.replace("\\javascript", "").replace(/\\/g,"/")}/batch`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app")
177
+ if (!fs.existsSync(out_directory)){
178
+ fs.mkdirSync(out_directory)
179
+ }
180
+ fs.writeFileSync(`${out_directory}/sample.csv`, csv.join("\n"))
181
+ // shell.showItemInFolder(`${out_directory}/sample.csv`)
182
+ er.shell.showItemInFolder(`${out_directory}/sample.csv`)
183
+ spawn(`explorer`, [out_directory], {stdio: "ignore"})
184
+ })
185
+
186
+ window.readFileTxt = (file) => {
187
+ return new Promise((resolve, reject) => {
188
+ const dataLines = []
189
+ const reader = new FileReader()
190
+ reader.readAsText(file)
191
+ reader.onloadend = () => {
192
+ const lines = reader.result.replace(/\r\n/g, "\n").split("\n")
193
+ lines.forEach(line => {
194
+ if (line.trim().length) {
195
+ const record = {}
196
+ record.game_id = window.currentModel.games[0].gameId
197
+ record.voice_id = window.currentModel.games[0].voiceId
198
+ record.text = line
199
+ if (window.currentModel.hifi) {
200
+ record.vocoder = "hifi"
201
+ }
202
+ dataLines.push(record)
203
+ }
204
+ })
205
+ resolve(dataLines)
206
+ }
207
+ })
208
+ }
209
+
210
+ window.readFile = (file) => {
211
+ return new Promise((resolve, reject) => {
212
+ const dataLines = []
213
+ const reader = new FileReader()
214
+ reader.readAsText(file)
215
+ reader.onloadend = () => {
216
+ const lines = reader.result.replace(/\r\n/g, "\n").split("\n")
217
+ const headerLine = lines.shift()
218
+
219
+ const doRest = () => {
220
+
221
+ const header = headerLine.split(window.userSettings.batch_delimiter).map(head => head.replace(/\r/, ""))
222
+
223
+ lines.forEach(line => {
224
+ const record = {}
225
+ if (line.trim().length) {
226
+ const parts = CSVToArray(line, window.userSettings.batch_delimiter)[0]
227
+ parts.forEach((val, vi) => {
228
+ try {
229
+ let header_val = header[vi].replace(/^"/, "").replace(/"$/, "")
230
+ record[header_val.replace(/\s/g,"")] = (val||"").replace(/\\/g, "/")
231
+ } catch (e) {
232
+ window.errorModal(`Error parsing line: ${val}`)
233
+ console.log(e)
234
+ window.appLogger.log(e)
235
+ }
236
+ })
237
+ dataLines.push(record)
238
+ }
239
+ })
240
+ resolve(dataLines)
241
+ }
242
+
243
+ const headerLine_clean = headerLine.replaceAll("\"", "")
244
+ if (headerLine_clean.includes(window.userSettings.batch_delimiter)) {
245
+ doRest()
246
+ } else {
247
+ const potentialDelimiter = headerLine_clean.split("voice_id")[0].split("game_id")[1]
248
+
249
+ window.confirmModal(window.i18n.BATCH_CHANGE_DELIMITER.replace("_1", window.userSettings.batch_delimiter).replace("_2", potentialDelimiter)).then(response => {
250
+ if (response) {
251
+ window.userSettings.batch_delimiter = potentialDelimiter
252
+ setting_batch_delimiter.value = potentialDelimiter
253
+ window.saveUserSettings()
254
+ doRest()
255
+ }
256
+ })
257
+ }
258
+ }
259
+ })
260
+ }
261
+
262
+ let handleMetadataCSVdrop_response = undefined
263
+ const sleep = ms => {
264
+ return new Promise(resolve => {
265
+ setTimeout(() => {
266
+ resolve()
267
+ }, ms)
268
+ })
269
+ }
270
+ const handleMetadataCSVdrop = (file) => {
271
+ return new Promise(async resolve => {
272
+ i18n_batch_metadata_open_btn.click()
273
+
274
+ handleMetadataCSVdrop_response = undefined
275
+ batch_metadata_input_gameID.value = window.currentGame.gameId
276
+ if (window.currentModel) {
277
+ batch_metadata_input_voiceID.value = window.currentModel.voiceId
278
+
279
+ }
280
+
281
+ await sleep(1000)
282
+ while (handleMetadataCSVdrop_response === undefined) {
283
+ if (batchMetadataCSVContainer.style.opacity.length==0 || parseFloat(batchMetadataCSVContainer.style.opacity)==1) {
284
+ await sleep(1000)
285
+ } else {
286
+ handleMetadataCSVdrop_response = false
287
+ }
288
+ }
289
+
290
+ if (handleMetadataCSVdrop_response) {
291
+
292
+ const dataLines = []
293
+ const reader = new FileReader()
294
+ reader.readAsText(file)
295
+ reader.onloadend = () => {
296
+ const lines = reader.result.replace(/\r\n/g, "\n").split("\n")
297
+ lines.forEach(line => {
298
+ if (line.trim().length) {
299
+
300
+ const text = line.split("|")[1]
301
+ const record = {}
302
+ record.game_id = batch_metadata_input_gameID.value
303
+ record.voice_id = batch_metadata_input_voiceID.value
304
+ record.text = text
305
+ if (!window.currentModel || window.currentModel.hifi) {
306
+ record.vocoder = "hifi"
307
+ }
308
+ dataLines.push(record)
309
+ }
310
+ })
311
+ resolve(dataLines)
312
+ }
313
+
314
+ } else {
315
+ resolve(false)
316
+ }
317
+
318
+
319
+ })
320
+ }
321
+ i18n_batch_metadata_confirm_btn.addEventListener("click", () => {
322
+ handleMetadataCSVdrop_response = true
323
+ batchMetadataCSVContainer.click()
324
+ batchIcon.click()
325
+ })
326
+
327
+ window.uploadBatchCSVs = async (eType, event) => {
328
+
329
+ if (["dragenter", "dragover"].includes(eType)) {
330
+ batch_main.style.background = "#5b5b5b"
331
+ batch_main.style.color = "white"
332
+ }
333
+ if (["dragleave", "drop"].includes(eType)) {
334
+ batch_main.style.background = "#4b4b4b"
335
+ batch_main.style.color = "gray"
336
+ }
337
+
338
+ event.preventDefault()
339
+ event.stopPropagation()
340
+
341
+ const dataLines = []
342
+
343
+ if (eType=="drop") {
344
+
345
+ batchDropZoneNote.innerHTML = window.i18n.PROCESSING_DATA
346
+ window.batch_state.skippedExisting = 0
347
+
348
+ const dataTransfer = event.dataTransfer
349
+ const files = Array.from(dataTransfer.files)
350
+ for (let fi=0; fi<files.length; fi++) {
351
+ const file = files[fi]
352
+
353
+ if (!file.name.toLowerCase().endsWith(".csv") || file.name.toLowerCase()=="metadata.csv") {
354
+ if ( file.name.toLowerCase().endsWith(".txt") || file.name.toLowerCase()=="metadata.csv" ) {
355
+ if (window.currentModel || file.name.toLowerCase()=="metadata.csv") {
356
+ window.appLogger.log(`Reading file: ${file.name}`)
357
+
358
+ let records
359
+
360
+ if (file.name.toLowerCase()=="metadata.csv") {
361
+
362
+ records = await handleMetadataCSVdrop(file)
363
+ if (records===false) {
364
+ continue
365
+ }
366
+
367
+ } else {
368
+ if (window.currentModel) {
369
+ records = await window.readFileTxt(file)
370
+ }
371
+ }
372
+
373
+ if (window.currentModel || file.name.toLowerCase()=="metadata.csv") {
374
+ if (window.userSettings.batch_skipExisting) {
375
+ window.appLogger.log("Checking existing files before adding to queue")
376
+ } else {
377
+ window.appLogger.log("Adding files to queue")
378
+ }
379
+ records.forEach(item => {
380
+ let outPath
381
+
382
+ if (item.out_path && item.out_path.split("/").reverse()[0].includes(".")) {
383
+ outPath = item.out_path
384
+ } else {
385
+ if (item.out_path) {
386
+ outPath = item.out_path
387
+ } else {
388
+ outPath = window.userSettings.batchOutFolder
389
+ }
390
+ if (item.vc_content) {
391
+ let vc_content_fname = item.vc_content.split("/").reverse()[0]
392
+ outPath = `${outPath}/${vc_content_fname.slice(0,vc_content_fname.length-4).slice(0, window.userSettings.max_filename_chars-10).replace(/\.$/, "")}.${window.userSettings.audio.format}`
393
+ } else {
394
+ outPath = `${outPath}/${item.voice_id}_${item.vocoder}_${item.text.replace(/[\/\\:\*?<>"|]*/g, "").slice(0, window.userSettings.max_filename_chars-10).replace(/\.$/, "")}.${window.userSettings.audio.format}`
395
+ }
396
+ }
397
+
398
+ outPath = outPath.startsWith("./") ? window.userSettings.batchOutFolder + outPath.slice(1,100000) : outPath
399
+ item.out_path = outPath
400
+
401
+ if (window.userSettings.batch_skipExisting && fs.existsSync(outPath)) {
402
+ window.batch_state.skippedExisting++
403
+ } else {
404
+ dataLines.push(item)
405
+ }
406
+ })
407
+ }
408
+
409
+ }
410
+ continue
411
+ } else {
412
+ continue
413
+ }
414
+ }
415
+
416
+ window.appLogger.log(`Reading file: ${file.name}`)
417
+ const records = await window.readFile(file)
418
+ if (window.userSettings.batch_skipExisting) {
419
+ window.appLogger.log("Checking existing files before adding to queue")
420
+ } else {
421
+ window.appLogger.log("Adding files to queue")
422
+ }
423
+ records.forEach((item, ii) => {
424
+ let outPath
425
+
426
+ if (item.out_path && item.out_path.split("/").reverse()[0].includes(".")) {
427
+ outPath = item.out_path
428
+ } else {
429
+ if (item.out_path) {
430
+ outPath = item.out_path
431
+ } else {
432
+ outPath = window.userSettings.batchOutFolder
433
+ }
434
+ if (item.vc_content) {
435
+ let vc_content_fname = item.vc_content.split("/").reverse()[0]
436
+ outPath = `${outPath}/${vc_content_fname.slice(0,vc_content_fname.length-4).slice(0,item.vc_content.length-4).slice(0, window.userSettings.max_filename_chars-10).replace(/\.$/, "")}.${window.userSettings.audio.format}`
437
+ } else {
438
+ outPath = `${outPath}/${item.voice_id}_${item.vocoder}_${item.text.replace(/[\/\\:\*?<>"|]*/g, "").slice(0, window.userSettings.max_filename_chars-10).replace(/\.$/, "")}.${window.userSettings.audio.format}`
439
+ }
440
+ }
441
+
442
+ outPath = outPath.startsWith("./") ? window.userSettings.batchOutFolder + outPath.slice(1,100000) : outPath
443
+ item.out_path = outPath
444
+
445
+ if (window.userSettings.batch_skipExisting && fs.existsSync(outPath)) {
446
+ window.batch_state.skippedExisting++
447
+ } else {
448
+ dataLines.push(item)
449
+ }
450
+ })
451
+ }
452
+
453
+ if (dataLines.length==0 && window.batch_state.skippedExisting) {
454
+ batchDropZoneNote.innerHTML = window.i18n.BATCH_DROPZONE
455
+ return window.errorModal(window.i18n.BATCH_ERR_SKIPPEDALL.replace("_1", window.batch_state.skippedExisting))
456
+ }
457
+
458
+ window.batch_state.paginationIndex = 0
459
+ batch_pageNum.value = 1
460
+
461
+
462
+ window.appLogger.log("Preprocessing data...")
463
+ const cleanedData = window.preProcessCSVData(dataLines)
464
+ if (cleanedData.length) {
465
+ window.populateBatchRecordsList(cleanedData)
466
+ window.appLogger.log("Grouping up lines...")
467
+ const finalOrder = window.groupBatchLines()
468
+ window.refreshBatchRecordsList(finalOrder)
469
+ window.batch_state.lines = finalOrder
470
+ } else {
471
+ // batch_clearBtn.click()
472
+ }
473
+ window.appLogger.log("batch import done")
474
+
475
+ const numPages = Math.ceil(window.batch_state.lines.length/window.userSettings.batch_paginationSize)
476
+ batch_total_pages.innerHTML = `of ${numPages}`
477
+ batchDropZoneNote.innerHTML = window.i18n.BATCH_DROPZONE
478
+ }
479
+ }
480
+
481
+ window.preProcessCSVData = data => {
482
+
483
+ batch_main.style.display = "block"
484
+ batchDropZoneNote.style.display = "none"
485
+ batchRecordsHeader.style.display = "flex"
486
+ batch_clearBtn.style.display = "inline-block"
487
+ Array.from(batchRecordsHeader.children).forEach(item => item.style.backgroundColor = `#${window.currentGame.themeColourPrimary}`)
488
+
489
+ const availableGames = Object.keys(window.games)
490
+ for (let di=0; di<data.length; di++) {
491
+ try {
492
+ const record = data[di]
493
+
494
+ // Validate the records first
495
+ // ==================
496
+ if (!record.game_id) {
497
+ window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: ${window.i18n.MISSING} game_id`)
498
+ return []
499
+ }
500
+ if (!record.voice_id) {
501
+ window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: ${window.i18n.MISSING} voice_id`)
502
+ return []
503
+ }
504
+ if ((!record.text || record.text.length==0) && (!record.vc_content)) {
505
+ window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: ${window.i18n.MISSING} text/vc_content`)
506
+ return []
507
+ }
508
+
509
+ // Check that the game_id exists
510
+ if (!availableGames.includes(record.game_id)) {
511
+ window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: game_id "${record.game_id}" ${window.i18n.BATCH_ERR_GAMEID} <br><br>(${availableGames.join(', ')})`)
512
+ return []
513
+ }
514
+ // Check that the voice_id exists
515
+ const gameVoices = []
516
+ window.games[record.game_id].models.forEach(model => {
517
+ model.variants.forEach(variant => gameVoices.push(variant.voiceId))
518
+ })
519
+
520
+ if (!gameVoices.includes(record.voice_id)) {
521
+ window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: voice_id "${record.voice_id}" ${window.i18n.BATCH_ERR_VOICEID}: ${record.game_id}`)
522
+ return []
523
+ }
524
+ // Check that the vocoder exists
525
+ if (!["quickanddirty", "waveglow", "waveglowBIG", "hifi", "", "-", undefined].includes(record.vocoder)) {
526
+ window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: ${window.i18n.BATCHH_VOCODER} "${record.vocoder}" ${window.i18n.BATCH_ERR_VOCODER1}: quickanddirty, waveglow, waveglowBIG, hifi ${window.i18n.BATCH_ERR_VOCODER2}`)
527
+ return []
528
+ }
529
+
530
+ data[di].modelType = undefined
531
+ let hasHifi = false
532
+ window.games[data[di].game_id].models.forEach(model => {
533
+ model.variants.forEach(variant => {
534
+ if (variant.voiceId==data[di].voice_id) {
535
+ data[di].modelType = variant.modelType || model.modelType
536
+ record.voiceName = model.voiceName // For easy access later on
537
+ if (variant.hifi) {
538
+ hasHifi = variant.hifi
539
+ }
540
+ if (variant.lang && !record.lang) {
541
+ record.lang = "en"
542
+ }
543
+ // TODO allow batch mode voice conversion/speech to speech by computing the embs of specified audio files instead of using the base voice embedding
544
+ // Also TODO, might need to allow for custom voice embeddings
545
+ if (variant.modelType=="xVAPitch") {
546
+ record.base_emb = variant.base_speaker_emb
547
+ record.vocoder = "-"
548
+ }
549
+ }
550
+ })
551
+ })
552
+
553
+ // Fill with defaults
554
+ // ==================
555
+ if (!record.out_path) {
556
+ record.out_path = window.userSettings.batchOutFolder
557
+ }
558
+ if (!record.pacing) {
559
+ record.pacing = 1
560
+ }
561
+ record.pacing = parseFloat(record.pacing)
562
+
563
+
564
+ if (!record.pitch_amp) {
565
+ record.pitch_amp = 1
566
+ }
567
+ record.pitch_amp = parseFloat(record.pitch_amp)
568
+
569
+ if (!record.out_path.includes(":/") && !record.out_path.startsWith("./")) {
570
+ record.out_path = `./${record.out_path}`
571
+ }
572
+
573
+ if (!record.vocoder || (record.vocoder=="hifi" && !hasHifi)) {
574
+ record.vocoder = "quickanddirty"
575
+ }
576
+ } catch (e) {
577
+ console.log(e)
578
+ window.appLogger.log(e)
579
+ console.log(data[di])
580
+ console.log(window.games[data[di].game_id])
581
+ }
582
+ }
583
+
584
+ return data
585
+ }
586
+
587
+ window.populateBatchRecordsList = records => {
588
+ batch_synthesizeBtn.style.display = "inline-block"
589
+ batchDropZoneNote.style.display = "none"
590
+
591
+ records.forEach((record, ri) => {
592
+ const row = createElem("div")
593
+
594
+ const rNumElem = createElem("div", batchRecordsContainer.children.length.toString())
595
+ const rStatusElem = createElem("div", "Ready")
596
+ const rActionsElem = createElem("div")
597
+ const rVoiceElem = createElem("div", record.voice_id)
598
+ const rTextElem = createElem("div", (record.vc_content?record.vc_content:record.text).toString())
599
+ rTextElem.title = rTextElem.innerText
600
+ if (record.vc_content) {
601
+ rTextElem.style.fontStyle = "italic"
602
+ }
603
+ const rGameElem = createElem("div", record.game_id)
604
+ const rOutPathElem = createElem("div", "&lrm;"+record.out_path+"&lrm;")
605
+ rOutPathElem.title = record.out_path
606
+ const rBaseLangElem = createElem("div", record.vc_content?"-":(record.lang||" ").toString())
607
+ const rVCStyleElem = createElem("div", (record.vc_style||" ").toString())
608
+ rVCStyleElem.title = rVCStyleElem.innerText
609
+ const rVocoderElem = createElem("div", record.vocoder)
610
+ const rPacingElem = createElem("div", record.vc_content?"-":(record.pacing||" ").toString())
611
+ const rPitchAmpElem = createElem("div", record.vc_content?"-":(record.pitch_amp||" ").toString())
612
+
613
+
614
+ row.appendChild(rNumElem)
615
+ row.appendChild(rStatusElem)
616
+ row.appendChild(rActionsElem)
617
+ row.appendChild(rVoiceElem)
618
+ row.appendChild(rTextElem)
619
+ row.appendChild(rGameElem)
620
+ row.appendChild(rOutPathElem)
621
+ row.appendChild(rBaseLangElem)
622
+ row.appendChild(rVCStyleElem)
623
+ row.appendChild(rVocoderElem)
624
+ row.appendChild(rPacingElem)
625
+ row.appendChild(rPitchAmpElem)
626
+
627
+ window.batch_state.lines.push([record, row, ri])
628
+ })
629
+ }
630
+
631
+ window.refreshBatchRecordsList = (finalOrder) => {
632
+ batchRecordsContainer.innerHTML = ""
633
+ finalOrder = finalOrder ? finalOrder : window.batch_state.lines
634
+
635
+ const startIndex = (window.batch_state.paginationIndex*window.userSettings.batch_paginationSize)
636
+ const endIndex = Math.min(startIndex+window.userSettings.batch_paginationSize, finalOrder.length)
637
+
638
+ for (let ri=startIndex; ri<endIndex; ri++) {
639
+ const recordAndElem = finalOrder[ri]
640
+ recordAndElem[1].children[0].innerHTML = (ri+1)//batchRecordsContainer.children.length.toString()
641
+ batchRecordsContainer.appendChild(recordAndElem[1])
642
+ }
643
+ window.toggleNumericalRecordsDisplay()
644
+ }
645
+
646
+ // Sort the lines by voice_id, and then by vocoder used
647
+ window.groupBatchLines = () => {
648
+ if (window.userSettings.batch_doGrouping) {
649
+ const voices_order = []
650
+
651
+ const lines = window.batch_state.lines.sort((a,b) => {
652
+ return a.voice_id - b.voice_id
653
+ })
654
+
655
+ const voices_groups = {}
656
+
657
+ // Get the order of the voice_id, and group them up
658
+ window.batch_state.lines.forEach(record => {
659
+ if (!voices_order.includes(record[0].voice_id)) {
660
+ voices_order.push(record[0].voice_id)
661
+ voices_groups[record[0].voice_id] = []
662
+ }
663
+ voices_groups[record[0].voice_id].push(record)
664
+ })
665
+
666
+ // Go through the voice groups and sort them by vocoder
667
+ if (window.userSettings.batch_doVocoderGrouping) {
668
+ voices_order.forEach(voice_id => {
669
+ voices_groups[voice_id] = voices_groups[voice_id].sort((a,b) => a[0].vocoder<b[0].vocoder?1:-1)
670
+ })
671
+ }
672
+
673
+ // Collate everything back into the final order
674
+ const finalOrder = []
675
+ voices_order.forEach(voice_id => {
676
+ voices_groups[voice_id].forEach(record => finalOrder.push(record))
677
+ })
678
+
679
+ return finalOrder
680
+
681
+ } else {
682
+ return window.batch_state.lines
683
+ }
684
+ }
685
+
686
+
687
+ batch_clearBtn.addEventListener("click", () => {
688
+
689
+ window.batch_state.lines = []
690
+ batch_main.style.display = "flex"
691
+ batchDropZoneNote.style.display = "block"
692
+ batchRecordsHeader.style.display = "none"
693
+ batch_clearBtn.style.display = "none"
694
+ batch_outputFolderInput.style.display = "inline-block"
695
+ batch_clearDirOpts.style.display = "flex"
696
+ batch_skipExistingOpts.style.display = "flex"
697
+ batch_useSR.style.display = "flex"
698
+ batch_useCleanup.style.display = "flex"
699
+ batch_outputNumericallyOpts.style.display = "flex"
700
+ batch_progressItems.style.display = "none"
701
+ batch_progressBar.style.display = "none"
702
+
703
+ batch_pauseBtn.style.display = "none"
704
+ batch_stopBtn.style.display = "none"
705
+ batch_synthesizeBtn.style.display = "none"
706
+
707
+ batchRecordsContainer.innerHTML = ""
708
+ })
709
+
710
+
711
+ window.startBatch = () => {
712
+
713
+ // Output directory
714
+ if (!fs.existsSync(window.userSettings.batchOutFolder)) {
715
+ window.userSettings.batchOutFolder.split("/")
716
+ .reduce((prevPath, folder) => {
717
+ const currentPath = path.join(prevPath, folder, path.sep);
718
+ if (!fs.existsSync(currentPath)){
719
+ try {
720
+ fs.mkdirSync(currentPath);
721
+ } catch (e) {
722
+ window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}:<br><br>`+e.message)
723
+ throw ""
724
+ }
725
+ }
726
+ return currentPath;
727
+ }, '');
728
+ }
729
+ if (batch_clearDirFirstCkbx.checked) {
730
+ const filesAndFolders = fs.readdirSync(window.userSettings.batchOutFolder)
731
+ filesAndFolders.forEach(faf => {
732
+ // Ignore .csv and .txt files
733
+ if (faf.toLowerCase().endsWith(".csv") || faf.toLowerCase().endsWith(".txt")) {
734
+ return
735
+ }
736
+ if (fs.lstatSync(`${window.userSettings.batchOutFolder}/${faf}`).isDirectory()) {
737
+ window.deleteFolderRecursive(`${window.userSettings.batchOutFolder}/${faf}`, false)
738
+ } else {
739
+ fs.unlinkSync(`${window.userSettings.batchOutFolder}/${faf}`)
740
+ }
741
+ console.log(`${window.userSettings.batchOutFolder}/${faf}`, )
742
+ })
743
+ }
744
+
745
+ batch_synthesizeBtn.style.display = "none"
746
+ batch_clearBtn.style.display = "none"
747
+ batch_outputFolderInput.style.display = "none"
748
+ batch_clearDirOpts.style.display = "none"
749
+ batch_skipExistingOpts.style.display = "none"
750
+ batch_useSR.style.display = "none"
751
+ batch_useCleanup.style.display = "none"
752
+ batch_outputNumericallyOpts.style.display = "none"
753
+ batch_progressItems.style.display = "flex"
754
+ batch_progressBar.style.display = "flex"
755
+ batch_pauseBtn.style.display = "inline-block"
756
+ batch_stopBtn.style.display = "inline-block"
757
+ batch_openDirBtn.style.display = "none"
758
+
759
+ window.batch_state.lines.forEach(record => {
760
+ record[1].children[1].innerHTML = window.i18n.READY
761
+ record[1].children[1].style.background = "none"
762
+ })
763
+
764
+ window.batch_state.fastModeOutputPromises = []
765
+ window.batch_state.fastModeActuallyFinishedTasks = 0
766
+ window.batch_state.lineIndex = 0
767
+ window.batch_state.state = true
768
+ window.batch_state.outPathsChecked = []
769
+ window.batch_state.startTime = new Date()
770
+ window.batch_state.linesDoneSinceStart = 0
771
+ window.performSynthesis()
772
+ }
773
+
774
+ window.batchChangeVoice = (game, voice, modelType) => {
775
+ return new Promise((resolve) => {
776
+ if (!window.batch_state.state) {
777
+ return resolve()
778
+ }
779
+ // Update the main app with any changes, if a voice has already been selected
780
+ if (window.currentModel) {
781
+ generateVoiceButton.innerHTML = window.i18n.LOAD_MODEL
782
+ keepSampleButton.style.display = "none"
783
+ wavesurferContainer.innerHTML = ""
784
+
785
+ const modelGameFolder = window.currentModel.audioPreviewPath.split("/")[0]
786
+ const modelFileName = window.currentModel.audioPreviewPath.split("/")[1].split(".wav")[0]
787
+ generateVoiceButton.dataset.modelQuery = JSON.stringify({
788
+ outputs: parseInt(window.currentModel.outputs),
789
+ model: `${window.path}/models/${modelGameFolder}/${modelFileName}`,
790
+ model_speakers: window.currentModel.emb_size,
791
+ cmudict: window.currentModel.cmudict
792
+ })
793
+ }
794
+
795
+
796
+ if (window.batch_state.state) {
797
+ batch_progressNotes.innerHTML = `${window.i18n.BATCH_CHANGING_MODEL_TO}: ${voice}`
798
+ }
799
+
800
+ let model
801
+ window.games[game].models.forEach(gameModel => {
802
+ gameModel.variants.forEach(variant => {
803
+ if (variant.voiceId==voice) {
804
+ model = variant
805
+ }
806
+ })
807
+ })
808
+
809
+ doFetch(`http://localhost:8008/loadModel`, {
810
+ method: "Post",
811
+ body: JSON.stringify({
812
+ "modelType": modelType,
813
+ "outputs": null,
814
+ "model": `${window.userSettings[`modelspath_${game}`]}/${voice}`,
815
+ "model_speakers": model.num_speakers,
816
+ "base_lang": model.lang,
817
+ "pluginsContext": JSON.stringify(window.pluginsContext)
818
+ })
819
+ }).then(r=>r.text()).then(res => {
820
+ resolve()
821
+ }).catch(async e => {
822
+ if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") {
823
+ await window.batchChangeVoice(game, voice, modelType)
824
+ resolve()
825
+ } else {
826
+ console.log(e)
827
+ window.appLogger.log(e)
828
+ batch_pauseBtn.click()
829
+
830
+ if (document.getElementById("activeModal")) {
831
+ activeModal.remove()
832
+ }
833
+ if (e.code=="ENOENT") {
834
+ window.errorModal(window.i18n.ERR_SERVER)
835
+ } else {
836
+ window.errorModal(e.message)
837
+ }
838
+ resolve()
839
+ }
840
+ })
841
+ })
842
+ }
843
+ window.batchChangeVocoder = (vocoder, game, voice) => {
844
+ return new Promise((resolve) => {
845
+ if (!window.batch_state.state) {
846
+ return resolve()
847
+ }
848
+ console.log("Changing vocoder: ", vocoder)
849
+ if (window.batch_state.state) {
850
+ batch_progressNotes.innerHTML = `${window.i18n.BATCH_CHANGING_VOCODER_TO}: ${vocoder}`
851
+ }
852
+
853
+ const vocoderMappings = [["waveglow", "256_waveglow"], ["waveglowBIG", "big_waveglow"], ["quickanddirty", "qnd"], ["hifi", `${game}/${voice}.hg.pt`]]
854
+ const vocoderId = vocoderMappings.find(record => record[0]==vocoder)[1]
855
+
856
+ doFetch(`http://localhost:8008/setVocoder`, {
857
+ method: "Post",
858
+ body: JSON.stringify({
859
+ vocoder: vocoderId,
860
+ modelPath: vocoderId=="256_waveglow" ? window.userSettings.waveglow_path : window.userSettings.bigwaveglow_path,
861
+ })
862
+ }).then(r=>r.text()).then((res) => {
863
+ if (res=="ENOENT") {
864
+ closeModal(undefined, batchGenerationContainer).then(() => {
865
+ setTimeout(() => {
866
+ vocoder_select.value = window.userSettings.vocoder
867
+ window.errorModal(`${window.i18n.BATCH_MODEL_NOT_FOUND}.${vocoderId.includes("waveglow")?" "+window.i18n.BATCH_DOWNLOAD_WAVEGLOW:""}`)
868
+ batch_pauseBtn.click()
869
+ resolve()
870
+ }, 300)
871
+ })
872
+ } else {
873
+ window.batch_state.lastVocoder = vocoder
874
+ resolve()
875
+ }
876
+ }).catch(async e => {
877
+ if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") {
878
+ await window.batchChangeVocoder(vocoder, game, voice)
879
+ resolve()
880
+ } else {
881
+ console.log(e)
882
+ window.appLogger.log(e)
883
+ batch_pauseBtn.click()
884
+
885
+ if (document.getElementById("activeModal")) {
886
+ activeModal.remove()
887
+ }
888
+ if (e.code=="ENOENT") {
889
+ window.errorModal(window.i18n.ERR_SERVER)
890
+ } else {
891
+ window.errorModal(e.message)
892
+ }
893
+ resolve()
894
+ }
895
+ })
896
+ })
897
+ }
898
+
899
+
900
+ window.prepareLinesBatchForSynth = () => {
901
+
902
+ const linesBatch = []
903
+ const records = []
904
+ let firstItemVoiceId = undefined
905
+ let firstItemVocoder = undefined
906
+ let speaker_i = 0
907
+
908
+ for (let i=0; i<Math.min(window.userSettings.batch_batchSize, window.batch_state.lines.length-window.batch_state.lineIndex); i++) {
909
+
910
+ const record = window.batch_state.lines[window.batch_state.lineIndex+i]
911
+
912
+ const vocoderMappings = [["waveglow", "256_waveglow"], ["waveglowBIG", "big_waveglow"], ["quickanddirty", "qnd"], ["hifi", `${record[0].game_id}/${record[0].voice_id}.hg.pt`]]
913
+ const vocoder = record[0].vocoder=="-"?"-":vocoderMappings.find(voc => voc[0]==record[0].vocoder)[1]
914
+
915
+ if (firstItemVoiceId==undefined) firstItemVoiceId = record[0].voice_id
916
+ if (firstItemVocoder==undefined) firstItemVocoder = vocoder
917
+
918
+ if (record[0].voice_id!=firstItemVoiceId || vocoder!=firstItemVocoder) {
919
+ break
920
+ }
921
+
922
+ let model
923
+ window.games[record[0].game_id].models.forEach(gamesModel => {
924
+ gamesModel.variants.forEach(variant => {
925
+ if (variant.voiceId==record[0].voice_id) {
926
+ model = variant
927
+ }
928
+ })
929
+ })
930
+
931
+ const sequence = record[0].text
932
+ const pitch = undefined // maybe later
933
+ const duration = undefined // maybe later
934
+ speaker_i = model.emb_i || 0
935
+ let pace = record[0].pacing
936
+ pace = Number.isNaN(pace) ? 1.0 : pace
937
+ let pitch_amp = record[0].pitch_amp
938
+ pitch_amp = Number.isNaN(pitch_amp) ? 1.0 : pitch_amp
939
+
940
+ const tempFileNum = `${Math.random().toString().split(".")[1]}`
941
+ const tempFileLocation = `${window.path}/output/temp-${tempFileNum}.wav`
942
+
943
+ let outPath
944
+ let outFolder
945
+
946
+ outPath = record[0].out_path
947
+ outFolder = String(record[0].out_path).split("/").reverse().slice(1,10000).reverse().join("/")
948
+ outFolder = outFolder.length ? outFolder : window.userSettings.batchOutFolder
949
+
950
+ if (batch_outputNumerically.checked) {
951
+ outPath = `${window.userSettings.batchOutFolder}/${String(record[2]).padStart(10, '0')}.${outPath.split(".").reverse()[0]}`
952
+ } else {
953
+ outPath = outPath.startsWith("./") ? window.userSettings.batchOutFolder + outPath.slice(1,100000) : outPath
954
+ }
955
+
956
+ linesBatch.push([sequence, pitch, duration, pace, tempFileLocation, outPath, outFolder, pitch_amp, record[0].lang, record[0].base_emb, record[0].vc_content, record[0].vc_style])
957
+ records.push(record)
958
+ }
959
+
960
+ return [speaker_i, firstItemVoiceId, firstItemVocoder, linesBatch, records]
961
+ }
962
+
963
+
964
+ window.addActionButtons = (records, ri) => {
965
+
966
+ let audioPreview
967
+ const playButton = createElem("button.smallButton", window.i18n.PLAY)
968
+ playButton.style.background = `#${window.currentGame.themeColourPrimary}`
969
+ playButton.addEventListener("click", () => {
970
+
971
+ let audioPreviewPath = records[ri][0].fileOutputPath
972
+ if (audioPreviewPath.startsWith("./")) {
973
+ audioPreviewPath = window.userSettings.batchOutFolder + audioPreviewPath.replace("./", "/")
974
+ }
975
+
976
+ if (audioPreview==undefined) {
977
+ const audioPreview = createElem("audio", {autoplay: false}, createElem("source", {
978
+ src: audioPreviewPath
979
+ }))
980
+ audioPreview.addEventListener("play", () => {
981
+ if (window.ctrlKeyIsPressed) {
982
+ audioPreview.setSinkId(window.userSettings.alt_speaker)
983
+ } else {
984
+ audioPreview.setSinkId(window.userSettings.base_speaker)
985
+ }
986
+ })
987
+ audioPreview.setSinkId(window.userSettings.base_speaker)
988
+ }
989
+ })
990
+ records[ri][1].children[2].appendChild(playButton)
991
+
992
+ // If not a Voice Conversion line
993
+ if (!records[ri][0].vc_content) {
994
+ const editButton = createElem("button.smallButton", window.i18n.EDIT)
995
+ editButton.style.background = `#${window.currentGame.themeColourPrimary}`
996
+ editButton.addEventListener("click", () => {
997
+ audioPreview = undefined
998
+
999
+
1000
+ if (window.batch_state.state) {
1001
+ window.errorModal(window.i18n.BATCH_ERR_EDIT)
1002
+ return
1003
+ }
1004
+
1005
+ // Change app theme to the voice's game
1006
+ if (window.currentGame.gameId!=records[ri][0].game_id) {
1007
+ window.changeGame(window.gameAssets[records[ri][0].game_id])
1008
+ }
1009
+
1010
+ dialogueInput.value = records[ri][0].text
1011
+
1012
+ // Simulate voice loading through the UI
1013
+ if (!window.currentModel || window.currentModel.voiceId != records[ri][0].voice_id) {
1014
+ const voiceName = records[ri][0].voiceName
1015
+ const voiceButton = Array.from(voiceTypeContainer.children).find(button => button.innerHTML==voiceName)
1016
+ voiceButton.click()
1017
+ vocoder_select.value = records[ri][0].vocoder=="hifi" ? `${records[ri][0].game_id}/${records[ri][0].voice_id}.hg.pt` : records[ri][0].vocoder
1018
+ generateVoiceButton.click()
1019
+ }
1020
+ window.closeModal(batchGenerationContainer)
1021
+
1022
+ setTimeout(() => {
1023
+ let audioPreviewPath = records[ri][0].fileOutputPath
1024
+ if (audioPreviewPath.startsWith("./")) {
1025
+ audioPreviewPath = window.userSettings.batchOutFolder + audioPreviewPath.replace("./", "/")
1026
+ }
1027
+ keepSampleButton.dataset.newFileLocation = "BATCH_EDIT"+audioPreviewPath
1028
+ generateVoiceButton.click()
1029
+ }, 500)
1030
+ })
1031
+ records[ri][1].children[2].appendChild(editButton)
1032
+ }
1033
+ }
1034
+
1035
+
1036
+ window.batchKickOffMPffmpegOutput = (records, tempPaths, outPaths, options, extraInfo) => {
1037
+ let hasShownError = false
1038
+ return new Promise((resolve, reject) => {
1039
+ doFetch(`http://localhost:8008/batchOutputAudio`, {
1040
+ method: "Post",
1041
+ body: JSON.stringify({
1042
+ input_paths: tempPaths,
1043
+ output_paths: outPaths,
1044
+ isBatchMode: true,
1045
+ pluginsContext: JSON.stringify(window.pluginsContext),
1046
+ processes: window.userSettings.batch_MPCount,
1047
+ extraInfo: extraInfo,
1048
+ options: JSON.stringify(options)
1049
+ })
1050
+ }).then(r=>r.text()).then(res => {
1051
+ res = res.split("\n")
1052
+ res.forEach((resItem, ri) => {
1053
+ if (resItem.length && resItem!="-") {
1054
+ window.appLogger.log(`Batch error, item ${ri} - ${resItem}`)
1055
+ if (window.batch_state.state) {
1056
+ batch_pauseBtn.click()
1057
+ }
1058
+ window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}:<br><br>`+resItem)
1059
+ hasShownError = true
1060
+
1061
+ records[ri][1].children[1].innerHTML = window.i18n.FAILED
1062
+ records[ri][1].children[1].style.background = "red"
1063
+
1064
+ } else {
1065
+
1066
+ records[ri][1].children[1].innerHTML = window.i18n.DONE
1067
+ records[ri][1].children[1].style.background = "green"
1068
+ fs.unlinkSync(tempPaths[ri])
1069
+ window.addActionButtons(records, ri)
1070
+ }
1071
+
1072
+ // if (!window.userSettings.batch_fastMode) { // No more fast modde. TODO, remove completely
1073
+ window.batch_state.lineIndex += 1
1074
+ // }
1075
+
1076
+ window.batch_state.fastModeActuallyFinishedTasks += 1
1077
+
1078
+ })
1079
+ const percentDone = (window.batch_state.fastModeActuallyFinishedTasks) / window.batch_state.lines.length * 100
1080
+ batch_progressBar.style.background = `linear-gradient(90deg, green ${parseInt(percentDone)}%, rgba(255,255,255,0) ${parseInt(percentDone)}%)`
1081
+ batch_progressBar.innerHTML = `${parseInt(percentDone* 100)/100}%`
1082
+ window.batch_state.taskBarPercent = percentDone/100
1083
+ window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent)
1084
+ window.adjustETA()
1085
+ resolve()
1086
+
1087
+ }).catch(async e => {
1088
+ if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") {
1089
+ await window.batchKickOffMPffmpegOutput(records, tempPaths, outPaths, options, extraInfo)
1090
+ resolve()
1091
+ } else {
1092
+ console.log(e)
1093
+ window.appLogger.log(e.stack)
1094
+ if (document.getElementById("activeModal")) {
1095
+ activeModal.remove()
1096
+ }
1097
+ if (!hasShownError) {
1098
+ window.errorModal(e.message)
1099
+ }
1100
+ resolve()
1101
+ }
1102
+ })
1103
+ })
1104
+ }
1105
+
1106
+
1107
+ window.batchKickOffFfmpegOutput = (ri, linesBatch, records, tempFileLocation, body) => {
1108
+ return new Promise((resolve, reject) => {
1109
+ doFetch(`http://localhost:8008/outputAudio`, {
1110
+ method: "Post",
1111
+ body
1112
+ }).then(r=>r.text()).then(res => {
1113
+ if (res.length && res!="-") {
1114
+ window.appLogger.log("res", res)
1115
+ if (window.batch_state.state) {
1116
+ batch_pauseBtn.click()
1117
+ }
1118
+
1119
+ for (let ri2=ri; ri2<linesBatch.length; ri2++) {
1120
+ records[ri][1].children[1].innerHTML = window.i18n.FAILED
1121
+ records[ri][1].children[1].style.background = "red"
1122
+ }
1123
+
1124
+ reject(res)
1125
+ } else {
1126
+ records[ri][1].children[1].innerHTML = window.i18n.DONE
1127
+ records[ri][1].children[1].style.background = "green"
1128
+ fs.unlinkSync(tempFileLocation)
1129
+ window.addActionButtons(records, ri)
1130
+ window.batch_state.fastModeActuallyFinishedTasks += 1
1131
+
1132
+ const percentDone = (window.batch_state.fastModeActuallyFinishedTasks) / window.batch_state.lines.length * 100
1133
+ batch_progressBar.style.background = `linear-gradient(90deg, green ${parseInt(percentDone)}%, rgba(255,255,255,0) ${parseInt(percentDone)}%)`
1134
+ batch_progressBar.innerHTML = `${parseInt(percentDone* 100)/100}%`
1135
+ window.batch_state.taskBarPercent = percentDone/100
1136
+ window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent)
1137
+ window.adjustETA()
1138
+ resolve()
1139
+ }
1140
+ }).catch(async e => {
1141
+ if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") {
1142
+ await window.batchKickOffFfmpegOutput(ri, linesBatch, records, tempFileLocation, body)
1143
+ resolve()
1144
+ } else {
1145
+ console.log(e)
1146
+ window.appLogger.log(e)
1147
+ batch_pauseBtn.click()
1148
+ if (document.getElementById("activeModal")) {
1149
+ activeModal.remove()
1150
+ }
1151
+ window.errorModal(e.message)
1152
+ resolve()
1153
+ }
1154
+ })
1155
+ })
1156
+ }
1157
+
1158
+ window.batchKickOffGeneration = () => {
1159
+ return new Promise((resolve) => {
1160
+ if (!window.batch_state.state) {
1161
+ return resolve()
1162
+ }
1163
+ const [speaker_i, voice_id, vocoder, linesBatch, records] = window.prepareLinesBatchForSynth()
1164
+
1165
+ records.forEach((record, ri) => {
1166
+ record[1].children[1].innerHTML = window.i18n.RUNNING
1167
+ record[1].children[1].style.background = "goldenrod"
1168
+ record[0].fileOutputPath = linesBatch[ri][5]
1169
+ })
1170
+
1171
+ const record = window.batch_state.lines[window.batch_state.lineIndex]
1172
+
1173
+ if (window.batch_state.state) {
1174
+ if (linesBatch.length==1) {
1175
+ batch_progressNotes.innerHTML = `${window.i18n.SYNTHESIZING}: <i>${record[0].text}</i>`
1176
+ } else {
1177
+ batch_progressNotes.innerHTML = `${window.i18n.SYNTHESIZING} ${linesBatch.length} ${window.i18n.LINES}`
1178
+ }
1179
+ }
1180
+ const batchPostData = {
1181
+ modelType: records[0][0].modelType,
1182
+ batchSize: window.userSettings.batch_batchSize,
1183
+ defaultOutFolder: window.userSettings.batchOutFolder,
1184
+ pluginsContext: JSON.stringify(window.pluginsContext),
1185
+ outputJSON: window.userSettings.batch_json,
1186
+ useSR: batch_useSRCkbx.checked,
1187
+ useCleanup: batch_useCleanupCkbx.checked,
1188
+ speaker_i, vocoder, linesBatch
1189
+ }
1190
+ doFetch(`http://localhost:8008/synthesize_batch`, {
1191
+ method: "Post",
1192
+ body: JSON.stringify(batchPostData)
1193
+ }).then(r=>r.text()).then(async (res) => {
1194
+
1195
+ if (res && res!="-") {
1196
+ if (res=="CUDA OOM") {
1197
+ window.errorModal(window.i18n.BATCH_ERR_CUDA_OOM)
1198
+ } else {
1199
+ window.errorModal(res.replace(/\n/g, "<br>"))
1200
+ }
1201
+ if (window.batch_state.state) {
1202
+ batch_pauseBtn.click()
1203
+ }
1204
+ return
1205
+ }
1206
+
1207
+
1208
+ // Create the output directory if it does not exist
1209
+ linesBatch.forEach(record => {
1210
+ let outFolder = record[6].startsWith("./") ? window.userSettings.batchOutFolder + record[6].slice(1,100000) : record[6]
1211
+
1212
+ if (!window.batch_state.outPathsChecked.includes(outFolder)) {
1213
+ window.batch_state.outPathsChecked.push(outFolder)
1214
+ if (!fs.existsSync(outFolder)) {
1215
+ window.createFolderRecursive(outFolder)
1216
+ }
1217
+ }
1218
+ })
1219
+
1220
+ if (window.userSettings.audio.ffmpeg) {
1221
+ const options = {
1222
+ hz: window.userSettings.audio.hz,
1223
+ padStart: window.userSettings.audio.padStart,
1224
+ padEnd: window.userSettings.audio.padEnd,
1225
+ bit_depth: window.userSettings.audio.bitdepth,
1226
+ amplitude: window.userSettings.audio.amplitude,
1227
+ pitchMult: window.userSettings.audio.pitchMult,
1228
+ tempo: window.userSettings.audio.tempo,
1229
+ deessing: window.userSettings.audio.deessing,
1230
+ nr: window.userSettings.audio.nr,
1231
+ nf: window.userSettings.audio.nf,
1232
+ useNR: window.userSettings.audio.useNR,
1233
+ useSR: batch_useSRCkbx.checked,
1234
+ useCleanup: batch_useCleanupCkbx.checked,
1235
+ }
1236
+
1237
+ if (window.batch_state.state) {
1238
+ batch_progressNotes.innerHTML = window.i18n.BATCH_OUTPUTTING_FFMPEG
1239
+ }
1240
+
1241
+ const tempPaths = linesBatch.map(line => line[4])
1242
+ const outPaths = linesBatch.map((line, li) => {
1243
+ let outPath = linesBatch[li][5].includes(":") || linesBatch[li][5].includes("./") ? linesBatch[li][5] : `${linesBatch[li][6]}/${linesBatch[li][5]}`
1244
+ if (outPath.startsWith("./")) {
1245
+ outPath = window.userSettings.batchOutFolder + outPath.slice(1,100000)
1246
+ }
1247
+ return outPath
1248
+ })
1249
+
1250
+ if (window.userSettings.batch_useMP) {
1251
+ const extraInfo = {
1252
+ game: records.map(rec => rec[0].game_id),
1253
+ voiceId: records.map(rec => rec[0].voice_id),
1254
+ voiceName: records.map(rec => rec[0].voiceName),
1255
+ inputSequence: records.map(rec => rec[0].text)
1256
+ }
1257
+ if (window.userSettings.batch_fastMode && false) { // No more fast mode. TODO, remove completely
1258
+ window.batch_state.fastModeOutputPromises.push(window.batchKickOffMPffmpegOutput(records, tempPaths, outPaths, options, JSON.stringify(extraInfo)))
1259
+ window.batch_state.lineIndex += records.length
1260
+ } else {
1261
+ await window.batchKickOffMPffmpegOutput(records, tempPaths, outPaths, options, JSON.stringify(extraInfo))
1262
+ }
1263
+ } else {
1264
+ for (let ri=0; ri<linesBatch.length; ri++) {
1265
+ let tempFileLocation = tempPaths[ri]
1266
+ let outPath = outPaths[ri]
1267
+ try {
1268
+ if (window.batch_state.state) {
1269
+ records[ri][1].children[1].innerHTML = window.i18n.OUTPUTTING
1270
+ const extraInfo = {
1271
+ game: records[ri][0].game_id,
1272
+ voiceId: records[ri][0].voiceId,
1273
+ voiceName: records[ri][0].voiceName,
1274
+ letters: records[ri][0].text
1275
+ }
1276
+
1277
+ if (window.userSettings.batch_fastMode && false) { // No more fast modde. TODO, remove completely
1278
+ window.batch_state.fastModeOutputPromises.push(window.batchKickOffFfmpegOutput(ri, linesBatch, records, tempFileLocation, JSON.stringify({
1279
+ input_path: tempFileLocation,
1280
+ output_path: outPath,
1281
+ isBatchMode: true,
1282
+ pluginsContext: JSON.stringify(window.pluginsContext),
1283
+ extraInfo: JSON.stringify(extraInfo),
1284
+ options: JSON.stringify(options)
1285
+ })))
1286
+ } else {
1287
+ await window.batchKickOffFfmpegOutput(ri, linesBatch, records, tempFileLocation, JSON.stringify({
1288
+ input_path: tempFileLocation,
1289
+ output_path: outPath,
1290
+ isBatchMode: true,
1291
+ pluginsContext: JSON.stringify(window.pluginsContext),
1292
+ extraInfo: JSON.stringify(extraInfo),
1293
+ options: JSON.stringify(options)
1294
+ }))
1295
+ }
1296
+ window.batch_state.lineIndex += 1
1297
+ }
1298
+ } catch (e) {
1299
+ console.log(e)
1300
+ window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}:<br><br>`+e)
1301
+ resolve()
1302
+ }
1303
+ }
1304
+ }
1305
+ window.batch_state.linesDoneSinceStart += linesBatch.length
1306
+ resolve()
1307
+ } else {
1308
+ linesBatch.forEach((lineRecord, li) => {
1309
+ let tempFileLocation = lineRecord[4]
1310
+ let outPath = lineRecord[5]
1311
+ try {
1312
+ fs.copyFileSync(tempFileLocation, outPath)
1313
+ records[li][1].children[1].innerHTML = window.i18n.DONE
1314
+ records[li][1].children[1].style.background = "green"
1315
+
1316
+ window.batch_state.lineIndex += 1
1317
+
1318
+ window.addActionButtons(records, li)
1319
+
1320
+ } catch (err) {
1321
+ console.log(err)
1322
+ window.appLogger.log(err)
1323
+ window.errorModal(err.message)
1324
+ batch_pauseBtn.click()
1325
+ }
1326
+ window.batch_state.linesDoneSinceStart += linesBatch.length
1327
+ resolve()
1328
+ })
1329
+ }
1330
+ }).catch(async e => {
1331
+ if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") {
1332
+ await window.batchKickOffGeneration()
1333
+ resolve()
1334
+ } else {
1335
+ console.log(e)
1336
+ window.appLogger.log(e)
1337
+ batch_pauseBtn.click()
1338
+ if (document.getElementById("activeModal")) {
1339
+ activeModal.remove()
1340
+ }
1341
+ console.log(e.message)
1342
+ window.errorModal(e.message).then(() => resolve())
1343
+ }
1344
+ })
1345
+ })
1346
+ }
1347
+
1348
+ window.performSynthesis = async () => {
1349
+
1350
+ if (batch_state.lineIndex-batch_state.fastModeActuallyFinishedTasks > window.userSettings.batch_fastModeMaxParallelizations) {
1351
+ console.log(`Ahead by ${batch_state.lineIndex-batch_state.fastModeActuallyFinishedTasks} tasks. Waiting...`)
1352
+ setTimeout(() => {window.performSynthesis()}, 1000)
1353
+ return
1354
+ }
1355
+
1356
+ if (!window.batch_state.state) {
1357
+ return
1358
+ }
1359
+
1360
+ if (window.batch_state.lineIndex==0) {
1361
+ const percentDone = (window.batch_state.lineIndex) / window.batch_state.lines.length * 100
1362
+ batch_progressBar.style.background = `linear-gradient(90deg, green ${parseInt(percentDone)}%, rgba(255,255,255,0) ${parseInt(percentDone)}%)`
1363
+ batch_progressBar.innerHTML = `${parseInt(percentDone* 100)/100}%`
1364
+ window.batch_state.taskBarPercent = percentDone/100
1365
+ window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent)
1366
+ }
1367
+
1368
+
1369
+ const record = window.batch_state.lines[window.batch_state.lineIndex]
1370
+
1371
+ // Change the voice model if the next line uses a different one
1372
+ if (window.batch_state.lastModel!=record[0].voice_id) {
1373
+ await window.batchChangeVoice(record[0].game_id, record[0].voice_id, record[0].modelType)
1374
+ window.batch_state.lastModel = record[0].voice_id
1375
+ }
1376
+
1377
+ // Change the vocoder if the next line uses a different one
1378
+ if (window.batch_state.lastVocoder!=record[0].vocoder && record[0].vocoder!="-") {
1379
+ await window.batchChangeVocoder(record[0].vocoder, record[0].game_id, record[0].voice_id)
1380
+ }
1381
+
1382
+ await window.batchKickOffGeneration()
1383
+
1384
+ if (window.batch_state.lineIndex==window.batch_state.lines.length) {
1385
+ // The end
1386
+ if (window.userSettings.batch_fastMode && false) { // No more fast modde. TODO, remove completely
1387
+ Promise.all(window.batch_state.fastModeOutputPromises).then(() => {
1388
+ window.stopBatch()
1389
+ batch_openDirBtn.style.display = "inline-block"
1390
+ })
1391
+ } else {
1392
+ window.stopBatch()
1393
+ batch_openDirBtn.style.display = "inline-block"
1394
+ }
1395
+
1396
+ } else {
1397
+ window.performSynthesis()
1398
+ }
1399
+ }
1400
+
1401
+ window.pauseResumeBatch = () => {
1402
+
1403
+ batch_progressNotes.innerHTML = window.i18n.PAUSED
1404
+
1405
+ const isRunning = window.batch_state.state
1406
+ batch_pauseBtn.innerHTML = isRunning ? window.i18n.RESUME : window.i18n.PAUSE
1407
+ window.batch_state.state = !isRunning
1408
+
1409
+ window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent?window.batch_state.taskBarPercent:1, {mode: isRunning ? "paused" : "normal"})
1410
+
1411
+ if (window.batch_state.state) {
1412
+ window.batch_state.startTime = new Date()
1413
+ window.batch_state.linesDoneSinceStart = 0
1414
+ }
1415
+
1416
+
1417
+ if (!isRunning) {
1418
+ window.performSynthesis()
1419
+ }
1420
+ }
1421
+
1422
+ window.stopBatch = (stoppedByUser) => {
1423
+ window.electronBrowserWindow.setProgressBar(0)
1424
+ window.batch_state.state = false
1425
+ window.batch_state.lineIndex = 0
1426
+
1427
+ batch_ETA_container.style.opacity = 0
1428
+ batch_synthesizeBtn.style.display = "inline-block"
1429
+ batch_clearBtn.style.display = "inline-block"
1430
+ batch_outputFolderInput.style.display = "inline-block"
1431
+ batch_clearDirOpts.style.display = "flex"
1432
+ batch_skipExistingOpts.style.display = "flex"
1433
+ batch_useSR.style.display = "flex"
1434
+ batch_useCleanup.style.display = "flex"
1435
+ batch_outputNumericallyOpts.style.display = "flex"
1436
+ batch_progressItems.style.display = "none"
1437
+ batch_progressBar.style.display = "none"
1438
+ batch_pauseBtn.style.display = "none"
1439
+ batch_stopBtn.style.display = "none"
1440
+
1441
+ window.batch_state.lines.forEach(record => {
1442
+ if (record[1].children[1].innerHTML==window.i18n.READY || record[1].children[1].innerHTML==window.i18n.RUNNING) {
1443
+ record[1].children[1].innerHTML = window.i18n.STOPPED
1444
+ record[1].children[1].style.background = "none"
1445
+ }
1446
+ })
1447
+
1448
+ const pluginData = {
1449
+ stoppedByUser: stoppedByUser
1450
+ }
1451
+ window.pluginsManager.runPlugins(window.pluginsManager.pluginsModules["batch-stop"]["post"], event="post batch-stop", pluginData)
1452
+ }
1453
+
1454
+ window.adjustETA = () => {
1455
+ if (window.batch_state.state && window.batch_state.fastModeActuallyFinishedTasks>=2) {
1456
+ batch_ETA_container.style.opacity = 1
1457
+
1458
+ // Lines per second
1459
+ const timeNow = new Date()
1460
+ const timeSinceStart = timeNow - window.batch_state.startTime
1461
+ const avgMSTimePerLine = timeSinceStart / window.batch_state.fastModeActuallyFinishedTasks
1462
+ batch_eta_lps.innerHTML = parseInt((1000/avgMSTimePerLine)*100)/100
1463
+
1464
+
1465
+ const remainingLines = window.batch_state.lines.length - window.batch_state.fastModeActuallyFinishedTasks
1466
+ let estTimeRemaining = avgMSTimePerLine*remainingLines
1467
+
1468
+ // Estimated finish time
1469
+ const finishTime = new Date(timeNow.getTime() + estTimeRemaining)
1470
+ let etaFinishTime = `${finishTime.getHours()}:${String(finishTime.getMinutes()).padStart(2, "0")}:${String(finishTime.getSeconds()).padStart(2, "0")}`
1471
+ const days = [window.i18n.SUNDAY, window.i18n.MONDAY, window.i18n.TUESDAY, window.i18n.WEDNESDAY, window.i18n.THURSDAY, window.i18n.FRIDAY, window.i18n.SATURDAY]
1472
+ etaFinishTime = `${days[finishTime.getDay()]} ${etaFinishTime}`
1473
+
1474
+ batch_eta_eta.innerHTML = etaFinishTime
1475
+
1476
+ // Time remaining
1477
+ let etaTimeDisplay = []
1478
+ if (estTimeRemaining > (1000*60*60)) { // hours
1479
+ const hours = parseInt(estTimeRemaining/(1000*60*60))
1480
+ etaTimeDisplay.push(hours+"h")
1481
+ estTimeRemaining -= hours*(1000*60*60)
1482
+ }
1483
+ if (estTimeRemaining > (1000*60)) { // minutes
1484
+ const minutes = parseInt(estTimeRemaining/(1000*60))
1485
+ etaTimeDisplay.push(String(minutes).padStart(2, "0")+"m")
1486
+ estTimeRemaining -= minutes*(1000*60)
1487
+ }
1488
+ if (estTimeRemaining > (1000)) { // seconds
1489
+ const seconds = parseInt(estTimeRemaining/(1000))
1490
+ etaTimeDisplay.push(String(seconds).padStart(2, "0")+"s")
1491
+ estTimeRemaining -= seconds*(1000)
1492
+ }
1493
+ batch_eta_time.innerHTML = etaTimeDisplay.join(" ")
1494
+
1495
+ } else {
1496
+ batch_ETA_container.style.opacity = 0
1497
+ }
1498
+ }
1499
+
1500
+
1501
+ const openOutput = () => {
1502
+ er.shell.showItemInFolder(window.userSettings.batchOutFolder+"/dummy.txt")
1503
+ spawn(`explorer`, [window.userSettings.batchOutFolder.replace(/\//g, "\\")], {stdio: "ignore"})
1504
+ }
1505
+
1506
+
1507
+ batch_paginationPrev.addEventListener("click", () => {
1508
+ batch_pageNum.value = Math.max(1, parseInt(batch_pageNum.value)-1)
1509
+ window.batch_state.paginationIndex = batch_pageNum.value-1
1510
+ window.refreshBatchRecordsList()
1511
+ })
1512
+ batch_paginationNext.addEventListener("click", () => {
1513
+ const numPages = Math.ceil(window.batch_state.lines.length/window.userSettings.batch_paginationSize)
1514
+ batch_pageNum.value = Math.min(parseInt(batch_pageNum.value)+1, numPages)
1515
+ window.batch_state.paginationIndex = batch_pageNum.value-1
1516
+ window.refreshBatchRecordsList()
1517
+ })
1518
+ batch_pageNum.addEventListener("change", () => {
1519
+ const numPages = Math.ceil(window.batch_state.lines.length/window.userSettings.batch_paginationSize)
1520
+ batch_pageNum.value = Math.max(1, Math.min(parseInt(batch_pageNum.value), numPages))
1521
+ window.batch_state.paginationIndex = batch_pageNum.value-1
1522
+ window.refreshBatchRecordsList()
1523
+ })
1524
+ setting_batch_paginationSize.addEventListener("change", () => {
1525
+ const numPages = Math.ceil(window.batch_state.lines.length/window.userSettings.batch_paginationSize)
1526
+ batch_pageNum.value = Math.max(1, Math.min(parseInt(batch_pageNum.value), numPages))
1527
+ window.batch_state.paginationIndex = batch_pageNum.value-1
1528
+ batch_total_pages.innerHTML = window.i18n.PAGINATION_TOTAL_OF.replace("_1", numPages)
1529
+
1530
+ window.refreshBatchRecordsList()
1531
+ })
1532
+
1533
+ window.toggleNumericalRecordsDisplay = () => {
1534
+ window.batch_state.lines.forEach(record => {
1535
+ record[1].children[6].innerHTML = batch_outputNumerically.checked ? `${window.userSettings.batchOutFolder}/${String(record[2]).padStart(10, '0')}` : record[0].out_path
1536
+ })
1537
+ }
1538
+ batch_outputNumerically.addEventListener("click", () => {
1539
+ window.toggleNumericalRecordsDisplay()
1540
+ })
1541
+ batch_saveToCSV.addEventListener("click", () => {
1542
+
1543
+ try {
1544
+ const csv_file = [`game_id|voice_id|text`]
1545
+ window.batch_state.lines.forEach(line => {
1546
+ csv_file.push(`${line[0].game_id}|${line[0].voice_id}|${line[0].text}`)
1547
+ })
1548
+
1549
+ const outFileName = JSON.stringify(new Date()).replace("\"","").replaceAll(":","_").split(".")[0]+"_batch.csv"
1550
+
1551
+ fs.writeFileSync(`${window.userSettings.batchOutFolder}/${outFileName}`, csv_file.join("\n"), "utf8")
1552
+
1553
+ window.createModal("error", `${window.i18n.BATCH_TOCSV_DONE}<br><br>${window.userSettings.batchOutFolder}/${outFileName}`)
1554
+
1555
+ } catch(e) {
1556
+ console.log(e)
1557
+ window.appLogger.log(e.stack)
1558
+ window.errorModal(e.stack)
1559
+ }
1560
+
1561
+ })
1562
+
1563
+
1564
+ batch_main.addEventListener("dragenter", event => window.uploadBatchCSVs("dragenter", event), false)
1565
+ batch_main.addEventListener("dragleave", event => window.uploadBatchCSVs("dragleave", event), false)
1566
+ batch_main.addEventListener("dragover", event => window.uploadBatchCSVs("dragover", event), false)
1567
+ batch_main.addEventListener("drop", event => window.uploadBatchCSVs("drop", event), false)
1568
+
1569
+
1570
+ batch_synthesizeBtn.addEventListener("click", window.startBatch)
1571
+ batch_pauseBtn.addEventListener("click", window.pauseResumeBatch)
1572
+ batch_stopBtn.addEventListener("click", () => window.stopBatch(true))
1573
+ batch_openDirBtn.addEventListener("click", openOutput)
javascript/dragdrop_model_install.js ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ window.dragDropModelInstallation = (eType, event) => {
4
+ if (["dragenter", "dragover"].includes(eType)) {
5
+ left.style.background = "#5b5b5b"
6
+ left.style.color = "white"
7
+ }
8
+ if (["dragleave", "drop"].includes(eType)) {
9
+ left.style.background = "rgba(0,0,0,0)"
10
+ left.style.color = "white"
11
+ }
12
+
13
+ event.preventDefault()
14
+ event.stopPropagation()
15
+
16
+ const dataLines = []
17
+
18
+ if (eType=="drop") {
19
+ const dataTransfer = event.dataTransfer
20
+ const files = Array.from(dataTransfer.files)
21
+
22
+ const modelGroups = {} // Group up all files by their base name (for if loose files are given)
23
+ files.forEach(file => {
24
+ const baseName = file.name.split(".")[0]
25
+ if (!modelGroups[baseName]) {
26
+ modelGroups[baseName] = []
27
+ }
28
+ modelGroups[baseName].push(file.path)
29
+ })
30
+
31
+ const modelGroupsComplete = []
32
+ const modelGroupsNotComplete = []
33
+ Object.keys(modelGroups).forEach(key => {
34
+ if (modelGroups[key][0].split(".").at(-1)!="zip") {
35
+ const fileExtensions = modelGroups[key].map(filePath => filePath.split(".").at(-1))
36
+ if (fileExtensions.includes("json") && fileExtensions.includes("pt")) {
37
+ modelGroupsComplete.push(key)
38
+ } else {
39
+ modelGroupsNotComplete.push(key)
40
+ }
41
+ } else {
42
+ modelGroupsComplete.push(key)
43
+ }
44
+ })
45
+
46
+ if (modelGroupsNotComplete.length) {
47
+ return window.errorModal(window.i18n.MODEL_INSTALL_DRAGDROP_INCOMPLETE.replace("_1", modelGroupsNotComplete.join(", ")))
48
+ }
49
+
50
+ const modelsInstalledSuccessfully = []
51
+ const modelsFailedInstallation = []
52
+ let lastGameInstalledOkFor = undefined
53
+
54
+
55
+ const handleZip = (files, key) => {
56
+ return new Promise(resolve => {
57
+ let installedOk = false
58
+ let game = undefined
59
+
60
+ try {
61
+ if (fs.existsSync(`${window.path}/downloads`)) {
62
+ fs.readdirSync(`${window.path}/downloads`).forEach(fileName => {
63
+ fs.unlinkSync(`${window.path}/downloads/${fileName}`)
64
+ })
65
+ } else {
66
+ fs.mkdirSync(`${window.path}/downloads`)
67
+ }
68
+
69
+ window.unzipFileTo(files[0], `${window.path}/downloads`).then(() => {
70
+ const allFiles = fs.readdirSync(`${window.path}/downloads`)
71
+ const jsonFiles = allFiles.filter(fname => fname.endsWith(".json"))
72
+
73
+ jsonFiles.forEach(jsonFile => {
74
+ const jsonData = JSON.parse(fs.readFileSync(`${window.path}/downloads/${jsonFile}`))
75
+
76
+ game = jsonData.games[0].gameId
77
+ const voiceId = jsonData.games[0].voiceId
78
+ const modelsFolder = window.userSettings[`modelspath_${game}`]
79
+
80
+ const allFilesForThisModel = allFiles.filter(fname => fname.includes(voiceId))
81
+ allFilesForThisModel.forEach(fname => {
82
+ fs.copyFileSync(`${window.path}/downloads/${fname}`, `${modelsFolder}/${fname}`)
83
+ })
84
+
85
+ installedOk = true
86
+ })
87
+
88
+ resolve([game, key, installedOk])
89
+ })
90
+
91
+ } catch (e) {
92
+ resolve([game, key, false])
93
+ }
94
+ })
95
+ }
96
+
97
+ const handleLoose = (files, key) => {
98
+ let game = undefined
99
+ return new Promise(resolve => {
100
+ try {
101
+ const jsonFile = files.find(fname => fname.endsWith(".json"))
102
+ const parentFolder = jsonFile.replaceAll("\\", "/").split("/").reverse().slice(1, 100000).reverse().join("/")
103
+ // const jsonData = JSON.parse(fs.readFileSync(`${parentFolder}/${jsonFile}`))
104
+ const jsonData = JSON.parse(fs.readFileSync(`${jsonFile}`))
105
+
106
+ const game = jsonData.games[0].gameId
107
+ const voiceId = jsonData.games[0].voiceId
108
+ const modelsFolder = window.userSettings[`modelspath_${game}`]
109
+
110
+ const allFilesForThisModel = fs.readdirSync(parentFolder).filter(fname => fname.includes(voiceId))
111
+ allFilesForThisModel.forEach(fname => {
112
+ fs.copyFileSync(`${parentFolder}/${fname}`, `${modelsFolder}/${fname}`)
113
+ // fs.copyFileSync(`${fname}`, `${modelsFolder}/${fname}`)
114
+ })
115
+
116
+ // lastGameInstalledOkFor = game
117
+ // modelsInstalledSuccessfully.push(key)
118
+ resolve([game, key, true])
119
+ } catch (e) {
120
+ resolve([game, key, false])
121
+ }
122
+ })
123
+ }
124
+
125
+ const installPromises = []
126
+
127
+ modelGroupsComplete.forEach(key => {
128
+ try {
129
+ const files = modelGroups[key]
130
+ if (files[0].endsWith(".zip")) {
131
+ installPromises.push(handleZip(files, key))
132
+ } else {
133
+ installPromises.push(handleLoose(files, key))
134
+ }
135
+
136
+ } catch (e) {
137
+ console.log(e)
138
+ window.appLogger.log(e)
139
+ modelsFailedInstallation.push(key)
140
+ }
141
+ })
142
+
143
+
144
+ Promise.all(installPromises).then(responses => {
145
+
146
+ responses.forEach(([game, key, installedOk]) => {
147
+ if (installedOk) {
148
+ lastGameInstalledOkFor = game
149
+ modelsInstalledSuccessfully.push(key)
150
+ } else {
151
+ modelsFailedInstallation.push(key)
152
+ }
153
+ })
154
+
155
+ let outputMessage = ""
156
+
157
+ if (modelsInstalledSuccessfully.length) {
158
+ outputMessage += window.i18n.MODEL_INSTALL_DRAGDROP_SUCCESS.replace("_1", modelsInstalledSuccessfully.length)
159
+ }
160
+ if (modelsFailedInstallation.length) {
161
+ outputMessage += window.i18n.MODEL_INSTALL_DRAGDROP_FAILED.replace("_1", modelsFailedInstallation.length).replace("_2", modelsFailedInstallation.join(", "))
162
+ }
163
+ window.infoModal(outputMessage)
164
+
165
+ if (lastGameInstalledOkFor) {
166
+ window.changeGame(window.gameAssets[lastGameInstalledOkFor])
167
+ }
168
+ window.displayAllModels(true)
169
+ window.loadAllModels(true).then(() => {
170
+ changeGame(window.currentGame)
171
+ })
172
+ })
173
+
174
+
175
+ }
176
+ }
177
+ left.addEventListener("dragenter", event => window.dragDropModelInstallation("dragenter", event), false)
178
+ left.addEventListener("dragleave", event => window.dragDropModelInstallation("dragleave", event), false)
179
+ left.addEventListener("dragover", event => window.dragDropModelInstallation("dragover", event), false)
180
+ left.addEventListener("drop", event => window.dragDropModelInstallation("drop", event), false)
javascript/editor.js ADDED
@@ -0,0 +1,2089 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+
4
+ class Editor {
5
+
6
+ constructor () {
7
+
8
+ this.isCreated = false
9
+
10
+ this.hasChanged = false
11
+ this.autoInferTimer = null
12
+
13
+ this.adjustedLetters = new Set()
14
+
15
+ this.LEFT_RIGHT_SEQ_PADDING = 20
16
+ this.EDITOR_HEIGHT = 150
17
+ this.LETTERS_Y_OFFSET = 40
18
+ this.SLIDER_GRABBER_H = 15
19
+ this.MIN_LETTER_LENGTH = 20
20
+ this.MAX_LETTER_LENGTH = 100
21
+ this.SPACE_BETWEEN_LETTERS = 5
22
+
23
+ this.default_pitchSliderRange = 4
24
+ this.pitchSliderRange = 4
25
+ this.duration_visual_size_multiplier = 1
26
+
27
+ this.default_MIN_ENERGY = 3.45
28
+ this.MIN_ENERGY = 3.45
29
+ this.default_MAX_ENERGY = 4.35
30
+ this.MAX_ENERGY = 4.35
31
+ this.ENERGY_GRABBER_RADIUS = 8
32
+ this.EMOTION_STYLE_GRABBER_RADIUS = 8
33
+
34
+ this.MIN_EMOTIONS = 0
35
+ this.MAX_EMOTIONS = 1.06
36
+
37
+ this.MIN_STYLES = 0
38
+ this.MAX_STYLES = 1.06
39
+
40
+ this.clear() // And thus init
41
+
42
+ this.registeredStyleKeys = []
43
+ this.historyState = [] // TODO, add support for undo/redo across all editor functions
44
+ }
45
+
46
+ clear () {
47
+ this.sliderBoxes = []
48
+ this.grabbers = [] // Pitch (original single grabber)
49
+ this.energyGrabbers = []
50
+ this.emAngryGrabbers = []
51
+ this.emHappyGrabbers = []
52
+ this.emSadGrabbers = []
53
+ this.emSurpriseGrabbers = []
54
+
55
+ this.styleGrabbers = {}
56
+ this.styleValuesNew = {}
57
+ this.styleValuesReset = {}
58
+ this.multiLetterStyleDelta = {}
59
+ this.multiLetterStartStyleVals = {}
60
+
61
+ this.letters = []
62
+ this.pitchNew = []
63
+ this.dursNew = []
64
+ this.energyNew = []
65
+ this.emAngryNew = []
66
+ this.emHappyNew = []
67
+ this.emSadNew = []
68
+ this.emSurpriseNew = []
69
+ this.pacing = 1
70
+ this.ampFlatCounter = 0
71
+
72
+ this.inputSequence = undefined
73
+ this.currentVoice = undefined
74
+ this.letterFocus = []
75
+ this.lastSelected = 0
76
+ this.letterClasses = []
77
+ this.resetDurs = []
78
+ this.resetPitch = []
79
+ this.resetEnergy = []
80
+ this.resetEmAngry = []
81
+ this.resetEmHappy = []
82
+ this.resetEmSad = []
83
+ this.resetEmSurprise = []
84
+
85
+ this.multiLetterPitchDelta = undefined
86
+ this.multiLetterStartPitchVals = []
87
+ this.multiLetterStartDursVals = []
88
+ this.multiLetterEnergyDelta = undefined
89
+ this.multiLetterStartEnergyVals = []
90
+ this.multiLetterEmotionDelta = undefined
91
+ this.multiLetterStartEmotionVals = []
92
+
93
+ this.multiLetterLengthDelta = undefined
94
+ this.multiLetterStartLengthVals = []
95
+ }
96
+
97
+ loadStylesData (editorStyles) {
98
+ this.registeredStyleKeys = []
99
+ Object.keys(window.appState.currentModelEmbeddings).forEach(styleKey => {
100
+ if (styleKey=="default") return
101
+ this.registeredStyleKeys.push(styleKey)
102
+
103
+ this.styleGrabbers[styleKey] = []
104
+ this.styleValuesReset[styleKey] = this.resetPitch.map(v=>0)
105
+ this.styleValuesNew[styleKey] = (editorStyles&&editorStyles[styleKey]&&editorStyles[styleKey].sliders) ? editorStyles[styleKey].sliders : this.resetPitch.map(v=>0)
106
+ this.multiLetterStyleDelta[styleKey] = undefined
107
+ this.multiLetterStartStyleVals[styleKey] = []
108
+ })
109
+ }
110
+
111
+ init () {
112
+
113
+ // Clear away old instance
114
+ if (this.isCreated) {
115
+ editorContainer.innerHTML = ""
116
+ delete this.canvas
117
+ delete this.context
118
+ }
119
+
120
+ let canvasWidth = 0
121
+ if (this.sliderBoxes.length) {
122
+ canvasWidth = this.sliderBoxes.at(-1).getX() + this.SPACE_BETWEEN_LETTERS * 2 + 100
123
+ } else {
124
+ canvasWidth = this.LEFT_RIGHT_SEQ_PADDING*2 // Padding
125
+ this.dursNew.forEach((dur,di) => {
126
+ if (di) {
127
+ canvasWidth += this.SPACE_BETWEEN_LETTERS
128
+ }
129
+
130
+ let value = dur
131
+ value = value * this.pacing
132
+ value = Math.max(0.1, value)
133
+ value = Math.min(value, 20)
134
+ const percentAcross = value/20
135
+ const width = percentAcross * (this.MAX_LETTER_LENGTH-this.MIN_LETTER_LENGTH) + this.MIN_LETTER_LENGTH
136
+ canvasWidth += width
137
+ })
138
+ }
139
+
140
+ this.canvas = document.createElement("canvas")
141
+ this.context = this.canvas.getContext("2d")
142
+ this.context.textAlign = "center"
143
+ this.canvas.width = canvasWidth
144
+ this.canvas.height = 200
145
+
146
+ editorContainer.appendChild(this.canvas)
147
+
148
+
149
+ // Mouse cursor
150
+ this.canvas.addEventListener("mousemove", event => {
151
+ const mouseX = parseInt(event.offsetX)
152
+ const mouseY = parseInt(event.offsetY)
153
+ this.canvas.style.cursor = "default"
154
+
155
+ // Check energy grabber hover
156
+ const isOnEGrabber = seq_edit_view_select.value.includes("energy") && this.energyGrabbers.find((eGrabber, egi) => {
157
+ if (!this.enabled_disabled_items[egi]) return
158
+ const grabberX = eGrabber.getXLeft()+eGrabber.sliderBox.width/2-this.ENERGY_GRABBER_RADIUS
159
+ return (mouseX>grabberX && mouseX<grabberX+this.ENERGY_GRABBER_RADIUS*2+4) && (mouseY>eGrabber.topLeftY-this.ENERGY_GRABBER_RADIUS-2 && mouseY<eGrabber.topLeftY+this.ENERGY_GRABBER_RADIUS+2)
160
+ })
161
+ if (isOnEGrabber && isOnEGrabber!=undefined) {
162
+ this.canvas.style.cursor = "row-resize"
163
+ return
164
+ }
165
+
166
+ // One grabber type
167
+ if (seq_edit_view_select.value !== "pitch_energy") {
168
+ this.sliderBoxes.forEach((sbox, sboxi) => {
169
+ if (!this.enabled_disabled_items[sboxi]) return;
170
+
171
+ // is outside slider box => return
172
+ if (!(
173
+ mouseX>sbox.getXLeft()
174
+ && mouseX<sbox.getXLeft() + sbox.width
175
+ )) {
176
+ return;
177
+ }
178
+
179
+ this.canvas.style.cursor = "row-resize"
180
+ })
181
+ }
182
+
183
+ // Check emotion grabber hover
184
+ const isHoveringOverEmotionGrabber = emotionGrabbers => {
185
+ return emotionGrabbers.find((eGrabber, egi) => {
186
+ if (!this.enabled_disabled_items[egi]) return
187
+ const grabberX = eGrabber.getXLeft()+eGrabber.sliderBox.width/2-this.EMOTION_STYLE_GRABBER_RADIUS
188
+ return (mouseX>grabberX && mouseX<grabberX+this.EMOTION_STYLE_GRABBER_RADIUS*2+4) && (mouseY>eGrabber.topLeftY-this.EMOTION_STYLE_GRABBER_RADIUS-2 && mouseY<eGrabber.topLeftY+this.EMOTION_STYLE_GRABBER_RADIUS+2)
189
+ })
190
+ }
191
+ if (window.currentModel.modelType=="xVAPitch") {
192
+ const isOnEmGrabber = seq_edit_view_select.value.startsWith("em") && (isHoveringOverEmotionGrabber(this.emAngryGrabbers) || isHoveringOverEmotionGrabber(this.emHappyGrabbers) || isHoveringOverEmotionGrabber(this.emSadGrabbers) || isHoveringOverEmotionGrabber(this.emSurpriseGrabbers))
193
+ if (isOnEmGrabber && isOnEmGrabber!=undefined) {
194
+ this.canvas.style.cursor = "row-resize"
195
+ return
196
+ }
197
+ }
198
+ // Check pitch grabber hover
199
+ const isOnGrabber = seq_edit_view_select.value.includes("pitch") && this.grabbers.find((grabber, gi) => {
200
+ if (!this.enabled_disabled_items[gi]) return
201
+ const grabberX = grabber.getXLeft()
202
+ return (mouseX>grabberX && mouseX<grabberX+grabber.width) && (mouseY>grabber.topLeftY && mouseY<grabber.topLeftY+grabber.height)
203
+ })
204
+ if (isOnGrabber && isOnGrabber!=undefined) {
205
+ this.canvas.style.cursor = "n-resize"
206
+ return
207
+ }
208
+
209
+ // Check styles grabbers
210
+ if (this.registeredStyleKeys && this.registeredStyleKeys.length) {
211
+ let isOnStyleGrabber
212
+ this.registeredStyleKeys.forEach(styleKey => {
213
+ if (isOnStyleGrabber) return // Skip unnecessary work if already found
214
+ if (seq_edit_view_select.value.startsWith("style_") && seq_edit_view_select.value.includes(styleKey)) {
215
+ isOnStyleGrabber = this.styleGrabbers[styleKey].find((grabber, gi) => {
216
+ if (!this.enabled_disabled_items[gi]) return
217
+ const grabberX = grabber.getXLeft()
218
+ return (mouseX>grabberX && mouseX<grabberX+grabber.width) && (mouseY>grabber.topLeftY && mouseY<grabber.topLeftY+grabber.height)
219
+ })
220
+ if (isOnStyleGrabber) {
221
+ this.canvas.style.cursor = "row-resize"
222
+ return
223
+ }
224
+ }
225
+ })
226
+ }
227
+
228
+ // Check letter hover
229
+ const isOnLetter = this.letterClasses.find((letter, l) => {
230
+ if (!this.enabled_disabled_items[l]) return
231
+ return (mouseY<this.LETTERS_Y_OFFSET) && (mouseX>this.sliderBoxes[l].getXLeft() && mouseX<this.sliderBoxes[l].getXLeft()+this.sliderBoxes[l].width)
232
+ })
233
+ if (isOnLetter!=undefined) {
234
+ this.canvas.style.cursor = "pointer"
235
+ return
236
+ }
237
+ // Check box length dragger
238
+ const isBetweenBoxes = this.sliderBoxes.find((box, bi) => {
239
+ if (!this.enabled_disabled_items[bi]) return
240
+ const boxX = box.getXLeft()
241
+ return (mouseY>box.topY && mouseY<box.topY+box.height) && (mouseX>(boxX+box.width-10) && mouseX<(boxX+box.width+10)+5)
242
+ })
243
+ if (isBetweenBoxes!=undefined) {
244
+ this.canvas.style.cursor = "w-resize"
245
+ return
246
+ }
247
+ })
248
+
249
+ let elemDragged = undefined
250
+ let mouseDownStart = {x: undefined, y: undefined}
251
+ this.canvas.addEventListener("mousedown", event => {
252
+ const mouseX = parseInt(event.offsetX)
253
+ const mouseY = parseInt(event.offsetY)
254
+ mouseDownStart.x = mouseX
255
+ mouseDownStart.y = mouseY
256
+
257
+ // Check up-down pitch dragging box first
258
+ const isOnGrabber = seq_edit_view_select.value.includes("pitch") && this.grabbers.find((grabber, gi) => {
259
+ if (!this.enabled_disabled_items[gi]) return
260
+ const grabberX = grabber.getXLeft()
261
+ return (mouseX>grabberX && mouseX<grabberX+grabber.width) && (mouseY>grabber.topLeftY && mouseY<grabber.topLeftY+grabber.height)
262
+ })
263
+ if (isOnGrabber) {
264
+
265
+ const slider = isOnGrabber
266
+ if (this.letterFocus.length <= 1 || (!this.letterFocus.includes(slider.index))) {
267
+ this.setLetterFocus(this.grabbers.indexOf(slider), event.ctrlKey, event.shiftKey, event.altKey)
268
+ }
269
+ this.multiLetterPitchDelta = slider.topLeftY
270
+ this.multiLetterStartPitchVals = this.grabbers.map(slider => slider.topLeftY)
271
+
272
+ elemDragged = isOnGrabber
273
+ return
274
+ }
275
+
276
+ // Check sideways dragging
277
+ const isBetweenBoxes = this.sliderBoxes.find((box, bi) => {
278
+ if (!this.enabled_disabled_items[bi]) return
279
+ const boxX = box.getXLeft()
280
+ return (mouseY>box.topY && mouseY<box.topY+box.height) && (mouseX>(boxX+box.width-10) && mouseX<(boxX+box.width+10)+5)
281
+ })
282
+ if (isBetweenBoxes) {
283
+ this.multiLetterStartDursVals = this.sliderBoxes.map(box => box.width)
284
+
285
+ isBetweenBoxes.dragStart.width = isBetweenBoxes.width
286
+ elemDragged = isBetweenBoxes
287
+ return
288
+ }
289
+
290
+ // Check up-down emotion dragging
291
+ const findGrabber = emotionGrabbers => {
292
+ return emotionGrabbers.find((eGrabber, egi) => {
293
+ if (!this.enabled_disabled_items[egi]) return
294
+ const boxX = eGrabber.sliderBox.getXLeft()
295
+ return (
296
+ (mouseX > boxX)
297
+ && (mouseX < (boxX + eGrabber.sliderBox.width))
298
+ )
299
+ })
300
+ }
301
+ const handleEmGrabber = (emGrabber, grabbersList) => {
302
+ if (this.letterFocus.length <= 1 || (!this.letterFocus.includes(emGrabber.index))) {
303
+ this.setLetterFocus(grabbersList.indexOf(emGrabber), event.ctrlKey, event.shiftKey, event.altKey)
304
+ }
305
+ this.multiLetterEmotionDelta = emGrabber.topLeftY
306
+ this.multiLetterStartEmotionVals = grabbersList.map(emGrabber => emGrabber.topLeftY)
307
+
308
+ return emGrabber
309
+ }
310
+ const handleStyleGrabber = (styleGrabber, grabbersList, styleKey) => {
311
+ if (this.letterFocus.length <= 1 || (!this.letterFocus.includes(styleGrabber.index))) {
312
+ this.setLetterFocus(grabbersList.indexOf(styleGrabber), event.ctrlKey, event.shiftKey, event.altKey)
313
+ }
314
+ this.multiLetterStyleDelta[styleKey] = styleGrabber.topLeftY
315
+ this.multiLetterStartStyleVals[styleKey] = grabbersList.map(styleGrabber => styleGrabber.topLeftY)
316
+ return styleGrabber
317
+ }
318
+
319
+ // Check up-down energy dragging
320
+ const isOnEGrabber = seq_edit_view_select.value.includes("energy") && this.energyGrabbers.find((eGrabber, egi) => {
321
+ if (!this.enabled_disabled_items[egi]) return
322
+ const grabberX = eGrabber.getXLeft()+eGrabber.sliderBox.width/2-this.ENERGY_GRABBER_RADIUS
323
+ return (mouseX>grabberX && mouseX<grabberX+this.ENERGY_GRABBER_RADIUS*2+4) && (mouseY>eGrabber.topLeftY-this.ENERGY_GRABBER_RADIUS-2 && mouseY<eGrabber.topLeftY+this.ENERGY_GRABBER_RADIUS+2)
324
+ })
325
+ if (isOnEGrabber) {
326
+
327
+ const eGrabber = isOnEGrabber
328
+ if (this.letterFocus.length <= 1 || (!this.letterFocus.includes(eGrabber.index))) {
329
+ this.setLetterFocus(this.energyGrabbers.indexOf(eGrabber), event.ctrlKey, event.shiftKey, event.altKey)
330
+ }
331
+ this.multiLetterEnergyDelta = eGrabber.topLeftY
332
+ this.multiLetterStartEnergyVals = this.energyGrabbers.map(eGrabber => eGrabber.topLeftY)
333
+
334
+ elemDragged = isOnEGrabber
335
+ return
336
+ }
337
+
338
+ // Check clicking on the top letters
339
+ const isOnLetter = this.letterClasses.find((letter, l) => {
340
+ if (!this.enabled_disabled_items[l]) return
341
+ return (mouseY<this.LETTERS_Y_OFFSET) && (mouseX>this.sliderBoxes[l].getXLeft() && mouseX<this.sliderBoxes[l].getXLeft()+this.sliderBoxes[l].width)
342
+ })
343
+
344
+ if (isOnLetter) {
345
+ this.setLetterFocus(this.letterClasses.indexOf(isOnLetter), event.ctrlKey, event.shiftKey, event.altKey)
346
+ return;
347
+ }
348
+
349
+ // Not on letter
350
+
351
+ // Drag grabber when only single type of grabber
352
+ if (seq_edit_view_select.value === "pitch_energy")
353
+ {
354
+ return;
355
+ }
356
+ // Fetch any grabber within letter column
357
+ const isOnGrabberCol = seq_edit_view_select.value=="pitch" && findGrabber(this.grabbers)
358
+ if (isOnGrabberCol) {
359
+ const slider = isOnGrabberCol
360
+ this.multiLetterPitchDelta = slider.topLeftY
361
+ this.multiLetterStartPitchVals = this.grabbers.map(slider => slider.topLeftY)
362
+
363
+ elemDragged = isOnGrabberCol
364
+ return
365
+ }
366
+ const isOnEGrabberCol = seq_edit_view_select.value=="energy" && findGrabber(this.energyGrabbers)
367
+ if (isOnEGrabberCol) {
368
+ this.multiLetterEnergyDelta = isOnEGrabberCol.topLeftY
369
+ this.multiLetterStartEnergyVals = this.energyGrabbers.map(isOnEGrabberCol => isOnEGrabberCol.topLeftY)
370
+
371
+ elemDragged = isOnEGrabberCol
372
+ return
373
+ }
374
+
375
+ if (window.currentModel.modelType !== "xVAPitch") {
376
+ return
377
+ }
378
+
379
+ // v3 model
380
+ if (seq_edit_view_select.value.startsWith("style_") && this.registeredStyleKeys.length) {
381
+ this.registeredStyleKeys.forEach(styleKey => {
382
+ if (seq_edit_view_select.value.includes(styleKey)) {
383
+ const isOnStyleGrabber = findGrabber(this.styleGrabbers[styleKey])
384
+ if (isOnStyleGrabber) {
385
+ elemDragged = handleStyleGrabber(isOnStyleGrabber, this.styleGrabbers[styleKey], styleKey)
386
+ return
387
+ }
388
+ }
389
+ })
390
+ }
391
+
392
+ const isOnEmAngryGrabber = seq_edit_view_select.value=="emAngry" && findGrabber(this.emAngryGrabbers)
393
+ if (isOnEmAngryGrabber) {
394
+ elemDragged = handleEmGrabber(isOnEmAngryGrabber, this.emAngryGrabbers)
395
+ return
396
+ }
397
+ const isOnEmHappyGrabber = seq_edit_view_select.value=="emHappy" && findGrabber(this.emHappyGrabbers)
398
+ if (isOnEmHappyGrabber) {
399
+ elemDragged = handleEmGrabber(isOnEmHappyGrabber, this.emHappyGrabbers)
400
+ return
401
+ }
402
+ const isOnEmSadGrabber = seq_edit_view_select.value=="emSad" && findGrabber(this.emSadGrabbers)
403
+ if (isOnEmSadGrabber) {
404
+ elemDragged = handleEmGrabber(isOnEmSadGrabber, this.emSadGrabbers)
405
+ return
406
+ }
407
+ const isOnEmSurpriseGrabber = seq_edit_view_select.value=="emSurprise" && findGrabber(this.emSurpriseGrabbers)
408
+ if (isOnEmSurpriseGrabber) {
409
+ elemDragged = handleEmGrabber(isOnEmSurpriseGrabber, this.emSurpriseGrabbers)
410
+ return
411
+ }
412
+ })
413
+
414
+ this.canvas.addEventListener("mouseup", event => {
415
+ mouseDownStart = {x: undefined, y: undefined}
416
+ if (autoplay_ckbx.checked && this.hasChanged) {
417
+ generateVoiceButton.click()
418
+ }
419
+ this.init()
420
+ })
421
+
422
+ this.canvas.addEventListener("mousemove", event => {
423
+ if (mouseDownStart.x && mouseDownStart.y) {
424
+
425
+ if (elemDragged && (parseInt(event.offsetX)-mouseDownStart.x || parseInt(event.offsetY)-mouseDownStart.y)) {
426
+ this.hasChanged = true
427
+ this.letterFocus.forEach(index => this.adjustedLetters.add(index))
428
+ }
429
+
430
+ if (elemDragged) {
431
+ if (elemDragged.type=="slider") { // Pitch sliders, specifically
432
+
433
+ elemDragged.setValueFromCoords(parseInt(event.offsetY)-elemDragged.height/2)
434
+
435
+ // If there's a multi-selection, update all of their values, otherwise update the numerical input
436
+ if (this.letterFocus.length>1) {
437
+ this.letterFocus.forEach(li => {
438
+ if (li!=elemDragged.index) {
439
+ this.grabbers[li].setValueFromCoords(this.multiLetterStartPitchVals[li]+(elemDragged.topLeftY-this.multiLetterPitchDelta))
440
+ }
441
+ })
442
+ } else {
443
+ letterPitchNumb.value = parseInt(this.pitchNew[elemDragged.index]*100)/100
444
+ }
445
+
446
+
447
+ } else if (elemDragged.type=="box") { // Durations being dragged sideways
448
+
449
+ // If there's a multi-selection, update all of their values, otherwise update the numerical input
450
+ if (this.letterFocus.length>1) {
451
+ this.letterFocus.forEach(li => {
452
+ let newWidth = this.multiLetterStartDursVals[li] + parseInt(elemDragged.width - elemDragged.dragStart.width)
453
+ newWidth = Math.max(20, newWidth)
454
+ newWidth = Math.min(newWidth, this.MAX_LETTER_LENGTH)
455
+
456
+ this.sliderBoxes[li].width = newWidth
457
+
458
+ this.sliderBoxes[li].percentAcross = (this.sliderBoxes[li].width-20) / (this.MAX_LETTER_LENGTH-20)
459
+ this.dursNew[this.sliderBoxes[li].index] = Math.max(0.1, this.sliderBoxes[li].percentAcross*20)
460
+
461
+ this.sliderBoxes[li].grabber.width = this.sliderBoxes[li].width-2
462
+ this.sliderBoxes[li].letter.centerX = this.sliderBoxes[li].leftX + this.sliderBoxes[li].width/2
463
+ })
464
+ } else {
465
+
466
+ letterLengthNumb.value = parseInt(this.dursNew[elemDragged.index]*100)/100
467
+ }
468
+
469
+
470
+ let newWidth = elemDragged.dragStart.width + parseInt(event.offsetX)-mouseDownStart.x
471
+ newWidth = Math.max(20, newWidth)
472
+ newWidth = Math.min(newWidth, this.MAX_LETTER_LENGTH)
473
+ elemDragged.width = newWidth
474
+
475
+ elemDragged.percentAcross = (elemDragged.width-20) / (this.MAX_LETTER_LENGTH-20)
476
+ this.dursNew[elemDragged.index] = Math.max(0.1, elemDragged.percentAcross*20)
477
+
478
+ elemDragged.grabber.width = elemDragged.width-2
479
+ elemDragged.letter.centerX = elemDragged.leftX + elemDragged.width/2
480
+
481
+ } else if (elemDragged.type=="energy_slider") { // Energy sliders
482
+
483
+ elemDragged.setValueFromCoords(parseInt(event.offsetY)-elemDragged.height/2)
484
+
485
+ // If there's a multi-selection, update all of their values, otherwise update the numerical input
486
+ if (this.letterFocus.length>1) {
487
+ this.letterFocus.forEach(li => {
488
+ if (li!=elemDragged.index) {
489
+ this.energyGrabbers[li].setValueFromCoords(this.multiLetterStartEnergyVals[li]+(elemDragged.topLeftY-this.multiLetterEnergyDelta))
490
+ }
491
+ })
492
+ } else {
493
+ letterEnergyNumb.value = parseInt(this.energyNew[elemDragged.index]*100)/100
494
+ }
495
+
496
+ } else if (elemDragged.type=="emotion_slider") { // Emotion sliders
497
+
498
+ elemDragged.setValueFromCoords(parseInt(event.offsetY)-elemDragged.height/2)
499
+
500
+ // If there's a multi-selection, update all of their values, otherwise update the numerical input
501
+ if (this.letterFocus.length>1) {
502
+ this.letterFocus.forEach(li => {
503
+ if (li!=elemDragged.index) {
504
+ if (seq_edit_view_select.value=="emAngry") {
505
+ this.emAngryGrabbers[li].setValueFromCoords(this.multiLetterStartEmotionVals[li]+(elemDragged.topLeftY-this.multiLetterEmotionDelta))
506
+ } else if (seq_edit_view_select.value=="emHappy") {
507
+ this.emHappyGrabbers[li].setValueFromCoords(this.multiLetterStartEmotionVals[li]+(elemDragged.topLeftY-this.multiLetterEmotionDelta))
508
+ } else if (seq_edit_view_select.value=="emSad") {
509
+ this.emSadGrabbers[li].setValueFromCoords(this.multiLetterStartEmotionVals[li]+(elemDragged.topLeftY-this.multiLetterEmotionDelta))
510
+ } else if (seq_edit_view_select.value=="emSurprise") {
511
+ this.emSurpriseGrabbers[li].setValueFromCoords(this.multiLetterStartEmotionVals[li]+(elemDragged.topLeftY-this.multiLetterEmotionDelta))
512
+ }
513
+ }
514
+ })
515
+ } else {
516
+ if (seq_edit_view_select.value=="emAngry") {
517
+ letterEmotionNumb.value = parseFloat(this.emAngryNew[elemDragged.index]*100)/100
518
+ } else if (seq_edit_view_select.value=="emHappy") {
519
+ letterEmotionNumb.value = parseFloat(this.emHappyNew[elemDragged.index]*100)/100
520
+ } else if (seq_edit_view_select.value=="emSad") {
521
+ letterEmotionNumb.value = parseFloat(this.emSadNew[elemDragged.index]*100)/100
522
+ } else if (seq_edit_view_select.value=="emSurprise") {
523
+ letterEmotionNumb.value = parseFloat(this.emSurpriseNew[elemDragged.index]*100)/100
524
+ }
525
+ }
526
+ } else if (elemDragged.type=="style_slider") { // Style sliders
527
+
528
+ elemDragged.setValueFromCoords(parseInt(event.offsetY)-elemDragged.height/2)
529
+
530
+ if (this.registeredStyleKeys.length) {
531
+ this.registeredStyleKeys.forEach(styleKey => {
532
+ if (seq_edit_view_select.value.startsWith("style_") && seq_edit_view_select.value.includes(styleKey)) {
533
+ // If there's a multi-selection, update all of their values, otherwise update the numerical input
534
+ if (this.letterFocus.length>1) {
535
+ this.letterFocus.forEach(li => {
536
+ if (li!=elemDragged.index) {
537
+ this.styleGrabbers[styleKey][li].setValueFromCoords(this.multiLetterStartStyleVals[styleKey][li]+(elemDragged.topLeftY-this.multiLetterStyleDelta[styleKey]))
538
+ }
539
+ })
540
+ } else {
541
+ letterStyleNumb.value = parseInt(this.styleValuesNew[styleKey][elemDragged.index]*100)/100
542
+ }
543
+ }
544
+ })
545
+ }
546
+ }
547
+ }
548
+ }
549
+ })
550
+
551
+ if (!this.isCreated) {
552
+ this.render()
553
+ }
554
+ this.isCreated = true
555
+ }
556
+
557
+
558
+
559
+
560
+ render () {
561
+ if (this.context!=undefined) {
562
+ this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
563
+ this.letterClasses.forEach((letter, li) => {
564
+ if (this.letters[li]=="<PAD>") return
565
+ letter.context = this.context
566
+ letter.render()
567
+ })
568
+ this.sliderBoxes.forEach((sliderBox, sbi) => {
569
+ if (this.letters[sbi]=="<PAD>") return
570
+ sliderBox.context = this.context
571
+ sliderBox.render()
572
+ })
573
+ if (seq_edit_view_select.value=="pitch_energy" || seq_edit_view_select.value=="pitch") {
574
+ this.grabbers.forEach((grabber,gi) => {
575
+ if (this.letters[gi]=="<PAD>") return
576
+ grabber.context = this.context
577
+ grabber.render()
578
+ })
579
+ }
580
+ if (seq_edit_view_select.value=="pitch_energy" || seq_edit_view_select.value=="energy") {
581
+ this.energyGrabbers.forEach((eGrabber, egi) => {
582
+ if (this.letters[egi]=="<PAD>") return
583
+ eGrabber.context = this.context
584
+ eGrabber.render()
585
+ })
586
+ }
587
+ if (window.currentModel.modelType=="xVAPitch") {
588
+ if (seq_edit_view_select.value=="emAngry") {
589
+ this.emAngryGrabbers.forEach((eGrabber, egi) => {
590
+ if (this.letters[egi]=="<PAD>") return
591
+ eGrabber.context = this.context
592
+ eGrabber.render()
593
+ })
594
+ }
595
+ if (seq_edit_view_select.value=="emHappy") {
596
+ this.emHappyGrabbers.forEach((eGrabber, egi) => {
597
+ if (this.letters[egi]=="<PAD>") return
598
+ eGrabber.context = this.context
599
+ eGrabber.render()
600
+ })
601
+ }
602
+ if (seq_edit_view_select.value=="emSad") {
603
+ this.emSadGrabbers.forEach((eGrabber, egi) => {
604
+ if (this.letters[egi]=="<PAD>") return
605
+ eGrabber.context = this.context
606
+ eGrabber.render()
607
+ })
608
+ }
609
+ if (seq_edit_view_select.value=="emSurprise") {
610
+ this.emSurpriseGrabbers.forEach((eGrabber, egi) => {
611
+ if (this.letters[egi]=="<PAD>") return
612
+ eGrabber.context = this.context
613
+ eGrabber.render()
614
+ })
615
+ }
616
+ if (this.registeredStyleKeys && this.registeredStyleKeys.length) {
617
+ this.registeredStyleKeys.forEach(styleKey => {
618
+ if (seq_edit_view_select.value.startsWith("style_") && seq_edit_view_select.value.includes(styleKey)) {
619
+ this.styleGrabbers[styleKey].forEach((styleGrabber, sgi) => {
620
+ if (this.letters[sgi]=="<PAD>") return
621
+ styleGrabber.context = this.context
622
+ styleGrabber.render()
623
+ })
624
+ }
625
+ })
626
+ }
627
+ }
628
+ }
629
+ requestAnimationFrame(() => {this.render()})
630
+ }
631
+
632
+
633
+
634
+
635
+ update (modelType=undefined, sliderRange=undefined) {
636
+
637
+ self.modelType = modelType
638
+
639
+ // Make model-specific adjustments
640
+ if (modelType=="xVAPitch") {
641
+ this.default_pitchSliderRange = 6
642
+ this.pitchSliderRange = sliderRange || 6
643
+ this.duration_visual_size_multiplier = 1
644
+ this.MAX_LETTER_LENGTH = 200
645
+ this.default_MIN_ENERGY = 0
646
+ this.MIN_ENERGY = 0
647
+ this.default_MAX_ENERGY = 1.07
648
+ this.MAX_ENERGY = 1.07
649
+
650
+ this.styleGrabbers = {}
651
+ this.registeredStyleKeys.forEach(styleKey => {
652
+ this.styleGrabbers[styleKey] = []
653
+ })
654
+
655
+ } else {
656
+ this.default_pitchSliderRange = 4
657
+ this.pitchSliderRange = sliderRange || 4
658
+ this.duration_visual_size_multiplier = 1
659
+ this.MAX_LETTER_LENGTH = 100
660
+ }
661
+
662
+ this.letterClasses = []
663
+ this.sliderBoxes = []
664
+ this.grabbers = []
665
+ this.energyGrabbers = []
666
+ this.emAngryGrabbers = []
667
+ this.emHappyGrabbers = []
668
+ this.emSadGrabbers = []
669
+ this.emSurpriseGrabbers = []
670
+
671
+ this.enabled_disabled_items = []
672
+
673
+ let xCounter = 0
674
+ let lastBox = undefined
675
+ let letter_counter = 0
676
+ this.letters.forEach((letter, li) => {
677
+
678
+ if (letter=="<PAD>") {
679
+ this.enabled_disabled_items.push(false)
680
+ letter_counter += 1
681
+ } else {
682
+ this.enabled_disabled_items.push(true)
683
+ }
684
+ letter_counter += 1
685
+
686
+ const dur = this.dursNew[li]
687
+ const width = Math.max(25, dur*10)
688
+
689
+
690
+ // Slider box
691
+ const sliderBox = new SliderBox(this.context, li, lastBox, this.LETTERS_Y_OFFSET, this.EDITOR_HEIGHT, this.MIN_LETTER_LENGTH, this.MAX_LETTER_LENGTH, letter_counter%2==0)
692
+ sliderBox.render()
693
+ if (lastBox) {
694
+ lastBox.rightBox = sliderBox
695
+ }
696
+ lastBox = sliderBox
697
+ this.sliderBoxes.push(sliderBox)
698
+
699
+ // Letter text
700
+ const letterClass = new Letter(this.context, li, letter, sliderBox, 20, 20+xCounter, width)
701
+ if (this.letterFocus.includes(li)) {
702
+ letterClass.colour = "red"
703
+ }
704
+ letterClass.render()
705
+ this.letterClasses.push(letterClass)
706
+
707
+ // Slider grabber thing
708
+ const pitchPercent = 1-(this.pitchNew[li]+this.pitchSliderRange)/(this.pitchSliderRange*2)
709
+ const grabber = new SliderGrabber(this.context, li, sliderBox, (this.LETTERS_Y_OFFSET+1)+(this.SLIDER_GRABBER_H/2)+((this.EDITOR_HEIGHT-2)-this.SLIDER_GRABBER_H)*pitchPercent-this.SLIDER_GRABBER_H/2, width-2, this.SLIDER_GRABBER_H, this.pitchSliderRange)
710
+ grabber.render()
711
+ this.grabbers.push(grabber)
712
+
713
+ if (this.energyNew && this.energyNew.length) {
714
+ // Energy round grabber
715
+ let energyPercent
716
+ if (modelType=="xVAPitch") {
717
+ energyPercent = ( (this.energyNew[li]-this.MIN_ENERGY) / (this.MAX_ENERGY-this.MIN_ENERGY) )
718
+ } else {
719
+ energyPercent = 1 - ( (this.energyNew[li]-this.MIN_ENERGY) / (this.MAX_ENERGY-this.MIN_ENERGY) )
720
+ }
721
+ energyPercent = Math.max(0, energyPercent)
722
+ energyPercent = Math.min(energyPercent, 1)
723
+
724
+ let topLeftY = (1 - energyPercent) * (this.EDITOR_HEIGHT-2-this.ENERGY_GRABBER_RADIUS) + (this.LETTERS_Y_OFFSET)
725
+ const energyGrabber = new EnergyEmotionGrabber(this.context, li, sliderBox, topLeftY, width-2, this.ENERGY_GRABBER_RADIUS, undefined, modelType, this.ENERGY_GRABBER_RADIUS, "energy")
726
+ energyGrabber.render()
727
+ this.energyGrabbers.push(energyGrabber)
728
+ }
729
+
730
+ if (modelType=="xVAPitch") {
731
+ if (this.emAngryNew && this.emAngryNew.length) {
732
+ let emotionPercent = ( (this.emAngryNew[li]-this.MIN_EMOTIONS) / (this.MAX_EMOTIONS-this.MIN_EMOTIONS) )
733
+ emotionPercent = Math.max(0, emotionPercent)
734
+ emotionPercent = Math.min(emotionPercent, 1)
735
+
736
+ let topLeftY = (1 - emotionPercent) * (this.EDITOR_HEIGHT-2-this.EMOTION_STYLE_GRABBER_RADIUS) + (this.LETTERS_Y_OFFSET)
737
+ const emAngryGrabber = new EnergyEmotionGrabber(this.context, li, sliderBox, topLeftY, width-2, this.EMOTION_STYLE_GRABBER_RADIUS, undefined, modelType, this.EMOTION_STYLE_GRABBER_RADIUS, "emotion")
738
+ emAngryGrabber.render()
739
+ this.emAngryGrabbers.push(emAngryGrabber)
740
+ }
741
+ if (this.emHappyNew && this.emHappyNew.length) {
742
+ let emotionPercent = ( (this.emHappyNew[li]-this.MIN_EMOTIONS) / (this.MAX_EMOTIONS-this.MIN_EMOTIONS) )
743
+ emotionPercent = Math.max(0, emotionPercent)
744
+ emotionPercent = Math.min(emotionPercent, 1)
745
+
746
+ let topLeftY = (1 - emotionPercent) * (this.EDITOR_HEIGHT-2-this.EMOTION_STYLE_GRABBER_RADIUS) + (this.LETTERS_Y_OFFSET)
747
+ const emHappyGrabber = new EnergyEmotionGrabber(this.context, li, sliderBox, topLeftY, width-2, this.EMOTION_STYLE_GRABBER_RADIUS, undefined, modelType, this.EMOTION_STYLE_GRABBER_RADIUS, "emotion")
748
+ emHappyGrabber.render()
749
+ this.emHappyGrabbers.push(emHappyGrabber)
750
+ }
751
+ if (this.emSadNew && this.emSadNew.length) {
752
+ let emotionPercent = ( (this.emSadNew[li]-this.MIN_EMOTIONS) / (this.MAX_EMOTIONS-this.MIN_EMOTIONS) )
753
+ emotionPercent = Math.max(0, emotionPercent)
754
+ emotionPercent = Math.min(emotionPercent, 1)
755
+
756
+ let topLeftY = (1 - emotionPercent) * (this.EDITOR_HEIGHT-2-this.EMOTION_STYLE_GRABBER_RADIUS) + (this.LETTERS_Y_OFFSET)
757
+ const emSadGrabber = new EnergyEmotionGrabber(this.context, li, sliderBox, topLeftY, width-2, this.EMOTION_STYLE_GRABBER_RADIUS, undefined, modelType, this.EMOTION_STYLE_GRABBER_RADIUS, "emotion")
758
+ emSadGrabber.render()
759
+ this.emSadGrabbers.push(emSadGrabber)
760
+ }
761
+ if (this.emSurpriseNew && this.emSurpriseNew.length) {
762
+ let emotionPercent = ( (this.emSurpriseNew[li]-this.MIN_EMOTIONS) / (this.MAX_EMOTIONS-this.MIN_EMOTIONS) )
763
+ emotionPercent = Math.max(0, emotionPercent)
764
+ emotionPercent = Math.min(emotionPercent, 1)
765
+
766
+ let topLeftY = (1 - emotionPercent) * (this.EDITOR_HEIGHT-2-this.EMOTION_STYLE_GRABBER_RADIUS) + (this.LETTERS_Y_OFFSET)
767
+ const emSurpriseGrabber = new EnergyEmotionGrabber(this.context, li, sliderBox, topLeftY, width-2, this.EMOTION_STYLE_GRABBER_RADIUS, undefined, modelType, this.EMOTION_STYLE_GRABBER_RADIUS, "emotion")
768
+ emSurpriseGrabber.render()
769
+ this.emSurpriseGrabbers.push(emSurpriseGrabber)
770
+ }
771
+
772
+ // Initialize grabbers dynamically for every style
773
+ this.registeredStyleKeys.forEach(styleKey => {
774
+ let stylePercent = ( (this.styleValuesNew[styleKey][li]-this.MIN_STYLES) / (this.MAX_STYLES-this.MIN_STYLES) )
775
+ stylePercent = Math.max(0, stylePercent)
776
+ stylePercent = Math.min(stylePercent, 1)
777
+
778
+ let topLeftY = (1 - stylePercent) * (this.EDITOR_HEIGHT-2-this.EMOTION_STYLE_GRABBER_RADIUS) + (this.LETTERS_Y_OFFSET)
779
+ const styleGrabber = new EnergyEmotionGrabber(this.context, li, sliderBox, topLeftY, width-2, this.EMOTION_STYLE_GRABBER_RADIUS, undefined, modelType, this.EMOTION_STYLE_GRABBER_RADIUS, "style")
780
+ styleGrabber.render()
781
+ this.styleGrabbers[styleKey].push(styleGrabber)
782
+ })
783
+ }
784
+
785
+ sliderBox.letter = letterClass
786
+ sliderBox.grabber = grabber
787
+
788
+ sliderBox.setValueFromValue(dur)
789
+
790
+ xCounter += width + 5
791
+
792
+ })
793
+
794
+ this.canvas.width = this.sliderBoxes.at(-1).getX() + this.SPACE_BETWEEN_LETTERS * 2 + 100
795
+ }
796
+
797
+ setLetterFocus (l, ctrlKey, shiftKey, altKey) {
798
+
799
+ // NONE = Clear selection, add l to selection
800
+ // Ctrl = Add l to existing selection
801
+ // Shift = Add all letters from the last selected letter up to and including l to existing selection
802
+ // Ctrl + Shift = (See Shift)
803
+ // Alt = Clear selection and select word surrounding l (space delimited)
804
+ // Ctrl + Alt = Add word surrounding l to existing selection
805
+ // Shift + Alt = Same as shift, then afterwards add word (space delimited) around l to selection
806
+ // Ctrl + Shift + Alt = (See Shift + Alt)
807
+
808
+ // If nothing is selected and we hold shift, we assume we start from the first letter: at position 0
809
+
810
+ // If we don't press shift or ctrl, we can clear our current selection.
811
+ if (!(ctrlKey || shiftKey) && this.letterFocus.length){
812
+ this.letterFocus.forEach(li => {
813
+ this.letterClasses[li].colour = "black"
814
+ })
815
+ this.letterFocus = []
816
+ this.lastSelected = 0
817
+ }
818
+ if (shiftKey){
819
+ if (l>this.lastSelected) {
820
+ for (let i=this.lastSelected; i<=l; i++) {
821
+ this.letterFocus.push(i)
822
+ }
823
+ } else {
824
+ for (let i=l; i<=this.lastSelected; i++) {
825
+ this.letterFocus.push(i)
826
+ }
827
+ }
828
+ }
829
+ this.letterFocus.push(l) // Push l
830
+ this.lastSelected = l
831
+ if (altKey){
832
+ let l2 = l
833
+ // Looking backwards
834
+ while (l2>=0) {
835
+ let prevLetter = this.letters[l2]
836
+ if (prevLetter!="_") {
837
+ this.letterFocus.push(l2)
838
+ } else {
839
+ break
840
+ }
841
+ l2--
842
+ }
843
+ l2 = l
844
+ // Looking forward
845
+ while (l2<this.letters.length) {
846
+ let nextLetter = this.letters[l2]
847
+ if (nextLetter!="_") {
848
+ this.letterFocus.push(l2)
849
+ } else {
850
+ break
851
+ }
852
+ l2++
853
+ }
854
+ }
855
+
856
+ this.letterFocus = Array.from(new Set(this.letterFocus.sort()))
857
+ this.letterFocus.forEach(li => {
858
+ this.letterClasses[li].colour = "red"
859
+ })
860
+
861
+
862
+ letterStyleNumb.value = ""
863
+ letterStyleNumb.disabled = true
864
+ if (this.letterFocus.length==1) {
865
+ if (this.energyNew.length) {
866
+ letterEnergyNumb.value = parseFloat(this.energyNew[this.letterFocus[0]])
867
+ letterEnergyNumb.disabled = false
868
+ }
869
+ if (this.emAngryNew && this.emAngryNew.length) {
870
+ letterEmotionNumb.value = parseFloat(this.emAngryNew[this.letterFocus[0]])
871
+ letterEmotionNumb.disabled = false
872
+ }
873
+ if (this.emHappyNew && this.emHappyNew.length) {
874
+ letterEmotionNumb.value = parseFloat(this.emHappyNew[this.letterFocus[0]])
875
+ letterEmotionNumb.disabled = false
876
+ }
877
+ if (this.emSadNew && this.emSadNew.length) {
878
+ letterEmotionNumb.value = parseFloat(this.emSadNew[this.letterFocus[0]])
879
+ letterEmotionNumb.disabled = false
880
+ }
881
+ if (this.emSurpriseNew && this.emSurpriseNew.length) {
882
+ letterEmotionNumb.value = parseFloat(this.emSurpriseNew[this.letterFocus[0]])
883
+ letterEmotionNumb.disabled = false
884
+ }
885
+ if (this.registeredStyleKeys) {
886
+ this.registeredStyleKeys.forEach(styleKey => {
887
+ if (seq_edit_view_select.value.startsWith("style_") && seq_edit_view_select.value.includes(styleKey)) {
888
+ letterStyleNumb.value = parseFloat(this.styleValuesNew[styleKey][this.letterFocus[0]])
889
+ letterStyleNumb.disabled = false
890
+ }
891
+ })
892
+ }
893
+ letterPitchNumb.value = parseInt(this.pitchNew[this.letterFocus[0]]*100)/100
894
+ letterLengthNumb.value = parseInt(parseFloat(this.dursNew[this.letterFocus[0]])*100)/100
895
+
896
+ letterPitchNumb.disabled = false
897
+ letterLengthNumb.disabled = false
898
+ } else {
899
+ letterEnergyNumb.disabled = true
900
+ letterEnergyNumb.value = ""
901
+ letterEmotionNumb.disabled = true
902
+ letterEmotionNumb.value = ""
903
+ letterPitchNumb.disabled = true
904
+ letterPitchNumb.value = ""
905
+ letterLengthNumb.disabled = true
906
+ letterLengthNumb.value = ""
907
+ }
908
+ }
909
+
910
+
911
+ getChangedTimeStamps (startI, endI, audioSDuration) {
912
+
913
+ const adjustedLetters = Array.from(this.adjustedLetters)
914
+
915
+ // Skip this if start/end indexes are not found (new sample)
916
+ if ((startI==-1 || endI==-1) && !adjustedLetters.length) {
917
+ return undefined
918
+ }
919
+ startI = startI==-1 ? this.letters.length : parseInt(startI)
920
+ endI = endI==-1 ? 0 : parseInt(endI)
921
+
922
+ // Check OUTSIDE of the given changed indexes for TEXT, to see if there were other changes, to eg pitch/duration
923
+ if (adjustedLetters.length) {
924
+ startI = Math.min(startI, Math.min(adjustedLetters))
925
+ endI = Math.max(endI, Math.max(adjustedLetters))
926
+ }
927
+
928
+ const newStartI = startI
929
+ const newEndI = endI
930
+
931
+ // Then, look through the duration values of the audio, and get a percent into the audio where those new start/end points are
932
+ const totalDuration = this.dursNew.reduce((p,c)=>p+c,0)
933
+ const durAtStart = this.dursNew.filter((v,vi) => vi<=newStartI).reduce((p,c)=>p+c,0)
934
+ const durAtEnd = this.dursNew.filter((v,vi) => vi<=newEndI).reduce((p,c)=>p+c,0)
935
+ const startPercent = durAtStart/totalDuration
936
+ const endPercent = durAtEnd/totalDuration
937
+
938
+ // Then, multiply this by the seconds duration of the generated audio, and pad with ~500ms, to get the final start/end of the section of the audio to play
939
+ const startSeconds = Math.max(0, startPercent*audioSDuration-0.5)
940
+ const endSeconds = Math.min(audioSDuration, endPercent*audioSDuration+0.5)
941
+
942
+ return [startSeconds, endSeconds]
943
+ }
944
+ }
945
+
946
+ class Letter {
947
+ constructor (context, index, letter, sliderBox, centerY, left, width) {
948
+ this.type = "letter"
949
+ this.context = context
950
+ this.letter = letter
951
+ this.sliderBox = sliderBox
952
+ this.centerY = centerY
953
+ this.index = index
954
+
955
+ this.left = left
956
+ this.width = width
957
+ this.colour = "black"
958
+ }
959
+
960
+ render () {
961
+ this.context.fillStyle = this.colour
962
+ this.context.font = "20pt Arial"
963
+ this.context.textAlign = "center"
964
+ this.context.textBaseline = "middle"
965
+ this.context.fillText(this.letter, this.sliderBox.getXLeft()+this.sliderBox.width/2, this.centerY)
966
+ }
967
+ }
968
+
969
+ class SliderGrabber {
970
+
971
+ constructor (context, index, sliderBox, topLeftY, width, height, sliderRange) {
972
+ this.type = "slider"
973
+ this.context = context
974
+ this.sliderBox = sliderBox
975
+ this.topLeftY = topLeftY
976
+ this.width = width
977
+ this.height = height
978
+ this.index = index
979
+ this.sliderRange = sliderRange
980
+
981
+ this.isBeingDragged = false
982
+ this.dragStart = {x: undefined, y: undefined}
983
+
984
+ this.fillStyle = `#${window.currentGame.themeColourPrimary}`
985
+ }
986
+
987
+ render () {
988
+
989
+ this.context.beginPath()
990
+ this.context.rect(this.sliderBox.getXLeft()+1, this.topLeftY, this.width, this.height)
991
+ this.context.stroke()
992
+
993
+ this.context.fillStyle = this.fillStyle
994
+ this.context.fillRect(this.sliderBox.getXLeft()+1, this.topLeftY, this.width, this.height)
995
+ }
996
+
997
+ getXLeft () {
998
+ return this.sliderBox.getXLeft()
999
+ }
1000
+
1001
+ setValueFromCoords (topLeftY) {
1002
+
1003
+ this.topLeftY = topLeftY
1004
+ this.topLeftY = Math.max(window.sequenceEditor.LETTERS_Y_OFFSET+1, this.topLeftY)
1005
+ this.topLeftY = Math.min(this.topLeftY, window.sequenceEditor.LETTERS_Y_OFFSET+window.sequenceEditor.EDITOR_HEIGHT-this.height-1)
1006
+
1007
+ this.percentUp = (this.topLeftY-window.sequenceEditor.LETTERS_Y_OFFSET) / (window.sequenceEditor.EDITOR_HEIGHT-this.height)
1008
+ window.sequenceEditor.pitchNew[this.index] = (1-this.percentUp)*(this.sliderRange*2)-this.sliderRange
1009
+ }
1010
+
1011
+ setValueFromValue (value) {
1012
+ value = Math.max(-this.sliderRange, value)
1013
+ value = Math.min(value, this.sliderRange)
1014
+ this.percentUp = (value+this.sliderRange)/(this.sliderRange*2)
1015
+
1016
+ this.topLeftY = (1-this.percentUp) * (window.sequenceEditor.EDITOR_HEIGHT-this.height) + window.sequenceEditor.LETTERS_Y_OFFSET
1017
+ }
1018
+
1019
+ }
1020
+
1021
+
1022
+ class EnergyEmotionGrabber extends SliderGrabber {
1023
+
1024
+ constructor (context, index, sliderBox, topLeftY, width, height, sliderRange, modelType, radius, sliderType) {
1025
+ super(context, index, sliderBox, topLeftY, width, height, sliderRange)
1026
+ this.type = `${sliderType}_slider`
1027
+ this.modelType = modelType
1028
+ this.radius = radius
1029
+ }
1030
+
1031
+ render () {
1032
+ this.context.fillStyle = this.fillStyle
1033
+ this.context.beginPath()
1034
+ this.context.lineWidth = 1
1035
+ let x = this.sliderBox.getXLeft()+1 + this.sliderBox.width/2 // Centered
1036
+ let y = this.topLeftY
1037
+ this.context.arc(x, y, this.radius, 0, 2 * Math.PI)
1038
+ this.context.fill()
1039
+ this.context.stroke()
1040
+ this.context.lineWidth = 1
1041
+ }
1042
+
1043
+ setValueFromCoords (topLeftY) {
1044
+
1045
+ this.topLeftY = topLeftY
1046
+ this.topLeftY = Math.max(window.sequenceEditor.LETTERS_Y_OFFSET+this.radius, this.topLeftY)
1047
+ this.topLeftY = Math.min(this.topLeftY, window.sequenceEditor.LETTERS_Y_OFFSET+(window.sequenceEditor.EDITOR_HEIGHT-2-this.radius/2))
1048
+
1049
+ if (this.type=="energy_slider") {
1050
+ if (this.modelType=="xVAPitch") {
1051
+ this.percentUp = (this.topLeftY-window.sequenceEditor.LETTERS_Y_OFFSET)/(window.sequenceEditor.EDITOR_HEIGHT-this.radius)
1052
+ } else {
1053
+ this.percentUp = 1-(this.topLeftY-window.sequenceEditor.LETTERS_Y_OFFSET)/(window.sequenceEditor.EDITOR_HEIGHT-this.radius)
1054
+ }
1055
+ window.sequenceEditor.energyNew[this.index] = window.sequenceEditor.MAX_ENERGY - (window.sequenceEditor.MAX_ENERGY-window.sequenceEditor.MIN_ENERGY)*this.percentUp
1056
+ } else if (this.type=="style_slider") {
1057
+
1058
+ this.percentUp = (this.topLeftY-window.sequenceEditor.LETTERS_Y_OFFSET)/(window.sequenceEditor.EDITOR_HEIGHT-this.radius)
1059
+
1060
+ window.sequenceEditor.registeredStyleKeys.forEach(styleKey => {
1061
+ if (seq_edit_view_select.value.startsWith("style_") && seq_edit_view_select.value.includes(styleKey)) {
1062
+ window.sequenceEditor.styleValuesNew[styleKey][this.index] = window.sequenceEditor.MAX_STYLES - (window.sequenceEditor.MAX_STYLES-window.sequenceEditor.MIN_STYLES)*this.percentUp
1063
+ }
1064
+ })
1065
+
1066
+ } else {
1067
+ this.percentUp = (this.topLeftY-window.sequenceEditor.LETTERS_Y_OFFSET)/(window.sequenceEditor.EDITOR_HEIGHT-this.radius)
1068
+ if (seq_edit_view_select.value=="emAngry") {
1069
+ window.sequenceEditor.emAngryNew[this.index] = window.sequenceEditor.MAX_EMOTIONS - (window.sequenceEditor.MAX_EMOTIONS-window.sequenceEditor.MIN_ENERGY)*this.percentUp
1070
+ } else if (seq_edit_view_select.value=="emHappy") {
1071
+ window.sequenceEditor.emHappyNew[this.index] = window.sequenceEditor.MAX_EMOTIONS - (window.sequenceEditor.MAX_EMOTIONS-window.sequenceEditor.MIN_ENERGY)*this.percentUp
1072
+ } else if (seq_edit_view_select.value=="emSad") {
1073
+ window.sequenceEditor.emSadNew[this.index] = window.sequenceEditor.MAX_EMOTIONS - (window.sequenceEditor.MAX_EMOTIONS-window.sequenceEditor.MIN_ENERGY)*this.percentUp
1074
+ } else if (seq_edit_view_select.value=="emSurprise") {
1075
+ window.sequenceEditor.emSurpriseNew[this.index] = window.sequenceEditor.MAX_EMOTIONS - (window.sequenceEditor.MAX_EMOTIONS-window.sequenceEditor.MIN_ENERGY)*this.percentUp
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ setValueFromValue (value) {
1081
+ if (this.type=="energy_slider") {
1082
+ value = Math.max(window.sequenceEditor.MIN_ENERGY, value)
1083
+ value = Math.min(value, window.sequenceEditor.MAX_ENERGY)
1084
+ if (this.modelType=="xVAPitch") {
1085
+ this.percentUp = ( (value-window.sequenceEditor.MIN_ENERGY) / (window.sequenceEditor.MAX_ENERGY-window.sequenceEditor.MIN_ENERGY) )
1086
+ } else {
1087
+ this.percentUp = 1 - ( (value-window.sequenceEditor.MIN_ENERGY) / (window.sequenceEditor.MAX_ENERGY-window.sequenceEditor.MIN_ENERGY) )
1088
+ }
1089
+ } else if (this.type=="style_slider") {
1090
+ value = Math.max(window.sequenceEditor.MIN_STYLES, value)
1091
+ value = Math.min(value, window.sequenceEditor.MAX_STYLES)
1092
+ this.percentUp = ( (value-window.sequenceEditor.MIN_STYLES) / (window.sequenceEditor.MAX_STYLES-window.sequenceEditor.MIN_STYLES) )
1093
+ } else {
1094
+ value = Math.max(window.sequenceEditor.MIN_EMOTIONS, value)
1095
+ value = Math.min(value, window.sequenceEditor.MAX_EMOTIONS)
1096
+ this.percentUp = ( (value-window.sequenceEditor.MIN_EMOTIONS) / (window.sequenceEditor.MAX_EMOTIONS-window.sequenceEditor.MIN_EMOTIONS) )
1097
+ }
1098
+
1099
+ this.topLeftY = (1 - this.percentUp) * (window.sequenceEditor.EDITOR_HEIGHT-2-this.radius*2) + (window.sequenceEditor.LETTERS_Y_OFFSET+1)
1100
+ }
1101
+ }
1102
+
1103
+
1104
+ class SliderBox {
1105
+ constructor (context, index, leftBox, topY, height, minLetterLength, maxLetterLength, alternateColour=false) {
1106
+ this.type = "box"
1107
+ this.context = context
1108
+ this.leftBox = leftBox
1109
+ this.topY = topY
1110
+ this.height = height
1111
+ this.index = index
1112
+ this.alternateColour = alternateColour
1113
+
1114
+ this.LEFT_RIGHT_SEQ_PADDING = 20
1115
+ this.MIN_LETTER_LENGTH = minLetterLength
1116
+ this.MAX_LETTER_LENGTH = maxLetterLength
1117
+
1118
+ this.isBeingDragged = false
1119
+ this.dragStart = {width: undefined, y: undefined}
1120
+ }
1121
+
1122
+ render () {
1123
+ this.context.globalAlpha = 0.3
1124
+ this.context.fillStyle = this.alternateColour ? "white" : "black"
1125
+ this.context.fillRect(this.LEFT_RIGHT_SEQ_PADDING+ (this.getLeftBox()?this.getLeftBox().getX():0), this.topY, this.width, this.height)
1126
+
1127
+ this.context.beginPath()
1128
+ this.context.rect(this.LEFT_RIGHT_SEQ_PADDING+ (this.getLeftBox()?this.getLeftBox().getX():0), this.topY, this.width, this.height)
1129
+ this.context.stroke()
1130
+
1131
+
1132
+ this.context.globalAlpha = 1
1133
+ }
1134
+
1135
+ setValueFromValue (value) {
1136
+
1137
+ value = value * window.sequenceEditor.pacing
1138
+ value = Math.max(0.1, value)
1139
+ value = Math.min(value, 20)
1140
+
1141
+ this.percentAcross = value/20
1142
+ this.width = this.percentAcross * (this.MAX_LETTER_LENGTH-this.MIN_LETTER_LENGTH) + this.MIN_LETTER_LENGTH
1143
+
1144
+ this.grabber.width = this.width-2
1145
+ this.letter.centerX = this.leftX + this.width/2
1146
+ }
1147
+
1148
+ getLeftBox () {
1149
+ if (this.leftBox) {
1150
+ if (window.sequenceEditor.enabled_disabled_items[this.leftBox.index]) {
1151
+ return this.leftBox
1152
+ } else {
1153
+ return this.leftBox.leftBox
1154
+ }
1155
+ }
1156
+ }
1157
+
1158
+
1159
+ getX () {
1160
+ if (this.leftBox) {
1161
+ return this.getLeftBox().getX() + this.width + 5
1162
+ }
1163
+ return 0 + this.width + 5
1164
+ }
1165
+ getXLeft () {
1166
+ if (this.leftBox) {
1167
+ return this.LEFT_RIGHT_SEQ_PADDING+this.getLeftBox().getX()
1168
+ }
1169
+ return this.LEFT_RIGHT_SEQ_PADDING
1170
+ }
1171
+ }
1172
+
1173
+
1174
+
1175
+
1176
+
1177
+
1178
+
1179
+ const infer = () => {
1180
+ window.sequenceEditor.hasChanged = false
1181
+ if (!isGenerating) {
1182
+ generateVoiceButton.click()
1183
+ }
1184
+ }
1185
+ const kickOffAutoInferTimer = () => {
1186
+ if (window.sequenceEditor.autoInferTimer != null) {
1187
+ clearTimeout(window.sequenceEditor.autoInferTimer)
1188
+ window.sequenceEditor.autoInferTimer = null
1189
+ }
1190
+ if (autoplay_ckbx.checked) {
1191
+ window.sequenceEditor.autoInferTimer = setTimeout(infer, 500)
1192
+ }
1193
+ }
1194
+
1195
+
1196
+ // Un-select letters when clicking anywhere else
1197
+ right.addEventListener("click", event => {
1198
+ if (event.target.nodeName=="BUTTON" || event.target.nodeName=="INPUT" || event.target.nodeName=="SVG" || event.target.nodeName=="IMG" || event.target.nodeName=="path" || event.target == window.sequenceEditor.canvas || event.target.id=="dialogueInput" || (event.target.classList && event.target.classList.contains("autocomplete_option"))) {
1199
+ return
1200
+ }
1201
+ window.sequenceEditor.letterFocus.forEach(li => {
1202
+ window.sequenceEditor.letterClasses[li].colour = "black"
1203
+ })
1204
+ window.sequenceEditor.letterFocus = []
1205
+
1206
+ letterEnergyNumb.disabled = true
1207
+ letterEnergyNumb.value = ""
1208
+ letterPitchNumb.disabled = true
1209
+ letterPitchNumb.value = ""
1210
+ letterLengthNumb.disabled = true
1211
+ letterLengthNumb.value = ""
1212
+ letterEmotionNumb.disabled = true
1213
+ letterEmotionNumb.value = ""
1214
+ letterStyleNumb.disabled = true
1215
+ letterStyleNumb.value = ""
1216
+ })
1217
+
1218
+ letterEnergyNumb.addEventListener("click", () => {
1219
+ const lpnValue = parseFloat(letterEnergyNumb.value) || 0
1220
+ if (window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1221
+ window.sequenceEditor.hasChanged = true
1222
+ }
1223
+ })
1224
+ letterEnergyNumb.addEventListener("input", () => {
1225
+ const lpnValue = parseFloat(letterEnergyNumb.value) || 0
1226
+ if (window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1227
+ window.sequenceEditor.hasChanged = true
1228
+ }
1229
+ window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]] = lpnValue
1230
+ window.sequenceEditor.energyGrabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(lpnValue)
1231
+ kickOffAutoInferTimer()
1232
+ })
1233
+ letterEnergyNumb.addEventListener("change", () => {
1234
+ const lpnValue = parseFloat(letterEnergyNumb.value) || 0
1235
+ if (window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1236
+ window.sequenceEditor.hasChanged = true
1237
+ }
1238
+ window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]] = lpnValue
1239
+ window.sequenceEditor.energyGrabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(lpnValue)
1240
+ kickOffAutoInferTimer()
1241
+ })
1242
+
1243
+ letterEmotionNumb.addEventListener("click", () => {
1244
+ const lpnValue = parseFloat(letterEmotionNumb.value) || 0
1245
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1246
+ if (data[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1247
+ window.sequenceEditor.hasChanged = true
1248
+ }
1249
+ })
1250
+ letterEmotionNumb.addEventListener("input", () => {
1251
+ const lpnValue = parseFloat(letterEmotionNumb.value) || 0
1252
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1253
+ if (window.sequenceEditor.data[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1254
+ window.sequenceEditor.hasChanged = true
1255
+ }
1256
+ window.sequenceEditor.data[window.sequenceEditor.letterFocus[0]] = lpnValue
1257
+ grabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(lpnValue)
1258
+ kickOffAutoInferTimer()
1259
+ })
1260
+ letterEmotionNumb.addEventListener("change", () => {
1261
+ const lpnValue = parseFloat(letterEmotionNumb.value) || 0
1262
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1263
+ if (data[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1264
+ window.sequenceEditor.hasChanged = true
1265
+ }
1266
+ data[window.sequenceEditor.letterFocus[0]] = lpnValue
1267
+ grabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(lpnValue)
1268
+ kickOffAutoInferTimer()
1269
+ })
1270
+
1271
+ const getSelectedStyleDataAndGrabbers = () => {
1272
+ let data, grabbers
1273
+ window.sequenceEditor.registeredStyleKeys.forEach(styleKey => {
1274
+ if (seq_edit_view_select.value.startsWith("style_") && seq_edit_view_select.value.includes(styleKey)) {
1275
+ data = window.sequenceEditor.styleValuesNew[styleKey]
1276
+ grabbers = window.sequenceEditor.styleGrabbers[styleKey]
1277
+ }
1278
+ })
1279
+ return [data, grabbers]
1280
+ }
1281
+
1282
+ letterStyleNumb.addEventListener("click", () => {
1283
+ const lpnValue = parseFloat(letterStyleNumb.value) || 0
1284
+ let [data, grabbers] = getSelectedStyleDataAndGrabbers()
1285
+ if (data && data[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1286
+ window.sequenceEditor.hasChanged = true
1287
+ }
1288
+ })
1289
+ letterStyleNumb.addEventListener("input", () => {
1290
+ const lpnValue = parseFloat(letterStyleNumb.value) || 0
1291
+ let [data, grabbers] = getSelectedStyleDataAndGrabbers()
1292
+ if (data[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1293
+ window.sequenceEditor.hasChanged = true
1294
+ }
1295
+ data[window.sequenceEditor.letterFocus[0]] = lpnValue
1296
+ grabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(lpnValue)
1297
+ setNewDataToSelectedStyle(data)
1298
+ kickOffAutoInferTimer()
1299
+ })
1300
+ letterStyleNumb.addEventListener("change", () => {
1301
+ const lpnValue = parseFloat(letterStyleNumb.value) || 0
1302
+ let [data, grabbers] = getSelectedStyleDataAndGrabbers()
1303
+ if (data[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1304
+ window.sequenceEditor.hasChanged = true
1305
+ }
1306
+ data[window.sequenceEditor.letterFocus[0]] = lpnValue
1307
+ grabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(lpnValue)
1308
+ setNewDataToSelectedStyle(data)
1309
+ kickOffAutoInferTimer()
1310
+ })
1311
+
1312
+
1313
+ letterPitchNumb.addEventListener("click", () => {
1314
+ const lpnValue = parseFloat(letterPitchNumb.value) || 0
1315
+ if (window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1316
+ window.sequenceEditor.hasChanged = true
1317
+ }
1318
+ })
1319
+ letterPitchNumb.addEventListener("input", () => {
1320
+ const lpnValue = parseFloat(letterPitchNumb.value) || 0
1321
+ if (window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1322
+ window.sequenceEditor.hasChanged = true
1323
+ }
1324
+ window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]] = lpnValue
1325
+ window.sequenceEditor.grabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(letterPitchNumb.value)
1326
+ kickOffAutoInferTimer()
1327
+ })
1328
+ letterPitchNumb.addEventListener("change", () => {
1329
+ const lpnValue = parseFloat(letterPitchNumb.value) || 0
1330
+ if (window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]!=lpnValue) {
1331
+ window.sequenceEditor.hasChanged = true
1332
+ }
1333
+ window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]] = lpnValue
1334
+ window.sequenceEditor.grabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(letterPitchNumb.value)
1335
+ kickOffAutoInferTimer()
1336
+ })
1337
+
1338
+ resetLetter_btn.addEventListener("click", () => {
1339
+ if (window.sequenceEditor.letterFocus.length==0) {
1340
+ return
1341
+ }
1342
+
1343
+ window.sequenceEditor.letterFocus.forEach(l => {
1344
+ if (window.sequenceEditor.dursNew[l] != window.sequenceEditor.resetDurs[l]) {
1345
+ window.sequenceEditor.hasChanged = true
1346
+ }
1347
+ window.sequenceEditor.dursNew[l] = window.sequenceEditor.resetDurs[l]
1348
+ window.sequenceEditor.pitchNew[l] = window.sequenceEditor.resetPitch[l]
1349
+
1350
+ window.sequenceEditor.grabbers[l].setValueFromValue(window.sequenceEditor.resetPitch[l])
1351
+ window.sequenceEditor.sliderBoxes[l].setValueFromValue(window.sequenceEditor.resetDurs[l])
1352
+ })
1353
+
1354
+ if (window.sequenceEditor.letterFocus.length==1) {
1355
+ letterLengthNumb.value = parseFloat(window.sequenceEditor.dursNew[window.sequenceEditor.letterFocus[0]])
1356
+ letterPitchNumb.value = parseInt(window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]*100)/100
1357
+ letterEnergyNumb.value = parseInt(window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]])
1358
+
1359
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1360
+
1361
+ window.sequenceEditor.emAngryNew[window.sequenceEditor.letterFocus[0]] = window.sequenceEditor.resetEmAngry[window.sequenceEditor.letterFocus[0]]
1362
+ window.sequenceEditor.emAngryGrabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(window.sequenceEditor.emAngryNew[window.sequenceEditor.letterFocus[0]])
1363
+ window.sequenceEditor.emHappyNew[window.sequenceEditor.letterFocus[0]] = window.sequenceEditor.resetEmHappy[window.sequenceEditor.letterFocus[0]]
1364
+ window.sequenceEditor.emHappyGrabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(window.sequenceEditor.emHappyNew[window.sequenceEditor.letterFocus[0]])
1365
+ window.sequenceEditor.emSadNew[window.sequenceEditor.letterFocus[0]] = window.sequenceEditor.resetEmSad[window.sequenceEditor.letterFocus[0]]
1366
+ window.sequenceEditor.emSadGrabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(window.sequenceEditor.emSadNew[window.sequenceEditor.letterFocus[0]])
1367
+ window.sequenceEditor.emSurpriseNew[window.sequenceEditor.letterFocus[0]] = window.sequenceEditor.resetEmSurprise[window.sequenceEditor.letterFocus[0]]
1368
+ window.sequenceEditor.emSurpriseGrabbers[window.sequenceEditor.letterFocus[0]].setValueFromValue(window.sequenceEditor.emSurpriseNew[window.sequenceEditor.letterFocus[0]])
1369
+
1370
+ if (data) {
1371
+ letterEmotionNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]])
1372
+ }
1373
+
1374
+ window.sequenceEditor.registeredStyleKeys.forEach(styleKey => {
1375
+ window.sequenceEditor.styleValuesNew[styleKey][window.sequenceEditor.letterFocus[0]] = window.sequenceEditor.styleValuesReset[styleKey][window.sequenceEditor.letterFocus[0]]
1376
+ window.sequenceEditor.styleGrabbers[styleKey][window.sequenceEditor.letterFocus[0]].setValueFromValue(window.sequenceEditor.styleValuesNew[styleKey][window.sequenceEditor.letterFocus[0]])
1377
+ })
1378
+
1379
+ let [styleData, styleGrabbers] = getSelectedStyleDataAndGrabbers()
1380
+ if (styleData) {
1381
+ letterStyleNumb.value = parseInt(styleData[window.sequenceEditor.letterFocus[0]])
1382
+ }
1383
+ }
1384
+ })
1385
+ const updateLetterLengthFromInput = () => {
1386
+ if (window.sequenceEditor.dursNew[window.sequenceEditor.letterFocus[0]] != letterLengthNumb.value) {
1387
+ window.sequenceEditor.hasChanged = true
1388
+ }
1389
+ window.sequenceEditor.dursNew[window.sequenceEditor.letterFocus[0]] = parseFloat(letterLengthNumb.value)
1390
+
1391
+ window.sequenceEditor.letterFocus.forEach(l => {
1392
+ window.sequenceEditor.sliderBoxes[l].setValueFromValue(window.sequenceEditor.dursNew[l])
1393
+ })
1394
+ kickOffAutoInferTimer()
1395
+ }
1396
+ letterLengthNumb.addEventListener("input", () => {
1397
+ updateLetterLengthFromInput()
1398
+ })
1399
+ letterLengthNumb.addEventListener("change", () => {
1400
+ updateLetterLengthFromInput()
1401
+ })
1402
+
1403
+ // Reset button
1404
+ window.resetEnergy = () => {
1405
+ window.sequenceEditor.energyNew = window.sequenceEditor.resetEnergy.map(v => v)
1406
+ window.sequenceEditor.energyGrabbers.forEach((slider, l) => {
1407
+ slider.setValueFromValue(window.sequenceEditor.energyNew[l])
1408
+ })
1409
+ if (window.sequenceEditor.letterFocus.length==1) {
1410
+ letterEnergyNumb.value = parseInt(window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]]*100)/100
1411
+ }
1412
+ if (window.sequenceEditor.letterFocus.length==1) {
1413
+ letterEnergyNumb.value = parseInt(window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]])
1414
+ }
1415
+ }
1416
+ window.resetStyle = () => {
1417
+ window.sequenceEditor.registeredStyleKeys.forEach(styleKey => {
1418
+ window.sequenceEditor.styleValuesNew[styleKey] = window.sequenceEditor.styleValuesReset[styleKey].map(v => v)
1419
+ window.sequenceEditor.styleGrabbers[styleKey].forEach((slider, l) => slider.setValueFromValue(window.sequenceEditor.styleValuesNew[styleKey][l]))
1420
+
1421
+ })
1422
+ let [data, grabbers] = getSelectedStyleDataAndGrabbers()
1423
+ if (data) {
1424
+ if (window.sequenceEditor.letterFocus.length==1) {
1425
+ letterStyleNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1426
+ }
1427
+ if (window.sequenceEditor.letterFocus.length==1) {
1428
+ letterStyleNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]])
1429
+ }
1430
+ }
1431
+ }
1432
+ window.resetEmotion = () => {
1433
+ window.sequenceEditor.emAngryNew = window.sequenceEditor.resetEmAngry.map(v => v)
1434
+ window.sequenceEditor.emHappyNew = window.sequenceEditor.resetEmHappy.map(v => v)
1435
+ window.sequenceEditor.emSadNew = window.sequenceEditor.resetEmSad.map(v => v)
1436
+ window.sequenceEditor.emSurpriseNew = window.sequenceEditor.resetEmSurprise.map(v => v)
1437
+
1438
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1439
+
1440
+ window.sequenceEditor.emAngryGrabbers.forEach((slider, l) => slider.setValueFromValue(window.sequenceEditor.emAngryNew[l]))
1441
+ window.sequenceEditor.emHappyGrabbers.forEach((slider, l) => slider.setValueFromValue(window.sequenceEditor.emHappyNew[l]))
1442
+ window.sequenceEditor.emSadGrabbers.forEach((slider, l) => slider.setValueFromValue(window.sequenceEditor.emSadNew[l]))
1443
+ window.sequenceEditor.emSurpriseGrabbers.forEach((slider, l) => slider.setValueFromValue(window.sequenceEditor.emSurpriseNew[l]))
1444
+
1445
+ if (data && window.sequenceEditor.letterFocus.length==1) {
1446
+ letterEmotionNumb.value = parseFloat(data[window.sequenceEditor.letterFocus[0]])
1447
+ }
1448
+ }
1449
+ window.resetPitch = () => {
1450
+ window.sequenceEditor.pitchNew = window.sequenceEditor.resetPitch.map(p=>p)
1451
+ // Update the editor pitch values
1452
+ window.sequenceEditor.grabbers.forEach((slider, i) => {
1453
+ slider.setValueFromValue(window.sequenceEditor.pitchNew[i])
1454
+ })
1455
+ if (window.sequenceEditor.letterFocus.length==1) {
1456
+ letterPitchNumb.value = parseInt(window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]*100)/100
1457
+ }
1458
+ }
1459
+ window.resetDursPace = () => {
1460
+
1461
+ pace_slid.value = 1
1462
+ paceNumbInput.value = 1
1463
+ window.sequenceEditor.pacing = parseFloat(pace_slid.value)
1464
+
1465
+ window.sequenceEditor.dursNew = window.sequenceEditor.resetDurs.map(v => v)
1466
+ // Update the editor lengths
1467
+ window.sequenceEditor.sliderBoxes.forEach((box,i) => {
1468
+ box.setValueFromValue(window.sequenceEditor.dursNew[i])
1469
+ })
1470
+ if (window.sequenceEditor.letterFocus.length==1) {
1471
+ letterLengthNumb.value = parseFloat(window.sequenceEditor.dursNew[window.sequenceEditor.letterFocus[0]])
1472
+ }
1473
+
1474
+ }
1475
+ reset_btn.addEventListener("click", () => {
1476
+
1477
+ if (window.shiftKeyIsPressed) {
1478
+ if (seq_edit_edit_select.value=="energy") {
1479
+ resetEnergy()
1480
+ resetDursPace()
1481
+
1482
+ } else if (seq_edit_edit_select.value=="pitch") {
1483
+ resetPitch()
1484
+ resetDursPace()
1485
+
1486
+ } else if (seq_edit_edit_select.value=="emotion") {
1487
+ resetEmotion()
1488
+ resetDursPace()
1489
+
1490
+ } else if (seq_edit_edit_select.value=="style") {
1491
+ resetStyle()
1492
+ resetDursPace()
1493
+ }
1494
+
1495
+ window.sequenceEditor.init()
1496
+ } else {
1497
+ reset_what_open_btn.click()
1498
+ }
1499
+ })
1500
+ reset_what_confirm_btn.addEventListener("click", () => {
1501
+ resetContainer.click()
1502
+ if (reset_what_pitch.checked) {
1503
+ resetPitch()
1504
+ }
1505
+ if (reset_what_energy.checked) {
1506
+ resetEnergy()
1507
+ }
1508
+ if (reset_what_duration.checked) {
1509
+ resetDursPace()
1510
+ }
1511
+ if (reset_what_emotion.checked) {
1512
+ resetEmotion()
1513
+ }
1514
+ if (reset_what_style.checked) {
1515
+ resetStyle()
1516
+ }
1517
+ window.sequenceEditor.init()
1518
+ })
1519
+
1520
+ const getSelectedEmotionDataAndGrabbers = () => {
1521
+ let data, grabbers
1522
+ if (seq_edit_view_select.value=="emAngry") {
1523
+ data = window.sequenceEditor.emAngryNew
1524
+ grabbers = window.sequenceEditor.emAngryGrabbers
1525
+ } else if (seq_edit_view_select.value=="emHappy") {
1526
+ data = window.sequenceEditor.emHappyNew
1527
+ grabbers = window.sequenceEditor.emHappyGrabbers
1528
+ } else if (seq_edit_view_select.value=="emSad") {
1529
+ data = window.sequenceEditor.emSadNew
1530
+ grabbers = window.sequenceEditor.emSadGrabbers
1531
+ } else if (seq_edit_view_select.value=="emSurprise") {
1532
+ data = window.sequenceEditor.emSurpriseNew
1533
+ grabbers = window.sequenceEditor.emSurpriseGrabbers
1534
+ }
1535
+ return [data, grabbers]
1536
+ }
1537
+ const setNewDataToSelectedEmotion = (data) => {
1538
+ if (seq_edit_view_select.value=="emAngry") {
1539
+ window.sequenceEditor.emAngryNew = data
1540
+ } else if (seq_edit_view_select.value=="emHappy") {
1541
+ window.sequenceEditor.emHappyNew = data
1542
+ } else if (seq_edit_view_select.value=="emSad") {
1543
+ window.sequenceEditor.emSadNew = data
1544
+ } else if (seq_edit_view_select.value=="emSurprise") {
1545
+ window.sequenceEditor.emSurpriseNew = data
1546
+ }
1547
+ }
1548
+ const setNewDataToSelectedStyle = (data) => {
1549
+ window.sequenceEditor.registeredStyleKeys.forEach(styleKey => {
1550
+ if (seq_edit_view_select.value.startsWith("style_") && seq_edit_view_select.value.includes(styleKey)) {
1551
+ window.sequenceEditor.styleValuesNew[styleKey] = data
1552
+ }
1553
+ })
1554
+ }
1555
+
1556
+ amplify_btn.addEventListener("click", () => {
1557
+ if (seq_edit_edit_select.value=="pitch") {
1558
+ window.sequenceEditor.pitchNew = window.sequenceEditor.pitchNew.map((p, pi) => {
1559
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(pi)==-1) {
1560
+ return p
1561
+ }
1562
+ const newVal = p*1.025
1563
+ return newVal>0 ? Math.min(window.sequenceEditor.pitchSliderRange, newVal) : Math.max(-window.sequenceEditor.pitchSliderRange, newVal)
1564
+ })
1565
+ window.sequenceEditor.grabbers.forEach((slider, l) => {
1566
+ slider.setValueFromValue(window.sequenceEditor.pitchNew[l])
1567
+ })
1568
+ if (window.sequenceEditor.letterFocus.length==1) {
1569
+ letterPitchNumb.value = parseInt(window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]*100)/100
1570
+ }
1571
+ } else if (seq_edit_edit_select.value=="energy") {
1572
+ window.sequenceEditor.energyNew = window.sequenceEditor.energyNew.map((e, ei) => {
1573
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1574
+ return e
1575
+ }
1576
+ const distFromMiddle = (e-window.sequenceEditor.MIN_ENERGY) - (window.sequenceEditor.MAX_ENERGY-window.sequenceEditor.MIN_ENERGY)/2
1577
+ const newVal = e + distFromMiddle*0.025
1578
+ return newVal>0 ? Math.min(window.sequenceEditor.MAX_ENERGY, newVal) : Math.max(window.sequenceEditor.MIN_ENERGY, newVal)
1579
+ })
1580
+ window.sequenceEditor.energyGrabbers.forEach((slider, l) => {
1581
+ slider.setValueFromValue(window.sequenceEditor.energyNew[l])
1582
+ })
1583
+ if (window.sequenceEditor.letterFocus.length==1) {
1584
+ letterEnergyNumb.value = parseInt(window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]]*100)/100
1585
+ }
1586
+ } else if (seq_edit_view_select.value.startsWith("style_")) {
1587
+
1588
+ let [data, grabbers] = getSelectedStyleDataAndGrabbers()
1589
+
1590
+ data = data.map((e, ei) => {
1591
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1592
+ return e
1593
+ }
1594
+ const distFromMiddle = (e-window.sequenceEditor.MIN_STYLES) - (window.sequenceEditor.MAX_STYLES-window.sequenceEditor.MIN_STYLES)/2
1595
+ const newVal = e + distFromMiddle*0.025
1596
+ return newVal>0 ? Math.min(window.sequenceEditor.MAX_STYLES, newVal) : Math.max(window.sequenceEditor.MIN_STYLES, newVal)
1597
+ })
1598
+ grabbers.forEach((slider, l) => {
1599
+ slider.setValueFromValue(data[l])
1600
+ })
1601
+ if (window.sequenceEditor.letterFocus.length==1) {
1602
+ letterStyleNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1603
+ }
1604
+ setNewDataToSelectedStyle(data)
1605
+
1606
+ } else if (seq_edit_edit_select.value=="emotion") {
1607
+
1608
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1609
+
1610
+ data = data.map((e, ei) => {
1611
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1612
+ return e
1613
+ }
1614
+ const distFromMiddle = (e-window.sequenceEditor.MIN_EMOTIONS) - (window.sequenceEditor.MAX_EMOTIONS-window.sequenceEditor.MIN_EMOTIONS)/2
1615
+ const newVal = e + distFromMiddle*0.025
1616
+ return newVal>0 ? Math.min(window.sequenceEditor.MAX_EMOTIONS, newVal) : Math.max(window.sequenceEditor.MIN_EMOTIONS, newVal)
1617
+ })
1618
+ grabbers.forEach((slider, l) => {
1619
+ slider.setValueFromValue(data[l])
1620
+ })
1621
+ if (window.sequenceEditor.letterFocus.length==1) {
1622
+ letterEmotionNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1623
+ }
1624
+ setNewDataToSelectedEmotion(data)
1625
+ }
1626
+ kickOffAutoInferTimer()
1627
+ })
1628
+ flatten_btn.addEventListener("click", () => {
1629
+ if (seq_edit_edit_select.value=="pitch") {
1630
+ window.sequenceEditor.pitchNew = window.sequenceEditor.pitchNew.map((p,pi) => {
1631
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(pi)==-1) {
1632
+ return p
1633
+ }
1634
+ return p*(1-0.025)
1635
+ })
1636
+ window.sequenceEditor.grabbers.forEach((slider, l) => {
1637
+ slider.setValueFromValue(window.sequenceEditor.pitchNew[l])
1638
+ })
1639
+ if (window.sequenceEditor.letterFocus.length==1) {
1640
+ letterPitchNumb.value = parseInt(window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]*100)/100
1641
+ }
1642
+
1643
+ } else if (seq_edit_edit_select.value=="energy") {
1644
+ window.sequenceEditor.energyNew = window.sequenceEditor.energyNew.map((e,ei) => {
1645
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1646
+ return e
1647
+ }
1648
+ const distFromMiddle = (e-window.sequenceEditor.MIN_ENERGY) - (window.sequenceEditor.MAX_ENERGY-window.sequenceEditor.MIN_ENERGY)/2
1649
+ const newVal = e + distFromMiddle*-0.025
1650
+ return newVal>0 ? Math.min(window.sequenceEditor.MAX_ENERGY, newVal) : Math.max(window.sequenceEditor.MIN_ENERGY, newVal)
1651
+ })
1652
+ window.sequenceEditor.energyGrabbers.forEach((slider, l) => {
1653
+ slider.setValueFromValue(window.sequenceEditor.energyNew[l])
1654
+ })
1655
+ if (window.sequenceEditor.letterFocus.length==1) {
1656
+ letterEnergyNumb.value = parseInt(window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]]*100)/100
1657
+ }
1658
+ } else if (seq_edit_view_select.value.startsWith("style_")) {
1659
+
1660
+ let [data, grabbers] = getSelectedStyleDataAndGrabbers()
1661
+
1662
+ data = data.map((e,ei) => {
1663
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1664
+ return e
1665
+ }
1666
+ const distFromMiddle = (e-window.sequenceEditor.MIN_STYLES) - (window.sequenceEditor.MAX_STYLES-window.sequenceEditor.MIN_STYLES)/2
1667
+ const newVal = e + distFromMiddle*-0.025
1668
+ return newVal>0 ? Math.min(window.sequenceEditor.MAX_STYLES, newVal) : Math.max(window.sequenceEditor.MIN_STYLES, newVal)
1669
+ })
1670
+ grabbers.forEach((slider, l) => {
1671
+ slider.setValueFromValue(data[l])
1672
+ })
1673
+ if (window.sequenceEditor.letterFocus.length==1) {
1674
+ letterStyleNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1675
+ }
1676
+ setNewDataToSelectedStyle(data)
1677
+
1678
+
1679
+ } else if (seq_edit_edit_select.value=="emotion") {
1680
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1681
+
1682
+ data = data.map((e,ei) => {
1683
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1684
+ return e
1685
+ }
1686
+ const distFromMiddle = (e-window.sequenceEditor.MIN_EMOTIONS) - (window.sequenceEditor.MAX_EMOTIONS-window.sequenceEditor.MIN_EMOTIONS)/2
1687
+ const newVal = e + distFromMiddle*-0.025
1688
+ return newVal>0 ? Math.min(window.sequenceEditor.MAX_EMOTIONS, newVal) : Math.max(window.sequenceEditor.MIN_EMOTIONS, newVal)
1689
+ })
1690
+ grabbers.forEach((slider, l) => {
1691
+ slider.setValueFromValue(data[l])
1692
+ })
1693
+ if (window.sequenceEditor.letterFocus.length==1) {
1694
+ letterEmotionNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1695
+ }
1696
+ setNewDataToSelectedEmotion(data)
1697
+ }
1698
+ kickOffAutoInferTimer()
1699
+ })
1700
+
1701
+
1702
+ jitter_btn.addEventListener("click", () => {
1703
+ if (seq_edit_edit_select.value=="pitch") {
1704
+ window.sequenceEditor.pitchNew = window.sequenceEditor.pitchNew.map((p, pi) => {
1705
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(pi)==-1) {
1706
+ return p
1707
+ }
1708
+ let newVal
1709
+ if (p==0) {
1710
+ newVal = 1*(1+ (Math.random()*0.4+0.05) * ((Math.random()-0.5)>0 ? 1 : -1) )
1711
+ newVal -= 1
1712
+ } else {
1713
+ newVal = p*(1+ (Math.random()*0.2+0.05) * ((Math.random()-0.5)>0 ? 1 : -1) )
1714
+ }
1715
+ return newVal>0 ? Math.min(window.sequenceEditor.pitchSliderRange, newVal) : Math.max(-window.sequenceEditor.pitchSliderRange, newVal)
1716
+ })
1717
+ window.sequenceEditor.grabbers.forEach((slider, l) => {
1718
+ slider.setValueFromValue(window.sequenceEditor.pitchNew[l])
1719
+ })
1720
+ if (window.sequenceEditor.letterFocus.length==1) {
1721
+ letterPitchNumb.value = parseInt(window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]*100)/100
1722
+ }
1723
+ } else if (seq_edit_edit_select.value=="energy") {
1724
+ window.sequenceEditor.energyNew = window.sequenceEditor.energyNew.map((e, ei) => {
1725
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1726
+ return e
1727
+ }
1728
+ const distFromMiddle = (e-window.sequenceEditor.MIN_ENERGY) - (window.sequenceEditor.MAX_ENERGY-window.sequenceEditor.MIN_ENERGY)/2
1729
+ const newVal = e + distFromMiddle*(Math.random()*0.1+0.05) * ((Math.random()-0.5)>0 ? 1 : -1)
1730
+ return newVal>0 ? Math.min(window.sequenceEditor.MAX_ENERGY, newVal) : Math.max(window.sequenceEditor.MIN_ENERGY, newVal)
1731
+ })
1732
+ window.sequenceEditor.energyGrabbers.forEach((slider, l) => {
1733
+ slider.setValueFromValue(window.sequenceEditor.energyNew[l])
1734
+ })
1735
+ if (window.sequenceEditor.letterFocus.length==1) {
1736
+ letterEnergyNumb.value = parseInt(window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]]*100)/100
1737
+ }
1738
+ } else if (seq_edit_view_select.value.startsWith("style_")) {
1739
+
1740
+ let [data, grabbers] = getSelectedStyleDataAndGrabbers()
1741
+ data = data.map((e, ei) => {
1742
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1743
+ return e
1744
+ }
1745
+ const distFromMiddle = (e-window.sequenceEditor.MIN_STYLES) - (window.sequenceEditor.MAX_STYLES-window.sequenceEditor.MIN_STYLES)/2
1746
+ const newVal = e + distFromMiddle*(Math.random()*0.1+0.05) * ((Math.random()-0.5)>0 ? 1 : -1)
1747
+ return newVal>0 ? Math.min(window.sequenceEditor.MAX_STYLES, newVal) : Math.max(window.sequenceEditor.MIN_STYLES, newVal)
1748
+ })
1749
+ grabbers.forEach((slider, l) => {
1750
+ slider.setValueFromValue(data[l])
1751
+ })
1752
+ if (window.sequenceEditor.letterFocus.length==1) {
1753
+ letterStyleNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1754
+ }
1755
+ setNewDataToSelectedStyle(data)
1756
+
1757
+ } else if (seq_edit_edit_select.value=="emotion") {
1758
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1759
+ data = data.map((e, ei) => {
1760
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1761
+ return e
1762
+ }
1763
+ const distFromMiddle = (e-window.sequenceEditor.MIN_EMOTIONS) - (window.sequenceEditor.MAX_EMOTIONS-window.sequenceEditor.MIN_EMOTIONS)/2
1764
+ const newVal = e + distFromMiddle*(Math.random()*0.1+0.05) * ((Math.random()-0.5)>0 ? 1 : -1)
1765
+ return newVal>0 ? Math.min(window.sequenceEditor.MAX_EMOTIONS, newVal) : Math.max(window.sequenceEditor.MIN_EMOTIONS, newVal)
1766
+ })
1767
+ grabbers.forEach((slider, l) => {
1768
+ slider.setValueFromValue(data[l])
1769
+ })
1770
+ if (window.sequenceEditor.letterFocus.length==1) {
1771
+ letterEmotionNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1772
+ }
1773
+ setNewDataToSelectedEmotion(data)
1774
+ }
1775
+ kickOffAutoInferTimer()
1776
+ })
1777
+
1778
+
1779
+ increase_btn.addEventListener("click", () => {
1780
+ if (seq_edit_edit_select.value=="pitch") {
1781
+ window.sequenceEditor.pitchNew = window.sequenceEditor.pitchNew.map((p,pi) => {
1782
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(pi)==-1) {
1783
+ return p
1784
+ }
1785
+ return p+0.1
1786
+ })
1787
+ window.sequenceEditor.grabbers.forEach((slider, l) => {
1788
+ slider.setValueFromValue(window.sequenceEditor.pitchNew[l])
1789
+ })
1790
+ if (window.sequenceEditor.letterFocus.length==1) {
1791
+ letterPitchNumb.value = parseInt(window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]*100)/100
1792
+ }
1793
+ } else if (seq_edit_edit_select.value=="energy") {
1794
+ window.sequenceEditor.energyNew = window.sequenceEditor.energyNew.map((e,ei) => {
1795
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1796
+ return e
1797
+ }
1798
+ return window.currentModel.modelType=="xVAPitch" ? e+0.04 : e-0.04
1799
+ })
1800
+ window.sequenceEditor.energyGrabbers.forEach((slider, l) => {
1801
+ slider.setValueFromValue(window.sequenceEditor.energyNew[l])
1802
+ })
1803
+ if (window.sequenceEditor.letterFocus.length==1) {
1804
+ letterEnergyNumb.value = parseInt(window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]]*100)/100
1805
+ }
1806
+
1807
+ } else if (seq_edit_view_select.value.startsWith("style_")) {
1808
+
1809
+ let [data, grabbers] = getSelectedStyleDataAndGrabbers()
1810
+ data = data.map((e,ei) => {
1811
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1812
+ return e
1813
+ }
1814
+ return e+0.04
1815
+ })
1816
+ grabbers.forEach((slider, l) => {
1817
+ slider.setValueFromValue(data[l])
1818
+ })
1819
+ if (window.sequenceEditor.letterFocus.length==1) {
1820
+ letterStyleNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1821
+ }
1822
+ setNewDataToSelectedStyle(data)
1823
+
1824
+
1825
+ } else if (seq_edit_edit_select.value=="emotion") {
1826
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1827
+ data = data.map((e,ei) => {
1828
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1829
+ return e
1830
+ }
1831
+ return e+0.04
1832
+ })
1833
+ grabbers.forEach((slider, l) => {
1834
+ slider.setValueFromValue(data[l])
1835
+ })
1836
+ if (window.sequenceEditor.letterFocus.length==1) {
1837
+ letterEmotionNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1838
+ }
1839
+ setNewDataToSelectedEmotion(data)
1840
+ }
1841
+ kickOffAutoInferTimer()
1842
+ })
1843
+ decrease_btn.addEventListener("click", () => {
1844
+ if (seq_edit_edit_select.value=="pitch") {
1845
+ window.sequenceEditor.pitchNew = window.sequenceEditor.pitchNew.map((p,pi) => {
1846
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(pi)==-1) {
1847
+ return p
1848
+ }
1849
+ return p-0.1
1850
+ })
1851
+ window.sequenceEditor.grabbers.forEach((slider, l) => {
1852
+ slider.setValueFromValue(window.sequenceEditor.pitchNew[l])
1853
+ })
1854
+ if (window.sequenceEditor.letterFocus.length==1) {
1855
+ letterPitchNumb.value = parseInt(window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]*100)/100
1856
+ }
1857
+ } else if (seq_edit_edit_select.value=="energy") {
1858
+ window.sequenceEditor.energyNew = window.sequenceEditor.energyNew.map((e,ei) => {
1859
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1860
+ return e
1861
+ }
1862
+ return window.currentModel.modelType=="xVAPitch" ? e-0.04 : e+0.04
1863
+ })
1864
+ window.sequenceEditor.energyGrabbers.forEach((slider, l) => {
1865
+ slider.setValueFromValue(window.sequenceEditor.energyNew[l])
1866
+ })
1867
+ if (window.sequenceEditor.letterFocus.length==1) {
1868
+ letterEnergyNumb.value = parseInt(window.sequenceEditor.energyNew[window.sequenceEditor.letterFocus[0]]*100)/100
1869
+ }
1870
+
1871
+ } else if (seq_edit_view_select.value.startsWith("style_")) {
1872
+
1873
+ let [data, grabbers] = getSelectedStyleDataAndGrabbers()
1874
+ data = data.map((e,ei) => {
1875
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1876
+ return e
1877
+ }
1878
+ return e-0.04
1879
+ })
1880
+ grabbers.forEach((slider, l) => {
1881
+ slider.setValueFromValue(data[l])
1882
+ })
1883
+ if (window.sequenceEditor.letterFocus.length==1) {
1884
+ letterStyleNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1885
+ }
1886
+ setNewDataToSelectedStyle(data)
1887
+
1888
+
1889
+ } else if (seq_edit_edit_select.value=="emotion") {
1890
+ let [data, grabbers] = getSelectedEmotionDataAndGrabbers()
1891
+ data = data.map((e,ei) => {
1892
+ if (window.sequenceEditor.letterFocus.length>1 && window.sequenceEditor.letterFocus.indexOf(ei)==-1) {
1893
+ return e
1894
+ }
1895
+ return e-0.04
1896
+ })
1897
+ grabbers.forEach((slider, l) => {
1898
+ slider.setValueFromValue(data[l])
1899
+ })
1900
+ if (window.sequenceEditor.letterFocus.length==1) {
1901
+ letterEmotionNumb.value = parseInt(data[window.sequenceEditor.letterFocus[0]]*100)/100
1902
+ }
1903
+ setNewDataToSelectedEmotion(data)
1904
+ }
1905
+ kickOffAutoInferTimer()
1906
+ })
1907
+
1908
+ pace_slid.addEventListener("input", () => {
1909
+ paceNumbInput.value = pace_slid.value
1910
+ })
1911
+
1912
+ pace_slid.addEventListener("change", () => {
1913
+ editorTooltip.style.display = "none"
1914
+ if (autoplay_ckbx.checked) {
1915
+ generateVoiceButton.click()
1916
+ }
1917
+ paceNumbInput.value = pace_slid.value
1918
+ window.sequenceEditor.pacing = parseFloat(pace_slid.value)
1919
+ window.sequenceEditor.init()
1920
+ })
1921
+
1922
+ pace_slid.addEventListener("input", () => {
1923
+ window.sequenceEditor.pacing = parseFloat(pace_slid.value)
1924
+ window.sequenceEditor.sliderBoxes.forEach((box, i) => {
1925
+ box.setValueFromValue(window.sequenceEditor.dursNew[i])
1926
+ })
1927
+ })
1928
+ paceNumbInput.addEventListener("change", () => {
1929
+ pace_slid.value = paceNumbInput.value
1930
+ if (autoplay_ckbx.checked) {
1931
+ generateVoiceButton.click()
1932
+ }
1933
+ window.sequenceEditor.sliderBoxes.forEach((box, i) => {box.setValueFromValue(window.sequenceEditor.dursNew[i])})
1934
+ window.sequenceEditor.pacing = parseFloat(pace_slid.value)
1935
+ window.sequenceEditor.init()
1936
+ })
1937
+ paceNumbInput.addEventListener("keyup", () => {
1938
+ pace_slid.value = paceNumbInput.value
1939
+ window.sequenceEditor.sliderBoxes.forEach((box, i) => {box.setValueFromValue(window.sequenceEditor.dursNew[i])})
1940
+ window.sequenceEditor.pacing = parseFloat(pace_slid.value)
1941
+ window.sequenceEditor.init()
1942
+ })
1943
+ autoplay_ckbx.addEventListener("change", () => {
1944
+ window.userSettings.autoplay = autoplay_ckbx.checked
1945
+ saveUserSettings()
1946
+ })
1947
+
1948
+
1949
+ // Populate the languages dropdown
1950
+ window.supportedLanguages = {
1951
+ // "am": "Amharic",
1952
+ "ar": "Arabic",
1953
+ "da": "Danish",
1954
+ "de": "German",
1955
+ "el": "Greek",
1956
+ "en": "English",
1957
+ "es": "Spanish",
1958
+ "fi": "Finnish",
1959
+ "fr": "French",
1960
+ "ha": "Hausa",
1961
+ "hi": "Hindi",
1962
+ "hu": "Hungarian",
1963
+ "it": "Italian",
1964
+ "jp": "Japanese",
1965
+ "ko": "Korean",
1966
+ "la": "Latin",
1967
+ "nl": "Dutch",
1968
+ "pl": "Polish",
1969
+ "pt": "Portuguese",
1970
+ "ro": "Romanian",
1971
+ "ru": "Russian",
1972
+ "sv": "Swedish",
1973
+ "sw": "Swahili",
1974
+ // "th": "Thai",
1975
+ "tr": "Turkish",
1976
+ "uk": "Ukrainian",
1977
+ "vi": "Vietnamese",
1978
+ "wo": "Wolof",
1979
+ "yo": "Yoruba",
1980
+ "zh": "Chinese"
1981
+ }
1982
+ window.populateLanguagesDropdownsFromModel = (dropdown, modelJson=undefined) => {
1983
+ dropdown.innerHTML = ""
1984
+
1985
+ Object.keys(window.supportedLanguages).sort((a,b)=>window.supportedLanguages[a]<window.supportedLanguages[b]?-1:1).forEach(key => {
1986
+ if (!modelJson || !modelJson.lang_capabilities || modelJson.lang_capabilities.includes(key)) {
1987
+ const opt = createElem("option", window.supportedLanguages[key])
1988
+ opt.value = key
1989
+ dropdown.appendChild(opt)
1990
+ }
1991
+ })
1992
+ }
1993
+ window.populateLanguagesDropdownsFromModel(base_lang_select)
1994
+ window.populateLanguagesDropdownsFromModel(voiceWorkbenchLanguageDropdown)
1995
+ base_lang_select.value = "en"
1996
+ voiceWorkbenchLanguageDropdown.value = "en"
1997
+
1998
+
1999
+
2000
+ // For copying the generated ARPAbet sequence to the clipboard
2001
+ editorContainer.addEventListener("contextmenu", event => {
2002
+ event.preventDefault()
2003
+ ipcRenderer.send('show-context-menu-editor')
2004
+ })
2005
+ ipcRenderer.on('context-menu-command', (e, command) => {
2006
+
2007
+ if (command=="context-copy-editor") {
2008
+ if (window.sequenceEditor && window.sequenceEditor.sequence && window.sequenceEditor.sequence.length && window.currentModel && window.currentModel.modelType=="xVAPitch") {
2009
+
2010
+ let seqARPAbet = window.sequenceEditor.sequence
2011
+ if (seqARPAbet[0]=="_") {
2012
+ seqARPAbet = seqARPAbet.slice(1, seqARPAbet.length)
2013
+ }
2014
+ if (seqARPAbet[seqARPAbet.length-1]=="_") {
2015
+ seqARPAbet = seqARPAbet.slice(0, seqARPAbet.length-1)
2016
+ }
2017
+
2018
+ seqARPAbet = seqARPAbet.filter(val => val!="<PAD>")
2019
+ seqARPAbet = seqARPAbet.map(v => {
2020
+ if (v=="_") {
2021
+ return "} {"
2022
+ }
2023
+ return v
2024
+ })
2025
+
2026
+ clipboard.writeText("{"+seqARPAbet.join(" ")+"}")
2027
+ }
2028
+ }
2029
+ })
2030
+
2031
+
2032
+ // Audio player
2033
+ window.initWaveSurfer = (src) => {
2034
+ if (window.wavesurfer) {
2035
+ window.wavesurfer.stop()
2036
+ wavesurferContainer.innerHTML = ""
2037
+ } else {
2038
+ window.wavesurfer = WaveSurfer.create({
2039
+ container: '#wavesurferContainer',
2040
+ backend: 'MediaElement',
2041
+ waveColor: `#${window.currentGame.themeColourPrimary}`,
2042
+ height: 100,
2043
+ progressColor: 'white',
2044
+ responsive: true,
2045
+ })
2046
+ }
2047
+ try {
2048
+ window.wavesurfer.setSinkId(window.userSettings.base_speaker)
2049
+ } catch (e) {
2050
+ console.log("Can't set sinkId")
2051
+ }
2052
+ if (src) {
2053
+ window.wavesurfer.load(src)
2054
+ }
2055
+ window.wavesurfer.on("finish", () => {
2056
+ samplePlayPause.innerHTML = window.i18n.PLAY
2057
+ })
2058
+ window.wavesurfer.on("seek", event => {
2059
+ if (event!=0) {
2060
+ window.wavesurfer.play()
2061
+ samplePlayPause.innerHTML = window.i18n.PAUSE
2062
+ }
2063
+ })
2064
+ }
2065
+ window.samplePlayPauseHandler = event => {
2066
+ if (window.wavesurfer) {
2067
+ if (event.ctrlKey) {
2068
+ if (window.wavesurfer.sink_id!=window.userSettings.alt_speaker) {
2069
+ window.wavesurfer.setSinkId(window.userSettings.alt_speaker)
2070
+ }
2071
+ } else {
2072
+ if (window.wavesurfer.sink_id!=window.userSettings.base_speaker) {
2073
+ window.wavesurfer.setSinkId(window.userSettings.base_speaker)
2074
+ }
2075
+ }
2076
+
2077
+ if (window.wavesurfer.isPlaying()) {
2078
+ samplePlayPause.innerHTML = window.i18n.PLAY
2079
+ window.wavesurfer.playPause()
2080
+ } else {
2081
+ samplePlayPause.innerHTML = window.i18n.PAUSE
2082
+ window.wavesurfer.playPause()
2083
+ }
2084
+ }
2085
+ }
2086
+ samplePlayPause.addEventListener("click", window.samplePlayPauseHandler)
2087
+
2088
+
2089
+ exports.Editor = Editor
javascript/embeddings.js ADDED
@@ -0,0 +1,795 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+
4
+ window.openEmbeddingsWindow = () => {
5
+ closeModal(undefined, embeddingsContainer).then(() => {
6
+ embeddingsContainer.style.opacity = 0
7
+ embeddingsContainer.style.display = "flex"
8
+ requestAnimationFrame(() => requestAnimationFrame(() => embeddingsContainer.style.opacity = 1))
9
+ requestAnimationFrame(() => requestAnimationFrame(() => chromeBar.style.opacity = 1))
10
+ })
11
+ }
12
+
13
+ window.embeddingsState = {
14
+ data: {},
15
+ allData: {},
16
+ clickedObject: undefined,
17
+ spritesOn: true,
18
+ gendersOn: false,
19
+ voiceCheckboxes: [],
20
+ isReady: false,
21
+ isOpen: false,
22
+ sceneData: {},
23
+ mouseIsDown: false,
24
+ rightMouseIsDown: false,
25
+ mousePos: {x: 0, y: 0}
26
+ }
27
+
28
+
29
+
30
+ function componentToHex(c) {
31
+ c = Math.min(255, c)
32
+ var hex = c.toString(16);
33
+ return hex.length == 1 ? "0" + hex : hex;
34
+ }
35
+
36
+ function rgbToHex(r, g, b) {
37
+ return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
38
+ }
39
+ function hexToRgb(hex, normalize) {
40
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
41
+ const colour = result ? {
42
+ r: parseInt(result[1], 16),
43
+ g: parseInt(result[2], 16),
44
+ b: parseInt(result[3], 16)
45
+ } : null;
46
+ if (normalize && colour) {
47
+ colour.r /= 255
48
+ colour.g /= 255
49
+ colour.b /= 255
50
+ }
51
+ return colour
52
+ }
53
+
54
+
55
+ window.populateGamesList = () => {
56
+ embeddingsGamesListContainer.innerHTML = ""
57
+ Object.keys(window.gameAssets).sort((a,b)=>a<b?-1:1).forEach(gameId => {
58
+ const gameSelectContainer = createElem("div")
59
+ const gameCheckbox = createElem(`input#embs_${gameId}`, {type: "checkbox"})
60
+ gameCheckbox.checked = true
61
+ const gameButton = createElem("button.fixedColour")
62
+ gameButton.style.setProperty("background-color", `#${window.embeddingsState.gameColours[gameId]}`, "important")
63
+ gameButton.style.display = "flex"
64
+ gameButton.style.alignItems = "center"
65
+ gameButton.style.margin = "auto"
66
+ gameButton.style.marginTop = "8px"
67
+ const buttonLabel = createElem("span", window.embeddingsState.gameTitles[gameId])
68
+
69
+ gameButton.addEventListener("click", e => {
70
+ if (e.target==gameButton || e.target==buttonLabel) {
71
+ gameCheckbox.click()
72
+ window.populateVoicesList()
73
+ window.computeEmbsAndDimReduction()
74
+ }
75
+ })
76
+ gameButton.addEventListener("contextmenu", e => {
77
+ if (e.target==gameButton || e.target==buttonLabel) {
78
+ Array.from(embeddingsGamesListContainer.querySelectorAll("input")).forEach(ckbx => ckbx.checked = false)
79
+ gameCheckbox.click()
80
+ window.populateVoicesList()
81
+ window.computeEmbsAndDimReduction()
82
+ }
83
+ })
84
+ gameCheckbox.addEventListener("click", () => {
85
+ window.populateVoicesList()
86
+ window.computeEmbsAndDimReduction()
87
+ })
88
+
89
+
90
+ gameButton.appendChild(gameCheckbox)
91
+ gameButton.appendChild(buttonLabel)
92
+ gameSelectContainer.appendChild(gameButton)
93
+ embeddingsGamesListContainer.appendChild(gameSelectContainer)
94
+ })
95
+ }
96
+
97
+
98
+
99
+ window.populateVoicesList = () => {
100
+ const enabledGames = Array.from(embeddingsGamesListContainer.querySelectorAll("input"))
101
+ .map(elem => [elem.checked, elem.id.replace("embs_", "")])
102
+ .filter(checkedId => checkedId[0])
103
+ .map(checkedId => checkedId[1].replace("embs_", ""))
104
+
105
+ const checkboxes = []
106
+
107
+ embeddingsRecordsContainer.innerHTML = ""
108
+ Object.keys(window.games).forEach(gameId => {
109
+ if (!enabledGames.includes(gameId)) {
110
+ return
111
+ }
112
+
113
+ window.games[gameId].models.forEach(model => {
114
+
115
+ model.variants.forEach(variant => {
116
+ const voiceRowElem = createElem("div")
117
+
118
+ const voiceCkbx = createElem(`input#embsVoice_${variant.voiceId}`, {type: "checkbox"})
119
+ voiceCkbx.checked = true
120
+ voiceCkbx.addEventListener("click", () => {
121
+ window.computeEmbsAndDimReduction()
122
+ })
123
+ checkboxes.push(voiceCkbx)
124
+ const showVoiceBtn = createElem("button.smallButton.fixedColour", "Show")
125
+ showVoiceBtn.style.background = `#${window.embeddingsState.gameColours[model.gameId]}`
126
+ showVoiceBtn.addEventListener("click", () => {
127
+ if (!voiceCkbx.checked) {
128
+ return window.errorModal(window.i18n.VEMB_VOICE_NOT_ENABLED)
129
+ }
130
+ const point = window.embeddingsState.sceneData.points.find(point => point.data.voiceId==variant.voiceId)
131
+ window.embeddingsState.sceneData.controls.target.set(point.position.x, point.position.y, point.position.z)
132
+
133
+ const cameraPos = window.embeddingsState.sceneData.camera.position
134
+ const deltaX = (point.position.x - cameraPos.x)
135
+ const deltaY = (point.position.y - cameraPos.y)
136
+ const deltaZ = (point.position.z - cameraPos.z)
137
+
138
+ window.embeddingsState.sceneData.camera.position.set(cameraPos.x+deltaX/2, cameraPos.y+deltaY/2, cameraPos.z+deltaZ/2)
139
+
140
+ })
141
+
142
+ const nameElem = createElem("div", model.voiceName+(model.variants.length>1?` (${variant.variantName})`:""))
143
+ nameElem.title = model.voiceName
144
+ const gameElem = createElem("div", window.embeddingsState.gameTitles[model.gameId])
145
+ gameElem.title = window.embeddingsState.gameTitles[model.gameId]
146
+ const voiceGender = createElem("div", variant.gender)
147
+ voiceGender.title = variant.gender
148
+
149
+ voiceRowElem.appendChild(createElem("div", voiceCkbx))
150
+ voiceRowElem.appendChild(createElem("div", showVoiceBtn))
151
+ voiceRowElem.appendChild(nameElem)
152
+ voiceRowElem.appendChild(gameElem)
153
+ voiceRowElem.appendChild(voiceGender)
154
+
155
+ if (embeddingsSearchBar.value.length && !model.voiceName.toLowerCase().trim().includes(embeddingsSearchBar.value.toLowerCase().trim())) {
156
+ return
157
+ }
158
+ embeddingsRecordsContainer.appendChild(voiceRowElem)
159
+ })
160
+ })
161
+ })
162
+
163
+ window.embeddingsState.voiceCheckboxes = checkboxes
164
+
165
+ }
166
+ embeddingsSearchBar.addEventListener("keyup", () => window.populateVoicesList())
167
+
168
+
169
+
170
+ window.initDataMappings = () => {
171
+
172
+ window.embeddingsState.voiceIdToModel = {}
173
+ window.embeddingsState.gameShortIdToGameId = {}
174
+ window.embeddingsState.gameColours = {}
175
+ window.embeddingsState.gameTitles = {}
176
+
177
+
178
+ const idToGame = {}
179
+ Object.keys(window.gameAssets).forEach(gameId => {
180
+ const id = window.gameAssets[gameId].gameCode
181
+ idToGame[id] = gameId
182
+ })
183
+
184
+ Object.keys(window.gameAssets).forEach(gameId => {
185
+ // Short game ID to full game ID
186
+ const gameShortId = window.gameAssets[gameId].gameCode.toLowerCase()
187
+ window.embeddingsState.gameShortIdToGameId[gameShortId] = gameId.toLowerCase()
188
+
189
+ // Game title
190
+ const title = window.gameAssets[gameId].gameName
191
+ window.embeddingsState.gameTitles[gameId] = title
192
+
193
+ // Game colour
194
+ let colour = window.gameAssets[gameId].themeColourPrimary
195
+ colour = colour.length==3 ? `${colour[0]}${colour[0]}${colour[1]}${colour[1]}${colour[2]}${colour[2]}` : colour
196
+ window.embeddingsState.gameColours[gameId] = colour
197
+ })
198
+
199
+ Object.keys(window.games).forEach(gameId => {
200
+ // Voice Id to model data
201
+ window.games[gameId].models.forEach(model => {
202
+ model.variants.forEach(variant => {
203
+ window.embeddingsState.voiceIdToModel[variant.voiceId] = JSON.parse(JSON.stringify(model))
204
+ if (model.variants.length>1) {
205
+ let voiceName = window.embeddingsState.voiceIdToModel[variant.voiceId].voiceName
206
+ voiceName = `${voiceName} (${variant.variantName})`
207
+ window.embeddingsState.voiceIdToModel[variant.voiceId].voiceName = voiceName
208
+ }
209
+ })
210
+ })
211
+ })
212
+ }
213
+
214
+
215
+
216
+
217
+ window.initEmbeddingsScene = () => {
218
+ window.initDataMappings()
219
+ window.populateGamesList()
220
+ window.populateVoicesList()
221
+
222
+ embeddingsSceneContainer.addEventListener("mousedown", (event) => {
223
+ window.embeddingsState.mousePos.x = parseInt(event.layerX)
224
+ window.embeddingsState.mousePos.y = parseInt(event.layerY)
225
+ })
226
+ embeddingsSceneContainer.addEventListener("mouseup", (event) => {
227
+ const mouseX = parseInt(event.layerX)
228
+ const mouseY = parseInt(event.layerY)
229
+
230
+ if (event.button==0) {
231
+ if (Math.abs(mouseX-window.embeddingsState.mousePos.x)<10 && Math.abs(mouseY-window.embeddingsState.mousePos.y)<10) {
232
+ window.embeddingsState.mouseIsDown = true
233
+ setTimeout(() => {window.embeddingsState.mouseIsDown = false}, 100)
234
+ }
235
+ } else if (event.button==2) {
236
+ if (Math.abs(mouseX-window.embeddingsState.mousePos.x)<10 && Math.abs(mouseY-window.embeddingsState.mousePos.y)<10) {
237
+ window.embeddingsState.rightMouseIsDown = true
238
+ setTimeout(() => {window.embeddingsState.rightMouseIsDown = false}, 100)
239
+ }
240
+ }
241
+ })
242
+ window.embeddingsState.isReady = false
243
+
244
+ const SPHERE_RADIUS = 3
245
+ const SPHERE_V_COUNT = 50
246
+
247
+ // Renderer
248
+ window.embeddingsState.renderer = new THREE.WebGLRenderer({alpha: true, antialias: true})
249
+ window.embeddingsState.renderer.setPixelRatio( window.devicePixelRatio )
250
+ window.embeddingsState.renderer.setSize(embeddingsSceneContainer.offsetWidth, embeddingsSceneContainer.offsetHeight)
251
+ embeddingsSceneContainer.appendChild(window.embeddingsState.renderer.domElement)
252
+
253
+ // Scene and camera
254
+ const scene = new THREE.Scene()
255
+ window.embeddingsState.sceneData.camera = new THREE.PerspectiveCamera(60, embeddingsSceneContainer.offsetWidth/embeddingsSceneContainer.offsetHeight, 0.001, 100000)
256
+ window.embeddingsState.sceneData.camera.position.set( -100, 0, 0 )
257
+
258
+ // Controls
259
+ window.embeddingsState.sceneData.controls = new THREE.OrbitControls(window.embeddingsState.sceneData.camera, window.embeddingsState.renderer.domElement)
260
+ window.embeddingsState.sceneData.controls2 = new THREE.TrackballControls(window.embeddingsState.sceneData.camera, window.embeddingsState.renderer.domElement)
261
+ window.embeddingsState.sceneData.controls.target.set(window.embeddingsState.sceneData.camera.position.x+0.15, window.embeddingsState.sceneData.camera.position.y, window.embeddingsState.sceneData.camera.position.z)
262
+
263
+
264
+ window.embeddingsState.sceneData.controls.enableDamping = true
265
+ window.embeddingsState.sceneData.controls.dampingFactor = 0.025
266
+ window.embeddingsState.sceneData.controls.screenSpacePanning = true
267
+ window.embeddingsState.sceneData.controls.rotateSpeed = 1/6
268
+ window.embeddingsState.sceneData.controls.panSpeed = 1
269
+ window.embeddingsState.sceneData.controls.minDistance = 50
270
+ window.embeddingsState.sceneData.controls.maxDistance = 500
271
+
272
+ window.embeddingsState.sceneData.controls2.noRotate = true
273
+ window.embeddingsState.sceneData.controls2.noPan = true
274
+ window.embeddingsState.sceneData.controls2.noZoom = true
275
+ window.embeddingsState.sceneData.controls2.zoomSpeed = 1/2// 1.5
276
+ window.embeddingsState.sceneData.controls2.dynamicDampingFactor = 0.2
277
+
278
+
279
+
280
+ const light = new THREE.DirectionalLight( 0xffffff, 0.5 )
281
+ light.position.set( -1, 1, 1 ).normalize()
282
+ scene.add(light)
283
+ scene.add(new THREE.AmbientLight( 0xffffff, 0.5 ))
284
+
285
+ // Mouse event ray caster
286
+ const raycaster = new THREE.Raycaster()
287
+ const mouse = new THREE.Vector2()
288
+ window.embeddingsState.renderer.domElement.addEventListener("mousemove", event => {
289
+ const sizeY = event.target.height
290
+ const sizeX = event.target.width
291
+ mouse.x = event.offsetX / sizeX * 2 - 1
292
+ mouse.y = -event.offsetY / sizeY * 2 + 1
293
+ }, false)
294
+
295
+
296
+ window.embeddingsState.sceneData.sprites = []
297
+ window.embeddingsState.sceneData.points = []
298
+
299
+ window.refreshEmbeddingsScenePoints = () => {
300
+
301
+ const enabledGames = Array.from(embeddingsGamesListContainer.querySelectorAll("input"))
302
+ .map(elem => [elem.checked, elem.id.replace("embs_", "")])
303
+ .filter(checkedId => checkedId[0])
304
+ .map(checkedId => checkedId[1].replace("embs_", ""))
305
+
306
+
307
+ const data = window.embeddingsState.data
308
+
309
+ const newDataNames = Object.keys(data)
310
+ const oldDataKept = []
311
+
312
+ const newSprites = []
313
+ const newPoints = []
314
+
315
+ // Remove any existing data
316
+ ;[window.embeddingsState.sceneData.points, window.embeddingsState.sceneData.sprites].forEach(dataList => {
317
+ dataList.forEach(object => {
318
+ const objectName = object.data.voiceId
319
+ if (newDataNames.includes(objectName)) {
320
+ oldDataKept.push(objectName)
321
+ const coords = {
322
+ x: parseFloat(data[objectName][0]),
323
+ y: parseFloat(data[objectName][1])-(object.data.type=="text"?SPHERE_RADIUS*1.5:0),
324
+ z: parseFloat(data[objectName][2])
325
+ }
326
+ object.data.isMoving = true
327
+ object.data.newPos = coords
328
+ if (object.data.type=="text") {
329
+ newSprites.push(object)
330
+ } else {
331
+ newPoints.push(object)
332
+ }
333
+ } else {
334
+ scene.remove(object)
335
+ }
336
+ })
337
+ })
338
+
339
+ // Add the new data
340
+ window.embeddingsState.sceneData.sprites = newSprites
341
+ window.embeddingsState.sceneData.points = newPoints
342
+ Object.keys(data).forEach(voiceId => {
343
+
344
+ if (oldDataKept.includes(voiceId)) {
345
+ return
346
+ }
347
+ const game = Object.keys(window.embeddingsState.gameShortIdToGameId).includes(voiceId.split("_")[0]) ? window.embeddingsState.gameShortIdToGameId[voiceId.split("_")[0]] : "other"
348
+ let gender
349
+ if (Object.keys(window.embeddingsState.voiceIdToModel).includes(voiceId)) {
350
+ gender = window.embeddingsState.voiceIdToModel[voiceId].gender || window.embeddingsState.voiceIdToModel[voiceId].variants[0].gender
351
+ } else {
352
+ gender = window.embeddingsState.allData[voiceId].voiceGender
353
+ }
354
+ gender = gender ? gender.toLowerCase() : "other"
355
+
356
+ // if (!enabledGames.includes(game)) {
357
+ // return
358
+ // }
359
+
360
+ // Filter out data by gender
361
+ // if (gender=="male" && !embeddingsMalesCkbx.checked) {
362
+ // return
363
+ // }
364
+ // if (gender=="female" && !embeddingsFemalesCkbx.checked) {
365
+ // return
366
+ // }
367
+ // if (gender=="other" && !embeddingsOtherGendersCkbx.checked) {
368
+ // return
369
+ // }
370
+
371
+
372
+ // Colour dict
373
+ const colour = hexToRgb("#"+window.embeddingsState.gameColours[game])
374
+ const genderColours = {
375
+ "f": {r: 200, g: 0, b: 0},
376
+ "m": {r: 0, g: 0, b: 200},
377
+ "o": {r: 85, g: 85, b: 85},
378
+ }
379
+ const coords = {
380
+ x: parseFloat(data[voiceId][0]),
381
+ y: parseFloat(data[voiceId][1]),
382
+ z: parseFloat(data[voiceId][2])
383
+ }
384
+
385
+ const genderColour = gender=="female" ? genderColours["f"] : (gender=="male" ? genderColours["m"] : genderColours["o"])
386
+
387
+
388
+ const pointGeometry = new THREE.SphereGeometry(SPHERE_RADIUS, SPHERE_V_COUNT, SPHERE_V_COUNT)
389
+ const pointMaterial = new THREE.MeshLambertMaterial({
390
+ color: window.embeddingsState.gendersOn ? rgbToHex(genderColour.r, genderColour.g, genderColour.b) : "#"+window.embeddingsState.gameColours[game],
391
+ transparent: true
392
+ })
393
+ pointMaterial.emissive.emissiveIntensity = 1
394
+
395
+
396
+ // Point sphere
397
+ const point = new THREE.Mesh(pointGeometry, pointMaterial)
398
+ point.position.x = coords.x
399
+ point.position.y = coords.y
400
+ point.position.z = coords.z
401
+ point.name = `point|${voiceId}`
402
+ point.data = {
403
+ type: "point",
404
+ voiceId: voiceId,
405
+ game: game,
406
+ gameColour: {r: colour.r, g: colour.g, b: colour.b},
407
+ genderColour: genderColour
408
+ }
409
+ window.embeddingsState.sceneData.points.push(point)
410
+ scene.add(point)
411
+
412
+ let voiceName
413
+ if (Object.keys(window.embeddingsState.voiceIdToModel).includes(voiceId)) {
414
+ voiceName = window.embeddingsState.voiceIdToModel[voiceId].voiceName
415
+ } else {
416
+ voiceName = window.embeddingsState.allData[voiceId].voiceName
417
+ }
418
+
419
+ // Text sprite
420
+ const sprite = new THREE.TextSprite({
421
+ text: voiceName,
422
+ fontFamily: 'Helvetica, sans-serif',
423
+ fontSize: 2,
424
+ strokeColor: '#ffffff',
425
+ strokeWidth: 0,
426
+ color: '#24ff00',
427
+ material: {color: "white"}
428
+ })
429
+ sprite.position.x = coords.x
430
+ sprite.position.y = coords.y-SPHERE_RADIUS*1.5
431
+ sprite.position.z = coords.z
432
+ sprite.name = `sprite|${voiceId}`
433
+ sprite.data = {type: "text", voiceId: voiceId, game: game}
434
+ window.embeddingsState.sceneData.sprites.push(sprite)
435
+ scene.add(sprite)
436
+ })
437
+ }
438
+ window.refreshEmbeddingsScenePoints()
439
+
440
+ let hoveredObject = undefined
441
+ let clickedObject = undefined
442
+
443
+ window.embeddings_render = () => {
444
+ if (!window.embeddingsState.isReady) {
445
+ return
446
+ }
447
+ requestAnimationFrame(window.embeddings_render)
448
+
449
+ const target = window.embeddingsState.sceneData.controls.target
450
+ window.embeddingsState.sceneData.controls.update()
451
+ window.embeddingsState.sceneData.controls2.target.set(target.x, target.y, target.z)
452
+ window.embeddingsState.sceneData.controls2.update()
453
+
454
+ window.embeddingsState.sceneData.camera.updateMatrixWorld()
455
+
456
+ raycaster.setFromCamera( mouse, window.embeddingsState.sceneData.camera );
457
+
458
+ // Move objects
459
+ [window.embeddingsState.sceneData.points, window.embeddingsState.sceneData.sprites].forEach(dataList => {
460
+ dataList.forEach(object => {
461
+ if (object.data.isMoving) {
462
+ if (Math.abs(object.position.x-object.data.newPos.x)>0.005) {
463
+ object.position.x += (object.data.newPos.x - object.position.x) / 20
464
+ object.position.y += (object.data.newPos.y - object.position.y) / 20
465
+ object.position.z += (object.data.newPos.z - object.position.z) / 20
466
+
467
+ } else {
468
+ object.data.isMoving = false
469
+ object.data.newPos = undefined
470
+ }
471
+ }
472
+ })
473
+ })
474
+
475
+
476
+ // Handle mouse events
477
+ let intersects = raycaster.intersectObjects(scene.children, true)
478
+ if (intersects.length) {
479
+
480
+ if (intersects.length>2) {
481
+ intersects = [intersects.find(it => it.object.data.type=="point")]
482
+ }
483
+ if (intersects.length==0 || intersects[0]==undefined || intersects[0].object==undefined || intersects[0].object.data.type=="text") {
484
+ window.embeddingsState.renderer.render(scene, window.embeddingsState.sceneData.camera)
485
+ return
486
+ }
487
+
488
+ window.embeddingsState.renderer.domElement.style.cursor = "pointer"
489
+
490
+ if (hoveredObject != undefined && hoveredObject.object.data.voiceId!=intersects[0].object.data.voiceId) {
491
+ const colour = window.embeddingsState.gendersOn ? hoveredObject.object.data.genderColour : hoveredObject.object.data.gameColour
492
+ hoveredObject.object.material.color.r = Math.min(1, colour.r/255)
493
+ hoveredObject.object.material.color.g = Math.min(1, colour.g/255)
494
+ hoveredObject.object.material.color.b = Math.min(1, colour.b/255)
495
+ }
496
+ hoveredObject = intersects[0]
497
+
498
+ const colour = window.embeddingsState.gendersOn ? hoveredObject.object.data.genderColour : hoveredObject.object.data.gameColour
499
+ hoveredObject.object.material.color.r = Math.min(1, colour.r/255*1.5)
500
+ hoveredObject.object.material.color.g = Math.min(1, colour.g/255*1.5)
501
+ hoveredObject.object.material.color.b = Math.min(1, colour.b/255*1.5)
502
+
503
+
504
+ // Right click does voice audio preview
505
+ if (window.embeddingsState.rightMouseIsDown) {
506
+ window.embeddingsState.rightMouseIsDown = false
507
+ const voiceId = hoveredObject.object.data.voiceId
508
+ const gameId = hoveredObject.object.data.game
509
+ const modelsPathForGame = window.userSettings[`modelspath_${gameId}`]
510
+ const audioPreviewPath = `${modelsPathForGame}/${voiceId}.wav`
511
+
512
+ if (fs.existsSync(audioPreviewPath)) {
513
+ const audioPreview = createElem("audio", {autoplay: false}, createElem("source", {
514
+ src: audioPreviewPath
515
+ }))
516
+ audioPreview.setSinkId(window.userSettings.base_speaker)
517
+ } else {
518
+ window.errorModal(window.i18n.VEMB_NO_PREVIEW)
519
+ }
520
+ }
521
+
522
+ // Left click does voice click
523
+ if (window.embeddingsState.mouseIsDown) {
524
+ if (window.embeddingsState.clickedObject==undefined || window.embeddingsState.clickedObject.voiceId!=hoveredObject.object.data.voiceId) {
525
+ if (window.embeddingsState.clickedObject!=undefined) {
526
+ window.embeddingsState.clickedObject.object.material.emissive.setRGB(0,0,0)
527
+ }
528
+ window.embeddingsState.clickedObject = {
529
+ voiceId: hoveredObject.object.data.voiceId,
530
+ game: hoveredObject.object.data.game,
531
+ object: hoveredObject.object
532
+ }
533
+ hoveredObject.object.material.emissive.setRGB(0, 1, 0)
534
+
535
+ const voiceId = window.embeddingsState.clickedObject.object.data.voiceId
536
+
537
+ if (Object.keys(window.embeddingsState.voiceIdToModel).includes(voiceId)) {
538
+ embeddingsVoiceGameDisplay.innerHTML = window.embeddingsState.gameTitles[window.embeddingsState.clickedObject.object.data.game]
539
+ embeddingsVoiceNameDisplay.innerHTML = window.embeddingsState.voiceIdToModel[voiceId].voiceName
540
+ embeddingsVoiceGenderDisplay.innerHTML = window.embeddingsState.voiceIdToModel[voiceId].gender || window.embeddingsState.voiceIdToModel[voiceId].variants[0].gender
541
+ } else {
542
+ embeddingsVoiceGameDisplay.innerHTML = window.embeddingsState.gameTitles[window.embeddingsState.clickedObject.object.data.game]
543
+ embeddingsVoiceNameDisplay.innerHTML = window.embeddingsState.allData[voiceId].voiceName
544
+ embeddingsVoiceGenderDisplay.innerHTML = window.embeddingsState.allData[voiceId].voiceGender
545
+ }
546
+ }
547
+ }
548
+
549
+
550
+ } else {
551
+ window.embeddingsState.renderer.domElement.style.cursor = "default"
552
+ if (hoveredObject != undefined) {
553
+ const colour = window.embeddingsState.gendersOn ? hoveredObject.object.data.genderColour : hoveredObject.object.data.gameColour
554
+ hoveredObject.object.material.color.r = Math.min(1, colour.r/255)
555
+ hoveredObject.object.material.color.g = Math.min(1, colour.g/255)
556
+ hoveredObject.object.material.color.b = Math.min(1, colour.b/255)
557
+ }
558
+ if (window.embeddingsState.mouseIsDown && window.embeddingsState.clickedObject!=undefined) {
559
+ window.embeddingsState.clickedObject.object.material.emissive.setRGB(0,0,0)
560
+ window.embeddingsState.clickedObject = undefined
561
+
562
+ embeddingsVoiceGameDisplay.innerHTML = ""
563
+ embeddingsVoiceNameDisplay.innerHTML = ""
564
+ embeddingsVoiceGenderDisplay.innerHTML = ""
565
+ }
566
+ }
567
+
568
+ window.embeddingsState.renderer.render(scene, window.embeddingsState.sceneData.camera)
569
+ }
570
+ window.embeddingsState.isReady = true
571
+ window.embeddings_render()
572
+
573
+
574
+ window.toggleSprites = () => {
575
+ window.embeddingsState.spritesOn = !window.embeddingsState.spritesOn
576
+ window.embeddingsState.sceneData.sprites.forEach(sprite => {
577
+ sprite.material.visible = window.embeddingsState.spritesOn
578
+ })
579
+ }
580
+
581
+ window.toggleGenders = () => {
582
+ window.embeddingsState.gendersOn = !window.embeddingsState.gendersOn
583
+ window.embeddingsState.sceneData.points.forEach(point => {
584
+ const colour = window.embeddingsState.gendersOn ? point.data.genderColour : point.data.gameColour
585
+ point.material.color.r = Math.min(1, colour.r/255)
586
+ point.material.color.g = Math.min(1, colour.g/255)
587
+ point.material.color.b = Math.min(1, colour.b/255)
588
+ })
589
+ }
590
+ window.addEventListener("resize", () => {
591
+ if (window.embeddingsState.isOpen) {
592
+ window.embeddings_updateSize()
593
+ }
594
+ })
595
+ }
596
+ window.embeddings_updateSize = () => {
597
+ if (window.embeddingsState.isReady) {
598
+ window.embeddingsState.sceneData.camera.aspect = embeddingsSceneContainer.offsetWidth/embeddingsSceneContainer.offsetHeight
599
+ window.embeddingsState.sceneData.camera.updateProjectionMatrix()
600
+ window.embeddingsState.renderer.setSize(embeddingsSceneContainer.offsetWidth, embeddingsSceneContainer.offsetHeight)
601
+ }
602
+ }
603
+
604
+
605
+ embeddingsPreviewButton.addEventListener("click", () => {
606
+ if (window.embeddingsState.clickedObject) {
607
+
608
+ const voiceId = window.embeddingsState.clickedObject.object.data.voiceId
609
+ const gameId = window.embeddingsState.clickedObject.object.data.game
610
+ const modelsPathForGame = window.userSettings[`modelspath_${gameId}`]
611
+ const audioPreviewPath = `${modelsPathForGame}/${voiceId}.wav`
612
+
613
+ if (fs.existsSync(audioPreviewPath)) {
614
+ const audioPreview = createElem("audio", {autoplay: false}, createElem("source", {
615
+ src: audioPreviewPath
616
+ }))
617
+ audioPreview.setSinkId(window.userSettings.base_speaker)
618
+ } else {
619
+ window.errorModal(window.i18n.VEMB_NO_PREVIEW)
620
+ }
621
+ } else {
622
+ window.errorModal(window.i18n.VEMB_SELECT_VOICE_FIRST)
623
+ }
624
+ })
625
+ embeddingsLoadButton.addEventListener("click", () => {
626
+ if (window.embeddingsState.clickedObject) {
627
+
628
+ const voiceId = window.embeddingsState.clickedObject.object.data.voiceId
629
+ const gameId = window.embeddingsState.clickedObject.object.data.game
630
+ const modelsPathForGame = window.userSettings[`modelspath_${gameId}`]
631
+ const modelPath = `${modelsPathForGame}/${voiceId}.pt`
632
+
633
+ if (fs.existsSync(modelPath)) {
634
+
635
+ window.changeGame(window.gameAssets[gameId])
636
+
637
+ // Simulate voice loading through the UI
638
+ let voiceName
639
+ window.games[gameId].models.forEach(model => {
640
+ model.variants.forEach(variant => {
641
+ if (variant.voiceId==voiceId) {
642
+ voiceName = model.voiceName
643
+ }
644
+ })
645
+ })
646
+ const voiceButton = Array.from(voiceTypeContainer.children).find(button => button.innerHTML==voiceName)
647
+ voiceButton.click()
648
+ closeModal().then(() => {
649
+ generateVoiceButton.click()
650
+ })
651
+
652
+ } else {
653
+ window.errorModal(window.i18n.VEMB_NO_MODEL)
654
+ }
655
+ } else {
656
+ window.errorModal(window.i18n.VEMB_SELECT_VOICE_FIRST)
657
+ }
658
+ })
659
+
660
+ embeddingsKey.addEventListener("change", () => {
661
+ if (embeddingsKey.value=="embsKey_game" && window.embeddingsState.gendersOn) {
662
+ window.toggleGenders()
663
+ } else if (embeddingsKey.value=="embsKey_gender" && !window.embeddingsState.gendersOn) {
664
+ window.toggleGenders()
665
+ }
666
+ })
667
+ embeddingsMalesCkbx.addEventListener("change", () => window.computeEmbsAndDimReduction())
668
+ embeddingsFemalesCkbx.addEventListener("change", () => window.computeEmbsAndDimReduction())
669
+ embeddingsOtherGendersCkbx.addEventListener("change", () => window.computeEmbsAndDimReduction())
670
+ embeddingsOnlyInstalledCkbx.addEventListener("change", () => window.computeEmbsAndDimReduction())
671
+ embeddingsAlgorithm.addEventListener("change", () => window.computeEmbsAndDimReduction())
672
+
673
+
674
+ window.computeEmbsAndDimReduction = (includeAllVoices=false) => {
675
+
676
+ const enabledGames = Array.from(embeddingsGamesListContainer.querySelectorAll("input"))
677
+ .map(elem => [elem.checked, elem.id.replace("embs_", "")])
678
+ .filter(checkedId => checkedId[0])
679
+ .map(checkedId => checkedId[1].replace("embs_", ""))
680
+
681
+ const enabledVoices = window.embeddingsState.voiceCheckboxes
682
+ .map(elem => [elem.checked, elem.id.replace("embsVoice_", "")])
683
+ .filter(checkedId => checkedId[0])
684
+ .map(checkedId => checkedId[1].replace("embsVoice_", ""))
685
+
686
+ if (enabledVoices.length<=2) {
687
+ return window.errorModal(window.i18n.EMBEDDINGS_NEED_AT_LEAST_3)
688
+ }
689
+
690
+
691
+ // Get together a list of voiceId->.wav path mappings
692
+ const mappings = []
693
+
694
+ if (includeAllVoices) {
695
+
696
+ Object.keys(window.games).forEach(gameId => {
697
+ const modelsPathForGame = window.userSettings[`modelspath_${gameId}`]
698
+
699
+ window.games[gameId].models.forEach(model => {
700
+ model.variants.forEach(variant => {
701
+ const audioPreviewPath = `${modelsPathForGame}/${variant.voiceId}.wav`
702
+ if (fs.existsSync(audioPreviewPath)) {
703
+ mappings.push(`${variant.voiceId}=${audioPreviewPath}=${model.voiceName}=${variant.gender}=${gameId}`)
704
+ }
705
+ })
706
+ })
707
+ })
708
+
709
+ } else {
710
+ Object.keys(window.embeddingsState.allData).forEach(voiceId => {
711
+ try {
712
+ const voiceMeta = window.embeddingsState.allData[voiceId]
713
+ const gender = voiceMeta.voiceGender.toLowerCase()
714
+
715
+ // Filter game-level voices
716
+ if (!enabledGames.includes(voiceMeta.gameId)) {
717
+ return
718
+ }
719
+ // Filter out by voice
720
+ if (!enabledVoices.includes(voiceId)) {
721
+ return
722
+ }
723
+ // Filter out data by gender
724
+ if (gender=="male" && !embeddingsMalesCkbx.checked) {
725
+ return
726
+ }
727
+ if (gender=="female" && !embeddingsFemalesCkbx.checked) {
728
+ return
729
+ }
730
+ if (gender=="other" && !embeddingsOtherGendersCkbx.checked) {
731
+ return
732
+ }
733
+
734
+ const modelsPathForGame = window.userSettings[`modelspath_${voiceMeta.gameId}`]
735
+ let audioPreviewPath = `${modelsPathForGame}/${voiceId}.wav`
736
+
737
+ if (!fs.existsSync(audioPreviewPath)) {
738
+ audioPreviewPath = ""
739
+ }
740
+
741
+ mappings.push(`${voiceId}=${audioPreviewPath}=${voiceMeta.voiceName}=${voiceMeta.voiceGender.toLowerCase()}=${voiceMeta.gameId}`)
742
+
743
+ } catch (e) {console.log(e)}
744
+ })
745
+ }
746
+
747
+ if (mappings.length<=2) {
748
+ return window.errorModal(window.i18n.EMBEDDINGS_NEED_AT_LEAST_3)
749
+ }
750
+
751
+
752
+ window.spinnerModal(window.i18n.VEMB_RECOMPUTING)
753
+
754
+ doFetch(`http://localhost:8008/computeEmbsAndDimReduction`, {
755
+ method: "Post",
756
+ body: JSON.stringify({
757
+ mappings: mappings.join("\n"),
758
+ onlyInstalled: embeddingsOnlyInstalledCkbx.checked,
759
+ algorithm: embeddingsAlgorithm.value.split("_")[1],
760
+ includeAllVoices
761
+ })
762
+ }).then(r=>r.text()).then(res => {
763
+ window.embeddingsState.data = {}
764
+ res.split("\n").forEach(voiceMetaAndCoords => {
765
+ const voiceId = voiceMetaAndCoords.split("=")[0]
766
+ const voiceName = voiceMetaAndCoords.split("=")[1]
767
+ const voiceGender = voiceMetaAndCoords.split("=")[2]
768
+ const gameId = voiceMetaAndCoords.split("=")[3]
769
+ const coords = voiceMetaAndCoords.split("=")[4].split(",").map(v => parseFloat(v))
770
+ window.embeddingsState.data[voiceId] = coords
771
+ if (includeAllVoices) {
772
+ window.embeddingsState.allData[voiceId] = {
773
+ voiceName,
774
+ voiceGender,
775
+ coords,
776
+ gameId
777
+ }
778
+ }
779
+ })
780
+ window.refreshEmbeddingsScenePoints()
781
+ closeModal(undefined, embeddingsContainer)
782
+
783
+ }).catch(e => {
784
+ console.log(e)
785
+ if (e.code =="ENOENT") {
786
+ closeModal(null, modalContainer).then(() => {
787
+ window.errorModal(window.i18n.ERR_SERVER)
788
+ })
789
+ }
790
+ })
791
+ }
792
+
793
+ embeddingsCloseHelpUI.addEventListener("click", () => {
794
+ embeddingsHelpUI.style.display = "none"
795
+ })
javascript/i18n.js ADDED
@@ -0,0 +1,1070 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ window.i18n = {}
3
+
4
+ window.i18n.setEnglish = () => {
5
+ window.i18n.SELECT_GAME = "Select Game"
6
+ window.i18n.SEARCH_VOICES = "Search voices..."
7
+ window.i18n.SELECT_VOICE = "Select voice"
8
+ window.i18n.SELECT_VOICE_TYPE = "Select Voice Type"
9
+ window.i18n.KEEP_SAMPLE = "Keep Sample"
10
+ window.i18n.GENERATE = "Generate"
11
+ window.i18n.GENERATE_VOICE = "Generate Voice"
12
+ window.i18n.RENAME_THE_FILE = "Rename the file"
13
+ window.i18n.DELETE_FILE = "Delete file"
14
+
15
+ window.i18n.PITCH_AND_ENERGY = "Pitch+Energy"
16
+ window.i18n.PITCH = "Pitch"
17
+ window.i18n.ENERGY = "Energy"
18
+ window.i18n.EMOTION = "Emotion"
19
+ window.i18n.DURATIONS = "Durations"
20
+ window.i18n.MANAGE = "Manage"
21
+ window.i18n.DEFAULT = "Default"
22
+ window.i18n.ENABLED = "Enabled"
23
+ window.i18n.ACTIONS = "Actions"
24
+ window.i18n.DESCRIPTION = "Description"
25
+ window.i18n.UNKNOWN = "Unknown"
26
+
27
+ window.i18n.VIEW_IS = "View:"
28
+ window.i18n.PITCH_IS = "Pitch:"
29
+ window.i18n.ENERGY_IS = "Energy:"
30
+ window.i18n.EMOTION_IS_ANGRY = "Emotion: Angry"
31
+ window.i18n.EMOTION_IS_HAPPY = "Emotion: Happy"
32
+ window.i18n.EMOTION_IS_SAD = "Emotion: Sad"
33
+ window.i18n.EMOTION_IS_SURPRISE = "Emotion: Surprise"
34
+ window.i18n.DURATION_IS = "Duration:"
35
+ window.i18n.EMOTION_IS = "Emotion:"
36
+ window.i18n.LENGTH = "Length:"
37
+ window.i18n.RESET_LETTER = "Reset Letter"
38
+ window.i18n.AUTO_REGEN = "Auto regenerate"
39
+ window.i18n.VOCODER = "Vocoder:"
40
+ window.i18n.BASE_LANGUAGE = "Base Language"
41
+ window.i18n.BASE_LANGUAGE_IS = "Base Language:"
42
+ window.i18n.USE_SR = "Use SR"
43
+ window.i18n.USE_SR_IS = "Use SR:"
44
+ window.i18n.USE_CLEANUP = "Use Clean-up"
45
+ window.i18n.USE_CLEANUP_IS = "Use Clean-up:"
46
+ window.i18n.USE_SR_TITLE = "Super-resolution (Hz) - SLOW ON CPU"
47
+ window.i18n.USE_SR_HINT = "Super-resolution improves the quality of your audio through Super-resolution of 22050Hz audio into 48000Hz audio. To be able to hear the difference, you need to make sure your ffmpeg settings don't then convert the audio back down to something low like 22050, in the post-processing. Keep the Hz setting to something higher like 48000 or 44100.<br><br>Also to note, this is a fairly slow process on the CPU, but it's pretty quick on the GPU, so I'd recommend switching on GPU usage if you have an NVIDIA card.<br><br>Hide this notice in the future?"
48
+ window.i18n.BASE_STYLE_EMB_IS = "Base Style:"
49
+ window.i18n.STYLE_EMB_IS = "Style:"
50
+ window.i18n.VARIANT_IS = "Variant:"
51
+
52
+ window.i18n.SEARCH_GAMES = "Search games..."
53
+ window.i18n.SEARCH_SETTINGS = "Search settings..."
54
+ window.i18n.SEARCH_N_VOICES = "Search _ voices..."
55
+ window.i18n.SEARCH_N_GAMES_WITH_N2_VOICES = "Search _1 games with _2 voices..."
56
+ window.i18n.RESET = "Reset"
57
+ window.i18n.AMPLIFY = "Amplify"
58
+ window.i18n.JITTER = "Jitter"
59
+ window.i18n.FLATTEN = "Flatten"
60
+ window.i18n.RAISE = "Raise"
61
+ window.i18n.LOWER = "Lower"
62
+ window.i18n.PACING = "Pacing"
63
+ window.i18n.OPEN = "Open"
64
+ window.i18n.DOWNLOAD = "Download"
65
+ window.i18n.VRAM_USAGE = "VRAM usage:"
66
+
67
+ window.i18n.SETTINGS = "Settings"
68
+ window.i18n.SETTINGS_GPU = "Use GPU (requires CUDA)"
69
+ window.i18n.SETTINGS_AUTOPLAY = "Autoplay generated audio"
70
+ window.i18n.SETTINGS_DEFAULT_HIFI = "Default to loading the HiFi vocoder on voice change, if available"
71
+ window.i18n.SETTINGS_KEEP_PACING = "Keep the same pacing value on new text generations"
72
+ window.i18n.SETTINGS_TOOLTIP = "Show the sliders tooltip"
73
+
74
+ window.i18n.SETTINGS_SHOW_DISCORD = "Show Discord status"
75
+ window.i18n.SETTINGS_DARKMODE = "Dark mode text prompt"
76
+ window.i18n.SETTINGS_PROMPTSIZE = "Text prompt font size"
77
+ window.i18n.SETTINGS_BG_FADE = "Background image fade opacity"
78
+ window.i18n.SETTINGS_AUTORELOADVOICES = "Auto-reload voices on files changes"
79
+ window.i18n.SETTINGS_KEEPEDITORSTATE = "Keep editor state on voice change"
80
+ window.i18n.SETTINGS_PITCHRANGEOVERRIDE = "Pitch range over-ride"
81
+ window.i18n.SETTINGS_OUTPUTJSON = "Output .json (needed for editing)"
82
+ window.i18n.SETTINGS_SEQNUMBERING = "Use sequential numbering for file names"
83
+ window.i18n.SETTINGS_SPACEPADDING = "Automatically pad text sequence with spaces (better quality, usually)"
84
+ window.i18n.SETTINGS_BASE_SPEAKER = "Base app output device"
85
+ window.i18n.SETTINGS_ALT_SPEAKER = "Alternate output device (ctrl+click play)"
86
+ window.i18n.SETTINGS_EXTERNALEDIT = "External program for editing audio"
87
+ window.i18n.SETTINGS_FFMPEG = "Use ffmpeg post-processing"
88
+ window.i18n.SETTINGS_FFMPEG_FORMAT = "Audio format (wav, mp3, etc)"
89
+ window.i18n.SETTINGS_FFMPEG_HZ = "Audio sample rate (Hz)"
90
+ window.i18n.SETTINGS_FFMPEG_PADSTART = "Silence padding start (ms)"
91
+ window.i18n.SETTINGS_FFMPEG_PADEND = "Silence padding end (ms)"
92
+ window.i18n.SETTINGS_FFMPEG_PITCHMULT = "Pitch multiplier"
93
+ window.i18n.SETTINGS_FFMPEG_TEMPO = "Tempo"
94
+ window.i18n.SETTINGS_FFMPEG_DEESSING = "De-essing"
95
+ window.i18n.SETTINGS_FFMPEG_BITDEPTH = "Audio bit depth"
96
+ window.i18n.SETTINGS_FFMPEG_NR = "Noise Reduction (db)"
97
+ window.i18n.SETTINGS_FFMPEG_NF = "Noise Floor (db)"
98
+ window.i18n.SETTINGS_FFMPEG_AMPLITUDE = "Amplitude multiplier"
99
+ window.i18n.SETTINGS_BATCH_JSON = "Output .json editor data for batch lines"
100
+ // window.i18n.SETTINGS_BATCH_FASTMODE = "Use fast mode for Batch synth (start next batch in parallel to current batch outputting via ffmpeg)"
101
+ // window.i18n.SETTINGS_BATCH_FASTMODE_MAX_PARALLELIZATIONS = "Maximum parallelizations of lines for Fast mode (lower this if running out of RAM)"
102
+ window.i18n.SETTINGS_BATCH_USEMULTIP = "Use multi-processing for batch mode ffmpeg output"
103
+ window.i18n.SETTINGS_BATCH_MULTIPCOUNT = "Number of processes (0 for cpu threads count -1)"
104
+ window.i18n.SETTINGS_MICROPHONE = "Microphone"
105
+ // window.i18n.SETTINGS_S2S_VOICE = "Speech-to-Speech voice"
106
+ window.i18n.SETTINGS_AUTOGENERATEVOICE = "Automatically generate voice"
107
+ window.i18n.SETTINGS_S2S_PREADJUST_PITCH = "Pre-adjust the input audio average pitch to match the xVASpeech model voice's"
108
+ window.i18n.SETTINGS_S2S_BGNOISE = "Remove background noise from microphone. You need to record a background noise clip first. (requires sox >= v14.4.2) "
109
+ window.i18n.SETTINGS_S2S_RECNOISE = "Record noise"
110
+ window.i18n.SETTINGS_S2S_BGNOISE_STRENGTH = "Noise removal strength (0.2-0.3 recommended)"
111
+ window.i18n.SETTINGS_VC_STRENGTH = "Voice Conversion strength (1-2 recommended)"
112
+ window.i18n.SETTINGS_MODELS_PATH = "models path"
113
+ window.i18n.SETTINGS_OUTPUT_PATH = "output path"
114
+ window.i18n.SETTINGS_RESET_SETTINGS = "Reset Settings"
115
+ window.i18n.SETTINGS_RESET_PATHS = "Reset Paths"
116
+ window.i18n.RESET_WHAT_PROMPT = "What would you like to reset?"
117
+ window.i18n.RESET_WHAT_TIP = "Shift+click the Reset button to reset all 3."
118
+ window.i18n.BATCH_METADATA_CONFIRM = "Please confirm the voice details"
119
+ window.i18n.BATCH_METADATA_TIP = "Select the voice in the main app, to pre-fill these"
120
+ window.i18n.SAVE_TO_CSV = "Save to CSV"
121
+
122
+ window.i18n.UPDATES_VERSION = "This app version: 1.0.0"
123
+ window.i18n.THIS_APP_VERSION = "This app version"
124
+ window.i18n.CHECK_FOR_UPDATES = "Check for updates now"
125
+ window.i18n.CANT_REACH_SERVER = "Can't reach server"
126
+ window.i18n.CHECKING_FOR_UPDATES = "Checking for updates..."
127
+ window.i18n.UPDATE_AVAILABLE = "Update available"
128
+ window.i18n.UPTODATE = "Up-to-date."
129
+ window.i18n.UPDATES_LOG = "Updates log:"
130
+ window.i18n.UPDATES_CHECK = "Check for updates now"
131
+
132
+ window.i18n.AVAILABLE = "Available"
133
+ window.i18n.PLUGINS = "Plugins"
134
+ window.i18n.PLUGINS_TRUSTED = "Download plugins only from trusted sources"
135
+ window.i18n.PLUGINSH_ENABLED = "Enabled"
136
+ window.i18n.PLUGINSH_ORDER = "Order"
137
+ window.i18n.PLUGINSH_NAME = "Plugin Name"
138
+ window.i18n.PLUGINSH_AUTHOR = "Author"
139
+ window.i18n.PLUGINSH_VERSION = "Plugin Version"
140
+ window.i18n.PLUGINSH_TYPE = "Type"
141
+ window.i18n.PLUGINSH_MINV = "Min App Version"
142
+ window.i18n.PLUGINSH_MAXV = "Max App Version"
143
+ window.i18n.PLUGINSH_DESCRIPTION = "Description"
144
+ window.i18n.PLUGINSH_PLUGINID = "Plugin Id"
145
+ window.i18n.PLUGINS_MOVEUP = "Move Up"
146
+ window.i18n.PLUGINS_MOVEDOWN = "Move Down"
147
+ window.i18n.PLUGINS_APPLY = "Apply"
148
+
149
+ window.i18n.APP_INFO = "App info"
150
+ window.i18n.APP_INFO_INSTR_1 = "For instructions on how to use the app, please watch"
151
+ window.i18n.APP_INFO_INSTR_2 = "this short video"
152
+ window.i18n.APP_INFO_INSTR_3 = "showcase on YouTube."
153
+ window.i18n.APP_INFO_INSTR_4 = "You can also view and/or contribute to the community guide on GitHub "
154
+ window.i18n.APP_INFO_INSTR_5 = "here"
155
+
156
+ window.i18n.KEYBOARD_REFERENCE = "Keyboard shortcuts reference"
157
+ window.i18n.KEYBOARD_ENTER = "Enter"
158
+ window.i18n.KEYBOARD_ENTER_DO = "Generate the audio"
159
+ window.i18n.KEYBOARD_ESCAPE = "Escape"
160
+ window.i18n.KEYBOARD_ESCAPE_DO = "Close modals and menus"
161
+ window.i18n.KEYBOARD_SPACE = "Space"
162
+ window.i18n.KEYBOARD_SPACE_DO = "Bring focus to the input textarea"
163
+ window.i18n.KEYBOARD_CTRLS = "Ctrl+S"
164
+ window.i18n.KEYBOARD_CTRLS_DO = "Keep sample"
165
+ window.i18n.KEYBOARD_CTRLSHIFTS = "Ctrl+Shift-S"
166
+ window.i18n.KEYBOARD_CTRLSHIFTS_DO = "Keep sample (but with naming prompt)"
167
+ window.i18n.KEYBOARD_YN = "Y/N"
168
+ window.i18n.KEYBOARD_YN_DO = "Yes/No options in prompt modals"
169
+ window.i18n.KEYBOARD_LR = "Left/Right arrows"
170
+ window.i18n.KEYBOARD_LR_DO = "Move left/right along which letter is focused"
171
+ window.i18n.KEYBOARD_SHIFT_LR = "Shift-Left/Right arrows"
172
+ window.i18n.KEYBOARD_SHIFT_LR_DO = "Create multi-letter selection range"
173
+ window.i18n.KEYBOARD_ALT_CTRL_LR = "Alt-Ctrl-Left/Right arrows"
174
+ window.i18n.KEYBOARD_ALT_CTRL_LR_DO = "Adjust width of letter selection"
175
+ window.i18n.KEYBOARD_UD = "Up/Down arrows"
176
+ window.i18n.KEYBOARD_UD_DO = "Move pitch up/down for the letter(s) selected"
177
+ window.i18n.KEYBOARD_CTRL_LR = "Ctrl+Left/Right arrows"
178
+ window.i18n.KEYBOARD_CTRL_LR_DO = "Move the sequence-wide pacing slider"
179
+ window.i18n.KEYBOARD_CTRL_UD = "Ctrl+Up/Down arrows"
180
+ window.i18n.KEYBOARD_CTRL_UD_DO = "Pitch increase/decrease buttons"
181
+ window.i18n.KEYBOARD_CTRLSHIFTUD = "Ctrl+Shift+Up/Down arrows"
182
+ window.i18n.KEYBOARD_CTRLSHIFTUD_DO = "Pitch amplify/flatten buttons"
183
+ window.i18n.KEYBOARD_CTRLENTER = "Ctrl+Enter"
184
+ window.i18n.KEYBOARD_CTRLENTER_DO = "Manually re-generate a line"
185
+ window.i18n.KEYBOARD_CTRLA = "Ctrl+A"
186
+ window.i18n.KEYBOARD_CTRLA_DO = "Select all editor sequence letters"
187
+
188
+ window.i18n.SUPPORT = "Support"
189
+ window.i18n.SUPPORT_LINK = "You can support 'xVASynth' development on patreon"
190
+ window.i18n.SUPPORT_THANKS = "Special thanks to all xVASynth supporters:"
191
+
192
+ window.i18n.SUPPORT_GAMES = "Search games..."
193
+
194
+ window.i18n.EULA_ACCEPT = "I accept the EULA"
195
+ window.i18n.EULA_CLOSE = "Close"
196
+
197
+ window.i18n.BATCH_SYNTHESIS = "Batch Synthesis"
198
+ window.i18n.BATCH_SIZE = "Batch Size"
199
+ window.i18n.BATCH_INSTR1 = `Place the .csv batch file(s) into the box below. The mandatory columns are "game_id", "voice_id", and "text", but you can also specify output filename/filepath under "out_path", pacing under "pacing", and vocoder under "vocoder" (Available options: 'hifi', 'quickanddirty', 'waveglow', 'waveglowBIG'). Click the "Generate sample" button to generate an example .csv file if you need one. Watch`
200
+ window.i18n.BATCH_INSTR2 = "this short video"
201
+ window.i18n.BATCH_INSTR3 = "for a demo and more instructions."
202
+ window.i18n.BATCH_GEN_SAMPLE = "Generate Sample"
203
+ window.i18n.BATCH_INSTRUCTIONS = "Instructions"
204
+ window.i18n.BATCH_DROPZONE = "Drag and drop .csv files here"
205
+
206
+ window.i18n.BATCHH_NUM = "#"
207
+ window.i18n.BATCHH_STATUS = "Status"
208
+ window.i18n.BATCHH_ACTIONS = "Actions"
209
+ window.i18n.BATCHH_GAME = "Game"
210
+ window.i18n.BATCHH_VOICE = "Voice"
211
+ window.i18n.BATCHH_TEXT = "Text or <i>VC content</i>"
212
+ window.i18n.BATCHH_VC_STYLE = "VC Style"
213
+ window.i18n.BATCHH_VOCODER = "Vocoder"
214
+ window.i18n.BATCHH_OUTPATH = "Out Path"
215
+ window.i18n.BATCHH_PACING = "Pacing"
216
+ window.i18n.BATCHH_PITCH_AMP = "Pitch Amp."
217
+ window.i18n.BATCHH_BASE_LANG = "Base lang"
218
+ window.i18n.BATCH_ABS_DIR_PLACEHOLDER = "Complete absolute directory path to output"
219
+
220
+ window.i18n.BATCH_CLEAR_DIR = "Clear out the directory first"
221
+ window.i18n.BATCH_SKIP = "Skip existing output"
222
+ window.i18n.BATCH_OUTPUTNUMERICALLY = "Output file names in numerical order"
223
+ window.i18n.BATCH_CURRENTLYDOING = "currently doing..."
224
+ window.i18n.BATCH_SYNTHESIZE = "Synthesize Batch"
225
+ window.i18n.BATCH_PAUSE = "Pause"
226
+ window.i18n.BATCH_STOP = "Stop"
227
+ window.i18n.BATCH_CLEAR = "Clear"
228
+ window.i18n.BATCH_OPENOUT = "Open Output"
229
+
230
+ window.i18n.S2S_RECORD_SAMPLE = "Record sample"
231
+ window.i18n.FEMALE = "Female"
232
+ window.i18n.MALE = "Male"
233
+ window.i18n.S2S_OTHER = "Other"
234
+ window.i18n.VC_ONLY_FOR_V3 = "Voice conversion only available for v3 models."
235
+
236
+
237
+ window.i18n.VW_INPUT_TEXTAREA_PLACEHOLDER = "Enter a sentence to use for generating preview samples of your crafted voice with the current embedding and proposed delta change"
238
+ window.i18n.CURRENT = "Current"
239
+ window.i18n.CURRENT_EMB = "Current Embedding"
240
+ window.i18n.CURRENT_DELTA = "Current Delta"
241
+ window.i18n.STRENGTH = "Strength"
242
+ window.i18n.APPLY_DELTA = "Apply Delta"
243
+ window.i18n.VW_REF_FILE_A = "Reference Audio File A:"
244
+ window.i18n.VW_REF_FILE_B = "Reference Audio File B:"
245
+ window.i18n.VW_BASE_MODEL = "Base Model (v3 models only)"
246
+ window.i18n.NAME_OF_YOUR_VOICE = "Name of your voice"
247
+ window.i18n.UNIQUE_ID_FOR_VOICE = "A unique identifier for your voice (eg: f4_nate)"
248
+ window.i18n.YOUR_NAME_FOR_CREDITS = "Your name for credits"
249
+
250
+
251
+
252
+
253
+
254
+ // Dynamic
255
+ window.i18n.SOMETHING_WENT_WRONG = "Something went wrong"
256
+ window.i18n.THERE_WAS_A_PROBLEM = "There was a problem"
257
+ window.i18n.ENTER_DIR_PATH = "Please enter a directory path"
258
+ window.i18n.SURE_RESET_SETTINGS = `Are you sure you'd like to reset your settings?`
259
+ window.i18n.SURE_RESET_PATHS = `Are you sure you'd like to reset your paths? This includes the paths for models, and output.`
260
+ window.i18n.LOAD_MODEL = "Load model"
261
+ window.i18n.LOAD_TARGET_MODEL = "Please load a target voice from the panel on the left, first."
262
+ window.i18n.NO_XVASPEECH_MODELS = "No FastPitch1.1 models are installed"
263
+ window.i18n.ONLY_WAV_S2S = "Only .wav files are supported for speech-to-speech file input at the moment."
264
+ window.i18n.NO_MODELS_IN = "No models in"
265
+ window.i18n.NO_MODELS_FOUND = "No models found"
266
+ window.i18n.MODEL_REQUIRES_VERSION = `This model requires app version`
267
+ window.i18n.OPEN_CONTAINING_FOLDER = "Open containing folder"
268
+ window.i18n.ADJUST_SAMPLE_IN_EDITOR = "Adjust sample in the editor"
269
+ window.i18n.ENTER_NEW_FILENAME_UNCHANGED_CANCEL = "Enter new file name, or submit unchanged to cancel."
270
+ window.i18n.EDIT_IN_EXTERNAL_PROGRAM = "Edit in external program"
271
+ window.i18n.FOLLOWING_PATH_NOT_VALID = "The following program path is not valid"
272
+ window.i18n.SPECIFY_EDIT_TOOL = "Specify your audio editing tool in the settings"
273
+ window.i18n.SURE_DELETE = "Are you sure you'd like to delete this file?"
274
+ window.i18n.LOADING_VOICE = "Loading voice"
275
+ window.i18n.ERR_SERVER = "There was an issue connecting to the python server.<br><br>Try again in a few seconds. If the issue persists, make sure localhost port 8008 is free, or send the server.log file to me on GitHub or Nexus."
276
+ window.i18n.ABOUT_TO_SAVE_FROM_N1_TO_N2_WITH_OPTIONS = `About to save file from _1 to _2 with options`
277
+ window.i18n.SAVING_AUDIO_FILE = "Saving the audio file..."
278
+ window.i18n.TEMP_FILE_NOT_EXIST = "The temporary file does not exist at this file path"
279
+ window.i18n.OUT_DIR_NOT_EXIST = "The output directory does not exist at this file path"
280
+ window.i18n.YOU_CAN_CHANGE_IN_SETTINGS = "You can change this in the settings."
281
+ window.i18n.FILE_EXISTS_ADJUST = `File already exists. Adjust the file name here, or submit without changing to overwrite the old file.`
282
+ window.i18n.ENTER_FILE_NAME = `Enter file name`
283
+ window.i18n.WAVEGLOW_NOT_FOUND = "WaveGlow model not found. Download it also (separate download), and place the .pt file in the models folder."
284
+ window.i18n.BATCH_MODEL_NOT_FOUND = "Model not found."
285
+ window.i18n.BATCH_DOWNLOAD_WAVEGLOW = "Download WaveGlow files separately if you haven't, or check the path in the settings."
286
+ window.i18n.ERR_LOADING_MODELS_FOR_GAME = "ERROR loading models for game"
287
+ window.i18n.ERR_LOADING_MODELS_FOR_GAME_WITH_FILENAME = "ERROR loading models for game _1 with filename:"
288
+ window.i18n.ERR_XVASPEECH_MODEL_VERSION = `This xVASpeech model needs minimum app version _1. Your app version:`
289
+ window.i18n.ERR_ARPABET_NOT_EXIST = `The following ARPAbet symbol does not exist: _1`
290
+
291
+ window.i18n.ENTER_VOICE_NAME = "Please enter a voice name"
292
+ window.i18n.ENTER_VOICE_ID = "Please enter a voice ID"
293
+ window.i18n.VOICE_CREATED_AT = "Voice successfully saved at the following location:<br><br>_1"
294
+ window.i18n.CONFIRM_DELETE_CRAFTED_VOICE = "Are you sure you'd like to delete the crafted voice '_1' at the following location?<br><br>_2"
295
+ window.i18n.SUCCESSFULLY_DELETED_CRAFTED_VOICE = "Successfully deleted the crafted voice model."
296
+ window.i18n.ENTER_VOICE_CRAFTING_STARTING_EMB = "Please provide a starting embedding. Drag and drop a .wav audio file over the 'Current Embedding' field below."
297
+
298
+ window.i18n.CHANGING_MODELS = "Changing models..."
299
+ window.i18n.CHANGING_DEVICE = "Changing device..."
300
+ window.i18n.PROCESSING_DATA = "Processing data..."
301
+ window.i18n.DELETING_FILE = "Deleting file"
302
+ window.i18n.DELETING_NEW_FILE = "Deleting new file"
303
+ window.i18n.FAILED = "Failed"
304
+ window.i18n.DONE = "Done"
305
+ window.i18n.READY = "Ready"
306
+ window.i18n.RUNNING = "Running"
307
+ window.i18n.PAUSED = "Paused"
308
+ window.i18n.PAUSE = "Pause"
309
+ window.i18n.PLAY = "Play"
310
+ window.i18n.EDIT = "Edit"
311
+ window.i18n.EDIT_IS = "Edit:"
312
+ window.i18n.RESUME = "Resume"
313
+ window.i18n.STOPPED = "Stopped"
314
+ window.i18n.SYNTHESIZING = "Synthesizing"
315
+ window.i18n.LINES = "lines"
316
+ window.i18n.LINE = "Line"
317
+ window.i18n.ERROR = "Error"
318
+ window.i18n.MISSING = "Missing"
319
+ window.i18n.INPUT = "Input"
320
+ window.i18n.OUTPUT = "Output"
321
+ window.i18n.OUTPUTTING = "Outputting"
322
+ window.i18n.SUBMIT = "Submit"
323
+ window.i18n.CLOSE = "Close"
324
+ window.i18n.YES = "Yes"
325
+ window.i18n.NO = "No"
326
+ window.i18n.VOICE = "voice"
327
+ window.i18n.VOICE_PLURAL = "voices"
328
+ window.i18n.NEW = "new"
329
+ window.i18n.PAGE = "Page:"
330
+ window.i18n.NEXT = "Next"
331
+ window.i18n.PREVIOUS = "Previous"
332
+ window.i18n.LOADING = "Loading"
333
+ window.i18n.MAY_TAKE_A_MINUTE = "May take a minute (but not much more)"
334
+ window.i18n.BUILDING_FASTPITCH = "Building FastPitch model"
335
+ window.i18n.LOADING_WAVEGLOW = "Loading WaveGlow model"
336
+ window.i18n.STARTING_PYTHON = "Starting up the python backend"
337
+ window.i18n.NOT_USING_GPU = "Not using GPU"
338
+
339
+ window.i18n.BATCH_CHANGING_MODEL_TO = "Changing voice model to"
340
+ window.i18n.BATCH_CHANGING_VOCODER_TO = "Changing vocoder to"
341
+ window.i18n.BATCH_OUTPUTTING_FFMPEG = `Outputting audio via ffmpeg...`
342
+
343
+ window.i18n.BATCH_ERR_NO_VOICES = "No voice models available in the app. Load at least one."
344
+ window.i18n.BATCH_ERR_GAMEID = "does not match any available games"
345
+ window.i18n.BATCH_ERR_VOICEID = "does not match any in the game"
346
+ window.i18n.BATCH_ERR_VOCODER1 = "does not exist. Available options"
347
+ window.i18n.BATCH_ERR_VOCODER2 = "(or leaving it blank)"
348
+ window.i18n.BATCH_ERR_CUDA_OOM = "CUDA OOM: There is not enough VRAM to run this. Try lowering the batch size, or shortening very long sentences."
349
+ window.i18n.BATCH_ERR_IN_PROGRESS = "Batch synthesis is in progress. Loading a model in the main app now would break things."
350
+ window.i18n.BATCH_ERR_EDIT = "Batch synthesis is in progress. Pause or stop it first to enable editor."
351
+ window.i18n.BATCH_ERR_SKIPPEDALL = "No records imported, but _1 were skipped as they already exist."
352
+
353
+ window.i18n.ERR_LOADING_PLUGIN = "Error loading plugin"
354
+ window.i18n.SUCCESSFULLY_INITIALIZED = "Successfully initialized"
355
+ window.i18n.FAILED_INIT_FOLLOWING = "Failed to initialize the following"
356
+ window.i18n.CHECK_SERVERLOG = "Check the server.log file for detailed error traces"
357
+ window.i18n.SUCC_NO_ACTIVE_PLUGINS = "Success. No plugins active."
358
+ window.i18n.APP_RESTART_NEEDED = "App restart is required for at least one of the plugins to take effect."
359
+ window.i18n.ERR_LOADING_CSS = "Error loading style file for plugin"
360
+ window.i18n.PLUGIN = "Plugin"
361
+ window.i18n.PLUGINS = "Plugins"
362
+ window.i18n.CANT_IMPORT_FILE_FOR_HOOK_TASK_ENTRYPOINT = "Cannot import _1 file for _2 _3 entry-point"
363
+ window.i18n.ONLY_JS = "Only JavaScript files are supported right now."
364
+ window.i18n.PLUGIN_RUN_ERROR = "Plugin run error at event"
365
+
366
+ window.i18n.MONDAY = "Monday"
367
+ window.i18n.TUESDAY = "Tuesday"
368
+ window.i18n.WEDNESDAY = "Wednesday"
369
+ window.i18n.THURSDAY = "Thursday"
370
+ window.i18n.FRIDAY = "Friday"
371
+ window.i18n.SATURDAY = "Saturday"
372
+ window.i18n.SUNDAY = "Sunday"
373
+
374
+ window.i18n.EMBEDDINGS = "Embeddings"
375
+ window.i18n.EMB_NAME = "Embedding Name"
376
+ window.i18n.EMB_DESCRIPTION = "Embedding description"
377
+ window.i18n.EMB_ID = "Embedding ID"
378
+ window.i18n.STYLE_EMB_ID = "Embedding ID (Write a short, descriptive, alpha-numerical ID you think will be unique)"
379
+ window.i18n.EMB_ID = "Emb ID"
380
+ window.i18n.STYLE_EMBEDDINGS = "Style Embeddings"
381
+ window.i18n.STYLE_EMB_WAVPATH = "Wav file path"
382
+ window.i18n.STYLE_EMB_WAVPATH_PLACEHOLDER = "Drag+drop or full file path"
383
+ window.i18n.ERROR_FILE_MUST_BE_WAV = "File type must be .wav"
384
+ window.i18n.ERROR_NEED_WAV_FILE = "Add a wav file path"
385
+ window.i18n.STYLE_EMB_VALUES = "Style embedding values"
386
+ window.i18n.ERROR_MISSING_FIELDS = "Missing values for the following fields: _1"
387
+ window.i18n.CONFIRM_DELETE_STYLE_EMB = "Are you sure you want to delete this style embedding forever?"
388
+
389
+
390
+ window.i18n.TOTD_1 = "You can right-click a voice on the left to hear a preview of the voice"
391
+ window.i18n.TOTD_2 = "You can right-click the microphone icon after a recording, to hear back the audio you recorded/inserted"
392
+ window.i18n.TOTD_3 = "There are a number of keyboard shortcuts you can use. Check the info tab for a reference"
393
+ window.i18n.TOTD_4 = "Check the community guide for tips for how to get the best quality out of the tool. This is linked in the info (i) menu"
394
+ window.i18n.TOTD_5 = "You can create a multi-letter selection in the editor by Ctrl+clicking several letters"
395
+ window.i18n.TOTD_6 = "You can shift-click the 'Keep Sample' button (or Ctrl+Shift+S) to first give your file a custom name before saving"
396
+ window.i18n.TOTD_7 = "You can alt+click editor letters to make a multi-letter selection for the entire word you click on"
397
+ window.i18n.TOTD_8 = "You can drag+drop multiple .csv or .txt files into batch mode"
398
+ window.i18n.TOTD_9 = "You can use .txt files in batch mode instead of .csv files, if you first click a voice in the main app to assign the lines to"
399
+ window.i18n.TOTD_10 = "If you have a compatible NVIDIA GPU, and CUDA installed, you can switch to the CPU+GPU installation. Using the GPU is much faster, especially for batch mode."
400
+ window.i18n.TOTD_11 = "The HiFi-GAN vocoder (v1 and v2 models) is normally the best quality, but you can also download and use WaveGlow vocoders, if you'd like."
401
+ window.i18n.TOTD_12 = "(v1 and v2 models) If the 'Keep editor state on voice changes' option is ticked on, you can generate a line using one voice, then switch to a different voice, and click the 'Generate Voice' button again to generate a line using the new voice, but using a similar speaking style to the first voice."
402
+ window.i18n.TOTD_13 = "If you set the 'Alternative Output device' to something other than the default device, you can Ctrl-click when playing audio, to have it play on a different speaker. You can couple this with something like Voicemeeter Banana split, to have the app speak for you over the microphone, for voice chat, or other audio recording."
403
+ window.i18n.TOTD_14 = "If you add the path to an audio editing program to the 'External Program for Editing audio' setting, you can open generated audio straight in that program in one click, from the output records on the main page"
404
+ window.i18n.TOTD_15 = "FFmpeg automatically directly applies a few different audio post processing tasks on the generated audio. This can include Hz resampling, silence padding to the start and/or end of the audio, bit depth, loudness, noise reduction, de-essing, pitch and tempo modifiers, and different audio formats. Play with these to get the best quality for a particular voice"
405
+ window.i18n.TOTD_16 = "You can tick on the 'Fast mode' for batch mode to parallelize the audio generation and the audio output (via ffmpeg for example)"
406
+ window.i18n.TOTD_17 = "You can enable multiprocessing for ffmpeg file output in batch mode, to speed up the output process. This is especially useful if you use a large batch size, and your CPU has plenty of threads. This can be used together with Fast Mode."
407
+ window.i18n.TOTD_18 = "If you're having trouble formatting a .csv file for batch mode, you can change the delimiter in the settings to something else (for example a pipe symbol '|')"
408
+ window.i18n.TOTD_19 = "You can change the folder location of your output files, as well as the models. I'd recommend keeping your model files on an SSD, to reduce the loading time."
409
+ window.i18n.TOTD_20 = "Use the voice embeddings search menu to get a 3D visualisation of all the voices in the app (including some 'officially' trained voices not downloaded yet). You can use this as a reference for voice similarly search, to see what other voices there are, which sound similar to a particular voice."
410
+ window.i18n.TOTD_21 = "You can right click on the points in the 3D voice embeddings visualisation, to hear a preview of that voice. This will only work for the voices you have installed, locally."
411
+ window.i18n.TOTD_22 = "The app is customisable via third-party plugins. Plugins can be managed from the plugins menu, and they can change, or add to the front end app functionality/looks (the UI), as well as the python back-end (the machine learning code). If you're interested in developing such a plugin, there is a full developer reference on the GitHub wiki, here: https://github.com/DanRuta/xvasynth-community-guide"
412
+ window.i18n.TOTD_23 = "If you log into nexusmods.com from within the app, you can check for new and updated voice models on your chosen Nexus pages. You can also endorse these, as well as any plugins configured with a nexus link. If you have a premium membership for the Nexus, you can also download (or batch download) all available voices, and have them installed automatically."
413
+ window.i18n.TOTD_24 = "You can manage the list of Nexus pages to check for voice models by clicking the 'Manage Repos' button in the Nexus menu, or by editing the repositories.txt file"
414
+ window.i18n.TOTD_25 = "You can enable/disable error sounds in the settings. You can also pick a different sound, if you'd prefer something else"
415
+ window.i18n.TOTD_26 = "You can resize the window by dragging one of the bottom corners"
416
+ window.i18n.TOTD_27 = "You can right-click game buttons in the nexus window 'Games' list and voice embeddings 'Games' list, to de-select all other games apart from the one you right-clicked"
417
+ window.i18n.TOTD_28 = "With v3 models, you can change the default speaking style of your voice by creating an embedding for it. You do so by drag+dropping an example audio file (usually from the same original voice) into the Management menu."
418
+ window.i18n.TOTD_29 = "The v3 models don't pre-generate pitch or energy values. Instead, the values in the editor are multipliers rather than absolute values. So initially, tney are set to 1, and you can CHANGE what they are rather than setting values like for v1 and v2 models."
419
+ window.i18n.TOTD_30 = "To get the absolute highest quality from an audio file, you should enable the 'Use SR' option, to run super-resolution from the default 22050Hz into 48000Hz. It's best to use the GPU mode for this, else it can be quite slow. You also need to make sure that you didn't set the ffmpeg Hz post-processing value to something low like 22050, else you won't hear the benefits."
420
+ window.i18n.TOTD_31 = "With v3 models, you can right click the sliders editor to open the context menu, where you can select to copy the final symbol sequence to clipboard."
421
+ window.i18n.TOTD_32 = "You can use the Ctrl+Enter shortcut to manually kick offf re-generating a line."
422
+
423
+ window.i18n.TOTD_NO_UNSEEN = "There are no unseen tips left to show. Untick the 'Only show unseen tips' setting to show all tips."
424
+
425
+
426
+ window.i18n.LINES_PER_SECOND = "lines per second"
427
+ window.i18n.ETA_FINISHED = "Estimated time until finished:"
428
+ window.i18n.LOGGED_IN_AS = "Logged in as: "
429
+ window.i18n.GAMES = "Games"
430
+ window.i18n.MODELS = "Models"
431
+ window.i18n.SHOW_NEW_UPDATED = "Show only new/updated"
432
+ window.i18n.CHECK_NOW = "Check now"
433
+ window.i18n.MANAGE_REPOS = "Manage repos"
434
+ window.i18n.LOG_IN = "Log in"
435
+ window.i18n.LOG_OUT = "Log out"
436
+ window.i18n.NAME = "Name"
437
+ window.i18n.AUTHOR = "Author"
438
+ window.i18n.VERSION = "Version"
439
+ window.i18n.DATE = "Date"
440
+ window.i18n.TYPE = "Type"
441
+ window.i18n.NOTES = "Notes"
442
+ window.i18n.DOWNLOADING = "Downloading:"
443
+ window.i18n.INSTALLING = "Installing:"
444
+ window.i18n.FINISHED = "Finished:"
445
+ window.i18n.DOWNLOAD_ALL = "Download All"
446
+ window.i18n.REPOSITORIES = "Repositories"
447
+ window.i18n.ADD = "Add"
448
+ window.i18n.REMOVE = "Remove"
449
+ window.i18n.V_EMB_VIS = "Voice embeddings visualiser"
450
+ window.i18n.VOICES = "Voices"
451
+ window.i18n.SHOW = "Show"
452
+ window.i18n.GAME = "Game"
453
+ window.i18n.GENDER = "Gender"
454
+ window.i18n.GENDER_IS = "Gender:"
455
+ window.i18n.GAME_IS = "Game:"
456
+ window.i18n.PREVIEW = "Preview"
457
+ window.i18n.LOAD = "Load"
458
+
459
+ window.i18n.VOICE_NAME = "Voice Name"
460
+ window.i18n.VOICE_NAME_IS = "Voice Name:"
461
+
462
+ window.i18n.VEMB_INSTR_1 = "Left click drag to rotate"
463
+ window.i18n.VEMB_INSTR_2 = "Right click drag to pan"
464
+ window.i18n.VEMB_INSTR_3 = "Mouse wheel scroll to zoom"
465
+ window.i18n.VEMB_INSTR_4 = "Left click on voice to select"
466
+ window.i18n.VEMB_INSTR_5 = "Right click on voice to play sample"
467
+
468
+ window.i18n.MALES = "Males"
469
+ window.i18n.FEMALES = "Females"
470
+ window.i18n.OTHER = "Other"
471
+
472
+ window.i18n.SHOW_ONLY_INSTALED = "Show only installed voices"
473
+ window.i18n.KEY_IS = "Key:"
474
+ window.i18n.ALGORITHM = "Algorithm"
475
+
476
+ window.i18n.TOTD = "Tip of the day"
477
+ window.i18n.TOTD_SHOW = "Show tip of the day"
478
+ window.i18n.TOTD_SHOW_UNSEEN = "Only show unseen tips"
479
+ window.i18n.TOTD_PREV_TIP = "Previous tip"
480
+ window.i18n.TOTD_NEXT_TIP = "Next tip"
481
+
482
+ window.i18n.ENDORSE = "Endorse"
483
+ window.i18n.GET_MORE_VOICES = "Get more voices"
484
+
485
+ window.i18n.CURR_INSTALL = "Current installation:"
486
+ window.i18n.CHANGE_TO_GPU = "Change to CPU+GPU"
487
+ window.i18n.CHANGE_TO_CPU = "Change to CPU"
488
+ window.i18n.USE_SOUND_ERR = "Use sound for errors"
489
+ window.i18n.ERR_SOUNDFILE = "Error sound file"
490
+ window.i18n.SHOW_NOW = "Show now"
491
+ window.i18n.SETTINGS_PLAYCHANGEDAUDIO = "Play only changed audio, when regenerating"
492
+ window.i18n.SETTINGS_PREAPPLY_FFMPEG = "(recommended) Pre-apply ffmpeg effects to the preview sample"
493
+ window.i18n.SETTINGS_USE_NR = "Use noise reduction (recommended when using SR)"
494
+ window.i18n.SETTINGS_DOUBLE_AMP_DISPLAY = "Also display amplitude setting in the editor"
495
+ window.i18n.SETTINGS_CSV_DELIMITER = "CSV delimiter"
496
+ window.i18n.SETTINGS_PAGINATION_SIZE_BATCH = "Batch pagination size"
497
+ window.i18n.SETTINGS_PAGINATION_SIZE_ARPABET = "ARPAbet pagination size"
498
+ window.i18n.SETTINGS_MAX_FILENAME_LENGTH = "Maximum filename characters (trimming for maximum windows filepath length)"
499
+ window.i18n.SETTINGS_CLEAR_TEXT_AFTER_GENERATION = "Clear the text input after generation"
500
+ window.i18n.SETTINGS_GROUP_VOICEID = "Group voices by voiceId and vocoder in preprocessing to minimize model switching"
501
+ window.i18n.SETTINGS_GROUP_VOCODER = "Also do a secondary group by the vocoder - can take long to do with big files (100k+ lines)"
502
+ window.i18n.SETTING_HIGHLIGHT_ONLY_MODELS_V = "Highlight only models with at least this version"
503
+ window.i18n.SETTING_OUTPUTFILES_PAGINATION = "Output records pagination size"
504
+
505
+
506
+ window.i18n.SEARCH_OUTPUT = "Search output file names..."
507
+ window.i18n.SEARCH_OUTPUT_PROMPT = "Search prompts..."
508
+ window.i18n.DELETE = "Delete"
509
+ window.i18n.DELETE_ALL = "Delete all"
510
+ window.i18n.DELETE_ALL_FILES_CONFIRM = "Are you sure you'd like to delete all files for this voice? This will delete all _1 files in the following output directory:<br>_2"
511
+ window.i18n.DELETE_ALL_FILES_ERR_NO_FILES = "There are no files in the following output directory:<br>_1"
512
+ window.i18n.SORT_BY = "Sort by"
513
+ window.i18n.ASCENDING = "Ascending"
514
+ window.i18n.DESCENDING = "Descending"
515
+ window.i18n.TIME = "Time"
516
+
517
+ window.i18n.ERR_LOGGING_INTO_NEXUS = "Error attempting to log into nexusmods"
518
+ window.i18n.LOGGING_INTO_NEXUS = "Logging into nexusmods (check your browser)..."
519
+ window.i18n.NEXUS_PREMIUM = "Nexus requires premium membership for using their API for file downloads"
520
+ window.i18n.NEXUS_ORIG_ERR = "Original error message"
521
+ window.i18n.FAILED_DOWNLOAD = "Failed to download"
522
+ window.i18n.DONE_INSTALLING = "Done installing"
523
+ window.i18n.CHECKING_NEXUS = "Checking nexusmods.com..."
524
+ window.i18n.NEXUS_NOT_DOWNLOADED_MOD = "You need to first download something from this repo to be able to endorse it."
525
+ window.i18n.NEXUS_TOO_SOON_AFTER_DOWNLOAD = "Nexus requires you to wait at least 15 mins (at the time of writing) before you can endorse."
526
+ window.i18n.NEXUS_IS_OWN_MOD = "Nexus does not allow you to rate your own content."
527
+ window.i18n.YOURS = "Yours"
528
+ window.i18n.NEXUS_ENTER_LINK = "Enter the nexusmods.com link to use as a repository"
529
+ window.i18n.NEXUS_LINK_EXISTS = "This link already exists."
530
+ window.i18n.ERROR_FROM_NEXUS = "<h3>Error using Nexus API. Their response:</h3> <br>_1"
531
+
532
+ window.i18n.VEMB_VOICE_NOT_ENABLED = "This voice is not enabled"
533
+ window.i18n.VEMB_NO_PREVIEW = "No preview audio file available"
534
+ window.i18n.VEMB_SELECT_VOICE_FIRST = "Select a voice from the scene below first."
535
+ window.i18n.VEMB_NO_MODEL = "No model file available. Download it if you haven't already."
536
+ window.i18n.VEMB_RECOMPUTING = "Re-computing embeddings and dimensionality reduction on voices. May take a minute the first time, subsequent runs should be instant."
537
+
538
+ window.i18n.SETTINGS_FOR_PLUGIN = "Settings for plugin: <i>_1</i>"
539
+ window.i18n.EMBEDDINGS_NEED_AT_LEAST_3 = "You need at least 3 voices to run dimensionality reduction for the plot"
540
+
541
+
542
+
543
+ window.i18n.ARPABET_ERROR_BAD_SYMBOLS = "Found non-ARPAbet symbols: _1"
544
+ window.i18n.ARPABET_ERROR_EMPTY_INPUT = "Words or ARPAbet symbols can't be left empty"
545
+ window.i18n.PAGINATION_X_OF_Y = "_1 of _2"
546
+ window.i18n.ARPABET_CONFIRM_ENABLE_ALL = "Are you sure you'd like to enable ALL words for the following dictionary?<br><br><i>_1</i>"
547
+ window.i18n.ARPABET_CONFIRM_DISABLE_ALL = "Are you sure you'd like to disable ALL words for the following dictionary?<br><br><i>_1</i>"
548
+ window.i18n.ARPABET_CONFIRM_DELETE_WORD = "Are you sure you'd like to delete the following word?<br><br><i>_1</i>"
549
+ window.i18n.ARPABET_CONFIRM_SAME_WORD = "The word '_1' already exists in the following dictionaries:<br><br><i>_2</i><br><br>Are you sure you'd like to add it?"
550
+
551
+ window.i18n.ONLY_ENABLED = "Only enabled"
552
+
553
+ window.i18n.DICTIONARIES = "Dictionaries"
554
+ window.i18n.CANCEL = "Cancel"
555
+ window.i18n.START = "Start"
556
+ window.i18n.SAVE = "Save"
557
+ window.i18n.WORDS = "Words"
558
+ window.i18n.WORD_IS = "Word:"
559
+ window.i18n.WORD = "Word"
560
+ window.i18n.REFERENCE = "Reference"
561
+ window.i18n.SEARCH_WORDS = "Search words..."
562
+ window.i18n.ENABLE_ALL = "Enable All"
563
+ window.i18n.DISABLE_ALL = "Disable All"
564
+ window.i18n.PREV = "Prev"
565
+ window.i18n.LOADING_DICTIONARIES = "Loading ARPAbet dictionaries..."
566
+
567
+ window.i18n.ALL = "All"
568
+ window.i18n.MOD_NAME = "Mod name"
569
+ window.i18n.MOD_TITLE = "Mod title"
570
+ window.i18n.SEARCH_NEXUS = "Search Nexus"
571
+ window.i18n.MOD_REPOS_USED = "Mod repos used"
572
+ window.i18n.LINK = "Link"
573
+ window.i18n.ENDORSEMENTS = "Endorsements"
574
+ window.i18n.DOWNLOADS = "Downloads"
575
+
576
+ window.i18n.CONFIRM = "Confirm"
577
+ window.i18n.GAME_ID = "Game ID"
578
+ window.i18n.VOICE_ID = "Voice ID"
579
+ window.i18n.VOICE_ID_IS = "Voice ID:"
580
+ window.i18n.APP_VERSION_IS = "App version:"
581
+ window.i18n.MODEL_VERSION_IS = "Model version:"
582
+ window.i18n.MODEL_TYPE_IS = "Model type:"
583
+ window.i18n.LANGUAGE_IS = "Language:"
584
+ window.i18n.TRAINED_BY_IS = "Trained by:"
585
+ window.i18n.LICENSE_IS = "License:"
586
+
587
+ window.i18n.X_WORKSHOP_VOICES_INSTALLED = "_1 workshop voices installed"
588
+ window.i18n.WORKSHOP_GAMES_NOT_RECOGNISED = "The following workshop games were not recognised. Do you have the asset file installed?<i>_1</i>"
589
+
590
+ window.i18n.YOU_MUST_BE_LOGGED_IN = "You must be logged in to check what voices there are available on the nexus."
591
+ window.i18n.JOIN_DISCORD = "Join xVASynth server"
592
+
593
+ window.i18n.GETTING_SPEAKER_EMBEDDING = "Getting speaker embedding..."
594
+
595
+ window.i18n.INFO = "Info"
596
+ window.i18n.VOICE_CRAFTING_WORKBENCH = "Voice Crafting Workbench"
597
+ window.i18n.WORKBENCH = "Workbench"
598
+ window.i18n.FROM_FILE_IS_DRAG_N_DROP = "From file: (Drag and drop a .wav file)"
599
+ window.i18n.FROM_FILE_IS_FILEPATH = "From file: _1"
600
+
601
+
602
+ window.i18n.BATCH_CHANGE_DELIMITER = "The .csv delimiter is not found in the data. The delimiter in the settings is '_1', but the one in the .csv file is potentially '_2'. Do you want to change the delimiter used, and try again using this?"
603
+ window.i18n.BATCH_TOCSV_DONE = "Saved all lines to csv file at:"
604
+ window.i18n.PAGINATION_TOTAL_OF = "of _1"
605
+
606
+
607
+ window.i18n.VC_TOO_SHORT = "Recorded sample is too short and/or empty"
608
+
609
+ window.i18n.MODEL_INSTALL_DRAGDROP_INCOMPLETE = "Some of the loose files given were not complete models. Each model needs at least a .json file and a .pt file. Loose model files not complete:<br><br>_1"
610
+ window.i18n.MODEL_INSTALL_DRAGDROP_SUCCESS = "_1 models installed successfully. "
611
+ window.i18n.MODEL_INSTALL_DRAGDROP_FAILED = "_1 models failed to install:<br><br>_2"
612
+
613
+ // Useful during developing, to see if there are any strings left un-i18n-ed
614
+ // Object.keys(window.i18n).forEach(key => {
615
+ // if (!["setEnglish", "updateUI"].includes(key)) {
616
+ // window.i18n[key] = ""
617
+ // }
618
+ // })
619
+ }
620
+
621
+
622
+ window.i18n.updateUI = () => {
623
+
624
+
625
+
626
+ i18n_voiceInfo_name.innerHTML = window.i18n.VOICE_NAME_IS
627
+ i18n_voiceInfo_id.innerHTML = window.i18n.VOICE_ID_IS
628
+ i18n_voiceInfo_gender.innerHTML = window.i18n.GENDER_IS
629
+ i18n_voiceInfo_appVersion.innerHTML = window.i18n.APP_VERSION_IS
630
+ i18n_voiceInfo_modelVersion.innerHTML = window.i18n.MODEL_VERSION_IS
631
+ i18n_voiceInfo_modelType.innerHTML = window.i18n.MODEL_TYPE_IS
632
+ i18n_voiceInfo_lang.innerHTML = window.i18n.LANGUAGE_IS
633
+ i18n_voiceInfo_author.innerHTML = window.i18n.TRAINED_BY_IS
634
+ i18n_voiceInfo_license.innerHTML = window.i18n.LICENSE_IS
635
+
636
+
637
+ i18n_nexusRepos_mod_name.innerHTML = window.i18n.MOD_NAME
638
+ nexusReposSearchBar.placeholder = window.i18n.MOD_TITLE
639
+ i18n_nexusRepos_all.innerHTML = window.i18n.ALL
640
+ searchNexusButton.innerHTML = window.i18n.SEARCH_NEXUS
641
+ i18n_nexusRepos_game.innerHTML = window.i18n.GAME_IS
642
+ i18n_nexusRepos_modReposUsed.innerHTML = window.i18n.MOD_REPOS_USED
643
+
644
+ i18n_nexus_searchh_add.innerHTML = window.i18n.ADD
645
+ i18n_nexus_searchh_link.innerHTML = window.i18n.LINK
646
+ i18n_nexus_searchh_game.innerHTML = window.i18n.GAME
647
+ i18n_nexus_searchh_name.innerHTML = window.i18n.NAME
648
+ i18n_nexus_searchh_author.innerHTML = window.i18n.AUTHOR
649
+ i18n_nexus_searchh_endorsements.innerHTML = window.i18n.ENDORSEMENTS
650
+ i18n_nexus_searchh_downloads.innerHTML = window.i18n.DOWNLOADS
651
+
652
+ i18n_nexus_reposUsedh_link.innerHTML = window.i18n.LINK
653
+ i18n_nexus_reposUsedh_game.innerHTML = window.i18n.GAME
654
+ i18n_nexus_reposUsedh_name.innerHTML = window.i18n.NAME
655
+ i18n_nexus_reposUsedh_author.innerHTML = window.i18n.AUTHOR
656
+ i18n_nexus_reposUsedh_endorsements.innerHTML = window.i18n.ENDORSEMENTS
657
+ i18n_nexus_reposUsedh_downloads.innerHTML = window.i18n.DOWNLOADS
658
+ i18n_nexus_reposUsedh_remove.innerHTML = window.i18n.REMOVE
659
+
660
+
661
+
662
+ i18n_arpabet_dictionaries.innerHTML = window.i18n.DICTIONARIES
663
+ i18n_arpabet_words.innerHTML = window.i18n.WORDS
664
+ i18n_arpabet_reference.innerHTML = window.i18n.REFERENCE
665
+ arpabet_word_search_input.placeholder = window.i18n.SEARCH_WORDS
666
+ i18n_arpabet_ckbx_only_enabled.placeholder = window.i18n.ONLY_ENABLED
667
+ i18n_arpabet_word_is.innerHTML = window.i18n.WORD_IS
668
+ arpabet_save.innerHTML = window.i18n.SAVE
669
+ i18n_arpabetWordsListh_word.innerHTML = window.i18n.WORD
670
+ i18n_arpabetWordsListh_delete.innerHTML = window.i18n.DELETE
671
+ arpabet_enableall_button.innerHTML = window.i18n.ENABLE_ALL
672
+ arpabet_disableall_button.innerHTML = window.i18n.DISABLE_ALL
673
+ arpabet_prev_btn.innerHTML = window.i18n.PREV
674
+ arpabet_next_btn.innerHTML = window.i18n.NEXT
675
+
676
+
677
+ selectedGameDisplay.innerHTML = window.i18n.SELECT_GAME
678
+ voiceSearchInput.placeholder = window.i18n.SEARCH_VOICES
679
+ titleName.innerHTML = window.i18n.SELECT_VOICE_TYPE
680
+ generateVoiceButton.innerHTML = window.i18n.GENERATE_VOICE
681
+ keepSampleButton.innerHTML = window.i18n.KEEP_SAMPLE
682
+
683
+ i18n_seq_edit_edit.innerHTML = window.i18n.EDIT_IS
684
+ i18n_seq_edit_view.innerHTML = window.i18n.VIEW_IS
685
+ i18n_pitch.innerHTML = window.i18n.PITCH_IS
686
+ i18n_energy.innerHTML = window.i18n.ENERGY_IS
687
+ i18n_duration.innerHTML = window.i18n.DURATION_IS
688
+ i18n_emotion.innerHTML = window.i18n.EMOTION_IS
689
+ i18n_emotion_is.innerHTML = window.i18n.EMOTION_IS
690
+ seq_edit_view_pitch_energy.innerHTML = window.i18n.PITCH_AND_ENERGY
691
+ seq_edit_view_pitch.innerHTML = window.i18n.PITCH
692
+ seq_edit_view_energy.innerHTML = window.i18n.ENERGY
693
+ seq_edit_view_emAngry.innerHTML = window.i18n.EMOTION_IS_ANGRY
694
+ seq_edit_view_emHappy.innerHTML = window.i18n.EMOTION_IS_HAPPY
695
+ seq_edit_view_emSad.innerHTML = window.i18n.EMOTION_IS_SAD
696
+ seq_edit_view_emSurprise.innerHTML = window.i18n.EMOTION_IS_SURPRISE
697
+ seq_edit_edit_pitch.innerHTML = window.i18n.PITCH
698
+ seq_edit_edit_energy.innerHTML = window.i18n.ENERGY
699
+ seq_edit_edit_emotion.innerHTML = window.i18n.EMOTION
700
+
701
+ i18n_vramUsage.innerHTML = window.i18n.VRAM_USAGE
702
+ i18n_length.innerHTML = window.i18n.LENGTH
703
+ resetLetter_btn.innerHTML = window.i18n.RESET_LETTER
704
+ i18n_autoregen.innerHTML = window.i18n.AUTO_REGEN
705
+ i18n_vocoder.innerHTML = window.i18n.VOCODER
706
+ i18n_use_SR.innerHTML = window.i18n.USE_SR_IS
707
+ i18n_batch_useSR.innerHTML = window.i18n.USE_SR
708
+ i18n_use_SR.title = window.i18n.USE_SR_TITLE
709
+ i18n_use_cleanup.innerHTML = window.i18n.USE_CLEANUP_IS
710
+ i18n_batch_useCleanUp.innerHTML = window.i18n.USE_CLEANUP
711
+ i18n_base_lang.innerHTML = window.i18n.BASE_LANGUAGE_IS
712
+ i18n_style_emb_is.innerHTML = window.i18n.STYLE_EMB_IS
713
+ i18n_style.innerHTML = window.i18n.STYLE_EMB_IS
714
+ i18n_base_style_emb_is.innerHTML = window.i18n.BASE_STYLE_EMB_IS
715
+ default_opt_style_emb.innerHTML = window.i18n.DEFAULT
716
+ style_emb_manage_btn.innerHTML = window.i18n.MANAGE
717
+
718
+ batch_paginationPrev.innerHTML = window.i18n.PREVIOUS
719
+ main_paginationPrev.innerHTML = window.i18n.PREVIOUS
720
+ batch_paginationNext.innerHTML = window.i18n.NEXT
721
+ main_paginationNext.innerHTML = window.i18n.NEXT
722
+ i18n_page.innerHTML = window.i18n.PAGE
723
+ i18n_page_main.innerHTML = window.i18n.PAGE
724
+ i18n_batchLPS.innerHTML = window.i18n.LINES_PER_SECOND
725
+ i18n_etaFinished.innerHTML = window.i18n.ETA_FINISHED
726
+ nexusNameDisplay.innerHTML = window.i18n.LOGGED_IN_AS
727
+ i18n_games.innerHTML = window.i18n.GAMES
728
+ nexusGamesListEnableAllBtn.innerHTML = window.i18n.ENABLE_ALL
729
+ nexusGamesListDisableAllBtn.innerHTML = window.i18n.DISABLE_ALL
730
+ i18n_models.innerHTML = window.i18n.MODELS
731
+ i18n_showNewUpdated.innerHTML = window.i18n.SHOW_NEW_UPDATED
732
+ nexusCheckNow.innerHTML = window.i18n.CHECK_NOW
733
+ nexusManageReposButton.innerHTML = window.i18n.MANAGE_REPOS
734
+ nexusLogInButton.innerHTML = window.i18n.LOG_IN
735
+ i18n_nexush_name.innerHTML = window.i18n.NAME
736
+ i18n_nexush_author.innerHTML = window.i18n.AUTHOR
737
+ i18n_nexush_version.innerHTML = window.i18n.VERSION
738
+ i18n_nexush_date.innerHTML = window.i18n.DATE
739
+ i18n_nexush_type.innerHTML = window.i18n.TYPE
740
+ i18n_nexush_notes.innerHTML = window.i18n.NOTES
741
+ i18n_nexusDownloading.innerHTML = window.i18n.DOWNLOADING
742
+ i18n_nexusInstalling.innerHTML = window.i18n.INSTALLING
743
+ i18n_nexusFinished.innerHTML = window.i18n.FINISHED
744
+ nexusDownloadAllBtn.innerHTML = window.i18n.DOWNLOAD_ALL
745
+ i18n_repositories.innerHTML = window.i18n.REPOSITORIES
746
+
747
+
748
+
749
+ i18n_settings_curr_install.innerHTML = window.i18n.CURR_INSTALL
750
+ setting_change_installation.innerHTML = window.i18n.CHANGE_TO_GPU
751
+ i18n_settings_useSound.innerHTML = window.i18n.USE_SOUND_ERR
752
+ i18n_settings_err_soundfile.innerHTML = window.i18n.ERR_SOUNDFILE
753
+ i18n_settings_showTOTD.innerHTML = window.i18n.TOTD_SHOW
754
+ setting_btnShowTOTD.innerHTML = window.i18n.SHOW_NOW
755
+ i18n_settings_unseenTOTD.innerHTML = window.i18n.TOTD_SHOW_UNSEEN
756
+ i18n_settings_playChangedAudio.innerHTML = window.i18n.SETTINGS_PLAYCHANGEDAUDIO
757
+ // i18n_setting_ffmpeg_preapply.innerHTML = window.i18n.SETTINGS_PREAPPLY_FFMPEG
758
+ i18n_setting_useNR.innerHTML = window.i18n.SETTINGS_USE_NR
759
+ i18n_settings_doubleAmpDisplay.innerHTML = window.i18n.SETTINGS_DOUBLE_AMP_DISPLAY
760
+ i18n_settings_csv_delimiter.innerHTML = window.i18n.SETTINGS_CSV_DELIMITER
761
+ i18n_settings_paginationSize.innerHTML = window.i18n.SETTINGS_PAGINATION_SIZE_BATCH
762
+ i18n_settings_arpabetPagination.innerHTML = window.i18n.SETTINGS_PAGINATION_SIZE_ARPABET
763
+ i18n_settings_max_filename_chars.innerHTML = window.i18n.SETTINGS_MAX_FILENAME_LENGTH
764
+ i18n_settings_clear_text_after_synth.innerHTML = window.i18n.SETTINGS_CLEAR_TEXT_AFTER_GENERATION
765
+ i18n_settings_groupVoiceID.innerHTML = window.i18n.SETTINGS_GROUP_VOICEID
766
+ i18n_settings_groupVocoder.innerHTML = window.i18n.SETTINGS_GROUP_VOCODER
767
+
768
+
769
+ voiceSamplesSearch.placeholder = window.i18n.SEARCH_OUTPUT
770
+ voiceSamplesSearchPrompt.placeholder = window.i18n.SEARCH_OUTPUT_PROMPT
771
+ i18n_sortByOutput.innerHTML = window.i18n.SORT_BY
772
+ voiceRecordsOrderByButton.innerHTML = window.i18n.NAME
773
+ voiceRecordsOrderByOrderButton.innerHTML = window.i18n.ASCENDING
774
+ voiceRecordsDeleteAllButton.innerHTML = window.i18n.DELETE_ALL
775
+
776
+ i18n_pluginsh_endorse.innerHTML = window.i18n.ENDORSE
777
+
778
+ i18n_vembVis.innerHTML = window.i18n.V_EMB_VIS
779
+ i18n_games_vemb.innerHTML = window.i18n.GAMES
780
+ i18n_voices.innerHTML = window.i18n.VOICES
781
+ i18n_vembShow.innerHTML = window.i18n.SHOW
782
+ i18n_vembName.innerHTML = window.i18n.NAME
783
+ i18n_vembGame.innerHTML = window.i18n.GAME
784
+ i18n_vembGender.innerHTML = window.i18n.GENDER
785
+ embeddingsSearchBar.placeholder = window.i18n.SEARCH_VOICES
786
+ nexusSearchBar.placeholder = window.i18n.SEARCH_VOICES
787
+ i18n_voiceName.innerHTML = window.i18n.VOICE_NAME_IS
788
+ i18n_genderIs.innerHTML = window.i18n.GENDER_IS
789
+ i18n_vemb_game.innerHTML = window.i18n.GAME_IS
790
+ embeddingsPreviewButton.innerHTML = window.i18n.PREVIEW
791
+ embeddingsLoadButton.innerHTML = window.i18n.LOAD
792
+
793
+ i18n_vemb_instr1.innerHTML = window.i18n.VEMB_INSTR_1
794
+ i18n_vemb_instr2.innerHTML = window.i18n.VEMB_INSTR_2
795
+ i18n_vemb_instr3.innerHTML = window.i18n.VEMB_INSTR_3
796
+ i18n_vemb_instr4.innerHTML = window.i18n.VEMB_INSTR_4
797
+ i18n_vemb_instr5.innerHTML = window.i18n.VEMB_INSTR_5
798
+
799
+ i18n_vemb_males.innerHTML = window.i18n.MALES
800
+ i18n_vemb_females.innerHTML = window.i18n.FEMALES
801
+ i18n_vemb_other.innerHTML = window.i18n.OTHER
802
+
803
+ i18n_showOnlyInstalled.innerHTML = window.i18n.SHOW_ONLY_INSTALED
804
+ i18n_vemb_keyIs.innerHTML = window.i18n.KEY_IS
805
+ i18n_vemb_game_option.innerHTML = window.i18n.GAME
806
+ i18n_vemb_gender_option.innerHTML = window.i18n.GENDER
807
+ i18n_algorithm.innerHTML = window.i18n.ALGORITHM
808
+
809
+ i18n_totd.innerHTML = window.i18n.TOTD
810
+ i18n_totd_show.innerHTML = window.i18n.TOTD_SHOW
811
+ i18n_totd_show_unseen.innerHTML = window.i18n.TOTD_SHOW_UNSEEN
812
+ totdPrevTipBtn.innerHTML = window.i18n.TOTD_PREV_TIP
813
+ totdNextTipBtn.innerHTML = window.i18n.TOTD_NEXT_TIP
814
+ totd_close.innerHTML = window.i18n.CLOSE
815
+ embeddingsCloseHelpUI.innerHTML = window.i18n.CLOSE
816
+ nexusMenuButton.innerHTML = window.i18n.GET_MORE_VOICES
817
+
818
+ i18n_embeddings.innerHTML = window.i18n.STYLE_EMBEDDINGS
819
+ i18n_style_emb_wavpath.innerHTML = window.i18n.STYLE_EMB_WAVPATH
820
+ i18n_style_emb_author.innerHTML = window.i18n.AUTHOR
821
+ i18n_style_emb_gameId.innerHTML = window.i18n.GAME_ID
822
+ i18n_style_emb_voiceId.innerHTML = window.i18n.VOICE_ID
823
+ i18n_style_emb_name.innerHTML = window.i18n.EMB_NAME
824
+ i18n_style_emb_description.innerHTML = window.i18n.EMB_DESCRIPTION
825
+ i18n_style_emb_id.innerHTML = window.i18n.STYLE_EMB_ID
826
+ wavFilepathForEmbComputeInput.placeholder = window.i18n.STYLE_EMB_WAVPATH_PLACEHOLDER
827
+ i18n_style_emb_values.innerHTML = window.i18n.STYLE_EMB_VALUES
828
+ i18n_styleembsh_enabled.innerHTML = window.i18n.ENABLED
829
+ i18n_styleembsh_name.innerHTML = window.i18n.EMB_NAME
830
+ i18n_styleembsh_gameID.innerHTML = window.i18n.GAME_ID
831
+ i18n_styleembsh_voiceID.innerHTML = window.i18n.VOICE_ID
832
+ // i18n_styleembsh_actions.innerHTML = window.i18n.ACTIONS
833
+ i18n_styleembsh_description.innerHTML = window.i18n.DESCRIPTION
834
+ i18n_styleembsh_embId.innerHTML = window.i18n.EMB_ID
835
+ i18n_styleembsh_version.innerHTML = window.i18n.VERSION
836
+ // i18n_styleembsh_endorse.innerHTML = window.i18n.ENDORSE
837
+ styleEmbSave.innerHTML = window.i18n.SAVE
838
+ styleEmbDelete.innerHTML = window.i18n.DELETE
839
+
840
+
841
+ reset_btn.innerHTML = window.i18n.RESET
842
+ amplify_btn.innerHTML = window.i18n.AMPLIFY
843
+ jitter_btn.innerHTML = window.i18n.JITTER
844
+ flatten_btn.innerHTML = window.i18n.FLATTEN
845
+ increase_btn.innerHTML = window.i18n.RAISE
846
+ decrease_btn.innerHTML = window.i18n.LOWER
847
+ i18n_pacing.innerHTML = window.i18n.PACING
848
+
849
+ i18n_settings.innerHTML = window.i18n.SETTINGS
850
+ i18n_setting_gpu.innerHTML = window.i18n.SETTINGS_GPU
851
+ i18n_setting_autoplay.innerHTML = window.i18n.SETTINGS_AUTOPLAY
852
+ i18n_setting_defaulthifi.innerHTML = window.i18n.SETTINGS_DEFAULT_HIFI
853
+ i18n_setting_keeppacing.innerHTML = window.i18n.SETTINGS_KEEP_PACING
854
+ // i18n_setting_tooltip.innerHTML = window.i18n.SETTINGS_TOOLTIP
855
+
856
+ i18n_showDiscordStatus.innerHTML = window.i18n.SETTINGS_SHOW_DISCORD
857
+ // i18n_setting_darkmode.innerHTML = window.i18n.SETTINGS_DARKMODE
858
+ i18n_setting_promptfontsize.innerHTML = window.i18n.SETTINGS_PROMPTSIZE
859
+ i18n_setting_bg_fade.innerHTML = window.i18n.SETTINGS_BG_FADE
860
+ i18n_setting_autoreloadvoices.innerHTML = window.i18n.SETTINGS_AUTORELOADVOICES
861
+ i18n_setting_keepeditorstate.innerHTML = window.i18n.SETTINGS_KEEPEDITORSTATE
862
+ i18n_setting_pitchrangeoverride.innerHTML = window.i18n.SETTINGS_PITCHRANGEOVERRIDE
863
+ i18n_setting_outputjson.innerHTML = window.i18n.SETTINGS_OUTPUTJSON
864
+ i18n_setting_seqnumbering.innerHTML = window.i18n.SETTINGS_SEQNUMBERING
865
+ i18n_setting_spacepadding.innerHTML = window.i18n.SETTINGS_SPACEPADDING
866
+ i18n_setting_base_speaker.innerHTML = window.i18n.SETTINGS_BASE_SPEAKER
867
+ i18n_setting_alt_speaker.innerHTML = window.i18n.SETTINGS_ALT_SPEAKER
868
+ i18n_setting_external_edit.innerHTML = window.i18n.SETTINGS_EXTERNALEDIT
869
+ i18n_setting_ffmpeg.innerHTML = window.i18n.SETTINGS_FFMPEG
870
+ i18n_setting_ffmpeg_format.innerHTML = window.i18n.SETTINGS_FFMPEG_FORMAT
871
+ i18n_setting_ffmpeg_hz.innerHTML = window.i18n.SETTINGS_FFMPEG_HZ
872
+ i18n_setting_ffmpeg_padstart.innerHTML = window.i18n.SETTINGS_FFMPEG_PADSTART
873
+ i18n_setting_ffmpeg_padend.innerHTML = window.i18n.SETTINGS_FFMPEG_PADEND
874
+ i18n_setting_ffmpeg_pitchMult.innerHTML = window.i18n.SETTINGS_FFMPEG_PITCHMULT
875
+ i18n_setting_ffmpeg_tempo.innerHTML = window.i18n.SETTINGS_FFMPEG_TEMPO
876
+ i18n_setting_ffmpeg_deessing.innerHTML = window.i18n.SETTINGS_FFMPEG_DEESSING
877
+ i18n_setting_ffmpeg_bitdepth.innerHTML = window.i18n.SETTINGS_FFMPEG_BITDEPTH
878
+ i18n_setting_ffmpeg_nr.innerHTML = window.i18n.SETTINGS_FFMPEG_NR
879
+ i18n_setting_ffmpeg_nf.innerHTML = window.i18n.SETTINGS_FFMPEG_NF
880
+ i18n_setting_ffmpeg_amplitude.innerHTML = window.i18n.SETTINGS_FFMPEG_AMPLITUDE
881
+ i18n_setting_batch_json.innerHTML = window.i18n.SETTINGS_BATCH_JSON
882
+ // i18n_setting_batch_fastmode.innerHTML = window.i18n.SETTINGS_BATCH_FASTMODE
883
+ // i18n_settings_batch_mp_max_parallelizations.innerHTML = window.i18n.SETTINGS_BATCH_FASTMODE_MAX_PARALLELIZATIONS
884
+ i18n_setting_batch_multip.innerHTML = window.i18n.SETTINGS_BATCH_USEMULTIP
885
+ i18n_setting_batch_multip_count.innerHTML = window.i18n.SETTINGS_BATCH_MULTIPCOUNT
886
+ i18n_setting_microphone.innerHTML = window.i18n.SETTINGS_MICROPHONE
887
+ // i18n_setting_autogeneratevoice.innerHTML = window.i18n.SETTINGS_AUTOGENERATEVOICE
888
+ i18n_setting_s2s_bgnoise.innerHTML = window.i18n.SETTINGS_S2S_BGNOISE
889
+ s2s_settingsRecNoiseBtn.innerHTML = window.i18n.SETTINGS_S2S_RECNOISE
890
+ // i18n_setting_s2s_bgnoise_strength.innerHTML = window.i18n.SETTINGS_S2S_BGNOISE_STRENGTH
891
+ i18n_vc_strength.innerHTML = window.i18n.SETTINGS_VC_STRENGTH
892
+ reset_settings_btn.innerHTML = window.i18n.SETTINGS_RESET_SETTINGS
893
+ reset_paths_btn.innerHTML = window.i18n.SETTINGS_RESET_PATHS
894
+
895
+ updatesVersions.innerHTML = window.i18n.UPDATES_VERSION
896
+ i18n_updateslog.innerHTML = window.i18n.UPDATES_LOG
897
+ checkUpdates.innerHTML = window.i18n.UPDATES_CHECK
898
+
899
+ i18n_plugins.innerHTML = window.i18n.PLUGINS
900
+ i18n_plugins_trusted.innerHTML = window.i18n.PLUGINS_TRUSTED
901
+
902
+ i18n_pluginsh_enabled.innerHTML = window.i18n.PLUGINSH_ENABLED
903
+ i18n_pluginsh_order.innerHTML = window.i18n.PLUGINSH_ORDER
904
+ i18n_pluginsh_name.innerHTML = window.i18n.PLUGINSH_NAME
905
+ i18n_pluginsh_author.innerHTML = window.i18n.PLUGINSH_AUTHOR
906
+ i18n_pluginsh_version.innerHTML = window.i18n.PLUGINSH_VERSION
907
+ i18n_pluginsh_type.innerHTML = window.i18n.PLUGINSH_TYPE
908
+ i18n_pluginsh_minv.innerHTML = window.i18n.PLUGINSH_MINV
909
+ i18n_pluginsh_maxv.innerHTML = window.i18n.PLUGINSH_MAXV
910
+ i18n_pluginsh_description.innerHTML = window.i18n.PLUGINSH_DESCRIPTION
911
+ i18n_pluginsh_pluginid.innerHTML = window.i18n.PLUGINSH_PLUGINID
912
+ plugins_moveUpBtn.innerHTML = window.i18n.PLUGINS_MOVEUP
913
+ plugins_moveDownBtn.innerHTML = window.i18n.PLUGINS_MOVEDOWN
914
+ plugins_applyBtn.innerHTML = window.i18n.PLUGINS_APPLY
915
+
916
+ i18n_appinfo.innerHTML = window.i18n.APP_INFO
917
+ i18n_appinfo_instr_1.innerHTML = window.i18n.APP_INFO_INSTR_1
918
+ i18n_appinfo_instr_2.innerHTML = window.i18n.APP_INFO_INSTR_2
919
+ i18n_appinfo_instr_3.innerHTML = window.i18n.APP_INFO_INSTR_3
920
+ i18n_appinfo_instr_4.innerHTML = window.i18n.APP_INFO_INSTR_4
921
+ i18n_appinfo_instr_5.innerHTML = window.i18n.APP_INFO_INSTR_5
922
+
923
+ i18n_keyboard_reference.innerHTML = window.i18n.KEYBOARD_REFERENCE
924
+ i18n_keyboard_enter.innerHTML = window.i18n.KEYBOARD_ENTER
925
+ i18n_keyboard_enter_do.innerHTML = window.i18n.KEYBOARD_ENTER_DO
926
+ i18n_keyboard_escape.innerHTML = window.i18n.KEYBOARD_ESCAPE
927
+ i18n_keyboard_escape_do.innerHTML = window.i18n.KEYBOARD_ESCAPE_DO
928
+ i18n_keyboard_space.innerHTML = window.i18n.KEYBOARD_SPACE
929
+ i18n_keyboard_space_do.innerHTML = window.i18n.KEYBOARD_SPACE_DO
930
+ i18n_keyboard_ctrls.innerHTML = window.i18n.KEYBOARD_CTRLS
931
+ i18n_keyboard_ctrls_do.innerHTML = window.i18n.KEYBOARD_CTRLS_DO
932
+ i18n_keyboard_ctrlshifts.innerHTML = window.i18n.KEYBOARD_CTRLSHIFTS
933
+ i18n_keyboard_ctrlshifts_do.innerHTML = window.i18n.KEYBOARD_CTRLSHIFTS_DO
934
+ i18n_keyboard_yn.innerHTML = window.i18n.KEYBOARD_YN
935
+ i18n_keyboard_yn_do.innerHTML = window.i18n.KEYBOARD_YN_DO
936
+ i18n_keyboard_lr.innerHTML = window.i18n.KEYBOARD_LR
937
+ i18n_keyboard_lr_do.innerHTML = window.i18n.KEYBOARD_LR_DO
938
+ i18n_keyboard_shift_lr.innerHTML = window.i18n.KEYBOARD_SHIFT_LR
939
+ i18n_keyboard_shift_lr_do.innerHTML = window.i18n.KEYBOARD_SHIFT_LR_DO
940
+
941
+ i18n_keyboard_alt_ctrl_lr.innerHTML = window.i18n.KEYBOARD_ALT_CTRL_LR
942
+ i18n_keyboard_alt_ctrl_lr_do.innerHTML = window.i18n.KEYBOARD_ALT_CTRL_LR_DO
943
+ i18n_keyboard_ctrla.innerHTML = window.i18n.KEYBOARD_CTRLA
944
+ i18n_keyboard_ctrla_do.innerHTML = window.i18n.KEYBOARD_CTRLA_DO
945
+
946
+ i18n_keyboard_ud.innerHTML = window.i18n.KEYBOARD_UD
947
+ i18n_keyboard_ud_do.innerHTML = window.i18n.KEYBOARD_UD_DO
948
+ i18n_keyboard_ctrl_lr.innerHTML = window.i18n.KEYBOARD_CTRL_LR
949
+ i18n_keyboard_ctrl_lr_do.innerHTML = window.i18n.KEYBOARD_CTRL_LR_DO
950
+ i18n_keyboard_ctrl_ud.innerHTML = window.i18n.KEYBOARD_CTRL_UD
951
+ i18n_keyboard_ctrl_ud_do.innerHTML = window.i18n.KEYBOARD_CTRL_UD_DO
952
+ i18n_keyboard_ctrlshiftud.innerHTML = window.i18n.KEYBOARD_CTRLSHIFTUD
953
+ i18n_keyboard_ctrlshiftud_do.innerHTML = window.i18n.KEYBOARD_CTRLSHIFTUD_DO
954
+ i18n_keyboard_ctrlenter.innerHTML = window.i18n.KEYBOARD_CTRLENTER
955
+ i18n_keyboard_ctrlenter_do.innerHTML = window.i18n.KEYBOARD_CTRLENTER_DO
956
+
957
+ i18n_support.innerHTML = window.i18n.SUPPORT
958
+ // i18n_support_link.innerHTML = window.i18n.SUPPORT_LINK
959
+ i18n_support_thanks.innerHTML = window.i18n.SUPPORT_THANKS
960
+
961
+ searchGameInput.placeholder = window.i18n.SEARCH_GAMES
962
+ searchSettingsInput.placeholder = window.i18n.SEARCH_SETTINGS
963
+
964
+ i18n_eula_accept.innerHTML = window.i18n.EULA_ACCEPT
965
+ EULA_closeButon.innerHTML = window.i18n.EULA_CLOSE
966
+
967
+ i18n_batch_synthesis.innerHTML = window.i18n.BATCH_SYNTHESIS
968
+ i18n_batchsize.innerHTML = window.i18n.BATCH_SIZE
969
+ batch_generateSample.innerHTML = window.i18n.BATCH_GEN_SAMPLE
970
+ batch_instructions_btn.innerHTML = window.i18n.BATCH_INSTRUCTIONS
971
+ batchDropZoneNote.innerHTML = window.i18n.BATCH_DROPZONE
972
+
973
+ i18n_batchh_num.innerHTML = window.i18n.BATCHH_NUM
974
+ i18n_batchh_status.innerHTML = window.i18n.BATCHH_STATUS
975
+ i18n_batchh_actions.innerHTML = window.i18n.BATCHH_ACTIONS
976
+ i18n_nexush_actions.innerHTML = window.i18n.BATCHH_ACTIONS
977
+ i18n_batchh_game.innerHTML = window.i18n.BATCHH_GAME
978
+ i18n_nexush_game.innerHTML = window.i18n.BATCHH_GAME
979
+ i18n_batchh_voice.innerHTML = window.i18n.BATCHH_VOICE
980
+ i18n_batchh_text.innerHTML = window.i18n.BATCHH_TEXT
981
+ i18n_batchh_vocoder.innerHTML = window.i18n.BATCHH_VOCODER
982
+ i18n_batchh_vc_style.innerHTML = window.i18n.BATCHH_VC_STYLE
983
+ i18n_batchh_outpath.innerHTML = window.i18n.BATCHH_OUTPATH
984
+ i18n_batchh_pacing.innerHTML = window.i18n.BATCHH_PACING
985
+ i18n_batchh_pitch_amp.innerHTML = window.i18n.BATCHH_PITCH_AMP
986
+ i18n_batchh_base_lang.innerHTML = window.i18n.BATCHH_BASE_LANG
987
+ batch_outputFolderInput.placeholder = window.i18n.BATCH_ABS_DIR_PLACEHOLDER
988
+
989
+ i18n_batch_cleardir.innerHTML = window.i18n.BATCH_CLEAR_DIR
990
+ i18n_batch_skip.innerHTML = window.i18n.BATCH_SKIP
991
+ i18n_batch_outputNumerically.innerHTML = window.i18n.BATCH_OUTPUTNUMERICALLY
992
+ batch_progressNotes.innerHTML = window.i18n.BATCH_CURRENTLYDOING
993
+ batch_synthesizeBtn.innerHTML = window.i18n.BATCH_SYNTHESIZE
994
+ batch_pauseBtn.innerHTML = window.i18n.BATCH_PAUSE
995
+ batch_stopBtn.innerHTML = window.i18n.BATCH_STOP
996
+ batch_clearBtn.innerHTML = window.i18n.BATCH_CLEAR
997
+ batch_openDirBtn.innerHTML = window.i18n.BATCH_OPENOUT
998
+
999
+ s2s_voiceId_selected_label.innerHTML = window.i18n.VC_ONLY_FOR_V3
1000
+ i18n_settings_model_version_highlight.innerHTML = window.i18n.SETTING_HIGHLIGHT_ONLY_MODELS_V
1001
+ i18n_setting_output_files_pagination_size.innerHTML = window.i18n.SETTING_OUTPUTFILES_PAGINATION
1002
+
1003
+ openDiscord.innerHTML = window.i18n.JOIN_DISCORD
1004
+
1005
+ i18n_workbench.innerHTML = window.i18n.VOICE_CRAFTING_WORKBENCH
1006
+ voiceWorkbenchRefAFilePath.innerHTML = window.i18n.FROM_FILE_IS_DRAG_N_DROP
1007
+ voiceWorkbenchRefBFilePath.innerHTML = window.i18n.FROM_FILE_IS_DRAG_N_DROP
1008
+
1009
+ voiceWorkbenchInputTextArea.innerHTML = window.i18n.VW_INPUT_TEXTAREA_PLACEHOLDER
1010
+
1011
+ voiceWorkbenchGenerateSampleButton.innerHTML = window.i18n.GENERATE
1012
+ i18n_vw_current.innerHTML = window.i18n.CURRENT
1013
+ voiceWorkbenchAudioCurrentPlayPauseBtn.innerHTML = window.i18n.PLAY
1014
+ voiceWorkbenchAudioCurrentSaveBtn.innerHTML = window.i18n.SAVE
1015
+ voiceWorkbenchAudioNewPlayBtn.innerHTML = window.i18n.PLAY
1016
+ voiceWorkbenchAudioNewSaveBtn.innerHTML = window.i18n.SAVE
1017
+
1018
+ i18n_vw_current_emb.innerHTML = window.i18n.CURRENT_EMB
1019
+ i18n_vw_current_delta.innerHTML = window.i18n.CURRENT_DELTA
1020
+ i18n_vw_strength.innerHTML = window.i18n.STRENGTH
1021
+ voiceWorkshopApplyDeltaButton.innerHTML = window.i18n.APPLY_DELTA
1022
+ i18n_refAF_a.innerHTML = window.i18n.VW_REF_FILE_A
1023
+ i18n_refAF_b.innerHTML = window.i18n.VW_REF_FILE_B
1024
+
1025
+ i18n_vw_basemodel.innerHTML = window.i18n.VW_BASE_MODEL
1026
+ i18n_vw_game.innerHTML = window.i18n.GAME
1027
+ i18n_vw_voicename.innerHTML = window.i18n.VOICE_NAME
1028
+ i18n_vw_voiceid.innerHTML = window.i18n.VOICE_ID
1029
+ voiceWorkbenchVoiceNameInput.placeholder = window.i18n.NAME_OF_YOUR_VOICE
1030
+ voiceWorkbenchVoiceIDInput.placeholder = window.i18n.UNIQUE_ID_FOR_VOICE
1031
+ i18n_vw_gender.innerHTML = window.i18n.GENDER
1032
+ i18n_vw_author.innerHTML = window.i18n.AUTHOR
1033
+
1034
+ voiceWorkbenchAuthorInput.placeholder = window.i18n.YOUR_NAME_FOR_CREDITS
1035
+
1036
+ i18n_vw_baselang.innerHTML = window.i18n.BASE_LANGUAGE
1037
+ voiceWorkbenchStartButton.innerHTML = window.i18n.START
1038
+ voiceWorkbenchCancelButton.innerHTML = window.i18n.CANCEL
1039
+ voiceWorkbenchDeleteButton.innerHTML = window.i18n.DELETE
1040
+ voiceWorkbenchSaveButton.innerHTML = window.i18n.SAVE
1041
+
1042
+
1043
+ splashNextButton1.innerHTML = window.i18n.NEXT
1044
+
1045
+ i18n_variant.innerHTML = window.i18n.VARIANT_IS
1046
+ i18n_reset_what_prompt.innerHTML = window.i18n.RESET_WHAT_PROMPT
1047
+ i18n_reset_what_tip.innerHTML = window.i18n.RESET_WHAT_TIP
1048
+ reset_what_confirm_btn.innerHTML = window.i18n.RESET
1049
+ i18n_batch_metadata_confirm.innerHTML = window.i18n.BATCH_METADATA_CONFIRM
1050
+ i18n_batch_metadata_tip.innerHTML = window.i18n.BATCH_METADATA_TIP
1051
+ i18n_batch_metadata_voiceID.innerHTML = window.i18n.VOICE_ID
1052
+ i18n_batch_metadata_gameID.innerHTML = window.i18n.GAME_ID
1053
+ i18n_batch_metadata_confirm_btn.innerHTML = window.i18n.CONFIRM
1054
+ batch_saveToCSV.innerHTML = window.i18n.SAVE_TO_CSV
1055
+
1056
+ arpabetIcon.title = "ARPAbet"
1057
+ embeddingsIcon.title = "Embeddings visualiser"
1058
+ pluginsIcon.title = window.i18n.PLUGINS
1059
+ batchIcon.title = "Batch mode"
1060
+ updatesIcon.title = "Changelog"
1061
+ patreonIcon.title = "Patreon"
1062
+ infoIcon.title = window.i18n.INFO
1063
+ settingsCog.title = "Settings"
1064
+ workbenchIcon.title = window.i18n.WORKBENCH
1065
+
1066
+ }
1067
+
1068
+
1069
+ window.i18n.setEnglish()
1070
+ window.i18n.updateUI()
javascript/nexus.js ADDED
@@ -0,0 +1,983 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ window.nexusModelsList = []
4
+ window.endorsedRepos = new Set()
5
+ window.nexusState = {
6
+ key: null,
7
+ applicationSlug: "xvasynth",
8
+ socket: null,
9
+ uuid: null,
10
+ token: null,
11
+ downloadQueue: [],
12
+ installQueue: [],
13
+ finished: 0,
14
+ repoLinks: [],
15
+ primaryColumnSort: "game",
16
+ columnSortModifier: -1
17
+ }
18
+
19
+ // TEMP, maybe move to utils
20
+ // ==========================
21
+ const http = require("http")
22
+ const https = require("https")
23
+
24
+ // Utility for printing out in the dev console all the numerical game IDs on the Nexus
25
+ window.nexusGameIdToGameName = {}
26
+ window.getAllNexusGameIDs = (gameName) => {
27
+ return new Promise((resolve) => {
28
+ getData("", undefined, "GET").then(results => {
29
+ results = gameName ? results.filter(x=>x.name.toLowerCase().includes(gameName)) : results
30
+ resolve(results)
31
+ })
32
+ })
33
+ }
34
+
35
+ window.mod_search_nexus = (game_id, query) => {
36
+ return new Promise(resolve => {
37
+ doFetch(`https://search.nexusmods.com/mods/?game_id=${game_id}&terms=${encodeURI(query.split(' ').toString())}&include_adult=true`)
38
+ .then(r=>r.text())
39
+ .then(r => {
40
+ try {
41
+ const data = JSON.parse(r).results.map(res => {
42
+ return {
43
+ downloads: res.downloads,
44
+ endorsements: res.endorsements,
45
+ game_id: res.game_id,
46
+ name: res.name,
47
+ author: res.username,
48
+ url: `https://www.nexusmods.com/${res.game_name}/mods/${res.mod_id}`
49
+ }
50
+ })
51
+
52
+ resolve([r.total, data])
53
+ } catch (e) {
54
+ window.appLogger.log(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
55
+ window.errorModal(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
56
+ }
57
+ })
58
+ })
59
+ }
60
+
61
+ window.nexusDownload = (url, dest) => {
62
+ return new Promise((resolve, reject) => {
63
+ const file = fs.createWriteStream(dest)
64
+
65
+ const request = https.get(url.replace("http:", "https:"), (response) => {
66
+ // check if response is success
67
+ if (response.statusCode !== 200) {
68
+ console.log("url", url)
69
+ console.log("Response status was " + response.statusCode, response)
70
+ resolve()
71
+ return
72
+ }
73
+
74
+ response.pipe(file)
75
+ })
76
+
77
+ file.on("finish", () => {
78
+ file.close()
79
+ resolve()
80
+ })
81
+
82
+ // check for request error too
83
+ request.on("error", (err) => {
84
+ fs.unlink(dest)
85
+ return reject(err.message)
86
+ })
87
+
88
+ file.on("error", (err) => { // Handle errors
89
+ fs.unlink(dest) // Delete the file async. (But we don't check the result)
90
+ return reject(err.message)
91
+ })
92
+ })
93
+ }
94
+
95
+ window.initNexus = () => {
96
+ const data = fs.readFileSync(`${window.path}/repositories.json`, "utf8")
97
+ window.nexusReposList = JSON.parse(data)
98
+ return new Promise((resolve) => {
99
+
100
+ window.nexusState.key = localStorage.getItem("nexus_API_key")
101
+
102
+ if (window.nexusState.key) {
103
+ window.showUserName()
104
+ nexusLogInButton.innerHTML = window.i18n.LOG_OUT
105
+ resolve()
106
+ } else {
107
+ try {
108
+ window.nexusState.socket = new WebSocket("wss://sso.nexusmods.com")
109
+ } catch (e) {
110
+ console.log(e)
111
+ }
112
+
113
+ window.nexusState.socket.onclose = event => {
114
+ console.log("socket closed")
115
+ if (!window.nexusState.key) {
116
+ setTimeout(() => {window.initNexus()}, 5000)
117
+ }
118
+ }
119
+
120
+ window.nexusState.socket.onmessage = event => {
121
+ const response = JSON.parse(event.data)
122
+
123
+ if (response && response.success) {
124
+ if (response.data.hasOwnProperty('connection_token')) {
125
+ localStorage.setItem('connection_token', response.data.connection_token)
126
+ } else if (response.data.hasOwnProperty('api_key')) {
127
+ console.log("API Key Received: " + response.data.api_key)
128
+ window.nexusState.key = response.data.api_key
129
+ localStorage.setItem('uuid', window.nexusState.uuid)
130
+ localStorage.setItem('nexus_API_key', window.nexusState.key)
131
+ window.showUserName()
132
+ window.pluginsManager.updateUI()
133
+ closeModal(undefined, nexusContainer)
134
+ nexusLogInButton.innerHTML = window.i18n.LOG_OUT
135
+ resolve()
136
+ }
137
+ } else {
138
+ window.errorModal(`${window.i18n.ERR_LOGGING_INTO_NEXUS}: ${response.error}`)
139
+ reject()
140
+ }
141
+ }
142
+ }
143
+ })
144
+ }
145
+ nexusLogInButton.addEventListener("click", () => {
146
+
147
+ if (nexusLogInButton.innerHTML==window.i18n.LOG_IN) {
148
+ window.spinnerModal(window.i18n.LOGGING_INTO_NEXUS)
149
+
150
+ window.nexusState.uuid = localStorage.getItem("uuid")
151
+ window.nexusState.token = localStorage.getItem("connection_token")
152
+
153
+ if (window.nexusState.uuid==null) {
154
+ window.nexusState.uuid = uuidv4()
155
+ }
156
+
157
+ const data = {
158
+ id: window.nexusState.uuid,
159
+ token: window.nexusState.token,
160
+ protocol: 2
161
+ }
162
+ window.nexusState.socket.send(JSON.stringify(data))
163
+
164
+ shell.openExternal(`https://www.nexusmods.com/sso?id=${window.nexusState.uuid}&application=${window.nexusState.applicationSlug}`)
165
+
166
+ } else {
167
+ nexusNameDisplayContainer.style.opacity = 0
168
+ localStorage.removeItem("nexus_API_key")
169
+ localStorage.removeItem("uuid")
170
+ localStorage.removeItem("connection_token")
171
+ nexusAvatar.innerHTML = ""
172
+ nexusUserName.innerHTML = ""
173
+ nexusLogInButton.innerHTML = window.i18n.LOG_IN
174
+ window.nexusState.uuid = null
175
+ window.nexusState.key = null
176
+ window.pluginsManager.updateUI()
177
+ }
178
+ })
179
+
180
+
181
+
182
+ window.downloadFile = ([nexusGameId, nexusRepoId, outputFileName, fileId]) => {
183
+ nexusDownloadLog.appendChild(createElem("div", `Downloading: ${outputFileName}`))
184
+ return new Promise(async (resolve, reject) => {
185
+ if (!fs.existsSync(`${window.path}/downloads`)) {
186
+ fs.mkdirSync(`${window.path}/downloads`)
187
+ }
188
+
189
+ const downloadLink = await getData(`${nexusGameId}/mods/${nexusRepoId}/files/${fileId}/download_link.json`)
190
+ if (!downloadLink.length && downloadLink.code==403) {
191
+
192
+ window.errorModal(`${window.i18n.NEXUS_PREMIUM}<br><br>${window.i18n.NEXUS_ORIG_ERR}:<br>${downloadLink.message}`).then(() => {
193
+ const queueIndex = window.nexusState.downloadQueue.findIndex(it => it[1]==fileId)
194
+ window.nexusState.downloadQueue.splice(queueIndex, 1)
195
+ nexusDownloadingCount.innerHTML = window.nexusState.downloadQueue.length
196
+
197
+ nexusDownloadLog.appendChild(createElem("div", `${window.i18n.FAILED_DOWNLOAD}: ${outputFileName}`))
198
+
199
+ reject()
200
+ })
201
+
202
+ } else {
203
+ await window.nexusDownload(downloadLink[0].URI.replace("https", "http"), `${window.path}/downloads/${outputFileName}.zip`)
204
+
205
+ const queueIndex = window.nexusState.downloadQueue.findIndex(it => it[1]==fileId)
206
+ window.nexusState.downloadQueue.splice(queueIndex, 1)
207
+ nexusDownloadingCount.innerHTML = window.nexusState.downloadQueue.length
208
+
209
+ resolve()
210
+ }
211
+ })
212
+
213
+ }
214
+ window.installDownloadedModel = ([game, zipName]) => {
215
+ nexusDownloadLog.appendChild(createElem("div", `${window.i18n.INSTALLING} ${zipName}`))
216
+ return new Promise(resolve => {
217
+ try {
218
+ const modelsFolder = window.userSettings[`modelspath_${game}`]
219
+
220
+ const unzipper = require('unzipper')
221
+ const zipPath = `${window.path}/downloads/${zipName}.zip`
222
+
223
+ if (!fs.existsSync(modelsFolder)) {
224
+ fs.mkdirSync(modelsFolder)
225
+ }
226
+
227
+ if (!fs.existsSync(`${window.path}/downloads`)) {
228
+ fs.mkdirSync(`${window.path}/downloads`)
229
+ }
230
+
231
+ fs.createReadStream(zipPath).pipe(unzipper.Parse()).on("entry", entry => {
232
+ const fileName = entry.path
233
+ const dirOrFile = entry.type
234
+
235
+ if (/\/$/.test(fileName)) { // It's a directory
236
+ return
237
+ }
238
+
239
+ let fileContainerFolderPath = fileName.split("/").reverse()
240
+ const justFileName = fileContainerFolderPath[0]
241
+
242
+ entry.pipe(fs.createWriteStream(`${modelsFolder}/${justFileName}`))
243
+ })
244
+ .promise()
245
+ .then(() => {
246
+ window.appLogger.log(`${window.i18n.DONE_INSTALLING} ${zipName}`)
247
+
248
+ const queueIndex = window.nexusState.installQueue.findIndex(it => it[1]==zipName)
249
+ window.nexusState.installQueue.splice(queueIndex, 1)
250
+ nexusInstallingCount.innerHTML = window.nexusState.installQueue.length
251
+
252
+ nexusDownloadLog.appendChild(createElem("div", `${window.i18n.FINISHED} ${zipName}`))
253
+ resolve()
254
+ }, e => {
255
+ console.log(e)
256
+ window.appLogger.log(e)
257
+ window.errorModal(e.message)
258
+ })
259
+ } catch (e) {
260
+ console.log(e)
261
+ window.appLogger.log(e)
262
+ window.errorModal(e.message)
263
+ resolve()
264
+ }
265
+ })
266
+ }
267
+
268
+ nexusDownloadAllBtn.addEventListener("click", async () => {
269
+
270
+ for (let mi=0; mi<window.nexusState.filteredDownloadableModels.length; mi++) {
271
+ const modelMeta = window.nexusState.filteredDownloadableModels[mi]
272
+ window.nexusState.downloadQueue.push([modelMeta.voiceId, modelMeta.nexus_file_id])
273
+ nexusDownloadingCount.innerHTML = window.nexusState.downloadQueue.length
274
+ }
275
+
276
+ for (let mi=0; mi<window.nexusState.filteredDownloadableModels.length; mi++) {
277
+
278
+ const modelMeta = window.nexusState.filteredDownloadableModels[mi]
279
+ await window.downloadFile([modelMeta.nexusGameId, modelMeta.nexusRepoId, modelMeta.voiceId, modelMeta.nexus_file_id])
280
+
281
+ // Install the downloaded voice
282
+ window.nexusState.installQueue.push([modelMeta.game, modelMeta.voiceId])
283
+ nexusInstallingCount.innerHTML = window.nexusState.installQueue.length
284
+ await window.installDownloadedModel([modelMeta.game, modelMeta.voiceId])
285
+
286
+ fs.unlinkSync(`${window.path}/downloads/${modelMeta.voiceId}.zip`)
287
+
288
+ window.nexusState.finished += 1
289
+ nexusFinishedCount.innerHTML = window.nexusState.finished
290
+ window.displayAllModels(true)
291
+ window.loadAllModels(true).then(() => {
292
+ changeGame(window.currentGame)
293
+ })
294
+ }
295
+ })
296
+
297
+
298
+ const getJSONData = (url) => {
299
+ return new Promise(resolve => {
300
+ doFetch(url).then(r=>r.json())
301
+ })
302
+ }
303
+
304
+ window.showUserName = async () => {
305
+ const data = await getuserData("validate.json")
306
+ const img = createElem("img")
307
+ img.src = data.profile_url
308
+ img.style.height = "40px"
309
+ nexusAvatar.innerHTML = ""
310
+ img.addEventListener("load", () => {
311
+ nexusAvatar.appendChild(img)
312
+ nexusUserName.innerHTML = data.name
313
+ nexusNameDisplayContainer.style.opacity = 1
314
+ })
315
+ }
316
+
317
+ const getuserData = (url, data) => {
318
+ return new Promise(resolve => {
319
+ doFetch(`https://api.nexusmods.com/v1/users/${url}`, {
320
+ method: "GET",
321
+ headers: {
322
+ apikey: window.nexusState.key
323
+ }
324
+ })
325
+ .then(r=>r.text())
326
+ .then(r => {
327
+ try {
328
+ resolve(JSON.parse(r))
329
+ } catch (e) {
330
+ window.appLogger.log(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
331
+ window.errorModal(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
332
+ }
333
+ })
334
+ .catch(err => {
335
+ console.log("err", err)
336
+ resolve()
337
+ })
338
+ })
339
+ }
340
+ const getData = (url, data, type="GET") => {
341
+ return new Promise(resolve => {
342
+ const payload = {
343
+ method: type,
344
+ headers: {
345
+ apikey: window.nexusState.key
346
+ }
347
+ }
348
+ if (type=="POST") {
349
+ const params = new URLSearchParams()
350
+ Object.keys(data).forEach(key => {
351
+ params.append(key, data[key])
352
+ })
353
+ payload.body = params
354
+ }
355
+
356
+ doFetch(`https://api.nexusmods.com/v1/games/${url}`, payload)
357
+ .then(r=>r.text())
358
+ .then(r => {
359
+ try {
360
+ resolve(JSON.parse(r))
361
+ } catch (e) {
362
+ window.appLogger.log(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
363
+ window.errorModal(window.i18n.ERROR_FROM_NEXUS.replace("_1", r))
364
+ }
365
+ })
366
+ .catch(err => {
367
+ console.log("err", err)
368
+ resolve()
369
+ })
370
+ })
371
+ }
372
+ window.nexus_getData = getData
373
+ // ==========================
374
+
375
+
376
+
377
+
378
+ let hasPopulatedNexusGameListDropdown = false
379
+ window.openNexusWindow = () => {
380
+ closeModal(undefined, nexusContainer).then(() => {
381
+ nexusContainer.style.opacity = 0
382
+ nexusContainer.style.display = "flex"
383
+ requestAnimationFrame(() => requestAnimationFrame(() => nexusContainer.style.opacity = 1))
384
+ requestAnimationFrame(() => requestAnimationFrame(() => chromeBar.style.opacity = 1))
385
+ })
386
+
387
+ const gameColours = {}
388
+ Object.keys(window.gameAssets).forEach(gameId => {
389
+ const colour = window.gameAssets[gameId].themeColourPrimary
390
+ gameColours[gameId] = colour
391
+ })
392
+
393
+ nexusGamesList.innerHTML = ""
394
+ Object.keys(window.gameAssets).sort((a,b)=>a<b?-1:1).forEach(gameId => {
395
+ const gameSelectContainer = createElem("div")
396
+ const gameCheckbox = createElem(`input#ngl_${gameId}`, {type: "checkbox"})
397
+ gameCheckbox.checked = true
398
+ const gameButton = createElem("button.fixedColour")
399
+ gameButton.style.setProperty("background-color", `#${gameColours[gameId]}`, "important")
400
+ gameButton.style.display = "flex"
401
+ gameButton.style.alignItems = "center"
402
+ gameButton.style.margin = "auto"
403
+ gameButton.style.marginTop = "8px"
404
+ const buttonLabel = createElem("span", window.gameAssets[gameId].gameName)
405
+
406
+ gameButton.addEventListener("contextmenu", e => {
407
+ if (e.target==gameButton || e.target==buttonLabel) {
408
+ Array.from(nexusGamesList.querySelectorAll("input")).forEach(ckbx => ckbx.checked = false)
409
+ gameCheckbox.click()
410
+ window.displayAllModels()
411
+ }
412
+ })
413
+
414
+ gameButton.addEventListener("click", e => {
415
+ if (e.target==gameButton || e.target==buttonLabel) {
416
+ gameCheckbox.click()
417
+ window.displayAllModels()
418
+ }
419
+ })
420
+ gameCheckbox.addEventListener("change", () => {
421
+ window.displayAllModels()
422
+ })
423
+
424
+ gameButton.appendChild(gameCheckbox)
425
+ gameButton.appendChild(buttonLabel)
426
+ gameSelectContainer.appendChild(gameButton)
427
+ nexusGamesList.appendChild(gameSelectContainer)
428
+ })
429
+
430
+ // Populate the game IDs for the Nexus repo searching
431
+ if (!hasPopulatedNexusGameListDropdown) {
432
+ window.getAllNexusGameIDs().then(results => {
433
+ if (results && results.length) {
434
+ results = results.sort((a,b)=>a.name.toLowerCase()<b.name.toLowerCase()?-1:1)
435
+ results.forEach(res => {
436
+
437
+ window.nexusGameIdToGameName[res.id] = res.name
438
+
439
+ const opt = createElem("option", {value: res.id})
440
+ opt.innerHTML = res.name
441
+ nexusAllGamesSelect.appendChild(opt)
442
+ })
443
+ if (fs.existsSync(`${window.path}/repositories.json`)) {
444
+ const data = fs.readFileSync(`${window.path}/repositories.json`, "utf8")
445
+ window.nexusReposList = JSON.parse(data)
446
+ window.nexusUpdateModsUsedPanel()
447
+ }
448
+ hasPopulatedNexusGameListDropdown = true
449
+ }
450
+ })
451
+ }
452
+
453
+ }
454
+ window.setupModal(nexusMenuButton, nexusContainer, window.openNexusWindow)
455
+
456
+ nexusGamesListEnableAllBtn.addEventListener("click", () => {
457
+ Array.from(nexusGamesList.querySelectorAll("input")).forEach(ckbx => ckbx.checked = true)
458
+ window.displayAllModels()
459
+ })
460
+ nexusGamesListDisableAllBtn.addEventListener("click", () => {
461
+ Array.from(nexusGamesList.querySelectorAll("input")).forEach(ckbx => ckbx.checked = false)
462
+ window.displayAllModels()
463
+ })
464
+
465
+
466
+
467
+ nexusSearchBar.addEventListener("keyup", () => {
468
+ window.displayAllModels()
469
+ })
470
+
471
+
472
+ window.getLatestModelsList = async () => {
473
+ if (!nexusState.key) {
474
+ return window.errorModal(window.i18n.YOU_MUST_BE_LOGGED_IN)
475
+ } else {
476
+ try {
477
+ window.spinnerModal(window.i18n.CHECKING_NEXUS)
478
+ window.nexusModelsList = []
479
+
480
+ const idToGame = {}
481
+ Object.keys(window.gameAssets).forEach(gameId => {
482
+ const id = window.gameAssets[gameId].gameCode.toLowerCase()
483
+ idToGame[id] = gameId
484
+ })
485
+
486
+ const repoLinks = window.nexusReposList.repos.filter(r=>r.enabled).map(r=>r.url)
487
+
488
+ for (let li=0; li<repoLinks.length; li++) {
489
+
490
+ const link = repoLinks[li].replace("\r","")
491
+ const repoInfo = await getData(`${link.split(".com/")[1]}.json`)
492
+ const author = repoInfo.author
493
+ const nexusRepoId = repoInfo.mod_id
494
+ const nexusRepoVersion = repoInfo.version
495
+ const nexusGameId = repoInfo.domain_name
496
+
497
+ const files = await getData(`${link.split(".com/")[1]}/files.json`)
498
+ files["files"].forEach(file => {
499
+
500
+ if (file.category_name=="OPTIONAL" || file.category_name=="OPTIONAL") {
501
+
502
+ if (!file.description.includes("Voice model")) {
503
+ return
504
+ }
505
+
506
+ const description = file.description
507
+ const parts = description.split("<br />")
508
+ let voiceId = parts.filter(line => line.startsWith("Voice ID:") || line.startsWith("VoiceID:"))[0]
509
+ voiceId = voiceId.includes("Voice ID: ") ? voiceId.split("Voice ID: ")[1].split(" ")[0] : voiceId.split("VoiceID: ")[1].split(" ")[0]
510
+ const game = idToGame[voiceId.split("_")[0]]
511
+ const name = parts.filter(line => line.startsWith("Voice model"))[0].split(" - ")[1]
512
+ const date = file.uploaded_time
513
+ const nexus_file_id = file.file_id
514
+
515
+ if (repoInfo.endorsement.endorse_status=="Endorsed") {
516
+ window.endorsedRepos.add(game)
517
+ }
518
+
519
+ const hasT2 = description.includes("Tacotron2")
520
+ const hasHiFi = description.includes("HiFi-GAN")
521
+ const version = file.version
522
+
523
+ let type
524
+ if (description.includes("Model:")) {
525
+ type = parts.filter(line => line.startsWith("Model: "))[0].split("Model: ")[1]
526
+ } else {
527
+ type = "FastPitch"
528
+ if (type=="FastPitch") {
529
+ if (hasT2) {
530
+ type = "T2+"+type
531
+ }
532
+ }
533
+ if (hasHiFi) {
534
+ type += "+HiFi"
535
+ }
536
+ }
537
+
538
+ const notes = description.includes("Notes:") ? parts.filter(line => line.startsWith("Notes: "))[0].split("Notes: ")[1] : ""
539
+ const meta = {author, description, version, voiceId, game, name, type, notes, date, nexusRepoId, nexusRepoVersion, nexusGameId, nexus_file_id, repoLink: link}
540
+ window.nexusModelsList.push(meta)
541
+ }
542
+ })
543
+ }
544
+
545
+ window.closeModal(undefined, nexusContainer)
546
+ window.displayAllModels()
547
+
548
+ } catch (e) {
549
+ console.log(e)
550
+ window.appLogger.log(e)
551
+ window.errorModal(e.message)
552
+ }
553
+ }
554
+ }
555
+
556
+ const clearColumsFocus = () => {
557
+ nexusRecordsHeader.querySelectorAll("div").forEach(elem => elem.style.textDecoration = "none")
558
+ }
559
+ const setColumnSort = (elem, key) => {
560
+ clearColumsFocus()
561
+ elem.style.textDecoration = "underline"
562
+ if (window.nexusState.primaryColumnSort==key) {
563
+ window.nexusState.columnSortModifier = window.nexusState.columnSortModifier * -1
564
+ }
565
+ window.nexusState.primaryColumnSort = key
566
+ window.displayAllModels()
567
+ }
568
+ i18n_nexush_game.addEventListener("click", () => setColumnSort(i18n_nexush_game, "game"))
569
+ i18n_nexush_name.addEventListener("click", () => setColumnSort(i18n_nexush_game, "name"))
570
+ i18n_nexush_author.addEventListener("click", () => setColumnSort(i18n_nexush_author, "author"))
571
+ i18n_nexush_version.addEventListener("click", () => setColumnSort(i18n_nexush_version, "version"))
572
+ i18n_nexush_date.addEventListener("click", () => setColumnSort(i18n_nexush_date, "date"))
573
+ i18n_nexush_type.addEventListener("click", () => setColumnSort(i18n_nexush_type, "type"))
574
+
575
+
576
+ const runNestedSort = (modelsList, primKey) => {
577
+ // Perform the primary sorting
578
+ const primaryGroup = {}
579
+ let modelsOrder = []
580
+ modelsList.forEach(item => {
581
+ if (!Object.keys(primaryGroup).includes(item[primKey]||"")) {
582
+ modelsOrder.push(item[primKey]||"")
583
+ primaryGroup[item[primKey]||""] = []
584
+ }
585
+ primaryGroup[item[primKey]||""].push(item)
586
+ })
587
+
588
+ // Sort the primary key in the correct direction
589
+ modelsOrder = modelsOrder.sort((a,b) => a<b?window.nexusState.columnSortModifier:-window.nexusState.columnSortModifier)
590
+
591
+ // Sort the secondary criteria (the voice names) within the primary groups
592
+ modelsOrder.forEach(primaryKey => {
593
+ primaryGroup[primaryKey] = primaryGroup[primaryKey].sort((a,b) => (a.name||"").toLowerCase()<(b.name||"").toLowerCase()?window.nexusState.columnSortModifier:-window.nexusState.columnSortModifier)
594
+ })
595
+ // Collate everything back into the final order
596
+ const finalOrder = []
597
+ modelsOrder.forEach(primaryKey => {
598
+ primaryGroup[primaryKey].forEach(record => finalOrder.push(record))
599
+ })
600
+ return finalOrder
601
+ }
602
+
603
+ window.displayAllModels = (forceUpdate=false) => {
604
+
605
+ if (!forceUpdate && window.nexusState.installQueue.length) {
606
+ return
607
+ }
608
+
609
+ const enabledGames = Array.from(nexusGamesList.querySelectorAll("input"))
610
+ .map(elem => [elem.checked, elem.id.replace("ngl_", "")])
611
+ .filter(checkedId => checkedId[0])
612
+ .map(checkedId => checkedId[1])
613
+
614
+ const gameColours = {}
615
+ Object.keys(window.gameAssets).forEach(gameId => {
616
+ const colour = window.gameAssets[gameId].themeColourPrimary
617
+ gameColours[gameId] = colour
618
+ })
619
+ const gameTitles = {}
620
+ Object.keys(window.gameAssets).forEach(gameId => {
621
+ const title = window.gameAssets[gameId].gameName
622
+ gameTitles[gameId] = title
623
+ })
624
+
625
+ nexusRecordsContainer.innerHTML = ""
626
+
627
+ window.nexusState.filteredDownloadableModels = []
628
+
629
+
630
+ let sortedModelsList = []
631
+ // Allow sorting by another column. But should still sort based on voice name alphabetically, as a secondary criteria
632
+ // Primary sortable columns: Game, VoiceName, Author, Version, Date, Type
633
+ if (window.nexusState.primaryColumnSort=="name") {
634
+ sortedModelsList = window.nexusModelsList.sort((a,b) => (a.name||"").toLowerCase()<(b.name||"").toLowerCase()?window.nexusState.columnSortModifier:-window.nexusState.columnSortModifier)
635
+ } else {
636
+ sortedModelsList = runNestedSort(window.nexusModelsList, window.nexusState.primaryColumnSort)
637
+ }
638
+
639
+ sortedModelsList.forEach(modelMeta => {
640
+ if (!enabledGames.includes(modelMeta.game)) {
641
+ return
642
+ }
643
+ if (nexusSearchBar.value.toLowerCase().trim().length && !modelMeta.name.toLowerCase().includes(nexusSearchBar.value.toLowerCase().trim())) {
644
+ return
645
+ }
646
+ let existingModel = undefined
647
+ if (Object.keys(window.games).includes(modelMeta.game)) {
648
+ for (let mi=0; mi<window.games[modelMeta.game].models.length; mi++) {
649
+ if (existingModel) continue
650
+
651
+ const variants = window.games[modelMeta.game].models[mi].variants
652
+
653
+ for (let vi=0; vi<variants.length; vi++) {
654
+ if (variants[vi].voiceId==modelMeta.voiceId) {
655
+ existingModel = variants[vi]
656
+ break
657
+ }
658
+ }
659
+ }
660
+ }
661
+
662
+ if (existingModel && nexusOnlyNewUpdatedCkbx.checked && (window.checkVersionRequirements(modelMeta.version, String(existingModel.modelVersion)) || (modelMeta.version.replace(".0","")==String(existingModel.modelVersion))) ){
663
+ return
664
+ }
665
+
666
+
667
+
668
+ const recordRow = createElem("div")
669
+ const actionsElem = createElem("div")
670
+
671
+ // Open link to the repo in the browser
672
+ const openButton = createElem("button.smallButton.fixedColour", window.i18n.OPEN)
673
+ openButton.style.setProperty("background-color", `#${gameColours[modelMeta.game]}`, "important")
674
+ openButton.addEventListener("click", () => {
675
+ shell.openExternal(modelMeta.repoLink)
676
+ })
677
+ actionsElem.appendChild(openButton)
678
+
679
+
680
+
681
+ // Download
682
+ const downloadButton = createElem("button.smallButton.fixedColour", window.i18n.DOWNLOAD)
683
+ downloadButton.style.setProperty("background-color", `#${gameColours[modelMeta.game]}`, "important")
684
+ downloadButton.addEventListener("click", async () => {
685
+ // Download the voice
686
+ window.nexusState.downloadQueue.push([modelMeta.voiceId, modelMeta.nexus_file_id])
687
+ nexusDownloadingCount.innerHTML = window.nexusState.downloadQueue.length
688
+ try {
689
+ await window.downloadFile([modelMeta.nexusGameId, modelMeta.nexusRepoId, modelMeta.voiceId, modelMeta.nexus_file_id])
690
+
691
+ // Install the downloaded voice
692
+ window.nexusState.installQueue.push([modelMeta.game, modelMeta.voiceId])
693
+ nexusInstallingCount.innerHTML = window.nexusState.installQueue.length
694
+ await window.installDownloadedModel([modelMeta.game, modelMeta.voiceId])
695
+
696
+ window.nexusState.finished += 1
697
+ nexusFinishedCount.innerHTML = window.nexusState.finished
698
+ window.displayAllModels()
699
+ window.loadAllModels(true).then(() => {
700
+ changeGame(window.currentGame)
701
+ })
702
+
703
+ } catch (e) {}
704
+
705
+ })
706
+ if (existingModel && (modelMeta.version.replace(".0","")==String(existingModel.modelVersion) || window.checkVersionRequirements(modelMeta.version, String(existingModel.modelVersion)) ) ) {
707
+ } else {
708
+ window.nexusState.filteredDownloadableModels.push(modelMeta)
709
+ actionsElem.appendChild(downloadButton)
710
+ }
711
+
712
+
713
+ // Endorse
714
+ const endorsed = window.endorsedRepos.has(modelMeta.game)
715
+ const endorseButton = createElem("button.smallButton.fixedColour", endorsed?"Unendorse":"Endorse")
716
+ if (endorsed) {
717
+ endorseButton.style.background = "none"
718
+ endorseButton.style.border = `2px solid #${gameColours[modelMeta.game]}`
719
+ } else {
720
+ endorseButton.style.setProperty("background-color", `#${gameColours[modelMeta.game]}`, "important")
721
+ }
722
+ endorseButton.addEventListener("click", async () => {
723
+ let response
724
+ if (endorsed) {
725
+ response = await getData(`${modelMeta.nexusGameId}/mods/${modelMeta.nexusRepoId}/abstain.json`, {
726
+ game_domain_name: modelMeta.nexusGameId,
727
+ id: modelMeta.nexusRepoId,
728
+ version: modelMeta.nexusRepoVersion
729
+ }, "POST")
730
+ } else {
731
+ response = await getData(`${modelMeta.nexusGameId}/mods/${modelMeta.nexusRepoId}/endorse.json`, {
732
+ game_domain_name: modelMeta.nexusGameId,
733
+ id: modelMeta.nexusRepoId,
734
+ version: modelMeta.nexusRepoVersion
735
+ }, "POST")
736
+ }
737
+ if (response && response.message && response.status=="Error") {
738
+ if (response.message=="NOT_DOWNLOADED_MOD") {
739
+ response.message = window.i18n.NEXUS_NOT_DOWNLOADED_MOD
740
+ } else if (response.message=="TOO_SOON_AFTER_DOWNLOAD") {
741
+ response.message = window.i18n.NEXUS_TOO_SOON_AFTER_DOWNLOAD
742
+ } else if (response.message=="IS_OWN_MOD") {
743
+ response.message = window.i18n.NEXUS_IS_OWN_MOD
744
+ }
745
+
746
+ window.errorModal(response.message)
747
+ } else {
748
+ if (endorsed) {
749
+ window.endorsedRepos.delete(modelMeta.game)
750
+ } else {
751
+ window.endorsedRepos.add(modelMeta.game)
752
+ }
753
+ window.displayAllModels()
754
+ }
755
+ })
756
+ actionsElem.appendChild(endorseButton)
757
+
758
+
759
+
760
+ const gameElem = createElem("div", gameTitles[modelMeta.game])
761
+ gameElem.title = gameTitles[modelMeta.game]
762
+ const nameElem = createElem("div", modelMeta.name)
763
+ nameElem.title = modelMeta.name
764
+ const authorElem = createElem("div", modelMeta.author)
765
+ authorElem.title = modelMeta.author
766
+ let versionElemText
767
+ if (existingModel) {
768
+ const yoursVersion = String(existingModel.modelVersion).includes(".") ? existingModel.modelVersion : existingModel.modelVersion+".0"
769
+ versionElemText = `${modelMeta.version} (${window.i18n.YOURS}: ${yoursVersion})`
770
+ } else {
771
+ versionElemText = modelMeta.version
772
+ }
773
+ const versionElem = createElem("div", versionElemText)
774
+ versionElem.title = versionElemText
775
+
776
+ const date = new Date(modelMeta.date)
777
+ const dateString = `${date.getDate()}/${date.getMonth()+1}/${date.getYear()+1900}`
778
+ const dateElem = createElem("div", dateString)
779
+ dateElem.title = dateString
780
+
781
+ const typeElem = createElem("div", modelMeta.type)
782
+ typeElem.title = modelMeta.type
783
+ const notesElem = createElem("div", modelMeta.notes)
784
+ notesElem.title = modelMeta.notes
785
+
786
+
787
+ recordRow.appendChild(actionsElem)
788
+ recordRow.appendChild(gameElem)
789
+ recordRow.appendChild(nameElem)
790
+ recordRow.appendChild(authorElem)
791
+ recordRow.appendChild(versionElem)
792
+ recordRow.appendChild(dateElem)
793
+ recordRow.appendChild(typeElem)
794
+ recordRow.appendChild(notesElem)
795
+
796
+ nexusRecordsContainer.appendChild(recordRow)
797
+ })
798
+ }
799
+
800
+
801
+ nexusCheckNow.addEventListener("click", () => window.getLatestModelsList())
802
+
803
+ window.setupModal(nexusManageReposButton, nexusReposContainer)
804
+
805
+ window.nexusUpdateModsUsedPanel = () => {
806
+ nexusReposUsedContainer.innerHTML = ""
807
+
808
+ window.nexusReposList.repos.forEach((repo, ri) => {
809
+ const row = createElem("div")
810
+
811
+ const enabledCkbx = createElem("input", {type: "checkbox"})
812
+ enabledCkbx.checked = repo.enabled
813
+ enabledCkbx.addEventListener("click", () => {
814
+ window.nexusReposList.repos[ri].enabled = enabledCkbx.checked
815
+ fs.writeFileSync(`${window.path}/repositories.json`, JSON.stringify(window.nexusReposList, null, 4), "utf8")
816
+ })
817
+ const enabledCkbxElem = createElem("div", enabledCkbx)
818
+
819
+ const removeButton = createElem("button.smallButton", window.i18n.REMOVE)
820
+ removeButton.style.background = `#${window.currentGame.themeColourPrimary}`
821
+ const removeButtonElem = createElem("div", removeButton)
822
+ const linkButton = createElem("button.smallButton", window.i18n.OPEN)
823
+ linkButton.style.background = `#${window.currentGame.themeColourPrimary}`
824
+ linkButton.addEventListener("click", () => {
825
+ shell.openExternal(repo.url)
826
+ })
827
+ const linkButtonElem = createElem("div", linkButton)
828
+ const gameElem = createElem("div", window.nexusGameIdToGameName[repo.game_id])
829
+ gameElem.title = window.nexusGameIdToGameName[repo.game_id]
830
+ const nameElem = createElem("div", repo.name)
831
+ nameElem.title = repo.name
832
+ const authorElem = createElem("div", repo.author)
833
+ authorElem.title = repo.author
834
+ const endorsementsElem = createElem("div", String(repo.endorsements))
835
+ const downloadsElem = createElem("div", String(repo.downloads))
836
+
837
+ endorsementsElem.style.display = "none" // TEMP
838
+ downloadsElem.style.display = "none" // TEMP
839
+
840
+ row.appendChild(enabledCkbxElem)
841
+ row.appendChild(linkButtonElem)
842
+ row.appendChild(gameElem)
843
+ row.appendChild(nameElem)
844
+ row.appendChild(authorElem)
845
+ row.appendChild(endorsementsElem)
846
+ row.appendChild(downloadsElem)
847
+ row.appendChild(removeButtonElem)
848
+ nexusReposUsedContainer.appendChild(row)
849
+ })
850
+ }
851
+
852
+
853
+ window.addRepoToApp = (repo) => {
854
+ repo.enabled = true
855
+ window.nexusReposList.repos.push(repo)
856
+ window.nexusReposList.repos = window.nexusReposList.repos.sort((a,b)=>a.endorsements<b.endorsements?1:-1)
857
+ fs.writeFileSync(`${window.path}/repositories.json`, JSON.stringify(window.nexusReposList, null, 4), "utf8")
858
+ window.nexusUpdateModsUsedPanel()
859
+ }
860
+
861
+
862
+
863
+
864
+ nexusReposSearchBar.addEventListener("keydown", e => {
865
+ if (e.key.toLowerCase()=="enter" && nexusReposSearchBar.value.length) {
866
+ searchNexusButton.click()
867
+ }
868
+ })
869
+ searchNexusButton.addEventListener("click", () => {
870
+ const gameId = nexusAllGamesSelect.value ? parseInt(nexusAllGamesSelect.value) : undefined
871
+ const query = nexusReposSearchBar.value
872
+ nexusSearchContainer.innerHTML = ""
873
+ window.mod_search_nexus(gameId, query).then(results => {
874
+
875
+ const numResults = results[0]
876
+ results = results[1]
877
+
878
+ results.forEach(repo => {
879
+
880
+ const row = createElem("div")
881
+ const addButton = createElem("button.smallButton", window.i18n.ADD)
882
+ const addButtonElem = createElem("div", addButton)
883
+ addButton.style.background = `#${window.currentGame.themeColourPrimary}`
884
+ if (window.nexusReposList.repos.find(r=>r.url==repo.url)) {
885
+ addButton.disabled = true
886
+ }
887
+ addButton.addEventListener("click", () => {
888
+ window.addRepoToApp(repo)
889
+ addButton.disabled = true
890
+ })
891
+
892
+ const linkButton = createElem("button.smallButton", window.i18n.OPEN)
893
+ linkButton.style.background = `#${window.currentGame.themeColourPrimary}`
894
+ linkButton.addEventListener("click", () => {
895
+ shell.openExternal(repo.url)
896
+ })
897
+ const linkButtonElem = createElem("div", linkButton)
898
+ const gameElem = createElem("div", window.nexusGameIdToGameName[repo.game_id])
899
+ gameElem.title = window.nexusGameIdToGameName[repo.game_id]
900
+ const nameElem = createElem("div", repo.name)
901
+ nameElem.title = repo.name
902
+ const authorElem = createElem("div", repo.author)
903
+ authorElem.title = repo.author
904
+ const endorsementsElem = createElem("div", String(repo.endorsements))
905
+ const downloadsElem = createElem("div", String(repo.downloads))
906
+
907
+
908
+ row.appendChild(addButtonElem)
909
+ row.appendChild(linkButtonElem)
910
+ row.appendChild(gameElem)
911
+ row.appendChild(nameElem)
912
+ row.appendChild(authorElem)
913
+ row.appendChild(endorsementsElem)
914
+ row.appendChild(downloadsElem)
915
+ nexusSearchContainer.appendChild(row)
916
+ })
917
+
918
+ })
919
+ })
920
+
921
+ nexusOnlyNewUpdatedCkbx.addEventListener("change", () => window.displayAllModels())
922
+ window.initNexus()
923
+
924
+
925
+ // The app will support voice installation via Steam workshop. However, workshop installations can only install voices into the game directory
926
+ // Moreover, users can pick their own locations for voice models. To handle this, I'll have all voices go into the "workshop" folder. From here,
927
+ // the app will (on start-up) check if there's anything there, and it will move it to the correct location
928
+ window.checkForWorkshopInstallations = () => {
929
+
930
+ let voicesInstalled = 0
931
+ let badGameIDs = []
932
+
933
+ if (fs.existsSync(`${window.path}/workshop`) && fs.existsSync(`${window.path}/workshop/voices`)) {
934
+
935
+ const gameFolders = fs.readdirSync(`${window.path}/workshop/voices`)
936
+
937
+ gameFolders.forEach(gameId => {
938
+
939
+ const userModelDir = window.userSettings[`modelspath_${gameId}`]
940
+
941
+ if (!userModelDir) {
942
+ badGameIDs.push(gameId)
943
+ return
944
+ }
945
+
946
+ const voiceIDs_jsons = fs.readdirSync(`${window.path}/workshop/voices/${gameId}`).filter(fName => fName.endsWith(".json"))
947
+
948
+ voiceIDs_jsons.forEach(voiceIDs_json => {
949
+ const voiceID = voiceIDs_json.replace(".json", "")
950
+ const voiceFiles = fs.readdirSync(`${window.path}/workshop/voices/${gameId}`).filter(fName => fName.includes(voiceID))
951
+
952
+ voiceFiles.forEach(voiceFileName => {
953
+ if (!fs.existsSync(`${userModelDir}/${voiceFileName}`)) {
954
+ fs.copyFileSync(`${window.path}/workshop/voices/${gameId}/${voiceFileName}`, `${userModelDir}/${voiceFileName}`)
955
+ }
956
+ fs.unlinkSync(`${window.path}/workshop/voices/${gameId}/${voiceFileName}`)
957
+ })
958
+
959
+ voicesInstalled++
960
+ })
961
+ })
962
+ }
963
+
964
+ if (voicesInstalled || badGameIDs.length) {
965
+ setTimeout(() => {
966
+
967
+ let modalMessage = ""
968
+
969
+ if (voicesInstalled) {
970
+ modalMessage += window.i18n.X_WORKSHOP_VOICES_INSTALLED.replace("_1", voicesInstalled)
971
+ }
972
+ if (voicesInstalled && badGameIDs.length) {
973
+ modalMessage += "<br><br>"
974
+ }
975
+ if (badGameIDs) {
976
+ modalMessage += window.i18n.WORKSHOP_GAMES_NOT_RECOGNISED.replace("_1", badGameIDs.join(","))
977
+ }
978
+
979
+ createModal("error", modalMessage)
980
+ window.updateGameList()
981
+ }, 1000)
982
+ }
983
+ }
javascript/outputFiles.js ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ const spawn = require("child_process").spawn
4
+
5
+ window.outputFilesState = {
6
+ paginationIndex: 0,
7
+ records: [],
8
+ totalPages: 0
9
+ }
10
+
11
+ window.initMainPagePagination = (directory) => {
12
+ window.outputFilesState.records = []
13
+ window.outputFilesState.totalPages = 0
14
+ window.resetPagination()
15
+
16
+ if (!fs.existsSync(directory)) {
17
+ return
18
+ }
19
+
20
+ const records = []
21
+ const files = fs.readdirSync(directory)
22
+ files.forEach(file => {
23
+ if (!["wav", "mp3", "ogg", "opus", "wma", "xwm"].includes(file.split(".").reverse()[0].toLowerCase())) {
24
+ return
25
+ }
26
+
27
+ let jsonData
28
+
29
+ if (fs.existsSync(`${directory}/${file}.json`)) {
30
+ try {
31
+ const lineMeta = fs.readFileSync(`${directory}/${file}.json`, "utf8")
32
+ jsonData = JSON.parse(lineMeta)
33
+ } catch (e) {
34
+ // console.log(e)
35
+ }
36
+ }
37
+
38
+ const record = {}
39
+ record.fileName = file
40
+ record.lastChanged = fs.statSync(`${directory}/${file}`).mtime
41
+ record.jsonPath = `${directory}/${file}`
42
+ records.push([record, jsonData])
43
+ })
44
+
45
+ window.outputFilesState.records = records
46
+ window.reOrderMainPageRecords()
47
+ }
48
+ window.resetPagination = () => {
49
+ window.outputFilesState.paginationIndex = 0
50
+ const numPages = Math.ceil(window.outputFilesState.records.length/window.userSettings.output_files_pagination_size)
51
+ main_total_pages.innerHTML = window.i18n.PAGINATION_TOTAL_OF.replace("_1", numPages)
52
+ window.outputFilesState.totalPages = numPages
53
+ main_pageNum.value = 1
54
+ }
55
+ window.reOrderMainPageRecords = () => {
56
+ const reverse = window.userSettings.voiceRecordsOrderByOrder=="ascending"
57
+ const sortBy = window.userSettings.voiceRecordsOrderBy
58
+
59
+ window.outputFilesState.records = window.outputFilesState.records.sort((a,b) => {
60
+ if (sortBy=="name") {
61
+ return a[0].fileName.toLowerCase()<b[0].fileName.toLowerCase() ? (reverse?-1:1) : (reverse?1:-1)
62
+ } else if (sortBy=="time") {
63
+ return a[0].lastChanged<b[0].lastChanged ? (reverse?-1:1) : (reverse?1:-1)
64
+ } else {
65
+ console.warn("sort by type not recognised", sortBy)
66
+ }
67
+ })
68
+ }
69
+
70
+ window.makeSample = (src, newSample) => {
71
+ const fileName = src.split("/").reverse()[0].split("%20").join(" ")
72
+ const fileFormat = fileName.split(".").reverse()[0]
73
+ const fileNameElem = createElem("div", fileName)
74
+ const promptText = createElem("div.samplePromptText")
75
+
76
+ if (fs.existsSync(src+".json")) {
77
+ try {
78
+ const lineMeta = fs.readFileSync(src+".json", "utf8")
79
+ promptText.innerHTML = JSON.parse(lineMeta).inputSequence
80
+ if (promptText.innerHTML.length > 130) {
81
+ promptText.innerHTML = promptText.innerHTML.slice(0, 130)+"..."
82
+ }
83
+ } catch (e) {
84
+ // console.log(e)
85
+ }
86
+ }
87
+ const sample = createElem("div.sample", createElem("div", fileNameElem, promptText))
88
+ const audioControls = createElem("div.sampleAudioControls")
89
+ const audio = createElem("audio", {controls: true}, createElem("source", {
90
+ src: src,
91
+ type: `audio/${fileFormat}`
92
+ }))
93
+ audio.addEventListener("play", () => {
94
+ if (window.ctrlKeyIsPressed) {
95
+ audio.setSinkId(window.userSettings.alt_speaker)
96
+ } else {
97
+ audio.setSinkId(window.userSettings.base_speaker)
98
+ }
99
+ })
100
+ audio.setSinkId(window.userSettings.base_speaker)
101
+
102
+ const audioSVG = window.getAudioPlayTriangleSVG()
103
+ audioSVG.addEventListener("click", () => {
104
+ audio.play()
105
+ })
106
+
107
+ const openFileLocationButton = createElem("div", {title: window.i18n.OPEN_CONTAINING_FOLDER})
108
+ openFileLocationButton.innerHTML = `<svg class="openFolderSVG" id="svg" version="1.1" xmlns="http:\/\/www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="350" viewBox="0, 0, 400,350"><g id="svgg"><path id="path0" d="M39.960 53.003 C 36.442 53.516,35.992 53.635,30.800 55.422 C 15.784 60.591,3.913 74.835,0.636 91.617 C -0.372 96.776,-0.146 305.978,0.872 310.000 C 5.229 327.228,16.605 339.940,32.351 345.172 C 40.175 347.773,32.175 347.630,163.000 347.498 L 281.800 347.378 285.600 346.495 C 304.672 342.065,321.061 332.312,330.218 319.944 C 330.648 319.362,332.162 317.472,333.581 315.744 C 335.001 314.015,336.299 312.420,336.467 312.200 C 336.634 311.980,337.543 310.879,338.486 309.753 C 340.489 307.360,342.127 305.341,343.800 303.201 C 344.460 302.356,346.890 299.375,349.200 296.575 C 351.510 293.776,353.940 290.806,354.600 289.975 C 355.260 289.144,356.561 287.505,357.492 286.332 C 358.422 285.160,359.952 283.267,360.892 282.126 C 362.517 280.153,371.130 269.561,375.632 264.000 C 376.789 262.570,380.427 258.097,383.715 254.059 C 393.790 241.689,396.099 237.993,398.474 230.445 C 403.970 212.972,394.149 194.684,376.212 188.991 C 369.142 186.747,368.803 186.724,344.733 186.779 C 330.095 186.812,322.380 186.691,322.216 186.425 C 322.078 186.203,321.971 178.951,321.977 170.310 C 321.995 146.255,321.401 141.613,317.200 133.000 C 314.009 126.457,307.690 118.680,303.142 115.694 C 302.560 115.313,301.300 114.438,300.342 113.752 C 295.986 110.631,288.986 107.881,282.402 106.704 C 280.540 106.371,262.906 106.176,220.400 106.019 L 161.000 105.800 160.763 98.800 C 159.961 75.055,143.463 56.235,120.600 52.984 C 115.148 52.208,45.292 52.225,39.960 53.003 M120.348 80.330 C 130.472 83.988,133.993 90.369,133.998 105.071 C 134.003 120.968,137.334 127.726,147.110 131.675 L 149.400 132.600 213.800 132.807 C 272.726 132.996,278.392 133.071,280.453 133.690 C 286.872 135.615,292.306 141.010,294.261 147.400 C 294.928 149.578,294.996 151.483,294.998 168.000 L 295.000 186.200 292.800 186.449 C 291.590 186.585,254.330 186.725,210.000 186.759 C 163.866 186.795,128.374 186.977,127.000 187.186 C 115.800 188.887,104.936 192.929,96.705 198.458 C 95.442 199.306,94.302 200.000,94.171 200.000 C 93.815 200.000,89.287 203.526,87.000 205.583 C 84.269 208.039,80.083 212.649,76.488 217.159 C 72.902 221.657,72.598 222.031,70.800 224.169 C 70.030 225.084,68.770 226.620,68.000 227.582 C 67.230 228.544,66.054 229.977,65.387 230.766 C 64.720 231.554,62.727 234.000,60.957 236.200 C 59.188 238.400,56.346 241.910,54.642 244.000 C 52.938 246.090,50.163 249.510,48.476 251.600 C 44.000 257.146,36.689 266.126,36.212 266.665 C 35.985 266.921,34.900 268.252,33.800 269.623 C 32.700 270.994,30.947 273.125,29.904 274.358 C 28.861 275.591,28.006 276.735,28.004 276.900 C 28.002 277.065,27.728 277.200,27.395 277.200 C 26.428 277.200,26.700 96.271,27.670 93.553 C 30.020 86.972,35.122 81.823,40.800 80.300 C 44.238 79.378,47.793 79.296,81.800 79.351 L 117.800 79.410 120.348 80.330 M369.400 214.800 C 374.239 217.220,374.273 222.468,369.489 228.785 C 367.767 231.059,364.761 234.844,364.394 235.200 C 364.281 235.310,362.373 237.650,360.154 240.400 C 357.936 243.150,354.248 247.707,351.960 250.526 C 347.732 255.736,346.053 257.821,343.202 261.400 C 341.505 263.530,340.849 264.336,334.600 271.965 C 332.400 274.651,330.204 277.390,329.720 278.053 C 329.236 278.716,328.246 279.945,327.520 280.785 C 326.794 281.624,325.300 283.429,324.200 284.794 C 323.100 286.160,321.726 287.845,321.147 288.538 C 320.568 289.232,318.858 291.345,317.347 293.233 C 308.372 304.449,306.512 306.609,303.703 309.081 C 299.300 312.956,290.855 317.633,286.000 318.886 C 277.958 320.960,287.753 320.819,159.845 320.699 C 33.557 320.581,42.330 320.726,38.536 318.694 C 34.021 316.276,35.345 310.414,42.386 301.647 C 44.044 299.583,45.940 297.210,46.600 296.374 C 47.260 295.538,48.340 294.169,49.000 293.332 C 49.660 292.495,51.550 290.171,53.200 288.167 C 54.850 286.164,57.100 283.395,58.200 282.015 C 59.300 280.635,60.920 278.632,61.800 277.564 C 62.680 276.496,64.210 274.617,65.200 273.389 C 66.190 272.162,67.188 270.942,67.418 270.678 C 67.649 270.415,71.591 265.520,76.179 259.800 C 80.767 254.080,84.634 249.310,84.773 249.200 C 84.913 249.090,87.117 246.390,89.673 243.200 C 92.228 240.010,95.621 235.780,97.213 233.800 C 106.328 222.459,116.884 215.713,128.200 213.998 C 129.300 213.832,183.570 213.719,248.800 213.748 L 367.400 213.800 369.400 214.800 " stroke="none" fill="#050505" fill-rule="evenodd"></path><path id="path1" d="M0.000 46.800 C 0.000 72.540,0.072 93.600,0.159 93.600 C 0.246 93.600,0.516 92.460,0.759 91.066 C 3.484 75.417,16.060 60.496,30.800 55.422 C 35.953 53.648,36.338 53.550,40.317 52.981 C 46.066 52.159,114.817 52.161,120.600 52.984 C 143.463 56.235,159.961 75.055,160.763 98.800 L 161.000 105.800 220.400 106.019 C 262.906 106.176,280.540 106.371,282.402 106.704 C 288.986 107.881,295.986 110.631,300.342 113.752 C 301.300 114.438,302.560 115.313,303.142 115.694 C 307.690 118.680,314.009 126.457,317.200 133.000 C 321.401 141.613,321.995 146.255,321.977 170.310 C 321.971 178.951,322.078 186.203,322.216 186.425 C 322.380 186.691,330.095 186.812,344.733 186.779 C 368.803 186.724,369.142 186.747,376.212 188.991 C 381.954 190.814,388.211 194.832,391.662 198.914 C 395.916 203.945,397.373 206.765,399.354 213.800 C 399.842 215.533,399.922 201.399,399.958 107.900 L 400.000 0.000 200.000 0.000 L 0.000 0.000 0.000 46.800 M44.000 79.609 C 35.903 81.030,30.492 85.651,27.670 93.553 C 26.700 96.271,26.428 277.200,27.395 277.200 C 27.728 277.200,28.002 277.065,28.004 276.900 C 28.006 276.735,28.861 275.591,29.904 274.358 C 30.947 273.125,32.700 270.994,33.800 269.623 C 34.900 268.252,35.985 266.921,36.212 266.665 C 36.689 266.126,44.000 257.146,48.476 251.600 C 50.163 249.510,52.938 246.090,54.642 244.000 C 56.346 241.910,59.188 238.400,60.957 236.200 C 62.727 234.000,64.720 231.554,65.387 230.766 C 66.054 229.977,67.230 228.544,68.000 227.582 C 68.770 226.620,70.030 225.084,70.800 224.169 C 72.598 222.031,72.902 221.657,76.488 217.159 C 80.083 212.649,84.269 208.039,87.000 205.583 C 89.287 203.526,93.815 200.000,94.171 200.000 C 94.302 200.000,95.442 199.306,96.705 198.458 C 104.936 192.929,115.800 188.887,127.000 187.186 C 128.374 186.977,163.866 186.795,210.000 186.759 C 254.330 186.725,291.590 186.585,292.800 186.449 L 295.000 186.200 294.998 168.000 C 294.996 151.483,294.928 149.578,294.261 147.400 C 292.306 141.010,286.872 135.615,280.453 133.690 C 278.392 133.071,272.726 132.996,213.800 132.807 L 149.400 132.600 147.110 131.675 C 137.334 127.726,134.003 120.968,133.998 105.071 C 133.993 90.369,130.472 83.988,120.348 80.330 L 117.800 79.410 81.800 79.351 C 62.000 79.319,44.990 79.435,44.000 79.609 M128.200 213.998 C 116.884 215.713,106.328 222.459,97.213 233.800 C 95.621 235.780,92.228 240.010,89.673 243.200 C 87.117 246.390,84.913 249.090,84.773 249.200 C 84.634 249.310,80.767 254.080,76.179 259.800 C 71.591 265.520,67.649 270.415,67.418 270.678 C 67.188 270.942,66.190 272.162,65.200 273.389 C 64.210 274.617,62.680 276.496,61.800 277.564 C 60.920 278.632,59.300 280.635,58.200 282.015 C 57.100 283.395,54.850 286.164,53.200 288.167 C 51.550 290.171,49.660 292.495,49.000 293.332 C 48.340 294.169,47.260 295.538,46.600 296.374 C 45.940 297.210,44.044 299.583,42.386 301.647 C 35.345 310.414,34.021 316.276,38.536 318.694 C 42.330 320.726,33.557 320.581,159.845 320.699 C 287.753 320.819,277.958 320.960,286.000 318.886 C 290.855 317.633,299.300 312.956,303.703 309.081 C 306.512 306.609,308.372 304.449,317.347 293.233 C 318.858 291.345,320.568 289.232,321.147 288.538 C 321.726 287.845,323.100 286.160,324.200 284.794 C 325.300 283.429,326.794 281.624,327.520 280.785 C 328.246 279.945,329.236 278.716,329.720 278.053 C 330.204 277.390,332.400 274.651,334.600 271.965 C 340.849 264.336,341.505 263.530,343.202 261.400 C 346.053 257.821,347.732 255.736,351.960 250.526 C 354.248 247.707,357.936 243.150,360.154 240.400 C 362.373 237.650,364.281 235.310,364.394 235.200 C 364.761 234.844,367.767 231.059,369.489 228.785 C 374.273 222.468,374.239 217.220,369.400 214.800 L 367.400 213.800 248.800 213.748 C 183.570 213.719,129.300 213.832,128.200 213.998 M399.600 225.751 C 399.600 231.796,394.623 240.665,383.715 254.059 C 380.427 258.097,376.789 262.570,375.632 264.000 C 371.130 269.561,362.517 280.153,360.892 282.126 C 359.952 283.267,358.422 285.160,357.492 286.332 C 356.561 287.505,355.260 289.144,354.600 289.975 C 353.940 290.806,351.510 293.776,349.200 296.575 C 346.890 299.375,344.460 302.356,343.800 303.201 C 342.127 305.341,340.489 307.360,338.486 309.753 C 337.543 310.879,336.634 311.980,336.467 312.200 C 336.299 312.420,335.001 314.015,333.581 315.744 C 332.162 317.472,330.648 319.362,330.218 319.944 C 321.061 332.312,304.672 342.065,285.600 346.495 L 281.800 347.378 163.000 347.498 C 32.175 347.630,40.175 347.773,32.351 345.172 C 16.471 339.895,3.810 325.502,0.820 309.326 C 0.591 308.085,0.312 306.979,0.202 306.868 C 0.091 306.757,-0.000 327.667,-0.000 353.333 L 0.000 400.000 200.000 400.000 L 400.000 400.000 400.000 312.400 C 400.000 264.220,399.910 224.800,399.800 224.800 C 399.690 224.800,399.600 225.228,399.600 225.751 " stroke="none" fill="#fbfbfb" fill-rule="evenodd"></path></g></svg>`
109
+ openFileLocationButton.addEventListener("click", () => {
110
+ const containingFolder = src.split("/").slice(0, -1).join("/").replaceAll("/", "\\")
111
+ // shell.showItemInFolder(src)
112
+ // er.shell.showItemInFolder(src)
113
+
114
+ // Electron suddenly isn't working anymore, to open folders, since updating from v2 to v19
115
+ // Couldn't figure it out, so I'm just gonna do it myself, manually. It doesn't show the file, but at least it opens the folder
116
+
117
+ er.shell.showItemInFolder(src)
118
+ spawn(`explorer`, [containingFolder], {stdio: "ignore"})
119
+ })
120
+
121
+ if (fs.existsSync(`${src}.json`)) {
122
+ const editButton = createElem("div", {title: window.i18n.ADJUST_SAMPLE_IN_EDITOR})
123
+ editButton.innerHTML = `<svg class="renameSVG" version="1.0" xmlns="http:\/\/www.w3.org/2000/svg" width="344.000000pt" height="344.000000pt" viewBox="0 0 344.000000 344.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,344.000000) scale(0.100000,-0.100000)" fill="#555555" stroke="none"><path d="M1489 2353 l-936 -938 -197 -623 c-109 -343 -195 -626 -192 -629 2 -3 284 84 626 193 l621 198 937 938 c889 891 937 940 934 971 -11 108 -86 289 -167 403 -157 219 -395 371 -655 418 l-34 6 -937 -937z m1103 671 c135 -45 253 -135 337 -257 41 -61 96 -178 112 -241 l12 -48 -129 -129 -129 -129 -287 287 -288 288 127 127 c79 79 135 128 148 128 11 0 55 -12 97 -26z m-1798 -1783 c174 -79 354 -248 436 -409 59 -116 72 -104 -213 -196 l-248 -80 -104 104 c-58 58 -105 109 -105 115 0 23 154 495 162 495 5 0 37 -13 72 -29z"/></g></svg>`
124
+ editButton.addEventListener("click", () => {
125
+
126
+ const doTheRest = () => {
127
+ let editData = fs.readFileSync(`${src}.json`, "utf8")
128
+ editData = JSON.parse(editData)
129
+
130
+ generateVoiceButton.dataset.modelIDLoaded = editData.pitchEditor ? editData.pitchEditor.currentVoice : editData.currentVoice
131
+
132
+ window.sequenceEditor.historyState.push(editData.inputSequence.trim())
133
+ window.sequenceEditor.isEditingFromFile = true
134
+ window.sequenceEditor.inputSequence = editData.inputSequence
135
+ window.sequenceEditor.pacing = editData.pacing
136
+ window.sequenceEditor.letters = editData.pitchEditor ? editData.pitchEditor.letters : editData.letters
137
+ window.sequenceEditor.currentVoice = editData.pitchEditor ? editData.pitchEditor.currentVoice : editData.currentVoice
138
+ window.sequenceEditor.resetEnergy = (editData.pitchEditor && editData.pitchEditor.resetEnergy) ? editData.pitchEditor.resetEnergy : editData.resetEnergy
139
+ window.sequenceEditor.resetPitch = editData.pitchEditor ? editData.pitchEditor.resetPitch : editData.resetPitch
140
+ window.sequenceEditor.resetDurs = editData.pitchEditor ? editData.pitchEditor.resetDurs : editData.resetDurs
141
+ window.sequenceEditor.resetEmAngry = editData.resetEmAngry
142
+ window.sequenceEditor.resetEmHappy = editData.resetEmHappy
143
+ window.sequenceEditor.resetEmSad = editData.resetEmSad
144
+ window.sequenceEditor.resetEmSurprise = editData.resetEmSurprise
145
+ window.sequenceEditor.letterFocus = []
146
+ window.sequenceEditor.ampFlatCounter = 0
147
+ window.sequenceEditor.hasChanged = false
148
+ window.sequenceEditor.sequence = editData.pitchEditor ? editData.pitchEditor.sequence : editData.sequence
149
+ window.sequenceEditor.energyNew = (editData.pitchEditor && editData.pitchEditor.energyNew) ? editData.pitchEditor.energyNew : editData.energyNew
150
+ window.sequenceEditor.pitchNew = editData.pitchEditor ? editData.pitchEditor.pitchNew : editData.pitchNew
151
+ window.sequenceEditor.dursNew = editData.pitchEditor ? editData.pitchEditor.dursNew : editData.dursNew
152
+ window.sequenceEditor.emAngryNew = editData.emAngryNew
153
+ window.sequenceEditor.emHappyNew = editData.emHappyNew
154
+ window.sequenceEditor.emSadNew = editData.emSadNew
155
+ window.sequenceEditor.emSurpriseNew = editData.emSurpriseNew
156
+
157
+ if (editData.styleValuesReset) {
158
+ window.sequenceEditor.loadStylesData()
159
+
160
+ window.sequenceEditor.registeredStyleKeys.forEach(styleKey => {
161
+ window.sequenceEditor.styleValuesReset[styleKey] = editData.styleValuesReset[styleKey]
162
+ window.sequenceEditor.styleValuesNew[styleKey] = editData.styleValuesNew[styleKey]
163
+ })
164
+ }
165
+
166
+
167
+ window.sequenceEditor.init()
168
+ window.sequenceEditor.update(window.currentModel.modelType)
169
+ window.sequenceEditor.autoInferTimer = null
170
+
171
+ dialogueInput.value = editData.inputSequence
172
+ window.refreshText()
173
+ paceNumbInput.value = editData.pacing
174
+ pace_slid.value = editData.pacing
175
+
176
+ window.sequenceEditor.sliderBoxes.forEach((box, i) => {box.setValueFromValue(window.sequenceEditor.dursNew[i])})
177
+ window.sequenceEditor.update(window.currentModel.modelType)
178
+
179
+ if (!window.wavesurfer) {
180
+ window.initWaveSurfer(src)
181
+ } else {
182
+ window.wavesurfer.load(src)
183
+ }
184
+
185
+ samplePlayPause.style.display = "block"
186
+ }
187
+
188
+ if (window.currentModel.loaded) {
189
+ doTheRest()
190
+ } else {
191
+ window.loadModel().then(() => {
192
+ doTheRest()
193
+ })
194
+ }
195
+ })
196
+ audioControls.appendChild(editButton)
197
+ }
198
+
199
+ const renameButton = createElem("div", {title: window.i18n.RENAME_THE_FILE})
200
+ renameButton.innerHTML = `<svg class="renameSVG" version="1.0" xmlns="http://www.w3.org/2000/svg" width="166.000000pt" height="336.000000pt" viewBox="0 0 166.000000 336.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,336.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"> <path d="M165 3175 c-30 -31 -35 -42 -35 -84 0 -34 6 -56 21 -75 42 -53 58 -56 324 -56 l245 0 0 -1290 0 -1290 -245 0 c-266 0 -282 -3 -324 -56 -15 -19 -21 -41 -21 -75 0 -42 5 -53 35 -84 l36 -35 281 0 280 0 41 40 c30 30 42 38 48 28 5 -7 9 -16 9 -21 0 -4 15 -16 33 -27 30 -19 51 -20 319 -20 l287 0 36 35 c30 31 35 42 35 84 0 34 -6 56 -21 75 -42 53 -58 56 -324 56 l-245 0 0 1290 0 1290 245 0 c266 0 282 3 324 56 15 19 21 41 21 75 0 42 -5 53 -35 84 l-36 35 -287 0 c-268 0 -289 -1 -319 -20 -18 -11 -33 -23 -33 -27 0 -5 -4 -14 -9 -21 -6 -10 -18 -2 -48 28 l-41 40 -280 0 -281 0 -36 -35z"/></g></svg>`
201
+
202
+ renameButton.addEventListener("click", () => {
203
+ createModal("prompt", {
204
+ prompt: window.i18n.ENTER_NEW_FILENAME_UNCHANGED_CANCEL,
205
+ value: sample.querySelector("div").innerHTML
206
+ }).then(newFileName => {
207
+ if (newFileName!=fileName) {
208
+ const oldPath = src.split("/").reverse()
209
+ const newPath = src.split("/").reverse()
210
+ oldPath[0] = sample.querySelector("div").innerHTML
211
+ newPath[0] = newFileName
212
+
213
+ const oldPathComposed = oldPath.reverse().join("/")
214
+ const newPathComposed = newPath.reverse().join("/")
215
+ fs.renameSync(oldPathComposed, newPathComposed)
216
+
217
+ if (fs.existsSync(`${oldPathComposed}.json`)) {
218
+ fs.renameSync(oldPathComposed+".json", newPathComposed+".json")
219
+ }
220
+ if (fs.existsSync(`${oldPathComposed.replace(/\.wav$/, "")}.lip`)) {
221
+ fs.renameSync(oldPathComposed.replace(/\.wav$/, "")+".lip", newPathComposed.replace(/\.wav$/, "")+".lip")
222
+ }
223
+ if (fs.existsSync(`${oldPathComposed.replace(/\.wav$/, "")}.fuz`)) {
224
+ fs.renameSync(oldPathComposed.replace(/\.wav$/, "")+".fuz", newPathComposed.replace(/\.wav$/, "")+".fuz")
225
+ }
226
+
227
+ oldPath.reverse()
228
+ oldPath.splice(0,1)
229
+ refreshRecordsList(oldPath.reverse().join("/"))
230
+ }
231
+ })
232
+ })
233
+
234
+ const editInProgramButton = createElem("div", {title: window.i18n.EDIT_IN_EXTERNAL_PROGRAM})
235
+ editInProgramButton.innerHTML = `<svg class="renameSVG" version="1.0" width="175.000000pt" height="240.000000pt" viewBox="0 0 175.000000 240.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,240.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"><path d="M615 2265 l-129 -125 -68 0 c-95 0 -98 -4 -98 -150 0 -146 3 -150 98 -150 l68 0 129 -125 c128 -123 165 -145 179 -109 8 20 8 748 0 768 -14 36 -51 14 -179 -109z"/> <path d="M1016 2344 c-22 -21 -20 -30 10 -51 66 -45 126 -109 151 -162 22 -47 27 -69 27 -141 0 -72 -5 -94 -27 -141 -25 -53 -85 -117 -151 -162 -30 -20 -33 -39 -11 -57 22 -18 64 3 132 64 192 173 164 491 -54 636 -54 35 -56 35 -77 14z"/> <path d="M926 2235 c-8 -22 1 -37 46 -70 73 -53 104 -149 78 -241 -13 -44 -50 -92 -108 -136 -26 -21 -27 -31 -6 -52 37 -38 150 68 179 167 27 91 13 181 -41 259 -49 70 -133 112 -148 73z"/> <path d="M834 2115 c-9 -23 2 -42 33 -57 53 -25 56 -108 4 -134 -35 -18 -44 -30 -36 -53 8 -25 34 -27 76 -6 92 48 92 202 0 250 -38 19 -70 19 -77 0z"/> <path d="M1381 1853 c-33 -47 -182 -253 -264 -364 -100 -137 -187 -262 -187 -270 0 -8 140 -204 177 -249 5 -6 41 41 109 141 30 45 60 86 65 93 48 54 197 276 226 336 33 68 37 83 37 160 1 71 -3 93 -23 130 -53 101 -82 106 -140 23z"/> <path d="M211 1861 c-56 -60 -68 -184 -27 -283 15 -38 106 -168 260 -371 130 -173 236 -320 236 -328 0 -8 -9 -25 -20 -39 -11 -14 -20 -29 -20 -33 0 -5 -10 -23 -23 -40 -12 -18 -27 -41 -33 -52 -13 -24 -65 -114 -80 -138 -10 -17 -13 -16 -60 7 -98 49 -209 43 -305 -17 -83 -51 -129 -141 -129 -251 0 -161 115 -283 275 -294 101 -6 173 22 243 96 56 58 79 97 133 227 46 112 101 203 164 274 l53 60 42 -45 c27 -29 69 -103 124 -217 86 -176 133 -250 197 -306 157 -136 405 -73 478 123 37 101 21 202 -46 290 -91 118 -275 147 -402 63 -30 -20 -42 -23 -49 -14 -5 7 -48 82 -96 167 -47 85 -123 202 -168 260 -45 58 -111 143 -146 190 -85 110 -251 326 -321 416 -31 40 -65 84 -76 100 -11 15 -35 46 -54 68 -19 23 -45 58 -59 79 -30 45 -54 47 -91 8z m653 -943 c20 -28 20 -33 0 -52 -42 -43 -109 10 -69 54 24 26 50 25 69 -2z m653 -434 c49 -20 87 -85 87 -149 -2 -135 -144 -209 -257 -134 -124 82 -89 265 58 299 33 8 64 4 112 -16z m-1126 -20 c47 -24 73 -71 77 -139 3 -50 0 -65 -20 -94 -34 -50 -71 -73 -125 -78 -99 -9 -173 53 -181 152 -11 135 126 223 249 159z"/></g></svg>`
236
+ editInProgramButton.addEventListener("click", () => {
237
+
238
+ if (window.userSettings.externalAudioEditor && window.userSettings.externalAudioEditor.length) {
239
+ const fileName = audio.children[0].src.split("file:///")[1].split("%20").join(" ")
240
+ const sp = spawn(window.userSettings.externalAudioEditor, [fileName], {'detached': true}, (err, data) => {
241
+ if (err) {
242
+ console.log(err)
243
+ console.log(err.message)
244
+ window.errorModal(err.message)
245
+ }
246
+ })
247
+
248
+ sp.on("error", err => {
249
+ if (err.message.includes("ENOENT")) {
250
+ window.errorModal(`${window.i18n.FOLLOWING_PATH_NOT_VALID}:<br><br> ${window.userSettings.externalAudioEditor}`)
251
+ } else {
252
+ window.errorModal(err.message)
253
+ }
254
+ })
255
+
256
+ } else {
257
+ window.errorModal(window.i18n.SPECIFY_EDIT_TOOL)
258
+ }
259
+ })
260
+
261
+
262
+ const deleteFileButton = createElem("div", {title: window.i18n.DELETE_FILE})
263
+ deleteFileButton.innerHTML = "&#10060;"
264
+ deleteFileButton.addEventListener("click", () => {
265
+ confirmModal(`${window.i18n.SURE_DELETE}<br><br><i>${fileName}</i>`).then(confirmation => {
266
+ if (confirmation) {
267
+ window.appLogger.log(`${newSample?window.i18n.DELETING_NEW_FILE:window.i18n.DELETING}: ${src}`)
268
+ if (fs.existsSync(src)) {
269
+ fs.unlinkSync(src)
270
+ }
271
+ sample.remove()
272
+ if (fs.existsSync(`${src}.json`)) {
273
+ fs.unlinkSync(`${src}.json`)
274
+ }
275
+ }
276
+ })
277
+ })
278
+ audioControls.appendChild(renameButton)
279
+ audioControls.appendChild(audioSVG)
280
+ audioControls.appendChild(editInProgramButton)
281
+ audioControls.appendChild(openFileLocationButton)
282
+ audioControls.appendChild(deleteFileButton)
283
+ sample.appendChild(audioControls)
284
+ return sample
285
+ }
286
+
287
+
288
+ window.refreshRecordsList = () => {
289
+ voiceSamples.innerHTML = ""
290
+ const outputFilesPaginationSize = window.userSettings.output_files_pagination_size
291
+
292
+
293
+ const filteredRecords = window.outputFilesState.records.filter(recordAndJson => {
294
+ if (!recordAndJson[0].fileName.toLowerCase().includes(voiceSamplesSearch.value.toLowerCase().trim())) {
295
+ return
296
+ }
297
+ if (voiceSamplesSearchPrompt.value.length) {
298
+ if (!recordAndJson[1] || !recordAndJson[1].inputSequence.toLowerCase().includes(voiceSamplesSearchPrompt.value.toLowerCase().trim())) {
299
+ return
300
+ }
301
+ }
302
+ return recordAndJson
303
+ })
304
+
305
+
306
+ const startIndex = (window.outputFilesState.paginationIndex*outputFilesPaginationSize)
307
+ const endIndex = Math.min(startIndex+outputFilesPaginationSize, filteredRecords.length)
308
+
309
+ for (let ri=startIndex; ri<endIndex; ri++) {
310
+ voiceSamples.appendChild(window.makeSample(filteredRecords[ri][0].jsonPath))
311
+ }
312
+ const numPages = Math.ceil(filteredRecords.length/outputFilesPaginationSize)
313
+ main_total_pages.innerHTML = window.i18n.PAGINATION_TOTAL_OF.replace("_1", numPages)
314
+ window.outputFilesState.totalPages = numPages
315
+ }
316
+ main_paginationPrev.addEventListener("click", () => {
317
+ main_pageNum.value = Math.max(1, parseInt(main_pageNum.value)-1)
318
+ window.outputFilesState.paginationIndex = main_pageNum.value-1
319
+ window.refreshRecordsList()
320
+ })
321
+ main_paginationNext.addEventListener("click", () => {
322
+ main_pageNum.value = Math.min(parseInt(main_pageNum.value)+1, window.outputFilesState.totalPages)
323
+ window.outputFilesState.paginationIndex = main_pageNum.value-1
324
+ window.refreshRecordsList()
325
+ })
326
+
327
+
328
+ // Delete all output files for a voice
329
+ voiceRecordsDeleteAllButton.addEventListener("click", () => {
330
+ if (window.currentModel) {
331
+ const outDir = window.userSettings[`outpath_${window.currentGame.gameId}`]+`/${currentModel.voiceId}`
332
+
333
+ const files = fs.readdirSync(outDir)
334
+ if (files.length) {
335
+ window.confirmModal(window.i18n.DELETE_ALL_FILES_CONFIRM.replace("_1", files.length).replace("_2", outDir)).then(resp => {
336
+ if (resp) {
337
+ window.deleteFolderRecursive(outDir, true)
338
+ window.initMainPagePagination(outDir)
339
+ window.refreshRecordsList()
340
+ }
341
+ })
342
+ } else {
343
+ window.errorModal(window.i18n.DELETE_ALL_FILES_ERR_NO_FILES.replace("_1", outDir))
344
+ }
345
+ }
346
+ })
347
+
348
+ voiceRecordsOrderByButton.addEventListener("click", () => {
349
+ window.userSettings.voiceRecordsOrderBy = window.userSettings.voiceRecordsOrderBy=="name" ? "time" : "name"
350
+ saveUserSettings()
351
+ const labels = {
352
+ "name": window.i18n.NAME,
353
+ "time": window.i18n.TIME
354
+ }
355
+ voiceRecordsOrderByButton.innerHTML = labels[window.userSettings.voiceRecordsOrderBy]
356
+ if (window.currentModel) {
357
+ const voiceRecordsList = window.userSettings[`outpath_${window.currentGame.gameId}`]+`/${window.currentModel.voiceId}`
358
+ window.reOrderMainPageRecords()
359
+ window.refreshRecordsList()
360
+ }
361
+ })
362
+ voiceRecordsOrderByOrderButton.addEventListener("click", () => {
363
+ window.userSettings.voiceRecordsOrderByOrder = window.userSettings.voiceRecordsOrderByOrder=="ascending" ? "descending" : "ascending"
364
+ saveUserSettings()
365
+ const labels = {
366
+ "ascending": window.i18n.ASCENDING,
367
+ "descending": window.i18n.DESCENDING
368
+ }
369
+ voiceRecordsOrderByOrderButton.innerHTML = labels[window.userSettings.voiceRecordsOrderByOrder]
370
+ if (window.currentModel) {
371
+ const voiceRecordsList = window.userSettings[`outpath_${window.currentGame.gameId}`]+`/${window.currentModel.voiceId}`
372
+ window.reOrderMainPageRecords()
373
+ window.refreshRecordsList()
374
+ }
375
+ })
376
+ voiceSamplesSearch.addEventListener("keyup", () => {
377
+ if (window.currentModel) {
378
+ window.outputFilesState.paginationIndex = 0
379
+ main_pageNum.value = 1
380
+ const voiceRecordsList = window.userSettings[`outpath_${window.currentGame.gameId}`]+`/${window.currentModel.voiceId}`
381
+ window.refreshRecordsList()
382
+ }
383
+ })
384
+ voiceSamplesSearchPrompt.addEventListener("keyup", () => {
385
+ if (window.currentModel) {
386
+ window.outputFilesState.paginationIndex = 0
387
+ main_pageNum.value = 1
388
+ const voiceRecordsList = window.userSettings[`outpath_${window.currentGame.gameId}`]+`/${window.currentModel.voiceId}`
389
+ window.refreshRecordsList()
390
+ }
391
+ })
392
+
393
+ if (Object.keys(window.userSettings).includes("voiceRecordsOrderBy")) {
394
+ const labels = {
395
+ "name": window.i18n.NAME,
396
+ "time": window.i18n.TIME
397
+ }
398
+ voiceRecordsOrderByButton.innerHTML = labels[window.userSettings.voiceRecordsOrderBy]
399
+ } else {
400
+ window.userSettings.voiceRecordsOrderBy = "name"
401
+ saveUserSettings()
402
+ }
403
+ if (Object.keys(window.userSettings).includes("voiceRecordsOrderByOrder")) {
404
+ const labels = {
405
+ "ascending": window.i18n.ASCENDING,
406
+ "descending": window.i18n.DESCENDING
407
+ }
408
+ voiceRecordsOrderByOrderButton.innerHTML = labels[window.userSettings.voiceRecordsOrderByOrder]
409
+ } else {
410
+ window.userSettings.voiceRecordsOrderByOrder = "ascending"
411
+ saveUserSettings()
412
+ }
javascript/plugins_manager.js ADDED
@@ -0,0 +1,594 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ const fs = require("fs")
4
+ const er = require('@electron/remote')
5
+
6
+ class PluginsManager {
7
+
8
+ constructor (path, appLogger, appVersion) {
9
+
10
+ this.path = `${__dirname.replace(/\\/g,"/").replace("/javascript", "")}/`.replace("/resources/app/resources/app", "/resources/app")
11
+ this.appVersion = appVersion
12
+ this.appLogger = appLogger
13
+ this.plugins = []
14
+ this.selectedPlugin = undefined
15
+ this.hasRunPostStartPlugins = false
16
+ this.changesToApply = {
17
+ ticked: [],
18
+ unticked: []
19
+ }
20
+ this.teardownModules = {}
21
+ this.resetModules()
22
+
23
+
24
+ this.scanPlugins()
25
+ this.savePlugins()
26
+ this.appLogger.log(`${this.path}/plugins`)
27
+ if (fs.existsSync(`${this.path}/plugins`)) {
28
+ fs.watch(`${this.path}/plugins`, {recursive: false, persistent: true}, (eventType, filename) => {
29
+ this.scanPlugins()
30
+ this.updateUI()
31
+ this.savePlugins()
32
+ })
33
+ }
34
+
35
+ plugins_moveUpBtn.addEventListener("click", () => {
36
+
37
+ if (!this.selectedPlugin || this.selectedPlugin[1]==0) return
38
+
39
+ const plugin = this.plugins.splice(this.selectedPlugin[1], 1)[0]
40
+ this.plugins.splice(this.selectedPlugin[1]-1, 0, plugin)
41
+ this.selectedPlugin[1] -= 1
42
+ this.updateUI()
43
+ plugins_applyBtn.disabled = false
44
+ })
45
+
46
+ plugins_moveDownBtn.addEventListener("click", () => {
47
+
48
+ if (!this.selectedPlugin || this.selectedPlugin[1]==this.plugins.length-1) return
49
+
50
+ const plugin = this.plugins.splice(this.selectedPlugin[1], 1)[0]
51
+ this.plugins.splice(this.selectedPlugin[1]+1, 0, plugin)
52
+ this.selectedPlugin[1] += 1
53
+ this.updateUI()
54
+ plugins_applyBtn.disabled = false
55
+ })
56
+
57
+ plugins_applyBtn.addEventListener("click", () => this.apply())
58
+ plugins_main.addEventListener("click", (e) => {
59
+ if (e.target == plugins_main) {
60
+ this.selectedPlugin = undefined
61
+ this.updateUI()
62
+ }
63
+ })
64
+
65
+ window.pluginsManager = this
66
+ this.loadModules()
67
+ }
68
+
69
+ resetModules () {
70
+ this.setupModules = new Set()
71
+ this.pluginsModules = {
72
+ "start": {
73
+ "pre": [],
74
+ "post": []
75
+ },
76
+ "keep-sample": {
77
+ "pre": [],
78
+ "mid": [],
79
+ "post": []
80
+ },
81
+ "batch-stop": {
82
+ "post": []
83
+ },
84
+ "generate-voice": {
85
+ "pre": []
86
+ }
87
+ }
88
+ pluginsCSS.innerHTML = ""
89
+ }
90
+
91
+ scanPlugins () {
92
+
93
+ const plugins = []
94
+
95
+ try {
96
+ const pluginIDs = fs.readdirSync(`${this.path}/plugins`)
97
+ pluginIDs.forEach(pluginId => {
98
+ try {
99
+ const pluginData = JSON.parse(fs.readFileSync(`${this.path}/plugins/${pluginId}/plugin.json`))
100
+
101
+ const minVersionOk = window.checkVersionRequirements(pluginData["min-app-version"], this.appVersion)
102
+ const maxVersionOk = window.checkVersionRequirements(pluginData["max-app-version"], this.appVersion, true)
103
+
104
+ plugins.push([pluginId, pluginData, false, minVersionOk, maxVersionOk])
105
+
106
+ } catch (e) {
107
+ this.appLogger.log(`${window.i18n.ERR_LOADING_PLUGIN} ${pluginId}: ${e}`)
108
+ }
109
+ })
110
+
111
+
112
+ } catch (e) {
113
+ console.log(e)
114
+ }
115
+
116
+ const orderedPlugins = []
117
+
118
+ // Order the found known plugins
119
+ window.userSettings.plugins.loadOrder.split(",").forEach(pluginId => {
120
+ for (let i=0; i<plugins.length; i++) {
121
+ if (pluginId.replace("*", "")==plugins[i][0]) {
122
+ plugins[i][2] = pluginId.includes("*") && plugins[i][3] && plugins[i][4]
123
+ orderedPlugins.push(plugins[i])
124
+ plugins.splice(i,1)
125
+ break
126
+ }
127
+ }
128
+ })
129
+
130
+ // Add any remaining (new) plugins at the bottom of the list
131
+ plugins.forEach(p => orderedPlugins.push(p))
132
+ this.plugins = orderedPlugins
133
+ }
134
+
135
+ updateUI () {
136
+
137
+ pluginsRecordsContainer.innerHTML = ""
138
+
139
+ this.plugins.forEach(([pluginId, pluginData, isEnabled, minVersionOk, maxVersionOk], pi) => {
140
+ const record = createElem("div")
141
+ const enabledCkbx = createElem("input", {type: "checkbox"})
142
+ enabledCkbx.checked = isEnabled
143
+ record.appendChild(createElem("div", enabledCkbx))
144
+ record.appendChild(createElem("div", `${pi}`))
145
+
146
+ const pluginNameElem = createElem("div", pluginData["plugin-name"])
147
+ pluginNameElem.title = pluginData["plugin-name"]
148
+ record.appendChild(pluginNameElem)
149
+
150
+ const pluginAuthorElem = createElem("div", pluginData["author"]||"")
151
+ pluginAuthorElem.title = pluginData["author"]||""
152
+ record.appendChild(pluginAuthorElem)
153
+
154
+ const endorseButtonContainer = createElem("div")
155
+ record.appendChild(endorseButtonContainer)
156
+ if (pluginData["nexus-link"] && window.nexusState.key) {
157
+
158
+ if (window.nexusState.key) {
159
+ window.nexus_getData(`${pluginData["nexus-link"].split(".com/")[1]}.json`).then(repoInfo => {
160
+ const endorseButton = createElem("button.smallButton", "Endorse")
161
+ const gameId = repoInfo.game_id
162
+ const nexusRepoId = repoInfo.mod_id
163
+
164
+ if (repoInfo.endorsement.endorse_status=="Endorsed") {
165
+ window.endorsedRepos.add(`plugin:${pluginId}`)
166
+ endorseButton.innerHTML = "Unendorse"
167
+ endorseButton.style.background = "none"
168
+ endorseButton.style.border = `2px solid #${window.currentGame ? currentGame.themeColourPrimary : "aaa"}`
169
+ } else {
170
+ endorseButton.style.setProperty("background-color", `#${window.currentGame ? currentGame.themeColourPrimary : "aaa"}`, "important")
171
+ }
172
+
173
+ endorseButtonContainer.appendChild(endorseButton)
174
+ endorseButton.addEventListener("click", async () => {
175
+ let response
176
+ if (window.endorsedRepos.has(`plugin:${pluginId}`)) {
177
+ response = await window.nexus_getData(`${gameId}/mods/${nexusRepoId}/abstain.json`, {
178
+ game_domain_name: gameId,
179
+ id: nexusRepoId,
180
+ version: repoInfo.version
181
+ }, "POST")
182
+ } else {
183
+ response = await window.nexus_getData(`${gameId}/mods/${nexusRepoId}/endorse.json`, {
184
+ game_domain_name: gameId,
185
+ id: nexusRepoId,
186
+ version: repoInfo.version
187
+ }, "POST")
188
+ }
189
+ if (response && response.message && response.status=="Error") {
190
+ if (response.message=="NOT_DOWNLOADED_MOD") {
191
+ response.message = "You need to first download something from this repo to be able to endorse it."
192
+ } else if (response.message=="TOO_SOON_AFTER_DOWNLOAD") {
193
+ response.message = "Nexus requires you to wait at least 15 mins (at the time of writing) before you can endorse."
194
+ } else if (response.message=="IS_OWN_MOD") {
195
+ response.message = "Nexus does not allow you to rate your own content."
196
+ }
197
+
198
+ window.errorModal(response.message)
199
+ } else {
200
+
201
+ if (window.endorsedRepos.has(`plugin:${pluginId}`)) {
202
+ window.endorsedRepos.delete(`plugin:${pluginId}`)
203
+ } else {
204
+ window.endorsedRepos.add(`plugin:${pluginId}`)
205
+ }
206
+ this.updateUI()
207
+ }
208
+ })
209
+ })
210
+ }
211
+ }
212
+
213
+
214
+ const hasBackendScript = !!Object.keys(pluginData["back-end-hooks"]).find(key => {
215
+ return (key=="custom-event" && pluginData["back-end-hooks"]["custom-event"]["file"]) ||
216
+ (pluginData["back-end-hooks"][key]["pre"] && pluginData["back-end-hooks"][key]["pre"]["file"]) ||
217
+ (pluginData["back-end-hooks"][key]["mid"] && pluginData["back-end-hooks"][key]["mid"]["file"]) ||
218
+ (pluginData["back-end-hooks"][key]["post"] && pluginData["back-end-hooks"][key]["post"]["file"])
219
+ })
220
+ const hasFrontendScript = !!pluginData["front-end-hooks"]
221
+ const type = hasFrontendScript && hasBackendScript ? "Both": (!hasFrontendScript && !hasBackendScript ? "None" : (hasFrontendScript ? "Front" : "Back"))
222
+
223
+ record.appendChild(createElem("div", pluginData["plugin-version"]))
224
+ record.appendChild(createElem("div", type))
225
+ // Min app version requirement
226
+ const minAppVersionElem = createElem("div", pluginData["min-app-version"])
227
+ record.appendChild(minAppVersionElem)
228
+ if (pluginData["min-app-version"] && !minVersionOk) {
229
+ minAppVersionElem.style.color = "red"
230
+ enabledCkbx.checked = false
231
+ enabledCkbx.disabled = true
232
+ }
233
+
234
+ // Max app version requirement
235
+ const maxAppVersionElem = createElem("div", pluginData["max-app-version"])
236
+ record.appendChild(maxAppVersionElem)
237
+ if (pluginData["max-app-version"] && !maxVersionOk) {
238
+ maxAppVersionElem.style.color = "red"
239
+ enabledCkbx.checked = false
240
+ enabledCkbx.disabled = true
241
+ }
242
+
243
+ const shortDescriptionElem = createElem("div", pluginData["plugin-short-description"])
244
+ shortDescriptionElem.title = pluginData["plugin-short-description"]
245
+ record.appendChild(shortDescriptionElem)
246
+
247
+ const pluginIdElem = createElem("div", pluginId)
248
+ pluginIdElem.title = pluginId
249
+ record.appendChild(pluginIdElem)
250
+
251
+ pluginsRecordsContainer.appendChild(record)
252
+
253
+ enabledCkbx.addEventListener("click", () => {
254
+ this.plugins[pi][2] = enabledCkbx.checked
255
+ plugins_applyBtn.disabled = false
256
+ })
257
+
258
+ record.addEventListener("click", (e) => {
259
+
260
+ if (e.target==enabledCkbx || e.target.nodeName=="BUTTON") {
261
+ return
262
+ }
263
+
264
+ if (this.selectedPlugin) {
265
+ this.selectedPlugin[0].style.background = "none"
266
+ Array.from(this.selectedPlugin[0].children).forEach(child => child.style.color = "white")
267
+ }
268
+
269
+ this.selectedPlugin = [record, pi, pluginData]
270
+ this.selectedPlugin[0].style.background = "white"
271
+ Array.from(this.selectedPlugin[0].children).forEach(child => child.style.color = "black")
272
+
273
+ plugins_moveUpBtn.disabled = false
274
+ plugins_moveDownBtn.disabled = false
275
+
276
+ })
277
+ if (this.selectedPlugin && pi==this.selectedPlugin[1]) {
278
+ this.selectedPlugin = [record, pi, pluginData]
279
+ this.selectedPlugin[0].style.background = "white"
280
+ Array.from(this.selectedPlugin[0].children).forEach(child => child.style.color = "black")
281
+ }
282
+
283
+ })
284
+ }
285
+
286
+ savePlugins () {
287
+ window.userSettings.plugins.loadOrder = this.plugins.map(([pluginId, pluginData, isEnabled]) => `${isEnabled?"*":""}${pluginId}`).join(",")
288
+ saveUserSettings()
289
+ fs.writeFileSync(`./plugins.txt`, window.userSettings.plugins.loadOrder.replace(/,/g, "\n"))
290
+ }
291
+
292
+ apply () {
293
+
294
+ const enabledPlugins = this.plugins.filter(([pluginId, pluginData, isEnabled]) => isEnabled).map(([pluginId, pluginData, isEnabled]) => pluginId)
295
+ const newPlugins = enabledPlugins.filter(pluginId => !window.userSettings.plugins.loadOrder.includes(`*${pluginId}`))
296
+ const removedPlugins = window.userSettings.plugins.loadOrder.split(",").filter(pluginId => pluginId.startsWith("*") && !enabledPlugins.includes(pluginId.slice(1, 100000)) ).map(pluginId => pluginId.slice(1, 100000))
297
+
298
+ removedPlugins.forEach(pluginId => {
299
+ if (this.teardownModules[pluginId]) {
300
+ this.teardownModules[pluginId].forEach(func => func())
301
+ }
302
+ })
303
+
304
+ const pluginLoadStatus = this.loadModules()
305
+ if (pluginLoadStatus) {
306
+ window.errorModal(`${window.i18n.FAILED_INIT_FOLLOWING} ${window.i18n.PLUGIN.toLowerCase()}: ${pluginLoadStatus}`)
307
+ return
308
+ }
309
+ this.savePlugins()
310
+ this.resetModules()
311
+ plugins_applyBtn.disabled = true
312
+
313
+ doFetch(`http://localhost:8008/refreshPlugins`, {
314
+ method: "Post",
315
+ body: "{}"
316
+ }).then(r=>r.text()).then(status => {
317
+
318
+ const plugins = status.split(",")
319
+ const successful = plugins.filter(p => p=="OK")
320
+ const failed = plugins.filter(p => p!="OK")
321
+
322
+ let message = `${window.i18n.SUCCESSFULLY_INITIALIZED} ${successful.length} ${successful.length>1||successful.length==0?window.i18n.PLUGINS:window.i18n.PLUGIN}.`
323
+ if (failed.length) {
324
+ if (successful.length==0) {
325
+ message = ""
326
+ }
327
+ message += ` ${window.i18n.FAILED_INIT_FOLLOWING} ${failed.length>1?window.i18n.PLUGINS:window.i18n.PLUGIN}: <br>${failed.join("<br>")} <br><br>${window.i18n.CHECK_SERVERLOG}`
328
+ }
329
+
330
+ if (!status.length || successful.length==0 && failed.length==0) {
331
+ message = window.i18n.SUCC_NO_ACTIVE_PLUGINS
332
+ }
333
+
334
+ const restartRequired = newPlugins.map(newPluginId => this.plugins.find(([pluginId, pluginData, isEnabled]) => pluginId==newPluginId))
335
+ .filter(([pluginId, pluginData, isEnabled]) => !!pluginData["install-requires-restart"]).length +
336
+ removedPlugins.map(removedPluginId => this.plugins.find(([pluginId, pluginData, isEnabled]) => pluginId==removedPluginId))
337
+ .filter(([pluginId, pluginData, isEnabled]) => !!pluginData["uninstall-requires-restart"]).length
338
+ if (restartRequired) {
339
+ message += `<br><br> ${window.i18n.APP_RESTART_NEEDED}`
340
+ }
341
+
342
+ // Don't use window.errorModal, otherwise you get the error sound
343
+ createModal("error", message)
344
+ })
345
+ }
346
+
347
+
348
+ loadModules () {
349
+ for (let pi=0; pi<this.plugins.length; pi++) {
350
+ const [pluginId, pluginData, enabled] = this.plugins[pi]
351
+ if (!enabled) continue
352
+ let failed
353
+
354
+ failed = this.loadModuleFns(pluginId, pluginData, "start", "pre")
355
+ if (failed) return `${pluginId}->start->pre<br><br>${failed}`
356
+
357
+ failed = this.loadModuleFns(pluginId, pluginData, "start", "post")
358
+ if (failed) return `${pluginId}->start->post<br><br>${failed}`
359
+
360
+ this.loadModuleFns(pluginId, pluginData, "keep-sample", "pre")
361
+ if (failed) return `${pluginId}->keep-sample->pre<br><br>${failed}`
362
+
363
+ this.loadModuleFns(pluginId, pluginData, "keep-sample", "mid")
364
+ if (failed) return `${pluginId}->keep-sample->mid<br><br>${failed}`
365
+
366
+ this.loadModuleFns(pluginId, pluginData, "keep-sample", "post")
367
+ if (failed) return `${pluginId}->keep-sample->post<br><br>${failed}`
368
+
369
+ this.loadModuleFns(pluginId, pluginData, "generate-voice", "pre")
370
+ if (failed) return `${pluginId}->generate-voice->pre<br><br>${failed}`
371
+
372
+ this.loadModuleFns(pluginId, pluginData, "batch-stop", "post")
373
+ if (failed) return `${pluginId}->batch-stop->post<br><br>${failed}`
374
+
375
+
376
+ if (Object.keys(pluginData).includes("front-end-style-files") && pluginData["front-end-style-files"].length) {
377
+ pluginData["front-end-style-files"].forEach(styleFile => {
378
+ try {
379
+ if (styleFile.endsWith(".css")) {
380
+ const styleData = fs.readFileSync(`${this.path}/plugins/${pluginId}/${styleFile}`)
381
+ pluginsCSS.innerHTML += styleData
382
+ }
383
+ } catch (e) {
384
+ window.appLogger.log(`${window.i18n.ERR_LOADING_CSS} ${pluginId}: ${e}`)
385
+ }
386
+ })
387
+ }
388
+ }
389
+ }
390
+
391
+ loadModuleFns (pluginId, pluginData, task, hookTime) {
392
+ try {
393
+ if (Object.keys(pluginData).includes("front-end-hooks") && Object.keys(pluginData["front-end-hooks"]).includes(task) && Object.keys(pluginData["front-end-hooks"][task]).includes(hookTime) ) {
394
+
395
+ const file = pluginData["front-end-hooks"][task][hookTime]["file"]
396
+ const functionName = pluginData["front-end-hooks"][task][hookTime]["function"]
397
+
398
+ if (!file.endsWith(".js")) {
399
+ window.appLogger.log(`[${window.i18n.PLUGIN}: ${pluginId}]: ${window.i18n.CANT_IMPORT_FILE_FOR_HOOK_TASK_ENTRYPOINT.replace("_1", file).replace("_2", hookTime).replace("_3", task)}: ${window.i18n.ONLY_JS}`)
400
+ return
401
+ }
402
+
403
+ if (file && functionName) {
404
+ const module = require(`${this.path}/plugins/${pluginId}/${file}`)
405
+
406
+ if (module.teardown) {
407
+ if (!Object.keys(this.teardownModules).includes(pluginId)) {
408
+ this.teardownModules[pluginId] = []
409
+ }
410
+ this.teardownModules[pluginId].push(module.teardown)
411
+ }
412
+
413
+ if (module.setup && !this.setupModules.has(`${pluginId}/${file}`)) {
414
+ window.appLogger.setPrefix(pluginId)
415
+ module.setup(window)
416
+ window.appLogger.setPrefix("")
417
+ this.setupModules.add(`${pluginId}/${file}`)
418
+ }
419
+
420
+ this.pluginsModules[task][hookTime].push([pluginId, module[functionName]])
421
+ }
422
+ }
423
+ } catch (e) {
424
+ console.log(`${window.i18n.ERR_LOADING_PLUGIN} ${pluginId}->${task}->${hookTime}: ` + e.stack)
425
+ window.appLogger.log(`${window.i18n.ERR_LOADING_PLUGIN} ${pluginId}->${task}->${hookTime}: ` + e)
426
+ return e.stack
427
+ }
428
+
429
+ }
430
+
431
+ runPlugins (pList, event, data) {
432
+ if (pList.length) {
433
+ console.log(`Running plugin for event: ${event}`)
434
+ }
435
+ pList.forEach(([pluginId, pluginFn]) => {
436
+ try {
437
+ window.appLogger.setPrefix(pluginId)
438
+ pluginFn(window, data)
439
+ window.appLogger.setPrefix("")
440
+
441
+ } catch (e) {
442
+ console.log(e, pluginFn)
443
+ window.appLogger.log(`[${window.i18n.PLUGIN_RUN_ERROR} "${event}": ${pluginId}]: ${e}`)
444
+ }
445
+ })
446
+ }
447
+
448
+
449
+ _saveINIFile (IniSettings, settingsKey, pluginId, filePath) {
450
+ const outputIni = []
451
+ settingsOptionsContainer.querySelectorAll(`.${pluginId}_plugin_setting>div>input, .${pluginId}_plugin_setting>div>select`).forEach(input => {
452
+
453
+ if (input.tagName=="SELECT") {
454
+ const select = input
455
+ const optionsList = Array.from(select.querySelectorAll("option")).map(option => {
456
+ return [option.innerHTML, option.value]
457
+ })
458
+ const optionsListString = `{${optionsList.map(kv => kv.join(":")).join(";")}}`
459
+
460
+ outputIni.push(`${select.name.toLowerCase()}=${select.value} # ${optionsListString} ${select.getAttribute("comment")!="undefined" ? select.getAttribute("comment") : ""}`)
461
+ IniSettings[select.name.toLowerCase()] = select.value
462
+
463
+ } else {
464
+ const value = input.type=="checkbox" ? (input.checked ? true : false) : input.value
465
+ outputIni.push(`${input.name.toLowerCase()}=${value}${input.getAttribute("comment")!="undefined" ? " # "+input.getAttribute("comment") : ""}`)
466
+ IniSettings[input.name.toLowerCase()] = value
467
+ }
468
+ })
469
+
470
+ fs.writeFileSync(filePath, outputIni.join("\n"), "utf8")
471
+ window.pluginsContext[settingsKey] = IniSettings
472
+ }
473
+
474
+ registerINIFile (pluginId, settingsKey, filePath) {
475
+
476
+ if (!pluginId || !settingsKey || !filePath) {
477
+ return window.appLogger.log(`You must provide the following to register an ini file: pluginId, settingsKey, filePath`)
478
+ }
479
+
480
+ if (fs.existsSync(filePath)) {
481
+
482
+ if (document.querySelectorAll(`.${pluginId}_plugin_setting`).length) {
483
+ return
484
+ }
485
+
486
+ const IniSettings = {}
487
+ const iniFileData = fs.readFileSync(filePath, "utf8").split("\n")
488
+
489
+ const hr = createElem(`hr.${pluginId}_plugin_setting`)
490
+ settingsOptionsContainer.appendChild(hr)
491
+ settingsOptionsContainer.appendChild(createElem(`div.centeredSettingsSectionPlugins.${pluginId}_plugin_setting`, createElem("div", window.i18n.SETTINGS_FOR_PLUGIN.replace("_1", pluginId)) ))
492
+
493
+ iniFileData.forEach(keyVal => {
494
+ if (!keyVal.trim().length) {
495
+ return
496
+ }
497
+ let comment = keyVal.includes("#") ? keyVal.split("#")[1].trim() : undefined
498
+ keyVal = keyVal.split("#")[0].trim()
499
+ const key = keyVal.split("=")[0].trim()
500
+ let val = keyVal.split("=")[1].trim()
501
+ if (val=="false") val = false
502
+ if (val=="true") val = true
503
+ IniSettings[key.toLowerCase()] = val
504
+
505
+ const labelText = key[0].toUpperCase() + key.substring(1)
506
+ let label, input
507
+ const extraElems = []
508
+
509
+ if (comment && (comment.includes("$filepicker") || comment.includes("$folderpicker"))) {
510
+
511
+ input = createElem("input", {name: key, comment: comment})
512
+ input.style.width = "80%"
513
+ input.value = val
514
+ const button = createElem("button.svgButton")
515
+ button.innerHTML = `<svg class="openFolderSVG" width="400" height="350" viewBox="0, 0, 400,350"><g id="svgg" ><path id="path0" d="M39.960 53.003 C 36.442 53.516,35.992 53.635,30.800 55.422 C 15.784 60.591,3.913 74.835,0.636 91.617 C -0.372 96.776,-0.146 305.978,0.872 310.000 C 5.229 327.228,16.605 339.940,32.351 345.172 C 40.175 347.773,32.175 347.630,163.000 347.498 L 281.800 347.378 285.600 346.495 C 304.672 342.065,321.061 332.312,330.218 319.944 C 330.648 319.362,332.162 317.472,333.581 315.744 C 335.001 314.015,336.299 312.420,336.467 312.200 C 336.634 311.980,337.543 310.879,338.486 309.753 C 340.489 307.360,342.127 305.341,343.800 303.201 C 344.460 302.356,346.890 299.375,349.200 296.575 C 351.510 293.776,353.940 290.806,354.600 289.975 C 355.260 289.144,356.561 287.505,357.492 286.332 C 358.422 285.160,359.952 283.267,360.892 282.126 C 362.517 280.153,371.130 269.561,375.632 264.000 C 376.789 262.570,380.427 258.097,383.715 254.059 C 393.790 241.689,396.099 237.993,398.474 230.445 C 403.970 212.972,394.149 194.684,376.212 188.991 C 369.142 186.747,368.803 186.724,344.733 186.779 C 330.095 186.812,322.380 186.691,322.216 186.425 C 322.078 186.203,321.971 178.951,321.977 170.310 C 321.995 146.255,321.401 141.613,317.200 133.000 C 314.009 126.457,307.690 118.680,303.142 115.694 C 302.560 115.313,301.300 114.438,300.342 113.752 C 295.986 110.631,288.986 107.881,282.402 106.704 C 280.540 106.371,262.906 106.176,220.400 106.019 L 161.000 105.800 160.763 98.800 C 159.961 75.055,143.463 56.235,120.600 52.984 C 115.148 52.208,45.292 52.225,39.960 53.003 M120.348 80.330 C 130.472 83.988,133.993 90.369,133.998 105.071 C 134.003 120.968,137.334 127.726,147.110 131.675 L 149.400 132.600 213.800 132.807 C 272.726 132.996,278.392 133.071,280.453 133.690 C 286.872 135.615,292.306 141.010,294.261 147.400 C 294.928 149.578,294.996 151.483,294.998 168.000 L 295.000 186.200 292.800 186.449 C 291.590 186.585,254.330 186.725,210.000 186.759 C 163.866 186.795,128.374 186.977,127.000 187.186 C 115.800 188.887,104.936 192.929,96.705 198.458 C 95.442 199.306,94.302 200.000,94.171 200.000 C 93.815 200.000,89.287 203.526,87.000 205.583 C 84.269 208.039,80.083 212.649,76.488 217.159 C 72.902 221.657,72.598 222.031,70.800 224.169 C 70.030 225.084,68.770 226.620,68.000 227.582 C 67.230 228.544,66.054 229.977,65.387 230.766 C 64.720 231.554,62.727 234.000,60.957 236.200 C 59.188 238.400,56.346 241.910,54.642 244.000 C 52.938 246.090,50.163 249.510,48.476 251.600 C 44.000 257.146,36.689 266.126,36.212 266.665 C 35.985 266.921,34.900 268.252,33.800 269.623 C 32.700 270.994,30.947 273.125,29.904 274.358 C 28.861 275.591,28.006 276.735,28.004 276.900 C 28.002 277.065,27.728 277.200,27.395 277.200 C 26.428 277.200,26.700 96.271,27.670 93.553 C 30.020 86.972,35.122 81.823,40.800 80.300 C 44.238 79.378,47.793 79.296,81.800 79.351 L 117.800 79.410 120.348 80.330 M369.400 214.800 C 374.239 217.220,374.273 222.468,369.489 228.785 C 367.767 231.059,364.761 234.844,364.394 235.200 C 364.281 235.310,362.373 237.650,360.154 240.400 C 357.936 243.150,354.248 247.707,351.960 250.526 C 347.732 255.736,346.053 257.821,343.202 261.400 C 341.505 263.530,340.849 264.336,334.600 271.965 C 332.400 274.651,330.204 277.390,329.720 278.053 C 329.236 278.716,328.246 279.945,327.520 280.785 C 326.794 281.624,325.300 283.429,324.200 284.794 C 323.100 286.160,321.726 287.845,321.147 288.538 C 320.568 289.232,318.858 291.345,317.347 293.233 C 308.372 304.449,306.512 306.609,303.703 309.081 C 299.300 312.956,290.855 317.633,286.000 318.886 C 277.958 320.960,287.753 320.819,159.845 320.699 C 33.557 320.581,42.330 320.726,38.536 318.694 C 34.021 316.276,35.345 310.414,42.386 301.647 C 44.044 299.583,45.940 297.210,46.600 296.374 C 47.260 295.538,48.340 294.169,49.000 293.332 C 49.660 292.495,51.550 290.171,53.200 288.167 C 54.850 286.164,57.100 283.395,58.200 282.015 C 59.300 280.635,60.920 278.632,61.800 277.564 C 62.680 276.496,64.210 274.617,65.200 273.389 C 66.190 272.162,67.188 270.942,67.418 270.678 C 67.649 270.415,71.591 265.520,76.179 259.800 C 80.767 254.080,84.634 249.310,84.773 249.200 C 84.913 249.090,87.117 246.390,89.673 243.200 C 92.228 240.010,95.621 235.780,97.213 233.800 C 106.328 222.459,116.884 215.713,128.200 213.998 C 129.300 213.832,183.570 213.719,248.800 213.748 L 367.400 213.800 369.400 214.800 " stroke="none" fill="#fbfbfb" fill-rule="evenodd"></path><path id="path1" fill-opacity="0" d="M0.000 46.800 C 0.000 72.540,0.072 93.600,0.159 93.600 C 0.246 93.600,0.516 92.460,0.759 91.066 C 3.484 75.417,16.060 60.496,30.800 55.422 C 35.953 53.648,36.338 53.550,40.317 52.981 C 46.066 52.159,114.817 52.161,120.600 52.984 C 143.463 56.235,159.961 75.055,160.763 98.800 L 161.000 105.800 220.400 106.019 C 262.906 106.176,280.540 106.371,282.402 106.704 C 288.986 107.881,295.986 110.631,300.342 113.752 C 301.300 114.438,302.560 115.313,303.142 115.694 C 307.690 118.680,314.009 126.457,317.200 133.000 C 321.401 141.613,321.995 146.255,321.977 170.310 C 321.971 178.951,322.078 186.203,322.216 186.425 C 322.380 186.691,330.095 186.812,344.733 186.779 C 368.803 186.724,369.142 186.747,376.212 188.991 C 381.954 190.814,388.211 194.832,391.662 198.914 C 395.916 203.945,397.373 206.765,399.354 213.800 C 399.842 215.533,399.922 201.399,399.958 107.900 L 400.000 0.000 200.000 0.000 L 0.000 0.000 0.000 46.800 M44.000 79.609 C 35.903 81.030,30.492 85.651,27.670 93.553 C 26.700 96.271,26.428 277.200,27.395 277.200 C 27.728 277.200,28.002 277.065,28.004 276.900 C 28.006 276.735,28.861 275.591,29.904 274.358 C 30.947 273.125,32.700 270.994,33.800 269.623 C 34.900 268.252,35.985 266.921,36.212 266.665 C 36.689 266.126,44.000 257.146,48.476 251.600 C 50.163 249.510,52.938 246.090,54.642 244.000 C 56.346 241.910,59.188 238.400,60.957 236.200 C 62.727 234.000,64.720 231.554,65.387 230.766 C 66.054 229.977,67.230 228.544,68.000 227.582 C 68.770 226.620,70.030 225.084,70.800 224.169 C 72.598 222.031,72.902 221.657,76.488 217.159 C 80.083 212.649,84.269 208.039,87.000 205.583 C 89.287 203.526,93.815 200.000,94.171 200.000 C 94.302 200.000,95.442 199.306,96.705 198.458 C 104.936 192.929,115.800 188.887,127.000 187.186 C 128.374 186.977,163.866 186.795,210.000 186.759 C 254.330 186.725,291.590 186.585,292.800 186.449 L 295.000 186.200 294.998 168.000 C 294.996 151.483,294.928 149.578,294.261 147.400 C 292.306 141.010,286.872 135.615,280.453 133.690 C 278.392 133.071,272.726 132.996,213.800 132.807 L 149.400 132.600 147.110 131.675 C 137.334 127.726,134.003 120.968,133.998 105.071 C 133.993 90.369,130.472 83.988,120.348 80.330 L 117.800 79.410 81.800 79.351 C 62.000 79.319,44.990 79.435,44.000 79.609 M128.200 213.998 C 116.884 215.713,106.328 222.459,97.213 233.800 C 95.621 235.780,92.228 240.010,89.673 243.200 C 87.117 246.390,84.913 249.090,84.773 249.200 C 84.634 249.310,80.767 254.080,76.179 259.800 C 71.591 265.520,67.649 270.415,67.418 270.678 C 67.188 270.942,66.190 272.162,65.200 273.389 C 64.210 274.617,62.680 276.496,61.800 277.564 C 60.920 278.632,59.300 280.635,58.200 282.015 C 57.100 283.395,54.850 286.164,53.200 288.167 C 51.550 290.171,49.660 292.495,49.000 293.332 C 48.340 294.169,47.260 295.538,46.600 296.374 C 45.940 297.210,44.044 299.583,42.386 301.647 C 35.345 310.414,34.021 316.276,38.536 318.694 C 42.330 320.726,33.557 320.581,159.845 320.699 C 287.753 320.819,277.958 320.960,286.000 318.886 C 290.855 317.633,299.300 312.956,303.703 309.081 C 306.512 306.609,308.372 304.449,317.347 293.233 C 318.858 291.345,320.568 289.232,321.147 288.538 C 321.726 287.845,323.100 286.160,324.200 284.794 C 325.300 283.429,326.794 281.624,327.520 280.785 C 328.246 279.945,329.236 278.716,329.720 278.053 C 330.204 277.390,332.400 274.651,334.600 271.965 C 340.849 264.336,341.505 263.530,343.202 261.400 C 346.053 257.821,347.732 255.736,351.960 250.526 C 354.248 247.707,357.936 243.150,360.154 240.400 C 362.373 237.650,364.281 235.310,364.394 235.200 C 364.761 234.844,367.767 231.059,369.489 228.785 C 374.273 222.468,374.239 217.220,369.400 214.800 L 367.400 213.800 248.800 213.748 C 183.570 213.719,129.300 213.832,128.200 213.998 M399.600 225.751 C 399.600 231.796,394.623 240.665,383.715 254.059 C 380.427 258.097,376.789 262.570,375.632 264.000 C 371.130 269.561,362.517 280.153,360.892 282.126 C 359.952 283.267,358.422 285.160,357.492 286.332 C 356.561 287.505,355.260 289.144,354.600 289.975 C 353.940 290.806,351.510 293.776,349.200 296.575 C 346.890 299.375,344.460 302.356,343.800 303.201 C 342.127 305.341,340.489 307.360,338.486 309.753 C 337.543 310.879,336.634 311.980,336.467 312.200 C 336.299 312.420,335.001 314.015,333.581 315.744 C 332.162 317.472,330.648 319.362,330.218 319.944 C 321.061 332.312,304.672 342.065,285.600 346.495 L 281.800 347.378 163.000 347.498 C 32.175 347.630,40.175 347.773,32.351 345.172 C 16.471 339.895,3.810 325.502,0.820 309.326 C 0.591 308.085,0.312 306.979,0.202 306.868 C 0.091 306.757,-0.000 327.667,-0.000 353.333 L 0.000 400.000 200.000 400.000 L 400.000 400.000 400.000 312.400 C 400.000 264.220,399.910 224.800,399.800 224.800 C 399.690 224.800,399.600 225.228,399.600 225.751 " stroke="none" fill="#050505" fill-rule="evenodd"></path></g></svg>`
516
+
517
+ const openType = comment.includes("$filepicker") ? "openFile" : "openDirectory"
518
+ comment = comment.replace("$filepicker", "").replace("$folderpicker", "")
519
+
520
+ button.addEventListener("click", () => {
521
+ let filePathInput = er.dialog.showOpenDialog({ properties: [openType]})
522
+ if (filePathInput) {
523
+ filePathInput = filePathInput[0].replace(/\\/g, "/")
524
+ input.value = filePathInput.replace(/\\/g, "/")
525
+
526
+ this._saveINIFile(IniSettings, settingsKey, pluginId, filePath)
527
+ }
528
+ })
529
+ extraElems.push(button)
530
+
531
+
532
+
533
+ } else if (comment && comment.includes("{") && comment.includes(":")) {
534
+
535
+ const optionsList = comment.split("{")[1].split("}")[0].split(";").map(kv => {
536
+ return [kv.split(":")[0], kv.split(":")[1]]
537
+ })
538
+ const optionElems = optionsList.map(data => {
539
+ const opt = createElem("option", {value: data[1]})
540
+ opt.innerHTML = data[0]
541
+ return opt
542
+ })
543
+
544
+ comment = comment.split("}").reverse()[0].trim()
545
+
546
+ input = createElem("select", {name: key, comment: comment})
547
+ optionElems.forEach(option => {
548
+ input.appendChild(option)
549
+ })
550
+ input.value = val
551
+
552
+ } else {
553
+ const inputType = [true,false].includes(val) ? "checkbox" : "text"
554
+ input = createElem("input", {
555
+ type: inputType, name: key, comment: comment
556
+ })
557
+ if (inputType=="checkbox") {
558
+ input.checked = val
559
+ } else {
560
+ input.value = val
561
+ }
562
+ }
563
+
564
+ label = createElem("div", labelText.replace(/_/g, " ") + (comment ? `<br>(${comment})` : ""))
565
+
566
+
567
+ input.addEventListener("change", () => {
568
+ this._saveINIFile(IniSettings, settingsKey, pluginId, filePath)
569
+ })
570
+
571
+ const rhd_elem = createElem("div")
572
+ rhd_elem.appendChild(input)
573
+ extraElems.forEach(elem => rhd_elem.appendChild(elem))
574
+ if (extraElems.length) {
575
+ rhd_elem.style.flexDirection = "row"
576
+ }
577
+
578
+ settingsOptionsContainer.appendChild(createElem(`div.${pluginId}_plugin_setting`, [label, rhd_elem]))
579
+ })
580
+
581
+ window.pluginsContext[settingsKey] = IniSettings
582
+
583
+
584
+
585
+ } else {
586
+ window.appLogger.log(`Ini file does not exist here: ${filePath}`)
587
+ }
588
+
589
+ }
590
+
591
+ }
592
+
593
+
594
+ exports.PluginsManager = PluginsManager
javascript/script.js ADDED
@@ -0,0 +1,1730 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+ window.appVersion = "v3.0.3"
3
+
4
+ window.PRODUCTION = module.filename.includes("resources")
5
+ const path = window.PRODUCTION ? "./resources/app" : "."
6
+ window.path = path
7
+
8
+ const fs = require("fs")
9
+ const zipdir = require('zip-dir')
10
+ const {shell, ipcRenderer, clipboard} = require("electron")
11
+ const doFetch = require("node-fetch")
12
+ const {xVAAppLogger} = require("./javascript/appLogger.js")
13
+ window.appLogger = new xVAAppLogger(`./app.log`, window.appVersion)
14
+ process.on(`uncaughtException`, (data, origin) => {window.appLogger.log(`uncaughtException: ${data}`);window.appLogger.log(`uncaughtException: ${origin}`)})
15
+ window.onerror = (event, source, lineno, colno, error) => {window.appLogger.log(`onerror: ${error.stack}`)}
16
+ require("./javascript/i18n.js")
17
+ require("./javascript/util.js")
18
+ require("./javascript/nexus.js")
19
+ require("./javascript/dragdrop_model_install.js")
20
+ require("./javascript/embeddings.js")
21
+ require("./javascript/totd.js")
22
+ require("./javascript/arpabet.js")
23
+ require("./javascript/style_embeddings.js")
24
+ const {Editor} = require("./javascript/editor.js")
25
+ require("./javascript/textarea.js")
26
+ const {saveUserSettings, deleteFolderRecursive} = require("./javascript/settingsMenu.js")
27
+ const xVASpeech = require("./javascript/speech2speech.js")
28
+ require("./javascript/batch.js")
29
+ require("./javascript/outputFiles.js")
30
+ require("./javascript/workbench.js")
31
+ const er = require('@electron/remote')
32
+ window.electronBrowserWindow = er.getCurrentWindow()
33
+ const child = require("child_process").execFile
34
+ const spawn = require("child_process").spawn
35
+
36
+ // Newly introduced in v3. I will slowly start moving global context variables into this, and update code throughout to reference this
37
+ // instead of old variables such as window.games, window.currentModel, etc.
38
+ window.appState = {}
39
+
40
+
41
+ // Start the server
42
+ if (window.PRODUCTION) {
43
+ window.pythonProcess = spawn(`${path}/cpython_${window.userSettings.installation}/server.exe`, {stdio: "ignore"})
44
+ }
45
+
46
+ const {PluginsManager} = require("./javascript/plugins_manager.js")
47
+ window.pluginsContext = {}
48
+ window.pluginsManager = new PluginsManager(window.path, window.appLogger, window.appVersion)
49
+ window.pluginsManager.runPlugins(window.pluginsManager.pluginsModules["start"]["pre"], event="pre start")
50
+
51
+
52
+ let themeColour
53
+ let secondaryThemeColour
54
+ const oldCError = console.error
55
+ console.error = (...rest) => {
56
+ window.appLogger.log(`console.error: ${rest}`)
57
+ oldCError(rest)
58
+ }
59
+
60
+ window.addEventListener("error", function (e) {window.appLogger.log(`error: ${e.error.stack}`)})
61
+ window.addEventListener('unhandledrejection', function (e) {window.appLogger.log(`unhandledrejection: ${e.stack}`)})
62
+
63
+
64
+ setTimeout(() => {
65
+ window.electron = require("electron")
66
+ }, 1000)
67
+
68
+
69
+ window.games = {}
70
+ window.models = {}
71
+ window.sequenceEditor = new Editor()
72
+ window.currentModel = undefined
73
+ window.currentModelButton = undefined
74
+ window.watchedModelsDirs = []
75
+
76
+ window.appLogger.log(`Settings: ${JSON.stringify(window.userSettings)}`)
77
+
78
+ // Set up folders
79
+ try {fs.mkdirSync(`${path}/models`)} catch (e) {/*Do nothing*/}
80
+ try {fs.mkdirSync(`${path}/output`)} catch (e) {/*Do nothing*/}
81
+ try {fs.mkdirSync(`${path}/assets`)} catch (e) {/*Do nothing*/}
82
+
83
+ // Clean up temp files
84
+ const clearOldTempFiles = () => {
85
+ fs.readdir(`${__dirname.replace("/javascript", "")}/output`, (err, files) => {
86
+ if (err) {
87
+ window.appLogger.log(`Error cleaning up temp files: ${err}`)
88
+ }
89
+ if (files && files.length) {
90
+ files.filter(f => f.startsWith("temp-")).forEach(file => {
91
+ fs.unlink(`${__dirname.replace("/javascript", "")}/output/${file}`, err => err&&console.log(err))
92
+ })
93
+ }
94
+ })
95
+ }
96
+ clearOldTempFiles()
97
+
98
+ let fileRenameCounter = 0
99
+ let fileChangeCounter = 0
100
+ window.isGenerating = false
101
+
102
+
103
+
104
+
105
+ window.registerModel = (modelsPath, gameFolder, model, {gameId, voiceId, voiceName, voiceDescription, gender, variant, modelType, emb_i}) => {
106
+ // Add game, if the game hasn't been added yet
107
+ let audioPreviewPath
108
+ try {
109
+ audioPreviewPath = `${modelsPath}/${model.games.find(({gameId}) => gameId==gameFolder).voiceId}`
110
+ } catch (e) {}
111
+ if (!window.games.hasOwnProperty(gameId)) {
112
+
113
+ const gameAsset = fs.readdirSync(`${path}/assets`).find(f => f==gameId+".json")
114
+ if (gameAsset && gameAsset.length && gameAsset[0]) {
115
+ const gameTheme = JSON.parse(fs.readFileSync(`${path}/assets/${gameAsset}`))
116
+
117
+ window.games[gameId] = {
118
+ models: [],
119
+ gameTheme,
120
+ gameAsset
121
+ }
122
+
123
+ } else {
124
+ window.appLogger.log(`Something not right with loading model: ${voiceId} . The asset file for its game (${gameId}) could not be found here: ${path}/assets. You need a ${gameId}.json file. Loading a generic theme for this voice's game/category.`)
125
+
126
+ const dummyGameTheme = {
127
+ "gameName": gameId,
128
+ "assetFile": "other.jpg",
129
+ "themeColourPrimary": "aaaaaa",
130
+ "themeColourSecondary": null,
131
+ "gameCode": "x",
132
+ "nexusGamePageIDs": []
133
+ }
134
+ const dummyGameAsset = "other.json"
135
+ window.games[gameId] = {
136
+ models: [],
137
+ dummyGameTheme,
138
+ dummyGameAsset
139
+ }
140
+ audioPreviewPath = `${modelsPath}/${model.games[0].voiceId}`
141
+ }
142
+ }
143
+
144
+ // Catch duplicates, for when/if a model is registered for multiple games, but there's already the same model in that game, from another version
145
+ // const existingDuplicates = []
146
+ // window.games[gameId].models.forEach((item,i) => {
147
+ // if (item.voiceId==voiceId) {
148
+ // existingDuplicates.push([item, i])
149
+ // }
150
+ // })
151
+
152
+ // Check if a variant has already been added for this voice name, for this game
153
+ let foundVariantIndex = undefined
154
+ window.games[gameId].models.forEach((item,i) => {
155
+ if (foundVariantIndex!=undefined) return
156
+
157
+ if (item.voiceName.toLowerCase().trim()==voiceName.toLowerCase().trim()) {
158
+ foundVariantIndex = i
159
+ }
160
+ })
161
+
162
+ // Add the initial model metadata, if no existing variant has been added (will happen most of the time)
163
+ if (!foundVariantIndex) {
164
+ const modelData = {
165
+ gameId,
166
+ modelsPath,
167
+ voiceName,
168
+ lang_capabilities: model.lang_capabilities,
169
+ embOverABaseModel: model.embOverABaseModel,
170
+ variants: []
171
+ }
172
+ window.games[gameId].models.push(modelData)
173
+ foundVariantIndex = window.games[gameId].models.length-1
174
+ }
175
+
176
+ const variantData = {
177
+ author: model.author,
178
+ version: model.version,
179
+ modelVersion: model.modelVersion,
180
+ modelType: model.modelType,
181
+ base_speaker_emb: model.modelType=="xVAPitch" ? model.games[0].base_speaker_emb : undefined,
182
+ voiceId,
183
+ audioPreviewPath,
184
+ hifi: undefined,
185
+ num_speakers: model.emb_size,
186
+ emb_i,
187
+ variantName: variant ? variant.replace("Default :", "Default:").replace("Default:", "").trim() : "Default",
188
+ voiceDescription,
189
+ lang: model.lang,
190
+ gender,
191
+ modelType: modelType||model.modelType,
192
+ model,
193
+ }
194
+ const potentialHiFiPath = `${modelsPath}/${voiceId}.hg.pt`
195
+ if (fs.existsSync(potentialHiFiPath)) {
196
+ variantData.hifi = potentialHiFiPath
197
+ }
198
+
199
+ const isDefaultVariant = !variant || variant.toLowerCase().startsWith("default")
200
+
201
+ if (isDefaultVariant) {
202
+ // Place first in the list, if it's default
203
+ window.games[gameId].models[foundVariantIndex].audioPreviewPath = audioPreviewPath
204
+ window.games[gameId].models[foundVariantIndex].variants.splice(0,0,variantData)
205
+ } else {
206
+ window.games[gameId].models[foundVariantIndex].variants.push(variantData)
207
+ }
208
+
209
+
210
+ // // Using the detected duplicates, use only the latest version
211
+ // if (existingDuplicates.length) {
212
+ // if (existingDuplicates[0][0].modelVersion<model.modelVersion) {
213
+ // window.games[gameId].models.splice(existingDuplicates[0][1], 1)
214
+ // window.games[gameId].models.push(modelData)
215
+ // }
216
+ // } else {
217
+ // window.games[gameId].models.push(modelData)
218
+ // }
219
+ }
220
+
221
+ window.loadAllModels = (forceUpdate=false) => {
222
+ return new Promise(resolve => {
223
+
224
+ if (!forceUpdate && window.nexusState.installQueue.length) {
225
+ return
226
+ }
227
+
228
+ let gameFolder
229
+ let modelPathsKeys = Object.keys(window.userSettings).filter(key => key.includes("modelspath_"))
230
+ window.games = {}
231
+
232
+ // Do the current game first, and stop blocking the render process
233
+ if (window.currentGame) {
234
+ const currentGameFolder = window.userSettings[`modelspath_${window.currentGame.gameId}`]
235
+ gameFolder = currentGameFolder
236
+ try {
237
+ const files = fs.readdirSync(modelsPath).filter(f => f.endsWith(".json"))
238
+ files.forEach(fileName => {
239
+ try {
240
+ if (!models.hasOwnProperty(`${gameFolder}/${fileName}`)) {
241
+ models[`${gameFolder}/${fileName}`] = null
242
+ }
243
+ const model = JSON.parse(fs.readFileSync(`${modelsPath}/${fileName}`, "utf8"))
244
+ model.games.forEach(({gameId, voiceId, voiceName, voiceDescription, gender, variant, modelType, emb_i}) => {
245
+ window.registerModel(currentGameFolder, gameFolder, model, {gameId, voiceId, voiceName, voiceDescription, gender, variant, modelType, emb_i})
246
+ })
247
+
248
+ } catch (e) {
249
+ console.log(e)
250
+ // window.appLogger.log(`${window.i18n.ERR_LOADING_MODELS_FOR_GAME_WITH_FILENAME.replace("_1", gameFolder)} `+fileName)
251
+ // window.appLogger.log(e)
252
+ // window.appLogger.log(e.stack)
253
+ }
254
+ })
255
+ } catch (e) {
256
+ window.appLogger.log(`${window.i18n.ERR_LOADING_MODELS_FOR_GAME}: `+ gameFolder)
257
+ window.appLogger.log(e)
258
+ }
259
+ resolve() // Continue the rest but asynchronously
260
+ }
261
+
262
+
263
+ modelPathsKeys.forEach(modelsPathKey => {
264
+ const modelsPath = window.userSettings[modelsPathKey]
265
+ try {
266
+ const files = fs.readdirSync(modelsPath).filter(f => f.endsWith(".json"))
267
+
268
+ if (!files.length) {
269
+ return
270
+ }
271
+
272
+ files.forEach(fileName => {
273
+
274
+ gameFolder = modelsPathKey.split("_")[1]
275
+
276
+ try {
277
+ if (!models.hasOwnProperty(`${gameFolder}/${fileName}`)) {
278
+ models[`${gameFolder}/${fileName}`] = null
279
+ }
280
+
281
+ const model = JSON.parse(fs.readFileSync(`${modelsPath}/${fileName}`, "utf8"))
282
+ model.games.forEach(({gameId, voiceId, voiceName, voiceDescription, gender, variant, modelType, emb_i}) => {
283
+ window.registerModel(modelsPath, gameFolder, model, {gameId, voiceId, voiceName, voiceDescription, gender, variant, modelType, emb_i})
284
+ })
285
+ } catch (e) {
286
+ console.log(e)
287
+ setTimeout(() => {
288
+ window.errorModal(`${fileName}<br><br>${e.stack}`)
289
+ }, 1000)
290
+ window.appLogger.log(`${window.i18n.ERR_LOADING_MODELS_FOR_GAME_WITH_FILENAME.replace("_1", gameFolder)} `+fileName)
291
+ window.appLogger.log(e)
292
+ window.appLogger.log(e.stack)
293
+ }
294
+ })
295
+ } catch (e) {
296
+ window.appLogger.log(`${window.i18n.ERR_LOADING_MODELS_FOR_GAME}: `+ gameFolder)
297
+ window.appLogger.log(e)
298
+ }
299
+ })
300
+ window.updateGameList(false)
301
+ resolve()
302
+ })
303
+ }
304
+
305
+
306
+ // Change variant
307
+ let oldVariantSelection = undefined // For reverting, if versioning checks fail
308
+ variant_select.addEventListener("change", () => {
309
+
310
+ const model = window.games[window.currentGame.gameId].models.find(model => model.voiceName== window.currentModel.voiceName)
311
+ const variant = model.variants.find(variant => variant.variantName==variant_select.value)
312
+
313
+ const appVersionOk = window.checkVersionRequirements(variant.version, appVersion)
314
+ if (!appVersionOk) {
315
+ window.errorModal(`${window.i18n.MODEL_REQUIRES_VERSION} v${variant.version}<br><br>${window.i18n.THIS_APP_VERSION}: ${window.appVersion}`)
316
+ variant_select.value = oldVariantSelection
317
+ return
318
+ }
319
+
320
+ generateVoiceButton.dataset.modelQuery = JSON.stringify({
321
+ outputs: parseInt(model.outputs),
322
+ model: model.embOverABaseModel ? window.userSettings[`modelspath_${model.embOverABaseModel.split("/")[0]}`]+`/${model.embOverABaseModel.split("/")[1]}` : `${model.modelsPath}/${variant.voiceId}`,
323
+ modelType: variant.modelType,
324
+ version: variant.version,
325
+ model_speakers: model.num_speakers,
326
+ base_lang: model.lang || "en"
327
+ })
328
+ oldVariantSelection = variant_select.value
329
+
330
+ titleInfoVoiceID.innerHTML = variant.voiceId
331
+ titleInfoGender.innerHTML = variant.gender || "?"
332
+ titleInfoAppVersion.innerHTML = variant.version || "?"
333
+ titleInfoModelVersion.innerHTML = variant.modelVersion || "?"
334
+ titleInfoModelType.innerHTML = variant.modelType || "?"
335
+ titleInfoLanguage.innerHTML = variant.lang || window.currentModel.games[0].lang || "en"
336
+ titleInfoAuthor.innerHTML = variant.author || "?"
337
+
338
+ generateVoiceButton.click()
339
+ })
340
+
341
+
342
+
343
+ // Change game
344
+ window.changeGame = (meta) => {
345
+
346
+ titleInfo.style.display = "none"
347
+ window.currentGame = meta
348
+ themeColour = meta.themeColourPrimary
349
+ secondaryThemeColour = meta.themeColourSecondary
350
+ let titleID = meta.gameCode
351
+
352
+ generateVoiceButton.disabled = true
353
+ generateVoiceButton.innerHTML = window.i18n.GENERATE_VOICE
354
+ selectedGameDisplay.innerHTML = meta.gameName
355
+
356
+ // Change the app title
357
+ titleName.innerHTML = window.i18n.SELECT_VOICE_TYPE
358
+ if (window.games[window.currentGame.gameId] == undefined) {
359
+ titleName.innerHTML = `${window.i18n.NO_MODELS_IN}: ${window.userSettings[`modelspath_${window.currentGame.gameId}`]}`
360
+ }
361
+
362
+ const gameFolder = meta.gameId
363
+ const gameName = meta.gameName
364
+
365
+ setting_models_path_container.style.display = "flex"
366
+ setting_out_path_container.style.display = "flex"
367
+ setting_models_path_label.innerHTML = `<i style="display:inline">${gameName}</i><span>${window.i18n.SETTINGS_MODELS_PATH}</span>`
368
+ setting_models_path_input.value = window.userSettings[`modelspath_${gameFolder}`]
369
+ setting_out_path_label.innerHTML = `<i style="display:inline">${gameName}</i> ${window.i18n.SETTINGS_OUTPUT_PATH}`
370
+ setting_out_path_input.value = window.userSettings[`outpath_${gameFolder}`]
371
+
372
+ window.setTheme(window.currentGame)
373
+ try {
374
+ window.displayAllModels()
375
+ } catch (e) {console.log(e)}
376
+
377
+ try {fs.mkdirSync(`${path}/output/${meta.gameId}`)} catch (e) {/*Do nothing*/}
378
+ localStorage.setItem("lastGame", JSON.stringify(meta))
379
+
380
+ // Populate models
381
+ voiceTypeContainer.innerHTML = ""
382
+ voiceSamples.innerHTML = ""
383
+
384
+ const buttons = []
385
+ const totalNumVoices = (window.games[meta.gameId] ? window.games[meta.gameId].models : []).reduce((p,c)=>p+c.variants.length, 0)
386
+ voiceSearchInput.placeholder = window.i18n.SEARCH_N_VOICES.replace("_", window.games[meta.gameId] ? totalNumVoices : "0")
387
+ voiceSearchInput.value = ""
388
+
389
+ if (!window.games[meta.gameId]) {
390
+ return
391
+ }
392
+
393
+ (window.games[meta.gameId] ? window.games[meta.gameId].models : []).forEach(({modelsPath, audioPreviewPath, gameId, variants, voiceName, embOverABaseModel}) => {
394
+
395
+ const {voiceId, voiceDescription, hifi, model} = variants[0]
396
+ const modelVersion = variants[0].version
397
+
398
+ const button = createElem("div.voiceType", voiceName)
399
+ button.style.background = `#${themeColour}`
400
+ if (embOverABaseModel) {
401
+ button.style.fontStyle = "italic"
402
+ }
403
+ if (window.userSettings.do_model_version_highlight && parseFloat(modelVersion)<window.userSettings.model_version_highlight) {
404
+ button.style.border = `2px solid #${themeColour}`
405
+ button.style.padding = "0"
406
+ button.style.background = "none"
407
+ }
408
+ button.dataset.modelId = voiceId
409
+ if (secondaryThemeColour) {
410
+ button.style.color = `#${secondaryThemeColour}`
411
+ button.style.textShadow = `none`
412
+ }
413
+
414
+ // Quick voice set preview, if there is a preview file
415
+ button.addEventListener("contextmenu", () => {
416
+ window.appLogger.log(`${audioPreviewPath}.wav`)
417
+ const audioPreview = createElem("audio", {autoplay: false}, createElem("source", {
418
+ src: `${audioPreviewPath}.wav`
419
+ }))
420
+ audioPreview.style.height = "25px"
421
+ audioPreview.setSinkId(window.userSettings.base_speaker)
422
+ })
423
+
424
+ if (embOverABaseModel) {
425
+ const gameOfBaseModel = embOverABaseModel.split("/")[0]
426
+ if (gameOfBaseModel=="<base>") {
427
+ // For included base v3 models
428
+ modelsPath = `${window.path}/python/xvapitch/${embOverABaseModel.split("/")[1]}`
429
+ } else {
430
+ // For any other model
431
+ const gameModelsPath = `${window.userSettings[`outpath_${gameOfBaseModel}`]}`
432
+ modelsPath = `${gameModelsPath}/${embOverABaseModel.split("/")[1]}`
433
+ }
434
+ }
435
+
436
+ button.addEventListener("click", event => window.selectVoice(event, variants, hifi, gameId, voiceId, model, button, audioPreviewPath, modelsPath, meta, embOverABaseModel))
437
+ buttons.push(button)
438
+ })
439
+
440
+ buttons.sort((a,b) => a.innerHTML.toLowerCase()<b.innerHTML.toLowerCase()?-1:1)
441
+ .forEach(button => voiceTypeContainer.appendChild(button))
442
+
443
+ }
444
+
445
+
446
+ window.selectVoice = (event, variants, hifi, gameId, voiceId, model, button, audioPreviewPath, modelsPath, meta, embOverABaseModel) => {
447
+ // Just for easier packaging of the voice models for publishing - yes, lazy
448
+ if (event.ctrlKey && event.shiftKey) {
449
+ window.packageVoice(event.altKey, variants, {modelsPath, gameId})
450
+ }
451
+
452
+ variant_select.innerHTML = ""
453
+ oldVariantSelection = undefined
454
+ if (variants.length==1) {
455
+ variantElements.style.display = "none"
456
+ } else {
457
+ variantElements.style.display = "flex"
458
+ variants.forEach(variant => {
459
+ const option = createElem("option", {value: variant.variantName})
460
+ option.innerHTML = variant.variantName
461
+ variant_select.appendChild(option)
462
+ if (!oldVariantSelection) {
463
+ oldVariantSelection = variant.variantName
464
+ }
465
+ })
466
+ }
467
+
468
+
469
+ if (hifi) {
470
+ // Remove the bespoke hifi option if there was one already there
471
+ Array.from(vocoder_select.children).forEach(opt => {
472
+ if (opt.innerHTML=="Bespoke HiFi GAN") {
473
+ vocoder_select.removeChild(opt)
474
+ }
475
+ })
476
+ bespoke_hifi_bolt.style.opacity = 1
477
+ const option = createElem("option", "Bespoke HiFi GAN")
478
+ option.value = `${gameId}/${voiceId}.hg.pt`
479
+ vocoder_select.appendChild(option)
480
+ } else {
481
+ bespoke_hifi_bolt.style.opacity = 0
482
+ // Set the vocoder select to quick-and-dirty if bespoke hifi-gan was selected
483
+ if (vocoder_select.value.includes(".hg.")) {
484
+ vocoder_select.value = "qnd"
485
+ window.changeVocoder("qnd")
486
+ }
487
+ // Remove the bespoke hifi option if there was one already there
488
+ Array.from(vocoder_select.children).forEach(opt => {
489
+ if (opt.innerHTML=="Bespoke HiFi GAN") {
490
+ vocoder_select.removeChild(opt)
491
+ }
492
+ })
493
+ }
494
+
495
+ window.currentModel = model
496
+ window.currentModel.voiceId = voiceId
497
+ window.currentModel.voiceName = button.innerHTML
498
+ window.currentModel.hifi = hifi
499
+ window.currentModel.audioPreviewPath = audioPreviewPath
500
+ window.currentModelButton = button
501
+
502
+
503
+ generateVoiceButton.dataset.modelQuery = null
504
+
505
+ // The model is already loaded. Don't re-load it.
506
+ if (generateVoiceButton.dataset.modelIDLoaded == voiceId) {
507
+ generateVoiceButton.innerHTML = window.i18n.GENERATE_VOICE
508
+ generateVoiceButton.dataset.modelQuery = "null"
509
+
510
+ } else {
511
+ generateVoiceButton.innerHTML = window.i18n.LOAD_MODEL
512
+ generateVoiceButton.dataset.modelQuery = JSON.stringify({
513
+ outputs: parseInt(model.outputs),
514
+ model: model.embOverABaseModel ? window.userSettings[`modelspath_${model.embOverABaseModel.split("/")[0]}`]+`/${model.embOverABaseModel.split("/")[1]}` : `${modelsPath}/${model.voiceId}`,
515
+ modelType: model.modelType,
516
+ version: model.version,
517
+ model_speakers: model.emb_size,
518
+ cmudict: model.cmudict,
519
+ base_lang: model.lang || "en"
520
+ })
521
+ generateVoiceButton.dataset.modelIDToLoad = voiceId
522
+ }
523
+ generateVoiceButton.disabled = false
524
+
525
+ titleName.innerHTML = button.innerHTML
526
+ titleInfo.style.display = "flex"
527
+ titleInfoName.innerHTML = window.currentModel.voiceName
528
+ titleInfoVoiceID.innerHTML = voiceId
529
+ titleInfoGender.innerHTML = window.currentModel.games[0].gender || "?"
530
+ titleInfoAppVersion.innerHTML = window.currentModel.version || "?"
531
+ titleInfoModelVersion.innerHTML = window.currentModel.modelVersion || "?"
532
+ titleInfoModelType.innerHTML = window.currentModel.modelType || "?"
533
+ titleInfoLanguage.innerHTML = window.currentModel.lang || window.currentModel.games[0].lang || "en"
534
+ titleInfoAuthor.innerHTML = window.currentModel.author || "?"
535
+ titleInfoLicense.innerHTML = window.currentModel.license || window.i18n.UNKNOWN
536
+
537
+ title.dataset.modelId = voiceId
538
+ keepSampleButton.style.display = "none"
539
+ samplePlayPause.style.display = "none"
540
+
541
+ // Voice samples
542
+ voiceSamples.innerHTML = ""
543
+
544
+ window.initMainPagePagination(`${window.userSettings[`outpath_${meta.gameId}`]}/${button.dataset.modelId}`)
545
+ window.refreshRecordsList()
546
+ }
547
+
548
+ titleInfo.addEventListener("click", () => titleDetails.style.display = titleDetails.style.display=="none" ? "block" : "none")
549
+ window.addEventListener("click", event => {
550
+ if (event.target!=titleInfo && event.target!=titleDetails && event.target.parentNode && event.target.parentNode!=titleDetails && event.target.parentNode.parentNode!=titleDetails) {
551
+ titleDetails.style.display = "none"
552
+ }
553
+ })
554
+ titleDetails.style.display = "none"
555
+
556
+
557
+ window.loadModel = () => {
558
+ return new Promise(resolve => {
559
+ if (window.batch_state.state) {
560
+ window.errorModal(window.i18n.BATCH_ERR_IN_PROGRESS)
561
+ return
562
+ }
563
+
564
+ const body = JSON.parse(generateVoiceButton.dataset.modelQuery)
565
+
566
+ const appVersionOk = window.checkVersionRequirements(body.version, appVersion)
567
+ if (!appVersionOk) {
568
+ window.errorModal(`${window.i18n.MODEL_REQUIRES_VERSION} v${body.version}<br><br>${window.i18n.THIS_APP_VERSION}: ${window.appVersion}`)
569
+ return
570
+ }
571
+
572
+
573
+ window.appLogger.log(`${window.i18n.LOADING_VOICE}: ${JSON.parse(generateVoiceButton.dataset.modelQuery).model}`)
574
+ window.batch_state.lastModel = JSON.parse(generateVoiceButton.dataset.modelQuery).model.split("/").reverse()[0]
575
+
576
+ body["pluginsContext"] = JSON.stringify(window.pluginsContext)
577
+
578
+ spinnerModal(`${window.i18n.LOADING_VOICE}`)
579
+ doFetch(`http://localhost:8008/loadModel`, {
580
+ method: "Post",
581
+ body: JSON.stringify(body)
582
+ }).then(r=>r.text()).then(res => {
583
+
584
+ window.currentModel.loaded = true
585
+ generateVoiceButton.dataset.modelQuery = null
586
+ generateVoiceButton.innerHTML = window.i18n.GENERATE_VOICE
587
+ generateVoiceButton.dataset.modelIDLoaded = generateVoiceButton.dataset.modelIDToLoad
588
+
589
+ // Set the editor pitch/energy dropdowns to pitch, and freeze them, if energy is not supported by the model
590
+ window.appState.currentModelEmbeddings = {}
591
+ if (window.currentModel.modelType.toLowerCase()=="xvapitch" && !window.currentModel.isBaseModel) {
592
+ vocoder_options_container.style.display = "none"
593
+ base_lang_select.disabled = false
594
+ style_emb_select.disabled = false
595
+ window.loadStyleEmbsForVoice(window.currentModel)
596
+ mic_SVG.children[0].style.fill = "white"
597
+ base_lang_select.value = window.currentModel.lang
598
+ } else {
599
+ vocoder_options_container.style.display = "inline-block"
600
+ base_lang_select.disabled = true
601
+ style_emb_select.disabled = true
602
+ mic_SVG.children[0].style.fill = "grey"
603
+ }
604
+ if (window.currentModel.modelType.toLowerCase()=="fastpitch") {
605
+ seq_edit_view_select.value = "pitch"
606
+ seq_edit_edit_select.value = "pitch"
607
+ seq_edit_view_select.disabled = true
608
+ seq_edit_edit_select.disabled = true
609
+ } else {
610
+ seq_edit_view_select.value = "pitch_energy"
611
+ seq_edit_view_select.disabled = false
612
+ seq_edit_edit_select.disabled = false
613
+ }
614
+
615
+ window.populateLanguagesDropdownsFromModel(base_lang_select, window.currentModel)
616
+ base_lang_select.value = window.currentModel.lang
617
+
618
+ if (window.userSettings.defaultToHiFi && window.currentModel.hifi) {
619
+ vocoder_select.value = Array.from(vocoder_select.children).find(opt => opt.innerHTML=="Bespoke HiFi GAN").value
620
+ window.changeVocoder(vocoder_select.value).then(() => dialogueInput.focus())
621
+ } else if (window.userSettings.vocoder.includes(".hg.pt")) {
622
+ window.changeVocoder("qnd").then(() => dialogueInput.focus())
623
+ } else {
624
+ closeModal(null, [workbenchContainer]).then(() => dialogueInput.focus())
625
+ }
626
+ resolve()
627
+ }).catch(e => {
628
+ console.log(e)
629
+ if (e.code =="ENOENT") {
630
+ closeModal(null, [modalContainer, workbenchContainer]).then(() => {
631
+ window.errorModal(window.i18n.ERR_SERVER)
632
+ resolve()
633
+ })
634
+ }
635
+ })
636
+ })
637
+ }
638
+
639
+ // Return true/false for if the prompt is the same - BUT: allow phoneme swaps
640
+ window.checkIfPromptIsTheSame = (sequence) => {
641
+
642
+ // False if there was no previous prompt
643
+ if (!window.sequenceEditor.historyState.length) {
644
+ return false
645
+ }
646
+
647
+ const lastPrompt = window.sequenceEditor.historyState.at(-1)
648
+
649
+ // False if they're different lengths
650
+ if (sequence.length != lastPrompt.length) {
651
+ return false
652
+ }
653
+
654
+ // Split into words (and phonemes)
655
+ const currentParts = sequence.split(" ")
656
+ const lastParts = lastPrompt.split(" ")
657
+
658
+ for (let si=0; si<currentParts.length; si++) {
659
+ // False if a word is different, but not if it's an ARPAbet symbol
660
+ const cleaned = currentParts[si].replace(/[^a-zA-Z]/g, "")
661
+ if (currentParts[si]!=lastParts[si] && !window.ARPAbetSymbols.includes(cleaned)) {
662
+ return false
663
+ }
664
+ }
665
+
666
+ return true
667
+ }
668
+
669
+ window.synthesizeSample = () => {
670
+
671
+ const game = window.currentGame.gameId
672
+
673
+ if (window.isGenerating) {
674
+ return
675
+ }
676
+ if (!window.speech2speechState.s2s_running) {
677
+ clearOldTempFiles()
678
+ }
679
+
680
+ let sequence = dialogueInput.value.replace("…", "...").replace("’", "'")
681
+ if (window.userSettings.spacePadding && !window.sequenceEditor.isEditingFromFile) { // Pad start and end of the input sequence with spaces
682
+ sequence = " "+sequence.trim()+" "
683
+ }
684
+ window.sequenceEditor.isEditingFromFile = false
685
+
686
+ if (sequence.length==0) {
687
+ return
688
+ }
689
+ window.isGenerating = true
690
+
691
+ window.pluginsManager.runPlugins(window.pluginsManager.pluginsModules["generate-voice"]["pre"], event="pre generate-voice")
692
+
693
+ if (window.wavesurfer) {
694
+ try {
695
+ window.wavesurfer.stop()
696
+ } catch (e) {
697
+ console.log(e)
698
+ }
699
+ wavesurferContainer.style.opacity = 0
700
+ }
701
+ toggleSpinnerButtons()
702
+
703
+ const voiceType = title.dataset.modelId
704
+ const outputFileName = dialogueInput.value.slice(0, 260).replace(/\n/g, " ").replace(/[\/\\:\*?<>"|]*/g, "").replace(/^[\.\s]+/, "")
705
+
706
+ try {fs.unlinkSync(localStorage.getItem("tempFileLocation"))} catch (e) {/*Do nothing*/}
707
+
708
+ // For some reason, the samplePlay audio element does not update the source when the file name is the same
709
+ const tempFileNum = `${Math.random().toString().split(".")[1]}`
710
+ let tempFileLocation = `${path}/output/temp-${tempFileNum}.wav`
711
+ let pitch = []
712
+ let duration = []
713
+ let energy = []
714
+ let emAngry = []
715
+ let emHappy = []
716
+ let emSad = []
717
+ let emSurprise = []
718
+ let editorStyles = {}
719
+ let isFreshRegen = true
720
+ let old_sequence = undefined
721
+
722
+ if (editorContainer.innerHTML && editorContainer.innerHTML.length && generateVoiceButton.dataset.modelIDLoaded==window.sequenceEditor.currentVoice) {
723
+ if (window.sequenceEditor.audioInput || window.sequenceEditor.sequence && sequence!=window.sequenceEditor.inputSequence) {
724
+ old_sequence = window.sequenceEditor.inputSequence
725
+ }
726
+ }
727
+ // Don't use the old_sequence if running speech-to-speech
728
+ if (window.speech2speechState.s2s_running) {
729
+ old_sequence = undefined
730
+ window.speech2speechState.s2s_running = false
731
+ }
732
+
733
+ // Check if editing an existing line (otherwise it's a fresh new line)
734
+ const languageHasChanged = window.sequenceEditor.base_lang && window.sequenceEditor.base_lang != base_lang_select.value
735
+ const promptHasChanged = !window.checkIfPromptIsTheSame(sequence)
736
+ if (!promptHasChanged && !languageHasChanged && !window.arpabetMenuState.hasChangedARPAbet && !window.styleEmbsMenuState.hasChangedEmb &&
737
+ (speech2speechState.s2s_autogenerate || (editorContainer.innerHTML && editorContainer.innerHTML.length && (window.userSettings.keepEditorOnVoiceChange || generateVoiceButton.dataset.modelIDLoaded==window.sequenceEditor.currentVoice)))) {
738
+
739
+ speech2speechState.s2s_autogenerate = false
740
+ pitch = window.sequenceEditor.pitchNew.map(v=> v==undefined?0:v)
741
+ duration = window.sequenceEditor.dursNew.map(v => v*pace_slid.value).map(v=> v==undefined?0:v)
742
+ energy = window.sequenceEditor.energyNew ? window.sequenceEditor.energyNew.map(v => v==undefined?0:v).filter(v => !isNaN(v)) : []
743
+ if (window.currentModel.modelType=="xVAPitch") {
744
+ emAngry = window.sequenceEditor.emAngryNew ? window.sequenceEditor.emAngryNew.map(v => v==undefined?0:v).filter(v => !isNaN(v)) : []
745
+ emHappy = window.sequenceEditor.emHappyNew ? window.sequenceEditor.emHappyNew.map(v => v==undefined?0:v).filter(v => !isNaN(v)) : []
746
+ emSad = window.sequenceEditor.emSadNew ? window.sequenceEditor.emSadNew.map(v => v==undefined?0:v).filter(v => !isNaN(v)) : []
747
+ emSurprise = window.sequenceEditor.emSurpriseNew ? window.sequenceEditor.emSurpriseNew.map(v => v==undefined?0:v).filter(v => !isNaN(v)) : []
748
+
749
+ if (window.sequenceEditor.registeredStyleKeys) {
750
+ window.sequenceEditor.registeredStyleKeys.forEach(styleKey => {
751
+ editorStyles[styleKey] = {
752
+ embedding: window.appState.currentModelEmbeddings[styleKey][1],
753
+ sliders: window.sequenceEditor.styleValuesNew[styleKey].map(v => v==undefined?0:v).filter(v => !isNaN(v))// : []
754
+ }
755
+ })
756
+ }
757
+ }
758
+ isFreshRegen = false
759
+ }
760
+
761
+ window.arpabetMenuState.hasChangedARPAbet = false
762
+ window.styleEmbsMenuState.hasChangedEmb = false
763
+ window.sequenceEditor.currentVoice = generateVoiceButton.dataset.modelIDLoaded
764
+
765
+ const speaker_i = window.currentModel.games[0].emb_i
766
+ const pace = (window.userSettings.keepPaceOnNew && isFreshRegen)?pace_slid.value:1
767
+
768
+ window.appLogger.log(`${window.i18n.SYNTHESIZING}: ${sequence}`)
769
+
770
+ doFetch(`http://localhost:8008/synthesize`, {
771
+ method: "Post",
772
+ body: JSON.stringify({
773
+ sequence, pitch, duration, energy, emAngry, emHappy, emSad, emSurprise, editorStyles, speaker_i, pace,
774
+ base_lang: base_lang_select.value,
775
+ base_emb: style_emb_select.value||"",
776
+ modelType: window.currentModel.modelType,
777
+ old_sequence, // For partial re-generation
778
+ device: window.userSettings.installation=="cpu"?"cpu":(window.userSettings.useGPU?"cuda:0":"cpu"),
779
+ // device: window.userSettings.useGPU?"gpu":"cpu", // Switch to this once DirectML is installed
780
+ useSR: useSRCkbx.checked,
781
+ useCleanup: useCleanupCkbx.checked,
782
+ outfile: tempFileLocation,
783
+ pluginsContext: JSON.stringify(window.pluginsContext),
784
+ vocoder: window.currentModel.modelType=="xVAPitch" ? "n/a" : window.userSettings.vocoder,
785
+ waveglowPath: vocoder_select.value=="256_waveglow" ? window.userSettings.waveglow_path : window.userSettings.bigwaveglow_path
786
+ })
787
+ }).then(r=>r.text()).then(res => {
788
+ window.isGenerating = false
789
+
790
+ if (res=="ENOENT" || res.startsWith("ERR:")) {
791
+ console.log(res)
792
+ if (res.startsWith("ERR:")) {
793
+ if (res.includes("ARPABET_NOT_IN_LIST")) {
794
+ const symbolNotInList = res.split(":").reverse()[0]
795
+ window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}<br><br>${window.i18n.ERR_ARPABET_NOT_EXIST.replace("_1", symbolNotInList)}`)
796
+ } else {
797
+ window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}<br><br>${res.replace("ERR:","").replaceAll(/\n/g, "<br>")}`)
798
+ }
799
+ } else {
800
+ window.appLogger.log(res)
801
+ window.errorModal(`${window.i18n.BATCH_MODEL_NOT_FOUND}.${vocoder_select.value.includes("waveglow")?" "+window.i18n.BATCH_DOWNLOAD_WAVEGLOW:""}`)
802
+ }
803
+ toggleSpinnerButtons()
804
+ return
805
+ }
806
+
807
+ dialogueInput.focus()
808
+ window.sequenceEditor.historyState.push(sequence)
809
+
810
+ if (window.userSettings.clear_text_after_synth) {
811
+ dialogueInput.value = ""
812
+ }
813
+
814
+ res = res.split("\n")
815
+ let pitchData = res[0]
816
+ let durationsData = res[1]
817
+ let energyData = res[2]
818
+ let em_angryData = res[3]
819
+ let em_happyData = res[4]
820
+ let em_sadData = res[5]
821
+ let em_surpriseData = res[6]
822
+ const editorStyles = res[7]&&res[7].length ? JSON.parse(res[7]) : undefined
823
+ let cleanedSequence = res[8].split("|").map(c=>c.replaceAll("{", "").replaceAll("}", "").replace(/\s/g, "_"))
824
+ const start_index = res[9]
825
+ const end_index = res[10]
826
+ pitchData = pitchData.split(",").map(v => parseFloat(v))
827
+
828
+ // For use in adjusting editor range
829
+ const maxPitchVal = pitchData.reduce((p,c)=>Math.max(p, Math.abs(c)), 0)
830
+ if (maxPitchVal>window.sequenceEditor.default_pitchSliderRange) {
831
+ window.sequenceEditor.pitchSliderRange = maxPitchVal
832
+ } else {
833
+ window.sequenceEditor.pitchSliderRange = window.sequenceEditor.default_pitchSliderRange
834
+ }
835
+
836
+ em_angryData = em_angryData.length ? em_angryData.split(",").map(v => parseFloat(v)).filter(v => !isNaN(v)) : []
837
+ em_happyData = em_happyData.length ? em_happyData.split(",").map(v => parseFloat(v)).filter(v => !isNaN(v)) : []
838
+ em_sadData = em_sadData.length ? em_sadData.split(",").map(v => parseFloat(v)).filter(v => !isNaN(v)) : []
839
+ em_surpriseData = em_surpriseData.length ? em_surpriseData.split(",").map(v => parseFloat(v)).filter(v => !isNaN(v)) : []
840
+
841
+ if (energyData.length) {
842
+ energyData = energyData.split(",").map(v => parseFloat(v)).filter(v => !isNaN(v))
843
+
844
+ // For use in adjusting editor range
845
+ const maxEnergyVal = energyData.reduce((p,c)=>Math.max(p, c), 0)
846
+ const minEnergyVal = energyData.reduce((p,c)=>Math.min(p, c), 100)
847
+
848
+ if (minEnergyVal<window.sequenceEditor.default_MIN_ENERGY) {
849
+ window.sequenceEditor.MIN_ENERGY = minEnergyVal
850
+ } else {
851
+ window.sequenceEditor.MIN_ENERGY = window.sequenceEditor.default_MIN_ENERGY
852
+ }
853
+ if (maxEnergyVal>window.sequenceEditor.default_MAX_ENERGY) {
854
+ window.sequenceEditor.MAX_ENERGY = maxEnergyVal
855
+ } else {
856
+ window.sequenceEditor.MAX_ENERGY = window.sequenceEditor.default_MAX_ENERGY
857
+ }
858
+
859
+ } else {
860
+ energyData = []
861
+ }
862
+ durationsData = durationsData.split(",").map(v => isFreshRegen ? parseFloat(v) : parseFloat(v)/pace_slid.value)
863
+
864
+ const doTheRest = () => {
865
+
866
+ window.sequenceEditor.base_lang = base_lang_select.value
867
+ window.sequenceEditor.inputSequence = sequence
868
+ window.sequenceEditor.sequence = cleanedSequence
869
+
870
+ if (pitch.length==0 || isFreshRegen) {
871
+ window.sequenceEditor.resetPitch = pitchData
872
+ window.sequenceEditor.resetDurs = durationsData
873
+ window.sequenceEditor.resetEnergy = energyData
874
+ window.sequenceEditor.resetEmAngry = em_angryData
875
+ window.sequenceEditor.resetEmHappy = em_happyData
876
+ window.sequenceEditor.resetEmSad = em_sadData
877
+ window.sequenceEditor.resetEmSurprise = em_surpriseData
878
+ }
879
+
880
+ window.sequenceEditor.letters = cleanedSequence
881
+ window.sequenceEditor.pitchNew = pitchData.map(p=>p)
882
+ window.sequenceEditor.dursNew = durationsData.map(v=>v)
883
+ window.sequenceEditor.energyNew = energyData.map(v=>v)
884
+ if (window.currentModel.modelType=="xVAPitch") {
885
+ window.sequenceEditor.emAngryNew = em_angryData.map(v=>v)
886
+ window.sequenceEditor.emHappyNew = em_happyData.map(v=>v)
887
+ window.sequenceEditor.emSadNew = em_sadData.map(v=>v)
888
+ window.sequenceEditor.emSurpriseNew = em_surpriseData.map(v=>v)
889
+ window.sequenceEditor.loadStylesData(editorStyles)
890
+ }
891
+ window.sequenceEditor.init()
892
+ const pitchRange = window.userSettings.pitchrangeoverride ? window.userSettings.pitchrangeoverride : window.sequenceEditor.pitchSliderRange
893
+ window.sequenceEditor.update(window.currentModel.modelType, pitchRange)
894
+
895
+ window.sequenceEditor.sliderBoxes.forEach((box, i) => {box.setValueFromValue(window.sequenceEditor.dursNew[i])})
896
+ window.sequenceEditor.autoInferTimer = null
897
+ window.sequenceEditor.hasChanged = false
898
+
899
+
900
+ toggleSpinnerButtons()
901
+ if (keepSampleButton.dataset.newFileLocation && keepSampleButton.dataset.newFileLocation.startsWith("BATCH_EDIT")) {
902
+ console.log("_debug_")
903
+ } else {
904
+ if (window.userSettings[`outpath_${game}`]) {
905
+ keepSampleButton.dataset.newFileLocation = `${window.userSettings[`outpath_${game}`]}/${voiceType}/${outputFileName}.wav`
906
+ } else {
907
+ keepSampleButton.dataset.newFileLocation = `${__dirname.replace(/\\/g,"/")}/output/${voiceType}/${outputFileName}.wav`
908
+ }
909
+ }
910
+ keepSampleButton.disabled = false
911
+ window.tempFileLocation = tempFileLocation
912
+
913
+
914
+ // Wavesurfer
915
+ if (!window.wavesurfer) {
916
+ window.initWaveSurfer(`${__dirname.replace("/javascript", "")}/output/${tempFileLocation.split("/").reverse()[0]}`)
917
+ } else {
918
+ window.wavesurfer.load(`${__dirname.replace("/javascript", "")}/output/${tempFileLocation.split("/").reverse()[0]}`)
919
+ }
920
+
921
+ window.wavesurfer.on("ready", () => {
922
+
923
+ wavesurferContainer.style.opacity = 1
924
+
925
+ if (window.userSettings.autoPlayGen) {
926
+
927
+ if (window.userSettings.playChangedAudio) {
928
+ const playbackStartEnd = window.sequenceEditor.getChangedTimeStamps(start_index, end_index, window.wavesurfer.getDuration())
929
+ if (playbackStartEnd) {
930
+ wavesurfer.play(playbackStartEnd[0], playbackStartEnd[1])
931
+ } else {
932
+ wavesurfer.play()
933
+ }
934
+ } else {
935
+ wavesurfer.play()
936
+ }
937
+ window.sequenceEditor.adjustedLetters = new Set()
938
+ samplePlayPause.innerHTML = window.i18n.PAUSE
939
+ }
940
+ })
941
+
942
+ // Persistance across sessions
943
+ localStorage.setItem("tempFileLocation", tempFileLocation)
944
+ }
945
+
946
+
947
+ if (window.userSettings.audio.ffmpeg) {
948
+ const options = {
949
+ hz: window.userSettings.audio.hz,
950
+ padStart: window.userSettings.audio.padStart,
951
+ padEnd: window.userSettings.audio.padEnd,
952
+ bit_depth: window.userSettings.audio.bitdepth,
953
+ amplitude: window.userSettings.audio.amplitude,
954
+ pitchMult: window.userSettings.audio.pitchMult,
955
+ tempo: window.userSettings.audio.tempo,
956
+ deessing: window.userSettings.audio.deessing,
957
+ nr: window.userSettings.audio.nr,
958
+ nf: window.userSettings.audio.nf,
959
+ useNR: window.userSettings.audio.useNR,
960
+ useSR: useSRCkbx.checked,
961
+ useCleanup: useCleanupCkbx.checked,
962
+ }
963
+
964
+ const extraInfo = {
965
+ game: window.currentGame.gameId,
966
+ voiceId: window.currentModel.voiceId,
967
+ voiceName: window.currentModel.voiceName,
968
+ inputSequence: sequence,
969
+ letters: cleanedSequence,
970
+ pitch: pitchData.map(p=>p),
971
+ energy: energyData.map(p=>p),
972
+ em_angry: em_angryData.map(p=>p),
973
+ em_happy: em_happyData.map(p=>p),
974
+ em_sad: em_sadData.map(p=>p),
975
+ em_surprise: em_surpriseData.map(p=>p),
976
+ durations: durationsData.map(v=>v)
977
+ }
978
+
979
+ doFetch(`http://localhost:8008/outputAudio`, {
980
+ method: "Post",
981
+ body: JSON.stringify({
982
+ input_path: tempFileLocation,
983
+ output_path: tempFileLocation.replace(".wav", `_ffmpeg.${window.userSettings.audio.format}`),
984
+ pluginsContext: JSON.stringify(window.pluginsContext),
985
+ extraInfo: JSON.stringify(extraInfo),
986
+ isBatchMode: false,
987
+ options: JSON.stringify(options)
988
+ })
989
+ }).then(r=>r.text()).then(res => {
990
+ if (res.length && res!="-") {
991
+ console.log("res", res)
992
+ window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}<br><br>${res}`).then(() => toggleSpinnerButtons())
993
+ } else {
994
+ tempFileLocation = tempFileLocation.replace(".wav", `_ffmpeg.${window.userSettings.audio.format}`)
995
+ doTheRest()
996
+ }
997
+ }).catch(res => {
998
+ console.log(res)
999
+ window.appLogger.log(`outputAudio error: ${res}`)
1000
+ // closeModal().then(() => {
1001
+ window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}<br><br>${res}`)
1002
+ // })
1003
+ })
1004
+ } else {
1005
+ doTheRest()
1006
+ }
1007
+
1008
+
1009
+ }).catch(res => {
1010
+ window.isGenerating = false
1011
+ console.log(res)
1012
+ window.appLogger.log(res)
1013
+ window.errorModal(window.i18n.SOMETHING_WENT_WRONG)
1014
+ toggleSpinnerButtons()
1015
+ })
1016
+ }
1017
+
1018
+ generateVoiceButton.addEventListener("click", () => {
1019
+ try {fs.mkdirSync(window.userSettings[`outpath_${game}`])} catch (e) {/*Do nothing*/}
1020
+ try {fs.mkdirSync(`${window.userSettings[`outpath_${game}`]}/${voiceId}`)} catch (e) {/*Do nothing*/}
1021
+
1022
+ if (generateVoiceButton.dataset.modelQuery && generateVoiceButton.dataset.modelQuery!="null") {
1023
+ window.loadModel()
1024
+ } else {
1025
+ window.synthesizeSample()
1026
+ }
1027
+ })
1028
+
1029
+
1030
+
1031
+
1032
+ window.saveFile = (from, to, skipUIRecord=false) => {
1033
+ to = to.split("%20").join(" ")
1034
+ to = to.replace(".wav", `.${window.userSettings.audio.format}`)
1035
+
1036
+ // Make the containing folder if it does not already exist
1037
+ let containerFolderPath = to.split("/")
1038
+ containerFolderPath = containerFolderPath.slice(0,containerFolderPath.length-1).join("/")
1039
+
1040
+ try {fs.mkdirSync(containerFolderPath)} catch (e) {/*Do nothing*/}
1041
+
1042
+ // For plugins
1043
+ const pluginData = {
1044
+ game: window.currentGame.gameId,
1045
+ voiceId: window.currentModel.voiceId,
1046
+ voiceName: window.currentModel.voiceName,
1047
+ inputSequence: window.sequenceEditor.inputSequence,
1048
+ letters: window.sequenceEditor.letters,
1049
+ pitch: window.sequenceEditor.pitchNew,
1050
+ durations: window.sequenceEditor.dursNew,
1051
+ vocoder: vocoder_select.value,
1052
+ from, to
1053
+ }
1054
+ const options = {
1055
+ hz: window.userSettings.audio.hz,
1056
+ padStart: window.userSettings.audio.padStart,
1057
+ padEnd: window.userSettings.audio.padEnd,
1058
+ bit_depth: window.userSettings.audio.bitdepth,
1059
+ amplitude: window.userSettings.audio.amplitude,
1060
+ pitchMult: window.userSettings.audio.pitchMult,
1061
+ tempo: window.userSettings.audio.tempo,
1062
+ deessing: window.userSettings.audio.deessing,
1063
+ nr: window.userSettings.audio.nr,
1064
+ nf: window.userSettings.audio.nf,
1065
+ useNR: window.userSettings.audio.useNR,
1066
+ useSR: useSRCkbx.checked
1067
+ }
1068
+ pluginData.audioOptions = options
1069
+ window.pluginsManager.runPlugins(window.pluginsManager.pluginsModules["keep-sample"]["pre"], event="pre keep-sample", pluginData)
1070
+
1071
+ const jsonDataOut = {
1072
+ modelType: window.currentModel.modelType,
1073
+ modelVersion: window.currentModel.modelVersion,
1074
+ version: window.currentModel.version,
1075
+ inputSequence: dialogueInput.value.trim(),
1076
+ pacing: parseFloat(pace_slid.value),
1077
+ letters: window.sequenceEditor.letters,
1078
+ currentVoice: window.sequenceEditor.currentVoice,
1079
+ resetEnergy: window.sequenceEditor.resetEnergy,
1080
+ resetPitch: window.sequenceEditor.resetPitch,
1081
+ resetDurs: window.sequenceEditor.resetDurs,
1082
+ resetEmAngry: window.sequenceEditor.resetEmAngry,
1083
+ resetEmHappy: window.sequenceEditor.resetEmHappy,
1084
+ resetEmSad: window.sequenceEditor.resetEmSad,
1085
+ resetEmSurprise: window.sequenceEditor.resetEmSurprise,
1086
+ styleValuesReset: window.sequenceEditor.styleValuesReset,
1087
+ ampFlatCounter: window.sequenceEditor.ampFlatCounter,
1088
+ inputSequence: window.sequenceEditor.inputSequence,
1089
+ sequence: window.sequenceEditor.sequence,
1090
+ pitchNew: window.sequenceEditor.pitchNew,
1091
+ energyNew: window.sequenceEditor.energyNew,
1092
+ dursNew: window.sequenceEditor.dursNew,
1093
+ emAngryNew: window.sequenceEditor.emAngryNew,
1094
+ emHappyNew: window.sequenceEditor.emHappyNew,
1095
+ emSadNew: window.sequenceEditor.emSadNew,
1096
+ emSurpriseNew: window.sequenceEditor.emSurpriseNew,
1097
+ styleValuesNew: window.sequenceEditor.styleValuesNew,
1098
+ }
1099
+
1100
+ let outputFileName = to.split("/").reverse()[0].split(".").reverse().slice(1, 1000)
1101
+ const toExt = to.split(".").reverse()[0]
1102
+
1103
+ if (window.userSettings.filenameNumericalSeq) {
1104
+ outputFileName = outputFileName[0]+"."+outputFileName.slice(1,1000).reverse().join(".")
1105
+ } else {
1106
+ outputFileName = outputFileName.reverse().join(".")
1107
+ }
1108
+ to = `${to.split("/").reverse().slice(1,10000).reverse().join("/")}/${outputFileName}`
1109
+
1110
+
1111
+ const allFiles = fs.readdirSync(`${path}/output`).filter(fname => fname.includes(from.split("/").reverse()[0].split(".")[0]))
1112
+ const toFolder = to.split("/").reverse().slice(1, 1000).reverse().join("/")
1113
+
1114
+
1115
+ allFiles.forEach(fname => {
1116
+ const ext = fname.split(".").reverse()[0]
1117
+ fs.copyFile(`${path}/output/${fname}`, `${toFolder}/${outputFileName}.${ext}`, err => {
1118
+ if (err) {
1119
+ console.log(err)
1120
+ window.appLogger.log(`Error in saveFile->outputAudio[no ffmpeg]: ${err}`)
1121
+ if (!fs.existsSync(from)) {
1122
+ window.appLogger.log(`${window.i18n.TEMP_FILE_NOT_EXIST}: ${from}`)
1123
+ }
1124
+ if (!fs.existsSync(toFolder)) {
1125
+ window.appLogger.log(`${window.i18n.OUT_DIR_NOT_EXIST}: ${toFolder}`)
1126
+ }
1127
+ } else {
1128
+ if (window.userSettings.outputJSON && window.sequenceEditor.letters.length) {
1129
+ fs.writeFileSync(`${to}.${toExt}.json`, JSON.stringify(jsonDataOut, null, 4))
1130
+ }
1131
+ if (!skipUIRecord) {
1132
+ window.initMainPagePagination(`${window.userSettings[`outpath_${window.currentGame.gameId}`]}/${window.currentModel.voiceId}`)
1133
+ window.refreshRecordsList()
1134
+ }
1135
+ window.pluginsManager.runPlugins(window.pluginsManager.pluginsModules["keep-sample"]["post"], event="post keep-sample", pluginData)
1136
+ }
1137
+ })
1138
+ })
1139
+ }
1140
+
1141
+ window.keepSampleFunction = shiftClick => {
1142
+ if (keepSampleButton.dataset.newFileLocation) {
1143
+
1144
+ const skipUIRecord = keepSampleButton.dataset.newFileLocation.includes("BATCH_EDIT")
1145
+ let fromLocation = window.tempFileLocation
1146
+ let toLocation = keepSampleButton.dataset.newFileLocation.replace("BATCH_EDIT", "")
1147
+
1148
+ if (!skipUIRecord) {
1149
+ toLocation = toLocation.split("/")
1150
+ toLocation[toLocation.length-1] = toLocation[toLocation.length-1].replace(/[\/\\:\*?<>"|]*/g, "")
1151
+ toLocation[toLocation.length-1] = toLocation[toLocation.length-1].replace(/\.wav$/, "").slice(0, window.userSettings.max_filename_chars).replace(/\.$/, "")
1152
+ }
1153
+
1154
+
1155
+ // Numerical file name counter
1156
+ if (!skipUIRecord && window.userSettings.filenameNumericalSeq) {
1157
+ let existingFiles = []
1158
+ try {
1159
+ existingFiles = fs.readdirSync(toLocation.slice(0, toLocation.length-1).join("/")).filter(fname => !fname.endsWith(".json"))
1160
+ } catch (e) {
1161
+ console.log(e)
1162
+ }
1163
+ existingFiles = existingFiles.filter(fname => fname.includes(toLocation[toLocation.length-1]))
1164
+ existingFiles = existingFiles.map(fname => {
1165
+ const parts = fname.split(".")
1166
+ parts.reverse()
1167
+ if (parts.length>2 && parts.reverse()[0].length) {
1168
+ if (parseInt(parts[0]) != NaN) {
1169
+ return parseInt(parts[0])
1170
+ }
1171
+ }
1172
+ return null
1173
+ })
1174
+ existingFiles = existingFiles.filter(val => !!val)
1175
+ if (existingFiles.length==0) {
1176
+ existingFiles.push(0)
1177
+ }
1178
+
1179
+ if (existingFiles.length) {
1180
+ existingFiles = existingFiles.sort((a,b) => {a<b?-1:1})
1181
+ toLocation[toLocation.length-1] = `${toLocation[toLocation.length-1]}.${String(existingFiles[existingFiles.length-1]+1).padStart(4, "0")}`
1182
+ }
1183
+ }
1184
+
1185
+
1186
+ if (!skipUIRecord) {
1187
+ toLocation[toLocation.length-1] += ".wav"
1188
+ toLocation = toLocation.join("/")
1189
+ }
1190
+
1191
+
1192
+ const outFolder = toLocation.split("/").reverse().slice(2, 100).reverse().join("/")
1193
+ if (!fs.existsSync(outFolder)) {
1194
+ return void window.errorModal(`${window.i18n.OUT_DIR_NOT_EXIST}:<br><br><i>${outFolder}</i><br><br>${window.i18n.YOU_CAN_CHANGE_IN_SETTINGS}`)
1195
+ }
1196
+
1197
+ // File name conflict
1198
+ const alreadyExists = fs.existsSync(toLocation)
1199
+ if (alreadyExists || shiftClick) {
1200
+
1201
+ const promptText = alreadyExists ? window.i18n.FILE_EXISTS_ADJUST : window.i18n.ENTER_FILE_NAME
1202
+
1203
+ createModal("prompt", {
1204
+ prompt: promptText,
1205
+ value: toLocation.split("/").reverse()[0].replace(".wav", `.${window.userSettings.audio.format}`)
1206
+ }).then(newFileName => {
1207
+
1208
+ let toLocationOut = toLocation.split("/").reverse()
1209
+ toLocationOut[0] = newFileName.replace(`.${window.userSettings.audio.format}`, "") + `.${window.userSettings.audio.format}`
1210
+ let outDir = toLocationOut
1211
+ outDir.shift()
1212
+
1213
+ newFileName = (newFileName.replace(`.${window.userSettings.audio.format}`, "") + `.${window.userSettings.audio.format}`).replace(/[\/\\:\*?<>"|]*/g, "")
1214
+ toLocationOut.reverse()
1215
+ toLocationOut.push(newFileName)
1216
+
1217
+ if (fs.existsSync(outDir.slice(0, outDir.length-1).join("/"))) {
1218
+ const existingFiles = fs.readdirSync(outDir.slice(0, outDir.length-1).join("/"))
1219
+ const existingFileConflict = existingFiles.filter(name => name==newFileName)
1220
+
1221
+
1222
+ const finalOutLocation = toLocationOut.join("/")
1223
+
1224
+ if (existingFileConflict.length) {
1225
+ // Remove the entry from the output files' preview
1226
+ Array.from(voiceSamples.querySelectorAll("div.sample")).forEach(sampleElem => {
1227
+ let sourceSrc = sampleElem.children[0].children[0].innerText
1228
+ sourceSrc = sourceSrc.split("/").reverse()
1229
+ const finalFileName = finalOutLocation.split("/").reverse()
1230
+
1231
+ if (sourceSrc[0] == finalFileName[0]) {
1232
+ sampleElem.parentNode.removeChild(sampleElem)
1233
+ }
1234
+ })
1235
+
1236
+ // Remove the old file and write the new one in
1237
+ fs.unlink(finalOutLocation, err => {
1238
+ if (err) {
1239
+ console.log(err)
1240
+ window.appLogger.log(`Error in keepSample: ${err}`)
1241
+ }
1242
+ window.saveFile(fromLocation, finalOutLocation, skipUIRecord)
1243
+ })
1244
+ return
1245
+ } else {
1246
+ window.saveFile(fromLocation, toLocationOut.join("/"), skipUIRecord)
1247
+ return
1248
+ }
1249
+ }
1250
+ window.saveFile(fromLocation, toLocationOut.join("/"), skipUIRecord)
1251
+ })
1252
+
1253
+ } else {
1254
+ window.saveFile(fromLocation, toLocation, skipUIRecord)
1255
+ }
1256
+ }
1257
+ }
1258
+ keepSampleButton.addEventListener("click", event => keepSampleFunction(event.shiftKey))
1259
+
1260
+
1261
+
1262
+ // Weird recursive intermittent promises to repeatedly check if the server is up yet - but it works!
1263
+ window.serverIsUp = false
1264
+ const serverStartingMessage = `${window.i18n.LOADING}...<br>${window.i18n.MAY_TAKE_A_MINUTE}<br><br>${window.i18n.STARTING_PYTHON}...`
1265
+ window.doWeirdServerStartupCheck = () => {
1266
+ const check = () => {
1267
+ return new Promise(topResolve => {
1268
+ if (window.serverIsUp) {
1269
+ topResolve()
1270
+ } else {
1271
+ (new Promise((resolve, reject) => {
1272
+ // Gather the model paths to send to the server
1273
+ const modelsPaths = {}
1274
+ Object.keys(window.userSettings).filter(key => key.includes("modelspath_")).forEach(key => {
1275
+ modelsPaths[key.split("_")[1]] = window.userSettings[key]
1276
+ })
1277
+
1278
+ doFetch(`http://localhost:8008/checkReady`, {
1279
+ method: "Post",
1280
+ body: JSON.stringify({
1281
+ device: (window.userSettings.useGPU&&window.userSettings.installation=="gpu")?"gpu":"cpu",
1282
+ modelsPaths: JSON.stringify(modelsPaths)
1283
+ })
1284
+ }).then(r => r.text()).then(r => {
1285
+ closeModal([document.querySelector("#activeModal"), modalContainer], [totdContainer, EULAContainer], true).then(() => {
1286
+ window.pluginsManager.updateUI()
1287
+ if (!window.pluginsManager.hasRunPostStartPlugins) {
1288
+ window.pluginsManager.hasRunPostStartPlugins = true
1289
+ window.pluginsManager.runPlugins(window.pluginsManager.pluginsModules["start"]["post"], event="post start")
1290
+ window.electronBrowserWindow.setProgressBar(-1)
1291
+ window.checkForWorkshopInstallations()
1292
+ }
1293
+ })
1294
+ window.serverIsUp = true
1295
+ if (window.userSettings.installation=="cpu") {
1296
+
1297
+ if (useGPUCbx.checked) {
1298
+ doFetch(`http://localhost:8008/setDevice`, {
1299
+ method: "Post",
1300
+ body: JSON.stringify({device: "cpu"})
1301
+ })
1302
+ }
1303
+ useGPUCbx.checked = false
1304
+ useGPUCbx.disabled = true
1305
+ window.userSettings.useGPU = false
1306
+ saveUserSettings()
1307
+ }
1308
+
1309
+ resolve()
1310
+ }).catch((err) => {
1311
+ reject()
1312
+ })
1313
+ })).catch(() => {
1314
+ setTimeout(async () => {
1315
+ await check()
1316
+ topResolve()
1317
+ }, 100)
1318
+ })
1319
+ }
1320
+ })
1321
+ }
1322
+
1323
+ check()
1324
+ }
1325
+ window.doWeirdServerStartupCheck()
1326
+
1327
+ modalContainer.addEventListener("click", event => {
1328
+ try {
1329
+ if (event.target==modalContainer && activeModal.dataset.type!="spinner") {
1330
+ closeModal()
1331
+ }
1332
+ } catch (e) {}
1333
+ })
1334
+
1335
+
1336
+ // Cached UI stuff
1337
+ // =========
1338
+ dialogueInput.addEventListener("keyup", (event) => {
1339
+ localStorage.setItem("dialogueInput", " "+dialogueInput.value.trim()+" ")
1340
+ window.sequenceEditor.hasChanged = true
1341
+ })
1342
+
1343
+ const dialogueInputCache = localStorage.getItem("dialogueInput")
1344
+
1345
+ if (dialogueInputCache) {
1346
+ dialogueInput.value = dialogueInputCache
1347
+ }
1348
+
1349
+ // =========
1350
+
1351
+
1352
+
1353
+
1354
+
1355
+
1356
+
1357
+
1358
+ vocoder_select.value = window.userSettings.vocoder.includes(".hg.") ? "qnd" : window.userSettings.vocoder
1359
+ window.changeVocoder = vocoder => {
1360
+ return new Promise(resolve => {
1361
+ spinnerModal(window.i18n.CHANGING_MODELS)
1362
+ doFetch(`http://localhost:8008/setVocoder`, {
1363
+ method: "Post",
1364
+ body: JSON.stringify({
1365
+ vocoder,
1366
+ modelPath: vocoder=="256_waveglow" ? window.userSettings.waveglow_path : window.userSettings.bigwaveglow_path
1367
+ })
1368
+ }).then(r=>r.text()).then((res) => {
1369
+ closeModal().then(() => {
1370
+ setTimeout(() => {
1371
+ if (res=="ENOENT") {
1372
+ vocoder_select.value = window.userSettings.vocoder
1373
+ window.errorModal(`${window.i18n.BATCH_MODEL_NOT_FOUND}.${vocoder.includes("waveglow")?" "+window.i18n.BATCH_DOWNLOAD_WAVEGLOW:""}`)
1374
+ resolve()
1375
+ } else {
1376
+ window.batch_state.lastVocoder = vocoder
1377
+ window.userSettings.vocoder = vocoder
1378
+ saveUserSettings()
1379
+ resolve()
1380
+ }
1381
+ }, 300)
1382
+ })
1383
+ })
1384
+ })
1385
+ }
1386
+ vocoder_select.addEventListener("change", () => window.changeVocoder(vocoder_select.value))
1387
+
1388
+ useSRCkbx.addEventListener("click", () => {
1389
+ let userHasSeenThisAlready = localStorage.getItem("useSRHintSeen")
1390
+ if (useSRCkbx.checked && !userHasSeenThisAlready) {
1391
+ window.confirmModal(window.i18n.USE_SR_HINT).then(resp => {
1392
+ if (resp) {
1393
+ localStorage.setItem("useSRHintSeen", "true")
1394
+ }
1395
+ })
1396
+ }
1397
+
1398
+ })
1399
+
1400
+ dialogueInput.addEventListener("contextmenu", event => {
1401
+ event.preventDefault()
1402
+ ipcRenderer.send('show-context-menu')
1403
+ })
1404
+ ipcRenderer.on('context-menu-command', (e, command) => {
1405
+ if (command=="context-copy") {
1406
+ if (dialogueInput.selectionStart != dialogueInput.selectionEnd) {
1407
+ clipboard.writeText(dialogueInput.value.slice(dialogueInput.selectionStart, dialogueInput.selectionEnd))
1408
+ }
1409
+ } else if (command=="context-paste") {
1410
+ if (clipboard.readText().length) {
1411
+ let newString = dialogueInput.value.slice(0, dialogueInput.selectionStart) + clipboard.readText() + dialogueInput.value.slice(dialogueInput.selectionEnd, dialogueInput.value.length)
1412
+ dialogueInput.value = newString
1413
+ }
1414
+ }
1415
+ })
1416
+
1417
+
1418
+ window.setupModal(workbenchIcon, workbenchContainer, () => window.initVoiceWorkbench())
1419
+
1420
+
1421
+ // Info
1422
+ // ====
1423
+ window.setupModal(infoIcon, infoContainer)
1424
+
1425
+
1426
+ // Patreon
1427
+ // =======
1428
+ window.setupModal(patreonIcon, patreonContainer, () => {
1429
+ const data = fs.readFileSync(`${path}/patreon.txt`, "utf8") + ", minermanb"
1430
+ creditsList.innerHTML = data
1431
+ })
1432
+
1433
+
1434
+ // Updates
1435
+ // =======
1436
+ app_version.innerHTML = window.appVersion
1437
+ updatesVersions.innerHTML = `${window.i18n.THIS_APP_VERSION}: ${window.appVersion}`
1438
+
1439
+ const checkForUpdates = () => {
1440
+ doFetch("http://danruta.co.uk/xvasynth_updates.txt").then(r=>r.json()).then(data => {
1441
+ fs.writeFileSync(`${path}/updates.json`, JSON.stringify(data), "utf8")
1442
+ checkUpdates.innerHTML = window.i18n.CHECK_FOR_UPDATES
1443
+ window.showUpdates()
1444
+ }).catch(() => {
1445
+ checkUpdates.innerHTML = window.i18n.CANT_REACH_SERVER
1446
+ })
1447
+ }
1448
+ window.showUpdates = () => {
1449
+ window.updatesLog = fs.readFileSync(`${path}/updates.json`, "utf8")
1450
+ window.updatesLog = JSON.parse(window.updatesLog)
1451
+ const sortedLogVersions = Object.keys(window.updatesLog).map( a => a.split('.').map( n => +n+100000 ).join('.') ).sort()
1452
+ .map( a => a.split('.').map( n => +n-100000 ).join('.') )
1453
+
1454
+ const appVersion = window.appVersion.replace("v", "")
1455
+ const appIsUpToDate = sortedLogVersions.indexOf(appVersion)==(sortedLogVersions.length-1) || sortedLogVersions.indexOf(appVersion)==-1
1456
+
1457
+ if (!appIsUpToDate) {
1458
+ update_nothing.style.display = "none"
1459
+ update_something.style.display = "block"
1460
+ updatesVersions.innerHTML = `${window.i18n.THIS_APP_VERSION}: ${appVersion}. ${window.i18n.AVAILABLE}: ${sortedLogVersions[sortedLogVersions.length-1]}`
1461
+ } else {
1462
+ updatesVersions.innerHTML = `${window.i18n.THIS_APP_VERSION}: ${appVersion}. ${window.i18n.UPTODATE}`
1463
+ }
1464
+
1465
+ updatesLogList.innerHTML = ""
1466
+ sortedLogVersions.reverse().forEach(version => {
1467
+ const versionLabel = createElem("h2", version)
1468
+ const logItem = createElem("div", versionLabel)
1469
+ window.updatesLog[version].split("\n").forEach(line => {
1470
+ logItem.appendChild(createElem("div", line))
1471
+ })
1472
+ updatesLogList.appendChild(logItem)
1473
+ })
1474
+ }
1475
+ checkForUpdates()
1476
+ window.setupModal(updatesIcon, updatesContainer)
1477
+
1478
+ checkUpdates.addEventListener("click", () => {
1479
+ checkUpdates.innerHTML = window.i18n.CHECKING_FOR_UPDATES
1480
+ checkForUpdates()
1481
+ })
1482
+ window.showUpdates()
1483
+
1484
+
1485
+ // Batch generation
1486
+ // ========
1487
+ window.setupModal(batchIcon, batchGenerationContainer)
1488
+
1489
+ // Settings
1490
+ // ========
1491
+ window.setupModal(settingsCog, settingsContainer)
1492
+
1493
+ // Change Game
1494
+ // ===========
1495
+ window.setupModal(changeGameButton, gameSelectionContainer)
1496
+ changeGameButton.addEventListener("click", () => searchGameInput.focus())
1497
+
1498
+ window.gameAssets = {}
1499
+
1500
+ window.updateGameList = (doLoadAllModels=true) => {
1501
+ gameSelectionListContainer.innerHTML = ""
1502
+ const fileNames = fs.readdirSync(`${window.path}/assets`)
1503
+
1504
+ let totalVoices = 0
1505
+ let totalGames = new Set()
1506
+
1507
+ const itemsToSort = []
1508
+ // const gameIDs = doLoadAllModels ? fileNames.filter(fn=>fn.endsWith(".json")) : Object.keys(window.games).map(gID => gID+".json")
1509
+ const gameIDs = fileNames.filter(fn=>fn.endsWith(".json"))
1510
+
1511
+ gameIDs.forEach(gameId => {
1512
+
1513
+ const metadata = fs.existsSync(`${window.path}/assets/${gameId}`) ? JSON.parse(fs.readFileSync(`${window.path}/assets/${gameId}`)) : window.games[gameId.replace(".json", "")].dummyGameTheme
1514
+ gameId = gameId.replace(".json", "")
1515
+ metadata.gameId = gameId
1516
+ const assetFile = metadata.assetFile
1517
+
1518
+ const gameSelection = createElem("div.gameSelection")
1519
+ gameSelection.style.background = `url("assets/${assetFile}")`
1520
+
1521
+ const gameName = metadata.gameName
1522
+ const gameSelectionContent = createElem("div.gameSelectionContent")
1523
+
1524
+
1525
+ let numVoices = 0
1526
+ const modelsPath = window.userSettings[`modelspath_${gameId}`]
1527
+ if (fs.existsSync(modelsPath)) {
1528
+ const files = fs.readdirSync(modelsPath)
1529
+ numVoices = files.filter(fn => fn.includes(".json")).length
1530
+ totalVoices += numVoices
1531
+ }
1532
+ if (numVoices==0) {
1533
+ gameSelectionContent.style.background = "rgba(150,150,150,0.7)"
1534
+ } else {
1535
+ gameSelectionContent.classList.add("gameSelectionContentToHover")
1536
+ totalGames.add(gameId)
1537
+ }
1538
+
1539
+ gameSelectionContent.appendChild(createElem("div", `${numVoices} ${(numVoices>1||numVoices==0)?window.i18n.VOICE_PLURAL:window.i18n.VOICE}`))
1540
+ gameSelectionContent.appendChild(createElem("div", gameName))
1541
+
1542
+ gameSelection.appendChild(gameSelectionContent)
1543
+
1544
+ window.gameAssets[gameId] = metadata
1545
+ gameSelectionContent.addEventListener("click", () => {
1546
+ voiceSearchInput.focus()
1547
+ searchGameInput.value = ""
1548
+ changeGame(metadata)
1549
+ closeModal(gameSelectionContainer)
1550
+ Array.from(gameSelectionListContainer.children).forEach(elem => elem.style.display="flex")
1551
+ })
1552
+
1553
+ itemsToSort.push([numVoices, gameSelection])
1554
+
1555
+ const modelsDir = window.userSettings[`modelspath_${gameId}`]
1556
+ if (!window.watchedModelsDirs.includes(modelsDir)) {
1557
+ window.watchedModelsDirs.push(modelsDir)
1558
+
1559
+ try {
1560
+ fs.watch(modelsDir, {recursive: false, persistent: true}, (eventType, filename) => {
1561
+ if (window.userSettings.autoReloadVoices) {
1562
+ if (doLoadAllModels) {
1563
+ loadAllModels().then(() => changeGame(metadata))
1564
+ }
1565
+ }
1566
+ })
1567
+ } catch (e) {
1568
+ // console.log(e)
1569
+ }
1570
+ }
1571
+ })
1572
+
1573
+
1574
+ itemsToSort.sort((a,b) => a[0]<b[0]?1:-1).forEach(([numVoices, elem]) => {
1575
+ gameSelectionListContainer.appendChild(elem)
1576
+ })
1577
+
1578
+ searchGameInput.addEventListener("keyup", (event) => {
1579
+
1580
+ if (event.key=="Enter") {
1581
+ const voiceElems = Array.from(gameSelectionListContainer.children).filter(elem => elem.style.display=="flex")
1582
+ if (voiceElems.length==1) {
1583
+ voiceElems[0].children[0].click()
1584
+ searchGameInput.value = ""
1585
+ }
1586
+ }
1587
+
1588
+ const voiceElems = Array.from(gameSelectionListContainer.children)
1589
+ if (searchGameInput.value.length) {
1590
+ voiceElems.forEach(elem => {
1591
+ if (elem.children[0].children[1].innerHTML.toLowerCase().includes(searchGameInput.value)) {
1592
+ elem.style.display="flex"
1593
+ } else {
1594
+ elem.style.display="none"
1595
+ }
1596
+ })
1597
+
1598
+ } else {
1599
+ voiceElems.forEach(elem => elem.style.display="block")
1600
+ }
1601
+ })
1602
+
1603
+ searchGameInput.placeholder = window.i18n.SEARCH_N_GAMES_WITH_N2_VOICES.replace("_1", Array.from(totalGames).length).replace("_2", totalVoices)
1604
+
1605
+ if (doLoadAllModels) {
1606
+ loadAllModels().then(() => {
1607
+ // Load the last selected game
1608
+ const lastGame = localStorage.getItem("lastGame")
1609
+
1610
+ if (lastGame) {
1611
+ changeGame(JSON.parse(lastGame))
1612
+ }
1613
+ })
1614
+ }
1615
+ }
1616
+ window.updateGameList()
1617
+
1618
+
1619
+ // Embeddings
1620
+ // ==========
1621
+ window.setupModal(embeddingsIcon, embeddingsContainer, () => {
1622
+ setTimeout(() => {
1623
+ if (window.embeddingsState.isReady) {
1624
+ window.embeddings_updateSize()
1625
+ }
1626
+ }, 100)
1627
+ window.embeddings_updateSize()
1628
+ window.embeddingsState.isOpen = true
1629
+ if (!window.embeddingsState.ready) {
1630
+ setTimeout(() => {
1631
+ window.embeddingsState.ready = true
1632
+ window.initEmbeddingsScene()
1633
+ setTimeout(() => {
1634
+ window.computeEmbsAndDimReduction(true)
1635
+ }, 300)
1636
+ }, 100)
1637
+ }
1638
+ }, () => {
1639
+ window.embeddingsState.isOpen = false
1640
+ })
1641
+
1642
+ // Arpabet
1643
+ // =======
1644
+ window.setupModal(arpabetIcon, arpabetContainer, () => setTimeout(()=> !window.arpabetMenuState.hasInitialised && window.refreshDictionariesList(), 100))
1645
+
1646
+
1647
+ // Plugins
1648
+ // =======
1649
+ window.setupModal(pluginsIcon, pluginsContainer)
1650
+
1651
+
1652
+ window.setupModal(style_emb_manage_btn, styleEmbeddingsContainer, window.styleEmbsModalOpenCallback)
1653
+
1654
+
1655
+ // Other
1656
+ // =====
1657
+ window.setupModal(reset_what_open_btn, resetContainer)
1658
+ window.setupModal(i18n_batch_metadata_open_btn, batchMetadataCSVContainer)
1659
+
1660
+ voiceSearchInput.addEventListener("keyup", () => {
1661
+
1662
+ if (event.key=="Enter") {
1663
+ const voiceElems = Array.from(voiceTypeContainer.children).filter(elem => elem.style.display=="block")
1664
+ if (voiceElems.length==1) {
1665
+ voiceElems[0].click()
1666
+ generateVoiceButton.click()
1667
+ voiceSearchInput.value = ""
1668
+ }
1669
+ }
1670
+
1671
+ const voiceElems = Array.from(voiceTypeContainer.children)
1672
+
1673
+ if (voiceSearchInput.value.length) {
1674
+ voiceElems.forEach(elem => {
1675
+ if (elem.innerHTML.toLowerCase().includes(voiceSearchInput.value)) {
1676
+ elem.style.display="block"
1677
+ } else {
1678
+ elem.style.display="none"
1679
+ }
1680
+ })
1681
+
1682
+ } else {
1683
+ voiceElems.forEach(elem => elem.style.display="block")
1684
+ }
1685
+ })
1686
+
1687
+ // Splash/EULA
1688
+ splashNextButton1.addEventListener("click", () => {
1689
+ splash_screen1.style.display = "none"
1690
+ splash_screen2.style.display = "flex"
1691
+ })
1692
+ EULA_closeButon.addEventListener("click", () => {
1693
+ if (EULA_accept_ckbx.checked) {
1694
+ closeModal(EULAContainer)
1695
+ window.userSettings.EULA_accepted_2023 = true
1696
+ saveUserSettings()
1697
+
1698
+ if (!window.totd_state.startupChecked) {
1699
+ window.showTipIfEnabledAndNewDay().then(() => {
1700
+ if (!serverIsUp) {
1701
+ spinnerModal(serverStartingMessage)
1702
+ }
1703
+ })
1704
+ }
1705
+
1706
+ if (!serverIsUp && totdContainer.style.display=="none") {
1707
+ window.spinnerModal(serverStartingMessage)
1708
+ }
1709
+ }
1710
+ })
1711
+ if (!Object.keys(window.userSettings).includes("EULA_accepted_2023") || !window.userSettings.EULA_accepted_2023) {
1712
+ EULAContainer.style.opacity = 0
1713
+ EULAContainer.style.display = "flex"
1714
+ requestAnimationFrame(() => requestAnimationFrame(() => EULAContainer.style.opacity = 1))
1715
+ requestAnimationFrame(() => requestAnimationFrame(() => chromeBar.style.opacity = 1))
1716
+ } else {
1717
+ window.showTipIfEnabledAndNewDay().then(() => {
1718
+ // If not, or the user closed the window quickly, show the server is starting message if still booting up
1719
+ if (!window.serverIsUp) {
1720
+ spinnerModal(serverStartingMessage)
1721
+ }
1722
+ })
1723
+ }
1724
+
1725
+
1726
+ // Links
1727
+ document.querySelectorAll('a[href^="http"]').forEach(a => a.addEventListener("click", e => {
1728
+ event.preventDefault()
1729
+ shell.openExternal(a.href)
1730
+ }))
javascript/settingsMenu.js ADDED
@@ -0,0 +1,960 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ const er = require('@electron/remote')
4
+ const saveUserSettings = () => localStorage.setItem("userSettings", JSON.stringify(window.userSettings))
5
+ // const saveUserSettings = () => {}
6
+
7
+ const deleteFolderRecursive = function (directoryPath) {
8
+ if (fs.existsSync(directoryPath)) {
9
+ fs.readdirSync(directoryPath).forEach((file, index) => {
10
+ const curPath = `${directoryPath}/${file}`;
11
+ if (fs.lstatSync(curPath).isDirectory()) {
12
+ // recurse
13
+ deleteFolderRecursive(curPath);
14
+ } else {
15
+ // delete file
16
+ fs.unlinkSync(curPath);
17
+ }
18
+ });
19
+ fs.rmdirSync(directoryPath);
20
+ }
21
+ };
22
+
23
+ // Load user settings
24
+ window.userSettings = localStorage.getItem("userSettings") ||
25
+ // window.userSettings =
26
+ {
27
+ useGPU: false,
28
+ customWindowSize:`${window.innerHeight},${window.innerWidth}`,
29
+ base_speaker: "default",
30
+ autoplay: true,
31
+ autoPlayGen: false,
32
+ audio: {
33
+ format: "wav"
34
+ },
35
+ plugins: {
36
+
37
+ }
38
+ }
39
+ if ((typeof window.userSettings)=="string") {
40
+ window.userSettings = JSON.parse(window.userSettings)
41
+ }
42
+ if (!Object.keys(window.userSettings).includes("installation")) { // For backwards compatibility
43
+ window.userSettings.installation = "cpu"
44
+ }
45
+ if (!Object.keys(window.userSettings).includes("audio")) { // For backwards compatibility
46
+ window.userSettings.audio = {format: "wav", hz: 44100, padStart: 0, padEnd: 0, pitchMult: 1, tempo: 1, nr: 5, nf: -20, deessing: 0.1}
47
+ }
48
+ if (!Object.keys(window.userSettings).includes("sliderTooltip")) { // For backwards compatibility
49
+ window.userSettings.sliderTooltip = true
50
+ }
51
+ // if (!Object.keys(window.userSettings).includes("darkPrompt")) { // For backwards compatibility
52
+ // window.userSettings.darkPrompt = false
53
+ // }
54
+ if (!Object.keys(window.userSettings).includes("showDiscordStatus")) { // For backwards compatibility
55
+ window.userSettings.showDiscordStatus = true
56
+ }
57
+ if (!Object.keys(window.userSettings).includes("prompt_fontSize")) { // For backwards compatibility
58
+ window.userSettings.prompt_fontSize = 15
59
+ }
60
+ if (!Object.keys(window.userSettings).includes("bg_gradient_opacity")) { // For backwards compatibility
61
+ window.userSettings.bg_gradient_opacity = 13
62
+ }
63
+ if (!Object.keys(window.userSettings).includes("autoReloadVoices")) { // For backwards compatibility
64
+ window.userSettings.autoReloadVoices = false
65
+ }
66
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("hz")) { // For backwards compatibility
67
+ window.userSettings.audio.hz = 44100
68
+ }
69
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("padStart")) { // For backwards compatibility
70
+ window.userSettings.audio.padStart = 0
71
+ }
72
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("padEnd")) { // For backwards compatibility
73
+ window.userSettings.audio.padEnd = 0
74
+ }
75
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("pitchMult")) { // For backwards compatibility
76
+ window.userSettings.audio.pitchMult = 1
77
+ }
78
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("tempo")) { // For backwards compatibility
79
+ window.userSettings.audio.tempo = 1
80
+ }
81
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("deessing")) { // For backwards compatibility
82
+ window.userSettings.audio.deessing = 0.1
83
+ }
84
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("nr")) { // For backwards compatibility
85
+ window.userSettings.audio.nr = 5
86
+ }
87
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("nf")) { // For backwards compatibility
88
+ window.userSettings.audio.nf = -20
89
+ }
90
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("ffmpeg")) { // For backwards compatibility
91
+ window.userSettings.audio.ffmpeg = true
92
+ }
93
+ // if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("ffmpeg_preview")) { // For backwards compatibility
94
+ // window.userSettings.audio.ffmpeg_preview = true
95
+ // }
96
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("useNR")) { // For backwards compatibility
97
+ window.userSettings.audio.useNR = true
98
+ }
99
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("bitdepth")) { // For backwards compatibility
100
+ window.userSettings.audio.bitdepth = "pcm_s32le"
101
+ }
102
+ if (!Object.keys(window.userSettings).includes("showEditorFFMPEGAmplitude")) { // For backwards compatibility
103
+ window.userSettings.showEditorFFMPEGAmplitude = false
104
+ }
105
+ if (!Object.keys(window.userSettings).includes("vocoder")) { // For backwards compatibility
106
+ window.userSettings.vocoder = "256_waveglow"
107
+ }
108
+ if (!Object.keys(window.userSettings).includes("audio") || !Object.keys(window.userSettings.audio).includes("amplitude")) { // For backwards compatibility
109
+ window.userSettings.audio.amplitude = 1
110
+ }
111
+ if (!Object.keys(window.userSettings).includes("max_filename_chars")) { // For backwards compatibility
112
+ window.userSettings.max_filename_chars = 70
113
+ }
114
+ if (!Object.keys(window.userSettings).includes("clear_text_after_synth")) { // For backwards compatibility
115
+ window.userSettings.clear_text_after_synth = false
116
+ }
117
+ if (!Object.keys(window.userSettings).includes("do_model_version_highlight")) { // For backwards compatibility
118
+ window.userSettings.do_model_version_highlight = false
119
+ }
120
+ if (!Object.keys(window.userSettings).includes("model_version_highlight")) { // For backwards compatibility
121
+ window.userSettings.model_version_highlight = 3.0
122
+ }
123
+ if (!Object.keys(window.userSettings).includes("do_pitchrangeoverride")) { // For backwards compatibility
124
+ window.userSettings.do_pitchrangeoverride = false
125
+ }
126
+ if (!Object.keys(window.userSettings).includes("pitchrangeoverride")) { // For backwards compatibility
127
+ window.userSettings.pitchrangeoverride = 6.0
128
+ }
129
+
130
+ if (!Object.keys(window.userSettings).includes("keepPaceOnNew")) { // For backwards compatibility
131
+ window.userSettings.keepPaceOnNew = true
132
+ }
133
+ if (!Object.keys(window.userSettings).includes("batchOutFolder")) { // For backwards compatibility
134
+ window.userSettings.batchOutFolder = `${__dirname.replace(/\\/g,"/")}/batch`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
135
+ }
136
+ if (!Object.keys(window.userSettings).includes("batch_clearDirFirst")) { // For backwards compatibility
137
+ window.userSettings.batch_clearDirFirst = false
138
+ }
139
+ // if (!Object.keys(window.userSettings).includes("batch_fastMode")) { // For backwards compatibility
140
+ // window.userSettings.batch_fastMode = false
141
+ // }
142
+ // if (!Object.keys(window.userSettings).includes("batch_fastModeMaxParallelizations")) { // For backwards compatibility
143
+ // window.userSettings.batch_fastModeMaxParallelizations = 1000
144
+ // }
145
+ if (!Object.keys(window.userSettings).includes("batch_json")) { // For backwards compatibility
146
+ window.userSettings.batch_json = false
147
+ }
148
+ if (!Object.keys(window.userSettings).includes("batch_useMP")) { // For backwards compatibility
149
+ window.userSettings.batch_useMP = false
150
+ }
151
+ if (!Object.keys(window.userSettings).includes("batch_MPCount")) { // For backwards compatibility
152
+ window.userSettings.batch_MPCount = 0
153
+ }
154
+ if (!Object.keys(window.userSettings).includes("batch_skipExisting")) { // For backwards compatibility
155
+ window.userSettings.batch_skipExisting = true
156
+ }
157
+ if (!Object.keys(window.userSettings).includes("batch_doGrouping")) { // For backwards compatibility
158
+ window.userSettings.batch_doGrouping = true
159
+ }
160
+ if (!Object.keys(window.userSettings).includes("batch_doVocoderGrouping")) { // For backwards compatibility
161
+ window.userSettings.batch_doVocoderGrouping = false
162
+ }
163
+ if (!Object.keys(window.userSettings).includes("batch_delimiter")) { // For backwards compatibility
164
+ window.userSettings.batch_delimiter = ","
165
+ }
166
+ if (!Object.keys(window.userSettings).includes("batch_paginationSize")) { // For backwards compatibility
167
+ window.userSettings.batch_paginationSize = 100
168
+ }
169
+ if (!Object.keys(window.userSettings).includes("defaultToHiFi")) { // For backwards compatibility
170
+ window.userSettings.defaultToHiFi = true
171
+ }
172
+ if (!Object.keys(window.userSettings).includes("batch_batchSize")) { // For backwards compatibility
173
+ window.userSettings.batch_batchSize = 1
174
+ }
175
+ if (!Object.keys(window.userSettings).includes("autoPlayGen")) { // For backwards compatibility
176
+ window.userSettings.autoPlayGen = true
177
+ }
178
+ if (!Object.keys(window.userSettings).includes("outputJSON")) { // For backwards compatibility
179
+ window.userSettings.outputJSON = true
180
+ }
181
+ if (!Object.keys(window.userSettings).includes("keepEditorOnVoiceChange")) { // For backwards compatibility
182
+ window.userSettings.keepEditorOnVoiceChange = false
183
+ }
184
+ if (!Object.keys(window.userSettings).includes("filenameNumericalSeq")) { // For backwards compatibility
185
+ window.userSettings.filenameNumericalSeq = false
186
+ }
187
+ if (!Object.keys(window.userSettings).includes("spacePadding")) { // For backwards compatibility
188
+ window.userSettings.spacePadding = true
189
+ }
190
+ if (!Object.keys(window.userSettings).includes("useErrorSound")) { // For backwards compatibility
191
+ window.userSettings.useErrorSound = false
192
+ }
193
+ if (!Object.keys(window.userSettings).includes("showTipOfTheDay")) { // For backwards compatibility
194
+ window.userSettings.showTipOfTheDay = true
195
+ }
196
+ if (!Object.keys(window.userSettings).includes("showUnseenTipOfTheDay")) { // For backwards compatibility
197
+ window.userSettings.showUnseenTipOfTheDay = false
198
+ }
199
+ if (!Object.keys(window.userSettings).includes("playChangedAudio")) { // For backwards compatibility
200
+ window.userSettings.playChangedAudio = false
201
+ }
202
+
203
+ if (!Object.keys(window.userSettings).includes("errorSoundFile")) { // For backwards compatibility
204
+ window.userSettings.errorSoundFile = `${__dirname.replace(/\\/g,"/")}/lib/xp_error.mp3`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
205
+ }
206
+ if (!Object.keys(window.userSettings).includes("plugins")) { // For backwards compatibility
207
+ window.userSettings.plugins = {}
208
+ }
209
+ if (!Object.keys(window.userSettings.plugins).includes("loadOrder")) { // For backwards compatibility
210
+ window.userSettings.plugins.loadOrder = ""
211
+ }
212
+ if (!Object.keys(window.userSettings).includes("externalAudioEditor")) { // For backwards compatibility
213
+ window.userSettings.externalAudioEditor = ""
214
+ }
215
+ if (!Object.keys(window.userSettings).includes("s2s_autogenerate")) { // For backwards compatibility
216
+ window.userSettings.s2s_autogenerate = true
217
+ }
218
+ if (!Object.keys(window.userSettings).includes("s2s_prePitchShift")) { // For backwards compatibility
219
+ window.userSettings.s2s_prePitchShift = false
220
+ }
221
+ if (!Object.keys(window.userSettings).includes("s2s_removeNoise")) { // For backwards compatibility
222
+ window.userSettings.s2s_removeNoise = false
223
+ }
224
+ if (!Object.keys(window.userSettings).includes("s2s_noiseRemStrength")) { // For backwards compatibility
225
+ window.userSettings.s2s_noiseRemStrength = 0.25
226
+ }
227
+ if (!Object.keys(window.userSettings).includes("vc_strength")) { // For backwards compatibility
228
+ window.userSettings.vc_strength = 2
229
+ }
230
+ if (!Object.keys(window.userSettings).includes("waveglow_path")) { // For backwards compatibility
231
+ window.userSettings.waveglow_path = `${__dirname.replace(/\\/g,"/")}/models/waveglow_256channels_universal_v4.pt`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
232
+ }
233
+ if (!Object.keys(window.userSettings).includes("bigwaveglow_path")) { // For backwards compatibility
234
+ window.userSettings.bigwaveglow_path = `${__dirname.replace(/\\/g,"/")}/models/nvidia_waveglowpyt_fp32_20190427.pt`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
235
+ }
236
+ if (!Object.keys(window.userSettings).includes("arpabet_paginationSize")) { // For backwards compatibility
237
+ window.userSettings.arpabet_paginationSize = 200
238
+ }
239
+ if (!Object.keys(window.userSettings).includes("output_files_pagination_size")) { // For backwards compatibility
240
+ window.userSettings.output_files_pagination_size = 25
241
+ }
242
+
243
+ const updateUIWithSettings = () => {
244
+ useGPUCbx.checked = window.userSettings.useGPU
245
+ autoplay_ckbx.checked = window.userSettings.autoplay
246
+ // setting_slidersTooltip.checked = window.userSettings.sliderTooltip
247
+ setting_defaultToHiFi.checked = window.userSettings.defaultToHiFi
248
+ setting_keepPaceOnNew.checked = window.userSettings.keepPaceOnNew
249
+ setting_autoplaygenCbx.checked = window.userSettings.autoPlayGen
250
+ // setting_darkprompt.checked = window.userSettings.darkPrompt
251
+ setting_show_discord_status.checked = window.userSettings.showDiscordStatus
252
+ setting_prompt_fontSize.value = window.userSettings.prompt_fontSize
253
+ setting_bg_gradient_opacity.value = window.userSettings.bg_gradient_opacity
254
+ setting_areload_voices.checked = window.userSettings.autoReloadVoices
255
+ setting_output_json.checked = window.userSettings.outputJSON
256
+ setting_output_num_seq.checked = window.userSettings.filenameNumericalSeq
257
+ setting_space_padding.checked = window.userSettings.spacePadding
258
+ setting_keepEditorOnVoiceChange.checked = window.userSettings.keepEditorOnVoiceChange
259
+ setting_use_error_sound.checked = window.userSettings.useErrorSound
260
+ setting_error_sound_file.value = window.userSettings.errorSoundFile
261
+
262
+ setting_showTipOfTheDay.checked = window.userSettings.showTipOfTheDay
263
+ totdShowTips.checked = window.userSettings.showTipOfTheDay
264
+ setting_showUnseenTipOfTheDay.checked = window.userSettings.showUnseenTipOfTheDay
265
+ totdShowOnlyUnseenTips.checked = window.userSettings.showUnseenTipOfTheDay
266
+
267
+ setting_playChangedAudio.checked = window.userSettings.playChangedAudio
268
+
269
+ setting_external_audio_editor.value = window.userSettings.externalAudioEditor
270
+ setting_audio_ffmpeg.checked = window.userSettings.audio.ffmpeg
271
+ // setting_audio_ffmpeg_preview.checked = window.userSettings.audio.ffmpeg && window.userSettings.audio.ffmpeg_preview
272
+ setting_audio_useNR.checked = window.userSettings.audio.ffmpeg && window.userSettings.audio.useNR
273
+ setting_audio_format.value = window.userSettings.audio.format
274
+ setting_audio_hz.value = window.userSettings.audio.hz
275
+ setting_audio_pad_start.value = window.userSettings.audio.padStart
276
+ setting_audio_pad_end.value = window.userSettings.audio.padEnd
277
+ setting_audio_pitchMult.value = window.userSettings.audio.pitchMult
278
+ setting_audio_tempo.value = window.userSettings.audio.tempo
279
+ setting_audio_deessing.value = window.userSettings.audio.deessing
280
+ setting_audio_nr.value = window.userSettings.audio.nr
281
+ setting_audio_nf.value = window.userSettings.audio.nf
282
+ setting_audio_bitdepth.value = window.userSettings.audio.bitdepth
283
+ setting_audio_amplitude.value = window.userSettings.audio.amplitude
284
+ setting_editor_audio_amplitude.value = window.userSettings.audio.amplitude
285
+ setting_show_editor_ffmpegamplitude.checked = window.userSettings.showEditorFFMPEGAmplitude
286
+ editor_amplitude_options.style.display = window.userSettings.showEditorFFMPEGAmplitude ? "flex" : "none"
287
+
288
+ // setting_s2s_autogenerate.checked = window.userSettings.s2s_autogenerate
289
+ // setting_s2s_prePitchShift.checked = window.userSettings.s2s_prePitchShift
290
+ // setting_s2s_removeNoise.checked = window.userSettings.s2s_removeNoise
291
+ // setting_s2s_noiseRemStrength.value = window.userSettings.s2s_noiseRemStrength
292
+ setting_s2s_vcstrength.value = window.userSettings.vc_strength
293
+
294
+ setting_batch_json.checked = window.userSettings.batch_json
295
+ // setting_batch_fastmode.checked = window.userSettings.batch_fastMode // No more fast modde. TODO, remove completely
296
+ // setting_batch_maxFastModeParallelizations.value = window.userSettings.batch_fastModeMaxParallelizations // No more fast modde. TODO, remove completely
297
+ setting_batch_multip.checked = window.userSettings.batch_useMP
298
+ setting_batch_multip_count.value = window.userSettings.batch_MPCount
299
+ setting_batch_delimiter.value = window.userSettings.batch_delimiter
300
+ setting_batch_paginationSize.value = window.userSettings.batch_paginationSize
301
+ setting_batch_doGrouping.checked = window.userSettings.batch_doGrouping
302
+ setting_batch_doVocoderGrouping.checked = window.userSettings.batch_doVocoderGrouping
303
+
304
+ batch_batchSizeInput.value = parseInt(window.userSettings.batch_batchSize)
305
+ batch_skipExisting.checked = window.userSettings.batch_skipExisting
306
+ batch_clearDirFirstCkbx.checked = window.userSettings.batch_clearDirFirst
307
+
308
+ setting_256waveglow_path.value = window.userSettings.waveglow_path
309
+ setting_bigwaveglow_path.value = window.userSettings.bigwaveglow_path
310
+
311
+ setting_arpabet_paginationSize.value = window.userSettings.arpabet_paginationSize
312
+ setting_output_files_pagination_size.value = window.userSettings.output_files_pagination_size
313
+ setting_max_filename_chars.value = window.userSettings.max_filename_chars
314
+ setting_clear_text_after_synth.checked = window.userSettings.clear_text_after_synth
315
+ setting_do_model_version_highlight.checked = window.userSettings.do_model_version_highlight
316
+ setting_model_version_highlight.value = window.userSettings.model_version_highlight
317
+ setting_pitchrangeoverrideEnabledCkbx.checked = window.userSettings.do_pitchrangeoverride
318
+ setting_pitchrangeoverride.value = window.userSettings.pitchrangeoverride
319
+
320
+ const [height, width] = window.userSettings.customWindowSize.split(",").map(v => parseInt(v))
321
+ ipcRenderer.send("resize", {height, width})
322
+ }
323
+ updateUIWithSettings()
324
+ saveUserSettings()
325
+
326
+
327
+ // Add the SVG code this way, because otherwise the index.html file will be spammed with way too much svg code
328
+ Array.from(window.document.querySelectorAll(".svgButton")).forEach(svgButton => {
329
+ svgButton.innerHTML = `<svg class="openFolderSVG" width="400" height="350" viewBox="0, 0, 400,350"><g id="svgg" ><path id="path0" d="M39.960 53.003 C 36.442 53.516,35.992 53.635,30.800 55.422 C 15.784 60.591,3.913 74.835,0.636 91.617 C -0.372 96.776,-0.146 305.978,0.872 310.000 C 5.229 327.228,16.605 339.940,32.351 345.172 C 40.175 347.773,32.175 347.630,163.000 347.498 L 281.800 347.378 285.600 346.495 C 304.672 342.065,321.061 332.312,330.218 319.944 C 330.648 319.362,332.162 317.472,333.581 315.744 C 335.001 314.015,336.299 312.420,336.467 312.200 C 336.634 311.980,337.543 310.879,338.486 309.753 C 340.489 307.360,342.127 305.341,343.800 303.201 C 344.460 302.356,346.890 299.375,349.200 296.575 C 351.510 293.776,353.940 290.806,354.600 289.975 C 355.260 289.144,356.561 287.505,357.492 286.332 C 358.422 285.160,359.952 283.267,360.892 282.126 C 362.517 280.153,371.130 269.561,375.632 264.000 C 376.789 262.570,380.427 258.097,383.715 254.059 C 393.790 241.689,396.099 237.993,398.474 230.445 C 403.970 212.972,394.149 194.684,376.212 188.991 C 369.142 186.747,368.803 186.724,344.733 186.779 C 330.095 186.812,322.380 186.691,322.216 186.425 C 322.078 186.203,321.971 178.951,321.977 170.310 C 321.995 146.255,321.401 141.613,317.200 133.000 C 314.009 126.457,307.690 118.680,303.142 115.694 C 302.560 115.313,301.300 114.438,300.342 113.752 C 295.986 110.631,288.986 107.881,282.402 106.704 C 280.540 106.371,262.906 106.176,220.400 106.019 L 161.000 105.800 160.763 98.800 C 159.961 75.055,143.463 56.235,120.600 52.984 C 115.148 52.208,45.292 52.225,39.960 53.003 M120.348 80.330 C 130.472 83.988,133.993 90.369,133.998 105.071 C 134.003 120.968,137.334 127.726,147.110 131.675 L 149.400 132.600 213.800 132.807 C 272.726 132.996,278.392 133.071,280.453 133.690 C 286.872 135.615,292.306 141.010,294.261 147.400 C 294.928 149.578,294.996 151.483,294.998 168.000 L 295.000 186.200 292.800 186.449 C 291.590 186.585,254.330 186.725,210.000 186.759 C 163.866 186.795,128.374 186.977,127.000 187.186 C 115.800 188.887,104.936 192.929,96.705 198.458 C 95.442 199.306,94.302 200.000,94.171 200.000 C 93.815 200.000,89.287 203.526,87.000 205.583 C 84.269 208.039,80.083 212.649,76.488 217.159 C 72.902 221.657,72.598 222.031,70.800 224.169 C 70.030 225.084,68.770 226.620,68.000 227.582 C 67.230 228.544,66.054 229.977,65.387 230.766 C 64.720 231.554,62.727 234.000,60.957 236.200 C 59.188 238.400,56.346 241.910,54.642 244.000 C 52.938 246.090,50.163 249.510,48.476 251.600 C 44.000 257.146,36.689 266.126,36.212 266.665 C 35.985 266.921,34.900 268.252,33.800 269.623 C 32.700 270.994,30.947 273.125,29.904 274.358 C 28.861 275.591,28.006 276.735,28.004 276.900 C 28.002 277.065,27.728 277.200,27.395 277.200 C 26.428 277.200,26.700 96.271,27.670 93.553 C 30.020 86.972,35.122 81.823,40.800 80.300 C 44.238 79.378,47.793 79.296,81.800 79.351 L 117.800 79.410 120.348 80.330 M369.400 214.800 C 374.239 217.220,374.273 222.468,369.489 228.785 C 367.767 231.059,364.761 234.844,364.394 235.200 C 364.281 235.310,362.373 237.650,360.154 240.400 C 357.936 243.150,354.248 247.707,351.960 250.526 C 347.732 255.736,346.053 257.821,343.202 261.400 C 341.505 263.530,340.849 264.336,334.600 271.965 C 332.400 274.651,330.204 277.390,329.720 278.053 C 329.236 278.716,328.246 279.945,327.520 280.785 C 326.794 281.624,325.300 283.429,324.200 284.794 C 323.100 286.160,321.726 287.845,321.147 288.538 C 320.568 289.232,318.858 291.345,317.347 293.233 C 308.372 304.449,306.512 306.609,303.703 309.081 C 299.300 312.956,290.855 317.633,286.000 318.886 C 277.958 320.960,287.753 320.819,159.845 320.699 C 33.557 320.581,42.330 320.726,38.536 318.694 C 34.021 316.276,35.345 310.414,42.386 301.647 C 44.044 299.583,45.940 297.210,46.600 296.374 C 47.260 295.538,48.340 294.169,49.000 293.332 C 49.660 292.495,51.550 290.171,53.200 288.167 C 54.850 286.164,57.100 283.395,58.200 282.015 C 59.300 280.635,60.920 278.632,61.800 277.564 C 62.680 276.496,64.210 274.617,65.200 273.389 C 66.190 272.162,67.188 270.942,67.418 270.678 C 67.649 270.415,71.591 265.520,76.179 259.800 C 80.767 254.080,84.634 249.310,84.773 249.200 C 84.913 249.090,87.117 246.390,89.673 243.200 C 92.228 240.010,95.621 235.780,97.213 233.800 C 106.328 222.459,116.884 215.713,128.200 213.998 C 129.300 213.832,183.570 213.719,248.800 213.748 L 367.400 213.800 369.400 214.800 " stroke="none" fill="#fbfbfb" fill-rule="evenodd"></path><path id="path1" fill-opacity="0" d="M0.000 46.800 C 0.000 72.540,0.072 93.600,0.159 93.600 C 0.246 93.600,0.516 92.460,0.759 91.066 C 3.484 75.417,16.060 60.496,30.800 55.422 C 35.953 53.648,36.338 53.550,40.317 52.981 C 46.066 52.159,114.817 52.161,120.600 52.984 C 143.463 56.235,159.961 75.055,160.763 98.800 L 161.000 105.800 220.400 106.019 C 262.906 106.176,280.540 106.371,282.402 106.704 C 288.986 107.881,295.986 110.631,300.342 113.752 C 301.300 114.438,302.560 115.313,303.142 115.694 C 307.690 118.680,314.009 126.457,317.200 133.000 C 321.401 141.613,321.995 146.255,321.977 170.310 C 321.971 178.951,322.078 186.203,322.216 186.425 C 322.380 186.691,330.095 186.812,344.733 186.779 C 368.803 186.724,369.142 186.747,376.212 188.991 C 381.954 190.814,388.211 194.832,391.662 198.914 C 395.916 203.945,397.373 206.765,399.354 213.800 C 399.842 215.533,399.922 201.399,399.958 107.900 L 400.000 0.000 200.000 0.000 L 0.000 0.000 0.000 46.800 M44.000 79.609 C 35.903 81.030,30.492 85.651,27.670 93.553 C 26.700 96.271,26.428 277.200,27.395 277.200 C 27.728 277.200,28.002 277.065,28.004 276.900 C 28.006 276.735,28.861 275.591,29.904 274.358 C 30.947 273.125,32.700 270.994,33.800 269.623 C 34.900 268.252,35.985 266.921,36.212 266.665 C 36.689 266.126,44.000 257.146,48.476 251.600 C 50.163 249.510,52.938 246.090,54.642 244.000 C 56.346 241.910,59.188 238.400,60.957 236.200 C 62.727 234.000,64.720 231.554,65.387 230.766 C 66.054 229.977,67.230 228.544,68.000 227.582 C 68.770 226.620,70.030 225.084,70.800 224.169 C 72.598 222.031,72.902 221.657,76.488 217.159 C 80.083 212.649,84.269 208.039,87.000 205.583 C 89.287 203.526,93.815 200.000,94.171 200.000 C 94.302 200.000,95.442 199.306,96.705 198.458 C 104.936 192.929,115.800 188.887,127.000 187.186 C 128.374 186.977,163.866 186.795,210.000 186.759 C 254.330 186.725,291.590 186.585,292.800 186.449 L 295.000 186.200 294.998 168.000 C 294.996 151.483,294.928 149.578,294.261 147.400 C 292.306 141.010,286.872 135.615,280.453 133.690 C 278.392 133.071,272.726 132.996,213.800 132.807 L 149.400 132.600 147.110 131.675 C 137.334 127.726,134.003 120.968,133.998 105.071 C 133.993 90.369,130.472 83.988,120.348 80.330 L 117.800 79.410 81.800 79.351 C 62.000 79.319,44.990 79.435,44.000 79.609 M128.200 213.998 C 116.884 215.713,106.328 222.459,97.213 233.800 C 95.621 235.780,92.228 240.010,89.673 243.200 C 87.117 246.390,84.913 249.090,84.773 249.200 C 84.634 249.310,80.767 254.080,76.179 259.800 C 71.591 265.520,67.649 270.415,67.418 270.678 C 67.188 270.942,66.190 272.162,65.200 273.389 C 64.210 274.617,62.680 276.496,61.800 277.564 C 60.920 278.632,59.300 280.635,58.200 282.015 C 57.100 283.395,54.850 286.164,53.200 288.167 C 51.550 290.171,49.660 292.495,49.000 293.332 C 48.340 294.169,47.260 295.538,46.600 296.374 C 45.940 297.210,44.044 299.583,42.386 301.647 C 35.345 310.414,34.021 316.276,38.536 318.694 C 42.330 320.726,33.557 320.581,159.845 320.699 C 287.753 320.819,277.958 320.960,286.000 318.886 C 290.855 317.633,299.300 312.956,303.703 309.081 C 306.512 306.609,308.372 304.449,317.347 293.233 C 318.858 291.345,320.568 289.232,321.147 288.538 C 321.726 287.845,323.100 286.160,324.200 284.794 C 325.300 283.429,326.794 281.624,327.520 280.785 C 328.246 279.945,329.236 278.716,329.720 278.053 C 330.204 277.390,332.400 274.651,334.600 271.965 C 340.849 264.336,341.505 263.530,343.202 261.400 C 346.053 257.821,347.732 255.736,351.960 250.526 C 354.248 247.707,357.936 243.150,360.154 240.400 C 362.373 237.650,364.281 235.310,364.394 235.200 C 364.761 234.844,367.767 231.059,369.489 228.785 C 374.273 222.468,374.239 217.220,369.400 214.800 L 367.400 213.800 248.800 213.748 C 183.570 213.719,129.300 213.832,128.200 213.998 M399.600 225.751 C 399.600 231.796,394.623 240.665,383.715 254.059 C 380.427 258.097,376.789 262.570,375.632 264.000 C 371.130 269.561,362.517 280.153,360.892 282.126 C 359.952 283.267,358.422 285.160,357.492 286.332 C 356.561 287.505,355.260 289.144,354.600 289.975 C 353.940 290.806,351.510 293.776,349.200 296.575 C 346.890 299.375,344.460 302.356,343.800 303.201 C 342.127 305.341,340.489 307.360,338.486 309.753 C 337.543 310.879,336.634 311.980,336.467 312.200 C 336.299 312.420,335.001 314.015,333.581 315.744 C 332.162 317.472,330.648 319.362,330.218 319.944 C 321.061 332.312,304.672 342.065,285.600 346.495 L 281.800 347.378 163.000 347.498 C 32.175 347.630,40.175 347.773,32.351 345.172 C 16.471 339.895,3.810 325.502,0.820 309.326 C 0.591 308.085,0.312 306.979,0.202 306.868 C 0.091 306.757,-0.000 327.667,-0.000 353.333 L 0.000 400.000 200.000 400.000 L 400.000 400.000 400.000 312.400 C 400.000 264.220,399.910 224.800,399.800 224.800 C 399.690 224.800,399.600 225.228,399.600 225.751 " stroke="none" fill="#050505" fill-rule="evenodd"></path></g></svg>`
330
+ })
331
+
332
+
333
+ // Installation sever handling
334
+ // =========================
335
+ settings_installation.innerHTML = window.userSettings.installation=="cpu" ? `CPU` : "CPU+GPU"
336
+ setting_change_installation.innerHTML = window.userSettings.installation=="cpu" ? `Change to CPU+GPU` : `Change to CPU`
337
+
338
+
339
+ setting_change_installation.addEventListener("click", () => {
340
+ spinnerModal("Changing installation sever...")
341
+ doFetch(`http://localhost:8008/stopServer`, {
342
+ method: "Post",
343
+ body: JSON.stringify({})
344
+ }).then(r=>r.text()).then(console.log) // The server stopping should mean this never runs
345
+ .catch(() => {
346
+
347
+ if (window.userSettings.installation=="cpu") {
348
+ window.userSettings.installation = "gpu"
349
+ useGPUCbx.disabled = false
350
+ settings_installation.innerHTML = `GPU`
351
+ setting_change_installation.innerHTML = `Change to CPU`
352
+ } else {
353
+ doFetch(`http://localhost:8008/setDevice`, {
354
+ method: "Post",
355
+ body: JSON.stringify({device: "cpu"})
356
+ })
357
+
358
+ window.userSettings.installation = "cpu"
359
+ useGPUCbx.checked = false
360
+ useGPUCbx.disabled = true
361
+ window.userSettings.useGPU = false
362
+ settings_installation.innerHTML = `CPU`
363
+ setting_change_installation.innerHTML = `Change to CPU+GPU`
364
+ }
365
+ saveUserSettings()
366
+
367
+ // Start the new server
368
+ if (window.PRODUCTION) {
369
+ window.appLogger.log(window.userSettings.installation)
370
+ window.pythonProcess = spawn(`${path}/cpython_${window.userSettings.installation}/server.exe`, {stdio: "ignore"})
371
+ } else {
372
+ window.pythonProcess = spawn("python", [`${path}/server.py`], {stdio: "ignore"})
373
+ }
374
+
375
+ window.currentModel = undefined
376
+ titleName.innerHTML = window.i18n.SELECT_VOICE_TYPE
377
+ keepSampleButton.style.display = "none"
378
+ wavesurferContainer.innerHTML = ""
379
+ generateVoiceButton.dataset.modelQuery = "null"
380
+ generateVoiceButton.dataset.modelIDLoaded = undefined
381
+ generateVoiceButton.innerHTML = window.i18n.LOAD_MODEL
382
+ generateVoiceButton.disabled = true
383
+ window.serverIsUp = false
384
+ window.doWeirdServerStartupCheck(`${window.i18n.LOADING}...<br>${window.i18n.MAY_TAKE_A_MINUTE}<br><br>${window.i18n.STARTING_PYTHON}...`)
385
+ })
386
+ })
387
+
388
+ // =========================
389
+
390
+
391
+
392
+
393
+ // Audio hardware
394
+ // ==============
395
+ navigator.mediaDevices.enumerateDevices().then(devices => {
396
+ devices = devices.filter(device => device.kind=="audiooutput" && device.deviceId!="communications")
397
+
398
+ // Base device
399
+ devices.forEach(device => {
400
+ const option = createElem("option", device.label)
401
+ option.value = device.deviceId
402
+ setting_base_speaker.appendChild(option)
403
+ })
404
+ setting_base_speaker.addEventListener("change", () => {
405
+ window.userSettings.base_speaker = setting_base_speaker.value
406
+ window.saveUserSettings()
407
+
408
+ window.document.querySelectorAll("audio").forEach(audioElem => {
409
+ audioElem.setSinkId(window.userSettings.base_speaker)
410
+ })
411
+ })
412
+ if (Object.keys(window.userSettings).includes("base_speaker")) {
413
+ setting_base_speaker.value = window.userSettings.base_speaker
414
+ } else {
415
+ window.userSettings.base_speaker = setting_base_speaker.value
416
+ window.saveUserSettings()
417
+ }
418
+
419
+ // Alternate device
420
+ devices.forEach(device => {
421
+ const option = createElem("option", device.label)
422
+ option.value = device.deviceId
423
+ setting_alt_speaker.appendChild(option)
424
+ })
425
+ setting_alt_speaker.addEventListener("change", () => {
426
+ window.userSettings.alt_speaker = setting_alt_speaker.value
427
+ window.saveUserSettings()
428
+ })
429
+ if (Object.keys(window.userSettings).includes("alt_speaker")) {
430
+ setting_alt_speaker.value = window.userSettings.alt_speaker
431
+ } else {
432
+ window.userSettings.alt_speaker = setting_alt_speaker.value
433
+ window.saveUserSettings()
434
+ }
435
+ })
436
+
437
+
438
+
439
+ // Settings Menu
440
+ // =============
441
+ useGPUCbx.addEventListener("change", () => {
442
+ spinnerModal(window.i18n.CHANGING_DEVICE)
443
+ doFetch(`http://localhost:8008/setDevice`, {
444
+ method: "Post",
445
+ body: JSON.stringify({device: useGPUCbx.checked ? "gpu" : "cpu"})
446
+ }).then(r=>r.text()).then(res => {
447
+ window.closeModal(undefined, settingsContainer)
448
+ window.userSettings.useGPU = useGPUCbx.checked
449
+ saveUserSettings()
450
+ }).catch(e => {
451
+ console.log(e)
452
+ if (e.code =="ENOENT") {
453
+ window.closeModal(undefined, settingsContainer).then(() => {
454
+ window.errorModal(window.i18n.THERE_WAS_A_PROBLEM)
455
+ })
456
+ }
457
+ })
458
+ })
459
+
460
+
461
+ const initMenuSetting = (elem, setting, type, callback=undefined, valFn=undefined) => {
462
+
463
+ valFn = valFn ? valFn : x=>x
464
+
465
+ if (type=="checkbox") {
466
+ elem.addEventListener("click", () => {
467
+ if (setting.includes(".")) {
468
+ window.userSettings[setting.split(".")[0]][setting.split(".")[1]] = valFn(elem.checked)
469
+ } else {
470
+ window.userSettings[setting] = valFn(elem.checked)
471
+ }
472
+ saveUserSettings()
473
+ if (callback) callback()
474
+ })
475
+ } else {
476
+ elem.addEventListener("change", () => {
477
+ if (setting.includes(".")) {
478
+ window.userSettings[setting.split(".")[0]][setting.split(".")[1]] = valFn(elem.value)
479
+ } else {
480
+ window.userSettings[setting] = valFn(elem.value)
481
+ }
482
+ saveUserSettings()
483
+ if (callback) callback()
484
+ })
485
+ }
486
+ }
487
+ window.initFilePickerButton = (button, input, setting, properties, filters=undefined, defaultPath=undefined, callback=undefined) => {
488
+ button.addEventListener("click", () => {
489
+ const defaultPath = input.value.replace(/\//g, "\\")
490
+ er.dialog.showOpenDialog({ properties, filters, defaultPath}).then(filePath => {
491
+ if (filePath) {
492
+ filePath = filePath.filePaths[0].replace(/\\/g, "/")
493
+ input.value = filePath.replace(/\\/g, "/")
494
+ setting = typeof(setting)=="function" ? setting() : setting
495
+ window.userSettings[setting] = filePath
496
+ saveUserSettings()
497
+ if (callback) {
498
+ callback()
499
+ }
500
+ }
501
+ })
502
+ })
503
+ }
504
+
505
+ const setPromptTheme = () => {
506
+ // if (window.userSettings.darkPrompt) {
507
+ // dialogueInput.style.backgroundColor = "rgba(25,25,25,0.9)"
508
+ // dialogueInput.style.color = "white"
509
+ // } else {
510
+ // dialogueInput.style.backgroundColor = "rgba(255,255,255,0.9)"
511
+ // dialogueInput.style.color = "black"
512
+ // }
513
+ }
514
+ const updateDiscord = () => {
515
+ let gameName = undefined
516
+ if (window.userSettings.showDiscordStatus && window.currentGame) {
517
+ gameName = window.currentGame.gameName
518
+ }
519
+ ipcRenderer.send('updateDiscord', {details: gameName})
520
+ }
521
+ const setPromptFontSize = () => {
522
+ dialogueInput.style.fontSize = `${window.userSettings.prompt_fontSize}pt`
523
+ }
524
+ const updateBackground = () => {
525
+ const background = `linear-gradient(0deg, rgba(128,128,128,${window.userSettings.bg_gradient_opacity}) 0px, rgba(0,0,0,0)), url("assets/${window.currentGame.assetFile}")`
526
+ // Fade the background image transition
527
+ rightBG1.style.background = background
528
+ rightBG2.style.opacity = 0
529
+ setTimeout(() => {
530
+ rightBG2.style.background = rightBG1.style.background
531
+ rightBG2.style.opacity = 1
532
+ }, 1000)
533
+ }
534
+
535
+ initMenuSetting(setting_autoplaygenCbx, "autoPlayGen", "checkbox")
536
+ // initMenuSetting(setting_slidersTooltip, "sliderTooltip", "checkbox")
537
+ initMenuSetting(setting_defaultToHiFi, "defaultToHiFi", "checkbox")
538
+ initMenuSetting(setting_keepPaceOnNew, "keepPaceOnNew", "checkbox")
539
+ initMenuSetting(setting_areload_voices, "autoReloadVoices", "checkbox")
540
+ initMenuSetting(setting_output_json, "outputJSON", "checkbox")
541
+ initMenuSetting(setting_keepEditorOnVoiceChange, "keepEditorOnVoiceChange", "checkbox")
542
+ initMenuSetting(setting_output_num_seq, "filenameNumericalSeq", "checkbox")
543
+ initMenuSetting(setting_space_padding, "spacePadding", "checkbox")
544
+ // initMenuSetting(setting_darkprompt, "darkPrompt", "checkbox", setPromptTheme)
545
+ initMenuSetting(setting_show_discord_status, "showDiscordStatus", "checkbox", updateDiscord)
546
+ initMenuSetting(setting_prompt_fontSize, "prompt_fontSize", "number", setPromptFontSize)
547
+ initMenuSetting(setting_bg_gradient_opacity, "bg_gradient_opacity", "number", updateBackground)
548
+ initMenuSetting(setting_use_error_sound, "useErrorSound", "checkbox")
549
+ initMenuSetting(setting_error_sound_file, "errorSoundFile", "text")
550
+ initFilePickerButton(setting_errorSoundFileBtn, setting_error_sound_file, "errorSoundFile", ["openFile"], [{name: "Audio", extensions: ["wav", "mp3", "ogg"]}])
551
+
552
+ initMenuSetting(setting_showTipOfTheDay, "showTipOfTheDay", "checkbox", () => {
553
+ totdShowTips.checked = setting_showTipOfTheDay.checked
554
+ })
555
+ initMenuSetting(totdShowTips, "showTipOfTheDay", "checkbox", () => {
556
+ setting_showTipOfTheDay.checked = totdShowTips.checked
557
+ })
558
+ initMenuSetting(setting_showUnseenTipOfTheDay, "showUnseenTipOfTheDay", "checkbox", () => {
559
+ totdShowOnlyUnseenTips.checked = setting_showUnseenTipOfTheDay.checked
560
+ })
561
+ initMenuSetting(totdShowOnlyUnseenTips, "showUnseenTipOfTheDay", "checkbox", () => {
562
+ setting_showUnseenTipOfTheDay.checked = totdShowOnlyUnseenTips.checked
563
+ })
564
+
565
+ initMenuSetting(setting_playChangedAudio, "playChangedAudio", "checkbox")
566
+
567
+
568
+ initMenuSetting(setting_external_audio_editor, "externalAudioEditor", "text")
569
+ initFilePickerButton(setting_externalEditorButton, setting_external_audio_editor, "externalAudioEditor", ["openFile"])
570
+
571
+ initMenuSetting(setting_audio_ffmpeg, "audio.ffmpeg", "checkbox", () => {
572
+ // setting_audio_ffmpeg_preview.checked = window.userSettings.audio.ffmpeg && window.userSettings.audio.ffmpeg_preview
573
+ // setting_audio_ffmpeg_preview.disabled = !window.userSettings.audio.ffmpeg
574
+ setting_audio_useNR.checked = window.userSettings.audio.ffmpeg && window.userSettings.audio.useNR
575
+ setting_audio_useNR.disabled = !window.userSettings.audio.ffmpeg
576
+ setting_audio_format.disabled = !window.userSettings.audio.ffmpeg
577
+ setting_audio_hz.disabled = !window.userSettings.audio.ffmpeg
578
+ setting_audio_pad_start.disabled = !window.userSettings.audio.ffmpeg
579
+ setting_audio_pad_end.disabled = !window.userSettings.audio.ffmpeg
580
+ setting_audio_pitchMult.disabled = !window.userSettings.audio.ffmpeg
581
+ setting_audio_tempo.disabled = !window.userSettings.audio.ffmpeg
582
+ setting_audio_deessing.disabled = !window.userSettings.audio.ffmpeg
583
+ setting_audio_nr.disabled = !window.userSettings.audio.ffmpeg
584
+ setting_audio_nf.disabled = !window.userSettings.audio.ffmpeg
585
+ setting_audio_bitdepth.disabled = !window.userSettings.audio.ffmpeg
586
+ setting_audio_amplitude.disabled = !window.userSettings.audio.ffmpeg
587
+ setting_editor_audio_amplitude.disabled = !window.userSettings.audio.ffmpeg
588
+ })
589
+ // initMenuSetting(setting_audio_ffmpeg_preview, "audio.ffmpeg_preview", "checkbox")
590
+ initMenuSetting(setting_audio_useNR, "audio.useNR", "checkbox")
591
+ initMenuSetting(setting_audio_format, "audio.format", "text")
592
+ initMenuSetting(setting_audio_hz, "audio.hz", "text", undefined, parseInt)
593
+ initMenuSetting(setting_audio_pad_start, "audio.padStart", "text", undefined, parseInt)
594
+ initMenuSetting(setting_audio_pad_end, "audio.padEnd", "text", undefined, parseInt)
595
+ initMenuSetting(setting_audio_pitchMult, "audio.pitchMult", "number", undefined, parseFloat)
596
+ initMenuSetting(setting_audio_tempo, "audio.tempo", "number", undefined, parseFloat)
597
+ initMenuSetting(setting_audio_deessing, "audio.deessing", "number", undefined, parseFloat)
598
+ initMenuSetting(setting_audio_nr, "audio.nr", "number", undefined, parseFloat)
599
+ initMenuSetting(setting_audio_nf, "audio.nf", "number", undefined, parseFloat)
600
+ initMenuSetting(setting_audio_bitdepth, "audio.bitdepth", "select")
601
+ initMenuSetting(setting_audio_amplitude, "audio.amplitude", "number", () => {
602
+ setting_editor_audio_amplitude.value = setting_audio_amplitude.value
603
+ }, parseFloat)
604
+ initMenuSetting(setting_editor_audio_amplitude, "audio.amplitude", "number", () => {
605
+ setting_audio_amplitude.value = setting_editor_audio_amplitude.value
606
+ }, parseFloat)
607
+ initMenuSetting(setting_show_editor_ffmpegamplitude, "showEditorFFMPEGAmplitude", "checkbox", () => {
608
+ editor_amplitude_options.style.display = window.userSettings.showEditorFFMPEGAmplitude ? "flex" : "none"
609
+ })
610
+
611
+ initMenuSetting(setting_batch_json, "batch_json", "checkbox")
612
+ // initMenuSetting(setting_batch_fastmode, "batch_fastMode", "checkbox") // No more fast modde. TODO, remove completely
613
+ // initMenuSetting(setting_batch_maxFastModeParallelizations, "batch_fastModeMaxParallelizations", "number") // No more fast modde. TODO, remove completely
614
+ initMenuSetting(setting_batch_multip, "batch_useMP", "checkbox")
615
+ initMenuSetting(setting_batch_multip_count, "batch_MPCount", "number", undefined, parseInt)
616
+ initMenuSetting(setting_batch_doGrouping, "batch_doGrouping", "checkbox")
617
+ initMenuSetting(setting_batch_doVocoderGrouping, "batch_doVocoderGrouping", "checkbox")
618
+ initMenuSetting(batch_clearDirFirstCkbx, "batch_clearDirFirst", "checkbox")
619
+ initMenuSetting(batch_skipExisting, "batch_skipExisting", "checkbox")
620
+ initMenuSetting(batch_batchSizeInput, "batch_batchSize", "text", undefined, parseInt)
621
+ initMenuSetting(setting_batch_delimiter, "batch_delimiter")
622
+ initMenuSetting(setting_batch_paginationSize, "batch_paginationSize", "number", undefined, parseInt)
623
+
624
+ // initMenuSetting(setting_s2s_autogenerate, "s2s_autogenerate", "checkbox")
625
+ // initMenuSetting(setting_s2s_prePitchShift, "s2s_prePitchShift", "checkbox")
626
+ // initMenuSetting(setting_s2s_removeNoise, "s2s_removeNoise", "checkbox")
627
+ // initMenuSetting(setting_s2s_noiseRemStrength, "s2s_noiseRemStrength", "number", undefined, parseFloat)
628
+ initMenuSetting(setting_s2s_vcstrength, "vc_strength", "number", undefined, parseFloat)
629
+
630
+ initMenuSetting(setting_256waveglow_path, "waveglow_path", "text")
631
+ initFilePickerButton(setting_waveglowPathButton, setting_256waveglow_path, "waveglow_path", ["openFile"], [{name: "Pytorch checkpoint", extensions: ["pt"]}])
632
+ initMenuSetting(setting_bigwaveglow_path, "bigwaveglow_path", "text")
633
+ initFilePickerButton(setting_bigwaveglowPathButton, setting_bigwaveglow_path, "bigwaveglow_path", ["openFile"], [{name: "Pytorch checkpoint", extensions: ["pt"]}])
634
+
635
+ initFilePickerButton(setting_modelsPathButton, setting_models_path_input, ()=>`modelspath_${window.currentGame.gameId}`, ["openDirectory"], undefined, undefined, ()=>window.updateGameList())
636
+ initFilePickerButton(setting_outPathButton, setting_out_path_input, ()=>`outpath_${window.currentGame.gameId}`, ["openDirectory"], undefined, undefined, ()=>{
637
+ if (window.currentModelButton) {
638
+ window.currentModelButton.click()
639
+ }
640
+ })
641
+ initMenuSetting(setting_arpabet_paginationSize, "arpabet_paginationSize", "number", undefined, parseInt)
642
+ initMenuSetting(setting_output_files_pagination_size, "output_files_pagination_size", "number", () => {
643
+ window.resetPagination()
644
+ window.refreshRecordsList()
645
+ }, parseInt)
646
+ initMenuSetting(setting_max_filename_chars, "max_filename_chars", "number", undefined, parseInt)
647
+ initMenuSetting(setting_clear_text_after_synth, "clear_text_after_synth", "checkbox")
648
+ initMenuSetting(setting_do_model_version_highlight, "do_model_version_highlight", "checkbox", ()=>window.changeGame(window.currentGame))
649
+ initMenuSetting(setting_model_version_highlight, "model_version_highlight", "number", ()=>window.changeGame(window.currentGame), parseFloat)
650
+ const updateSequenceEditorRange = () => {
651
+ if (window.sequenceEditor.isCreated && window.currentModel) {
652
+ const pitchRange = window.userSettings.pitchrangeoverride ? window.userSettings.pitchrangeoverride : window.sequenceEditor.pitchSliderRange
653
+
654
+ // Make sure to cap the existing values if upding the range to be smaller than the current values
655
+ if (window.userSettings.pitchrangeoverride) {
656
+ window.sequenceEditor.pitchNew.forEach((val,vi) => {
657
+ if (Math.abs(val)>window.userSettings.pitchrangeoverride) {
658
+ val = Math.max(-window.userSettings.pitchrangeoverride, Math.min(window.userSettings.pitchrangeoverride, val))
659
+ window.sequenceEditor.pitchNew[vi] = val
660
+ }
661
+ })
662
+ }
663
+
664
+ window.sequenceEditor.update(window.currentModel.modelType, pitchRange)
665
+ }
666
+ }
667
+ initMenuSetting(setting_pitchrangeoverrideEnabledCkbx, "do_pitchrangeoverride", "checkbox", updateSequenceEditorRange)
668
+ initMenuSetting(setting_pitchrangeoverride, "pitchrangeoverride", "number", updateSequenceEditorRange, parseFloat)
669
+
670
+
671
+ setPromptTheme()
672
+ setPromptFontSize()
673
+
674
+ setting_audio_format.disabled = !window.userSettings.audio.ffmpeg
675
+ setting_audio_hz.disabled = !window.userSettings.audio.ffmpeg
676
+ setting_audio_pad_start.disabled = !window.userSettings.audio.ffmpeg
677
+ setting_audio_pad_end.disabled = !window.userSettings.audio.ffmpeg
678
+ setting_audio_pitchMult.disabled = !window.userSettings.audio.ffmpeg
679
+ setting_audio_tempo.disabled = !window.userSettings.audio.ffmpeg
680
+ setting_audio_deessing.disabled = !window.userSettings.audio.ffmpeg
681
+ setting_audio_nr.disabled = !window.userSettings.audio.ffmpeg
682
+ setting_audio_nf.disabled = !window.userSettings.audio.ffmpeg
683
+ setting_audio_bitdepth.disabled = !window.userSettings.audio.ffmpeg
684
+ setting_audio_amplitude.disabled = !window.userSettings.audio.ffmpeg
685
+ setting_editor_audio_amplitude.disabled = !window.userSettings.audio.ffmpeg
686
+
687
+
688
+ openDiscord.addEventListener("click", () => {
689
+ shell.openExternal("https://discord.gg/nv7c6E2TzV")
690
+ })
691
+
692
+
693
+ setting_models_path_input.addEventListener("change", () => {
694
+ const gameFolder = window.currentGame.gameId
695
+
696
+ setting_models_path_input.value = setting_models_path_input.value.replace(/\/\//g, "/").replace(/\\/g,"/")
697
+ window.userSettings[`modelspath_${gameFolder}`] = setting_models_path_input.value
698
+ window.saveUserSettings()
699
+ window.loadAllModels().then(() => {
700
+ window.changeGame(window.currentGame)
701
+ })
702
+
703
+ if (!window.watchedModelsDirs.includes(setting_models_path_input.value)) {
704
+ window.watchedModelsDirs.push(setting_models_path_input.value)
705
+ fs.watch(setting_models_path_input.value, {recursive: false, persistent: true}, (eventType, filename) => {
706
+ window.changeGame(window.currentGame)
707
+ })
708
+ }
709
+ window.updateGameList()
710
+
711
+ // Gather the model paths to send to the server
712
+ const modelsPaths = {}
713
+ Object.keys(window.userSettings).filter(key => key.includes("modelspath_")).forEach(key => {
714
+ modelsPaths[key.split("_")[1]] = window.userSettings[key]
715
+ })
716
+ doFetch(`http://localhost:8008/setAvailableVoices`, {
717
+ method: "Post",
718
+ body: JSON.stringify({
719
+ modelsPaths: JSON.stringify(modelsPaths)
720
+ })
721
+ })
722
+ })
723
+
724
+
725
+ // Output path
726
+ fs.readdir(`${window.path}/models`, (err, gameDirs) => {
727
+ gameDirs.filter(name => !name.includes(".")).forEach(gameFolder => {
728
+ // Initialize the default output directory setting for this game
729
+ if (!Object.keys(window.userSettings).includes(`outpath_${gameFolder}`)) {
730
+ window.userSettings[`outpath_${gameFolder}`] = `${__dirname.replace(/\\/g,"/")}/output/${gameFolder}`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
731
+ window.saveUserSettings()
732
+ }
733
+ })
734
+ })
735
+ setting_out_path_input.addEventListener("change", () => {
736
+ const gameFolder = window.currentGame.gameId
737
+
738
+ setting_out_path_input.value = setting_out_path_input.value.replace(/\/\//g, "/").replace(/\\/g,"/")
739
+ window.userSettings[`outpath_${gameFolder}`] = setting_out_path_input.value
740
+ saveUserSettings()
741
+ if (window.currentModelButton) {
742
+ window.currentModelButton.click()
743
+ }
744
+ })
745
+ // Models path
746
+ const assetFiles = fs.readdirSync(`${window.path}/assets`)
747
+
748
+ assetFiles.filter(fn=>fn.endsWith(".json")).forEach(assetFileName => {
749
+ const gameId = assetFileName.split(".json")[0]
750
+
751
+ // Initialize the default models directory setting for this game
752
+ if (!Object.keys(window.userSettings).includes(`modelspath_${gameId}`)) {
753
+ window.userSettings[`modelspath_${gameId}`] = `${__dirname.replace(/\\/g,"/")}/models/${gameId}`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
754
+ window.userSettings[`outpath_${gameId}`] = `${__dirname.replace(/\\/g,"/")}/output/${gameId}`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
755
+ saveUserSettings()
756
+ }
757
+ })
758
+
759
+
760
+ // Batch stuff
761
+ // Output folder
762
+ batch_outputFolderInput.addEventListener("change", () => {
763
+ if (batch_outputFolderInput.value.length==0) {
764
+ window.errorModal(window.i18n.ENTER_DIR_PATH)
765
+ batch_outputFolderInput.value = window.userSettings.batchOutFolder
766
+ } else {
767
+ window.userSettings.batchOutFolder = batch_outputFolderInput.value
768
+ saveUserSettings()
769
+ }
770
+ })
771
+ batch_outputFolderInput.value = window.userSettings.batchOutFolder
772
+ // ======
773
+
774
+
775
+
776
+ reset_settings_btn.addEventListener("click", () => {
777
+ window.confirmModal(window.i18n.SURE_RESET_SETTINGS).then(confirmation => {
778
+ if (confirmation) {
779
+ window.userSettings.audio.format = "wav"
780
+ window.userSettings.audio.hz = 44100
781
+ window.userSettings.audio.padStart = 0
782
+ window.userSettings.audio.padEnd = 0
783
+ window.userSettings.audio.pitchMult = 1
784
+ window.userSettings.audio.tempo = 1
785
+ window.userSettings.audio.deessing = 0.1
786
+ window.userSettings.audio.nr = 5
787
+ window.userSettings.audio.nf = -20
788
+ window.userSettings.audio.ffmpeg = true
789
+ // window.userSettings.audio.ffmpeg_preview = true
790
+ window.userSettings.audio.useNR = true
791
+ window.userSettings.audio.amplitude = 1
792
+ window.userSettings.autoPlayGen = true
793
+ window.userSettings.autoReloadVoices = false
794
+ window.userSettings.autoplay = true
795
+ // window.userSettings.darkPrompt = false
796
+ window.userSettings.showDiscordStatus = true
797
+ window.userSettings.prompt_fontSize = 15
798
+ window.userSettings.bg_gradient_opacity = 13
799
+ window.userSettings.outputJSON = true
800
+ window.userSettings.keepEditorOnVoiceChange = false
801
+ window.userSettings.filenameNumericalSeq = false
802
+ window.userSettings.spacePadding = true
803
+ window.userSettings.useErrorSound = false
804
+ window.userSettings.showTipOfTheDay = true
805
+ window.userSettings.showUnseenTipOfTheDay = false
806
+ window.userSettings.playChangedAudio = false
807
+
808
+
809
+ window.userSettings.plugins = {}
810
+ window.userSettings.plugins.loadOrder = ""
811
+ window.userSettings.externalAudioEditor = ""
812
+ window.userSettings.s2s_autogenerate = true // TODO, remove
813
+ window.userSettings.s2s_prePitchShift = false // TODO, remove
814
+ window.userSettings.s2s_removeNoise = false // TODO, remove
815
+ window.userSettings.s2s_noiseRemStrength = 0.25 // TODO, remove
816
+ window.userSettings.vc_strength = 2
817
+
818
+ window.userSettings.defaultToHiFi = true
819
+ window.userSettings.keepPaceOnNew = true
820
+ window.userSettings.sliderTooltip = true
821
+ window.userSettings.audio.bitdepth = "pcm_s32le"
822
+ window.userSettings.showEditorFFMPEGAmplitude = false
823
+ window.userSettings.vocoder = "256_waveglow"
824
+ window.userSettings.max_filename_chars = 70
825
+ window.userSettings.clear_text_after_synth = false
826
+ window.userSettings.do_model_version_highlight = false
827
+ window.userSettings.model_version_highlight = 3.0
828
+ window.userSettings.do_pitchrangeoverride = false
829
+ window.userSettings.pitchrangeoverride = 6.0
830
+ window.userSettings.keepPaceOnNew = true
831
+ window.userSettings.arpabet_paginationSize = 200
832
+ window.userSettings.output_files_pagination_size = 25
833
+
834
+ window.userSettings.batch_clearDirFirst = false
835
+ window.userSettings.batch_fastMode = false
836
+ window.userSettings.batch_batchSize = 1
837
+ window.userSettings.batch_skipExisting = true
838
+ window.userSettings.batch_fastModeMaxParallelizations = 1000
839
+ window.userSettings.batch_json = false
840
+ window.userSettings.batch_useMP = false
841
+ window.userSettings.batch_MPCount = 0
842
+ window.userSettings.batch_doGrouping = true
843
+ window.userSettings.batch_doVocoderGrouping = false
844
+ window.userSettings.batch_delimiter = ","
845
+ window.userSettings.batch_paginationSize = 100
846
+
847
+ updateUIWithSettings()
848
+ saveUserSettings()
849
+ }
850
+ })
851
+ })
852
+ reset_paths_btn.addEventListener("click", () => {
853
+ window.confirmModal(window.i18n.SURE_RESET_PATHS).then(confirmation => {
854
+ if (confirmation) {
855
+
856
+ const pathKeys = Object.keys(window.userSettings).filter(key => key.includes("modelspath_"))
857
+ pathKeys.forEach(key => {
858
+ delete window.userSettings[key]
859
+ })
860
+
861
+ const currGame = window.currentGame ? window.currentGame.gameId : undefined
862
+
863
+ // Models and output paths
864
+ const assetFiles = fs.readdirSync(`${path}/assets`)
865
+ assetFiles.filter(fn=>fn.endsWith(".json")).forEach(jsonFileName => {
866
+
867
+ const gameId = jsonFileName.split(".")[0]
868
+ window.userSettings[`modelspath_${gameId}`] = `${__dirname.replace(/\\/g,"/")}/models/${gameId}`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
869
+ window.userSettings[`outpath_${gameId}`] = `${__dirname.replace(/\\/g,"/")}/output/${gameId}`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
870
+ if (gameId==currGame) {
871
+ setting_models_path_input.value = window.userSettings[`modelspath_${gameId}`]
872
+ setting_out_path_input.value = window.userSettings[`outpath_${gameId}`]
873
+ }
874
+ })
875
+
876
+ if (window.currentModelButton) {
877
+ window.currentModelButton.click()
878
+ }
879
+
880
+ window.userSettings.errorSoundFile = `${__dirname.replace(/\\/g,"/")}/lib/xp_error.mp3`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
881
+ window.userSettings.batchOutFolder = `${__dirname.replace(/\\/g,"/")}/batch`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app").replace("/javascript", "")
882
+ batch_outputFolderInput.value = window.userSettings.batchOutFolder
883
+
884
+ window.loadAllModels().then(() => {
885
+ if (currGame) {
886
+ window.changeGame(window.currentGame)
887
+ }
888
+ })
889
+ saveUserSettings()
890
+
891
+ // Gather the model paths to send to the server
892
+ const modelsPaths = {}
893
+ Object.keys(window.userSettings).filter(key => key.includes("modelspath_")).forEach(key => {
894
+ modelsPaths[key.split("_")[1]] = window.userSettings[key]
895
+ })
896
+ doFetch(`http://localhost:8008/setAvailableVoices`, {
897
+ method: "Post",
898
+ body: JSON.stringify({
899
+ modelsPaths: JSON.stringify(modelsPaths)
900
+ })
901
+ })
902
+ }
903
+ })
904
+ })
905
+
906
+ // Search settings
907
+ const settingItems = Array.from(settingsOptionsContainer.children)
908
+ searchSettingsInput.addEventListener("keyup", () => {
909
+
910
+ const query = searchSettingsInput.value.trim().toLowerCase()
911
+
912
+ const filteredItems = settingItems.map(el => {
913
+ if (el.tagName=="HR") {return [el, true]}
914
+ if (el.tagName=="DIV") {
915
+ if (!query.length || el.children[0].innerHTML.toLowerCase().includes(query)) {
916
+ return [el, true]
917
+ }
918
+ }
919
+ return [el, false]
920
+ })
921
+
922
+ let lastIsHR = false
923
+ filteredItems.forEach(elem => {
924
+ const [el, showIt] = elem
925
+ if (el.tagName=="HR") {
926
+ if (lastIsHR) {
927
+ el.style.display = "none"
928
+ return
929
+ }
930
+ lastIsHR = true
931
+ el.style.display = "flex"
932
+ } else {
933
+ if (showIt) {
934
+ el.style.display = "flex"
935
+ lastIsHR = false
936
+ } else {
937
+ el.style.display = "none"
938
+ }
939
+ }
940
+ })
941
+ })
942
+
943
+
944
+ const currentWindow = er.getCurrentWindow()
945
+ currentWindow.on("move", () => {
946
+ const bounds = er.getCurrentWindow().webContents.getOwnerBrowserWindow().getBounds()
947
+ window.userSettings.customWindowPosition = `${bounds.x},${bounds.y}`
948
+ saveUserSettings()
949
+ })
950
+
951
+ if (window.userSettings.customWindowPosition) {
952
+ ipcRenderer.send('updatePosition', {details: window.userSettings.customWindowPosition.split(",")})
953
+ }
954
+
955
+
956
+
957
+
958
+ window.saveUserSettings = saveUserSettings
959
+ exports.saveUserSettings = saveUserSettings
960
+ exports.deleteFolderRecursive = deleteFolderRecursive
javascript/speech2speech.js ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ window.speech2speechState = {
4
+ isReadingMic: false,
5
+ elapsedRecording: 0,
6
+ s2s_running: false
7
+ }
8
+
9
+ // Populate the microphone dropdown with the available options
10
+ navigator.mediaDevices.enumerateDevices().then(devices => {
11
+ devices = devices.filter(device => device.kind=="audioinput" && device.deviceId!="default" && device.deviceId!="communications")
12
+ devices.forEach(device => {
13
+ const option = createElem("option", device.label)
14
+ option.value = device.deviceId
15
+ setting_mic_selection.appendChild(option)
16
+ })
17
+
18
+ setting_mic_selection.addEventListener("change", () => {
19
+ window.userSettings.microphone = setting_mic_selection.value
20
+ window.saveUserSettings()
21
+ window.initMic()
22
+ })
23
+
24
+ if (Object.keys(window.userSettings).includes("microphone")) {
25
+ setting_mic_selection.value = window.userSettings.microphone
26
+ } else {
27
+ window.userSettings.microphone = setting_mic_selection.value
28
+ window.saveUserSettings()
29
+ }
30
+ })
31
+
32
+
33
+
34
+
35
+ window.initMic = () => {
36
+ return new Promise(resolve => {
37
+ const deviceId = window.userSettings.microphone
38
+
39
+ navigator.mediaDevices.getUserMedia({audio: {deviceId: deviceId}}).then(stream => {
40
+ const audio_context = new AudioContext
41
+ const input = audio_context.createMediaStreamSource(stream)
42
+ window.speech2speechState.stream = stream
43
+ resolve()
44
+
45
+ }).catch(err => {
46
+ console.log(err)
47
+ resolve()
48
+ })
49
+ })
50
+ }
51
+ window.initMic()
52
+
53
+
54
+ const animateRecordingProgress = () => {
55
+ const percentDone = (Date.now() - window.speech2speechState.elapsedRecording) / 10000
56
+
57
+ if (percentDone >= 1 && percentDone!=Infinity) {
58
+ if (window.speech2speechState.isReadingMic) {
59
+ window.stopRecord()
60
+ }
61
+ } else {
62
+ const circle = mic_progress_SVG_circle
63
+ const radius = circle.r.baseVal.value
64
+ const circumference = radius * 2 * Math.PI
65
+ const offset = circumference - percentDone * circumference
66
+
67
+ circle.style.strokeDasharray = `${circumference} ${circumference}`
68
+ circle.style.strokeDashoffset = circumference
69
+ circle.style.strokeDashoffset = Math.round(offset)
70
+
71
+ requestAnimationFrame(animateRecordingProgress)
72
+ }
73
+ }
74
+
75
+
76
+ window.clearVCMicSpinnerProgress = (percent=0) => {
77
+ const circle = mic_progress_SVG_circle
78
+ circle.style.stroke = "transparent"
79
+ const radius = circle.r.baseVal.value
80
+ const circumference = radius * 2 * Math.PI
81
+ const offset = circumference - percent * circumference
82
+
83
+ circle.style.strokeDasharray = `${circumference} ${circumference}`
84
+ circle.style.strokeDashoffset = circumference
85
+ circle.style.strokeDashoffset = Math.floor(offset)
86
+ }
87
+ window.clearVCMicSpinnerProgress = clearVCMicSpinnerProgress
88
+
89
+ window.startRecord = async () => {
90
+
91
+ doFetch(`http://localhost:8008/start_microphone_recording`, {
92
+ method: "Post"
93
+ })
94
+
95
+ window.speech2speechState.isReadingMic = true
96
+ window.speech2speechState.elapsedRecording = Date.now()
97
+ window.clearVCMicSpinnerProgress()
98
+ mic_progress_SVG_circle.style.stroke = "red"
99
+ requestAnimationFrame(animateRecordingProgress)
100
+ }
101
+
102
+ window.outputS2SRecording = (outPath, callback) => {
103
+ toggleSpinnerButtons()
104
+ doFetch(`http://localhost:8008/move_recorded_file`, {
105
+ method: "Post",
106
+ body: JSON.stringify({
107
+ file_path: outPath
108
+ })
109
+ }).then(r=>r.text()).then(res => {
110
+ callback()
111
+ })
112
+ }
113
+
114
+ window.useWavFileForspeech2speech = (fileName) => {
115
+ // let sequence = dialogueInput.value.trim().replace("…", "...")
116
+
117
+ // For some reason, the samplePlay audio element does not update the source when the file name is the same
118
+ const tempFileNum = `${Math.random().toString().split(".")[1]}`
119
+ let tempFileLocation = `${path}/output/temp-${tempFileNum}.wav`
120
+
121
+ let style_emb = window.currentModel.audioPreviewPath // Default to the preview audio file, if an embedding can't be found in the json - This shouldn't happen
122
+ try {
123
+ style_emb = window.currentModel.games[0].base_speaker_emb // If this fails, the json isn't complete
124
+ } catch (e) {
125
+ console.log(e)
126
+ }
127
+
128
+ if (window.wavesurfer) {
129
+ window.wavesurfer.stop()
130
+ wavesurferContainer.style.opacity = 0
131
+ }
132
+ window.tempFileLocation = `${__dirname.replace("/javascript", "").replace("\\javascript", "")}/output/temp-${tempFileNum}.wav`
133
+
134
+
135
+ const options = {
136
+ hz: window.userSettings.audio.hz,
137
+ padStart: window.userSettings.audio.padStart,
138
+ padEnd: window.userSettings.audio.padEnd,
139
+ bit_depth: window.userSettings.audio.bitdepth,
140
+ amplitude: window.userSettings.audio.amplitude,
141
+ pitchMult: window.userSettings.audio.pitchMult,
142
+ tempo: window.userSettings.audio.tempo,
143
+ deessing: window.userSettings.audio.deessing,
144
+ nr: window.userSettings.audio.nr,
145
+ nf: window.userSettings.audio.nf,
146
+ useNR: window.userSettings.audio.useNR,
147
+ useSR: useSRCkbx.checked,
148
+ useCleanup: useCleanupCkbx.checked
149
+ }
150
+
151
+
152
+ doFetch(`http://localhost:8008/runSpeechToSpeech`, {
153
+ method: "Post",
154
+ body: JSON.stringify({
155
+ input_path: fileName,
156
+ useSR: useSRCkbx.checked,
157
+ useCleanup: useCleanupCkbx.checked,
158
+ isBatchMode: false,
159
+
160
+ style_emb: style_emb_select.value=="default" ? window.currentModel.games[0].base_speaker_emb : style_emb_select.value.split(",").map(v=>parseFloat(v)),
161
+ audio_out_path: tempFileLocation,
162
+
163
+ doPitchShift: window.userSettings.s2s_prePitchShift,
164
+ removeNoise: window.userSettings.s2s_removeNoise, // Removed from UI
165
+ removeNoiseStrength: window.userSettings.s2s_noiseRemStrength, // Removed from UI
166
+ vc_strength: window.userSettings.vc_strength,
167
+ n_speakers: undefined,
168
+ modelPath: undefined,
169
+ voiceId: undefined,
170
+
171
+ options: JSON.stringify(options)
172
+ })
173
+ }).then(r=>r.text()).then(res => {
174
+ // This block of code sometimes gets called before the audio file has actually finished flushing to file
175
+ // I need a better way to make sure that this doesn't get called until it IS finished, but "for now",
176
+ // I've set up some recursive re-attempts, below doTheRest
177
+ window.clearVCMicSpinnerProgress()
178
+ mic_progress_SVG.style.animation = "none"
179
+
180
+ if (res=="TOO_SHORT") {
181
+ window.toggleSpinnerButtons(true)
182
+ window.errorModal(`<h3>${window.i18n.VC_TOO_SHORT}</h3>`)
183
+ return
184
+ }
185
+
186
+ generateVoiceButton.disabled = true
187
+ let hasLoaded = false
188
+ let numRetries = 0
189
+ window.toggleSpinnerButtons(true)
190
+
191
+ const doTheRest = () => {
192
+ if (hasLoaded) {
193
+ return
194
+ }
195
+ // window.wavesurfer = undefined
196
+ tempFileLocation = tempFileLocation.replaceAll(/\\/g, "/")
197
+ tempFileLocation = tempFileLocation.replaceAll('/resources/app/resources/app', "/resources/app")
198
+ tempFileLocation = tempFileLocation.replaceAll('/resources/app', "")
199
+
200
+ dialogueInput.value = ""
201
+ textEditorElem.innerHTML = ""
202
+ window.isGenerating = false
203
+
204
+ window.speech2speechState.s2s_running = true
205
+
206
+
207
+
208
+ if (res.includes("Traceback")) {
209
+ window.errorModal(`<h3>${window.i18n.SOMETHING_WENT_WRONG}</h3>${res.replaceAll("\n", "<br>")}`)
210
+
211
+ } else if (res.includes("ERROR:APP_VERSION")) {
212
+ const speech2speechModelVersion = "v"+res.split(",")[1]
213
+ window.errorModal(`${window.i18n.ERR_XVASPEECH_MODEL_VERSION.replace("_1", speech2speechModelVersion)} ${window.appVersion}`)
214
+ } else {
215
+
216
+ keepSampleButton.disabled = false
217
+ window.tempFileLocation = tempFileLocation
218
+
219
+ // Wavesurfer
220
+ if (!window.wavesurfer) {
221
+ window.initWaveSurfer(window.tempFileLocation)
222
+ } else {
223
+ window.wavesurfer.load(window.tempFileLocation)
224
+ }
225
+ window.wavesurfer.on("ready", () => {
226
+
227
+ hasLoaded = true
228
+ wavesurferContainer.style.opacity = 1
229
+
230
+ if (window.userSettings.autoPlayGen) {
231
+
232
+ if (window.userSettings.playChangedAudio) {
233
+ const playbackStartEnd = window.sequenceEditor.getChangedTimeStamps(start_index, end_index, window.wavesurfer.getDuration())
234
+ if (playbackStartEnd) {
235
+ wavesurfer.play(playbackStartEnd[0], playbackStartEnd[1])
236
+ } else {
237
+ wavesurfer.play()
238
+ }
239
+ } else {
240
+ wavesurfer.play()
241
+ }
242
+ window.sequenceEditor.adjustedLetters = new Set()
243
+ samplePlayPause.innerHTML = window.i18n.PAUSE
244
+ }
245
+ })
246
+
247
+ // Persistance across sessions
248
+ localStorage.setItem("tempFileLocation", tempFileLocation)
249
+ generateVoiceButton.innerHTML = window.i18n.GENERATE_VOICE
250
+
251
+ if (window.userSettings.s2s_autogenerate) {
252
+ speech2speechState.s2s_autogenerate = true
253
+ generateVoiceButton.click()
254
+ }
255
+
256
+ keepSampleButton.dataset.newFileLocation = `${window.userSettings[`outpath_${window.currentGame.gameId}`]}/${title.dataset.modelId}/vc_${tempFileNum}.wav`
257
+ keepSampleButton.disabled = false
258
+ keepSampleButton.style.display = "block"
259
+ samplePlayPause.style.display = "block"
260
+
261
+ setTimeout(doTheRest, 100)
262
+ }
263
+
264
+ }
265
+
266
+ doTheRest()
267
+ }).catch(e => {
268
+ console.log(e)
269
+ window.toggleSpinnerButtons(true)
270
+ window.errorModal(`<h3>${window.i18n.SOMETHING_WENT_WRONG}</h3>`)
271
+ mic_progress_SVG.style.animation = "none"
272
+ })
273
+ }
274
+
275
+ window.stopRecord = (cancelled) => {
276
+ fs.writeFileSync(`${window.path}/python/temp_stop_recording`, "")
277
+
278
+ if (!cancelled) {
279
+ window.clearVCMicSpinnerProgress(0.35)
280
+ mic_progress_SVG.style.animation = "spin 1.5s linear infinite"
281
+ mic_progress_SVG_circle.style.stroke = "white"
282
+ const fileName = `${__dirname.replace("\\javascript", "").replace("/javascript", "").replace(/\\/g,"/")}/output/recorded_file.wav`
283
+
284
+ window.sequenceEditor.clear()
285
+
286
+ window.outputS2SRecording(fileName, () => {
287
+ window.useWavFileForspeech2speech(fileName)
288
+ })
289
+ }
290
+
291
+ window.speech2speechState.isReadingMic = false
292
+ window.speech2speechState.elapsedRecording = 0
293
+ window.clearVCMicSpinnerProgress()
294
+ }
295
+
296
+ window.micClickHandler = (ctrlKey) => {
297
+ if (window.speech2speechState.isReadingMic) {
298
+ window.stopRecord()
299
+ } else {
300
+ if (window.currentModel && generateVoiceButton.innerHTML == window.i18n.GENERATE_VOICE) {
301
+ if (window.currentModel.modelType.toLowerCase()=="xvapitch") {
302
+ window.startRecord()
303
+ }
304
+ } else {
305
+ window.errorModal(window.i18n.LOAD_TARGET_MODEL)
306
+ }
307
+ }
308
+ }
309
+ mic_SVG.addEventListener("mouseenter", () => {
310
+ if (!window.currentModel || window.currentModel.modelType.toLowerCase()!="xvapitch") {
311
+ s2s_voiceId_selected_label.style.display = "inline-block"
312
+ }
313
+ })
314
+ mic_SVG.addEventListener("mouseleave", () => {
315
+ s2s_voiceId_selected_label.style.display = "none"
316
+ })
317
+ mic_SVG.addEventListener("click", event => window.micClickHandler(event.ctrlKey))
318
+ mic_SVG.addEventListener("contextmenu", () => {
319
+ if (window.speech2speechState.isReadingMic) {
320
+ window.stopRecord(true)
321
+ } else {
322
+ const audioPreview = createElem("audio", {autoplay: false}, createElem("source", {
323
+ src: `${__dirname.replace("\\javascript", "").replace("/javascript", "").replace(/\\/g,"/")}/output/recorded_file_post${window.userSettings.s2s_prePitchShift?"_praat":""}.wav`
324
+ }))
325
+ audioPreview.setSinkId(window.userSettings.base_speaker)
326
+ }
327
+ })
328
+ window.clearVCMicSpinnerProgress()
329
+
330
+
331
+
332
+ // File dragging
333
+ window.uploadS2SFile = (eType, event) => {
334
+
335
+ if (["dragenter", "dragover"].includes(eType)) {
336
+ clearVCMicSpinnerProgress(1)
337
+ mic_progress_SVG_circle.style.stroke = "white"
338
+ }
339
+ if (["dragleave", "drop"].includes(eType)) {
340
+ window.clearVCMicSpinnerProgress()
341
+ }
342
+
343
+ event.preventDefault()
344
+ event.stopPropagation()
345
+
346
+ if (eType=="drop") {
347
+ if (window.currentModel && generateVoiceButton.innerHTML == window.i18n.GENERATE_VOICE) {
348
+ const dataTransfer = event.dataTransfer
349
+ const files = Array.from(dataTransfer.files)
350
+ const file = files[0]
351
+
352
+ if (!file.name.endsWith(".wav")) {
353
+ window.errorModal(window.i18n.ONLY_WAV_S2S)
354
+ return
355
+ }
356
+
357
+ clearVCMicSpinnerProgress(0.35)
358
+ // mic_progress_SVG.style.animation = "spin 1.5s linear infinite"
359
+ // mic_progress_SVG_circle.style.stroke = "white"
360
+
361
+ const fileName = `${__dirname.replace("\\javascript", "").replace("/javascript", "").replace(/\\/g,"/")}/output/recorded_file.wav`
362
+ fs.copyFileSync(file.path, fileName)
363
+ window.sequenceEditor.clear()
364
+ toggleSpinnerButtons()
365
+ window.useWavFileForspeech2speech(fileName)
366
+ } else {
367
+ window.errorModal(window.i18n.LOAD_TARGET_MODEL)
368
+ }
369
+ }
370
+ }
371
+
372
+ micContainer.addEventListener("dragenter", event => window.uploadS2SFile("dragenter", event), false)
373
+ micContainer.addEventListener("dragleave", event => window.uploadS2SFile("dragleave", event), false)
374
+ micContainer.addEventListener("dragover", event => window.uploadS2SFile("dragover", event), false)
375
+ micContainer.addEventListener("drop", event => window.uploadS2SFile("drop", event), false)
376
+
377
+ // Disable page navigation on badly dropped file
378
+ window.document.addEventListener("dragover", event => event.preventDefault(), false)
379
+ window.document.addEventListener("drop", event => event.preventDefault(), false)
javascript/style_embeddings.js ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ window.allStyleEmbs = {}
4
+ window.styleEmbsMenuState = {
5
+ embeddingsDir: `${window.path}/embeddings`,
6
+ hasChangedEmb: false, // For clearing the use of the editor state when re-generating a line with a different embedding
7
+ selectedEmb: undefined,
8
+ activatedEmbeddings: {}
9
+ }
10
+
11
+
12
+ window.loadStyleEmbsFromDisk = () => {
13
+ window.allStyleEmbs = {}
14
+
15
+ // Read the activated embeddings file
16
+ window.styleEmbsMenuState.activatedEmbeddings = {}
17
+ if (fs.existsSync(`./embeddings.txt`)) {
18
+ const embeddingsEnabled = fs.readFileSync(`./embeddings.txt`, "utf8").split("\n")
19
+ embeddingsEnabled.forEach(emb => {
20
+ window.styleEmbsMenuState.activatedEmbeddings[emb.replace("*","")] = emb.includes("*")
21
+ })
22
+ }
23
+
24
+
25
+ // Read all the embedding files
26
+ fs.mkdirSync(window.styleEmbsMenuState.embeddingsDir, {recursive: true})
27
+ const embFiles = fs.readdirSync(window.styleEmbsMenuState.embeddingsDir)
28
+ embFiles.forEach(jsonFName => {
29
+ const jsonData = JSON.parse(fs.readFileSync(`${window.styleEmbsMenuState.embeddingsDir}/${jsonFName}`))
30
+ jsonData.fileName = `${window.styleEmbsMenuState.embeddingsDir}/${jsonFName}`
31
+ if (!Object.keys(window.styleEmbsMenuState.activatedEmbeddings).includes(jsonData.emb_id)) {
32
+ window.styleEmbsMenuState.activatedEmbeddings[jsonData.emb_id] = true
33
+ }
34
+ jsonData.enabled = window.styleEmbsMenuState.activatedEmbeddings[jsonData.emb_id]
35
+ window.allStyleEmbs[jsonData.voiceId] = window.allStyleEmbs[jsonData.voiceId] || []
36
+
37
+ window.allStyleEmbs[jsonData.voiceId].push(jsonData)
38
+ })
39
+ window.saveEnabledStyleEmbs()
40
+ }
41
+ window.saveEnabledStyleEmbs = () => {
42
+ fs.writeFileSync(`./embeddings.txt`, Object.keys(window.styleEmbsMenuState.activatedEmbeddings).map(key => {
43
+ return `${window.styleEmbsMenuState.activatedEmbeddings[key]?"*":""}${key}`
44
+ }).join("\n"), "utf8")
45
+ }
46
+ window.loadStyleEmbsFromDisk()
47
+
48
+
49
+ window.resetStyleEmbFields = () => {
50
+ styleEmbAuthorInput.value = ""
51
+ styleEmbGameIdInput.value = ""
52
+ styleEmbVoiceIdInput.value = ""
53
+ styleEmbNameInput.value = ""
54
+ styleEmbDescriptionInput.value = ""
55
+ styleEmbIdInput.value = ""
56
+ wavFilepathForEmbComputeInput.value = ""
57
+ styleEmbValuesInput.value = ""
58
+
59
+ styleEmbGameIdInput.value = window.currentGame.gameId
60
+ if (window.currentModel) {
61
+ styleEmbVoiceIdInput.value = window.currentModel.voiceId
62
+ }
63
+ }
64
+
65
+ window.refreshStyleEmbsTable = () => {
66
+ styleembsRecordsContainer.innerHTML = ""
67
+ window.styleEmbsMenuState.selectedEmb = undefined
68
+ styleEmbDelete.disabled = true
69
+ window.resetStyleEmbFields()
70
+
71
+
72
+ Object.keys(window.allStyleEmbs).sort().forEach(key => {
73
+ window.allStyleEmbs[key].forEach((emb,ei) => {
74
+ const record = createElem("div")
75
+ const enabledCkbx = createElem("input", {type: "checkbox"})
76
+ enabledCkbx.checked = emb.enabled
77
+ record.appendChild(createElem("div", enabledCkbx))
78
+
79
+ const embName = createElem("div", emb["embeddingName"])
80
+ embName.title = emb["embeddingName"]
81
+ record.appendChild(embName)
82
+
83
+ const embGameID = createElem("div", emb["gameId"])
84
+ embGameID.title = emb["gameId"]
85
+ record.appendChild(embGameID)
86
+
87
+ const embVoiceID = createElem("div", emb["voiceId"])
88
+ embVoiceID.title = emb["voiceId"]
89
+ record.appendChild(embVoiceID)
90
+
91
+ const embDescription = createElem("div", emb["description"]||"")
92
+ embDescription.title = emb["description"]||""
93
+ record.appendChild(embDescription)
94
+
95
+ const embID = createElem("div", emb["emb_id"])
96
+ embID.title = emb["emb_id"]
97
+ record.appendChild(embID)
98
+
99
+ const embVersion = createElem("div", emb["version"]||"1.0")
100
+ embVersion.title = emb["version"]||"1.0"
101
+ record.appendChild(embVersion)
102
+
103
+ enabledCkbx.addEventListener("click", () => {
104
+ window.allStyleEmbs[key][ei].enabled = !window.allStyleEmbs[key][ei].enabled
105
+ window.styleEmbsMenuState.activatedEmbeddings[emb["emb_id"]] = window.allStyleEmbs[key][ei].enabled
106
+ window.saveEnabledStyleEmbs()
107
+
108
+ if (window.currentModel) {
109
+ window.loadStyleEmbsForVoice(window.currentModel)
110
+ }
111
+ })
112
+
113
+
114
+ record.addEventListener("click", (e) => {
115
+ if (e.target==enabledCkbx || e.target.nodeName=="BUTTON") {
116
+ return
117
+ }
118
+ // Clear visual selection of the old selected item, if there was already an item selected before
119
+ if (window.styleEmbsMenuState.selectedEmb) {
120
+ window.styleEmbsMenuState.selectedEmb[0].style.background = "none"
121
+ Array.from(window.styleEmbsMenuState.selectedEmb[0].children).forEach(child => child.style.color = "white")
122
+ }
123
+
124
+ window.styleEmbsMenuState.selectedEmb = [record, emb]
125
+
126
+ // Visually show that this row is selected
127
+ window.styleEmbsMenuState.selectedEmb[0].style.background = "white"
128
+ Array.from(window.styleEmbsMenuState.selectedEmb[0].children).forEach(child => child.style.color = "black")
129
+ styleEmbDelete.disabled = false
130
+
131
+
132
+ // Populate the edit fields
133
+ styleEmbAuthorInput.value = emb.author||""
134
+ styleEmbGameIdInput.value = emb.gameId||""
135
+ styleEmbVoiceIdInput.value = emb.voiceId||""
136
+ styleEmbNameInput.value = emb.embeddingName||""
137
+ styleEmbDescriptionInput.value = emb.description||""
138
+ styleEmbIdInput.value = emb.emb_id||""
139
+ wavFilepathForEmbComputeInput.value = ""
140
+ styleEmbValuesInput.value = emb.emb||""
141
+ })
142
+
143
+ styleembsRecordsContainer.appendChild(record)
144
+ })
145
+ })
146
+ }
147
+ styleembs_main.addEventListener("click", (e) => {
148
+ if (e.target == styleembs_main) {
149
+ window.refreshStyleEmbsTable()
150
+ }
151
+ })
152
+
153
+ styleEmbSave.addEventListener("click", () => {
154
+
155
+ const missingFieldsValues = []
156
+
157
+ if (!styleEmbAuthorInput.value.trim().length) {
158
+ missingFieldsValues.push(window.i18n.AUTHOR)
159
+ }
160
+ if (!styleEmbGameIdInput.value.trim().length) {
161
+ missingFieldsValues.push(window.i18n.GAME_ID)
162
+ }
163
+ if (!styleEmbVoiceIdInput.value.trim().length) {
164
+ missingFieldsValues.push(window.i18n.VOICE_ID)
165
+ }
166
+ if (!styleEmbNameInput.value.trim().length) {
167
+ missingFieldsValues.push(window.i18n.EMB_NAME)
168
+ }
169
+ if (!styleEmbIdInput.value.trim().length) {
170
+ missingFieldsValues.push(window.i18n.EMB_ID)
171
+ }
172
+ if (!styleEmbValuesInput.value.trim().length) {
173
+ missingFieldsValues.push(window.i18n.STYLE_EMB_VALUES)
174
+ }
175
+
176
+ if (missingFieldsValues.length) {
177
+ window.errorModal(window.i18n.ERROR_MISSING_FIELDS.replace("_1", missingFieldsValues.join(", ")))
178
+ } else {
179
+ let outputFilename
180
+ if (window.styleEmbsMenuState.selectedEmb) {
181
+ outputFilename = window.styleEmbsMenuState.selectedEmb[1].fileName
182
+ } else {
183
+ outputFilename = `${window.styleEmbsMenuState.embeddingsDir}/${styleEmbVoiceIdInput.value.trim().toLowerCase()}.${styleEmbGameIdInput.value.trim().toLowerCase()}.${styleEmbIdInput.value.trim().toLowerCase()}.${styleEmbAuthorInput.value.trim().toLowerCase()}.json`
184
+ }
185
+
186
+ const jsonData = {
187
+ "author": styleEmbAuthorInput.value.trim(),
188
+ "version": "1.0", // Should I make this editable in the UI?
189
+ "gameId": styleEmbGameIdInput.value.trim().toLowerCase(),
190
+ "voiceId": styleEmbVoiceIdInput.value.trim().toLowerCase(),
191
+ "description": styleEmbDescriptionInput.value.trim()||"",
192
+ "embeddingName": styleEmbNameInput.value.trim(),
193
+ "emb": styleEmbValuesInput.value.trim().split(",").map(v=>parseFloat(v)),
194
+ "emb_id": styleEmbIdInput.value.trim()
195
+ }
196
+
197
+ fs.writeFileSync(outputFilename, JSON.stringify(jsonData, null, 4), "utf8")
198
+ window.loadStyleEmbsFromDisk()
199
+ window.refreshStyleEmbsTable()
200
+ }
201
+ if (window.currentModel) {
202
+ window.loadStyleEmbsForVoice(window.currentModel)
203
+ }
204
+ })
205
+
206
+
207
+ styleEmbDelete.addEventListener("click", () => {
208
+ window.confirmModal(window.i18n.CONFIRM_DELETE_STYLE_EMB).then(response => {
209
+ if (response) {
210
+ fs.unlinkSync(window.styleEmbsMenuState.selectedEmb[1].fileName)
211
+ window.loadStyleEmbsFromDisk()
212
+ window.refreshStyleEmbsTable()
213
+ if (window.currentModel) {
214
+ window.loadStyleEmbsForVoice(window.currentModel)
215
+ }
216
+ }
217
+ })
218
+ })
219
+
220
+
221
+ // Return the default embedding, plus any other ones
222
+ window.loadStyleEmbsForVoice = (currentModel) => {
223
+
224
+ const embeddings = {}
225
+
226
+ // Add the default option from the model json
227
+ embeddings["default"] = [window.i18n.DEFAULT, currentModel.games[0].base_speaker_emb] // TODO, specialize to specific game?
228
+
229
+ // Load any other style embeddings available
230
+ if (Object.keys(window.allStyleEmbs).includes(currentModel.voiceId)) {
231
+ window.allStyleEmbs[currentModel.voiceId].forEach(loadedStyleEmb => {
232
+ if (loadedStyleEmb.enabled) {
233
+ embeddings[loadedStyleEmb.emb_id] = [loadedStyleEmb.embeddingName, loadedStyleEmb.emb]
234
+ }
235
+ })
236
+ }
237
+
238
+
239
+ // Add every option to the embeddings selection dropdown
240
+ style_emb_select.innerHTML = ""
241
+ Array.from(seq_edit_edit_select.children).forEach(option => {
242
+ if (option.value.startsWith("style_")) {
243
+ seq_edit_edit_select.removeChild(option)
244
+ }
245
+ })
246
+ // Add Default first
247
+ const opt = createElem("option", embeddings["default"][0])
248
+ opt.value = embeddings["default"][1].join(",")
249
+ style_emb_select.appendChild(opt)
250
+
251
+ Object.keys(embeddings).forEach(key => {
252
+ if (key=="default") {
253
+ return
254
+ }
255
+ const opt = createElem("option", embeddings[key][0])
256
+ opt.value = embeddings[key][1].join(",")
257
+ style_emb_select.appendChild(opt)
258
+ })
259
+
260
+
261
+ // First remove all existing styles from the dropdown
262
+ Array.from(seq_edit_view_select.children).forEach(elem => {
263
+ if (elem.value.startsWith("style")) {
264
+ seq_edit_view_select.removeChild(elem)
265
+ }
266
+ })
267
+
268
+ // Add every option (except Default) to the sliders viewing/editing dropdowns
269
+ Object.keys(embeddings).forEach(key => {
270
+ if (key=="default") {
271
+ return
272
+ }
273
+ const opt = createElem("option", `${window.i18n.STYLE_EMB_IS} ${embeddings[key][0]}`)
274
+ opt.value = `style_${key}`
275
+ seq_edit_view_select.appendChild(opt)
276
+
277
+ const opt2 = createElem("option", `${window.i18n.STYLE_EMB_IS} ${embeddings[key][0]}`)
278
+ opt2.value = `style_${key}`
279
+ seq_edit_edit_select.appendChild(opt2)
280
+ })
281
+
282
+ window.appState.currentModelEmbeddings = embeddings
283
+ }
284
+ style_emb_select.addEventListener("change", () => window.styleEmbsMenuState.hasChangedEmb)
285
+
286
+
287
+
288
+ window.styleEmbsModalOpenCallback = () => {
289
+ styleEmbGameIdInput.value = window.currentGame.gameId
290
+ if (window.currentModel) {
291
+ styleEmbVoiceIdInput.value = window.currentModel.voiceId
292
+ }
293
+ window.refreshStyleEmbsTable()
294
+ }
295
+ window.dragDropWavForEmbComputeFilepathInput = (eType, event) => {
296
+ if (["dragenter", "dragover"].includes(eType)) {
297
+ wavFileDragDropArea.style.background = "#5b5b5b"
298
+ wavFileDragDropArea.style.color = "white"
299
+ }
300
+ if (["dragleave", "drop"].includes(eType)) {
301
+ wavFileDragDropArea.style.background = "rgba(0,0,0,0)"
302
+ wavFileDragDropArea.style.color = "white"
303
+ }
304
+
305
+ event.preventDefault()
306
+ event.stopPropagation()
307
+
308
+ const dataLines = []
309
+
310
+ if (eType=="drop") {
311
+ const dataTransfer = event.dataTransfer
312
+ const files = Array.from(dataTransfer.files)
313
+
314
+ if (files[0].path.endsWith(".wav")) {
315
+ wavFilepathForEmbComputeInput.value = String(files[0].path).replaceAll(/\\/g, "/")
316
+ } else {
317
+ window.errorModal(window.i18n.ERROR_FILE_MUST_BE_WAV)
318
+ }
319
+ }
320
+ }
321
+
322
+ wavFileDragDropArea.addEventListener("dragenter", event => window.dragDropWavForEmbComputeFilepathInput("dragenter", event), false)
323
+ wavFileDragDropArea.addEventListener("dragleave", event => window.dragDropWavForEmbComputeFilepathInput("dragleave", event), false)
324
+ wavFileDragDropArea.addEventListener("dragover", event => window.dragDropWavForEmbComputeFilepathInput("dragover", event), false)
325
+ wavFileDragDropArea.addEventListener("drop", event => window.dragDropWavForEmbComputeFilepathInput("drop", event), false)
326
+
327
+
328
+ getStyleEmbeddingBtn.addEventListener("click", async () => {
329
+ if (!wavFilepathForEmbComputeInput.value.trim().length) {
330
+ window.errorModal(window.i18n.ERROR_NEED_WAV_FILE)
331
+ } else {
332
+ const embedding = await window.getSpeakerEmbeddingFromFilePath(wavFilepathForEmbComputeInput.value)
333
+ styleEmbValuesInput.value = embedding
334
+ }
335
+ })
javascript/textarea.js ADDED
@@ -0,0 +1,580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+
4
+ // This doesn't seem to work anymore. TODO, make it work again
5
+ function getCaretPosition() {
6
+ var sel = document.selection, range, rect;
7
+ var x = 0, y = 0;
8
+ if (sel) {
9
+ if (sel.type != "Control") {
10
+ range = sel.createRange();
11
+ range.collapse(true);
12
+ x = range.boundingLeft;
13
+ y = range.boundingTop;
14
+ }
15
+ } else if (window.getSelection) {
16
+ sel = window.getSelection();
17
+ if (sel.rangeCount) {
18
+ range = sel.getRangeAt(0).cloneRange();
19
+ if (range.getClientRects) {
20
+ range.collapse(true);
21
+ if (range.getClientRects().length>0){
22
+ rect = range.getClientRects()[0];
23
+ x = rect.left;
24
+ y = rect.top;
25
+ }
26
+ }
27
+ // Fall back to inserting a temporary element
28
+ if (x == 0 && y == 0) {
29
+ var span = document.createElement("span");
30
+ if (span.getClientRects) {
31
+ // Ensure span has dimensions and position by
32
+ // adding a zero-width space character
33
+ span.appendChild( document.createTextNode("\u200b") );
34
+ range.insertNode(span);
35
+ rect = span.getClientRects()[0];
36
+ x = rect.left;
37
+ y = rect.top;
38
+ var spanParent = span.parentNode;
39
+ spanParent.removeChild(span);
40
+
41
+ // Glue any broken text nodes back together
42
+ spanParent.normalize();
43
+ }
44
+ }
45
+ }
46
+ }
47
+ return { x: x, y: y };
48
+ }
49
+
50
+
51
+ window.ARPABET_SYMBOLS_v2 = [
52
+ "AA0", "AA1", "AA2", "AE0", "AE1", "AE2", "AH0", "AH1", "AH2", "AO0", "AO1","AO2", "AW0", "AW1", "AW2", "AY0", "AY1", "AY2", "B", "CH", "D", "DH",
53
+ "EH0", "EH1", "EH2", "ER0", "ER1", "ER2", "EY0", "EY1", "EY2", "F", "G", "HH", "IH0", "IH1", "IH2", "IY0", "IY1", "IY2", "JH", "K", "L", "M",
54
+ "N", "NG", "OW0", "OW1", "OW2", "OY0", "OY1", "OY2", "P", "R", "S", "SH", "T", "TH", "UH0", "UH1", "UH2", "UW0", "UW1", "UW2", "V", "W", "Y", "Z", "ZH"
55
+ ]
56
+
57
+ let ARPABET_SYMBOLS = [
58
+ 'AA0', 'AA1', 'AA2', 'AA', 'AE0', 'AE1', 'AE2', 'AE', 'AH0', 'AH1', 'AH2', 'AH',
59
+ 'AO0', 'AO1', 'AO2', 'AO', 'AW0', 'AW1', 'AW2', 'AW', 'AY0', 'AY1', 'AY2', 'AY',
60
+ 'B', 'CH', 'D', 'DH', 'EH0', 'EH1', 'EH2', 'EH', 'ER0', 'ER1', 'ER2', 'ER',
61
+ 'EY0', 'EY1', 'EY2', 'EY', 'F', 'G', 'HH', 'IH0', 'IH1', 'IH2', 'IH', 'IY0', 'IY1',
62
+ 'IY2', 'IY', 'JH', 'K', 'L', 'M', 'N', 'NG', 'OW0', 'OW1', 'OW2', 'OW', 'OY0',
63
+ 'OY1', 'OY2', 'OY', 'P', 'R', 'S', 'SH', 'T', 'TH', 'UH0', 'UH1', 'UH2', 'UH',
64
+ 'UW0', 'UW1', 'UW2', 'UW', 'V', 'W', 'Y', 'Z', 'ZH'
65
+ ]
66
+
67
+ let extra_arpabet_symbols = [
68
+ "AX",
69
+ "AXR",
70
+ "IX",
71
+ "UX",
72
+ "DX",
73
+ "EL",
74
+ "EM",
75
+ "EN0",
76
+ "EN1",
77
+ "EN2",
78
+ "EN",
79
+ "NX",
80
+ "Q",
81
+ "WH",
82
+ ]
83
+ let new_arpabet_symbols = [
84
+ "RRR",
85
+ "HR",
86
+ "OE",
87
+ "RH",
88
+ "TS",
89
+
90
+ "RR",
91
+ "UU",
92
+ "OO",
93
+ "KH",
94
+ "SJ",
95
+ "HJ",
96
+ "BR",
97
+ ]
98
+ window.ARPABET_SYMBOLS_v3 = ARPABET_SYMBOLS.concat(extra_arpabet_symbols).concat(new_arpabet_symbols).sort((a,b)=>a<b?-1:1)
99
+
100
+ const preprocess = (caretStart) => {
101
+
102
+ const defaultLang = "en"
103
+ let text = dialogueInput.value
104
+
105
+ const finishedParts = []
106
+ // const parts = text.split(/\\lang\{[a-z]{0,2}\}\{/gi)
107
+ const parts = text.split(/\\lang\[[a-z]{0,2}\]\[/gi)
108
+ // const matchIterator = text.matchAll(/\\lang\{[a-z]{0,2}\}\{/gi)
109
+ const matchIterator = text.matchAll(/\\lang\[[a-z]{0,2}\]\[/gi)
110
+ finishedParts.push([defaultLang, parts[0]])
111
+
112
+ const langStack = []
113
+
114
+ // console.log("parts", parts)
115
+ let textCounter = parts[0].length
116
+ let caretInARPAbet = false
117
+
118
+ parts.forEach((part,pi) => {
119
+ if (pi) {
120
+ const match = matchIterator.next().value[0]
121
+ const langCode = match.split("lang[")[1].split("]")[0]
122
+ langStack.push(langCode)
123
+ // console.log("add langStack", langStack)
124
+
125
+ textCounter += match.length
126
+
127
+ let unescaped_part = ""
128
+
129
+ part.split("]").forEach(sub_part => {
130
+ // console.log("sub_part", sub_part, textCounter, textCounter+sub_part.length)
131
+
132
+ if (caretStart > textCounter && caretStart <textCounter+sub_part.length) {
133
+ caretInARPAbet = true
134
+
135
+ // If backspace-ing a "{", and the next non-space character is "}", then delete that also
136
+
137
+ // Add ctrl+<space> to open the auto-complete
138
+
139
+ }
140
+
141
+ // if (sub_part.includes("[")) {
142
+ // unescaped_part += sub_part+"]"
143
+ // return
144
+ // }
145
+
146
+ sub_part = unescaped_part+sub_part
147
+
148
+ unescaped_part = ""
149
+
150
+ if (part.includes("]")) {
151
+ finishedParts.push([langStack.pop()||defaultLang, sub_part])
152
+ } else {
153
+ finishedParts.push([langStack[langStack.length-1], sub_part])
154
+ }
155
+ // finishedParts.push([langStack.pop()||defaultLang, sub_part])
156
+ })
157
+ }
158
+ })
159
+ return caretInARPAbet
160
+ }
161
+
162
+ window.hideAutocomplete = () => {
163
+ textEditorTooltip.style.display = "none"
164
+ textEditorTooltip.innerHTML = ""
165
+ autocomplete_callback = undefined
166
+ }
167
+ let textWrittenSinceAutocompleteWasShown = ""
168
+ const filterOrHideAutocomplete = () => {
169
+ if (textEditorTooltip.style.display=="flex") {
170
+ highlightedAutocompleteIndex = 0
171
+ let childrenShown = 0
172
+ Array.from(textEditorTooltip.children).forEach(child => {
173
+ if (child.classList.contains("autocomplete_option_active")) {
174
+ child.classList.toggle("autocomplete_option_active")
175
+ }
176
+
177
+ if (child.innerText.toLowerCase().startsWith(textWrittenSinceAutocompleteWasShown) || textWrittenSinceAutocompleteWasShown.length==0) {
178
+ child.style.display = "flex"
179
+ childrenShown += 1
180
+ } else {
181
+ child.style.display = "none"
182
+ }
183
+ })
184
+ if (childrenShown==0) {
185
+ hideAutocomplete()
186
+ return
187
+ }
188
+ setHighlightedAutocomplete(0, true)
189
+ }
190
+ }
191
+ let autocomplete_callback = undefined
192
+ const showAutocomplete = (options, callback) => {
193
+ const position = getCaretPosition(dialogueInput)
194
+
195
+ // The getCaretPosition function doesn't work anymore. At least center it
196
+ position.x = window.visualViewport.width/2
197
+
198
+ textEditorTooltip.style.left = position.x + "px"
199
+ textEditorTooltip.style.top = position.y + "px"
200
+
201
+ highlightedAutocompleteIndex = 0
202
+ textWrittenSinceAutocompleteWasShown = ""
203
+ autocomplete_callback = callback
204
+
205
+ options.forEach(option => {
206
+ const optElem = createElem("div.autocomplete_option", option[0])
207
+ optElem.dataset.autocomplete_return = option.length>1 ? option[1] : option[0]
208
+ optElem.addEventListener("click", () => {
209
+ callback(optElem.dataset.autocomplete_return)
210
+ hideAutocomplete()
211
+ refreshText()
212
+ })
213
+ textEditorTooltip.appendChild(optElem)
214
+ })
215
+ textEditorTooltip.style.display = "flex"
216
+ setHighlightedAutocomplete(0, true)
217
+ return
218
+ }
219
+
220
+ let highlightedAutocompleteIndex = 0
221
+ const setHighlightedAutocomplete = (delta, override=false) => {
222
+
223
+ let NEW_highlightedAutocompleteIndex = Math.min(textEditorTooltip.children.length-1, Math.max(0, highlightedAutocompleteIndex+delta))
224
+ if (override || NEW_highlightedAutocompleteIndex != highlightedAutocompleteIndex) {
225
+ if (!override) {
226
+ textEditorTooltip.children[highlightedAutocompleteIndex].classList.toggle("autocomplete_option_active")
227
+ }
228
+ textEditorTooltip.children[override ? delta : NEW_highlightedAutocompleteIndex].classList.toggle("autocomplete_option_active")
229
+ highlightedAutocompleteIndex = NEW_highlightedAutocompleteIndex
230
+ // textEditorTooltip.children[highlightedAutocompleteIndex].scrollIntoView()
231
+ }
232
+ }
233
+
234
+
235
+ dialogueInput.addEventListener("keydown", event => {
236
+ if (event.key=="Tab" || event.key=="Enter") {
237
+ event.preventDefault()
238
+
239
+ if (autocomplete_callback!==undefined) {
240
+ autocomplete_callback(textEditorTooltip.children[highlightedAutocompleteIndex].dataset.autocomplete_return)
241
+ hideAutocomplete()
242
+ refreshText()
243
+ return
244
+ }
245
+
246
+ let cursorIndex = dialogueInput.selectionStart
247
+ if (cursorIndex) {
248
+ // Move into the next [] bracket if already wrote down language
249
+ let textPreCursor = dialogueInput.value.slice(0, cursorIndex)
250
+ let textPostCursor = dialogueInput.value.slice(cursorIndex, dialogueInput.value.length)
251
+
252
+ if (textPreCursor.slice(textPreCursor.length-8, 7)=="\\lang[") {
253
+ dialogueInput.setSelectionRange(cursorIndex+2,cursorIndex+2)
254
+
255
+ // Move out of [] if at the end
256
+ } else if (textEditorTooltip.style.display=="none" && (textPostCursor.startsWith("]") || textPostCursor.startsWith("}"))) {
257
+ console.log("moving one")
258
+ dialogueInput.setSelectionRange(cursorIndex+1,cursorIndex+1)
259
+ }
260
+
261
+ }
262
+
263
+ }
264
+ })
265
+
266
+ const splitWords = (sequence, addSpace) => {
267
+ const words = []
268
+
269
+ // const sequenceProcessed = sequence
270
+ const sequenceProcessed = [] // Do further processing to also split on { } symbols, not just spaces
271
+ sequence.forEach(word => {
272
+ if (word.includes("{")) {
273
+ word.split("{").forEach((w, wi) => {
274
+ sequenceProcessed.push(wi ? ["{"+w, addSpace] : [w, false])
275
+ })
276
+ } else if (word.includes("}")) {
277
+ word.split("}").forEach((w, wi) => {
278
+ sequenceProcessed.push(wi ? [w, addSpace] : [w+"}", false])
279
+ })
280
+ } else {
281
+ sequenceProcessed.push([word, addSpace])
282
+ }
283
+ })
284
+
285
+ sequenceProcessed.forEach(([word, addSpace]) => {
286
+
287
+ if (word.startsWith("\\lang[")) {
288
+ words.push(word.split("][")[0]+"][")
289
+ word = word.split("][")[1]
290
+ }
291
+
292
+ ["}","]","[","{"].forEach(char => {
293
+ if (word.startsWith(char)) {
294
+ words.push(char)
295
+ word = word.slice(1,word.length)
296
+ }
297
+ })
298
+
299
+ const endExtras = [];
300
+
301
+ ["}","]","[","{"].forEach(char => {
302
+ if (word.endsWith(char)) {
303
+ endExtras.push(char)
304
+ word = word.slice(0,word.length-1)
305
+ }
306
+ })
307
+
308
+ words.push(word)
309
+ endExtras.reverse().forEach(extra => words.push(extra))
310
+
311
+ // if (word.startsWith("{")) {
312
+ // split_words.push("{")
313
+ // word = word.slice(1,word.length)
314
+ // }
315
+ // if (word.endsWith("}")) {
316
+ // split_words.push("{")
317
+ // word = word.slice(1,word.length)
318
+ // }
319
+
320
+ if (addSpace) {
321
+ words.push(" ")
322
+ }
323
+ })
324
+
325
+ return words
326
+ }
327
+
328
+ window.refreshText = () => {
329
+ let all_text = dialogueInput.value
330
+ textEditorElem.innerHTML = ""
331
+
332
+ let openedCurlys = 0
333
+ let openedLangs = 0
334
+
335
+ let split_words = splitWords(all_text.split(" "), true)
336
+ split_words = splitWords(split_words)
337
+ split_words = splitWords(split_words)
338
+ split_words = splitWords(split_words)
339
+
340
+ // console.log("split_words", split_words)
341
+ // dfgd()
342
+
343
+ let caretCounter = 0
344
+ let caretInARPAbet = false
345
+
346
+ let firstOpenCurly = undefined
347
+ let lastOpenCurly = undefined
348
+
349
+ split_words.forEach(word => {
350
+
351
+ if (caretCounter<=dialogueInput.selectionStart && (caretCounter+word.length)>dialogueInput.selectionStart) {
352
+ // console.log(`caret (${dialogueInput.selectionStart}) in counter (${caretCounter}): `, word, openedCurlys, openedLangs)
353
+ caretInARPAbet = openedCurlys > 0
354
+ }
355
+ caretCounter += word.length
356
+ const spanElem = createElem("span.manyWhitespace", word)
357
+
358
+ if (word.startsWith("\\lang[")) {
359
+ openedLangs += 1
360
+ }
361
+ if (word.startsWith("{")) {
362
+ openedCurlys += 1
363
+ if (!caretInARPAbet) {
364
+ firstOpenCurly = spanElem
365
+ }
366
+ }
367
+
368
+ if (openedCurlys) {
369
+ spanElem.style.fontWeight = "bold"
370
+ spanElem.style.fontStyle = "italic"
371
+ }
372
+ if (openedLangs) {
373
+ spanElem.style.background = "rgba(50, 150, 250, 0.2)"
374
+ }
375
+
376
+ ///====
377
+ if (word.includes("part-highlighted")) {
378
+ spanElem.style.textDecoration = "underline dotted red"
379
+ } else if (word.includes("highlighted")) {
380
+ spanElem.style.textDecoration = "underline solid red"
381
+ }
382
+ ///====
383
+
384
+ if (word.endsWith("]")) {
385
+ openedLangs -= 1
386
+ }
387
+ if (word.endsWith("}")) {
388
+ openedCurlys -= 1
389
+ if (caretInARPAbet && lastOpenCurly===undefined) {
390
+ lastOpenCurly = spanElem
391
+ }
392
+ }
393
+
394
+ textEditorElem.appendChild(spanElem)
395
+
396
+ })
397
+
398
+ preprocess(dialogueInput.selectionStart)
399
+ return [caretInARPAbet, firstOpenCurly, lastOpenCurly]
400
+ }
401
+
402
+ const languagesList = Object.keys(window.supportedLanguages)
403
+
404
+ const insertText = (inputTextArea, textToInsert, caretOffset=0) => {
405
+ let cursorIndex = inputTextArea.selectionStart
406
+ inputTextArea.value = inputTextArea.value.slice(0, cursorIndex) + textToInsert + inputTextArea.value.slice(cursorIndex, inputTextArea.value.length)
407
+ caretOffset += textToInsert.length
408
+ inputTextArea.setSelectionRange(cursorIndex+caretOffset,cursorIndex+caretOffset)
409
+ refreshText()
410
+ }
411
+
412
+ dialogueInput.addEventListener("keydown", event => {
413
+ generateVoiceButton.disabled = window.currentModel==undefined || !dialogueInput.value.length
414
+
415
+ if (event.key=="Enter") {
416
+ event.stopPropagation()
417
+ event.preventDefault()
418
+ return
419
+ }
420
+
421
+ if (textEditorTooltip.style.display=="flex" && (event.key=="ArrowDown" || event.key=="ArrowUp" || (!window.shiftKeyIsPressed && event.key=="ArrowLeft") || (!window.shiftKeyIsPressed && event.key=="ArrowRight"))) {
422
+ // if (textEditorTooltip.style.display=="flex" && (event.key=="ArrowDown" || event.key=="ArrowUp")) {
423
+ event.stopPropagation()
424
+ event.preventDefault()
425
+ return
426
+ }
427
+ if (event.key=="}") {
428
+ if (dialogueInput.value.slice(dialogueInput.selectionStart, dialogueInput.value.length-1).startsWith("}")) {
429
+ dialogueInput.setSelectionRange(dialogueInput.selectionStart+1, dialogueInput.selectionStart+1)
430
+ event.stopPropagation()
431
+ event.preventDefault()
432
+ return
433
+ }
434
+ }
435
+ if (event.key=="]") {
436
+ if (dialogueInput.value.slice(dialogueInput.selectionStart, dialogueInput.value.length-1).startsWith("]")) {
437
+ dialogueInput.setSelectionRange(dialogueInput.selectionStart+1, dialogueInput.selectionStart+1)
438
+ event.stopPropagation()
439
+ event.preventDefault()
440
+ return
441
+ }
442
+ }
443
+ })
444
+
445
+ let is_doing_gp2 = false
446
+ window.get_g2p = (text_to_g2p) => {
447
+ return new Promise(resolve => {
448
+ doFetch("http://localhost:8008/getG2P", {method: "Post", body: JSON.stringify({base_lang: base_lang_select.value, text: text_to_g2p})})
449
+ .then(r=>r.text()).then(res => {
450
+ is_doing_gp2 = false
451
+ resolve(res)
452
+ })
453
+ })
454
+ }
455
+
456
+ const handleTextUpdate = (event) => {
457
+ generateVoiceButton.disabled = window.currentModel==undefined || !dialogueInput.value.length
458
+
459
+ window.shiftKeyIsPressed = event.shiftKey
460
+
461
+ if (textEditorTooltip.style.display=="flex" && (event.type=="click" || (!window.shiftKeyIsPressed && event.key=="ArrowDown") || (!window.shiftKeyIsPressed && event.key=="ArrowRight"))) {
462
+ event.stopPropagation()
463
+ event.preventDefault()
464
+ setHighlightedAutocomplete(1)
465
+ return
466
+ }
467
+ if (textEditorTooltip.style.display=="flex" && (event.type=="click" || (!window.shiftKeyIsPressed && event.key=="ArrowLeft") || (!window.shiftKeyIsPressed && event.key=="ArrowUp"))) {
468
+ event.stopPropagation()
469
+ event.preventDefault()
470
+ setHighlightedAutocomplete(-1)
471
+ return
472
+ }
473
+
474
+
475
+ if (event.type!="click" && (event.key=="Shift" || event.key=="Control")) {
476
+ event.stopPropagation()
477
+ event.preventDefault()
478
+ return
479
+ }
480
+
481
+ const [caretInARPAbet, firstOpenCurly, lastOpenCurly] = refreshText()
482
+ if (caretInARPAbet) {
483
+ firstOpenCurly && (firstOpenCurly.style.color = "red")
484
+ lastOpenCurly && (lastOpenCurly.style.color = "red")
485
+ }
486
+
487
+
488
+ textEditorElem.scrollTop = dialogueInput.scrollTop
489
+
490
+
491
+ if (dialogueInput.selectionStart!=dialogueInput.selectionEnd && !is_doing_gp2) {
492
+
493
+ hideAutocomplete()
494
+
495
+ showAutocomplete([["&lt;Convert to phonemes&gt;"]], () => {
496
+
497
+ const text_to_g2p = dialogueInput.value.slice(dialogueInput.selectionStart, dialogueInput.selectionEnd)
498
+ is_doing_gp2 = true
499
+
500
+ get_g2p(text_to_g2p).then(phonemes => {
501
+ const initialStart = dialogueInput.selectionStart
502
+ dialogueInput.value = dialogueInput.value.slice(0, dialogueInput.selectionStart) + dialogueInput.value.slice(dialogueInput.selectionEnd, dialogueInput.value.length)
503
+
504
+ dialogueInput.selectionStart = initialStart
505
+
506
+ insertText(dialogueInput, phonemes, 0)
507
+ })
508
+ })
509
+
510
+ } else
511
+ // } else {
512
+ if (event.type!="click" && event.key.length==1 && event.key.match(/[a-z]/i)) {
513
+ textWrittenSinceAutocompleteWasShown += event.key.toLowerCase()
514
+ filterOrHideAutocomplete()
515
+ } else if (event.type!="click" && event.key=="Backspace") {
516
+ if (textWrittenSinceAutocompleteWasShown.length==0) {
517
+ hideAutocomplete()
518
+ } else {
519
+ textWrittenSinceAutocompleteWasShown = textWrittenSinceAutocompleteWasShown.slice(0,textWrittenSinceAutocompleteWasShown.length-1)
520
+ filterOrHideAutocomplete()
521
+ }
522
+ } else {
523
+ hideAutocomplete()
524
+ }
525
+
526
+ const ctrlSpace = event.ctrlKey && event.code=="Space"
527
+
528
+ if (event.type!="click" && (event.key=="{" || event.key=="") || ctrlSpace) {
529
+ if (!ctrlSpace && event.key!="") {
530
+ insertText(dialogueInput, "}", -1)
531
+ }
532
+
533
+ if (caretInARPAbet) {
534
+ let symbols = window.ARPABET_SYMBOLS_v3
535
+ if (window.currentModel&&window.currentModel.modelType=="FastPitch1.1") {
536
+ symbols = window.ARPABET_SYMBOLS_v2
537
+ } else if (window.currentModel&&window.currentModel.modelType=="FastPitch") {
538
+ symbols = ["&lt;ARPAbet only available for v2+ models&gt;"]
539
+ }
540
+ showAutocomplete(symbols.map(v=>{return [v]}), option => {
541
+ if (symbols.length>1) {
542
+ insertText(dialogueInput, option.slice(textWrittenSinceAutocompleteWasShown.length, option.length)+" ", 0)
543
+ }
544
+ })
545
+ }
546
+ if (event.key!="") {
547
+ handleTextUpdate({type: "keydown", key: ""})
548
+ }
549
+ }
550
+
551
+ if (event.type!="click" && event.key=="\\") {
552
+ // showAutocomplete([["\\lang[language][text]", "\\lang[][]"], ["\\sil[milliseconds]", "\\sil[]"]], (option) => {
553
+ showAutocomplete([["\\lang[language][text]", "\\lang[][]"]], (option) => {
554
+
555
+ if (option.includes("lang")) {
556
+ insertText(dialogueInput, option.slice(1, option.length), -3)
557
+ } else {
558
+ insertText(dialogueInput, option.slice(1, option.length), -1)
559
+ }
560
+
561
+ setTimeout(() => {
562
+ showAutocomplete(languagesList.map(v=>{return [v]}), option => {
563
+ insertText(dialogueInput, option.slice(textWrittenSinceAutocompleteWasShown.length, option.length), 2)
564
+ })
565
+ }, 100)
566
+ })
567
+ }
568
+ // }
569
+ // })
570
+ }
571
+ dialogueInput.addEventListener("click", event => handleTextUpdate(event))
572
+ dialogueInput.addEventListener("keyup", event => handleTextUpdate(event))
573
+ refreshText()
574
+ setTimeout(window.refreshText, 500)
575
+
576
+ window.addEventListener("click", event => {
577
+ if (event.target && event.target!=textEditorTooltip && event.target.className && event.target.className.includes && !event.target.className.includes("autocomplete_option")) {
578
+ hideAutocomplete()
579
+ }
580
+ })
javascript/totd.js ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+ // tip of the day
3
+
4
+ const tips = {
5
+ "1": window.i18n.TOTD_1,
6
+ "2": window.i18n.TOTD_2,
7
+ "3": window.i18n.TOTD_3,
8
+ "4": window.i18n.TOTD_4,
9
+ "5": window.i18n.TOTD_5,
10
+ "6": window.i18n.TOTD_6,
11
+ "7": window.i18n.TOTD_7,
12
+ "8": window.i18n.TOTD_8,
13
+ "9": window.i18n.TOTD_9,
14
+ "10": window.i18n.TOTD_10,
15
+ "11": window.i18n.TOTD_11,
16
+ "12": window.i18n.TOTD_12,
17
+ "13": window.i18n.TOTD_13,
18
+ "14": window.i18n.TOTD_14,
19
+ "15": window.i18n.TOTD_15,
20
+ "16": window.i18n.TOTD_16,
21
+ "17": window.i18n.TOTD_17,
22
+ "18": window.i18n.TOTD_18,
23
+ "19": window.i18n.TOTD_19,
24
+ "20": window.i18n.TOTD_20,
25
+ "21": window.i18n.TOTD_21,
26
+ "22": window.i18n.TOTD_22,
27
+ "23": window.i18n.TOTD_23,
28
+ "24": window.i18n.TOTD_24,
29
+ "25": window.i18n.TOTD_25,
30
+ "26": window.i18n.TOTD_26,
31
+ "27": window.i18n.TOTD_27,
32
+ "28": window.i18n.TOTD_28,
33
+ "29": window.i18n.TOTD_29,
34
+ "30": window.i18n.TOTD_30,
35
+ "31": window.i18n.TOTD_31,
36
+ "32": window.i18n.TOTD_32,
37
+ }
38
+
39
+ window.totd_state = {
40
+ startupChecked: false,
41
+ filteredIDs: [],
42
+ tipPageIndex: 0
43
+ }
44
+
45
+ const initTipOfTheDayMenu = (now, tipIDs) => {
46
+
47
+ window.totd_state.filteredIDs = tipIDs
48
+
49
+ totdContainer.style.opacity = 1
50
+ totdContainer.style.display = "flex"
51
+ chromeBar.style.opacity = 1
52
+ requestAnimationFrame(() => requestAnimationFrame(() => totdContainer.style.opacity = 1))
53
+
54
+ return new Promise(resolve => {
55
+ // Close button
56
+ totd_close.addEventListener("click", () => {
57
+ closeModal(totdContainer)
58
+ resolve()
59
+ })
60
+
61
+ localStorage.setItem("totd_lastDate", now.toJSON(now))
62
+ tipMessage.innerHTML = tips[window.totd_state.filteredIDs[0]]
63
+
64
+ totd_counter.innerHTML = `1/${window.totd_state.filteredIDs.length}`
65
+
66
+ saveSeenTip(window.totd_state.filteredIDs[0])
67
+ })
68
+ }
69
+
70
+
71
+ const saveSeenTip = ID => {
72
+ let seenTipIDs = localStorage.getItem("totd_seenIDs")
73
+ seenTipIDs = seenTipIDs ? seenTipIDs.split(",") : []
74
+ seenTipIDs = new Set(seenTipIDs)
75
+ seenTipIDs.add(ID)
76
+ localStorage.setItem("totd_seenIDs", Array.from(seenTipIDs).join(","))
77
+ }
78
+
79
+ setting_btnShowTOTD.addEventListener("click", () => {
80
+ window.showTipIfEnabledAndNewDay(true)
81
+ })
82
+
83
+
84
+ totdPrevTipBtn.addEventListener("click", () => {
85
+ const newIndex = Math.max(0, window.totd_state.tipPageIndex-1)
86
+ if (newIndex!=window.totd_state.tipPageIndex) {
87
+ window.totd_state.tipPageIndex = newIndex
88
+ tipMessage.innerHTML = tips[window.totd_state.filteredIDs[window.totd_state.tipPageIndex]]
89
+ saveSeenTip(window.totd_state.filteredIDs[window.totd_state.tipPageIndex])
90
+ totd_counter.innerHTML = `${window.totd_state.tipPageIndex+1}/${window.totd_state.filteredIDs.length}`
91
+ }
92
+ })
93
+
94
+ totdNextTipBtn.addEventListener("click", () => {
95
+ const newIndex = Math.min(window.totd_state.filteredIDs.length-1, window.totd_state.tipPageIndex+1)
96
+ if (newIndex!=window.totd_state.tipPageIndex) {
97
+ window.totd_state.tipPageIndex = newIndex
98
+ tipMessage.innerHTML = tips[window.totd_state.filteredIDs[window.totd_state.tipPageIndex]]
99
+ saveSeenTip(window.totd_state.filteredIDs[window.totd_state.tipPageIndex])
100
+ totd_counter.innerHTML = `${window.totd_state.tipPageIndex+1}/${window.totd_state.filteredIDs.length}`
101
+ }
102
+ })
103
+
104
+
105
+ window.showTipIfEnabledAndNewDay = (justShowIt) => {
106
+
107
+ window.totd_state.startupChecked = true
108
+
109
+ return new Promise(async resolve => {
110
+ const lastDate = localStorage.getItem("totd_lastDate")
111
+ const now = new Date()
112
+
113
+ // If this has never happened before, or the last date is not today, then show the tip menu
114
+ if (justShowIt || !lastDate || lastDate.split("T")[0]!=now.toJSON().split("T")[0]) {
115
+
116
+ // If the tips of the day are enabled
117
+ if (justShowIt || window.userSettings.showTipOfTheDay) {
118
+
119
+ let shuffledTipIDs = window.shuffle(Object.keys(tips))
120
+
121
+ // If only new/unseen tips are to be shown, get the seen list, and filter out the seen tips
122
+ if (window.userSettings.showUnseenTipOfTheDay) {
123
+ let seenTipIDs = localStorage.getItem("totd_seenIDs")
124
+ if (seenTipIDs) {
125
+ seenTipIDs = seenTipIDs.split(",")
126
+ shuffledTipIDs = shuffledTipIDs.filter(id => !seenTipIDs.includes(id))
127
+ }
128
+ }
129
+
130
+ // If there are any tips remaining, after any filtering, then show the menu
131
+ if (shuffledTipIDs && shuffledTipIDs.length) {
132
+ await initTipOfTheDayMenu(now, shuffledTipIDs)
133
+ resolve()
134
+ } else if (justShowIt) {
135
+ window.errorModal(window.i18n.TOTD_NO_UNSEEN)
136
+ }
137
+ } else {
138
+ resolve()
139
+ }
140
+ } else {
141
+ resolve()
142
+ }
143
+ })
144
+ }
145
+
javascript/util.js ADDED
@@ -0,0 +1,740 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+ const unzipper = require('unzipper')
4
+ const er = require('@electron/remote')
5
+
6
+ /**
7
+ * String.prototype.replaceAll() polyfill
8
+ * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
9
+ * @author Chris Ferdinandi
10
+ * @license MIT
11
+ */
12
+ if (!String.prototype.replaceAll) {
13
+ String.prototype.replaceAll = function(str, newStr){
14
+
15
+ // If a regex pattern
16
+ if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
17
+ return this.replace(str, newStr);
18
+ }
19
+
20
+ // If a string
21
+ return this.replace(new RegExp(str, 'g'), newStr);
22
+
23
+ };
24
+ }
25
+
26
+
27
+ window.toggleSpinnerButtons = (spinnerVisible=undefined) => {
28
+
29
+ if (spinnerVisible===undefined) {
30
+ spinnerVisible = window.getComputedStyle(spinner).display == "block"
31
+ }
32
+
33
+ spinner.style.display = spinnerVisible ? "none" : "block"
34
+ keepSampleButton.style.display = spinnerVisible ? "block" : "none"
35
+ generateVoiceButton.style.display = spinnerVisible ? "block" : "none"
36
+ samplePlayPause.style.display = spinnerVisible ? "flex" : "none"
37
+ }
38
+
39
+ window.infoModal = message => new Promise(resolve => resolve(createModal("info", message)))
40
+ window.confirmModal = message => new Promise(resolve => resolve(createModal("confirm", message)))
41
+ window.spinnerModal = message => new Promise(resolve => resolve(createModal("spinner", message)))
42
+ window.errorModal = message => {
43
+ window.errorModalHasOpened = true
44
+ if (window.userSettings.useErrorSound) {
45
+ const audioPreview = createElem("audio", {autoplay: false}, createElem("source", {
46
+ src: window.userSettings.errorSoundFile
47
+ }))
48
+ audioPreview.setSinkId(window.userSettings.base_speaker)
49
+ }
50
+ window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent?window.batch_state.taskBarPercent:1, {mode: "error"})
51
+ return new Promise(topResolve => {
52
+ createModal("error", message).then(() => {
53
+ window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent?window.batch_state.taskBarPercent:-1, {mode: window.batch_state.state?"normal":"paused"})
54
+ topResolve()
55
+ })
56
+ })
57
+ }
58
+ window.createModal = (type, message) => {
59
+ dialogueInput.blur()
60
+ return new Promise(resolve => {
61
+ modalContainer.innerHTML = ""
62
+ const displayMessage = message.prompt ? message.prompt : message
63
+ const modal = createElem("div.modal#activeModal", {style: {opacity: 0}}, createElem("span.createModalContents", displayMessage))
64
+ modal.dataset.type = type
65
+
66
+ if (type=="confirm") {
67
+ const yesButton = createElem("button", {style: {background: `#${themeColour}`}})
68
+ yesButton.innerHTML = window.i18n.YES
69
+ const noButton = createElem("button", {style: {background: `#${themeColour}`}})
70
+ noButton.innerHTML = window.i18n.NO
71
+ modal.appendChild(createElem("div", yesButton, noButton))
72
+
73
+ yesButton.addEventListener("click", () => {
74
+ closeModal(modalContainer).then(() => {
75
+ resolve(true)
76
+ })
77
+ })
78
+ noButton.addEventListener("click", () => {
79
+ closeModal(modalContainer).then(() => {
80
+ resolve(false)
81
+ })
82
+ })
83
+ } else if (type=="error" || type=="info") {
84
+ const closeButton = createElem("button", {style: {background: `#${themeColour}`}})
85
+ closeButton.innerHTML = window.i18n.CLOSE
86
+ modal.appendChild(createElem("div", closeButton))
87
+
88
+ closeButton.addEventListener("click", () => {
89
+ closeModal(modalContainer).then(() => {
90
+ resolve(false)
91
+ })
92
+ })
93
+ } else if (type=="prompt") {
94
+ const closeButton = createElem("button", {style: {background: `#${themeColour}`}})
95
+ closeButton.innerHTML = window.i18n.SUBMIT
96
+ const inputElem = createElem("input", {type: "text", value: message.value})
97
+ modal.appendChild(createElem("div", inputElem))
98
+ modal.appendChild(createElem("div", closeButton))
99
+
100
+ closeButton.addEventListener("click", () => {
101
+ closeModal(modalContainer).then(() => {
102
+ resolve(inputElem.value)
103
+ })
104
+ })
105
+ } else {
106
+ modal.appendChild(createElem("div.spinner", {style: {borderLeftColor: document.querySelector("button").style.background}}))
107
+ }
108
+
109
+ modalContainer.appendChild(modal)
110
+ modalContainer.style.opacity = 0
111
+ modalContainer.style.display = "flex"
112
+
113
+ requestAnimationFrame(() => requestAnimationFrame(() => modalContainer.style.opacity = 1))
114
+ requestAnimationFrame(() => requestAnimationFrame(() => chromeBar.style.opacity = 1))
115
+ })
116
+ }
117
+ window.closeModal = (container=undefined, notThisOne=undefined, skipIfErrorOpen=false) => {
118
+ return new Promise(resolve => {
119
+ if (window.errorModalHasOpened && skipIfErrorOpen) {
120
+ return resolve()
121
+ }
122
+ window.errorModalHasOpened = false
123
+ const allContainers = [batchGenerationContainer, gameSelectionContainer, updatesContainer, infoContainer, settingsContainer, patreonContainer, pluginsContainer, modalContainer, nexusContainer, embeddingsContainer, totdContainer, nexusReposContainer, EULAContainer, arpabetContainer, styleEmbeddingsContainer, workbenchContainer]
124
+ const containers = container==undefined ? allContainers : (Array.isArray(container) ? container.filter(c=>c!=undefined) : [container])
125
+
126
+ notThisOne = Array.isArray(notThisOne) ? notThisOne : (notThisOne==undefined ? [] : [notThisOne])
127
+
128
+ containers.forEach(cont => {
129
+ // Fade out the containers except the exceptions
130
+ if (cont!=undefined && !notThisOne.includes(cont)) {
131
+ cont.style.opacity = 0
132
+ }
133
+ })
134
+
135
+ const someOpenContainer = allContainers.filter(c=>c!=undefined).find(cont => cont.style.opacity==1 && cont.style.display!="none" && cont!=modalContainer)
136
+ if (!someOpenContainer || someOpenContainer==container) {
137
+ chromeBar.style.opacity = 0.88
138
+ }
139
+
140
+ setTimeout(() => {
141
+ if (window.errorModalHasOpened && skipIfErrorOpen) {
142
+ } else {
143
+ containers.forEach(cont => {
144
+ // Hide the containers except the exceptions
145
+ if (cont!=undefined && !notThisOne.includes(cont)) {
146
+ cont.style.display = "none"
147
+ const someOpenContainer2 = allContainers.filter(c=>c!=undefined).find(cont => cont.style.opacity==1 && cont.style.display!="none" && cont!=modalContainer)
148
+ if (!someOpenContainer2 || someOpenContainer2==container) {
149
+ chromeBar.style.opacity = 0.88
150
+ }
151
+ }
152
+ })
153
+ window.errorModalHasOpened = false
154
+ }
155
+ // resolve()
156
+ }, 200)
157
+ try {
158
+ activeModal.remove()
159
+ } catch (e) {}
160
+ resolve()
161
+ })
162
+ }
163
+
164
+
165
+ window.setTheme = (meta) => {
166
+
167
+ const primaryColour = meta.themeColourPrimary
168
+ const secondaryColour = meta.themeColourSecondary
169
+ const gameName = meta.gameName
170
+
171
+ if (window.userSettings.showDiscordStatus) {
172
+ ipcRenderer.send('updateDiscord', {details: gameName})
173
+ }
174
+
175
+ // Change batch panel colours, if it is initialized
176
+ try {
177
+ Array.from(batchRecordsHeader.children).forEach(item => item.style.backgroundColor = `#${primaryColour}`)
178
+ } catch (e) {}
179
+ try {
180
+ Array.from(pluginsRecordsHeader.children).forEach(item => item.style.backgroundColor = `#${primaryColour}`)
181
+ } catch (e) {}
182
+ try {
183
+ Array.from(styleembsRecordsHeader.children).forEach(item => item.style.backgroundColor = `#${primaryColour}`)
184
+ } catch (e) {}
185
+ try {
186
+ Array.from(nexusRecordsHeader.children).forEach(item => item.style.backgroundColor = `#${primaryColour}`)
187
+ } catch (e) {}
188
+ try {
189
+ Array.from(nexusSearchHeader.children).forEach(item => item.style.backgroundColor = `#${primaryColour}`)
190
+ } catch (e) {}
191
+ try {
192
+ Array.from(nexusReposUsedHeader.children).forEach(item => item.style.backgroundColor = `#${primaryColour}`)
193
+ } catch (e) {}
194
+ try {
195
+ Array.from(embeddingsRecordsHeader.children).forEach(item => item.style.backgroundColor = `#${primaryColour}`)
196
+ } catch (e) {}
197
+ try {
198
+ Array.from(arpabetWordsListHeader.children).forEach(item => item.style.backgroundColor = `#${primaryColour}`)
199
+ } catch (e) {}
200
+ try {
201
+ window.sequenceEditor.grabbers.forEach(grabber => grabber.fillStyle = `#${primaryColour}`)
202
+ window.sequenceEditor.energyGrabbers.forEach(grabber => grabber.fillStyle = `#${primaryColour}`)
203
+ } catch (e) {}
204
+
205
+ const background = `linear-gradient(0deg, rgba(128,128,128,${window.userSettings.bg_gradient_opacity}) 0px, rgba(0,0,0,0)), url("assets/${meta.assetFile}")`
206
+ Array.from(document.querySelectorAll("button:not(.fixedColour)")).forEach(e => e.style.background = `#${primaryColour}`)
207
+ Array.from(document.querySelectorAll(".voiceType")).forEach(e => e.style.background = `#${primaryColour}`)
208
+ Array.from(document.querySelectorAll(".spinner")).forEach(e => e.style.borderLeftColor = `#${primaryColour}`)
209
+ Array.from(document.querySelectorAll(".checkbox")).forEach(e => e.style.accentColor = `#${primaryColour}`)
210
+ Array.from(document.querySelectorAll(".input[type=range]")).forEach(e => e.style.accentColor = `#${primaryColour}`)
211
+
212
+ if (secondaryColour) {
213
+ Array.from(document.querySelectorAll("button:not(.fixedColour)")).forEach(e => e.style.color = `#${secondaryColour}`)
214
+ Array.from(document.querySelectorAll(".voiceType")).forEach(e => e.style.color = `#${secondaryColour}`)
215
+ Array.from(document.querySelectorAll("button")).forEach(e => e.style.textShadow = `none`)
216
+ Array.from(document.querySelectorAll(".voiceType")).forEach(e => e.style.textShadow = `none`)
217
+ } else {
218
+ Array.from(document.querySelectorAll("button:not(.fixedColour)")).forEach(e => e.style.color = `white`)
219
+ Array.from(document.querySelectorAll(".voiceType")).forEach(e => e.style.color = `white`)
220
+ Array.from(document.querySelectorAll("button")).forEach(e => e.style.textShadow = `0 0 2px black`)
221
+ Array.from(document.querySelectorAll(".voiceType")).forEach(e => e.style.textShadow = `0 0 2px black`)
222
+ }
223
+
224
+ if (window.wavesurfer) {
225
+ window.wavesurfer.setWaveColor(`#${window.currentGame.themeColourPrimary}`)
226
+ }
227
+
228
+ // Fade the background image transition
229
+ rightBG1.style.background = background
230
+ rightBG2.style.opacity = 0
231
+ setTimeout(() => {
232
+ rightBG2.style.background = rightBG1.style.background
233
+ rightBG2.style.opacity = 1
234
+ }, 1000)
235
+
236
+ cssHack.innerHTML = `::selection {
237
+ background: #${primaryColour};
238
+ }
239
+ ::-webkit-scrollbar-thumb {
240
+ background-color: #${primaryColour} !important;
241
+ }
242
+ .slider::-webkit-slider-thumb {
243
+ background-color: #${primaryColour} !important;
244
+ }
245
+ input[type=checkbox], input[type=range] {accent-color: #${primaryColour} !important;}
246
+ a {color: #${primaryColour}};
247
+ #batchRecordsHeader > div {background-color: #${primaryColour} !important;}
248
+ #pluginsRecordsHeader > div {background-color: #${primaryColour} !important;}
249
+
250
+ .invertedButton {
251
+ background: none !important;
252
+ border: 2px solid #${primaryColour} !important;
253
+ }
254
+
255
+ `
256
+ if (secondaryColour) {
257
+ cssHack.innerHTML += `
258
+ #batchRecordsHeader > div {color: #${secondaryColour} !important;text-shadow: none}
259
+ #pluginsRecordsHeader > div {color: #${secondaryColour} !important;text-shadow: none}
260
+ `
261
+ } else {
262
+ cssHack.innerHTML += `
263
+ #batchRecordsHeader > div {color: white !important;text-shadow: 0 0 2px black;}
264
+ #pluginsRecordsHeader > div {color: white !important;text-shadow: 0 0 2px black;}
265
+ `
266
+ }
267
+ }
268
+
269
+ window.getAudioPlayTriangleSVG = () => {
270
+ const div = createElem("div", `<svg class="renameSVG" version="1.0" xmlns="http://www.w3.org/2000/svg" width="770.000000pt" height="980.000000pt" viewBox="0 0 770.000000 980.000000"
271
+ preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,980.000000) scale(0.100000,-0.100000)"fill="#555555" stroke="none">
272
+ <path d="M26 9718 c-3 -46 -9 -2249 -12 -4897 -5 -4085 -4 -4813 8 -4809 8 3
273
+ 389 244 848 535 459 291 1598 1013 2530 1603 2872 1819 3648 2311 4182 2656
274
+ 60 38 108 75 108 81 0 55 -595 448 -2855 1886 -1259 801 -4552 2877 -4792
275
+ 3020 -8 5 -13 -16 -17 -75z"/>
276
+ </g>
277
+ </svg>`)
278
+ return div
279
+ }
280
+
281
+
282
+ window.addEventListener("resize", e => {
283
+ window.userSettings.customWindowSize = `${window.innerHeight},${window.innerWidth}`
284
+ saveUserSettings()
285
+ })
286
+
287
+ // Keyboard actions
288
+ // ================
289
+ window.addEventListener("keyup", event => {
290
+ if (!event.ctrlKey) {
291
+ window.ctrlKeyIsPressed = false
292
+ }
293
+ if (!event.shiftKey) {
294
+ window.shiftKeyIsPressed = false
295
+ }
296
+ })
297
+
298
+ dialogueInput.addEventListener("keydown", event => {
299
+ if (event.target==dialogueInput || event.target==letterPitchNumb || event.target==letterLengthNumb) {
300
+ // Enter: Generate sample
301
+ if (event.key=="Enter") {
302
+ generateVoiceButton.click()
303
+ event.preventDefault()
304
+ }
305
+ return
306
+ }
307
+ })
308
+
309
+ window.addEventListener("click", event => {
310
+ if (event.target.id!="dialogueInput") {
311
+ window.hideAutocomplete()
312
+ }
313
+ })
314
+
315
+ window.addEventListener("keydown", event => {
316
+
317
+ if (event.ctrlKey) {
318
+ window.ctrlKeyIsPressed = true
319
+ }
320
+ if (event.shiftKey) {
321
+ window.shiftKeyIsPressed = true
322
+ }
323
+
324
+ if (event.ctrlKey && event.key.toLowerCase()=="r") {
325
+ location.reload()
326
+ }
327
+
328
+ if (event.ctrlKey && event.shiftKey && event.key.toLowerCase()=="i") {
329
+ window.electron = require("electron")
330
+ er.BrowserWindow.getFocusedWindow().webContents.openDevTools()
331
+ return
332
+ }
333
+
334
+ if (event.ctrlKey) {
335
+ window.ctrlKeyIsPressed = true
336
+ }
337
+
338
+ // Re-gen the line if the user presses CTRL-ENTER, evne outside the prompt box
339
+ if (event.key=="Enter" && window.ctrlKeyIsPressed) {
340
+ generateVoiceButton.click()
341
+ }
342
+ // The Enter key to submit text input prompts in modals
343
+ if (event.key=="Enter" && modalContainer.style.display!="none" && event.target.tagName=="INPUT") {
344
+ activeModal.querySelector("button").click()
345
+ }
346
+ const key = event.key.toLowerCase()
347
+
348
+ // CTRL-S: Keep sample
349
+ if (key=="s" && event.ctrlKey && !event.shiftKey) {
350
+ keepSampleFunction(false)
351
+ }
352
+ // CTRL-SHIFT-S: Keep sample (but with rename prompt)
353
+ if (key=="s" && event.ctrlKey && event.shiftKey) {
354
+ keepSampleFunction(true)
355
+ }
356
+
357
+ // Disable keyboard controls while in a text input
358
+ if (event.target.tagName=="INPUT" && event.target.tagName!=dialogueInput || event.target.tagName=="TEXTAREA") {
359
+ return
360
+ }
361
+
362
+ if (event.target==dialogueInput || event.target==letterPitchNumb || event.target==letterLengthNumb) {
363
+ // Enter: Generate sample
364
+ if (event.key=="Enter") {
365
+ generateVoiceButton.click()
366
+ event.preventDefault()
367
+ }
368
+ return
369
+ }
370
+
371
+ // Escape: close modals
372
+ if (key=="escape") {
373
+ closeModal()
374
+ }
375
+ // Space: bring focus to the input textarea
376
+ if (key==" ") {
377
+ setTimeout(() => dialogueInput.focus(), 0)
378
+ }
379
+ // Create selection for all of the editor letters
380
+ if (key=="a" && event.ctrlKey && !event.shiftKey) {
381
+ window.sequenceEditor.letterFocus = []
382
+ window.sequenceEditor.sliderBoxes.forEach((_,i) => {
383
+ window.sequenceEditor.letterFocus.push(i)
384
+ window.sequenceEditor.setLetterFocus(i, true)
385
+ })
386
+ event.preventDefault()
387
+ return
388
+ }
389
+ // Y/N for prompt modals
390
+ if (key=="y" || key=="n" || key==" ") {
391
+ if (document.querySelector("#activeModal")) {
392
+ const buttons = Array.from(document.querySelector("#activeModal").querySelectorAll("button"))
393
+ const yesBtn = buttons.find(btn => btn.innerHTML.toLowerCase()=="yes")
394
+ const noBtn = buttons.find(btn => btn.innerHTML.toLowerCase()=="no")
395
+ if (key=="y") yesBtn.click()
396
+ if (key==" ") yesBtn.click()
397
+ if (key=="n") noBtn.click()
398
+ }
399
+ }
400
+ // Left/Right arrows: Move between letter focused (clears multi-select, picks the min(0,first-1) if left, max($L, last+1) if right)
401
+ // SHIFT-Left/Right: multi-letter create selection range
402
+ if ((key=="arrowleft" || key=="arrowright") && !event.ctrlKey) {
403
+ event.preventDefault()
404
+ if (window.sequenceEditor.letterFocus.length==0) {
405
+ window.sequenceEditor.setLetterFocus(0)
406
+ } else if (window.sequenceEditor.letterFocus.length==1) {
407
+ const curr_l = window.sequenceEditor.letterFocus[0]
408
+ const newIndex = key=="arrowleft" ? Math.max(0, curr_l-1) : Math.min(curr_l+1, window.sequenceEditor.letters.length-1)
409
+ window.sequenceEditor.setLetterFocus(newIndex, event.shiftKey)
410
+ } else {
411
+ if (key=="arrowleft") {
412
+ window.sequenceEditor.setLetterFocus(Math.max(0, Math.min(...window.sequenceEditor.letterFocus)-1), event.shiftKey)
413
+ } else {
414
+ window.sequenceEditor.setLetterFocus(Math.min(Math.max(...window.sequenceEditor.letterFocus)+1, window.sequenceEditor.letters.length-1), event.shiftKey)
415
+ }
416
+ }
417
+ }
418
+
419
+ // Up/Down arrows: Move pitch up/down for the letter(s) selected
420
+ if ((key=="arrowup" || key=="arrowdown") && !event.ctrlKey) {
421
+ event.preventDefault()
422
+ if (window.sequenceEditor.letterFocus.length) {
423
+ window.sequenceEditor.letterFocus.forEach(li => {
424
+ window.sequenceEditor.pitchNew[li] += (key=="arrowup" ? 0.1 : -0.1)
425
+ window.sequenceEditor.grabbers[li].setValueFromValue(window.sequenceEditor.pitchNew[li])
426
+ window.sequenceEditor.hasChanged = true
427
+ })
428
+ if (window.sequenceEditor.autoInferTimer != null) {
429
+ clearTimeout(window.sequenceEditor.autoInferTimer)
430
+ window.sequenceEditor.autoInferTimer = null
431
+ }
432
+ if (autoplay_ckbx.checked) {
433
+ window.sequenceEditor.autoInferTimer = setTimeout(infer, 500)
434
+ }
435
+
436
+ if (window.sequenceEditor.letterFocus.length==1) {
437
+ letterPitchNumb.value = window.sequenceEditor.pitchNew[window.sequenceEditor.letterFocus[0]]
438
+ }
439
+ }
440
+ }
441
+
442
+ // CTRL+Left/Right arrows: change the sequence-wide pacing
443
+ if ((key=="arrowleft" || key=="arrowright") && event.ctrlKey) {
444
+ if (event.altKey) {
445
+ window.sequenceEditor.letterFocus.forEach(li => {
446
+ window.sequenceEditor.dursNew[li] = window.sequenceEditor.dursNew[li] + (key=="arrowleft"? -0.1 : 0.1)
447
+ window.sequenceEditor.hasChanged = true
448
+ })
449
+ if (window.sequenceEditor.autoInferTimer != null) {
450
+ clearTimeout(window.sequenceEditor.autoInferTimer)
451
+ window.sequenceEditor.autoInferTimer = null
452
+ }
453
+ if (autoplay_ckbx.checked) {
454
+ window.sequenceEditor.autoInferTimer = setTimeout(infer, 500)
455
+ }
456
+ window.sequenceEditor.sliderBoxes.forEach((box, i) => box.setValueFromValue(window.sequenceEditor.dursNew[i]))
457
+
458
+ if (window.sequenceEditor.letterFocus.length==1) {
459
+ letterLengthNumb.value = parseInt(window.sequenceEditor.dursNew[window.sequenceEditor.letterFocus[0]]*100)/100
460
+ }
461
+ } else {
462
+ pace_slid.value = parseFloat(pace_slid.value) + (key=="arrowleft"? -0.01 : 0.01)
463
+ paceNumbInput.value = pace_slid.value
464
+ const new_lengths = window.sequenceEditor.dursNew.map((v,l) => v * pace_slid.value)
465
+ window.sequenceEditor.sliderBoxes.forEach((box, i) => box.setValueFromValue(window.sequenceEditor.dursNew[i]))
466
+ window.sequenceEditor.pacing = parseFloat(pace_slid.value)
467
+ window.sequenceEditor.init()
468
+
469
+ }
470
+ }
471
+
472
+ // CTRL+Up/Down arrows: increase/decrease buttons
473
+ if (key=="arrowup" && event.ctrlKey && !event.shiftKey) {
474
+ increase_btn.click()
475
+ }
476
+ if (key=="arrowdown" && event.ctrlKey && !event.shiftKey) {
477
+ decrease_btn.click()
478
+ }
479
+ // CTRL+SHIFT+Up/Down arrows: amplify/flatten buttons
480
+ if (key=="arrowup" && event.ctrlKey && event.shiftKey) {
481
+ amplify_btn.click()
482
+ }
483
+ if (key=="arrowdown" && event.ctrlKey && event.shiftKey) {
484
+ flatten_btn.click()
485
+ }
486
+ })
487
+
488
+
489
+
490
+ window.setupModal = (openingButton, modalContainerElem, callback, exitCallback) => {
491
+ if (openingButton) {
492
+ openingButton.addEventListener("click", () => {
493
+ closeModal(undefined, modalContainerElem).then(() => {
494
+ modalContainerElem.style.opacity = 0
495
+ modalContainerElem.style.display = "flex"
496
+ requestAnimationFrame(() => requestAnimationFrame(() => {
497
+ modalContainerElem.style.opacity = 1
498
+ chromeBar.style.opacity = 1
499
+ requestAnimationFrame(() => {
500
+ setTimeout(() => {
501
+ if (callback) {
502
+ callback()
503
+ }
504
+ }, 250)
505
+ })
506
+ }))
507
+ })
508
+ })
509
+ }
510
+ modalContainerElem.addEventListener("click", event => {
511
+ if (event.target==modalContainerElem) {
512
+ if (exitCallback) {
513
+ exitCallback()
514
+ }
515
+ window.closeModal(modalContainerElem)
516
+ }
517
+ })
518
+ }
519
+
520
+ window.checkVersionRequirements = (requirements, appVersion, checkMax=false) => {
521
+
522
+ if (!requirements) {
523
+ return true
524
+ }
525
+
526
+ const appVersionRequirement = requirements.toString().split(".").map(v=>parseInt(v))
527
+ const appVersionInts = appVersion.replace("v", "").split(".").map(v=>parseInt(v))
528
+ let appVersionOk = true
529
+
530
+ if (checkMax) {
531
+
532
+ if (appVersionRequirement[0] >= appVersionInts[0] ) {
533
+ if (appVersionRequirement.length>1 && parseInt(appVersionRequirement[0]) == appVersionInts[0]) {
534
+ if (appVersionRequirement[1] >= appVersionInts[1] ) {
535
+ if (appVersionRequirement.length>2 && parseInt(appVersionRequirement[1]) == appVersionInts[1]) {
536
+ if (appVersionRequirement[2] >= appVersionInts[2] ) {
537
+ } else {
538
+ appVersionOk = false
539
+ }
540
+ }
541
+ } else {
542
+ appVersionOk = false
543
+ }
544
+ }
545
+ } else {
546
+ appVersionOk = false
547
+ }
548
+
549
+
550
+ } else {
551
+ if (appVersionRequirement[0] <= appVersionInts[0] ) {
552
+ if (appVersionRequirement.length>1 && parseInt(appVersionRequirement[0]) == appVersionInts[0]) {
553
+ if (appVersionRequirement[1] <= appVersionInts[1] ) {
554
+ if (appVersionRequirement.length>2 && parseInt(appVersionRequirement[1]) == appVersionInts[1]) {
555
+ if (appVersionRequirement[2] <= appVersionInts[2] ) {
556
+ } else {
557
+ appVersionOk = false
558
+ }
559
+ }
560
+ } else {
561
+ appVersionOk = false
562
+ }
563
+ }
564
+ } else {
565
+ appVersionOk = false
566
+ }
567
+ }
568
+ return appVersionOk
569
+ }
570
+
571
+
572
+ // https://stackoverflow.com/questions/18052762/remove-directory-which-is-not-empty
573
+ const path = require('path')
574
+ window.deleteFolderRecursive = function (directoryPath, keepRoot=false) {
575
+ if (fs.existsSync(directoryPath)) {
576
+ fs.readdirSync(directoryPath).forEach((file, index) => {
577
+ const curPath = path.join(directoryPath, file);
578
+ if (fs.lstatSync(curPath).isDirectory()) {
579
+ // recurse
580
+ window.deleteFolderRecursive(curPath);
581
+ } else {
582
+ // delete file
583
+ fs.unlinkSync(curPath);
584
+ }
585
+ });
586
+ if (!keepRoot) {
587
+ fs.rmdirSync(directoryPath);
588
+ }
589
+ }
590
+ };
591
+
592
+ window.createFolderRecursive = (pathToMake) => {
593
+ console.log("createFolderRecursive", pathToMake)
594
+ pathToMake.split('/').reduce((directories, directory) => {
595
+ directories += `${directory}/`
596
+
597
+ if (!fs.existsSync(directories)) {
598
+ fs.mkdirSync(directories)
599
+ }
600
+
601
+ return directories
602
+ }, '')
603
+ }
604
+
605
+ window.uuidv4 = () => {
606
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
607
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8)
608
+ return v.toString(16)
609
+ })
610
+ }
611
+
612
+ // https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
613
+ window.shuffle = (array) => {
614
+ var currentIndex = array.length, randomIndex;
615
+
616
+ // While there remain elements to shuffle...
617
+ while (0 !== currentIndex) {
618
+
619
+ // Pick a remaining element...
620
+ randomIndex = Math.floor(Math.random() * currentIndex);
621
+ currentIndex--;
622
+
623
+ // And swap it with the current element.
624
+ [array[currentIndex], array[randomIndex]] = [
625
+ array[randomIndex], array[currentIndex]];
626
+ }
627
+
628
+ return array;
629
+ }
630
+
631
+
632
+ // Just for easier packaging of the voice models for publishing - yes, lazy
633
+ window.packageVoice_variantIndex = 0
634
+ window.packageVoice = (doVoicePreviewCreate, variants, {modelsPath, gameId}={}) => {
635
+
636
+ const {voiceId, hifi} = variants[window.packageVoice_variantIndex]
637
+
638
+ if (doVoicePreviewCreate) {
639
+ const files = fs.readdirSync(`./output`).filter(fname => fname.includes("temp-") && fname.includes(".wav"))
640
+ if (files.length) {
641
+ const options = {
642
+ hz: window.userSettings.audio.hz,
643
+ padStart: window.userSettings.audio.padStart,
644
+ padEnd: window.userSettings.audio.padEnd,
645
+ bit_depth: window.userSettings.audio.bitdepth,
646
+ amplitude: window.userSettings.audio.amplitude,
647
+ pitchMult: window.userSettings.audio.pitchMult,
648
+ tempo: window.userSettings.audio.tempo
649
+ }
650
+
651
+ doFetch(`http://localhost:8008/outputAudio`, {
652
+ method: "Post",
653
+ body: JSON.stringify({
654
+ input_path: `./output/${files[0]}`,
655
+ isBatchMode: false,
656
+ output_path: `${modelsPath}/${voiceId}_raw.wav`,
657
+ options: JSON.stringify(options)
658
+ })
659
+ }).then(r=>r.text()).then(() => {
660
+ try {
661
+ fs.unlinkSync(`${modelsPath}/${voiceId}.wav`)
662
+ } catch (e) {}
663
+ doFetch(`http://localhost:8008/normalizeAudio`, {
664
+ method: "Post",
665
+ body: JSON.stringify({
666
+ input_path: `${modelsPath}/${voiceId}_raw.wav`,
667
+ output_path: `${modelsPath}/${voiceId}.wav`
668
+ })
669
+ }).then(r=>r.text()).then((resp) => {
670
+ console.log(resp)
671
+ fs.unlinkSync(`${modelsPath}/${voiceId}_raw.wav`)
672
+ })
673
+ })
674
+ }
675
+ } else {
676
+ fs.mkdirSync(`./build/${voiceId}`)
677
+ fs.mkdirSync(`./build/${voiceId}/resources`)
678
+ fs.mkdirSync(`./build/${voiceId}/resources/app`)
679
+ fs.mkdirSync(`./build/${voiceId}/resources/app/models`)
680
+ fs.mkdirSync(`./build/${voiceId}/resources/app/models/${gameId}`)
681
+ fs.copyFileSync(`${modelsPath}/${voiceId}.json`, `./build/${voiceId}/resources/app/models/${gameId}/${voiceId}.json`)
682
+ fs.copyFileSync(`${modelsPath}/${voiceId}.wav`, `./build/${voiceId}/resources/app/models/${gameId}/${voiceId}.wav`)
683
+ fs.copyFileSync(`${modelsPath}/${voiceId}.pt`, `./build/${voiceId}/resources/app/models/${gameId}/${voiceId}.pt`)
684
+ // if (hifi) {
685
+ // fs.copyFileSync(`${modelsPath}/${voiceId}.hg.pt`, `./build/${voiceId}/resources/app/models/${gameId}/${voiceId}.hg.pt`)
686
+ // }
687
+ zipdir(`./build/${voiceId}`, {saveTo: `./build/${voiceId}.zip`}, (err, buffer) => deleteFolderRecursive(`./build/${voiceId}`))
688
+ }
689
+ }
690
+
691
+
692
+ const assetFiles = fs.readdirSync(`${window.path}/assets`)
693
+ const jsonFiles = fs.readdirSync(`${window.path}/assets`).filter(fn => fn.endsWith(".json"))
694
+ const missingAssetFiles = jsonFiles.filter(jsonFile => !(assetFiles.includes(jsonFile.replace(".json", ".png"))||assetFiles.includes(jsonFile.replace(".json", ".jpg"))) )
695
+ if (missingAssetFiles.length) {
696
+ noAssetFilesFoundMessage.style.display = "block"
697
+ assetDirLink.addEventListener("click", () => {
698
+ // shell.showItemInFolder((require("path")).resolve(`${window.path}/assets/other.jpg`))
699
+ er.shell.showItemInFolder((require("path")).resolve(`${window.path}/assets/other.jpg`))
700
+ spawn(`explorer`, [(require("path")).resolve(`${window.path}/assets`)], {stdio: "ignore"})
701
+ })
702
+ }
703
+
704
+
705
+ window.getSpeakerEmbeddingFromFilePath = (filePath) => {
706
+ spinnerModal(`${window.i18n.GETTING_SPEAKER_EMBEDDING}`)
707
+
708
+ return new Promise(resolve => {
709
+ doFetch(`http://localhost:8008/getWavV3StyleEmb`, {
710
+ method: "Post",
711
+ body: JSON.stringify({wav_path: filePath})
712
+ }).then(r=>r.text()).then(v => {
713
+ closeModal(undefined, [styleEmbeddingsContainer, workbenchContainer])
714
+ if (v=="ENOENT") {
715
+ window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}<br><br>ENOENT`)
716
+ } else {
717
+ resolve(v)
718
+ }
719
+ }).catch(() => {
720
+ window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}`)
721
+ })
722
+ })
723
+ }
724
+
725
+ window.unzipFileTo = (zipPath, outputFolder) => {
726
+ return fs.createReadStream(zipPath).pipe(unzipper.Parse()).on("entry", entry => {
727
+ const fileName = entry.path
728
+ const dirOrFile = entry.type
729
+
730
+ if (/\/$/.test(fileName)) { // It's a directory
731
+ return
732
+ }
733
+
734
+ let fileContainerFolderPath = fileName.split("/").reverse()
735
+ const justFileName = fileContainerFolderPath[0]
736
+
737
+ entry.pipe(fs.createWriteStream(`${outputFolder}/${justFileName}`))
738
+ })
739
+ .promise()
740
+ }
javascript/workbench.js ADDED
@@ -0,0 +1,497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+ const er = require('@electron/remote')
3
+ const dialog = er.dialog
4
+
5
+
6
+ window.voiceWorkbenchState = {
7
+ isInit: false,
8
+ isStarted: false,
9
+ currentAudioFilePath: undefined,
10
+ newAudioFilePath: undefined,
11
+ currentEmb: undefined,
12
+ refAEmb: undefined,
13
+ refBEmb: undefined,
14
+ }
15
+
16
+ window.initVoiceWorkbench = () => {
17
+ if (!window.voiceWorkbenchState.isInit) {
18
+ window.voiceWorkbenchState.isInit = true
19
+ window.refreshExistingCraftedVoices()
20
+ window.initDropdowns()
21
+ voiceWorkbenchLanguageDropdown.value = "en"
22
+ }
23
+ window.refreshExistingCraftedVoices()
24
+ }
25
+
26
+ window.refreshExistingCraftedVoices = () => {
27
+ voiceWorkbenchVoicesList.innerHTML = ""
28
+ Object.keys(window.games).sort((a,b)=>a>b?1:-1).forEach(gameId => {
29
+ if (Object.keys(window.gameAssets).includes(gameId)) {
30
+ const themeColour = window.gameAssets[gameId].themeColourPrimary
31
+ window.games[gameId].models.forEach(model => {
32
+ if (model.embOverABaseModel) {
33
+ const button = createElem("div.voiceType", model.voiceName)
34
+ button.style.background = `#${themeColour}`
35
+ button.addEventListener("click", () => window.voiceWorkbenchLoadOrResetCraftedVoice(model))
36
+ voiceWorkbenchVoicesList.appendChild(button)
37
+ }
38
+ })
39
+ }
40
+ })
41
+ }
42
+
43
+ window.voiceWorkbenchLoadOrResetCraftedVoice = (model) => {
44
+
45
+ voiceWorkbenchModelDropdown.value = model ? model.embOverABaseModel : "<base>/base_v1.0"
46
+ voiceWorkbenchVoiceNameInput.value = model ? model.voiceName : ""
47
+ voiceWorkbenchVoiceIDInput.value = model ? model.variants[0].voiceId : ""
48
+ voiceWorkbenchGenderDropdown.value = model ? model.variants[0].gender : "male"
49
+ voiceWorkbenchAuthorInput.value = model ? (model.variants[0].author || "Anonymous") : ""
50
+ voiceWorkbenchLanguageDropdown.value = model ? model.variants[0].lang : "en"
51
+ voiceWorkbenchGamesDropdown.value = model ? model.gameId : "other"
52
+ voiceWorkbenchCurrentEmbeddingInput.value = model ? model.variants[0].base_speaker_emb : ""
53
+ window.voiceWorkbenchState.currentEmb = model ? model.variants[0].base_speaker_emb : undefined
54
+
55
+ window.voiceWorkbenchState.currentAudioFilePath = undefined
56
+ window.voiceWorkbenchState.newAudioFilePath = undefined
57
+ window.voiceWorkbenchState.refAEmb = undefined
58
+ window.voiceWorkbenchState.refBEmb = undefined
59
+
60
+ voiceWorkbenchRefAInput.value = ""
61
+ voiceWorkbenchRefBInput.value = ""
62
+
63
+ if (model) {
64
+ voiceWorkbenchDeleteButton.disabled = false
65
+ voiceWorkbenchStartButton.click()
66
+ }
67
+ }
68
+
69
+ window.voiceWorkbenchGenerateVoice = async () => {
70
+
71
+ if (!voiceWorkbenchCurrentEmbeddingInput.value.length) {
72
+ return window.errorModal(window.i18n.ENTER_VOICE_CRAFTING_STARTING_EMB)
73
+ }
74
+
75
+ // Load the model if it hasn't been loaded already
76
+ let voiceId = voiceWorkbenchModelDropdown.value.split("/").at(-1)
77
+ if (!window.currentModel || window.currentModel.voiceId!=voiceId) {
78
+ let modelPath
79
+ if (voiceId.includes("Base xVAPitch Model")) {
80
+ modelPath = `${window.path}/python/xvapitch/base_v1.0.pt`
81
+ } else {
82
+ const gameId = voiceWorkbenchModelDropdown.value.split("/").at(0)
83
+ modelPath = window.userSettings[`modelspath_${gameId}`]+"/"+voiceId
84
+ }
85
+ await window.voiceWorkbenchChangeModel(modelPath, voiceId)
86
+ }
87
+
88
+ const base_lang = voiceWorkbenchLanguageDropdown.value//voiceId.includes("Base xVAPitch Model") ? "en" : window.currentModel.lang
89
+ let newEmb = undefined
90
+
91
+ // const currentEmbedding = voiceWorkbenchCurrentEmbeddingInput.value.split(",").map(v=>parseFloat(v))
92
+ const currentEmbedding = window.voiceWorkbenchState.currentEmb
93
+ // const currentDelta = voiceWorkbenchCurrentDeltaInput.value.split(",").map(v=>parseFloat(v))
94
+ const newEmbedding = window.getVoiceWorkbenchNewEmbedding()
95
+
96
+ const tempFileNum = `${Math.random().toString().split(".")[1]}`
97
+ const currentTempFileLocation = `${path}/output/temp-${tempFileNum}_current.wav`
98
+ const newTempFileLocation = `${path}/output/temp-${tempFileNum}_new.wav`
99
+
100
+ // Do the current embedding first
101
+ const synthRequests = []
102
+ synthRequests.push(doSynth(JSON.stringify({
103
+ sequence: voiceWorkbenchInputTextArea.value.trim(),
104
+ useCleanup: true, // TODO, user setting?
105
+ base_lang, base_emb: currentEmbedding.join(","), outfile: currentTempFileLocation
106
+ })))
107
+ let doingNewAudioFile = false
108
+ if (voiceWorkbenchCurrentDeltaInput.value.length) {
109
+ doingNewAudioFile = true
110
+ synthRequests.push(doSynth(JSON.stringify({
111
+ sequence: voiceWorkbenchInputTextArea.value.trim(),
112
+ useCleanup: true, // TODO, user setting?
113
+ base_lang, base_emb: newEmbedding.join(","), outfile: newTempFileLocation
114
+ })))
115
+ }
116
+
117
+ // toggleSpinnerButtons()
118
+ spinnerModal(`${window.i18n.SYNTHESIZING}`)
119
+ Promise.all(synthRequests).then(res => {
120
+ closeModal(undefined, [workbenchContainer])
121
+ window.voiceWorkbenchState.currentAudioFilePath = currentTempFileLocation
122
+ voiceWorkbenchAudioCurrentPlayPauseBtn.disabled = false
123
+ voiceWorkbenchAudioCurrentSaveBtn.disabled = false
124
+
125
+ if (doingNewAudioFile) {
126
+ window.voiceWorkbenchState.newAudioFilePath = newTempFileLocation
127
+ voiceWorkbenchAudioNewPlayBtn.disabled = false
128
+ voiceWorkbenchAudioNewSaveBtn.disabled = false
129
+ }
130
+ })
131
+ }
132
+ const doSynth = (body) => {
133
+ return new Promise(resolve => {
134
+ doFetch("http://localhost:8008/synthesizeSimple", {
135
+ method: "Post",
136
+ body
137
+ }).then(r=>r.text()).then(resolve)
138
+ })
139
+ }
140
+
141
+ window.getVoiceWorkbenchNewEmbedding = () => {
142
+ const currentDelta = voiceWorkbenchCurrentDeltaInput.value.split(",").map(v=>parseFloat(v))
143
+ const newEmb = window.voiceWorkbenchState.currentEmb.map((v,vi) => {
144
+ return v + currentDelta[vi]//*strength
145
+ })
146
+ return newEmb
147
+ }
148
+
149
+ window.voiceWorkbenchChangeModel = (modelPath, voiceId) => {
150
+ window.currentModel = {
151
+ outputs: undefined,
152
+ model: modelPath.replace(".pt", ""),
153
+ modelType: "xVAPitch",
154
+ base_lang: voiceWorkbenchLanguageDropdown.value,
155
+ isBaseModel: true,
156
+ voiceId: voiceId
157
+ }
158
+ generateVoiceButton.dataset.modelQuery = JSON.stringify(window.currentModel)
159
+ return window.loadModel()
160
+ }
161
+ voiceWorkbenchGenerateSampleButton.addEventListener("click", window.voiceWorkbenchGenerateVoice)
162
+
163
+
164
+ window.initDropdowns = () => {
165
+ // Games dropdown
166
+ Object.keys(window.games).sort((a,b)=>a>b?1:-1).forEach(gameId => {
167
+ if (gameId!="other") {
168
+ if (Object.keys(window.gameAssets).includes(gameId)) {
169
+ const gameName = window.games[gameId].gameTheme.gameName
170
+ const option = createElem("option", gameName)
171
+ option.value = gameId
172
+ voiceWorkbenchGamesDropdown.appendChild(option)
173
+ }
174
+ }
175
+ })
176
+
177
+ // Models dropdown
178
+ Object.keys(window.games).forEach(gameId => {
179
+ if (window.games[gameId].gameTheme) {
180
+ const gameName = window.games[gameId].gameTheme.gameName
181
+ window.games[gameId].models.forEach(modelMeta => {
182
+ const voiceName = modelMeta.voiceName
183
+ const voiceId = modelMeta.variants[0].voiceId
184
+
185
+ // Variants are not supported by v3 models, so pick the first one only. Also, filter out crafted voices
186
+ if (modelMeta.variants[0].modelType=="xVAPitch" && !modelMeta.embOverABaseModel) {
187
+
188
+ const option = createElem("option", `[${gameName}] ${voiceName}`)
189
+ option.value = `${gameId}/${voiceId}`
190
+ voiceWorkbenchModelDropdown.appendChild(option)
191
+ }
192
+ })
193
+ }
194
+ })
195
+ }
196
+
197
+ // Change the available languages when the model is changed
198
+ voiceWorkbenchModelDropdown.addEventListener("change", () => {
199
+ let voiceId = voiceWorkbenchModelDropdown.value.split("/").at(-1)
200
+ if (voiceId.includes("base_v1.0")) {
201
+ window.populateLanguagesDropdownsFromModel(voiceWorkbenchLanguageDropdown)
202
+ voiceWorkbenchLanguageDropdown.value = "en"
203
+ } else {
204
+ const gameId = voiceWorkbenchModelDropdown.value.split("/")[0]
205
+ if (Object.keys(window.games).includes(gameId)) {
206
+ const baseModelData = window.games[gameId].models.filter(model => {
207
+ return model.variants[0].voiceId == voiceWorkbenchModelDropdown.value.split("/").at(-1)
208
+ })[0]
209
+ window.populateLanguagesDropdownsFromModel(voiceWorkbenchLanguageDropdown, baseModelData)
210
+ voiceWorkbenchLanguageDropdown.value = baseModelData.variants[0].lang
211
+ }
212
+ }
213
+ })
214
+
215
+ voiceWorkbenchStartButton.addEventListener("click", () => {
216
+ window.voiceWorkbenchState.isStarted = true
217
+
218
+ voiceWorkbenchLoadedContent.style.display = "flex"
219
+ voiceWorkbenchLoadedContent2.style.display = "flex"
220
+ voiceWorkbenchStartButton.style.display = "none"
221
+
222
+
223
+ // Load the base model's embedding as a starting point, if it's not the built-in base model
224
+ let voiceId = voiceWorkbenchModelDropdown.value.split("/").at(-1)
225
+ if (voiceId.includes("base_v1.0")) {
226
+ } else {
227
+ const gameId = voiceWorkbenchModelDropdown.value.split("/")[0]
228
+ if (Object.keys(window.games).includes(gameId)) {
229
+ const baseModelData = window.games[gameId].models.filter(model => {
230
+ return model.variants[0].voiceId == voiceWorkbenchModelDropdown.value.split("/").at(-1)
231
+ })[0]
232
+ voiceWorkbenchCurrentEmbeddingInput.value = baseModelData.variants[0].base_speaker_emb.join(",")
233
+ window.voiceWorkbenchState.currentEmb = baseModelData.variants[0].base_speaker_emb
234
+ }
235
+ }
236
+ })
237
+
238
+ window.setupVoiceWorkbenchDropArea = (container, inputField, callback=undefined) => {
239
+ const dropFn = (eType, event) => {
240
+ if (["dragenter", "dragover"].includes(eType)) {
241
+ container.style.background = "#5b5b5b"
242
+ container.style.color = "white"
243
+ }
244
+ if (["dragleave", "drop"].includes(eType)) {
245
+ container.style.background = "rgba(0,0,0,0)"
246
+ container.style.color = "white"
247
+ }
248
+
249
+ event.preventDefault()
250
+ event.stopPropagation()
251
+
252
+ const dataLines = []
253
+
254
+ if (eType=="drop") {
255
+ const dataTransfer = event.dataTransfer
256
+ const files = Array.from(dataTransfer.files)
257
+
258
+ if (files[0].path.endsWith(".wav")) {
259
+ const filePath = String(files[0].path).replaceAll(/\\/g, "/")
260
+ console.log("filePath", filePath)
261
+ window.getSpeakerEmbeddingFromFilePath(filePath).then(embedding => {
262
+ inputField.value = embedding
263
+ if (callback) {
264
+ callback(filePath)
265
+ }
266
+ })
267
+ } else {
268
+ window.errorModal(window.i18n.ERROR_FILE_MUST_BE_WAV)
269
+ }
270
+ }
271
+ }
272
+
273
+ container.addEventListener("dragenter", event => dropFn("dragenter", event), false)
274
+ container.addEventListener("dragleave", event => dropFn("dragleave", event), false)
275
+ container.addEventListener("dragover", event => dropFn("dragover", event), false)
276
+ container.addEventListener("drop", event => dropFn("drop", event), false)
277
+ }
278
+
279
+ window.setupVoiceWorkbenchDropArea(voiceWorkbenchCurrentEmbeddingDropzone, voiceWorkbenchCurrentEmbeddingInput, () => {
280
+ window.voiceWorkbenchState.currentEmb = voiceWorkbenchCurrentEmbeddingInput.value.split(",").map(v=>parseFloat(v))
281
+ })
282
+ voiceWorkbenchCurrentEmbeddingInput.addEventListener("change", ()=>{
283
+ window.voiceWorkbenchState.currentEmb = voiceWorkbenchCurrentEmbeddingInput.value.split(",").map(v=>parseFloat(v))
284
+ })
285
+ window.setupVoiceWorkbenchDropArea(voiceWorkbenchRefADropzone, voiceWorkbenchRefAInput, (filePath) => {
286
+ voiceWorkbenchRefAFilePath.innerHTML = window.i18n.FROM_FILE_IS_FILEPATH.replace("_1", filePath)
287
+ voiceWorkshopApplyDeltaButton.disabled = false
288
+ window.voiceWorkbenchState.refAEmb = voiceWorkbenchRefAInput.value.split(",").map(v=>parseFloat(v))
289
+ window.voiceWorkbenchUpdateDelta()
290
+ })
291
+ window.setupVoiceWorkbenchDropArea(voiceWorkbenchRefBDropzone, voiceWorkbenchRefBInput, (filePath) => {
292
+ voiceWorkbenchRefBFilePath.innerHTML = window.i18n.FROM_FILE_IS_FILEPATH.replace("_1", filePath)
293
+ window.voiceWorkbenchState.refBEmb = voiceWorkbenchRefBInput.value.split(",").map(v=>parseFloat(v))
294
+ window.voiceWorkbenchUpdateDelta()
295
+ })
296
+
297
+ voiceWorkbenchInputTextArea.addEventListener("keyup", () => {
298
+ voiceWorkbenchGenerateSampleButton.disabled = voiceWorkbenchInputTextArea.value.trim().length==0
299
+ })
300
+ voiceWorkbenchAudioCurrentPlayPauseBtn.addEventListener("click", () => {
301
+ const audioPreview = createElem("audio", {autoplay: false}, createElem("source", {
302
+ src: window.voiceWorkbenchState.currentAudioFilePath
303
+ }))
304
+ audioPreview.setSinkId(window.userSettings.base_speaker)
305
+ })
306
+ voiceWorkbenchAudioCurrentSaveBtn.addEventListener("click", async () => {
307
+ const userChosenPath = await dialog.showSaveDialog({ defaultPath: window.voiceWorkbenchState.currentAudioFilePath })
308
+ if (userChosenPath && userChosenPath.filePath) {
309
+ const outFilePath = userChosenPath.filePath.split(".").at(-1)=="wav" ? userChosenPath.filePath : userChosenPath.filePath+".wav"
310
+ fs.copyFileSync(window.voiceWorkbenchState.currentAudioFilePath, outFilePath)
311
+ }
312
+ })
313
+ voiceWorkbenchAudioNewPlayBtn.addEventListener("click", () => {
314
+ const audioPreview = createElem("audio", {autoplay: false}, createElem("source", {
315
+ src: window.voiceWorkbenchState.newAudioFilePath
316
+ }))
317
+ audioPreview.setSinkId(window.userSettings.base_speaker)
318
+ })
319
+ voiceWorkbenchAudioNewSaveBtn.addEventListener("click", async () => {
320
+ const userChosenPath = await dialog.showSaveDialog({ defaultPath: window.voiceWorkbenchState.newAudioFilePath })
321
+ if (userChosenPath && userChosenPath.filePath) {
322
+ const outFilePath = userChosenPath.filePath.split(".").at(-1)=="wav" ? userChosenPath.filePath : userChosenPath.filePath+".wav"
323
+ fs.copyFileSync(window.voiceWorkbenchState.newAudioFilePath, outFilePath)
324
+ }
325
+ })
326
+
327
+ window.voiceWorkbenchUpdateDelta = () => {
328
+ // Don't do anything if reference file A isn't given
329
+ if (!window.voiceWorkbenchState.refAEmb) {
330
+ return
331
+ }
332
+
333
+ const strengthValue = parseFloat(voiceWorkbenchStrengthInput.value)
334
+
335
+ let delta
336
+
337
+ // When only Ref A is used, the delta is from <current> towards the first reference file A
338
+ if (window.voiceWorkbenchState.refBEmb == undefined) {
339
+ delta = window.voiceWorkbenchState.currentEmb.map((v,vi) => {
340
+ return (window.voiceWorkbenchState.refAEmb[vi] - v) * strengthValue
341
+ })
342
+ } else {
343
+ // When Ref B is also used, the delta is from ref A to ref B
344
+ delta = window.voiceWorkbenchState.refAEmb.map((v,vi) => {
345
+ return (window.voiceWorkbenchState.refBEmb[vi] - v) * strengthValue
346
+ })
347
+ }
348
+
349
+ voiceWorkbenchCurrentDeltaInput.value = delta.join(",")
350
+ }
351
+
352
+ voiceWorkbenchStrengthSlider.addEventListener("change", () => {
353
+ voiceWorkbenchStrengthInput.value = voiceWorkbenchStrengthSlider.value
354
+ window.voiceWorkbenchUpdateDelta()
355
+ })
356
+ voiceWorkbenchStrengthInput.addEventListener("change", () => {
357
+ voiceWorkbenchStrengthSlider.value = voiceWorkbenchStrengthInput.value
358
+ window.voiceWorkbenchUpdateDelta()
359
+ })
360
+ voiceWorkshopApplyDeltaButton.addEventListener("click", () => {
361
+ if (voiceWorkbenchCurrentDeltaInput.value.length) {
362
+ const newEmb = window.getVoiceWorkbenchNewEmbedding()
363
+ window.voiceWorkbenchState.currentEmb = newEmb
364
+ voiceWorkbenchCurrentEmbeddingInput.value = newEmb.join(",")
365
+ voiceWorkbenchCurrentDeltaInput.value = ""
366
+ voiceWorkshopApplyDeltaButton.disabled = true
367
+ voiceWorkbenchRefAInput.value = ""
368
+ window.voiceWorkbenchState.refAEmb = undefined
369
+ voiceWorkbenchRefBInput.value = ""
370
+ window.voiceWorkbenchState.refBEmb = undefined
371
+ }
372
+ })
373
+
374
+ /*
375
+ Drop file A over the reference audio file A area, to get its embedding
376
+ When only the reference A file is used, the current delta is this embedding multiplied by the strength
377
+
378
+ Drop file B over the B area, to get a second embedding
379
+ When both this and A are active, the current delta is the direction from A to B, multiplied by the strength
380
+ direction meaning B minus A, instead of <current> minus A
381
+ */
382
+
383
+ voiceWorkbenchSaveButton.addEventListener("click", () => {
384
+
385
+ const voiceName = voiceWorkbenchVoiceNameInput.value
386
+ const voiceId = voiceWorkbenchVoiceIDInput.value
387
+ const gender = voiceWorkbenchGenderDropdown.value
388
+ const author = voiceWorkbenchAuthorInput.value || "Anonymous"
389
+ const lang = voiceWorkbenchLanguageDropdown.value
390
+
391
+
392
+ if (!voiceName.trim().length) {
393
+ return window.errorModal(window.i18n.ENTER_VOICE_NAME)
394
+ }
395
+ if (!voiceId.trim().length) {
396
+ return window.errorModal(window.i18n.ENTER_VOICE_ID)
397
+ }
398
+
399
+ const modelJson = {
400
+ "version": "3.0",
401
+ "modelVersion": "3.0",
402
+ "modelType": "xVAPitch",
403
+ "author": author,
404
+ "lang": lang,
405
+ "embOverABaseModel": voiceWorkbenchModelDropdown.value,
406
+ "games": [
407
+ {
408
+ "gameId": voiceWorkbenchGamesDropdown.value,
409
+ "voiceId": voiceId,
410
+ "variant": "Default",
411
+ "voiceName": voiceName,
412
+ "base_speaker_emb": window.voiceWorkbenchState.currentEmb,
413
+ "gender": gender
414
+ }
415
+ ]
416
+ }
417
+ const gameModelsPath = `${window.userSettings[`modelspath_${voiceWorkbenchGamesDropdown.value}`]}`
418
+
419
+ const jsonDestination = `${gameModelsPath}/${voiceId}.json`
420
+ fs.writeFileSync(jsonDestination, JSON.stringify(modelJson, null, 4))
421
+
422
+ doSynth(JSON.stringify({
423
+ sequence: " This is what my voice sounds like. ",
424
+ useCleanup: true, // TODO, user setting?
425
+ base_lang: lang,
426
+ base_emb: window.voiceWorkbenchState.currentEmb.join(","), outfile: jsonDestination.replace(".json", ".wav")
427
+ })).then(() => {
428
+ voiceWorkbenchDeleteButton.disabled = false
429
+ window.currentModel = undefined
430
+ generateVoiceButton.dataset.modelQuery = null
431
+ window.infoModal(window.i18n.VOICE_CREATED_AT.replace("_1", jsonDestination))
432
+
433
+ // Clean up the temp file from the clean-up post-processing, if it exists
434
+ if (fs.existsSync(jsonDestination.replace(".json", "_preCleanup.wav"))) {
435
+ fs.unlinkSync(jsonDestination.replace(".json", "_preCleanup.wav"))
436
+ }
437
+
438
+ window.loadAllModels().then(() => {
439
+ window.refreshExistingCraftedVoices()
440
+
441
+ // Refresh the main page voice models if the same game is loaded as the target game models directory as saved into
442
+ if (window.currentGame.gameId==voiceWorkbenchGamesDropdown.value) {
443
+ window.changeGame(window.currentGame)
444
+ window.refreshExistingCraftedVoices()
445
+ }
446
+ })
447
+ })
448
+ })
449
+
450
+ voiceWorkbenchGamesDropdown.addEventListener("change", () => {
451
+ const gameModelsPath = `${window.userSettings[`modelspath_${voiceWorkbenchGamesDropdown.value}`]}`
452
+ const voiceId = voiceWorkbenchVoiceIDInput.value
453
+ const jsonLocation = `${gameModelsPath}/${voiceId}.json`
454
+ voiceWorkbenchDeleteButton.disabled = !fs.existsSync(jsonLocation)
455
+ })
456
+ voiceWorkbenchVoiceIDInput.addEventListener("change", () => {
457
+ const gameModelsPath = `${window.userSettings[`modelspath_${voiceWorkbenchGamesDropdown.value}`]}`
458
+ const voiceId = voiceWorkbenchVoiceIDInput.value
459
+ const jsonLocation = `${gameModelsPath}/${voiceId}.json`
460
+ voiceWorkbenchDeleteButton.disabled = !fs.existsSync(jsonLocation)
461
+ })
462
+ voiceWorkbenchDeleteButton.addEventListener("click", () => {
463
+ const gameModelsPath = `${window.userSettings[`modelspath_${voiceWorkbenchGamesDropdown.value}`]}`
464
+ const voiceId = voiceWorkbenchVoiceIDInput.value
465
+ const jsonLocation = `${gameModelsPath}/${voiceId}.json`
466
+ window.confirmModal(window.i18n.CONFIRM_DELETE_CRAFTED_VOICE.replace("_1", voiceWorkbenchVoiceNameInput.value).replace("_2", jsonLocation)).then(resp => {
467
+ if (resp) {
468
+ if (fs.existsSync(jsonLocation.replace(".json", ".wav"))) {
469
+ fs.unlinkSync(jsonLocation.replace(".json", ".wav"))
470
+ }
471
+ fs.unlinkSync(jsonLocation)
472
+ }
473
+ window.infoModal(window.i18n.SUCCESSFULLY_DELETED_CRAFTED_VOICE)
474
+ window.loadAllModels().then(() => {
475
+
476
+ // Refresh the main page voice models if the same game is loaded as the target game models directory deleted from
477
+ if (window.currentGame.gameId==voiceWorkbenchGamesDropdown.value) {
478
+
479
+ window.changeGame(window.currentGame)
480
+ window.refreshExistingCraftedVoices()
481
+ }
482
+ voiceWorkbenchCancelButton.click()
483
+ })
484
+ })
485
+ })
486
+
487
+
488
+
489
+ voiceWorkbenchCancelButton.addEventListener("click", () => {
490
+ window.voiceWorkbenchState.isStarted = false
491
+
492
+ voiceWorkbenchLoadedContent.style.display = "none"
493
+ voiceWorkbenchLoadedContent2.style.display = "none"
494
+ voiceWorkbenchStartButton.style.display = "flex"
495
+
496
+ window.voiceWorkbenchLoadOrResetCraftedVoice()
497
+ })
lib/AbortControllerPolyfill.js ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ /* Polyfill service v3.105.0
2
+ * Disable minification (remove `.min` from URL path) for more info */
3
+
4
+ (function(self, undefined) {!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.WHATWGFetch={})}(this,function(t){"use strict";function e(t){return t&&DataView.prototype.isPrototypeOf(t)}function r(t){if("string"!=typeof t&&(t=String(t)),/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(t))throw new TypeError("Invalid character in header field name");return t.toLowerCase()}function o(t){return"string"!=typeof t&&(t=String(t)),t}function n(t){var e={next:function(){var e=t.shift();return{done:e===undefined,value:e}}};return v.iterable&&(e[Symbol.iterator]=function(){return e}),e}function i(t){this.map={},t instanceof i?t.forEach(function(t,e){this.append(e,t)},this):Array.isArray(t)?t.forEach(function(t){this.append(t[0],t[1])},this):t&&Object.getOwnPropertyNames(t).forEach(function(e){this.append(e,t[e])},this)}function s(t){if(t.bodyUsed)return Promise.reject(new TypeError("Already read"));t.bodyUsed=!0}function a(t){return new Promise(function(e,r){t.onload=function(){e(t.result)},t.onerror=function(){r(t.error)}})}function f(t){var e=new FileReader,r=a(e);return e.readAsArrayBuffer(t),r}function u(t){var e=new FileReader,r=a(e);return e.readAsText(t),r}function h(t){for(var e=new Uint8Array(t),r=new Array(e.length),o=0;o<e.length;o++)r[o]=String.fromCharCode(e[o]);return r.join("")}function d(t){if(t.slice)return t.slice(0);var e=new Uint8Array(t.byteLength);return e.set(new Uint8Array(t)),e.buffer}function c(){return this.bodyUsed=!1,this._initBody=function(t){this._bodyInit=t,t?"string"==typeof t?this._bodyText=t:v.blob&&Blob.prototype.isPrototypeOf(t)?this._bodyBlob=t:v.formData&&FormData.prototype.isPrototypeOf(t)?this._bodyFormData=t:v.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)?this._bodyText=t.toString():v.arrayBuffer&&v.blob&&e(t)?(this._bodyArrayBuffer=d(t.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):v.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(t)||A(t))?this._bodyArrayBuffer=d(t):this._bodyText=t=Object.prototype.toString.call(t):this._bodyText="",this.headers.get("content-type")||("string"==typeof t?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):v.searchParams&&URLSearchParams.prototype.isPrototypeOf(t)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},v.blob&&(this.blob=function(){var t=s(this);if(t)return t;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this._bodyArrayBuffer?s(this)||Promise.resolve(this._bodyArrayBuffer):this.blob().then(f)}),this.text=function(){var t=s(this);if(t)return t;if(this._bodyBlob)return u(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(h(this._bodyArrayBuffer));if(this._bodyFormData)throw new Error("could not read FormData body as text");return Promise.resolve(this._bodyText)},v.formData&&(this.formData=function(){return this.text().then(l)}),this.json=function(){return this.text().then(JSON.parse)},this}function y(t){var e=t.toUpperCase();return _.indexOf(e)>-1?e:t}function p(t,e){e=e||{};var r=e.body;if(t instanceof p){if(t.bodyUsed)throw new TypeError("Already read");this.url=t.url,this.credentials=t.credentials,e.headers||(this.headers=new i(t.headers)),this.method=t.method,this.mode=t.mode,this.signal=t.signal,r||null==t._bodyInit||(r=t._bodyInit,t.bodyUsed=!0)}else this.url=String(t);if(this.credentials=e.credentials||this.credentials||"same-origin",!e.headers&&this.headers||(this.headers=new i(e.headers)),this.method=y(e.method||this.method||"GET"),this.mode=e.mode||this.mode||null,this.signal=e.signal||this.signal,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&r)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(r)}function l(t){var e=new FormData;return t.trim().split("&").forEach(function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}}),e}function b(t){var e=new i;return t.replace(/\r?\n[\t ]+/g," ").split(/\r?\n/).forEach(function(t){var r=t.split(":"),o=r.shift().trim();if(o){var n=r.join(":").trim();e.append(o,n)}}),e}function m(t,e){e||(e={}),this.type="default",this.status=e.status===undefined?200:e.status,this.ok=this.status>=200&&this.status<300,this.statusText="statusText"in e?e.statusText:"OK",this.headers=new i(e.headers),this.url=e.url||"",this._initBody(t)}function w(e,r){return new Promise(function(o,n){function i(){a.abort()}var s=new p(e,r);if(s.signal&&s.signal.aborted)return n(new t.DOMException("Aborted","AbortError"));var a=new XMLHttpRequest;a.onload=function(){var t={status:a.status,statusText:a.statusText,headers:b(a.getAllResponseHeaders()||"")};t.url="responseURL"in a?a.responseURL:t.headers.get("X-Request-URL");var e="response"in a?a.response:a.responseText;o(new m(e,t))},a.onerror=function(){n(new TypeError("Network request failed"))},a.ontimeout=function(){n(new TypeError("Network request failed"))},a.onabort=function(){n(new t.DOMException("Aborted","AbortError"))},a.open(s.method,s.url,!0),"include"===s.credentials?a.withCredentials=!0:"omit"===s.credentials&&(a.withCredentials=!1),"responseType"in a&&v.blob&&(a.responseType="blob"),s.headers.forEach(function(t,e){a.setRequestHeader(e,t)}),s.signal&&(s.signal.addEventListener("abort",i),a.onreadystatechange=function(){4===a.readyState&&s.signal.removeEventListener("abort",i)}),a.send("undefined"==typeof s._bodyInit?null:s._bodyInit)})}var v={searchParams:"URLSearchParams"in self,iterable:"Symbol"in self&&"iterator"in Symbol,blob:"FileReader"in self&&"Blob"in self&&function(){try{return new Blob,!0}catch(t){return!1}}(),formData:"FormData"in self,arrayBuffer:"ArrayBuffer"in self};if(v.arrayBuffer)var E=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],A=ArrayBuffer.isView||function(t){return t&&E.indexOf(Object.prototype.toString.call(t))>-1};i.prototype.append=function(t,e){t=r(t),e=o(e);var n=this.map[t];this.map[t]=n?n+", "+e:e},i.prototype["delete"]=function(t){delete this.map[r(t)]},i.prototype.get=function(t){return t=r(t),this.has(t)?this.map[t]:null},i.prototype.has=function(t){return this.map.hasOwnProperty(r(t))},i.prototype.set=function(t,e){this.map[r(t)]=o(e)},i.prototype.forEach=function(t,e){for(var r in this.map)this.map.hasOwnProperty(r)&&t.call(e,this.map[r],r,this)},i.prototype.keys=function(){var t=[];return this.forEach(function(e,r){t.push(r)}),n(t)},i.prototype.values=function(){var t=[];return this.forEach(function(e){t.push(e)}),n(t)},i.prototype.entries=function(){var t=[];return this.forEach(function(e,r){t.push([r,e])}),n(t)},v.iterable&&(i.prototype[Symbol.iterator]=i.prototype.entries);var _=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];p.prototype.clone=function(){return new p(this,{body:this._bodyInit})},c.call(p.prototype),c.call(m.prototype),m.prototype.clone=function(){return new m(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new i(this.headers),url:this.url})},m.error=function(){var t=new m(null,{status:0,statusText:""});return t.type="error",t};var x=[301,302,303,307,308];m.redirect=function(t,e){if(-1===x.indexOf(e))throw new RangeError("Invalid status code");return new m(null,{status:e,headers:{location:t}})},t.DOMException=self.DOMException;try{new t.DOMException}catch(g){t.DOMException=function(t,e){this.message=t,this.name=e;var r=Error(t);this.stack=r.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}w.polyfill=!0,self.fetch=w,self.Headers=i,self.Request=p,self.Response=m,t.Headers=i,t.Request=p,t.Response=m,t.fetch=w,Object.defineProperty(t,"__esModule",{value:!0})});!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):(e=e||self,t(e.AbortControllerShim={}))}(this,function(e){"use strict";function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){for(var n,o=0;o<t.length;o++)n=t[o],n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}function r(e,t,n){return t&&o(e.prototype,t),n&&o(e,n),e}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&c(e,t)}function u(e){return(u=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function c(e,t){return(c=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function l(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function f(e,t){return!t||"object"!=typeof t&&"function"!=typeof t?l(e):t}function a(e){var t=C.get(e);return console.assert(null!=t,"'this' is expected an Event object, but got",e),t}function p(e){return null==e.passiveListener?void(!e.event.cancelable||(e.canceled=!0,"function"==typeof e.event.preventDefault&&e.event.preventDefault())):void("undefined"!=typeof console&&"function"==typeof console.error&&console.error("Unable to preventDefault inside passive event listener invocation.",e.passiveListener))}function s(e,t){C.set(this,{eventTarget:e,event:t,eventPhase:2,currentTarget:e,canceled:!1,stopped:!1,immediateStopped:!1,passiveListener:null,timeStamp:t.timeStamp||Date.now()}),Object.defineProperty(this,"isTrusted",{value:!1,enumerable:!0});for(var n,o=Object.keys(t),r=0;r<o.length;++r)(n=o[r])in this||Object.defineProperty(this,n,b(n))}function b(e){return{get:function(){return a(this).event[e]},set:function(t){a(this).event[e]=t},configurable:!0,enumerable:!0}}function y(e){return{value:function(){var t=a(this).event;return t[e].apply(t,arguments)},configurable:!0,enumerable:!0}}function v(e,t){function n(t,n){e.call(this,t,n)}var o=Object.keys(t);if(0===o.length)return e;n.prototype=Object.create(e.prototype,{constructor:{value:n,configurable:!0,writable:!0}});for(var r,i=0;i<o.length;++i)if(!((r=o[i])in e.prototype)){var u=Object.getOwnPropertyDescriptor(t,r),c="function"==typeof u.value;Object.defineProperty(n.prototype,r,c?y(r):b(r))}return n}function d(e){if(null==e||e===Object.prototype)return s;var t=M.get(e);return null==t&&(t=v(d(Object.getPrototypeOf(e)),e),M.set(e,t)),t}function g(e,t){return new(d(Object.getPrototypeOf(t)))(e,t)}function h(e){return a(e).immediateStopped}function j(e,t){a(e).eventPhase=t}function O(e,t){a(e).currentTarget=t}function m(e,t){a(e).passiveListener=t}function w(e){return null!==e&&"object"===t(e)}function P(e){var t=D.get(e);if(null==t)throw new TypeError("'this' is expected an EventTarget object, but got another value.");return t}function T(e){return{get:function(){for(var t=P(this),n=t.get(e);null!=n;){if(3===n.listenerType)return n.listener;n=n.next}return null},set:function(t){"function"==typeof t||w(t)||(t=null);for(var n=P(this),o=null,r=n.get(e);null!=r;)3===r.listenerType?null===o?null===r.next?n["delete"](e):n.set(e,r.next):o.next=r.next:o=r,r=r.next;if(null!==t){var i={listener:t,listenerType:3,passive:!1,once:!1,next:null};null===o?n.set(e,i):o.next=i}},configurable:!0,enumerable:!0}}function x(e,t){Object.defineProperty(e,"on".concat(t),T(t))}function S(e){function t(){E.call(this)}t.prototype=Object.create(E.prototype,{constructor:{value:t,configurable:!0,writable:!0}});for(var n=0;n<e.length;++n)x(t.prototype,e[n]);return t}function E(){if(this instanceof E)return void D.set(this,new Map);if(1===arguments.length&&Array.isArray(arguments[0]))return S(arguments[0]);if(0<arguments.length){for(var e=Array(arguments.length),t=0;t<arguments.length;++t)e[t]=arguments[t];return S(e)}throw new TypeError("Cannot call a class as a function")}function A(){var e=Object.create(L.prototype);return E.call(e),W.set(e,!1),e}function k(e){!1!==W.get(e)||(W.set(e,!0),e.dispatchEvent({type:"abort"}))}function _(e){var n=B.get(e);if(null==n)throw new TypeError("Expected 'this' to be an 'AbortController' object, but got ".concat(null===e?"null":t(e)));return n}var C=new WeakMap,M=new WeakMap;s.prototype={get type(){return a(this).event.type},get target(){return a(this).eventTarget},get currentTarget(){return a(this).currentTarget},composedPath:function(){var e=a(this).currentTarget;return null==e?[]:[e]},get NONE(){return 0},get CAPTURING_PHASE(){return 1},get AT_TARGET(){return 2},get BUBBLING_PHASE(){return 3},get eventPhase(){return a(this).eventPhase},stopPropagation:function(){var e=a(this);e.stopped=!0,"function"==typeof e.event.stopPropagation&&e.event.stopPropagation()},stopImmediatePropagation:function(){var e=a(this);e.stopped=!0,e.immediateStopped=!0,"function"==typeof e.event.stopImmediatePropagation&&e.event.stopImmediatePropagation()},get bubbles(){return!!a(this).event.bubbles},get cancelable(){return!!a(this).event.cancelable},preventDefault:function(){p(a(this))},get defaultPrevented(){return a(this).canceled},get composed(){return!!a(this).event.composed},get timeStamp(){return a(this).timeStamp},get srcElement(){return a(this).eventTarget},get cancelBubble(){return a(this).stopped},set cancelBubble(e){if(e){var t=a(this);t.stopped=!0,"boolean"==typeof t.event.cancelBubble&&(t.event.cancelBubble=!0)}},get returnValue(){return!a(this).canceled},set returnValue(e){e||p(a(this))},initEvent:function(){}},Object.defineProperty(s.prototype,"constructor",{value:s,configurable:!0,writable:!0}),"undefined"!=typeof window&&"undefined"!=typeof window.Event&&(Object.setPrototypeOf(s.prototype,window.Event.prototype),M.set(window.Event.prototype,s));var D=new WeakMap;E.prototype={addEventListener:function(e,t,n){if(null!=t){if("function"!=typeof t&&!w(t))throw new TypeError("'listener' should be a function or an object.");var o=P(this),r=w(n),i=r?!!n.capture:!!n,u=i?1:2,c={listener:t,listenerType:u,passive:r&&!!n.passive,once:r&&!!n.once,next:null},l=o.get(e);if(void 0===l)return void o.set(e,c);for(var f=null;null!=l;){if(l.listener===t&&l.listenerType===u)return;f=l,l=l.next}f.next=c}},removeEventListener:function(e,t,n){if(null!=t)for(var o=P(this),r=w(n)?!!n.capture:!!n,i=r?1:2,u=null,c=o.get(e);null!=c;){if(c.listener===t&&c.listenerType===i)return void(null===u?null===c.next?o["delete"](e):o.set(e,c.next):u.next=c.next);u=c,c=c.next}},dispatchEvent:function(e){if(null==e||"string"!=typeof e.type)throw new TypeError('"event.type" should be a string.');var t=P(this),n=e.type,o=t.get(n);if(null==o)return!0;for(var r=g(this,e),i=null;null!=o;){if(o.once?null===i?null===o.next?t["delete"](n):t.set(n,o.next):i.next=o.next:i=o,m(r,o.passive?o.listener:null),"function"==typeof o.listener)try{o.listener.call(this,r)}catch(e){"undefined"!=typeof console&&"function"==typeof console.error&&console.error(e)}else 3!==o.listenerType&&"function"==typeof o.listener.handleEvent&&o.listener.handleEvent(r);if(h(r))break;o=o.next}return m(r,null),j(r,0),O(r,null),!r.defaultPrevented}},Object.defineProperty(E.prototype,"constructor",{value:E,configurable:!0,writable:!0}),"undefined"!=typeof window&&"undefined"!=typeof window.EventTarget&&Object.setPrototypeOf(E.prototype,window.EventTarget.prototype);var L=function(e){function o(){throw n(this,o),f(this,u(o).call(this)),new TypeError("AbortSignal cannot be constructed directly")}return i(o,e),r(o,[{key:"aborted",get:function(){var e=W.get(this);if("boolean"!=typeof e)throw new TypeError("Expected 'this' to be an 'AbortSignal' object, but got ".concat(null===this?"null":t(this)));return e}}]),o}(E);x(L.prototype,"abort");var W=new WeakMap;Object.defineProperties(L.prototype,{aborted:{enumerable:!0}}),"function"==typeof Symbol&&"symbol"===t(Symbol.toStringTag)&&Object.defineProperty(L.prototype,Symbol.toStringTag,{configurable:!0,value:"AbortSignal"});var I=function(){function e(){n(this,e),B.set(this,A())}return r(e,[{key:"abort",value:function(){k(_(this))}},{key:"signal",get:function(){return _(this)}}]),e}(),B=new WeakMap;if(Object.defineProperties(I.prototype,{signal:{enumerable:!0},abort:{enumerable:!0}}),"function"==typeof Symbol&&"symbol"===t(Symbol.toStringTag)&&Object.defineProperty(I.prototype,Symbol.toStringTag,{configurable:!0,value:"AbortController"}),e.AbortController=I,e.AbortSignal=L,e["default"]=I,Object.defineProperty(e,"__esModule",{value:!0}),"undefined"==typeof module&&"undefined"==typeof define){var F=Function("return this")();"undefined"==typeof F.AbortController&&(F.AbortController=I,F.AbortSignal=L)}});})('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
lib/OrbitControls.js ADDED
@@ -0,0 +1,1102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ( function () {
2
+
3
+ // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
4
+ //
5
+ // Orbit - left mouse / touch: one-finger move
6
+ // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
7
+ // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
8
+
9
+ const _changeEvent = {
10
+ type: 'change'
11
+ };
12
+ const _startEvent = {
13
+ type: 'start'
14
+ };
15
+ const _endEvent = {
16
+ type: 'end'
17
+ };
18
+
19
+ class OrbitControls extends THREE.EventDispatcher {
20
+
21
+ constructor( object, domElement ) {
22
+
23
+ super();
24
+ if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' );
25
+ if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' );
26
+ this.object = object;
27
+ this.domElement = domElement;
28
+ this.domElement.style.touchAction = 'none'; // disable touch scroll
29
+ // Set to false to disable this control
30
+
31
+ this.enabled = true; // "target" sets the location of focus, where the object orbits around
32
+
33
+ this.target = new THREE.Vector3(); // How far you can dolly in and out ( PerspectiveCamera only )
34
+
35
+ this.minDistance = 0;
36
+ this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only )
37
+
38
+ this.minZoom = 0;
39
+ this.maxZoom = Infinity; // How far you can orbit vertically, upper and lower limits.
40
+ // Range is 0 to Math.PI radians.
41
+
42
+ this.minPolarAngle = 0; // radians
43
+
44
+ this.maxPolarAngle = Math.PI; // radians
45
+ // How far you can orbit horizontally, upper and lower limits.
46
+ // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
47
+
48
+ this.minAzimuthAngle = - Infinity; // radians
49
+
50
+ this.maxAzimuthAngle = Infinity; // radians
51
+ // Set to true to enable damping (inertia)
52
+ // If damping is enabled, you must call controls.update() in your animation loop
53
+
54
+ this.enableDamping = false;
55
+ this.dampingFactor = 0.05; // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
56
+ // Set to false to disable zooming
57
+
58
+ this.enableZoom = true;
59
+ this.zoomSpeed = 1.0; // Set to false to disable rotating
60
+
61
+ this.enableRotate = true;
62
+ this.rotateSpeed = 1.0; // Set to false to disable panning
63
+
64
+ this.enablePan = true;
65
+ this.panSpeed = 1.0;
66
+ this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
67
+
68
+ this.keyPanSpeed = 7.0; // pixels moved per arrow key push
69
+ // Set to true to automatically rotate around the target
70
+ // If auto-rotate is enabled, you must call controls.update() in your animation loop
71
+
72
+ this.autoRotate = false;
73
+ this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60
74
+ // The four arrow keys
75
+
76
+ this.keys = {
77
+ LEFT: 'ArrowLeft',
78
+ UP: 'ArrowUp',
79
+ RIGHT: 'ArrowRight',
80
+ BOTTOM: 'ArrowDown'
81
+ }; // Mouse buttons
82
+
83
+ this.mouseButtons = {
84
+ LEFT: THREE.MOUSE.ROTATE,
85
+ MIDDLE: THREE.MOUSE.DOLLY,
86
+ RIGHT: THREE.MOUSE.PAN
87
+ }; // Touch fingers
88
+
89
+ this.touches = {
90
+ ONE: THREE.TOUCH.ROTATE,
91
+ TWO: THREE.TOUCH.DOLLY_PAN
92
+ }; // for reset
93
+
94
+ this.target0 = this.target.clone();
95
+ this.position0 = this.object.position.clone();
96
+ this.zoom0 = this.object.zoom; // the target DOM element for key events
97
+
98
+ this._domElementKeyEvents = null; //
99
+ // public methods
100
+ //
101
+
102
+ this.getPolarAngle = function () {
103
+
104
+ return spherical.phi;
105
+
106
+ };
107
+
108
+ this.getAzimuthalAngle = function () {
109
+
110
+ return spherical.theta;
111
+
112
+ };
113
+
114
+ this.listenToKeyEvents = function ( domElement ) {
115
+
116
+ domElement.addEventListener( 'keydown', onKeyDown );
117
+ this._domElementKeyEvents = domElement;
118
+
119
+ };
120
+
121
+ this.saveState = function () {
122
+
123
+ scope.target0.copy( scope.target );
124
+ scope.position0.copy( scope.object.position );
125
+ scope.zoom0 = scope.object.zoom;
126
+
127
+ };
128
+
129
+ this.reset = function () {
130
+
131
+ scope.target.copy( scope.target0 );
132
+ scope.object.position.copy( scope.position0 );
133
+ scope.object.zoom = scope.zoom0;
134
+ scope.object.updateProjectionMatrix();
135
+ scope.dispatchEvent( _changeEvent );
136
+ scope.update();
137
+ state = STATE.NONE;
138
+
139
+ }; // this method is exposed, but perhaps it would be better if we can make it private...
140
+
141
+
142
+ this.update = function () {
143
+
144
+ const offset = new THREE.Vector3(); // so camera.up is the orbit axis
145
+
146
+ const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
147
+ const quatInverse = quat.clone().invert();
148
+ const lastPosition = new THREE.Vector3();
149
+ const lastQuaternion = new THREE.Quaternion();
150
+ const twoPI = 2 * Math.PI;
151
+ return function update() {
152
+
153
+ const position = scope.object.position;
154
+ offset.copy( position ).sub( scope.target ); // rotate offset to "y-axis-is-up" space
155
+
156
+ offset.applyQuaternion( quat ); // angle from z-axis around y-axis
157
+
158
+ spherical.setFromVector3( offset );
159
+
160
+ if ( scope.autoRotate && state === STATE.NONE ) {
161
+
162
+ rotateLeft( getAutoRotationAngle() );
163
+
164
+ }
165
+
166
+ if ( scope.enableDamping ) {
167
+
168
+ spherical.theta += sphericalDelta.theta * scope.dampingFactor;
169
+ spherical.phi += sphericalDelta.phi * scope.dampingFactor;
170
+
171
+ } else {
172
+
173
+ spherical.theta += sphericalDelta.theta;
174
+ spherical.phi += sphericalDelta.phi;
175
+
176
+ } // restrict theta to be between desired limits
177
+
178
+
179
+ let min = scope.minAzimuthAngle;
180
+ let max = scope.maxAzimuthAngle;
181
+
182
+ if ( isFinite( min ) && isFinite( max ) ) {
183
+
184
+ if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
185
+ if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
186
+
187
+ if ( min <= max ) {
188
+
189
+ spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
190
+
191
+ } else {
192
+
193
+ spherical.theta = spherical.theta > ( min + max ) / 2 ? Math.max( min, spherical.theta ) : Math.min( max, spherical.theta );
194
+
195
+ }
196
+
197
+ } // restrict phi to be between desired limits
198
+
199
+
200
+ spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
201
+ spherical.makeSafe();
202
+ spherical.radius *= scale; // restrict radius to be between desired limits
203
+
204
+ spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); // move target to panned location
205
+
206
+ if ( scope.enableDamping === true ) {
207
+
208
+ scope.target.addScaledVector( panOffset, scope.dampingFactor );
209
+
210
+ } else {
211
+
212
+ scope.target.add( panOffset );
213
+
214
+ }
215
+
216
+ offset.setFromSpherical( spherical ); // rotate offset back to "camera-up-vector-is-up" space
217
+
218
+ offset.applyQuaternion( quatInverse );
219
+ position.copy( scope.target ).add( offset );
220
+ scope.object.lookAt( scope.target );
221
+
222
+ if ( scope.enableDamping === true ) {
223
+
224
+ sphericalDelta.theta *= 1 - scope.dampingFactor;
225
+ sphericalDelta.phi *= 1 - scope.dampingFactor;
226
+ panOffset.multiplyScalar( 1 - scope.dampingFactor );
227
+
228
+ } else {
229
+
230
+ sphericalDelta.set( 0, 0, 0 );
231
+ panOffset.set( 0, 0, 0 );
232
+
233
+ }
234
+
235
+ scale = 1; // update condition is:
236
+ // min(camera displacement, camera rotation in radians)^2 > EPS
237
+ // using small-angle approximation cos(x/2) = 1 - x^2 / 8
238
+
239
+ if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
240
+
241
+ scope.dispatchEvent( _changeEvent );
242
+ lastPosition.copy( scope.object.position );
243
+ lastQuaternion.copy( scope.object.quaternion );
244
+ zoomChanged = false;
245
+ return true;
246
+
247
+ }
248
+
249
+ return false;
250
+
251
+ };
252
+
253
+ }();
254
+
255
+ this.dispose = function () {
256
+
257
+ scope.domElement.removeEventListener( 'contextmenu', onContextMenu );
258
+ scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
259
+ scope.domElement.removeEventListener( 'pointercancel', onPointerCancel );
260
+ scope.domElement.removeEventListener( 'wheel', onMouseWheel );
261
+ scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
262
+ scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
263
+
264
+ if ( scope._domElementKeyEvents !== null ) {
265
+
266
+ scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
267
+
268
+ } //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
269
+
270
+ }; //
271
+ // internals
272
+ //
273
+
274
+
275
+ const scope = this;
276
+ const STATE = {
277
+ NONE: - 1,
278
+ ROTATE: 0,
279
+ DOLLY: 1,
280
+ PAN: 2,
281
+ TOUCH_ROTATE: 3,
282
+ TOUCH_PAN: 4,
283
+ TOUCH_DOLLY_PAN: 5,
284
+ TOUCH_DOLLY_ROTATE: 6
285
+ };
286
+ let state = STATE.NONE;
287
+ const EPS = 0.000001; // current position in spherical coordinates
288
+
289
+ const spherical = new THREE.Spherical();
290
+ const sphericalDelta = new THREE.Spherical();
291
+ let scale = 1;
292
+ const panOffset = new THREE.Vector3();
293
+ let zoomChanged = false;
294
+ const rotateStart = new THREE.Vector2();
295
+ const rotateEnd = new THREE.Vector2();
296
+ const rotateDelta = new THREE.Vector2();
297
+ const panStart = new THREE.Vector2();
298
+ const panEnd = new THREE.Vector2();
299
+ const panDelta = new THREE.Vector2();
300
+ const dollyStart = new THREE.Vector2();
301
+ const dollyEnd = new THREE.Vector2();
302
+ const dollyDelta = new THREE.Vector2();
303
+ const pointers = [];
304
+ const pointerPositions = {};
305
+
306
+ function getAutoRotationAngle() {
307
+
308
+ return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
309
+
310
+ }
311
+
312
+ function getZoomScale() {
313
+
314
+ return Math.pow( 0.95, scope.zoomSpeed );
315
+
316
+ }
317
+
318
+ function rotateLeft( angle ) {
319
+
320
+ sphericalDelta.theta -= angle;
321
+
322
+ }
323
+
324
+ function rotateUp( angle ) {
325
+
326
+ sphericalDelta.phi -= angle;
327
+
328
+ }
329
+
330
+ const panLeft = function () {
331
+
332
+ const v = new THREE.Vector3();
333
+ return function panLeft( distance, objectMatrix ) {
334
+
335
+ v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
336
+
337
+ v.multiplyScalar( - distance );
338
+ panOffset.add( v );
339
+
340
+ };
341
+
342
+ }();
343
+
344
+ const panUp = function () {
345
+
346
+ const v = new THREE.Vector3();
347
+ return function panUp( distance, objectMatrix ) {
348
+
349
+ if ( scope.screenSpacePanning === true ) {
350
+
351
+ v.setFromMatrixColumn( objectMatrix, 1 );
352
+
353
+ } else {
354
+
355
+ v.setFromMatrixColumn( objectMatrix, 0 );
356
+ v.crossVectors( scope.object.up, v );
357
+
358
+ }
359
+
360
+ v.multiplyScalar( distance );
361
+ panOffset.add( v );
362
+
363
+ };
364
+
365
+ }(); // deltaX and deltaY are in pixels; right and down are positive
366
+
367
+
368
+ const pan = function () {
369
+
370
+ const offset = new THREE.Vector3();
371
+ return function pan( deltaX, deltaY ) {
372
+
373
+ const element = scope.domElement;
374
+
375
+ if ( scope.object.isPerspectiveCamera ) {
376
+
377
+ // perspective
378
+ const position = scope.object.position;
379
+ offset.copy( position ).sub( scope.target );
380
+ let targetDistance = offset.length(); // half of the fov is center to top of screen
381
+
382
+ targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 ); // we use only clientHeight here so aspect ratio does not distort speed
383
+
384
+ panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
385
+ panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
386
+
387
+ } else if ( scope.object.isOrthographicCamera ) {
388
+
389
+ // orthographic
390
+ panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
391
+ panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
392
+
393
+ } else {
394
+
395
+ // camera neither orthographic nor perspective
396
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
397
+ scope.enablePan = false;
398
+
399
+ }
400
+
401
+ };
402
+
403
+ }();
404
+
405
+ function dollyOut( dollyScale ) {
406
+
407
+ if ( scope.object.isPerspectiveCamera ) {
408
+
409
+ scale /= dollyScale;
410
+
411
+ } else if ( scope.object.isOrthographicCamera ) {
412
+
413
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
414
+ scope.object.updateProjectionMatrix();
415
+ zoomChanged = true;
416
+
417
+ } else {
418
+
419
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
420
+ scope.enableZoom = false;
421
+
422
+ }
423
+
424
+ }
425
+
426
+ function dollyIn( dollyScale ) {
427
+
428
+ if ( scope.object.isPerspectiveCamera ) {
429
+
430
+ scale *= dollyScale;
431
+
432
+ } else if ( scope.object.isOrthographicCamera ) {
433
+
434
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
435
+ scope.object.updateProjectionMatrix();
436
+ zoomChanged = true;
437
+
438
+ } else {
439
+
440
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
441
+ scope.enableZoom = false;
442
+
443
+ }
444
+
445
+ } //
446
+ // event callbacks - update the object state
447
+ //
448
+
449
+
450
+ function handleMouseDownRotate( event ) {
451
+
452
+ rotateStart.set( event.clientX, event.clientY );
453
+
454
+ }
455
+
456
+ function handleMouseDownDolly( event ) {
457
+
458
+ dollyStart.set( event.clientX, event.clientY );
459
+
460
+ }
461
+
462
+ function handleMouseDownPan( event ) {
463
+
464
+ panStart.set( event.clientX, event.clientY );
465
+
466
+ }
467
+
468
+ function handleMouseMoveRotate( event ) {
469
+
470
+ rotateEnd.set( event.clientX, event.clientY );
471
+ rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
472
+ const element = scope.domElement;
473
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
474
+
475
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
476
+ rotateStart.copy( rotateEnd );
477
+ scope.update();
478
+
479
+ }
480
+
481
+ function handleMouseMoveDolly( event ) {
482
+
483
+ dollyEnd.set( event.clientX, event.clientY );
484
+ dollyDelta.subVectors( dollyEnd, dollyStart );
485
+
486
+ if ( dollyDelta.y > 0 ) {
487
+
488
+ dollyOut( getZoomScale() );
489
+
490
+ } else if ( dollyDelta.y < 0 ) {
491
+
492
+ dollyIn( getZoomScale() );
493
+
494
+ }
495
+
496
+ dollyStart.copy( dollyEnd );
497
+ scope.update();
498
+
499
+ }
500
+
501
+ function handleMouseMovePan( event ) {
502
+
503
+ panEnd.set( event.clientX, event.clientY );
504
+ panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
505
+ pan( panDelta.x, panDelta.y );
506
+ panStart.copy( panEnd );
507
+ scope.update();
508
+
509
+ }
510
+
511
+ function handleMouseUp( ) { // no-op
512
+ }
513
+
514
+ function handleMouseWheel( event ) {
515
+
516
+ if ( event.deltaY < 0 ) {
517
+
518
+ dollyIn( getZoomScale() );
519
+
520
+ } else if ( event.deltaY > 0 ) {
521
+
522
+ dollyOut( getZoomScale() );
523
+
524
+ }
525
+
526
+ scope.update();
527
+
528
+ }
529
+
530
+ function handleKeyDown( event ) {
531
+
532
+ let needsUpdate = false;
533
+
534
+ switch ( event.code ) {
535
+
536
+ case scope.keys.UP:
537
+ pan( 0, scope.keyPanSpeed );
538
+ needsUpdate = true;
539
+ break;
540
+
541
+ case scope.keys.BOTTOM:
542
+ pan( 0, - scope.keyPanSpeed );
543
+ needsUpdate = true;
544
+ break;
545
+
546
+ case scope.keys.LEFT:
547
+ pan( scope.keyPanSpeed, 0 );
548
+ needsUpdate = true;
549
+ break;
550
+
551
+ case scope.keys.RIGHT:
552
+ pan( - scope.keyPanSpeed, 0 );
553
+ needsUpdate = true;
554
+ break;
555
+
556
+ }
557
+
558
+ if ( needsUpdate ) {
559
+
560
+ // prevent the browser from scrolling on cursor keys
561
+ event.preventDefault();
562
+ scope.update();
563
+
564
+ }
565
+
566
+ }
567
+
568
+ function handleTouchStartRotate() {
569
+
570
+ if ( pointers.length === 1 ) {
571
+
572
+ rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );
573
+
574
+ } else {
575
+
576
+ const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );
577
+ const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );
578
+ rotateStart.set( x, y );
579
+
580
+ }
581
+
582
+ }
583
+
584
+ function handleTouchStartPan() {
585
+
586
+ if ( pointers.length === 1 ) {
587
+
588
+ panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY );
589
+
590
+ } else {
591
+
592
+ const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX );
593
+ const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY );
594
+ panStart.set( x, y );
595
+
596
+ }
597
+
598
+ }
599
+
600
+ function handleTouchStartDolly() {
601
+
602
+ const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX;
603
+ const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY;
604
+ const distance = Math.sqrt( dx * dx + dy * dy );
605
+ dollyStart.set( 0, distance );
606
+
607
+ }
608
+
609
+ function handleTouchStartDollyPan() {
610
+
611
+ if ( scope.enableZoom ) handleTouchStartDolly();
612
+ if ( scope.enablePan ) handleTouchStartPan();
613
+
614
+ }
615
+
616
+ function handleTouchStartDollyRotate() {
617
+
618
+ if ( scope.enableZoom ) handleTouchStartDolly();
619
+ if ( scope.enableRotate ) handleTouchStartRotate();
620
+
621
+ }
622
+
623
+ function handleTouchMoveRotate( event ) {
624
+
625
+ if ( pointers.length == 1 ) {
626
+
627
+ rotateEnd.set( event.pageX, event.pageY );
628
+
629
+ } else {
630
+
631
+ const position = getSecondPointerPosition( event );
632
+ const x = 0.5 * ( event.pageX + position.x );
633
+ const y = 0.5 * ( event.pageY + position.y );
634
+ rotateEnd.set( x, y );
635
+
636
+ }
637
+
638
+ rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
639
+ const element = scope.domElement;
640
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
641
+
642
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
643
+ rotateStart.copy( rotateEnd );
644
+
645
+ }
646
+
647
+ function handleTouchMovePan( event ) {
648
+
649
+ if ( pointers.length === 1 ) {
650
+
651
+ panEnd.set( event.pageX, event.pageY );
652
+
653
+ } else {
654
+
655
+ const position = getSecondPointerPosition( event );
656
+ const x = 0.5 * ( event.pageX + position.x );
657
+ const y = 0.5 * ( event.pageY + position.y );
658
+ panEnd.set( x, y );
659
+
660
+ }
661
+
662
+ panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
663
+ pan( panDelta.x, panDelta.y );
664
+ panStart.copy( panEnd );
665
+
666
+ }
667
+
668
+ function handleTouchMoveDolly( event ) {
669
+
670
+ const position = getSecondPointerPosition( event );
671
+ const dx = event.pageX - position.x;
672
+ const dy = event.pageY - position.y;
673
+ const distance = Math.sqrt( dx * dx + dy * dy );
674
+ dollyEnd.set( 0, distance );
675
+ dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
676
+ dollyOut( dollyDelta.y );
677
+ dollyStart.copy( dollyEnd );
678
+
679
+ }
680
+
681
+ function handleTouchMoveDollyPan( event ) {
682
+
683
+ if ( scope.enableZoom ) handleTouchMoveDolly( event );
684
+ if ( scope.enablePan ) handleTouchMovePan( event );
685
+
686
+ }
687
+
688
+ function handleTouchMoveDollyRotate( event ) {
689
+
690
+ if ( scope.enableZoom ) handleTouchMoveDolly( event );
691
+ if ( scope.enableRotate ) handleTouchMoveRotate( event );
692
+
693
+ }
694
+
695
+ function handleTouchEnd( ) { // no-op
696
+ } //
697
+ // event handlers - FSM: listen for events and reset state
698
+ //
699
+
700
+
701
+ function onPointerDown( event ) {
702
+
703
+ if ( scope.enabled === false ) return;
704
+
705
+ if ( pointers.length === 0 ) {
706
+
707
+ scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove );
708
+ scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp );
709
+
710
+ } //
711
+
712
+
713
+ addPointer( event );
714
+
715
+ if ( event.pointerType === 'touch' ) {
716
+
717
+ onTouchStart( event );
718
+
719
+ } else {
720
+
721
+ onMouseDown( event );
722
+
723
+ }
724
+
725
+ }
726
+
727
+ function onPointerMove( event ) {
728
+
729
+ if ( scope.enabled === false ) return;
730
+
731
+ if ( event.pointerType === 'touch' ) {
732
+
733
+ onTouchMove( event );
734
+
735
+ } else {
736
+
737
+ onMouseMove( event );
738
+
739
+ }
740
+
741
+ }
742
+
743
+ function onPointerUp( event ) {
744
+
745
+ if ( scope.enabled === false ) return;
746
+
747
+ if ( event.pointerType === 'touch' ) {
748
+
749
+ onTouchEnd();
750
+
751
+ } else {
752
+
753
+ onMouseUp( event );
754
+
755
+ }
756
+
757
+ removePointer( event ); //
758
+
759
+ if ( pointers.length === 0 ) {
760
+
761
+ scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
762
+ scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
763
+
764
+ }
765
+
766
+ }
767
+
768
+ function onPointerCancel( event ) {
769
+
770
+ removePointer( event );
771
+
772
+ }
773
+
774
+ function onMouseDown( event ) {
775
+
776
+ let mouseAction;
777
+
778
+ switch ( event.button ) {
779
+
780
+ case 0:
781
+ mouseAction = scope.mouseButtons.LEFT;
782
+ break;
783
+
784
+ case 1:
785
+ mouseAction = scope.mouseButtons.MIDDLE;
786
+ break;
787
+
788
+ case 2:
789
+ mouseAction = scope.mouseButtons.RIGHT;
790
+ break;
791
+
792
+ default:
793
+ mouseAction = - 1;
794
+
795
+ }
796
+
797
+ switch ( mouseAction ) {
798
+
799
+ case THREE.MOUSE.DOLLY:
800
+ if ( scope.enableZoom === false ) return;
801
+ handleMouseDownDolly( event );
802
+ state = STATE.DOLLY;
803
+ break;
804
+
805
+ case THREE.MOUSE.ROTATE:
806
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
807
+
808
+ if ( scope.enablePan === false ) return;
809
+ handleMouseDownPan( event );
810
+ state = STATE.PAN;
811
+
812
+ } else {
813
+
814
+ if ( scope.enableRotate === false ) return;
815
+ handleMouseDownRotate( event );
816
+ state = STATE.ROTATE;
817
+
818
+ }
819
+
820
+ break;
821
+
822
+ case THREE.MOUSE.PAN:
823
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
824
+
825
+ if ( scope.enableRotate === false ) return;
826
+ handleMouseDownRotate( event );
827
+ state = STATE.ROTATE;
828
+
829
+ } else {
830
+
831
+ if ( scope.enablePan === false ) return;
832
+ handleMouseDownPan( event );
833
+ state = STATE.PAN;
834
+
835
+ }
836
+
837
+ break;
838
+
839
+ default:
840
+ state = STATE.NONE;
841
+
842
+ }
843
+
844
+ if ( state !== STATE.NONE ) {
845
+
846
+ scope.dispatchEvent( _startEvent );
847
+
848
+ }
849
+
850
+ }
851
+
852
+ function onMouseMove( event ) {
853
+
854
+ if ( scope.enabled === false ) return;
855
+
856
+ switch ( state ) {
857
+
858
+ case STATE.ROTATE:
859
+ if ( scope.enableRotate === false ) return;
860
+ handleMouseMoveRotate( event );
861
+ break;
862
+
863
+ case STATE.DOLLY:
864
+ if ( scope.enableZoom === false ) return;
865
+ handleMouseMoveDolly( event );
866
+ break;
867
+
868
+ case STATE.PAN:
869
+ if ( scope.enablePan === false ) return;
870
+ handleMouseMovePan( event );
871
+ break;
872
+
873
+ }
874
+
875
+ }
876
+
877
+ function onMouseUp( event ) {
878
+
879
+ handleMouseUp( event );
880
+ scope.dispatchEvent( _endEvent );
881
+ state = STATE.NONE;
882
+
883
+ }
884
+
885
+ function onMouseWheel( event ) {
886
+
887
+ if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE && state !== STATE.ROTATE ) return;
888
+ event.preventDefault();
889
+ scope.dispatchEvent( _startEvent );
890
+ handleMouseWheel( event );
891
+ scope.dispatchEvent( _endEvent );
892
+
893
+ }
894
+
895
+ function onKeyDown( event ) {
896
+
897
+ if ( scope.enabled === false || scope.enablePan === false ) return;
898
+ handleKeyDown( event );
899
+
900
+ }
901
+
902
+ function onTouchStart( event ) {
903
+
904
+ trackPointer( event );
905
+
906
+ switch ( pointers.length ) {
907
+
908
+ case 1:
909
+ switch ( scope.touches.ONE ) {
910
+
911
+ case THREE.TOUCH.ROTATE:
912
+ if ( scope.enableRotate === false ) return;
913
+ handleTouchStartRotate();
914
+ state = STATE.TOUCH_ROTATE;
915
+ break;
916
+
917
+ case THREE.TOUCH.PAN:
918
+ if ( scope.enablePan === false ) return;
919
+ handleTouchStartPan();
920
+ state = STATE.TOUCH_PAN;
921
+ break;
922
+
923
+ default:
924
+ state = STATE.NONE;
925
+
926
+ }
927
+
928
+ break;
929
+
930
+ case 2:
931
+ switch ( scope.touches.TWO ) {
932
+
933
+ case THREE.TOUCH.DOLLY_PAN:
934
+ if ( scope.enableZoom === false && scope.enablePan === false ) return;
935
+ handleTouchStartDollyPan();
936
+ state = STATE.TOUCH_DOLLY_PAN;
937
+ break;
938
+
939
+ case THREE.TOUCH.DOLLY_ROTATE:
940
+ if ( scope.enableZoom === false && scope.enableRotate === false ) return;
941
+ handleTouchStartDollyRotate();
942
+ state = STATE.TOUCH_DOLLY_ROTATE;
943
+ break;
944
+
945
+ default:
946
+ state = STATE.NONE;
947
+
948
+ }
949
+
950
+ break;
951
+
952
+ default:
953
+ state = STATE.NONE;
954
+
955
+ }
956
+
957
+ if ( state !== STATE.NONE ) {
958
+
959
+ scope.dispatchEvent( _startEvent );
960
+
961
+ }
962
+
963
+ }
964
+
965
+ function onTouchMove( event ) {
966
+
967
+ trackPointer( event );
968
+
969
+ switch ( state ) {
970
+
971
+ case STATE.TOUCH_ROTATE:
972
+ if ( scope.enableRotate === false ) return;
973
+ handleTouchMoveRotate( event );
974
+ scope.update();
975
+ break;
976
+
977
+ case STATE.TOUCH_PAN:
978
+ if ( scope.enablePan === false ) return;
979
+ handleTouchMovePan( event );
980
+ scope.update();
981
+ break;
982
+
983
+ case STATE.TOUCH_DOLLY_PAN:
984
+ if ( scope.enableZoom === false && scope.enablePan === false ) return;
985
+ handleTouchMoveDollyPan( event );
986
+ scope.update();
987
+ break;
988
+
989
+ case STATE.TOUCH_DOLLY_ROTATE:
990
+ if ( scope.enableZoom === false && scope.enableRotate === false ) return;
991
+ handleTouchMoveDollyRotate( event );
992
+ scope.update();
993
+ break;
994
+
995
+ default:
996
+ state = STATE.NONE;
997
+
998
+ }
999
+
1000
+ }
1001
+
1002
+ function onTouchEnd( event ) {
1003
+
1004
+ handleTouchEnd( event );
1005
+ scope.dispatchEvent( _endEvent );
1006
+ state = STATE.NONE;
1007
+
1008
+ }
1009
+
1010
+ function onContextMenu( event ) {
1011
+
1012
+ if ( scope.enabled === false ) return;
1013
+ event.preventDefault();
1014
+
1015
+ }
1016
+
1017
+ function addPointer( event ) {
1018
+
1019
+ pointers.push( event );
1020
+
1021
+ }
1022
+
1023
+ function removePointer( event ) {
1024
+
1025
+ delete pointerPositions[ event.pointerId ];
1026
+
1027
+ for ( let i = 0; i < pointers.length; i ++ ) {
1028
+
1029
+ if ( pointers[ i ].pointerId == event.pointerId ) {
1030
+
1031
+ pointers.splice( i, 1 );
1032
+ return;
1033
+
1034
+ }
1035
+
1036
+ }
1037
+
1038
+ }
1039
+
1040
+ function trackPointer( event ) {
1041
+
1042
+ let position = pointerPositions[ event.pointerId ];
1043
+
1044
+ if ( position === undefined ) {
1045
+
1046
+ position = new THREE.Vector2();
1047
+ pointerPositions[ event.pointerId ] = position;
1048
+
1049
+ }
1050
+
1051
+ position.set( event.pageX, event.pageY );
1052
+
1053
+ }
1054
+
1055
+ function getSecondPointerPosition( event ) {
1056
+
1057
+ const pointer = event.pointerId === pointers[ 0 ].pointerId ? pointers[ 1 ] : pointers[ 0 ];
1058
+ return pointerPositions[ pointer.pointerId ];
1059
+
1060
+ } //
1061
+
1062
+
1063
+ scope.domElement.addEventListener( 'contextmenu', onContextMenu );
1064
+ scope.domElement.addEventListener( 'pointerdown', onPointerDown );
1065
+ scope.domElement.addEventListener( 'pointercancel', onPointerCancel );
1066
+ scope.domElement.addEventListener( 'wheel', onMouseWheel, {
1067
+ passive: false
1068
+ } ); // force an update at start
1069
+
1070
+ this.update();
1071
+
1072
+ }
1073
+
1074
+ } // This set of controls performs orbiting, dollying (zooming), and panning.
1075
+ // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
1076
+ // This is very similar to OrbitControls, another set of touch behavior
1077
+ //
1078
+ // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate
1079
+ // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
1080
+ // Pan - left mouse, or arrow keys / touch: one-finger move
1081
+
1082
+
1083
+ class MapControls extends OrbitControls {
1084
+
1085
+ constructor( object, domElement ) {
1086
+
1087
+ super( object, domElement );
1088
+ this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up
1089
+
1090
+ this.mouseButtons.LEFT = THREE.MOUSE.PAN;
1091
+ this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE;
1092
+ this.touches.ONE = THREE.TOUCH.PAN;
1093
+ this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE;
1094
+
1095
+ }
1096
+
1097
+ }
1098
+
1099
+ THREE.MapControls = MapControls;
1100
+ THREE.OrbitControls = OrbitControls;
1101
+
1102
+ } )();
lib/Three.min.js ADDED
The diff for this file is too large to render. See raw diff
 
lib/Three.sprite.js ADDED
@@ -0,0 +1 @@
 
 
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("three"),require("@seregpie/three.text-texture")):"function"==typeof define&&define.amd?define(["three","@seregpie/three.text-texture"],t):((e="undefined"!=typeof globalThis?globalThis:e||self).THREE=e.THREE||{},e.THREE.TextSprite=t(e.THREE,e.THREE.TextTexture))}(this,(function(e,t){"use strict";function i(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var r=i(t);let o=class extends e.Sprite{constructor({fontSize:t=1,...i}={},o=new e.SpriteMaterial({depthWrite:!1})){super(o);let a=new r.default({fontSize:t,...i});this.material.map=a}onBeforeRender(e,t,i){let{material:r}=this,{map:o}=r;if(o.checkFontFace()){let{scale:t}=this,{height:r,width:a}=o;a&&r?(t.setX(a).setY(r),o.setOptimalPixelRatio(this,e,i),o.redraw()):t.setScalar(1)}else o.loadFontFace()}dispose(){let{material:e}=this,{map:t}=e;t.dispose(),e.dispose()}};return["alignment","backgroundColor","color","fontFamily","fontSize","fontStyle","fontVariant","fontWeight","lineGap","padding","strokeColor","strokeWidth","text"].forEach((e=>{Object.defineProperty(o.prototype,e,{get(){return this.material.map[e]},set(t){this.material.map[e]=t}})})),o.prototype.isTextSprite=!0,o}));
lib/Three.texture.js ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ! function(t, e) {
2
+ "object" == typeof exports && "undefined" != typeof module ? module.exports = e(require("three")) : "function" == typeof define && define.amd ? define(["three"], e) : ((t = "undefined" != typeof globalThis ? globalThis : t || self).THREE = t.THREE || {}, t.THREE.TextTexture = e(t.THREE))
3
+ }(this, (function(t) {
4
+ "use strict";
5
+ let e = class extends t.Texture {
6
+ constructor() {
7
+ super(document.createElement("canvas"));
8
+ let e = null,
9
+ i = () => e || (e = this.createDrawable()),
10
+ n = () => i().width,
11
+ o = () => i().height,
12
+ r = !0,
13
+ l = 1,
14
+ a = () => t.MathUtils.ceilPowerOfTwo(n() * l),
15
+ s = () => t.MathUtils.ceilPowerOfTwo(o() * l),
16
+ h = t => {
17
+ if (l !== t) {
18
+ let e = a(),
19
+ i = s();
20
+ l = t;
21
+ let n = a(),
22
+ o = s();
23
+ n === e && o === i || (r = !0)
24
+ }
25
+ },
26
+ c = (() => {
27
+ let e = new t.Vector3,
28
+ i = new t.Vector2,
29
+ r = new t.Vector3,
30
+ l = new t.Vector3,
31
+ a = new t.Vector2;
32
+ return (s, h, c) => {
33
+ if (a.set(n(), o()), a.x && a.y) {
34
+ s.getWorldPosition(r), c.getWorldPosition(e);
35
+ let n = r.distanceTo(e);
36
+ if (c.isPerspectiveCamera && (n *= 2 * Math.tan(t.MathUtils.degToRad(c.fov) / 2)), (c.isPerspectiveCamera || c.isOrthographicCamera) && (n /= c.zoom), n) {
37
+ var f, d;
38
+ s.getWorldScale(l);
39
+ let t = null !== (f = null === (d = h.capabilities) || void 0 === d ? void 0 : d.maxTextureSize) && void 0 !== f ? f : 1 / 0;
40
+ return h.getDrawingBufferSize(i), Math.min(Math.max(l.x / n * (i.x / a.x), l.y / n * (i.y / a.y)), t / a.x, t / a.y)
41
+ }
42
+ }
43
+ return 0
44
+ }
45
+ })();
46
+ Object.defineProperties(this, {
47
+ width: {
48
+ get: n
49
+ },
50
+ height: {
51
+ get: o
52
+ },
53
+ pixelRatio: {
54
+ get: () => l,
55
+ set: h
56
+ },
57
+ needsRedraw: {
58
+ set(t) {
59
+ t && (r = !0, e = null)
60
+ }
61
+ }
62
+ }), Object.assign(this, {
63
+ redraw() {
64
+ if (r) {
65
+ let t = this.image,
66
+ e = t.getContext("2d");
67
+ e.clearRect(0, 0, t.width, t.height), t.width = a(), t.height = s(), t.width && t.height ? (e.save(), e.scale(t.width / n(), t.height / o()), ((...t) => {
68
+ i().draw(...t)
69
+ })(e), e.restore()) : t.width = t.height = 1, r = !1, this.needsUpdate = !0
70
+ }
71
+ },
72
+ setOptimalPixelRatio(...t) {
73
+ h(c(...t))
74
+ }
75
+ })
76
+ }
77
+ };
78
+ e.prototype.isDynamicTexture = !0;
79
+ let i = class extends e {
80
+ constructor({
81
+ alignment: t = "center",
82
+ backgroundColor: e = "rgba(0,0,0,0)",
83
+ color: i = "#fff",
84
+ fontFamily: n = "sans-serif",
85
+ fontSize: o = 16,
86
+ fontStyle: r = "normal",
87
+ fontVariant: l = "normal",
88
+ fontWeight: a = "normal",
89
+ lineGap: s = 1 / 4,
90
+ padding: h = .5,
91
+ strokeColor: c = "#fff",
92
+ strokeWidth: f = 0,
93
+ text: d = ""
94
+ } = {}) {
95
+ super(), Object.entries({
96
+ alignment: t,
97
+ backgroundColor: e,
98
+ color: i,
99
+ fontFamily: n,
100
+ fontSize: o,
101
+ fontStyle: r,
102
+ fontVariant: l,
103
+ fontWeight: a,
104
+ lineGap: s,
105
+ padding: h,
106
+ strokeColor: c,
107
+ strokeWidth: f,
108
+ text: d
109
+ }).forEach((([t, e]) => {
110
+ Object.defineProperty(this, t, {
111
+ get: () => e,
112
+ set(t) {
113
+ e !== t && (e = t, this.needsRedraw = !0)
114
+ }
115
+ })
116
+ }))
117
+ }
118
+ get lines() {
119
+ let {
120
+ text: t
121
+ } = this;
122
+ return t ? t.split("\n") : []
123
+ }
124
+ get font() {
125
+ return function(t, e, i, n, o) {
126
+ let r = document.createElement("span");
127
+ return r.style.font = "1px serif", r.style.fontFamily = t, r.style.fontSize = "".concat(e, "px"), r.style.fontStyle = i, r.style.fontVariant = n, r.style.fontWeight = o, r.style.font
128
+ }(this.fontFamily, this.fontSize, this.fontStyle, this.fontVariant, this.fontWeight)
129
+ }
130
+ checkFontFace() {
131
+ try {
132
+ let {
133
+ font: t
134
+ } = this;
135
+ return document.fonts.check(t)
136
+ } catch (e) {}
137
+ return !0
138
+ }
139
+ async loadFontFace() {
140
+ try {
141
+ let {
142
+ font: t
143
+ } = this;
144
+ await document.fonts.load(t)
145
+ } catch (e) {}
146
+ }
147
+ createDrawable() {
148
+ let {
149
+ alignment: t,
150
+ backgroundColor: e,
151
+ color: i,
152
+ font: n,
153
+ fontSize: o,
154
+ lineGap: r,
155
+ lines: l,
156
+ padding: a,
157
+ strokeColor: s,
158
+ strokeWidth: h
159
+ } = this;
160
+ a *= o, r *= o, h *= o;
161
+ let c = l.length,
162
+ f = o + r,
163
+ d = c ? (() => {
164
+ let t = document.createElement("canvas").getContext("2d");
165
+ return t.font = n, Math.max(...l.map((e => t.measureText(e).width)))
166
+ })() : 0,
167
+ g = a + h / 2,
168
+ u = d + 2 * g;
169
+ return {
170
+ width: u,
171
+ height: (c ? o + f * (c - 1) : 0) + 2 * g,
172
+ draw(r) {
173
+ let a;
174
+ r.fillStyle = e, r.fillRect(0, 0, r.canvas.width, r.canvas.height);
175
+ let c = g + o / 2;
176
+ Object.assign(r, {
177
+ fillStyle: i,
178
+ font: n,
179
+ lineWidth: h,
180
+ miterLimit: 1,
181
+ strokeStyle: s,
182
+ textAlign: (() => {
183
+ switch (t) {
184
+ case "left":
185
+ return a = g, "left";
186
+ case "right":
187
+ return a = u - g, "right"
188
+ }
189
+ return a = u / 2, "center"
190
+ })(),
191
+ textBaseline: "middle"
192
+ }), l.forEach((t => {
193
+
194
+ // r.lineWidth=60
195
+ // r.shadowColor="white"
196
+ // r.shadowBlur=2
197
+ // r.fillStyle = "white"
198
+ // r.fillText(t, a, c);
199
+
200
+ r.lineWidth=5
201
+ r.shadowColor="black"
202
+ r.shadowBlur=0
203
+ r.fillStyle = "black"
204
+ r.fillText(t, a, c);
205
+
206
+ h && r.strokeText(t, a, c), c += f
207
+ }))
208
+ }
209
+ }
210
+ }
211
+ };
212
+ return i.prototype.isTextTexture = !0, i
213
+ }));
lib/TrackballControls.js ADDED
@@ -0,0 +1,778 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ( function () {
2
+
3
+ const _changeEvent = {
4
+ type: 'change'
5
+ };
6
+ const _startEvent = {
7
+ type: 'start'
8
+ };
9
+ const _endEvent = {
10
+ type: 'end'
11
+ };
12
+
13
+ class TrackballControls extends THREE.EventDispatcher {
14
+
15
+ constructor( object, domElement ) {
16
+
17
+ super();
18
+ if ( domElement === undefined ) console.warn( 'THREE.TrackballControls: The second parameter "domElement" is now mandatory.' );
19
+ if ( domElement === document ) console.error( 'THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' );
20
+ const scope = this;
21
+ const STATE = {
22
+ NONE: - 1,
23
+ ROTATE: 0,
24
+ ZOOM: 1,
25
+ PAN: 2,
26
+ TOUCH_ROTATE: 3,
27
+ TOUCH_ZOOM_PAN: 4
28
+ };
29
+ this.object = object;
30
+ this.domElement = domElement;
31
+ this.domElement.style.touchAction = 'none'; // disable touch scroll
32
+ // API
33
+
34
+ this.enabled = true;
35
+ this.screen = {
36
+ left: 0,
37
+ top: 0,
38
+ width: 0,
39
+ height: 0
40
+ };
41
+ this.rotateSpeed = 1.0;
42
+ this.zoomSpeed = 1.2;
43
+ this.panSpeed = 0.3;
44
+ this.noRotate = false;
45
+ this.noZoom = false;
46
+ this.noPan = false;
47
+ this.staticMoving = false;
48
+ this.dynamicDampingFactor = 0.2;
49
+ this.minDistance = 0;
50
+ this.maxDistance = Infinity;
51
+ this.keys = [ 'KeyA',
52
+ /*A*/
53
+ 'KeyS',
54
+ /*S*/
55
+ 'KeyD'
56
+ /*D*/
57
+ ];
58
+ this.mouseButtons = {
59
+ LEFT: THREE.MOUSE.ROTATE,
60
+ MIDDLE: THREE.MOUSE.DOLLY,
61
+ RIGHT: THREE.MOUSE.PAN
62
+ }; // internals
63
+
64
+ this.target = new THREE.Vector3();
65
+ const EPS = 0.000001;
66
+ const lastPosition = new THREE.Vector3();
67
+ let lastZoom = 1;
68
+ let _state = STATE.NONE,
69
+ _keyState = STATE.NONE,
70
+ _touchZoomDistanceStart = 0,
71
+ _touchZoomDistanceEnd = 0,
72
+ _lastAngle = 0;
73
+
74
+ const _eye = new THREE.Vector3(),
75
+ _movePrev = new THREE.Vector2(),
76
+ _moveCurr = new THREE.Vector2(),
77
+ _lastAxis = new THREE.Vector3(),
78
+ _zoomStart = new THREE.Vector2(),
79
+ _zoomEnd = new THREE.Vector2(),
80
+ _panStart = new THREE.Vector2(),
81
+ _panEnd = new THREE.Vector2(),
82
+ _pointers = [],
83
+ _pointerPositions = {}; // for reset
84
+
85
+
86
+ this.target0 = this.target.clone();
87
+ this.position0 = this.object.position.clone();
88
+ this.up0 = this.object.up.clone();
89
+ this.zoom0 = this.object.zoom; // methods
90
+
91
+ this.handleResize = function () {
92
+
93
+ const box = scope.domElement.getBoundingClientRect(); // adjustments come from similar code in the jquery offset() function
94
+
95
+ const d = scope.domElement.ownerDocument.documentElement;
96
+ scope.screen.left = box.left + window.pageXOffset - d.clientLeft;
97
+ scope.screen.top = box.top + window.pageYOffset - d.clientTop;
98
+ scope.screen.width = box.width;
99
+ scope.screen.height = box.height;
100
+
101
+ };
102
+
103
+ const getMouseOnScreen = function () {
104
+
105
+ const vector = new THREE.Vector2();
106
+ return function getMouseOnScreen( pageX, pageY ) {
107
+
108
+ vector.set( ( pageX - scope.screen.left ) / scope.screen.width, ( pageY - scope.screen.top ) / scope.screen.height );
109
+ return vector;
110
+
111
+ };
112
+
113
+ }();
114
+
115
+ const getMouseOnCircle = function () {
116
+
117
+ const vector = new THREE.Vector2();
118
+ return function getMouseOnCircle( pageX, pageY ) {
119
+
120
+ vector.set( ( pageX - scope.screen.width * 0.5 - scope.screen.left ) / ( scope.screen.width * 0.5 ), ( scope.screen.height + 2 * ( scope.screen.top - pageY ) ) / scope.screen.width // screen.width intentional
121
+ );
122
+ return vector;
123
+
124
+ };
125
+
126
+ }();
127
+
128
+ this.rotateCamera = function () {
129
+
130
+ const axis = new THREE.Vector3(),
131
+ quaternion = new THREE.Quaternion(),
132
+ eyeDirection = new THREE.Vector3(),
133
+ objectUpDirection = new THREE.Vector3(),
134
+ objectSidewaysDirection = new THREE.Vector3(),
135
+ moveDirection = new THREE.Vector3();
136
+ return function rotateCamera() {
137
+
138
+ moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 );
139
+ let angle = moveDirection.length();
140
+
141
+ if ( angle ) {
142
+
143
+ _eye.copy( scope.object.position ).sub( scope.target );
144
+
145
+ eyeDirection.copy( _eye ).normalize();
146
+ objectUpDirection.copy( scope.object.up ).normalize();
147
+ objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize();
148
+ objectUpDirection.setLength( _moveCurr.y - _movePrev.y );
149
+ objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x );
150
+ moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) );
151
+ axis.crossVectors( moveDirection, _eye ).normalize();
152
+ angle *= scope.rotateSpeed;
153
+ quaternion.setFromAxisAngle( axis, angle );
154
+
155
+ _eye.applyQuaternion( quaternion );
156
+
157
+ scope.object.up.applyQuaternion( quaternion );
158
+
159
+ _lastAxis.copy( axis );
160
+
161
+ _lastAngle = angle;
162
+
163
+ } else if ( ! scope.staticMoving && _lastAngle ) {
164
+
165
+ _lastAngle *= Math.sqrt( 1.0 - scope.dynamicDampingFactor );
166
+
167
+ _eye.copy( scope.object.position ).sub( scope.target );
168
+
169
+ quaternion.setFromAxisAngle( _lastAxis, _lastAngle );
170
+
171
+ _eye.applyQuaternion( quaternion );
172
+
173
+ scope.object.up.applyQuaternion( quaternion );
174
+
175
+ }
176
+
177
+ _movePrev.copy( _moveCurr );
178
+
179
+ };
180
+
181
+ }();
182
+
183
+ this.zoomCamera = function () {
184
+
185
+ let factor;
186
+
187
+ if ( _state === STATE.TOUCH_ZOOM_PAN ) {
188
+
189
+ factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
190
+ _touchZoomDistanceStart = _touchZoomDistanceEnd;
191
+
192
+ if ( scope.object.isPerspectiveCamera ) {
193
+
194
+ _eye.multiplyScalar( factor );
195
+
196
+ } else if ( scope.object.isOrthographicCamera ) {
197
+
198
+ scope.object.zoom *= factor;
199
+ scope.object.updateProjectionMatrix();
200
+
201
+ } else {
202
+
203
+ console.warn( 'THREE.TrackballControls: Unsupported camera type' );
204
+
205
+ }
206
+
207
+ } else {
208
+
209
+ factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * scope.zoomSpeed;
210
+
211
+ if ( factor !== 1.0 && factor > 0.0 ) {
212
+
213
+ if ( scope.object.isPerspectiveCamera ) {
214
+
215
+ _eye.multiplyScalar( factor );
216
+
217
+ } else if ( scope.object.isOrthographicCamera ) {
218
+
219
+ scope.object.zoom /= factor;
220
+ scope.object.updateProjectionMatrix();
221
+
222
+ } else {
223
+
224
+ console.warn( 'THREE.TrackballControls: Unsupported camera type' );
225
+
226
+ }
227
+
228
+ }
229
+
230
+ if ( scope.staticMoving ) {
231
+
232
+ _zoomStart.copy( _zoomEnd );
233
+
234
+ } else {
235
+
236
+ _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
237
+
238
+ }
239
+
240
+ }
241
+
242
+ };
243
+
244
+ this.panCamera = function () {
245
+
246
+ const mouseChange = new THREE.Vector2(),
247
+ objectUp = new THREE.Vector3(),
248
+ pan = new THREE.Vector3();
249
+ return function panCamera() {
250
+
251
+ mouseChange.copy( _panEnd ).sub( _panStart );
252
+
253
+ if ( mouseChange.lengthSq() ) {
254
+
255
+ if ( scope.object.isOrthographicCamera ) {
256
+
257
+ const scale_x = ( scope.object.right - scope.object.left ) / scope.object.zoom / scope.domElement.clientWidth;
258
+ const scale_y = ( scope.object.top - scope.object.bottom ) / scope.object.zoom / scope.domElement.clientWidth;
259
+ mouseChange.x *= scale_x;
260
+ mouseChange.y *= scale_y;
261
+
262
+ }
263
+
264
+ mouseChange.multiplyScalar( _eye.length() * scope.panSpeed );
265
+ pan.copy( _eye ).cross( scope.object.up ).setLength( mouseChange.x );
266
+ pan.add( objectUp.copy( scope.object.up ).setLength( mouseChange.y ) );
267
+ scope.object.position.add( pan );
268
+ scope.target.add( pan );
269
+
270
+ if ( scope.staticMoving ) {
271
+
272
+ _panStart.copy( _panEnd );
273
+
274
+ } else {
275
+
276
+ _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( scope.dynamicDampingFactor ) );
277
+
278
+ }
279
+
280
+ }
281
+
282
+ };
283
+
284
+ }();
285
+
286
+ this.checkDistances = function () {
287
+
288
+ if ( ! scope.noZoom || ! scope.noPan ) {
289
+
290
+ if ( _eye.lengthSq() > scope.maxDistance * scope.maxDistance ) {
291
+
292
+ scope.object.position.addVectors( scope.target, _eye.setLength( scope.maxDistance ) );
293
+
294
+ _zoomStart.copy( _zoomEnd );
295
+
296
+ }
297
+
298
+ if ( _eye.lengthSq() < scope.minDistance * scope.minDistance ) {
299
+
300
+ scope.object.position.addVectors( scope.target, _eye.setLength( scope.minDistance ) );
301
+
302
+ _zoomStart.copy( _zoomEnd );
303
+
304
+ }
305
+
306
+ }
307
+
308
+ };
309
+
310
+ this.update = function () {
311
+
312
+ _eye.subVectors( scope.object.position, scope.target );
313
+
314
+ if ( ! scope.noRotate ) {
315
+
316
+ scope.rotateCamera();
317
+
318
+ }
319
+
320
+ if ( ! scope.noZoom ) {
321
+
322
+ scope.zoomCamera();
323
+
324
+ }
325
+
326
+ if ( ! scope.noPan ) {
327
+
328
+ scope.panCamera();
329
+
330
+ }
331
+
332
+ scope.object.position.addVectors( scope.target, _eye );
333
+
334
+ if ( scope.object.isPerspectiveCamera ) {
335
+
336
+ scope.checkDistances();
337
+ scope.object.lookAt( scope.target );
338
+
339
+ if ( lastPosition.distanceToSquared( scope.object.position ) > EPS ) {
340
+
341
+ scope.dispatchEvent( _changeEvent );
342
+ lastPosition.copy( scope.object.position );
343
+
344
+ }
345
+
346
+ } else if ( scope.object.isOrthographicCamera ) {
347
+
348
+ scope.object.lookAt( scope.target );
349
+
350
+ if ( lastPosition.distanceToSquared( scope.object.position ) > EPS || lastZoom !== scope.object.zoom ) {
351
+
352
+ scope.dispatchEvent( _changeEvent );
353
+ lastPosition.copy( scope.object.position );
354
+ lastZoom = scope.object.zoom;
355
+
356
+ }
357
+
358
+ } else {
359
+
360
+ console.warn( 'THREE.TrackballControls: Unsupported camera type' );
361
+
362
+ }
363
+
364
+ };
365
+
366
+ this.reset = function () {
367
+
368
+ _state = STATE.NONE;
369
+ _keyState = STATE.NONE;
370
+ scope.target.copy( scope.target0 );
371
+ scope.object.position.copy( scope.position0 );
372
+ scope.object.up.copy( scope.up0 );
373
+ scope.object.zoom = scope.zoom0;
374
+ scope.object.updateProjectionMatrix();
375
+
376
+ _eye.subVectors( scope.object.position, scope.target );
377
+
378
+ scope.object.lookAt( scope.target );
379
+ scope.dispatchEvent( _changeEvent );
380
+ lastPosition.copy( scope.object.position );
381
+ lastZoom = scope.object.zoom;
382
+
383
+ }; // listeners
384
+
385
+
386
+ function onPointerDown( event ) {
387
+
388
+ if ( scope.enabled === false ) return;
389
+
390
+ if ( _pointers.length === 0 ) {
391
+
392
+ scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove );
393
+ scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp );
394
+
395
+ } //
396
+
397
+
398
+ addPointer( event );
399
+
400
+ if ( event.pointerType === 'touch' ) {
401
+
402
+ onTouchStart( event );
403
+
404
+ } else {
405
+
406
+ onMouseDown( event );
407
+
408
+ }
409
+
410
+ }
411
+
412
+ function onPointerMove( event ) {
413
+
414
+ if ( scope.enabled === false ) return;
415
+
416
+ if ( event.pointerType === 'touch' ) {
417
+
418
+ onTouchMove( event );
419
+
420
+ } else {
421
+
422
+ onMouseMove( event );
423
+
424
+ }
425
+
426
+ }
427
+
428
+ function onPointerUp( event ) {
429
+
430
+ if ( scope.enabled === false ) return;
431
+
432
+ if ( event.pointerType === 'touch' ) {
433
+
434
+ onTouchEnd( event );
435
+
436
+ } else {
437
+
438
+ onMouseUp();
439
+
440
+ } //
441
+
442
+
443
+ removePointer( event );
444
+
445
+ if ( _pointers.length === 0 ) {
446
+
447
+ scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
448
+ scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
449
+
450
+ }
451
+
452
+ }
453
+
454
+ function onPointerCancel( event ) {
455
+
456
+ removePointer( event );
457
+
458
+ }
459
+
460
+ function keydown( event ) {
461
+
462
+ if ( scope.enabled === false ) return;
463
+ window.removeEventListener( 'keydown', keydown );
464
+
465
+ if ( _keyState !== STATE.NONE ) {
466
+
467
+ return;
468
+
469
+ } else if ( event.code === scope.keys[ STATE.ROTATE ] && ! scope.noRotate ) {
470
+
471
+ _keyState = STATE.ROTATE;
472
+
473
+ } else if ( event.code === scope.keys[ STATE.ZOOM ] && ! scope.noZoom ) {
474
+
475
+ _keyState = STATE.ZOOM;
476
+
477
+ } else if ( event.code === scope.keys[ STATE.PAN ] && ! scope.noPan ) {
478
+
479
+ _keyState = STATE.PAN;
480
+
481
+ }
482
+
483
+ }
484
+
485
+ function keyup() {
486
+
487
+ if ( scope.enabled === false ) return;
488
+ _keyState = STATE.NONE;
489
+ window.addEventListener( 'keydown', keydown );
490
+
491
+ }
492
+
493
+ function onMouseDown( event ) {
494
+
495
+ if ( _state === STATE.NONE ) {
496
+
497
+ switch ( event.button ) {
498
+
499
+ case scope.mouseButtons.LEFT:
500
+ _state = STATE.ROTATE;
501
+ break;
502
+
503
+ case scope.mouseButtons.MIDDLE:
504
+ _state = STATE.ZOOM;
505
+ break;
506
+
507
+ case scope.mouseButtons.RIGHT:
508
+ _state = STATE.PAN;
509
+ break;
510
+
511
+ default:
512
+ _state = STATE.NONE;
513
+
514
+ }
515
+
516
+ }
517
+
518
+ const state = _keyState !== STATE.NONE ? _keyState : _state;
519
+
520
+ if ( state === STATE.ROTATE && ! scope.noRotate ) {
521
+
522
+ _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
523
+
524
+ _movePrev.copy( _moveCurr );
525
+
526
+ } else if ( state === STATE.ZOOM && ! scope.noZoom ) {
527
+
528
+ _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
529
+
530
+ _zoomEnd.copy( _zoomStart );
531
+
532
+ } else if ( state === STATE.PAN && ! scope.noPan ) {
533
+
534
+ _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
535
+
536
+ _panEnd.copy( _panStart );
537
+
538
+ }
539
+
540
+ scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove );
541
+ scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp );
542
+ scope.dispatchEvent( _startEvent );
543
+
544
+ }
545
+
546
+ function onMouseMove( event ) {
547
+
548
+ const state = _keyState !== STATE.NONE ? _keyState : _state;
549
+
550
+ if ( state === STATE.ROTATE && ! scope.noRotate ) {
551
+
552
+ _movePrev.copy( _moveCurr );
553
+
554
+ _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
555
+
556
+ } else if ( state === STATE.ZOOM && ! scope.noZoom ) {
557
+
558
+ _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
559
+
560
+ } else if ( state === STATE.PAN && ! scope.noPan ) {
561
+
562
+ _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
563
+
564
+ }
565
+
566
+ }
567
+
568
+ function onMouseUp() {
569
+
570
+ _state = STATE.NONE;
571
+ scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
572
+ scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
573
+ scope.dispatchEvent( _endEvent );
574
+
575
+ }
576
+
577
+ function onMouseWheel( event ) {
578
+
579
+ if ( scope.enabled === false ) return;
580
+ if ( scope.noZoom === true ) return;
581
+ event.preventDefault();
582
+
583
+ switch ( event.deltaMode ) {
584
+
585
+ case 2:
586
+ // Zoom in pages
587
+ _zoomStart.y -= event.deltaY * 0.025;
588
+ break;
589
+
590
+ case 1:
591
+ // Zoom in lines
592
+ _zoomStart.y -= event.deltaY * 0.01;
593
+ break;
594
+
595
+ default:
596
+ // undefined, 0, assume pixels
597
+ _zoomStart.y -= event.deltaY * 0.00025;
598
+ break;
599
+
600
+ }
601
+
602
+ scope.dispatchEvent( _startEvent );
603
+ scope.dispatchEvent( _endEvent );
604
+
605
+ }
606
+
607
+ function onTouchStart( event ) {
608
+
609
+ trackPointer( event );
610
+
611
+ switch ( _pointers.length ) {
612
+
613
+ case 1:
614
+ _state = STATE.TOUCH_ROTATE;
615
+
616
+ _moveCurr.copy( getMouseOnCircle( _pointers[ 0 ].pageX, _pointers[ 0 ].pageY ) );
617
+
618
+ _movePrev.copy( _moveCurr );
619
+
620
+ break;
621
+
622
+ default:
623
+ // 2 or more
624
+ _state = STATE.TOUCH_ZOOM_PAN;
625
+ const dx = _pointers[ 0 ].pageX - _pointers[ 1 ].pageX;
626
+ const dy = _pointers[ 0 ].pageY - _pointers[ 1 ].pageY;
627
+ _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
628
+ const x = ( _pointers[ 0 ].pageX + _pointers[ 1 ].pageX ) / 2;
629
+ const y = ( _pointers[ 0 ].pageY + _pointers[ 1 ].pageY ) / 2;
630
+
631
+ _panStart.copy( getMouseOnScreen( x, y ) );
632
+
633
+ _panEnd.copy( _panStart );
634
+
635
+ break;
636
+
637
+ }
638
+
639
+ scope.dispatchEvent( _startEvent );
640
+
641
+ }
642
+
643
+ function onTouchMove( event ) {
644
+
645
+ trackPointer( event );
646
+
647
+ switch ( _pointers.length ) {
648
+
649
+ case 1:
650
+ _movePrev.copy( _moveCurr );
651
+
652
+ _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
653
+
654
+ break;
655
+
656
+ default:
657
+ // 2 or more
658
+ const position = getSecondPointerPosition( event );
659
+ const dx = event.pageX - position.x;
660
+ const dy = event.pageY - position.y;
661
+ _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
662
+ const x = ( event.pageX + position.x ) / 2;
663
+ const y = ( event.pageY + position.y ) / 2;
664
+
665
+ _panEnd.copy( getMouseOnScreen( x, y ) );
666
+
667
+ break;
668
+
669
+ }
670
+
671
+ }
672
+
673
+ function onTouchEnd( event ) {
674
+
675
+ switch ( _pointers.length ) {
676
+
677
+ case 0:
678
+ _state = STATE.NONE;
679
+ break;
680
+
681
+ case 1:
682
+ _state = STATE.TOUCH_ROTATE;
683
+
684
+ _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
685
+
686
+ _movePrev.copy( _moveCurr );
687
+
688
+ break;
689
+
690
+ }
691
+
692
+ scope.dispatchEvent( _endEvent );
693
+
694
+ }
695
+
696
+ function contextmenu( event ) {
697
+
698
+ if ( scope.enabled === false ) return;
699
+ event.preventDefault();
700
+
701
+ }
702
+
703
+ function addPointer( event ) {
704
+
705
+ _pointers.push( event );
706
+
707
+ }
708
+
709
+ function removePointer( event ) {
710
+
711
+ delete _pointerPositions[ event.pointerId ];
712
+
713
+ for ( let i = 0; i < _pointers.length; i ++ ) {
714
+
715
+ if ( _pointers[ i ].pointerId == event.pointerId ) {
716
+
717
+ _pointers.splice( i, 1 );
718
+
719
+ return;
720
+
721
+ }
722
+
723
+ }
724
+
725
+ }
726
+
727
+ function trackPointer( event ) {
728
+
729
+ let position = _pointerPositions[ event.pointerId ];
730
+
731
+ if ( position === undefined ) {
732
+
733
+ position = new THREE.Vector2();
734
+ _pointerPositions[ event.pointerId ] = position;
735
+
736
+ }
737
+
738
+ position.set( event.pageX, event.pageY );
739
+
740
+ }
741
+
742
+ function getSecondPointerPosition( event ) {
743
+
744
+ const pointer = event.pointerId === _pointers[ 0 ].pointerId ? _pointers[ 1 ] : _pointers[ 0 ];
745
+ return _pointerPositions[ pointer.pointerId ];
746
+
747
+ }
748
+
749
+ this.dispose = function () {
750
+
751
+ scope.domElement.removeEventListener( 'contextmenu', contextmenu );
752
+ scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
753
+ scope.domElement.removeEventListener( 'pointercancel', onPointerCancel );
754
+ scope.domElement.removeEventListener( 'wheel', onMouseWheel );
755
+ window.removeEventListener( 'keydown', keydown );
756
+ window.removeEventListener( 'keyup', keyup );
757
+
758
+ };
759
+
760
+ this.domElement.addEventListener( 'contextmenu', contextmenu );
761
+ this.domElement.addEventListener( 'pointerdown', onPointerDown );
762
+ this.domElement.addEventListener( 'pointercancel', onPointerCancel );
763
+ this.domElement.addEventListener( 'wheel', onMouseWheel, {
764
+ passive: false
765
+ } );
766
+ window.addEventListener( 'keydown', keydown );
767
+ window.addEventListener( 'keyup', keyup );
768
+ this.handleResize(); // force an update at start
769
+
770
+ this.update();
771
+
772
+ }
773
+
774
+ }
775
+
776
+ THREE.TrackballControls = TrackballControls;
777
+
778
+ } )();
lib/ffmpeg_normalize/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from ._ffmpeg_normalize import FFmpegNormalize
2
+ from ._media_file import MediaFile
3
+ from ._version import __version__
4
+
5
+ __all__ = ["FFmpegNormalize", "MediaFile", "__version__"]
lib/ffmpeg_normalize/__main__.py ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import textwrap
3
+ import logging
4
+ import os
5
+ import shlex
6
+ import json
7
+
8
+ try:
9
+ from json.decoder import JSONDecodeError
10
+ except ImportError:
11
+ JSONDecodeError = ValueError
12
+
13
+ from ._version import __version__
14
+ from ._ffmpeg_normalize import FFmpegNormalize, NORMALIZATION_TYPES
15
+ from ._errors import FFmpegNormalizeError
16
+ from ._logger import setup_custom_logger
17
+
18
+ logger = setup_custom_logger("ffmpeg_normalize")
19
+
20
+
21
+ def create_parser():
22
+ parser = argparse.ArgumentParser(
23
+ prog="ffmpeg-normalize",
24
+ description=textwrap.dedent(
25
+ """\
26
+ ffmpeg-normalize v{} -- command line tool for normalizing audio files
27
+ """.format(
28
+ __version__
29
+ )
30
+ ),
31
+ usage=textwrap.dedent(
32
+ """\
33
+ ffmpeg-normalize input [input ...]
34
+ [-h]
35
+ [-o OUTPUT [OUTPUT ...]] [-of OUTPUT_FOLDER]
36
+ [-f] [-d] [-v] [-q] [-n] [-pr]
37
+ [--version]
38
+ [-nt {ebu,rms,peak}] [-t TARGET_LEVEL] [-p]
39
+ [-lrt LOUDNESS_RANGE_TARGET] [-tp TRUE_PEAK] [--offset OFFSET] [--dual-mono]
40
+ [-c:a AUDIO_CODEC] [-b:a AUDIO_BITRATE] [-ar SAMPLE_RATE] [-koa]
41
+ [-prf PRE_FILTER] [-pof POST_FILTER]
42
+ [-vn] [-c:v VIDEO_CODEC]
43
+ [-sn] [-mn] [-cn]
44
+ [-ei EXTRA_INPUT_OPTIONS] [-e EXTRA_OUTPUT_OPTIONS]
45
+ [-ofmt OUTPUT_FORMAT]
46
+ [-ext EXTENSION]
47
+ """
48
+ ),
49
+ formatter_class=argparse.RawTextHelpFormatter,
50
+ epilog=textwrap.dedent(
51
+ """\
52
+ The program additionally respects environment variables:
53
+
54
+ - `TMP` / `TEMP` / `TMPDIR`
55
+ Sets the path to the temporary directory in which files are
56
+ stored before being moved to the final output directory.
57
+ Note: You need to use full paths.
58
+
59
+ - `FFMPEG_PATH`
60
+ Sets the full path to an `ffmpeg` executable other than
61
+ the system default.
62
+
63
+
64
+ Author: Werner Robitza
65
+ License: MIT
66
+ Homepage / Issues: https://github.com/slhck/ffmpeg-normalize
67
+ """
68
+ ),
69
+ )
70
+
71
+ group_io = parser.add_argument_group("File Input/output")
72
+ group_io.add_argument("input", nargs="+", help="Input media file(s)")
73
+ group_io.add_argument(
74
+ "-o",
75
+ "--output",
76
+ nargs="+",
77
+ help=textwrap.dedent(
78
+ """\
79
+ Output file names. Will be applied per input file.
80
+
81
+ If no output file name is specified for an input file, the output files
82
+ will be written to the default output folder with the name `<input>.<ext>`,
83
+ where `<ext>` is the output extension (see `-ext` option).
84
+
85
+ Example: ffmpeg-normalize 1.wav 2.wav -o 1n.wav 2n.wav
86
+ """
87
+ ),
88
+ )
89
+ group_io.add_argument(
90
+ "-of",
91
+ "--output-folder",
92
+ type=str,
93
+ help=textwrap.dedent(
94
+ """\
95
+ Output folder (default: `normalized`)
96
+
97
+ This folder will be used for input files that have no explicit output
98
+ name specified.
99
+ """
100
+ ),
101
+ default="normalized",
102
+ )
103
+
104
+ group_general = parser.add_argument_group("General Options")
105
+ group_general.add_argument(
106
+ "-f", "--force", action="store_true", help="Force overwrite existing files"
107
+ )
108
+ group_general.add_argument(
109
+ "-d", "--debug", action="store_true", help="Print debugging output"
110
+ )
111
+ group_general.add_argument(
112
+ "-v", "--verbose", action="store_true", help="Print verbose output"
113
+ )
114
+ group_general.add_argument(
115
+ "-q", "--quiet", action="store_true", help="Only print errors in output"
116
+ )
117
+ group_general.add_argument(
118
+ "-n",
119
+ "--dry-run",
120
+ action="store_true",
121
+ help="Do not run normalization, only print what would be done",
122
+ )
123
+ group_general.add_argument(
124
+ "-pr",
125
+ "--progress",
126
+ action="store_true",
127
+ help="Show progress bar for files and streams",
128
+ )
129
+ group_general.add_argument(
130
+ "--version",
131
+ action="version",
132
+ version=f"%(prog)s v{__version__}",
133
+ help="Print version and exit",
134
+ )
135
+
136
+ group_normalization = parser.add_argument_group("Normalization")
137
+ group_normalization.add_argument(
138
+ "-nt",
139
+ "--normalization-type",
140
+ type=str,
141
+ choices=NORMALIZATION_TYPES,
142
+ help=textwrap.dedent(
143
+ """\
144
+ Normalization type (default: `ebu`).
145
+
146
+ EBU normalization performs two passes and normalizes according to EBU
147
+ R128.
148
+
149
+ RMS-based normalization brings the input file to the specified RMS
150
+ level.
151
+
152
+ Peak normalization brings the signal to the specified peak level.
153
+ """
154
+ ),
155
+ default="ebu",
156
+ )
157
+ group_normalization.add_argument(
158
+ "-t",
159
+ "--target-level",
160
+ type=float,
161
+ help=textwrap.dedent(
162
+ """\
163
+ Normalization target level in dB/LUFS (default: -23).
164
+
165
+ For EBU normalization, it corresponds to Integrated Loudness Target
166
+ in LUFS. The range is -70.0 - -5.0.
167
+
168
+ Otherwise, the range is -99 to 0.
169
+ """
170
+ ),
171
+ default=-23.0,
172
+ )
173
+ group_normalization.add_argument(
174
+ "-p",
175
+ "--print-stats",
176
+ action="store_true",
177
+ help="Print first pass loudness statistics formatted as JSON to stdout",
178
+ )
179
+
180
+ # group_normalization.add_argument(
181
+ # '--threshold',
182
+ # type=float,
183
+ # help=textwrap.dedent("""\
184
+ # Threshold below which normalization should not be run.
185
+
186
+ # If the stream falls within the threshold, it will simply be copied.
187
+ # """),
188
+ # default=0.5
189
+ # )
190
+
191
+ group_ebu = parser.add_argument_group("EBU R128 Normalization")
192
+ group_ebu.add_argument(
193
+ "-lrt",
194
+ "--loudness-range-target",
195
+ type=float,
196
+ help=textwrap.dedent(
197
+ """\
198
+ EBU Loudness Range Target in LUFS (default: 7.0).
199
+ Range is 1.0 - 20.0.
200
+ """
201
+ ),
202
+ default=7.0,
203
+ )
204
+
205
+ group_ebu.add_argument(
206
+ "-tp",
207
+ "--true-peak",
208
+ type=float,
209
+ help=textwrap.dedent(
210
+ """\
211
+ EBU Maximum True Peak in dBTP (default: -2.0).
212
+ Range is -9.0 - +0.0.
213
+ """
214
+ ),
215
+ default=-2.0,
216
+ )
217
+
218
+ group_ebu.add_argument(
219
+ "--offset",
220
+ type=float,
221
+ help=textwrap.dedent(
222
+ """\
223
+ EBU Offset Gain (default: 0.0).
224
+ The gain is applied before the true-peak limiter in the first pass only.
225
+ The offset for the second pass will be automatically determined based on the first pass statistics.
226
+ Range is -99.0 - +99.0.
227
+ """
228
+ ),
229
+ default=0.0,
230
+ )
231
+
232
+ group_ebu.add_argument(
233
+ "--dual-mono",
234
+ action="store_true",
235
+ help=textwrap.dedent(
236
+ """\
237
+ Treat mono input files as "dual-mono".
238
+
239
+ If a mono file is intended for playback on a stereo system, its EBU R128
240
+ measurement will be perceptually incorrect. If set, this option will
241
+ compensate for this effect. Multi-channel input files are not affected
242
+ by this option.
243
+ """
244
+ ),
245
+ )
246
+
247
+ group_acodec = parser.add_argument_group("Audio Encoding")
248
+ group_acodec.add_argument(
249
+ "-c:a",
250
+ "--audio-codec",
251
+ type=str,
252
+ help=textwrap.dedent(
253
+ """\
254
+ Audio codec to use for output files.
255
+ See `ffmpeg -encoders` for a list.
256
+
257
+ Will use PCM audio with input stream bit depth by default.
258
+ """
259
+ ),
260
+ )
261
+ group_acodec.add_argument(
262
+ "-b:a",
263
+ "--audio-bitrate",
264
+ type=str,
265
+ help=textwrap.dedent(
266
+ """\
267
+ Audio bitrate in bits/s, or with K suffix.
268
+
269
+ If not specified, will use codec default.
270
+ """
271
+ ),
272
+ )
273
+ group_acodec.add_argument(
274
+ "-ar",
275
+ "--sample-rate",
276
+ type=str,
277
+ help=textwrap.dedent(
278
+ """\
279
+ Audio sample rate to use for output files in Hz.
280
+
281
+ Will use input sample rate by default, except for EBU normalization,
282
+ which will change the input sample rate to 192 kHz.
283
+ """
284
+ ),
285
+ )
286
+ group_acodec.add_argument(
287
+ "-koa",
288
+ "--keep-original-audio",
289
+ action="store_true",
290
+ help="Copy original, non-normalized audio streams to output file",
291
+ )
292
+ group_acodec.add_argument(
293
+ "-prf",
294
+ "--pre-filter",
295
+ type=str,
296
+ help=textwrap.dedent(
297
+ """\
298
+ Add an audio filter chain before applying normalization.
299
+ Multiple filters can be specified by comma-separating them.
300
+ """
301
+ ),
302
+ )
303
+ group_acodec.add_argument(
304
+ "-pof",
305
+ "--post-filter",
306
+ type=str,
307
+ help=textwrap.dedent(
308
+ """\
309
+ Add an audio filter chain after applying normalization.
310
+ Multiple filters can be specified by comma-separating them.
311
+
312
+ For EBU, the filter will be applied during the second pass.
313
+ """
314
+ ),
315
+ )
316
+
317
+ group_vcodec = parser.add_argument_group("Other Encoding Options")
318
+ group_vcodec.add_argument(
319
+ "-vn",
320
+ "--video-disable",
321
+ action="store_true",
322
+ help="Do not write video streams to output",
323
+ )
324
+ group_vcodec.add_argument(
325
+ "-c:v",
326
+ "--video-codec",
327
+ type=str,
328
+ help=textwrap.dedent(
329
+ """\
330
+ Video codec to use for output files (default: 'copy').
331
+ See `ffmpeg -encoders` for a list.
332
+
333
+ Will attempt to copy video codec by default.
334
+ """
335
+ ),
336
+ default="copy",
337
+ )
338
+ group_vcodec.add_argument(
339
+ "-sn",
340
+ "--subtitle-disable",
341
+ action="store_true",
342
+ help="Do not write subtitle streams to output",
343
+ )
344
+ group_vcodec.add_argument(
345
+ "-mn",
346
+ "--metadata-disable",
347
+ action="store_true",
348
+ help="Do not write metadata to output",
349
+ )
350
+ group_vcodec.add_argument(
351
+ "-cn",
352
+ "--chapters-disable",
353
+ action="store_true",
354
+ help="Do not write chapters to output",
355
+ )
356
+
357
+ group_format = parser.add_argument_group("Input/Output options")
358
+ group_format.add_argument(
359
+ "-ei",
360
+ "--extra-input-options",
361
+ type=str,
362
+ help=textwrap.dedent(
363
+ """\
364
+ Extra input options list.
365
+
366
+ A list of extra ffmpeg command line arguments valid for the input,
367
+ applied before ffmpeg's `-i`.
368
+
369
+ You can either use a JSON-formatted list (i.e., a list of
370
+ comma-separated, quoted elements within square brackets), or a simple
371
+ string of space-separated arguments.
372
+
373
+ If JSON is used, you need to wrap the whole argument in quotes to
374
+ prevent shell expansion and to preserve literal quotes inside the
375
+ string. If a simple string is used, you need to specify the argument
376
+ with `-e=`.
377
+
378
+ Examples: `-e '[ "-f", "mpegts" ]'` or `-e="-f mpegts"`
379
+ """
380
+ ),
381
+ )
382
+ group_format.add_argument(
383
+ "-e",
384
+ "--extra-output-options",
385
+ type=str,
386
+ help=textwrap.dedent(
387
+ """\
388
+ Extra output options list.
389
+
390
+ A list of extra ffmpeg command line arguments.
391
+
392
+ You can either use a JSON-formatted list (i.e., a list of
393
+ comma-separated, quoted elements within square brackets), or a simple
394
+ string of space-separated arguments.
395
+
396
+ If JSON is used, you need to wrap the whole argument in quotes to
397
+ prevent shell expansion and to preserve literal quotes inside the
398
+ string. If a simple string is used, you need to specify the argument
399
+ with `-e=`.
400
+
401
+ Examples: `-e '[ "-vbr", "3" ]'` or `-e="-vbr 3"`
402
+ """
403
+ ),
404
+ )
405
+ group_format.add_argument(
406
+ "-ofmt",
407
+ "--output-format",
408
+ type=str,
409
+ help=textwrap.dedent(
410
+ """\
411
+ Media format to use for output file(s).
412
+ See 'ffmpeg -formats' for a list.
413
+
414
+ If not specified, the format will be inferred by ffmpeg from the output
415
+ file name. If the output file name is not explicitly specified, the
416
+ extension will govern the format (see '--extension' option).
417
+ """
418
+ ),
419
+ )
420
+ group_format.add_argument(
421
+ "-ext",
422
+ "--extension",
423
+ type=str,
424
+ help=textwrap.dedent(
425
+ """\
426
+ Output file extension to use for output files that were not explicitly
427
+ specified. (Default: `mkv`)
428
+ """
429
+ ),
430
+ default="mkv",
431
+ )
432
+ return parser
433
+
434
+
435
+ def _split_options(opts):
436
+ """
437
+ Parse extra options (input or output) into a list
438
+ """
439
+ if not opts:
440
+ return []
441
+ try:
442
+ if opts.startswith("["):
443
+ try:
444
+ ret = [str(s) for s in json.loads(opts)]
445
+ except JSONDecodeError:
446
+ ret = shlex.split(opts)
447
+ else:
448
+ ret = shlex.split(opts)
449
+ except Exception as e:
450
+ raise FFmpegNormalizeError(f"Could not parse extra_options: {e}")
451
+ return ret
452
+
453
+
454
+ def main():
455
+ cli_args = create_parser().parse_args()
456
+
457
+ if cli_args.quiet:
458
+ logger.setLevel(logging.ERROR)
459
+ elif cli_args.debug:
460
+ logger.setLevel(logging.DEBUG)
461
+ elif cli_args.verbose:
462
+ logger.setLevel(logging.INFO)
463
+
464
+ # parse extra options
465
+ extra_input_options = _split_options(cli_args.extra_input_options)
466
+ extra_output_options = _split_options(cli_args.extra_output_options)
467
+
468
+ ffmpeg_normalize = FFmpegNormalize(
469
+ normalization_type=cli_args.normalization_type,
470
+ target_level=cli_args.target_level,
471
+ print_stats=cli_args.print_stats,
472
+ loudness_range_target=cli_args.loudness_range_target,
473
+ # threshold=cli_args.threshold,
474
+ true_peak=cli_args.true_peak,
475
+ offset=cli_args.offset,
476
+ dual_mono=cli_args.dual_mono,
477
+ audio_codec=cli_args.audio_codec,
478
+ audio_bitrate=cli_args.audio_bitrate,
479
+ sample_rate=cli_args.sample_rate,
480
+ keep_original_audio=cli_args.keep_original_audio,
481
+ pre_filter=cli_args.pre_filter,
482
+ post_filter=cli_args.post_filter,
483
+ video_codec=cli_args.video_codec,
484
+ video_disable=cli_args.video_disable,
485
+ subtitle_disable=cli_args.subtitle_disable,
486
+ metadata_disable=cli_args.metadata_disable,
487
+ chapters_disable=cli_args.chapters_disable,
488
+ extra_input_options=extra_input_options,
489
+ extra_output_options=extra_output_options,
490
+ output_format=cli_args.output_format,
491
+ dry_run=cli_args.dry_run,
492
+ progress=cli_args.progress,
493
+ )
494
+
495
+ if (
496
+ cli_args.output is not None
497
+ and len(cli_args.output) > 0
498
+ and len(cli_args.input) > len(cli_args.output)
499
+ ):
500
+ logger.warning(
501
+ "There are more input files than output file names given. "
502
+ "Please specify one output file name per input file using -o <output1> <output2> ... "
503
+ "Will apply default file naming for the remaining ones."
504
+ )
505
+
506
+ for index, input_file in enumerate(cli_args.input):
507
+ if cli_args.output is not None and index < len(cli_args.output):
508
+ if cli_args.output_folder and cli_args.output_folder != "normalized":
509
+ logger.warning(
510
+ "Output folder {} is ignored for input file {}".format(
511
+ cli_args.output_folder, input_file
512
+ )
513
+ )
514
+ output_file = cli_args.output[index]
515
+ output_dir = os.path.dirname(output_file)
516
+ if output_dir != "" and not os.path.isdir(output_dir):
517
+ raise FFmpegNormalizeError(
518
+ f"Output file path {output_dir} does not exist"
519
+ )
520
+ else:
521
+ output_file = os.path.join(
522
+ cli_args.output_folder,
523
+ os.path.splitext(os.path.basename(input_file))[0]
524
+ + "."
525
+ + cli_args.extension,
526
+ )
527
+ if not os.path.isdir(cli_args.output_folder) and not cli_args.dry_run:
528
+ logger.warning(
529
+ "Output directory '{}' does not exist, will create".format(
530
+ cli_args.output_folder
531
+ )
532
+ )
533
+ os.makedirs(cli_args.output_folder)
534
+
535
+ if os.path.exists(output_file) and not cli_args.force:
536
+ logger.error(
537
+ "Output file {} already exists, skipping. Use -f to force overwriting.".format(
538
+ output_file
539
+ )
540
+ )
541
+ else:
542
+ ffmpeg_normalize.add_media_file(input_file, output_file)
543
+
544
+ ffmpeg_normalize.run_normalization()
545
+
546
+
547
+ if __name__ == "__main__":
548
+ main()
lib/ffmpeg_normalize/_cmd_utils.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import division
2
+ import os
3
+ import sys
4
+ import subprocess
5
+ from platform import system as _current_os
6
+ import re
7
+ from ffmpeg_progress_yield import FfmpegProgress
8
+
9
+ from ._errors import FFmpegNormalizeError
10
+ from ._logger import setup_custom_logger
11
+
12
+ logger = setup_custom_logger("ffmpeg_normalize")
13
+
14
+ CUR_OS = _current_os()
15
+ IS_WIN = CUR_OS in ["Windows", "cli"]
16
+ IS_NIX = (not IS_WIN) and any(
17
+ CUR_OS.startswith(i)
18
+ for i in ["CYGWIN", "MSYS", "Linux", "Darwin", "SunOS", "FreeBSD", "NetBSD"]
19
+ )
20
+ NUL = "NUL" if IS_WIN else "/dev/null"
21
+ DUR_REGEX = re.compile(
22
+ r"Duration: (?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})\.(?P<ms>\d{2})"
23
+ )
24
+
25
+
26
+ # https://gist.github.com/Hellowlol/5f8545e999259b4371c91ac223409209
27
+ def to_ms(s=None, des=None, **kwargs):
28
+ if s:
29
+ hour = int(s[0:2])
30
+ minute = int(s[3:5])
31
+ sec = int(s[6:8])
32
+ ms = int(s[10:11])
33
+ else:
34
+ hour = int(kwargs.get("hour", 0))
35
+ minute = int(kwargs.get("min", 0))
36
+ sec = int(kwargs.get("sec", 0))
37
+ ms = int(kwargs.get("ms"))
38
+
39
+ result = (hour * 60 * 60 * 1000) + (minute * 60 * 1000) + (sec * 1000) + ms
40
+ if des and isinstance(des, int):
41
+ return round(result, des)
42
+ return result
43
+
44
+
45
+ class CommandRunner:
46
+ def __init__(self, cmd, dry=False):
47
+ self.cmd = cmd
48
+ self.dry = dry
49
+ self.output = None
50
+
51
+ def run_ffmpeg_command(self):
52
+ # wrapper for 'ffmpeg-progress-yield'
53
+ ff = FfmpegProgress(self.cmd, dry_run=self.dry)
54
+ for progress in ff.run_command_with_progress():
55
+ yield progress
56
+
57
+ self.output = ff.stderr
58
+
59
+ def run_command(self):
60
+ logger.debug(f"Running command: {self.cmd}")
61
+
62
+ if self.dry:
63
+ logger.debug("Dry mode specified, not actually running command")
64
+ return
65
+
66
+ p = subprocess.Popen(
67
+ self.cmd,
68
+ stdin=subprocess.PIPE, # Apply stdin isolation by creating separate pipe.
69
+ stdout=subprocess.PIPE,
70
+ stderr=subprocess.PIPE,
71
+ universal_newlines=False,
72
+ )
73
+
74
+ # simple running of command
75
+ stdout, stderr = p.communicate()
76
+
77
+ stdout = stdout.decode("utf8", errors="replace")
78
+ stderr = stderr.decode("utf8", errors="replace")
79
+
80
+ if p.returncode == 0:
81
+ self.output = stdout + stderr
82
+ else:
83
+ raise RuntimeError(
84
+ f"Error running command {self.cmd}: {str(stderr)}"
85
+ )
86
+
87
+ def get_output(self):
88
+ return self.output
89
+
90
+
91
+ def which(program):
92
+ """
93
+ Find a program in PATH and return path
94
+ From: http://stackoverflow.com/q/377017/
95
+ """
96
+
97
+ def is_exe(fpath):
98
+ found = os.path.isfile(fpath) and os.access(fpath, os.X_OK)
99
+ if not found and sys.platform == "win32":
100
+ fpath = fpath + ".exe"
101
+ found = os.path.isfile(fpath) and os.access(fpath, os.X_OK)
102
+ return found
103
+
104
+ fpath, _ = os.path.split(program)
105
+ if fpath:
106
+ if is_exe(program):
107
+ logger.debug("found executable: " + str(program))
108
+ return program
109
+ else:
110
+ for path in os.environ["PATH"].split(os.pathsep):
111
+ path = os.path.expandvars(os.path.expanduser(path)).strip('"')
112
+ exe_file = os.path.join(path, program)
113
+ if is_exe(exe_file):
114
+ logger.debug("found executable in path: " + str(exe_file))
115
+ return exe_file
116
+
117
+ return None
118
+
119
+
120
+ def dict_to_filter_opts(opts):
121
+ filter_opts = []
122
+ for k, v in opts.items():
123
+ filter_opts.append(f"{k}={v}")
124
+ return ":".join(filter_opts)
125
+
126
+
127
+ # def get_ffmpeg_exe():
128
+ # """
129
+ # Return path to ffmpeg executable
130
+ # """
131
+ # ffmpeg_path = os.getenv("FFMPEG_PATH")
132
+ # if ffmpeg_path:
133
+ # if os.sep in ffmpeg_path:
134
+ # ffmpeg_exe = ffmpeg_path
135
+ # if not os.path.isfile(ffmpeg_exe):
136
+ # raise FFmpegNormalizeError(f"No file exists at {ffmpeg_exe}")
137
+ # else:
138
+ # ffmpeg_exe = which(ffmpeg_path)
139
+ # if not ffmpeg_exe:
140
+ # raise FFmpegNormalizeError(
141
+ # f"Could not find '{ffmpeg_path}' in your $PATH."
142
+ # )
143
+ # else:
144
+ # ffmpeg_exe = which("ffmpeg")
145
+
146
+ # if not ffmpeg_exe:
147
+ # if which("avconv"):
148
+ # raise FFmpegNormalizeError(
149
+ # "avconv is not supported. "
150
+ # "Please install ffmpeg from http://ffmpeg.org instead."
151
+ # )
152
+ # else:
153
+ # raise FFmpegNormalizeError(
154
+ # "Could not find ffmpeg in your $PATH or $FFMPEG_PATH. "
155
+ # "Please install ffmpeg from http://ffmpeg.org"
156
+ # )
157
+
158
+ # return ffmpeg_exe
159
+
160
+
161
+ def ffmpeg_has_loudnorm(ffmpeg_path):
162
+ """
163
+ Run feature detection on ffmpeg, returns True if ffmpeg supports
164
+ the loudnorm filter
165
+ """
166
+ # cmd_runner = CommandRunner([get_ffmpeg_exe(), "-filters"])
167
+ cmd_runner = CommandRunner([ffmpeg_path, "-filters"])
168
+ cmd_runner.run_command()
169
+ output = cmd_runner.get_output()
170
+ if "loudnorm" in output:
171
+ return True
172
+ else:
173
+ logger.error(
174
+ "Your ffmpeg version does not support the 'loudnorm' filter. "
175
+ "Please make sure you are running ffmpeg v3.1 or above."
176
+ )
177
+ return False
lib/ffmpeg_normalize/_errors.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import logging
3
+ from ._logger import setup_custom_logger
4
+
5
+ logger = setup_custom_logger("ffmpeg_normalize")
6
+
7
+
8
+ class FFmpegNormalizeError(Exception):
9
+ def __init__(self, message):
10
+ super(FFmpegNormalizeError, self).__init__(message)
11
+ if logger.getEffectiveLevel() == logging.DEBUG:
12
+ logger.error(f"{self.__class__.__name__}: {message}")
13
+ else:
14
+ logger.error(message)
15
+ sys.exit(1)
lib/ffmpeg_normalize/_ffmpeg_normalize.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from numbers import Number
4
+ from tqdm import tqdm
5
+
6
+ from ._cmd_utils import ffmpeg_has_loudnorm # get_ffmpeg_exe
7
+ from ._media_file import MediaFile
8
+ from ._errors import FFmpegNormalizeError
9
+ from ._logger import setup_custom_logger
10
+
11
+ logger = setup_custom_logger("ffmpeg_normalize")
12
+
13
+ NORMALIZATION_TYPES = ["ebu", "rms", "peak"]
14
+ PCM_INCOMPATIBLE_FORMATS = ["mp4", "mp3", "ogg", "webm"]
15
+ PCM_INCOMPATIBLE_EXTS = ["mp4", "m4a", "mp3", "ogg", "webm"]
16
+
17
+
18
+ def check_range(number, min_r, max_r, name=""):
19
+ """
20
+ Check if a number is within a given range
21
+ """
22
+ try:
23
+ number = float(number)
24
+ if number < min_r or number > max_r:
25
+ raise FFmpegNormalizeError(
26
+ f"{name} must be within [{min_r},{max_r}]"
27
+ )
28
+ return number
29
+ pass
30
+ except Exception as e:
31
+ raise e
32
+
33
+
34
+ class FFmpegNormalize:
35
+ """
36
+ ffmpeg-normalize class.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ normalization_type="ebu",
42
+ target_level=-23.0,
43
+ print_stats=False,
44
+ # threshold=0.5,
45
+ loudness_range_target=7.0,
46
+ true_peak=-2.0,
47
+ offset=0.0,
48
+ dual_mono=False,
49
+ audio_codec="pcm_s16le",
50
+ audio_bitrate=None,
51
+ sample_rate=None,
52
+ keep_original_audio=False,
53
+ pre_filter=None,
54
+ post_filter=None,
55
+ video_codec="copy",
56
+ video_disable=False,
57
+ subtitle_disable=False,
58
+ metadata_disable=False,
59
+ chapters_disable=False,
60
+ extra_input_options=None,
61
+ extra_output_options=None,
62
+ output_format=None,
63
+ dry_run=False,
64
+ debug=False,
65
+ progress=False,
66
+ ffmpeg_exe=None
67
+ ):
68
+ # self.ffmpeg_exe = get_ffmpeg_exe()
69
+ self.ffmpeg_exe = ffmpeg_exe
70
+ self.has_loudnorm_capabilities = ffmpeg_has_loudnorm(self.ffmpeg_exe)
71
+
72
+ self.normalization_type = normalization_type
73
+ if not self.has_loudnorm_capabilities and self.normalization_type == "ebu":
74
+ raise FFmpegNormalizeError(
75
+ "Your ffmpeg version does not support the 'loudnorm' EBU R128 filter. "
76
+ "Please install ffmpeg v3.1 or above, or choose another normalization type."
77
+ )
78
+
79
+ if self.normalization_type == "ebu":
80
+ self.target_level = check_range(target_level, -70, -5, name="target_level")
81
+ else:
82
+ self.target_level = check_range(target_level, -99, 0, name="target_level")
83
+
84
+ self.print_stats = print_stats
85
+
86
+ # self.threshold = float(threshold)
87
+
88
+ self.loudness_range_target = check_range(
89
+ loudness_range_target, 1, 20, name="loudness_range_target"
90
+ )
91
+ self.true_peak = check_range(true_peak, -9, 0, name="true_peak")
92
+ self.offset = check_range(offset, -99, 99, name="offset")
93
+
94
+ self.dual_mono = True if dual_mono in ["true", True] else False
95
+ self.audio_codec = audio_codec
96
+ self.audio_bitrate = audio_bitrate
97
+ self.sample_rate = int(sample_rate) if sample_rate is not None else None
98
+ self.keep_original_audio = keep_original_audio
99
+ self.video_codec = video_codec
100
+ self.video_disable = video_disable
101
+ self.subtitle_disable = subtitle_disable
102
+ self.metadata_disable = metadata_disable
103
+ self.chapters_disable = chapters_disable
104
+
105
+ self.extra_input_options = extra_input_options
106
+ self.extra_output_options = extra_output_options
107
+ self.pre_filter = pre_filter
108
+ self.post_filter = post_filter
109
+
110
+ self.output_format = output_format
111
+ self.dry_run = dry_run
112
+ self.debug = debug
113
+ self.progress = progress
114
+
115
+ self.stats = []
116
+
117
+ if (
118
+ self.output_format
119
+ and (self.audio_codec is None or "pcm" in self.audio_codec)
120
+ and self.output_format in PCM_INCOMPATIBLE_FORMATS
121
+ ):
122
+ raise FFmpegNormalizeError(
123
+ f"Output format {self.output_format} does not support PCM audio. "
124
+ + "Please choose a suitable audio codec with the -c:a option."
125
+ )
126
+
127
+ if normalization_type not in NORMALIZATION_TYPES:
128
+ raise FFmpegNormalizeError(
129
+ f"Normalization type must be one of {NORMALIZATION_TYPES}"
130
+ )
131
+
132
+ if self.target_level and not isinstance(self.target_level, Number):
133
+ raise FFmpegNormalizeError("target_level must be a number")
134
+
135
+ if self.loudness_range_target and not isinstance(
136
+ self.loudness_range_target, Number
137
+ ):
138
+ raise FFmpegNormalizeError("loudness_range_target must be a number")
139
+
140
+ if self.true_peak and not isinstance(self.true_peak, Number):
141
+ raise FFmpegNormalizeError("true_peak must be a number")
142
+
143
+ if float(target_level) > 0:
144
+ raise FFmpegNormalizeError("Target level must be below 0")
145
+
146
+ self.media_files = []
147
+ self.file_count = 0
148
+
149
+ def add_media_file(self, input_file, output_file):
150
+ """
151
+ Add a media file to normalize
152
+
153
+ Arguments:
154
+ input_file {str} -- Path to input file
155
+ output_file {str} -- Path to output file
156
+ """
157
+ if not os.path.exists(input_file):
158
+ raise FFmpegNormalizeError("file " + input_file + " does not exist")
159
+
160
+ ext = os.path.splitext(output_file)[1][1:]
161
+ if (
162
+ self.audio_codec is None or "pcm" in self.audio_codec
163
+ ) and ext in PCM_INCOMPATIBLE_EXTS:
164
+ raise FFmpegNormalizeError(
165
+ f"Output extension {ext} does not support PCM audio. " +
166
+ "Please choose a suitable audio codec with the -c:a option."
167
+ )
168
+
169
+ mf = MediaFile(self, input_file, output_file)
170
+ self.media_files.append(mf)
171
+
172
+ self.file_count += 1
173
+
174
+ def run_normalization(self):
175
+ """
176
+ Run the normalization procedures
177
+ """
178
+ for index, media_file in enumerate(
179
+ tqdm(self.media_files, desc="File", disable=not self.progress, position=0)
180
+ ):
181
+ logger.info(
182
+ f"Normalizing file {media_file} ({index + 1} of {self.file_count})"
183
+ )
184
+
185
+ try:
186
+ media_file.run_normalization()
187
+ except Exception as e:
188
+ if len(self.media_files) > 1:
189
+ # simply warn and do not die
190
+ logger.error(
191
+ "Error processing input file {}, will continue batch-processing. Error was: {}".format(
192
+ media_file, e
193
+ )
194
+ )
195
+ else:
196
+ # raise the error so the program will exit
197
+ raise e
198
+
199
+ logger.info(f"Normalized file written to {media_file.output_file}")
200
+
201
+ if self.print_stats and self.stats:
202
+ print(json.dumps(self.stats, indent=4))
lib/ffmpeg_normalize/_logger.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from platform import system
3
+ from tqdm import tqdm
4
+ import sys
5
+
6
+ loggers = {}
7
+
8
+
9
+ # https://stackoverflow.com/questions/38543506/
10
+ class TqdmLoggingHandler(logging.StreamHandler):
11
+ def __init__(self):
12
+ super().__init__(sys.stderr)
13
+
14
+ def emit(self, record):
15
+ try:
16
+ msg = self.format(record)
17
+ set_mp_lock()
18
+ tqdm.write(msg, file=sys.stderr)
19
+ self.flush()
20
+ except (KeyboardInterrupt, SystemExit):
21
+ raise
22
+ except Exception:
23
+ self.handleError(record)
24
+
25
+ def set_mp_lock():
26
+ try:
27
+ from multiprocessing import Lock
28
+ tqdm.set_lock(Lock())
29
+ except (ImportError, OSError):
30
+ # Some python environments do not support multiprocessing
31
+ # See: https://github.com/slhck/ffmpeg-normalize/issues/156
32
+ pass
33
+
34
+ def setup_custom_logger(name):
35
+ """
36
+ Create a logger with a certain name and level
37
+ """
38
+ global loggers
39
+
40
+ if loggers.get(name):
41
+ return loggers.get(name)
42
+
43
+ formatter = logging.Formatter(fmt="%(levelname)s: %(message)s")
44
+
45
+ # handler = logging.StreamHandler()
46
+ handler = TqdmLoggingHandler()
47
+ handler.setFormatter(formatter)
48
+
49
+ # \033[1;30m - black
50
+ # \033[1;31m - red
51
+ # \033[1;32m - green
52
+ # \033[1;33m - yellow
53
+ # \033[1;34m - blue
54
+ # \033[1;35m - magenta
55
+ # \033[1;36m - cyan
56
+ # \033[1;37m - white
57
+
58
+ if system() not in ["Windows", "cli"]:
59
+ logging.addLevelName(
60
+ logging.ERROR, f"{logging.getLevelName(logging.ERROR)}"
61
+ )
62
+ logging.addLevelName(
63
+ logging.WARNING,
64
+ f"{logging.getLevelName(logging.WARNING)}",
65
+ )
66
+ logging.addLevelName(
67
+ logging.INFO, f"{logging.getLevelName(logging.INFO)}"
68
+ )
69
+ logging.addLevelName(
70
+ logging.DEBUG, f"{logging.getLevelName(logging.DEBUG)}"
71
+ )
72
+
73
+ logger = logging.getLogger(name)
74
+ logger.setLevel(logging.WARNING)
75
+
76
+ # if (logger.hasHandlers()):
77
+ # logger.handlers.clear()
78
+ if logger.handlers:
79
+ logger.handlers = []
80
+ logger.addHandler(handler)
81
+ loggers.update(dict(name=logger))
82
+
83
+ return logger
lib/ffmpeg_normalize/_media_file.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import tempfile
4
+ import shutil
5
+ from tqdm import tqdm
6
+ import shlex
7
+
8
+ from ._streams import AudioStream, VideoStream, SubtitleStream
9
+ from ._errors import FFmpegNormalizeError
10
+ from ._cmd_utils import NUL, CommandRunner, DUR_REGEX, to_ms
11
+ from ._logger import setup_custom_logger
12
+
13
+ logger = setup_custom_logger("ffmpeg_normalize")
14
+
15
+
16
+ class MediaFile:
17
+ """
18
+ Class that holds a file, its streams and adjustments
19
+ """
20
+
21
+ def __init__(self, ffmpeg_normalize, input_file, output_file=None):
22
+ """
23
+ Initialize a media file for later normalization.
24
+
25
+ Arguments:
26
+ ffmpeg_normalize {FFmpegNormalize} -- reference to overall settings
27
+ input_file {str} -- Path to input file
28
+
29
+ Keyword Arguments:
30
+ output_file {str} -- Path to output file (default: {None})
31
+ """
32
+ self.ffmpeg_normalize = ffmpeg_normalize
33
+ self.skip = False
34
+ self.input_file = input_file
35
+ self.output_file = output_file
36
+ self.streams = {"audio": {}, "video": {}, "subtitle": {}}
37
+
38
+ self.parse_streams()
39
+
40
+ def _stream_ids(self):
41
+ return (
42
+ list(self.streams["audio"].keys())
43
+ + list(self.streams["video"].keys())
44
+ + list(self.streams["subtitle"].keys())
45
+ )
46
+
47
+ def __repr__(self):
48
+ return os.path.basename(self.input_file)
49
+
50
+ def parse_streams(self):
51
+ """
52
+ Try to parse all input streams from file
53
+ """
54
+ logger.debug(f"Parsing streams of {self.input_file}")
55
+
56
+ cmd = [
57
+ self.ffmpeg_normalize.ffmpeg_exe,
58
+ "-i",
59
+ self.input_file,
60
+ "-c",
61
+ "copy",
62
+ "-t",
63
+ "0",
64
+ "-map",
65
+ "0",
66
+ "-f",
67
+ "null",
68
+ NUL,
69
+ ]
70
+
71
+ cmd_runner = CommandRunner(cmd)
72
+ cmd_runner.run_command()
73
+ output = cmd_runner.get_output()
74
+
75
+ logger.debug("Stream parsing command output:")
76
+ logger.debug(output)
77
+
78
+ output_lines = [line.strip() for line in output.split("\n")]
79
+
80
+ duration = None
81
+ for line in output_lines:
82
+
83
+ if "Duration" in line:
84
+ duration_search = DUR_REGEX.search(line)
85
+ if not duration_search:
86
+ logger.warning("Could not extract duration from input file!")
87
+ else:
88
+ duration = duration_search.groupdict()
89
+ duration = to_ms(**duration) / 1000
90
+ logger.debug("Found duration: " + str(duration) + " s")
91
+
92
+ if not line.startswith("Stream"):
93
+ continue
94
+
95
+ stream_id_match = re.search(r"#0:([\d]+)", line)
96
+ if stream_id_match:
97
+ stream_id = int(stream_id_match.group(1))
98
+ if stream_id in self._stream_ids():
99
+ continue
100
+ else:
101
+ continue
102
+
103
+ if "Audio" in line:
104
+ logger.debug(f"Found audio stream at index {stream_id}")
105
+ sample_rate_match = re.search(r"(\d+) Hz", line)
106
+ sample_rate = (
107
+ int(sample_rate_match.group(1)) if sample_rate_match else None
108
+ )
109
+ bit_depth_match = re.search(r"s(\d+)p?,", line)
110
+ bit_depth = int(bit_depth_match.group(1)) if bit_depth_match else None
111
+ self.streams["audio"][stream_id] = AudioStream(
112
+ self,
113
+ self.ffmpeg_normalize,
114
+ stream_id,
115
+ sample_rate,
116
+ bit_depth,
117
+ duration,
118
+ )
119
+
120
+ elif "Video" in line:
121
+ logger.debug(f"Found video stream at index {stream_id}")
122
+ self.streams["video"][stream_id] = VideoStream(
123
+ self, self.ffmpeg_normalize, stream_id
124
+ )
125
+
126
+ elif "Subtitle" in line:
127
+ logger.debug(f"Found subtitle stream at index {stream_id}")
128
+ self.streams["subtitle"][stream_id] = SubtitleStream(
129
+ self, self.ffmpeg_normalize, stream_id
130
+ )
131
+
132
+ if not self.streams["audio"]:
133
+ raise FFmpegNormalizeError(
134
+ f"Input file {self.input_file} does not contain any audio streams"
135
+ )
136
+
137
+ if (
138
+ os.path.splitext(self.output_file)[1].lower() in [".wav", ".mp3", ".aac"]
139
+ and len(self.streams["audio"].values()) > 1
140
+ ):
141
+ logger.warning(
142
+ "Output file only supports one stream. "
143
+ "Keeping only first audio stream."
144
+ )
145
+ first_stream = list(self.streams["audio"].values())[0]
146
+ self.streams["audio"] = {first_stream.stream_id: first_stream}
147
+ self.streams["video"] = {}
148
+ self.streams["subtitle"] = {}
149
+
150
+ def run_normalization(self):
151
+ logger.debug(f"Running normalization for {self.input_file}")
152
+
153
+ # run the first pass to get loudness stats
154
+ self._first_pass()
155
+
156
+ # run the second pass as a whole
157
+ if self.ffmpeg_normalize.progress:
158
+ with tqdm(total=100, position=1, desc="Second Pass") as pbar:
159
+ for progress in self._second_pass():
160
+ pbar.update(progress - pbar.n)
161
+ else:
162
+ for _ in self._second_pass():
163
+ pass
164
+
165
+ def _first_pass(self):
166
+ logger.debug(f"Parsing normalization info for {self.input_file}")
167
+
168
+ for index, audio_stream in enumerate(self.streams["audio"].values()):
169
+ if self.ffmpeg_normalize.normalization_type == "ebu":
170
+ fun = getattr(audio_stream, "parse_loudnorm_stats")
171
+ else:
172
+ fun = getattr(audio_stream, "parse_volumedetect_stats")
173
+
174
+ if self.ffmpeg_normalize.progress:
175
+ with tqdm(
176
+ total=100,
177
+ position=1,
178
+ desc=f"Stream {index + 1}/{len(self.streams['audio'].values())}",
179
+ ) as pbar:
180
+ for progress in fun():
181
+ pbar.update(progress - pbar.n)
182
+ else:
183
+ for _ in fun():
184
+ pass
185
+
186
+ if self.ffmpeg_normalize.print_stats:
187
+ stats = [
188
+ audio_stream.get_stats()
189
+ for audio_stream in self.streams["audio"].values()
190
+ ]
191
+ self.ffmpeg_normalize.stats.extend(stats)
192
+
193
+ def _get_audio_filter_cmd(self):
194
+ """
195
+ Return filter_complex command and output labels needed
196
+ """
197
+ filter_chains = []
198
+ output_labels = []
199
+
200
+ for audio_stream in self.streams["audio"].values():
201
+ if self.ffmpeg_normalize.normalization_type == "ebu":
202
+ normalization_filter = audio_stream.get_second_pass_opts_ebu()
203
+ else:
204
+ normalization_filter = audio_stream.get_second_pass_opts_peakrms()
205
+
206
+ input_label = f"[0:{audio_stream.stream_id}]"
207
+ output_label = f"[norm{audio_stream.stream_id}]"
208
+ output_labels.append(output_label)
209
+
210
+ filter_chain = []
211
+
212
+ if self.ffmpeg_normalize.pre_filter:
213
+ filter_chain.append(self.ffmpeg_normalize.pre_filter)
214
+
215
+ filter_chain.append(normalization_filter)
216
+
217
+ if self.ffmpeg_normalize.post_filter:
218
+ filter_chain.append(self.ffmpeg_normalize.post_filter)
219
+
220
+ filter_chains.append(input_label + ",".join(filter_chain) + output_label)
221
+
222
+ filter_complex_cmd = ";".join(filter_chains)
223
+
224
+ return filter_complex_cmd, output_labels
225
+
226
+ def _second_pass(self):
227
+ """
228
+ Construct the second pass command and run it
229
+
230
+ FIXME: make this method simpler
231
+ """
232
+ logger.info(f"Running second pass for {self.input_file}")
233
+
234
+ # get the target output stream types depending on the options
235
+ output_stream_types = ["audio"]
236
+ if not self.ffmpeg_normalize.video_disable:
237
+ output_stream_types.append("video")
238
+ if not self.ffmpeg_normalize.subtitle_disable:
239
+ output_stream_types.append("subtitle")
240
+
241
+ # base command, here we will add all other options
242
+ cmd = [self.ffmpeg_normalize.ffmpeg_exe, "-y", "-nostdin"]
243
+
244
+ # extra options (if any)
245
+ if self.ffmpeg_normalize.extra_input_options:
246
+ cmd.extend(self.ffmpeg_normalize.extra_input_options)
247
+
248
+ # get complex filter command
249
+ audio_filter_cmd, output_labels = self._get_audio_filter_cmd()
250
+
251
+ # add input file and basic filter
252
+ cmd.extend(["-i", self.input_file, "-filter_complex", audio_filter_cmd])
253
+
254
+ # map metadata, only if needed
255
+ if self.ffmpeg_normalize.metadata_disable:
256
+ cmd.extend(["-map_metadata", "-1"])
257
+ else:
258
+ # map global metadata
259
+ cmd.extend(["-map_metadata", "0"])
260
+ # map per-stream metadata (e.g. language tags)
261
+ for stream_type in output_stream_types:
262
+ stream_key = stream_type[0]
263
+ if stream_type not in self.streams:
264
+ continue
265
+ for idx, _ in enumerate(self.streams[stream_type].items()):
266
+ cmd.extend(
267
+ [
268
+ f"-map_metadata:s:{stream_key}:{idx}",
269
+ f"0:s:{stream_key}:{idx}",
270
+ ]
271
+ )
272
+
273
+ # map chapters if needed
274
+ if self.ffmpeg_normalize.chapters_disable:
275
+ cmd.extend(["-map_chapters", "-1"])
276
+ else:
277
+ cmd.extend(["-map_chapters", "0"])
278
+
279
+ # collect all '-map' and codecs needed for output video based on input video
280
+ if not self.ffmpeg_normalize.video_disable:
281
+ for s in self.streams["video"].keys():
282
+ cmd.extend(["-map", f"0:{s}"])
283
+ # set codec (copy by default)
284
+ cmd.extend(["-c:v", self.ffmpeg_normalize.video_codec])
285
+
286
+ # ... and map the output of the normalization filters
287
+ for ol in output_labels:
288
+ cmd.extend(["-map", ol])
289
+
290
+ # set audio codec (never copy)
291
+ if self.ffmpeg_normalize.audio_codec:
292
+ cmd.extend(["-c:a", self.ffmpeg_normalize.audio_codec])
293
+ else:
294
+ for index, (_, audio_stream) in enumerate(self.streams["audio"].items()):
295
+ cmd.extend([f"-c:a:{index}", audio_stream.get_pcm_codec()])
296
+
297
+ # other audio options (if any)
298
+ if self.ffmpeg_normalize.audio_bitrate:
299
+ cmd.extend(["-b:a", str(self.ffmpeg_normalize.audio_bitrate)])
300
+ if self.ffmpeg_normalize.sample_rate:
301
+ cmd.extend(["-ar", str(self.ffmpeg_normalize.sample_rate)])
302
+ else:
303
+ if self.ffmpeg_normalize.normalization_type == "ebu":
304
+ logger.warn(
305
+ "The sample rate will automatically be set to 192 kHz by the loudnorm filter. "
306
+ "Specify -ar/--sample-rate to override it."
307
+ )
308
+
309
+ # ... and subtitles
310
+ if not self.ffmpeg_normalize.subtitle_disable:
311
+ for s in self.streams["subtitle"].keys():
312
+ cmd.extend(["-map", f"0:{s}"])
313
+ # copy subtitles
314
+ cmd.extend(["-c:s", "copy"])
315
+
316
+ if self.ffmpeg_normalize.keep_original_audio:
317
+ highest_index = len(self.streams["audio"])
318
+ for index, (_, s) in enumerate(self.streams["audio"].items()):
319
+ cmd.extend(["-map", f"0:a:{index}"])
320
+ cmd.extend([f"-c:a:{highest_index + index}", "copy"])
321
+
322
+ # extra options (if any)
323
+ if self.ffmpeg_normalize.extra_output_options:
324
+ cmd.extend(self.ffmpeg_normalize.extra_output_options)
325
+
326
+ # output format (if any)
327
+ if self.ffmpeg_normalize.output_format:
328
+ cmd.extend(["-f", self.ffmpeg_normalize.output_format])
329
+
330
+ # if dry run, only show sample command
331
+ if self.ffmpeg_normalize.dry_run:
332
+ cmd.append(self.output_file)
333
+ cmd_runner = CommandRunner(cmd, dry=True)
334
+ cmd_runner.run_command()
335
+ yield 100
336
+ return
337
+
338
+ # create a temporary output file name
339
+ temp_dir = tempfile.gettempdir()
340
+ output_file_suffix = os.path.splitext(self.output_file)[1]
341
+ temp_file_name = os.path.join(
342
+ temp_dir, next(tempfile._get_candidate_names()) + output_file_suffix
343
+ )
344
+ cmd.append(temp_file_name)
345
+
346
+ # run the actual command
347
+ try:
348
+ cmd_runner = CommandRunner(cmd)
349
+ try:
350
+ for progress in cmd_runner.run_ffmpeg_command():
351
+ yield progress
352
+ except Exception as e:
353
+ logger.error(
354
+ "Error while running command {}! Error: {}".format(
355
+ " ".join([shlex.quote(c) for c in cmd]), e
356
+ )
357
+ )
358
+ raise e
359
+ else:
360
+ # move file from TMP to output file
361
+ logger.debug(
362
+ f"Moving temporary file from {temp_file_name} to {self.output_file}"
363
+ )
364
+ shutil.move(temp_file_name, self.output_file)
365
+ except Exception as e:
366
+ # remove dangling temporary file
367
+ if os.path.isfile(temp_file_name):
368
+ os.remove(temp_file_name)
369
+ raise e
370
+
371
+ logger.debug("Normalization finished")
lib/ffmpeg_normalize/_streams.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import math
5
+
6
+ from ._errors import FFmpegNormalizeError
7
+ from ._cmd_utils import NUL, CommandRunner, dict_to_filter_opts
8
+ from ._logger import setup_custom_logger
9
+
10
+ logger = setup_custom_logger("ffmpeg_normalize")
11
+
12
+
13
+ class MediaStream(object):
14
+ def __init__(self, ffmpeg_normalize, media_file, stream_type, stream_id):
15
+ """
16
+ Arguments:
17
+ media_file {MediaFile} -- parent media file
18
+ stream_type {str} -- stream type
19
+ stream_id {int} -- Audio stream id
20
+ """
21
+ self.ffmpeg_normalize = ffmpeg_normalize
22
+ self.media_file = media_file
23
+ self.stream_type = stream_type
24
+ self.stream_id = stream_id
25
+
26
+ def __repr__(self):
27
+ return "<{}, {} stream {}>".format(
28
+ os.path.basename(self.media_file.input_file),
29
+ self.stream_type,
30
+ self.stream_id,
31
+ )
32
+
33
+
34
+ class VideoStream(MediaStream):
35
+ def __init__(self, ffmpeg_normalize, media_file, stream_id):
36
+ super(VideoStream, self).__init__(
37
+ media_file, ffmpeg_normalize, "video", stream_id
38
+ )
39
+
40
+
41
+ class SubtitleStream(MediaStream):
42
+ def __init__(self, ffmpeg_normalize, media_file, stream_id):
43
+ super(SubtitleStream, self).__init__(
44
+ media_file, ffmpeg_normalize, "subtitle", stream_id
45
+ )
46
+
47
+
48
+ class AudioStream(MediaStream):
49
+ def __init__(
50
+ self,
51
+ ffmpeg_normalize,
52
+ media_file,
53
+ stream_id,
54
+ sample_rate=None,
55
+ bit_depth=None,
56
+ duration=None,
57
+ ):
58
+ """
59
+ Arguments:
60
+ sample_rate {int} -- in Hz
61
+ bit_depth {int}
62
+ duration {int} -- duration in seconds
63
+ """
64
+ super(AudioStream, self).__init__(
65
+ media_file, ffmpeg_normalize, "audio", stream_id
66
+ )
67
+
68
+ self.loudness_statistics = {"ebu": None, "mean": None, "max": None}
69
+
70
+ self.sample_rate = sample_rate
71
+ self.bit_depth = bit_depth
72
+ self.duration = duration
73
+
74
+ if (
75
+ self.ffmpeg_normalize.normalization_type == "ebu"
76
+ and self.duration
77
+ and self.duration <= 3
78
+ ):
79
+ logger.warn(
80
+ "Audio stream has a duration of less than 3 seconds. "
81
+ "Normalization may not work. "
82
+ "See https://github.com/slhck/ffmpeg-normalize/issues/87 for more info."
83
+ )
84
+
85
+ def __repr__(self):
86
+ return "<{}, audio stream {}>".format(
87
+ os.path.basename(self.media_file.input_file), self.stream_id
88
+ )
89
+
90
+ def get_stats(self):
91
+ """
92
+ Return statistics
93
+ """
94
+ stats = {
95
+ "input_file": self.media_file.input_file,
96
+ "output_file": self.media_file.output_file,
97
+ "stream_id": self.stream_id,
98
+ }
99
+ stats.update(self.loudness_statistics)
100
+ return stats
101
+
102
+ def get_pcm_codec(self):
103
+ if not self.bit_depth:
104
+ return "pcm_s16le"
105
+ elif self.bit_depth <= 8:
106
+ return "pcm_s8"
107
+ elif self.bit_depth in [16, 24, 32, 64]:
108
+ return f"pcm_s{self.bit_depth}le"
109
+ else:
110
+ logger.warning(
111
+ f"Unsupported bit depth {self.bit_depth}, falling back to pcm_s16le"
112
+ )
113
+ return "pcm_s16le"
114
+
115
+ def _get_filter_str_with_pre_filter(self, current_filter):
116
+ """
117
+ Get a filter stringΒ for current_filter, with the pre-filter
118
+ added before. Applies the input label before.
119
+ """
120
+ input_label = f"[0:{self.stream_id}]"
121
+ filter_chain = []
122
+ if self.media_file.ffmpeg_normalize.pre_filter:
123
+ filter_chain.append(self.media_file.ffmpeg_normalize.pre_filter)
124
+ filter_chain.append(current_filter)
125
+ filter_str = input_label + ",".join(filter_chain)
126
+ return filter_str
127
+
128
+ def parse_volumedetect_stats(self):
129
+ """
130
+ Use ffmpeg with volumedetect filter to get the mean volume of the input file.
131
+ """
132
+ logger.info(
133
+ f"Running first pass volumedetect filter for stream {self.stream_id}"
134
+ )
135
+
136
+ filter_str = self._get_filter_str_with_pre_filter("volumedetect")
137
+
138
+ cmd = [
139
+ self.media_file.ffmpeg_normalize.ffmpeg_exe,
140
+ "-nostdin",
141
+ "-y",
142
+ "-i",
143
+ self.media_file.input_file,
144
+ "-filter_complex",
145
+ filter_str,
146
+ "-vn",
147
+ "-sn",
148
+ "-f",
149
+ "null",
150
+ NUL,
151
+ ]
152
+
153
+ cmd_runner = CommandRunner(cmd)
154
+ for progress in cmd_runner.run_ffmpeg_command():
155
+ yield progress
156
+ output = cmd_runner.get_output()
157
+
158
+ logger.debug("Volumedetect command output:")
159
+ logger.debug(output)
160
+
161
+ mean_volume_matches = re.findall(r"mean_volume: ([\-\d\.]+) dB", output)
162
+ if mean_volume_matches:
163
+ self.loudness_statistics["mean"] = float(mean_volume_matches[0])
164
+ else:
165
+ raise FFmpegNormalizeError(
166
+ f"Could not get mean volume for {self.media_file.input_file}"
167
+ )
168
+
169
+ max_volume_matches = re.findall(r"max_volume: ([\-\d\.]+) dB", output)
170
+ if max_volume_matches:
171
+ self.loudness_statistics["max"] = float(max_volume_matches[0])
172
+ else:
173
+ raise FFmpegNormalizeError(
174
+ f"Could not get max volume for {self.media_file.input_file}"
175
+ )
176
+
177
+ def parse_loudnorm_stats(self):
178
+ """
179
+ Run a first pass loudnorm filter to get measured data.
180
+ """
181
+ logger.info(f"Running first pass loudnorm filter for stream {self.stream_id}")
182
+
183
+ opts = {
184
+ "i": self.media_file.ffmpeg_normalize.target_level,
185
+ "lra": self.media_file.ffmpeg_normalize.loudness_range_target,
186
+ "tp": self.media_file.ffmpeg_normalize.true_peak,
187
+ "offset": self.media_file.ffmpeg_normalize.offset,
188
+ "print_format": "json",
189
+ }
190
+
191
+ if self.media_file.ffmpeg_normalize.dual_mono:
192
+ opts["dual_mono"] = "true"
193
+
194
+ filter_str = self._get_filter_str_with_pre_filter(
195
+ "loudnorm=" + dict_to_filter_opts(opts)
196
+ )
197
+
198
+ cmd = [
199
+ self.media_file.ffmpeg_normalize.ffmpeg_exe,
200
+ "-nostdin",
201
+ "-y",
202
+ "-i",
203
+ self.media_file.input_file,
204
+ "-filter_complex",
205
+ filter_str,
206
+ "-vn",
207
+ "-sn",
208
+ "-f",
209
+ "null",
210
+ NUL,
211
+ ]
212
+
213
+ cmd_runner = CommandRunner(cmd)
214
+ for progress in cmd_runner.run_ffmpeg_command():
215
+ yield progress
216
+ output = cmd_runner.get_output()
217
+
218
+ logger.debug("Loudnorm first pass command output:")
219
+ logger.debug(output)
220
+
221
+ output_lines = [line.strip() for line in output.split("\n")]
222
+
223
+ self.loudness_statistics["ebu"] = AudioStream._parse_loudnorm_output(
224
+ output_lines
225
+ )
226
+
227
+ @staticmethod
228
+ def _parse_loudnorm_output(output_lines):
229
+ loudnorm_start = False
230
+ loudnorm_end = False
231
+ for index, line in enumerate(output_lines):
232
+ if line.startswith("[Parsed_loudnorm"):
233
+ loudnorm_start = index + 1
234
+ continue
235
+ if loudnorm_start and line.startswith("}"):
236
+ loudnorm_end = index + 1
237
+ break
238
+
239
+ if not (loudnorm_start and loudnorm_end):
240
+ raise FFmpegNormalizeError(
241
+ "Could not parse loudnorm stats; no loudnorm-related output found"
242
+ )
243
+
244
+ try:
245
+ loudnorm_stats = json.loads(
246
+ "\n".join(output_lines[loudnorm_start:loudnorm_end])
247
+ )
248
+
249
+ logger.debug(f"Loudnorm stats parsed: {json.dumps(loudnorm_stats)}")
250
+
251
+ for key in [
252
+ "input_i",
253
+ "input_tp",
254
+ "input_lra",
255
+ "input_thresh",
256
+ "output_i",
257
+ "output_tp",
258
+ "output_lra",
259
+ "output_thresh",
260
+ "target_offset",
261
+ ]:
262
+ # handle infinite values
263
+ if float(loudnorm_stats[key]) == -float("inf"):
264
+ loudnorm_stats[key] = -99
265
+ elif float(loudnorm_stats[key]) == float("inf"):
266
+ loudnorm_stats[key] = 0
267
+ else:
268
+ # convert to floats
269
+ loudnorm_stats[key] = float(loudnorm_stats[key])
270
+
271
+ return loudnorm_stats
272
+ except Exception as e:
273
+ raise FFmpegNormalizeError(
274
+ f"Could not parse loudnorm stats; wrong JSON format in string: {e}"
275
+ )
276
+
277
+ def get_second_pass_opts_ebu(self):
278
+ """
279
+ Return second pass loudnorm filter options string for ffmpeg
280
+ """
281
+
282
+ if not self.loudness_statistics["ebu"]:
283
+ raise FFmpegNormalizeError(
284
+ "First pass not run, you must call parse_loudnorm_stats first"
285
+ )
286
+
287
+ input_i = float(self.loudness_statistics["ebu"]["input_i"])
288
+ if input_i > 0:
289
+ logger.warn(
290
+ "Input file had measured input loudness greater than zero ({}), capping at 0".format(
291
+ "input_i"
292
+ )
293
+ )
294
+ self.loudness_statistics["ebu"]["input_i"] = 0
295
+
296
+ opts = {
297
+ "i": self.media_file.ffmpeg_normalize.target_level,
298
+ "lra": self.media_file.ffmpeg_normalize.loudness_range_target,
299
+ "tp": self.media_file.ffmpeg_normalize.true_peak,
300
+ "offset": float(self.loudness_statistics["ebu"]["target_offset"]),
301
+ "measured_i": float(self.loudness_statistics["ebu"]["input_i"]),
302
+ "measured_lra": float(self.loudness_statistics["ebu"]["input_lra"]),
303
+ "measured_tp": float(self.loudness_statistics["ebu"]["input_tp"]),
304
+ "measured_thresh": float(self.loudness_statistics["ebu"]["input_thresh"]),
305
+ "linear": "true",
306
+ "print_format": "json",
307
+ }
308
+
309
+ if self.media_file.ffmpeg_normalize.dual_mono:
310
+ opts["dual_mono"] = "true"
311
+
312
+ return "loudnorm=" + dict_to_filter_opts(opts)
313
+
314
+ def get_second_pass_opts_peakrms(self):
315
+ """
316
+ Set the adjustment gain based on chosen option and mean/max volume,
317
+ return the matching ffmpeg volume filter.
318
+ """
319
+ normalization_type = self.media_file.ffmpeg_normalize.normalization_type
320
+ target_level = self.media_file.ffmpeg_normalize.target_level
321
+
322
+ if normalization_type == "peak":
323
+ adjustment = 0 + target_level - self.loudness_statistics["max"]
324
+ elif normalization_type == "rms":
325
+ adjustment = target_level - self.loudness_statistics["mean"]
326
+ else:
327
+ raise FFmpegNormalizeError(
328
+ "Can only set adjustment for peak and RMS normalization"
329
+ )
330
+
331
+ logger.info(
332
+ "Adjusting stream {} by {} dB to reach {}".format(
333
+ self.stream_id, adjustment, target_level
334
+ )
335
+ )
336
+
337
+ if self.loudness_statistics["max"] + adjustment > 0:
338
+ logger.warning(
339
+ "Adjusting will lead to clipping of {} dB".format(
340
+ self.loudness_statistics["max"] + adjustment
341
+ )
342
+ )
343
+
344
+ return f"volume={adjustment}dB"
lib/ffmpeg_normalize/_version.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = "1.22.4"
lib/osutils.js ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var _os = require('os');
2
+
3
+ window.platform = function(){
4
+ return process.platform;
5
+ }
6
+
7
+ window.cpuCount = function(){
8
+ return _os.cpus().length;
9
+ }
10
+
11
+ window.sysUptime = function(){
12
+ //seconds
13
+ return _os.uptime();
14
+ }
15
+
16
+ window.processUptime = function(){
17
+ //seconds
18
+ return process.uptime();
19
+ }
20
+
21
+
22
+
23
+ // Memory
24
+ window.freemem = function(){
25
+ return _os.freemem() / ( 1024 * 1024 );
26
+ }
27
+
28
+ window.totalmem = function(){
29
+
30
+ return _os.totalmem() / ( 1024 * 1024 );
31
+ }
32
+
33
+ window.freememPercentage = function(){
34
+ return _os.freemem() / _os.totalmem();
35
+ }
36
+
37
+ window.freeCommand = function(callback){
38
+
39
+ // Only Linux
40
+ require('child_process').exec('free -m', function(error, stdout, stderr) {
41
+
42
+ var lines = stdout.split("\n");
43
+
44
+
45
+ var str_mem_info = lines[1].replace( /[\s\n\r]+/g,' ');
46
+
47
+ var mem_info = str_mem_info.split(' ')
48
+
49
+ total_mem = parseFloat(mem_info[1])
50
+ free_mem = parseFloat(mem_info[3])
51
+ buffers_mem = parseFloat(mem_info[5])
52
+ cached_mem = parseFloat(mem_info[6])
53
+
54
+ used_mem = total_mem - (free_mem + buffers_mem + cached_mem)
55
+
56
+ callback(used_mem -2);
57
+ });
58
+ }
59
+
60
+
61
+ // Hard Disk Drive
62
+ window.harddrive = function(callback){
63
+
64
+ require('child_process').exec('df -k', function(error, stdout, stderr) {
65
+
66
+ var total = 0;
67
+ var used = 0;
68
+ var free = 0;
69
+
70
+ var lines = stdout.split("\n");
71
+
72
+ var str_disk_info = lines[1].replace( /[\s\n\r]+/g,' ');
73
+
74
+ var disk_info = str_disk_info.split(' ');
75
+
76
+ total = Math.ceil((disk_info[1] * 1024)/ Math.pow(1024,2));
77
+ used = Math.ceil(disk_info[2] * 1024 / Math.pow(1024,2)) ;
78
+ free = Math.ceil(disk_info[3] * 1024 / Math.pow(1024,2)) ;
79
+
80
+ callback(total, free, used);
81
+ });
82
+ }
83
+
84
+
85
+
86
+ // Return process running current
87
+ window.getProcesses = function(nProcess, callback){
88
+
89
+ // if nprocess is undefined then is function
90
+ if(typeof nProcess === 'function'){
91
+
92
+ callback =nProcess;
93
+ nProcess = 0
94
+ }
95
+
96
+ command = 'ps -eo pcpu,pmem,time,args | sort -k 1 -r | head -n'+10
97
+ //command = 'ps aux | head -n '+ 11
98
+ //command = 'ps aux | head -n '+ (nProcess + 1)
99
+ if (nProcess > 0)
100
+ command = 'ps -eo pcpu,pmem,time,args | sort -k 1 -r | head -n'+(nProcess + 1)
101
+
102
+ require('child_process').exec(command, function(error, stdout, stderr) {
103
+
104
+ var that = this
105
+
106
+ var lines = stdout.split("\n");
107
+ lines.shift()
108
+ lines.pop()
109
+
110
+ var result = ''
111
+
112
+
113
+ lines.forEach(function(_item,_i){
114
+
115
+ var _str = _item.replace( /[\s\n\r]+/g,' ');
116
+
117
+ _str = _str.split(' ')
118
+
119
+ // result += _str[10]+" "+_str[9]+" "+_str[2]+" "+_str[3]+"\n"; // process
120
+ result += _str[1]+" "+_str[2]+" "+_str[3]+" "+_str[4].substring((_str[4].length - 25))+"\n"; // process
121
+
122
+ });
123
+
124
+ callback(result);
125
+ });
126
+ }
127
+
128
+
129
+
130
+ /*
131
+ * Returns All the load average usage for 1, 5 or 15 minutes.
132
+ */
133
+ window.allLoadavg = function(){
134
+
135
+ var loads = _os.loadavg();
136
+
137
+ return loads[0].toFixed(4)+','+loads[1].toFixed(4)+','+loads[2].toFixed(4);
138
+ }
139
+
140
+ /*
141
+ * Returns the load average usage for 1, 5 or 15 minutes.
142
+ */
143
+ window.loadavg = function(_time){
144
+
145
+ if(_time === undefined || (_time !== 5 && _time !== 15) ) _time = 1;
146
+
147
+ var loads = _os.loadavg();
148
+ var v = 0;
149
+ if(_time == 1) v = loads[0];
150
+ if(_time == 5) v = loads[1];
151
+ if(_time == 15) v = loads[2];
152
+
153
+ return v;
154
+ }
155
+
156
+
157
+ window.cpuFree = function(callback){
158
+ getCPUUsage(callback, true);
159
+ }
160
+
161
+ window.cpuUsage = function(callback){
162
+ getCPUUsage(callback, false);
163
+ }
164
+
165
+ function getCPUUsage(callback, free){
166
+
167
+ var stats1 = getCPUInfo();
168
+ var startIdle = stats1.idle;
169
+ var startTotal = stats1.total;
170
+
171
+ setTimeout(function() {
172
+ var stats2 = getCPUInfo();
173
+ var endIdle = stats2.idle;
174
+ var endTotal = stats2.total;
175
+
176
+ var idle = endIdle - startIdle;
177
+ var total = endTotal - startTotal;
178
+ var perc = idle / total;
179
+
180
+ if(free === true)
181
+ callback( perc );
182
+ else
183
+ callback( (1 - perc) );
184
+
185
+ }, 1000 );
186
+ }
187
+
188
+ function getCPUInfo(callback){
189
+ var cpus = _os.cpus();
190
+
191
+ var user = 0;
192
+ var nice = 0;
193
+ var sys = 0;
194
+ var idle = 0;
195
+ var irq = 0;
196
+ var total = 0;
197
+
198
+ for(var cpu in cpus){
199
+ if (!cpus.hasOwnProperty(cpu)) continue;
200
+ user += cpus[cpu].times.user;
201
+ nice += cpus[cpu].times.nice;
202
+ sys += cpus[cpu].times.sys;
203
+ irq += cpus[cpu].times.irq;
204
+ idle += cpus[cpu].times.idle;
205
+ }
206
+
207
+ var total = user + nice + sys + idle + irq;
208
+
209
+ return {
210
+ 'idle': idle,
211
+ 'total': total
212
+ };
213
+ }
lib/wavesurfer.js ADDED
The diff for this file is too large to render. See raw diff
 
lib/xp_error.mp3 ADDED
Binary file (15.8 kB). View file
 
main.js ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ const PRODUCTION = process.mainModule.filename.includes("resources")
3
+ const path = PRODUCTION ? "resources/app" : "."
4
+
5
+ const remoteMain = require("@electron/remote/main")
6
+ remoteMain.initialize()
7
+
8
+ const fs = require("fs")
9
+ const {shell, app, BrowserWindow, ipcMain, Menu} = require("electron")
10
+ const {spawn} = require("child_process")
11
+
12
+ let pythonProcess
13
+
14
+ if (PRODUCTION) {
15
+ // pythonProcess = spawn(`${path}/cpython/server.exe`, {stdio: "ignore"})
16
+ } else {
17
+ pythonProcess = spawn("python", [`${path}/server.py`], {stdio: "ignore"})
18
+ }
19
+
20
+ let mainWindow
21
+ let discordClient
22
+ let discordClientStart = Date.now()
23
+
24
+ const createWindow = () => {
25
+ mainWindow = new BrowserWindow({
26
+ width: 1200,
27
+ height: 1000,
28
+ minHeight: 800,
29
+ minWidth: 1300,
30
+ frame: false,
31
+ webPreferences: {
32
+ nodeIntegration: true,
33
+ enableRemoteModule: true,
34
+ contextIsolation: false
35
+ },
36
+ icon: `${__dirname}/assets/x-icon.png`,
37
+ // show: false,
38
+ })
39
+ remoteMain.enable(mainWindow.webContents)
40
+
41
+ app.on('browser-window-created', (_, window) => {
42
+ require("@electron/remote/main").enable(mainWindow.webContents)
43
+ })
44
+
45
+ mainWindow.loadFile("index.html")
46
+ mainWindow.shell = shell
47
+
48
+ mainWindow.on("ready-to-show", () => {
49
+ mainWindow.show()
50
+ })
51
+
52
+ mainWindow.on("closed", () => mainWindow = null)
53
+ }
54
+
55
+ ipcMain.on("resize", (event, arg) => {
56
+ mainWindow.setSize(arg.width, arg.height)
57
+ })
58
+ ipcMain.on("updatePosition", (event, arg) => {
59
+ const bounds = mainWindow.getBounds()
60
+ bounds.x = parseInt(arg.details[0])
61
+ bounds.y = parseInt(arg.details[1])
62
+ mainWindow.setBounds(bounds)
63
+ })
64
+ ipcMain.on("updateDiscord", (event, arg) => {
65
+
66
+ // Disconnect if turned off
67
+ if (!Object.keys(arg).includes("details")) {
68
+ if (discordClient) {
69
+ try {
70
+ discordClient.disconnect()
71
+ } catch (e) {}
72
+ }
73
+ discordClient = undefined
74
+ return
75
+ }
76
+
77
+ if (!discordClient) {
78
+ discordClient = require('discord-rich-presence')('885096702648938546')
79
+ }
80
+
81
+ discordClient.updatePresence({
82
+ state: 'Generating AI voice acting',
83
+ details: arg.details,
84
+ startTimestamp: discordClientStart,
85
+ largeImageKey: 'xvasynth_512_512',
86
+ largeImageText: "xVASynth",
87
+ smallImageKey: 'xvasynth_512_512',
88
+ smallImageText: "xVASynth",
89
+ instance: true,
90
+ })
91
+ })
92
+ ipcMain.on("show-context-menu-editor", (event) => {
93
+ const template = [
94
+ {
95
+ label: 'Copy ARPAbet [v3]',
96
+ click: () => { event.sender.send('context-menu-command', 'context-copy-editor') }
97
+ },
98
+ ]
99
+ const menu = Menu.buildFromTemplate(template)
100
+ menu.popup(BrowserWindow.fromWebContents(event.sender))
101
+ })
102
+
103
+ ipcMain.on("show-context-menu", (event) => {
104
+ const template = [
105
+ {
106
+ label: 'Copy',
107
+ click: () => { event.sender.send('context-menu-command', 'context-copy') }
108
+ },
109
+ {
110
+ label: 'Paste',
111
+ click: () => { event.sender.send('context-menu-command', 'context-paste') }
112
+ },
113
+ // { type: 'separator' },
114
+ ]
115
+ const menu = Menu.buildFromTemplate(template)
116
+ menu.popup(BrowserWindow.fromWebContents(event.sender))
117
+ })
118
+
119
+ // This method will be called when Electron has finished
120
+ // initialization and is ready to create browser windows.
121
+ // Some APIs can only be used after this event occurs.
122
+ app.on("ready", createWindow)
123
+
124
+
125
+ // Quit when all windows are closed.
126
+ app.on("window-all-closed", () => {
127
+ // On OS X it is common for applications and their menu bar
128
+ // to stay active until the user quits explicitly with Cmd + Q
129
+ if (process.platform !== "darwin") {
130
+ app.quit()
131
+ }
132
+ })
133
+
134
+ app.on("activate", () => {
135
+ // On OS X it"s common to re-create a window in the app when the
136
+ // dock icon is clicked and there are no other windows open.
137
+ if (mainWindow === null) {
138
+ createWindow()
139
+ }
140
+ })
package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "xVASynth",
3
+ "version": "1.0.0",
4
+ "description": "Speech synthesis in the style of voice actors.",
5
+ "productName": "xVASynth",
6
+ "main": "main.js",
7
+ "scripts": {
8
+ "start": "electron .",
9
+ "package-windows": "electron-packager ./ --overwrite --prune=true --icon=assets/icon.ico --out=release-builds",
10
+ "package-linux": "electron-packager ./ --platform=linux --overwrite --prune=true --icon=assets/icon.ico --out=release-builds"
11
+ },
12
+ "repository": "https://github.com/DanRuta/xVA-Synth",
13
+ "author": "DanRuta",
14
+ "devDependencies": {
15
+ "electron": "19.0.0",
16
+ "electron-packager": "^17.1.2"
17
+ },
18
+ "dependencies": {
19
+ "@electron/remote": "^2.0.9",
20
+ "discord-rich-presence": "0.0.8",
21
+ "fs-extra": "^9.1.0",
22
+ "node-fetch": "^2.1.2",
23
+ "node-nvidia-smi": "^1.0.0",
24
+ "unzipper": "^0.10.11",
25
+ "zip-dir": "^2.0.0"
26
+ }
27
+ }
patreon.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ D0lphin, flyingvelociraptor, Caden Black, Max Loef, LadyVaudry, Thuggysmurf, radbeetle, TomahawkJackson, Solstice_, Bungles, midori95, eldayualien, John Detwiler, Cecell, Wandering Youth, ellia, Retlaw83, Trixie, CHASE MCKELVY, Leif, ionite, Joshua Jones, Jaktt1337, David Keith vun Kannon, Netherworks (Jo-Jo), neci, Rachel Wiles, Imogen, Deer, Linthar, sadfer, Danielle, Hector Medima, Sh1tMagnet, ReaperStoleMyStyle, AshbeeGaming, TCG, Lady Steel, Mikkel Jensen, CookieGalaxy, GrumpyBen, Adrilz, ReyVenom, dog, bourbonicRecluse, ShiningEdge, Dozen9292, manlethamlet, smokeandash, Elias V, EnculerDeTaMere, SKiLLsSoLoN, J, finalfrog, Hound740, Buck, Yael van Dok, ChrisTheStranger, Isabel, Fuzzy Lonesome, Drake, Beto, AceAvenger, bobbigmac, Alexandra Whitton, yic17, Joebobslim, ThatGuyWithaFace, Sergey Trifonov, Zensho, AgitoRivers, beccatoria, valo999, Ne0nFLaSH, Caro Tuts, Jack in the Hinter, Hammerhead96 ., Bewitched, Para, Wht??? Why??, Shadowtigers, PConD, Lulzar, Ryan W, Wyntilda, Gorim, Krazon, Tako-kun, Walt, Katsuki, Ember2528, RetconReality, Hazel Louise Steele, Laura Almeida, Althecow, PatronGuy, squirecrow, cramonty, crash blue, Syrr, David, Hawkbar, John S., Autumn, pimphat, FeralByrd, Comical, Dogmeat114, Dezmar-Sama, Michael Gill, Jacob Garbe, NerfViking, Dinonugget, RedneckJP007, stormalize, Golem, Luckystroker, Hapax, Vahzah Vulom, Tempuc, CAW CAW, stljeffbb, bart, MrJoy, Zoenna, Calvin, Aosana Bluewing, Dan Brookes, CDante, HunterAP, Kadisra, candied_skull, hairahcaz, nairaiwu, Mar, Paraffine, Nawen_Syaka, Amy Parker, Loseron, katiefraggle, Freon, deepbluefrog, myles.app, hanbonzan, Scientist Salari-Ren, Roman Tinkov, zackc1play, An abstract kind of horror, L, Mihu123, Trisket, Aelarr, Flipdark95, Timo Steiner, humocs, Optimist Vamscenes, Patrick VanDusen, praxis22, Rui Orey, Craig Fedynich, FrenchToast, Dorpz, cesm23, BoB, Cutup, Botty Butler, tjn2222, Matthew Warren, Tom Green, Passionate Lobster, Precipitation, Veks, Baki Balcioglu, Fenris, Patrik K., Oddbrother, E.M.A, DrogerKerchva, Camurai, hthek, iggyzee, Moppy, Stee_Muttlet, asbestos my beloved, TrueBlue, something106, woah00z, Sam Darling, JoshuaJSlone, vvvpppmmm, OvrTheTopMan, munchyfly, DarkNemphis, Justin McGough, Billyro, DIY_Rene, kevmasters, Stu, Sasquatch Bill, Inconsistent, Gothic 3 The Age of War, www48, Slothman, mavrodya petrov, ronaldomoon, Kostin Oleksandr Anatoliiovych, Ryan Lippen, Edward Hyde, Echoes, Vape Gwagwa, Kelg Celcs, Kneelers, Meryl Coker, Alan Gonzalez, PTC001, Hector Medima, CinnaMewRoll, Grant Spielbusch, Sean Lyons, Charles Hufnagel, Kirill Akimov, Mister Lyosea, Anthony Crane, Sh1tMagnet
plugins.txt ADDED
File without changes
plugins/eg_custom_event/frontendPlugin.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict"
2
+
3
+
4
+ const postStartFn = (window, data) => {
5
+ window.appLogger.log("postStartFn")
6
+
7
+ const button = document.createElement("button")
8
+ button.innerHTML = "Custom event"
9
+ // Style the button with the current game's colour
10
+ button.style.background = `#${window.currentGame[1]}`
11
+
12
+ adv_opts.children[1].appendChild(button)
13
+
14
+ button.addEventListener("click", () => {
15
+
16
+ fetch(`http://localhost:8008/customEvent`, {
17
+ method: "Post",
18
+ body: JSON.stringify({
19
+ pluginId: "eg_custom_event",
20
+ data1: "some data",
21
+ data2: "some more data",
22
+ // ....
23
+ })
24
+ }).then(r=>r.text()).then(() => {
25
+ window.appLogger.log("custom event finished")
26
+ console.log("custom event finished")
27
+ })
28
+ })
29
+
30
+ }
31
+
32
+
33
+ exports.postStartFn = postStartFn
plugins/eg_custom_event/main.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ logger = setupData["logger"]
2
+
3
+ import time
4
+
5
+ def custom_event_fn(data=None):
6
+ global logger, time
7
+ print(f'custom_event_fn: {data}')
8
+ logger.log(f'custom_event_fn: {data}')
9
+ time.sleep(2)
10
+
plugins/eg_custom_event/plugin.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "plugin-name": "Custom events example",
3
+ "author": "DanRuta",
4
+ "nexus-link": null,
5
+ "plugin-version": "1.0",
6
+ "plugin-short-description": "A demo plugin to show how custom python events work",
7
+ "min-app-version": "1.0.0",
8
+ "max-app-version": "1.4.0",
9
+ "install-requires-restart": false,
10
+ "uninstall-requires-restart": true,
11
+
12
+ "front-end-style-files": [],
13
+
14
+ "front-end-hooks": {
15
+ "start": {
16
+ "post": {
17
+ "file": "frontendPlugin.js",
18
+ "function": "postStartFn"
19
+ }
20
+ }
21
+ },
22
+ "back-end-hooks": {
23
+ "custom-event": {
24
+ "file": "main.py",
25
+ "function": "custom_event_fn"
26
+ }
27
+ }
28
+ }
plugins/test_plugin/custom_event.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ logger = setupData["logger"]
2
+
3
+ def custom_event_fn(data=None):
4
+ global logger
5
+ logger.log(f'custom_event_fn: {data}')