myetv-player 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +461 -155
- package/dist/myetv-player.js +391 -82
- package/dist/myetv-player.min.js +314 -53
- package/package.json +1 -1
- package/plugins/autosub/myetv-autosub-plugin.js +303 -155
- package/plugins/autosub/readme.md +63 -69
- package/plugins/radio/api.php +154 -0
- package/plugins/radio/icons/bolt-lightning-solid.png +0 -0
- package/plugins/radio/icons/compact-disc-solid.png +0 -0
- package/plugins/radio/icons/guitar-solid.png +0 -0
- package/plugins/radio/icons/headphones-solid.png +0 -0
- package/plugins/radio/icons/medal-solid.png +0 -0
- package/plugins/radio/icons/music-solid.png +0 -0
- package/plugins/radio/icons/newspaper-solid.png +0 -0
- package/plugins/radio/icons/radio-solid.png +0 -0
- package/plugins/radio/icons/readme.md +1 -0
- package/plugins/radio/myetv-radio-plugin.js +1655 -0
- package/plugins/radio/php/data/readme.md +1 -0
- package/plugins/radio/php/radio_stats.php +203 -0
- package/plugins/radio/proxy.php +26 -0
- package/plugins/radio/readme.md +176 -0
- package/plugins/radio/station_example.json +47 -0
- package/plugins/video-loading/myetv-video-loading-plugin.js +157 -0
- package/plugins/video-loading/readme.md +87 -0
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MYETV Player - Auto Subtitles Plugin
|
|
3
|
-
* Automatically generates subtitles via Transformers.js + Whisper
|
|
3
|
+
* Automatically generates subtitles via Transformers.js + Whisper
|
|
4
|
+
* OR Translates existing player subtitles on-the-fly.
|
|
4
5
|
*
|
|
5
6
|
* Compatible with myetv-player plugin system (registerMYETVPlugin)
|
|
6
7
|
* Author: Oscar Cosimo - MYETV
|
|
7
8
|
*
|
|
8
9
|
* Options:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* language {string} Transcription language ('italian','english','it','en',...). Default: null (auto-detect)
|
|
11
|
+
* modelSize {string} 'tiny' (~39MB), 'base' (~74MB), 'small' (~244MB). Default: 'base'
|
|
12
|
+
* autoGenerate {boolean} Auto-generate subtitles on video load. Default: false
|
|
13
|
+
* autoTranslation {string} Auto-translate subtitles into this language on load (ISO 2-letter code, e.g. 'en','it'). Default: null
|
|
14
|
+
* showButton {boolean} Show button in the control bar. Default: true
|
|
15
|
+
* position {string} 'right', 'left', or 'topbar'. Default: 'right'
|
|
16
|
+
* subtitleStyle {object} Custom CSS style object for the subtitle text div
|
|
17
|
+
* cacheEnabled {boolean} Cache transcription and translations in localStorage. Default: true
|
|
18
|
+
* idCache {string} Stable unique ID for localStorage cache key
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
(function () {
|
|
@@ -212,7 +213,9 @@
|
|
|
212
213
|
@media (max-width: 600px) {
|
|
213
214
|
.myetv-autosub-overlay { bottom: 60px; width: 96%; }
|
|
214
215
|
.myetv-autosub-text { font-size: clamp(0.6em, 3.5vw, 0.85em); padding: 3px 8px 4px; }
|
|
215
|
-
.myetv-autosub-drag-handle { display: none; }
|
|
216
|
+
.myetv-autosub-drag-handle { display: none; }
|
|
217
|
+
.myetv-autosub-overlay { pointer-events: all; cursor: grab; }
|
|
218
|
+
.myetv-autosub-overlay.dragging { cursor: grabbing; }
|
|
216
219
|
}
|
|
217
220
|
@media (max-width: 400px) {
|
|
218
221
|
.myetv-autosub-text { font-size: clamp(0.55em, 4vw, 0.75em); }
|
|
@@ -221,11 +224,9 @@
|
|
|
221
224
|
|
|
222
225
|
const ICON_CC = `<svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-9 8H9.5v-.5h-2v3h2V14H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V14H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z"/></svg>`;
|
|
223
226
|
const ICON_LOADING = `<svg viewBox="0 0 24 24">
|
|
224
|
-
<!-- cerchio rotante -->
|
|
225
227
|
<path d="M12 2A10 10 0 0 1 22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none">
|
|
226
228
|
<animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/>
|
|
227
229
|
</path>
|
|
228
|
-
<!-- icona CC fissa al centro, scala ridotta -->
|
|
229
230
|
<g transform="translate(4, 5) scale(0.65)">
|
|
230
231
|
<path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-9 8H9.5v-.5h-2v3h2V14H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V14H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z"/>
|
|
231
232
|
</g>
|
|
@@ -293,20 +294,25 @@
|
|
|
293
294
|
modelSize: 'base',
|
|
294
295
|
autoGenerate: false,
|
|
295
296
|
showButton: true,
|
|
296
|
-
position: 'right',
|
|
297
|
+
position: 'right',
|
|
297
298
|
subtitleStyle: {},
|
|
298
299
|
cacheEnabled: true,
|
|
299
300
|
idCache: null,
|
|
300
|
-
autoTranslation: null,
|
|
301
|
-
translationEngine: null,
|
|
301
|
+
autoTranslation: null,
|
|
302
|
+
translationEngine: null,
|
|
302
303
|
}, options);
|
|
303
304
|
|
|
304
305
|
this.isGenerating = false;
|
|
305
|
-
this.subtitles = [];
|
|
306
|
-
this.subtitlesTrans = [];
|
|
307
|
-
this.translateLang = 'off';
|
|
308
|
-
this._transCache = {};
|
|
306
|
+
this.subtitles = []; // Original segments (generated OR imported)
|
|
307
|
+
this.subtitlesTrans = []; // Translated segments
|
|
308
|
+
this.translateLang = 'off';
|
|
309
|
+
this._transCache = {}; // Persistent cache for translations { langCode: [...] }
|
|
309
310
|
this.subVisible = false;
|
|
311
|
+
|
|
312
|
+
// Flags for Native Subtitle Integration
|
|
313
|
+
this.hasNativeTracks = false;
|
|
314
|
+
this.isImportedSubs = false;
|
|
315
|
+
|
|
310
316
|
this.updateInterval = null;
|
|
311
317
|
this.btnWrapEl = null;
|
|
312
318
|
this.btnEl = null;
|
|
@@ -317,7 +323,7 @@
|
|
|
317
323
|
this._worker = null;
|
|
318
324
|
// Drag state
|
|
319
325
|
this._drag = { active: false, startX: 0, startY: 0, origLeft: 0, origTop: 0 };
|
|
320
|
-
// Overlay position
|
|
326
|
+
// Overlay position
|
|
321
327
|
this._overlayPos = null;
|
|
322
328
|
}
|
|
323
329
|
|
|
@@ -326,32 +332,118 @@
|
|
|
326
332
|
this._createOverlay();
|
|
327
333
|
this._createPanel();
|
|
328
334
|
if (this.opts.showButton) this._createButton();
|
|
335
|
+
|
|
336
|
+
// Integrate with player initialization lifecycle
|
|
337
|
+
this.player.addEventListener('playerready', () => {
|
|
338
|
+
this._checkForNativeTracks();
|
|
339
|
+
|
|
340
|
+
// If autoTranslation is enabled and we have native tracks, import immediately!
|
|
341
|
+
if (this.opts.autoTranslation && this.hasNativeTracks) {
|
|
342
|
+
// Give fetch a moment to load custom tracks via the core player
|
|
343
|
+
const attemptImport = setInterval(() => {
|
|
344
|
+
if (this._importPlayerSubtitles()) {
|
|
345
|
+
clearInterval(attemptImport);
|
|
346
|
+
if (this.player.options.debug) console.log('[AutoSub] Auto-imported native tracks for translation');
|
|
347
|
+
this.subVisible = true;
|
|
348
|
+
this._startDisplay();
|
|
349
|
+
this._setBtnActive(true);
|
|
350
|
+
|
|
351
|
+
// Trigger translation
|
|
352
|
+
this._setTranslationLang(this.opts.autoTranslation.toLowerCase().trim().slice(0, 2));
|
|
353
|
+
}
|
|
354
|
+
}, 500);
|
|
355
|
+
// Stop trying after 5 seconds to avoid infinite loops
|
|
356
|
+
setTimeout(() => clearInterval(attemptImport), 5000);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
329
360
|
if (this.opts.autoTranslation) {
|
|
330
361
|
this._scheduleAutoTranslation();
|
|
331
362
|
}
|
|
363
|
+
|
|
332
364
|
if (this.opts.autoGenerate) {
|
|
333
|
-
this.video.addEventListener('loadedmetadata', () =>
|
|
365
|
+
this.video.addEventListener('loadedmetadata', () => {
|
|
366
|
+
// Do NOT generate if the player already loaded native tracks
|
|
367
|
+
if (!this._checkForNativeTracks()) {
|
|
368
|
+
this.generate();
|
|
369
|
+
}
|
|
370
|
+
}, { once: true });
|
|
334
371
|
}
|
|
372
|
+
|
|
335
373
|
if (this.player.options.debug) console.log('[AutoSub] Plugin setup complete');
|
|
336
374
|
}
|
|
337
375
|
|
|
376
|
+
// Checks if the player has <track> elements or VTT/SRT data
|
|
377
|
+
_checkForNativeTracks() {
|
|
378
|
+
if (this.player.textTracks && this.player.textTracks.length > 0) {
|
|
379
|
+
this.hasNativeTracks = true;
|
|
380
|
+
if (this.btnEl) {
|
|
381
|
+
this.btnEl.setAttribute('title', 'Translate Subtitles');
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Steal subtitles from the player's custom parsed arrays
|
|
389
|
+
_importPlayerSubtitles() {
|
|
390
|
+
if (this.player.customSubtitles && this.player.customSubtitles.length > 0) {
|
|
391
|
+
let track = null;
|
|
392
|
+
|
|
393
|
+
// Try current active track first
|
|
394
|
+
if (this.player.currentCustomTrackIndex >= 0) {
|
|
395
|
+
track = this.player.customSubtitles[this.player.currentCustomTrackIndex];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Fallback to first available track with data
|
|
399
|
+
if (!track || !track.subtitles || track.subtitles.length === 0) {
|
|
400
|
+
track = this.player.customSubtitles.find(t => t.subtitles && t.subtitles.length > 0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (track && track.subtitles && track.subtitles.length > 0) {
|
|
404
|
+
// Deep copy segments
|
|
405
|
+
this.subtitles = track.subtitles.map(s => ({ start: s.start, end: s.end, text: s.text }));
|
|
406
|
+
this.isImportedSubs = true;
|
|
407
|
+
|
|
408
|
+
if (track.language) {
|
|
409
|
+
this.opts.language = track.language;
|
|
410
|
+
this._lastDetectedLang = track.language;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Disable native player subtitles to avoid double rendering (we take control)
|
|
414
|
+
if (typeof this.player.disableSubtitles === 'function') {
|
|
415
|
+
this.player.disableSubtitles();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Load any previously cached translations for this imported subtitle
|
|
419
|
+
const cached = this._loadFromCache();
|
|
420
|
+
if (cached && cached.transCache) {
|
|
421
|
+
this._transCache = cached.transCache;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Save imported state immediately
|
|
425
|
+
this._saveToCache(this.subtitles, null, null, this._lastDetectedLang);
|
|
426
|
+
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
338
433
|
_scheduleAutoTranslation() {
|
|
339
434
|
const lang = this.opts.autoTranslation.toLowerCase().trim().slice(0, 2);
|
|
340
|
-
|
|
341
|
-
// Set translateLang immediately so chunk_done incremental translation kicks in right away
|
|
342
435
|
this.translateLang = lang;
|
|
343
436
|
|
|
344
437
|
let tries = 0;
|
|
345
438
|
const check = setInterval(() => {
|
|
346
439
|
tries++;
|
|
347
|
-
// Wait until transcription is fully complete
|
|
348
|
-
// to catch any chunks that may have been missed or arrived out of order
|
|
440
|
+
// Wait until transcription/import is fully complete
|
|
349
441
|
if (this.subtitles.length > 0 && !this.isGenerating) {
|
|
350
442
|
clearInterval(check);
|
|
351
443
|
if (this.player.options.debug) console.log('[AutoSub] autoTranslation final pass, lang:', lang);
|
|
352
|
-
|
|
444
|
+
|
|
353
445
|
this.subtitlesTrans = [];
|
|
354
|
-
this._transCache
|
|
446
|
+
// Note: We DO NOT clear this._transCache here, to preserve cached translations!
|
|
355
447
|
this._setTranslationLang(lang);
|
|
356
448
|
this._updateMenu();
|
|
357
449
|
return;
|
|
@@ -360,30 +452,81 @@
|
|
|
360
452
|
}, 1000);
|
|
361
453
|
}
|
|
362
454
|
|
|
363
|
-
|
|
364
455
|
async handleButtonClick() {
|
|
365
|
-
// If
|
|
456
|
+
// 1. If native tracks exist but haven't been imported yet, try to import them
|
|
457
|
+
if (this._checkForNativeTracks() && this.subtitles.length === 0) {
|
|
458
|
+
const imported = this._importPlayerSubtitles();
|
|
459
|
+
if (imported) {
|
|
460
|
+
this.subVisible = true;
|
|
461
|
+
this._startDisplay();
|
|
462
|
+
this._setBtnActive(true);
|
|
463
|
+
this._updateMenu();
|
|
464
|
+
if (this.player.options.debug) console.log('[AutoSub] Imported subtitles from player on click');
|
|
465
|
+
this._toggleMenu();
|
|
466
|
+
return;
|
|
467
|
+
} else {
|
|
468
|
+
// Fetch is probably still pending. Check cache just in case we already saved imported subs.
|
|
469
|
+
const cached = this._loadFromCache();
|
|
470
|
+
if (cached && cached.isImportedSubs) {
|
|
471
|
+
this.subtitles = cached.subtitles;
|
|
472
|
+
this._transCache = cached.transCache || {};
|
|
473
|
+
this.isImportedSubs = true;
|
|
474
|
+
this.subVisible = true;
|
|
475
|
+
this._startDisplay();
|
|
476
|
+
this._setBtnActive(true);
|
|
477
|
+
this._updateMenu();
|
|
478
|
+
this._toggleMenu();
|
|
479
|
+
return;
|
|
480
|
+
} else {
|
|
481
|
+
// Let the user know we are processing the native tracks
|
|
482
|
+
this._updatePanel('Reading native subtitles...', 100);
|
|
483
|
+
this._openPanel();
|
|
484
|
+
setTimeout(() => this._closePanel(), 1500);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 2. If NO native tracks, check cache for Whisper generated ones
|
|
366
491
|
if (this.subtitles.length === 0 && !this.isGenerating) {
|
|
367
492
|
const cached = this._loadFromCache();
|
|
368
493
|
if (cached) {
|
|
369
|
-
this.subtitles = cached;
|
|
494
|
+
this.subtitles = cached.subtitles;
|
|
495
|
+
this._transCache = cached.transCache || {};
|
|
496
|
+
this.isImportedSubs = cached.isImportedSubs || false;
|
|
497
|
+
|
|
498
|
+
if (this.opts.autoTranslation) {
|
|
499
|
+
const tLang = this.opts.autoTranslation.toLowerCase().trim().slice(0, 2);
|
|
500
|
+
this.translateLang = tLang;
|
|
501
|
+
if (this._transCache[tLang]) {
|
|
502
|
+
this.subtitlesTrans = [...this._transCache[tLang]];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
370
506
|
this.subVisible = true;
|
|
371
507
|
this._startDisplay();
|
|
372
508
|
this._setBtnActive(true);
|
|
373
509
|
this._updateMenu();
|
|
510
|
+
|
|
511
|
+
if (this.translateLang && this.translateLang !== 'off') {
|
|
512
|
+
this._setTranslationLang(this.translateLang);
|
|
513
|
+
}
|
|
514
|
+
|
|
374
515
|
if (this.player.options.debug)
|
|
375
|
-
console.log('[AutoSub] Loaded from cache:', cached.length, 'segments');
|
|
376
|
-
|
|
516
|
+
console.log('[AutoSub] Loaded from cache:', cached.subtitles.length, 'segments');
|
|
517
|
+
|
|
377
518
|
this._toggleMenu();
|
|
378
519
|
return;
|
|
379
520
|
}
|
|
380
521
|
}
|
|
381
|
-
|
|
522
|
+
|
|
523
|
+
// Default: Toggle the menu
|
|
382
524
|
this._toggleMenu();
|
|
383
525
|
}
|
|
384
526
|
|
|
385
527
|
async generate() {
|
|
386
|
-
if
|
|
528
|
+
// Block generation if we are already using native imported subtitles
|
|
529
|
+
if (this.isGenerating || this.isImportedSubs) return;
|
|
387
530
|
|
|
388
531
|
let partialCache = null;
|
|
389
532
|
|
|
@@ -391,6 +534,17 @@
|
|
|
391
534
|
const cached = this._loadFromCache();
|
|
392
535
|
if (cached) {
|
|
393
536
|
this.subtitles = cached.subtitles;
|
|
537
|
+
this._transCache = cached.transCache || {};
|
|
538
|
+
this.isImportedSubs = cached.isImportedSubs || false;
|
|
539
|
+
|
|
540
|
+
if (this.opts.autoTranslation) {
|
|
541
|
+
const tLang = this.opts.autoTranslation.toLowerCase().trim().slice(0, 2);
|
|
542
|
+
this.translateLang = tLang;
|
|
543
|
+
if (this._transCache[tLang]) {
|
|
544
|
+
this.subtitlesTrans = [...this._transCache[tLang]];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
394
548
|
this.subVisible = true;
|
|
395
549
|
this._startDisplay();
|
|
396
550
|
this._setBtnActive(true);
|
|
@@ -401,7 +555,7 @@
|
|
|
401
555
|
return;
|
|
402
556
|
}
|
|
403
557
|
|
|
404
|
-
partialCache = cached;
|
|
558
|
+
partialCache = cached;
|
|
405
559
|
if (this.player.options.debug)
|
|
406
560
|
console.log('[AutoSub] Partial cache found — resuming from chunk', cached.chunksCompleted, 'of', cached.totalChunks);
|
|
407
561
|
}
|
|
@@ -411,45 +565,30 @@
|
|
|
411
565
|
this._setBtnGenerating(true);
|
|
412
566
|
this._updatePanel('Extracting audio...', 5);
|
|
413
567
|
try {
|
|
414
|
-
|
|
415
|
-
this.
|
|
416
|
-
this.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
this.
|
|
430
|
-
this.
|
|
431
|
-
this.
|
|
432
|
-
|
|
433
|
-
// Re-trigger translation if one was active, using the now-complete subtitles
|
|
434
|
-
if (this.translateLang && this.translateLang !== 'off') {
|
|
435
|
-
const lang = this.translateLang;
|
|
436
|
-
this.translateLang = 'off'; // reset so setTranslationLang doesn't skip
|
|
437
|
-
this.subtitlesTrans = [];
|
|
438
|
-
this._transCache = {};
|
|
439
|
-
await this._setTranslationLang(lang); // re-translate with full subtitles
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
setTimeout(() => { this._closePanel(); }, 800);
|
|
443
|
-
|
|
444
|
-
if (this.player.options.debug) console.log('[AutoSub] Generation complete:', this.subtitles.length, 'segments');
|
|
445
|
-
} catch (err) {
|
|
446
|
-
if (this.player.options.debug) console.error('[AutoSub] Generation error:', err);
|
|
447
|
-
this._updatePanel('Error: ' + (err.message || String(err)), 0);
|
|
448
|
-
} finally {
|
|
449
|
-
this.isGenerating = false;
|
|
450
|
-
this._setBtnGenerating(false);
|
|
568
|
+
const audioData = await this._extractAudio();
|
|
569
|
+
this._updatePanel('Downloading Transformers.js bundle...', 30);
|
|
570
|
+
await this._ensureBundle();
|
|
571
|
+
this._updatePanel('Preparing Whisper worker...', 38);
|
|
572
|
+
await this._transcribeChunksStreaming(audioData, partialCache);
|
|
573
|
+
|
|
574
|
+
this.subtitles.sort((a, b) => a.start - b.start);
|
|
575
|
+
|
|
576
|
+
this._saveToCache(this.subtitles, null, null, this._lastDetectedLang || null);
|
|
577
|
+
this._updatePanel('Subtitles ready!', 100);
|
|
578
|
+
this._setBtnActive(true);
|
|
579
|
+
this._updateMenu();
|
|
580
|
+
|
|
581
|
+
// Re-trigger translation if one was active
|
|
582
|
+
if (this.translateLang && this.translateLang !== 'off') {
|
|
583
|
+
const lang = this.translateLang;
|
|
584
|
+
this.translateLang = 'off';
|
|
585
|
+
this.subtitlesTrans = [];
|
|
586
|
+
await this._setTranslationLang(lang);
|
|
451
587
|
}
|
|
452
588
|
|
|
589
|
+
setTimeout(() => { this._closePanel(); }, 800);
|
|
590
|
+
|
|
591
|
+
if (this.player.options.debug) console.log('[AutoSub] Generation complete:', this.subtitles.length, 'segments');
|
|
453
592
|
} catch (err) {
|
|
454
593
|
if (this.player.options.debug) console.error('[AutoSub] Generation error:', err);
|
|
455
594
|
this._updatePanel('Error: ' + (err.message || String(err)), 0);
|
|
@@ -469,6 +608,10 @@
|
|
|
469
608
|
dispose() {
|
|
470
609
|
this._stopDisplay();
|
|
471
610
|
this._destroyDrag();
|
|
611
|
+
if (this._resizeObserver) {
|
|
612
|
+
this._resizeObserver.disconnect();
|
|
613
|
+
this._resizeObserver = null;
|
|
614
|
+
}
|
|
472
615
|
if (this._captureTimer) { clearTimeout(this._captureTimer); this._captureTimer = null; }
|
|
473
616
|
if (this._captureProgressInterval) { clearInterval(this._captureProgressInterval); this._captureProgressInterval = null; }
|
|
474
617
|
if (this._worker) { this._worker.terminate(); this._worker = null; }
|
|
@@ -507,7 +650,10 @@
|
|
|
507
650
|
|
|
508
651
|
const text = document.createElement('div');
|
|
509
652
|
text.className = 'myetv-autosub-text';
|
|
510
|
-
|
|
653
|
+
|
|
654
|
+
const styleWithoutFont = Object.assign({}, this.opts.subtitleStyle);
|
|
655
|
+
delete styleWithoutFont.fontSize;
|
|
656
|
+
Object.assign(text.style, styleWithoutFont);
|
|
511
657
|
|
|
512
658
|
overlay.appendChild(handle);
|
|
513
659
|
overlay.appendChild(text);
|
|
@@ -517,11 +663,50 @@
|
|
|
517
663
|
this.overlayText = text;
|
|
518
664
|
|
|
519
665
|
this._initDrag(handle, overlay);
|
|
666
|
+
|
|
667
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
668
|
+
this._applyResponsiveFontSize();
|
|
669
|
+
});
|
|
670
|
+
this._resizeObserver.observe(this.container);
|
|
671
|
+
|
|
672
|
+
this._applyResponsiveFontSize();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
_applyResponsiveFontSize() {
|
|
676
|
+
if (!this.overlayText) return;
|
|
677
|
+
|
|
678
|
+
const rect = this.container.getBoundingClientRect();
|
|
679
|
+
const w = rect.width;
|
|
680
|
+
const h = rect.height;
|
|
681
|
+
|
|
682
|
+
const REFERENCE_WIDTH = 800;
|
|
683
|
+
|
|
684
|
+
let basePx = 18;
|
|
685
|
+
const userFontSize = this.opts.subtitleStyle?.fontSize;
|
|
686
|
+
if (userFontSize) {
|
|
687
|
+
const match = String(userFontSize).match(/([\d.]+)(px|em|rem)?/);
|
|
688
|
+
if (match) {
|
|
689
|
+
const val = parseFloat(match[1]);
|
|
690
|
+
const unit = match[2] || 'px';
|
|
691
|
+
if (unit === 'px') basePx = val;
|
|
692
|
+
else if (unit === 'em' || unit === 'rem') basePx = val * 16;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let size = basePx * (w / REFERENCE_WIDTH);
|
|
697
|
+
|
|
698
|
+
if (w < 260 || h < 150) {
|
|
699
|
+
size = Math.min(size, 8);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
size = Math.max(size, 8);
|
|
703
|
+
size = Math.min(size, basePx);
|
|
704
|
+
|
|
705
|
+
this.overlayText.style.fontSize = size + 'px';
|
|
520
706
|
}
|
|
521
707
|
|
|
522
708
|
_initDrag(handle, overlay) {
|
|
523
709
|
const onStart = (e) => {
|
|
524
|
-
// Prevent conflicts with player controls
|
|
525
710
|
e.stopPropagation();
|
|
526
711
|
const isTouch = e.type === 'touchstart';
|
|
527
712
|
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
|
@@ -530,7 +715,6 @@
|
|
|
530
715
|
const rect = overlay.getBoundingClientRect();
|
|
531
716
|
const contRect = this.container.getBoundingClientRect();
|
|
532
717
|
|
|
533
|
-
// Switch to absolute positioning on first drag
|
|
534
718
|
if (!this._overlayPos) {
|
|
535
719
|
const currentLeft = rect.left - contRect.left + rect.width / 2;
|
|
536
720
|
const currentTop = rect.top - contRect.top + rect.height / 2;
|
|
@@ -583,7 +767,9 @@
|
|
|
583
767
|
};
|
|
584
768
|
|
|
585
769
|
handle.addEventListener('mousedown', onStart);
|
|
770
|
+
overlay.addEventListener('touchstart', onStart, { passive: false });
|
|
586
771
|
handle.addEventListener('touchstart', onStart, { passive: false });
|
|
772
|
+
|
|
587
773
|
this._drag._onEnd = onEnd;
|
|
588
774
|
}
|
|
589
775
|
|
|
@@ -646,7 +832,6 @@
|
|
|
646
832
|
_createButton() {
|
|
647
833
|
const position = this.opts.position;
|
|
648
834
|
|
|
649
|
-
// Button wrapper only (menu is appended directly to body)
|
|
650
835
|
const wrap = document.createElement('div');
|
|
651
836
|
wrap.style.cssText = 'position:relative;display:inline-flex;align-items:center;';
|
|
652
837
|
|
|
@@ -662,13 +847,11 @@
|
|
|
662
847
|
|
|
663
848
|
wrap.appendChild(btn);
|
|
664
849
|
|
|
665
|
-
// Menu is appended directly to body — escapes any stacking context from the player
|
|
666
850
|
const menu = document.createElement('div');
|
|
667
851
|
menu.className = 'myetv-autosub-menu';
|
|
668
852
|
menu.addEventListener('click', (e) => e.stopPropagation());
|
|
669
853
|
document.body.appendChild(menu);
|
|
670
854
|
|
|
671
|
-
// Insert button into the player DOM
|
|
672
855
|
if (position === 'topbar') {
|
|
673
856
|
const settingsControl = this.container.querySelector('.settings-control');
|
|
674
857
|
if (settingsControl) settingsControl.insertBefore(wrap, settingsControl.firstChild);
|
|
@@ -692,7 +875,6 @@
|
|
|
692
875
|
this.btnEl = btn;
|
|
693
876
|
this.menuEl = menu;
|
|
694
877
|
|
|
695
|
-
// Close menu when clicking outside
|
|
696
878
|
this._menuOutsideHandler = (e) => {
|
|
697
879
|
if (!menu.contains(e.target) && e.target !== btn) {
|
|
698
880
|
this._closeMenuNow();
|
|
@@ -700,7 +882,6 @@
|
|
|
700
882
|
};
|
|
701
883
|
document.addEventListener('click', this._menuOutsideHandler);
|
|
702
884
|
|
|
703
|
-
// Close menu on Escape key
|
|
704
885
|
this._menuEscHandler = (e) => {
|
|
705
886
|
if (e.key === 'Escape') this._closeMenuNow();
|
|
706
887
|
};
|
|
@@ -718,10 +899,8 @@
|
|
|
718
899
|
return;
|
|
719
900
|
}
|
|
720
901
|
|
|
721
|
-
// Rebuild menu content
|
|
722
902
|
this._buildMenu();
|
|
723
903
|
|
|
724
|
-
// Position before showing (measure offset first)
|
|
725
904
|
this.menuEl.style.visibility = 'hidden';
|
|
726
905
|
this.menuEl.style.display = 'block';
|
|
727
906
|
|
|
@@ -731,14 +910,12 @@
|
|
|
731
910
|
const vpH = window.innerHeight;
|
|
732
911
|
const vpW = window.innerWidth;
|
|
733
912
|
|
|
734
|
-
// Open above or below depending on available space
|
|
735
913
|
const spaceAbove = btnRect.top - 8;
|
|
736
914
|
const spaceBelow = vpH - btnRect.bottom - 8;
|
|
737
915
|
let top = (spaceAbove >= menuH || spaceAbove >= spaceBelow)
|
|
738
916
|
? btnRect.top - menuH - 8
|
|
739
917
|
: btnRect.bottom + 8;
|
|
740
918
|
|
|
741
|
-
// Align to the right edge of the button, clamped to viewport
|
|
742
919
|
let left = btnRect.right - menuW;
|
|
743
920
|
left = Math.max(4, Math.min(vpW - menuW - 4, left));
|
|
744
921
|
top = Math.max(4, Math.min(vpH - menuH - 4, top));
|
|
@@ -783,27 +960,29 @@
|
|
|
783
960
|
sec1.appendChild(itemOff);
|
|
784
961
|
this.menuEl.appendChild(sec1);
|
|
785
962
|
|
|
786
|
-
// ── SECTION 2: GENERATE / PANEL
|
|
787
|
-
|
|
963
|
+
// ── SECTION 2: GENERATE / PANEL (Only if NOT using native imported subs)
|
|
964
|
+
if (!this.isImportedSubs) {
|
|
965
|
+
const sec2 = this._menuSection('Management');
|
|
788
966
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
967
|
+
const genLabel = isGenerating ? '⏳ Generating...' : hasSubs ? '🔄 Regenerate transcription' : '🎙️ Generate subtitles';
|
|
968
|
+
const itemGen = this._menuItem(genLabel, false, isGenerating, () => {
|
|
969
|
+
this._deleteCache();
|
|
970
|
+
this.subtitles = [];
|
|
971
|
+
this.subtitlesTrans = [];
|
|
972
|
+
this._transCache = {};
|
|
973
|
+
this._closeMenuNow();
|
|
974
|
+
this.generate();
|
|
975
|
+
});
|
|
798
976
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
977
|
+
const itemPanel = this._menuItem('📊 Show progress', false, !isGenerating, () => {
|
|
978
|
+
this._closeMenuNow();
|
|
979
|
+
this._openPanel();
|
|
980
|
+
});
|
|
803
981
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
982
|
+
sec2.appendChild(itemGen);
|
|
983
|
+
if (isGenerating) sec2.appendChild(itemPanel);
|
|
984
|
+
this.menuEl.appendChild(sec2);
|
|
985
|
+
}
|
|
807
986
|
|
|
808
987
|
// ── SECTION 3: TRANSLATION ────────────────────────────────────────────
|
|
809
988
|
if (hasSubs) {
|
|
@@ -884,7 +1063,6 @@
|
|
|
884
1063
|
return;
|
|
885
1064
|
}
|
|
886
1065
|
|
|
887
|
-
// Skip if source language equals target language
|
|
888
1066
|
const sourceLang = this._resolveLanguage(this.opts.language);
|
|
889
1067
|
if (sourceLang && sourceLang === langCode.slice(0, 2)) {
|
|
890
1068
|
if (this.player.options.debug) console.log('[AutoSub] Source = target lang, skipping translation');
|
|
@@ -893,7 +1071,6 @@
|
|
|
893
1071
|
return;
|
|
894
1072
|
}
|
|
895
1073
|
|
|
896
|
-
// Already cached in memory?
|
|
897
1074
|
if (this._transCache[langCode]) {
|
|
898
1075
|
this.subtitlesTrans = this._transCache[langCode];
|
|
899
1076
|
if (this.player.options.debug) console.log('[AutoSub] Translation cache hit:', langCode);
|
|
@@ -904,10 +1081,13 @@
|
|
|
904
1081
|
this._setBtnGenerating(true);
|
|
905
1082
|
|
|
906
1083
|
try {
|
|
907
|
-
// Batch segments to minimize API requests (max ~400 chars per request)
|
|
908
1084
|
const translated = await this._translateBatch(this.subtitles, langCode);
|
|
909
1085
|
this.subtitlesTrans = translated;
|
|
910
1086
|
this._transCache[langCode] = translated;
|
|
1087
|
+
|
|
1088
|
+
// Save translations persistently to cache
|
|
1089
|
+
this._saveToCache(this.subtitles, null, null, this._lastDetectedLang);
|
|
1090
|
+
|
|
911
1091
|
if (this.player.options.debug) console.log('[AutoSub] Translation done:', translated.length, 'segments');
|
|
912
1092
|
} catch (err) {
|
|
913
1093
|
if (this.player.options.debug) console.warn('[AutoSub] Translation error:', err.message);
|
|
@@ -921,7 +1101,6 @@
|
|
|
921
1101
|
async _translateText(text, sourceLang, targetLang) {
|
|
922
1102
|
const engine = this.opts.translationEngine;
|
|
923
1103
|
|
|
924
|
-
// ── MyMemory (default) ──────────────────────────────────────────────
|
|
925
1104
|
if (!engine || engine.type === 'mymemory') {
|
|
926
1105
|
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${sourceLang}|${targetLang}`;
|
|
927
1106
|
const resp = await fetch(url);
|
|
@@ -932,7 +1111,6 @@
|
|
|
932
1111
|
return json.responseData?.translatedText ?? text;
|
|
933
1112
|
}
|
|
934
1113
|
|
|
935
|
-
// ── LibreTranslate ──────────────────────────────────────────────────
|
|
936
1114
|
if (engine.type === 'libretranslate') {
|
|
937
1115
|
const body = { q: text, source: sourceLang, target: targetLang, format: 'text' };
|
|
938
1116
|
if (engine.apiKey) body.api_key = engine.apiKey;
|
|
@@ -947,7 +1125,6 @@
|
|
|
947
1125
|
return json.translatedText ?? text;
|
|
948
1126
|
}
|
|
949
1127
|
|
|
950
|
-
// ── MarianMT (Argos Translate API / custom REST endpoint) ───────────
|
|
951
1128
|
if (engine.type === 'marianmt') {
|
|
952
1129
|
const resp = await fetch(engine.url.replace(/\/$/, '') + '/translate', {
|
|
953
1130
|
method: 'POST',
|
|
@@ -966,7 +1143,6 @@
|
|
|
966
1143
|
const SEP = ' ||| ';
|
|
967
1144
|
const BATCH_CHARS = 400;
|
|
968
1145
|
|
|
969
|
-
// Detect source language
|
|
970
1146
|
let sourceLang = null;
|
|
971
1147
|
if (this.opts.language) {
|
|
972
1148
|
const lower = this.opts.language.toLowerCase().trim();
|
|
@@ -985,7 +1161,6 @@
|
|
|
985
1161
|
}
|
|
986
1162
|
if (!sourceLang) sourceLang = 'en';
|
|
987
1163
|
|
|
988
|
-
// Normalize target to 2-letter code
|
|
989
1164
|
const normTarget = LANGUAGE_MAP[targetLang]
|
|
990
1165
|
? targetLang
|
|
991
1166
|
: (Object.entries(LANGUAGE_MAP).find(([k, v]) => v === targetLang)?.[0] ?? targetLang.slice(0, 2));
|
|
@@ -999,17 +1174,18 @@
|
|
|
999
1174
|
|
|
1000
1175
|
const engine = this.opts.translationEngine;
|
|
1001
1176
|
|
|
1002
|
-
// ── LibreTranslate: array nativo, mapping 1:1 garantito ──────────
|
|
1003
1177
|
if (engine?.type === 'libretranslate') {
|
|
1004
1178
|
const BATCH_SIZE = 50;
|
|
1005
1179
|
const result = [];
|
|
1006
|
-
|
|
1180
|
+
|
|
1181
|
+
let url = engine.url.replace(/\/$/, '');
|
|
1182
|
+
if (!url.endsWith('/translate') && !url.includes('.php')) url += '/translate';
|
|
1007
1183
|
|
|
1008
1184
|
for (let i = 0; i < segments.length; i += BATCH_SIZE) {
|
|
1009
1185
|
const batch = segments.slice(i, i + BATCH_SIZE);
|
|
1010
1186
|
try {
|
|
1011
1187
|
const body = {
|
|
1012
|
-
q: batch.map(s => s.text),
|
|
1188
|
+
q: batch.map(s => s.text),
|
|
1013
1189
|
source: sourceLang,
|
|
1014
1190
|
target: normTarget,
|
|
1015
1191
|
format: 'text'
|
|
@@ -1044,7 +1220,6 @@
|
|
|
1044
1220
|
return result;
|
|
1045
1221
|
}
|
|
1046
1222
|
|
|
1047
|
-
// ── MyMemory fallback ────────────────────────────────────────────
|
|
1048
1223
|
const batches = [];
|
|
1049
1224
|
let current = [], charCount = 0;
|
|
1050
1225
|
for (const seg of segments) {
|
|
@@ -1074,13 +1249,13 @@
|
|
|
1074
1249
|
return result;
|
|
1075
1250
|
}
|
|
1076
1251
|
|
|
1077
|
-
|
|
1078
1252
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1079
1253
|
// PRIVATE — DISPLAY
|
|
1080
1254
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1081
1255
|
|
|
1082
1256
|
_startDisplay() {
|
|
1083
1257
|
this._stopDisplay();
|
|
1258
|
+
this._applyResponsiveFontSize();
|
|
1084
1259
|
this.updateInterval = setInterval(() => this._tick(), 80);
|
|
1085
1260
|
}
|
|
1086
1261
|
|
|
@@ -1096,7 +1271,6 @@
|
|
|
1096
1271
|
_tick() {
|
|
1097
1272
|
if (!this.subVisible) return;
|
|
1098
1273
|
|
|
1099
|
-
// Use translated segments if available, otherwise fall back to originals
|
|
1100
1274
|
const pool = (this.translateLang !== 'off' && this.subtitlesTrans.length > 0)
|
|
1101
1275
|
? this.subtitlesTrans
|
|
1102
1276
|
: this.subtitles;
|
|
@@ -1133,7 +1307,6 @@
|
|
|
1133
1307
|
|
|
1134
1308
|
_resolveMpdUrl() {
|
|
1135
1309
|
try {
|
|
1136
|
-
// Salvato sul container dal player al momento di initialize()
|
|
1137
1310
|
const url = this.container.dataset.mpdUrl || this.video.dataset.mpdUrl || null;
|
|
1138
1311
|
if (url && !url.startsWith('blob:')) return url;
|
|
1139
1312
|
} catch (_) { }
|
|
@@ -1158,7 +1331,6 @@
|
|
|
1158
1331
|
if (this.player.options.debug)
|
|
1159
1332
|
console.log('[AutoSub] _extractAudio — currentSrc:', this.video.currentSrc, '| adaptiveStreamingType:', this.player.adaptiveStreamingType);
|
|
1160
1333
|
|
|
1161
|
-
// ── DASH: resolve real MPD URL via dash.js instance ───────────────────
|
|
1162
1334
|
const mpdUrl = this._resolveMpdUrl();
|
|
1163
1335
|
if (mpdUrl) {
|
|
1164
1336
|
if (this.player.options.debug) console.log('[AutoSub] DASH mode — mpdUrl:', mpdUrl);
|
|
@@ -1169,13 +1341,11 @@
|
|
|
1169
1341
|
}
|
|
1170
1342
|
}
|
|
1171
1343
|
|
|
1172
|
-
// ── HLS ───────────────────────────────────────────────────────────────
|
|
1173
1344
|
const src = this.video.currentSrc || this.video.src || '';
|
|
1174
1345
|
if (src.includes('.m3u8') || this.player.adaptiveStreamingType === 'hls') {
|
|
1175
1346
|
return await this._extractAudioFromCaptureStream();
|
|
1176
1347
|
}
|
|
1177
1348
|
|
|
1178
|
-
// ── Normal MP4 / WebM — direct fetch ─────────────────────────────────
|
|
1179
1349
|
try { return await this._extractAudioFromFetch(); }
|
|
1180
1350
|
catch (err) {
|
|
1181
1351
|
if (this.player.options.debug) console.warn('[AutoSub] Fetch strategy failed:', err.message, '— fallback to captureStream');
|
|
@@ -1193,7 +1363,6 @@
|
|
|
1193
1363
|
const parser = new DOMParser();
|
|
1194
1364
|
const mpdDoc = parser.parseFromString(mpdText, 'application/xml');
|
|
1195
1365
|
|
|
1196
|
-
// Check for XML parse errors
|
|
1197
1366
|
const parseErr = mpdDoc.querySelector('parsererror');
|
|
1198
1367
|
if (parseErr) throw new Error('MPD XML parse error: ' + parseErr.textContent.slice(0, 100));
|
|
1199
1368
|
|
|
@@ -1213,7 +1382,6 @@
|
|
|
1213
1382
|
|
|
1214
1383
|
this._updatePanel('Downloading audio segments (0/' + segmentUrls.length + ')...', 12);
|
|
1215
1384
|
|
|
1216
|
-
// Download segments in parallel batches of 5
|
|
1217
1385
|
const BATCH = 5;
|
|
1218
1386
|
const segmentBuffers = new Array(segmentUrls.length);
|
|
1219
1387
|
|
|
@@ -1233,7 +1401,6 @@
|
|
|
1233
1401
|
);
|
|
1234
1402
|
}
|
|
1235
1403
|
|
|
1236
|
-
// Concatenate all segment buffers into one
|
|
1237
1404
|
this._updatePanel('Decoding audio...', 30);
|
|
1238
1405
|
const totalBytes = segmentBuffers.reduce((s, b) => s + b.byteLength, 0);
|
|
1239
1406
|
const combined = new Uint8Array(totalBytes);
|
|
@@ -1269,13 +1436,11 @@
|
|
|
1269
1436
|
|
|
1270
1437
|
_findAudioAdaptationSet(mpdDoc) {
|
|
1271
1438
|
const sets = Array.from(mpdDoc.querySelectorAll('AdaptationSet'));
|
|
1272
|
-
// Prefer explicit audio contentType or mimeType
|
|
1273
1439
|
return sets.find(s =>
|
|
1274
1440
|
s.getAttribute('contentType') === 'audio' ||
|
|
1275
1441
|
(s.getAttribute('mimeType') || '').startsWith('audio') ||
|
|
1276
1442
|
s.querySelector('Representation[mimeType^="audio"]') !== null
|
|
1277
1443
|
) || sets.find(s =>
|
|
1278
|
-
// Fallback: AdaptationSet that is not video
|
|
1279
1444
|
!(s.getAttribute('mimeType') || '').startsWith('video') &&
|
|
1280
1445
|
s.getAttribute('contentType') !== 'video'
|
|
1281
1446
|
);
|
|
@@ -1294,7 +1459,6 @@
|
|
|
1294
1459
|
_buildSegmentUrlList(mpdDoc, adaptationSet, representation, baseUrl) {
|
|
1295
1460
|
const urls = [];
|
|
1296
1461
|
|
|
1297
|
-
// ── SegmentList ──────────────────────────────────────────────────────
|
|
1298
1462
|
const segList = representation.querySelector('SegmentList')
|
|
1299
1463
|
|| adaptationSet.querySelector('SegmentList');
|
|
1300
1464
|
if (segList) {
|
|
@@ -1310,7 +1474,6 @@
|
|
|
1310
1474
|
return urls;
|
|
1311
1475
|
}
|
|
1312
1476
|
|
|
1313
|
-
// ── SegmentTemplate ──────────────────────────────────────────────────
|
|
1314
1477
|
const segTpl = representation.querySelector('SegmentTemplate')
|
|
1315
1478
|
|| adaptationSet.querySelector('SegmentTemplate');
|
|
1316
1479
|
if (segTpl) {
|
|
@@ -1345,7 +1508,6 @@
|
|
|
1345
1508
|
return urls;
|
|
1346
1509
|
}
|
|
1347
1510
|
|
|
1348
|
-
// ── BaseURL (single audio file) ───────────────────────────────────────
|
|
1349
1511
|
const baseURLEl = representation.querySelector('BaseURL')
|
|
1350
1512
|
|| adaptationSet.querySelector('BaseURL');
|
|
1351
1513
|
if (baseURLEl) {
|
|
@@ -1363,7 +1525,6 @@
|
|
|
1363
1525
|
}
|
|
1364
1526
|
|
|
1365
1527
|
_parseDuration(iso) {
|
|
1366
|
-
// Parse ISO 8601 duration: PT1H2M3.5S
|
|
1367
1528
|
const m = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?/);
|
|
1368
1529
|
if (!m) return 0;
|
|
1369
1530
|
return (parseFloat(m[1] || 0) * 3600) +
|
|
@@ -1437,7 +1598,6 @@
|
|
|
1437
1598
|
if (e.data.size === 0) return;
|
|
1438
1599
|
chunkQueue.push(e.data);
|
|
1439
1600
|
|
|
1440
|
-
// Estimate accumulated seconds using rough bitrate of 16 kbps (opus)
|
|
1441
1601
|
const totalSize = chunkQueue.reduce((s, b) => s + b.size, 0);
|
|
1442
1602
|
const estimatedSec = totalSize / (16000 / 8);
|
|
1443
1603
|
|
|
@@ -1451,7 +1611,6 @@
|
|
|
1451
1611
|
};
|
|
1452
1612
|
|
|
1453
1613
|
recorder.onstop = async () => {
|
|
1454
|
-
// Flush any remaining audio as the last chunk
|
|
1455
1614
|
if (chunkQueue.length > 0 && this._onAudioChunkReady) {
|
|
1456
1615
|
const blob = new Blob([...chunkQueue], { type: recorder.mimeType || 'audio/webm' });
|
|
1457
1616
|
chunkQueue.length = 0;
|
|
@@ -1463,7 +1622,6 @@
|
|
|
1463
1622
|
|
|
1464
1623
|
recorder.onerror = e => reject(e.error || new Error('MediaRecorder error'));
|
|
1465
1624
|
|
|
1466
|
-
// Update capture progress based on video currentTime
|
|
1467
1625
|
const duration = isFinite(this.video.duration) && this.video.duration > 0
|
|
1468
1626
|
? this.video.duration : null;
|
|
1469
1627
|
|
|
@@ -1487,10 +1645,8 @@
|
|
|
1487
1645
|
|
|
1488
1646
|
this.video.addEventListener('ended', stop, { once: true });
|
|
1489
1647
|
|
|
1490
|
-
// Slice every 2s so ondataavailable fires frequently
|
|
1491
1648
|
recorder.start(2000);
|
|
1492
1649
|
|
|
1493
|
-
// Resolve immediately with sentinel — actual transcription flows via callbacks
|
|
1494
1650
|
resolve({ _captureMode: true, stop });
|
|
1495
1651
|
});
|
|
1496
1652
|
}
|
|
@@ -1544,14 +1700,12 @@ self.onmessage = async function(e) {
|
|
|
1544
1700
|
const opts = { return_timestamps: true, task: 'transcribe', chunk_length_s: payload.chunkSec };
|
|
1545
1701
|
if (payload.lang) opts.language = payload.lang;
|
|
1546
1702
|
const result = await transcriber(audio, opts);
|
|
1547
|
-
// Include detected language in the response so the main thread can reuse it
|
|
1548
1703
|
const detectedLang = result.language || payload.lang || null;
|
|
1549
1704
|
self.postMessage({ type: 'chunk_done', payload: { result, timeOffset: payload.timeOffset, chunkIndex: payload.chunkIndex, totalChunks: payload.totalChunks, isLast: payload.isLast, detectedLang } });
|
|
1550
1705
|
} catch(err) {
|
|
1551
1706
|
self.postMessage({ type: 'error', payload: err.message || String(err) });
|
|
1552
1707
|
}
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1708
|
+
}
|
|
1555
1709
|
};
|
|
1556
1710
|
`;
|
|
1557
1711
|
|
|
@@ -1578,8 +1732,6 @@ self.onmessage = async function(e) {
|
|
|
1578
1732
|
}
|
|
1579
1733
|
|
|
1580
1734
|
async _transcribeChunksStreaming(audioData, partialCache = null) {
|
|
1581
|
-
|
|
1582
|
-
// ── CAPTURE STREAM MODE (HLS or DASH fallback) ───────────────────────
|
|
1583
1735
|
if (audioData?._captureMode) {
|
|
1584
1736
|
const SAMPLE_RATE = 16000;
|
|
1585
1737
|
const lang = this._resolveLanguage(this.opts.language);
|
|
@@ -1587,7 +1739,6 @@ self.onmessage = async function(e) {
|
|
|
1587
1739
|
|
|
1588
1740
|
if (!this.subVisible) { this.subVisible = true; this._startDisplay(); }
|
|
1589
1741
|
|
|
1590
|
-
// Worker starts in parallel while audio is still being captured
|
|
1591
1742
|
const worker = this._createTranscriberWorker();
|
|
1592
1743
|
this._worker = worker;
|
|
1593
1744
|
|
|
@@ -1716,7 +1867,6 @@ self.onmessage = async function(e) {
|
|
|
1716
1867
|
return promise;
|
|
1717
1868
|
}
|
|
1718
1869
|
|
|
1719
|
-
// ── NORMAL MODE (Float32Array from fetch or DASH segment download) ────
|
|
1720
1870
|
const float32Audio = audioData;
|
|
1721
1871
|
const SAMPLE_RATE = 16000;
|
|
1722
1872
|
const CHUNK_SEC = 30;
|
|
@@ -1726,7 +1876,6 @@ self.onmessage = async function(e) {
|
|
|
1726
1876
|
let detectedLang = lang || partialCache?.detectedLang || null;
|
|
1727
1877
|
const modelId = WHISPER_MODELS[this.opts.modelSize] || WHISPER_MODELS.base;
|
|
1728
1878
|
|
|
1729
|
-
// Use partialCache passed from generate() — no second localStorage read
|
|
1730
1879
|
const startChunk = (partialCache?.chunksCompleted != null && partialCache.chunksCompleted < totalChunks)
|
|
1731
1880
|
? partialCache.chunksCompleted
|
|
1732
1881
|
: 0;
|
|
@@ -1766,7 +1915,6 @@ self.onmessage = async function(e) {
|
|
|
1766
1915
|
if (type === 'chunk_done') {
|
|
1767
1916
|
const { result, timeOffset, chunkIndex } = payload;
|
|
1768
1917
|
|
|
1769
|
-
// On first processed chunk, save the detected language and reuse it for all subsequent chunks
|
|
1770
1918
|
if (chunkIndex === startChunk && payload.detectedLang && !detectedLang) {
|
|
1771
1919
|
detectedLang = payload.detectedLang;
|
|
1772
1920
|
if (this.player.options.debug) console.log('[AutoSub] Language auto-detected:', detectedLang);
|
|
@@ -1788,20 +1936,21 @@ self.onmessage = async function(e) {
|
|
|
1788
1936
|
const normalized = this._normalizeChunks(rawChunks);
|
|
1789
1937
|
this.subtitles.push(...normalized);
|
|
1790
1938
|
|
|
1791
|
-
// Save partial progress after every chunk
|
|
1792
1939
|
this._saveToCache(this.subtitles, chunkIndex + 1, totalChunks, detectedLang);
|
|
1793
1940
|
|
|
1794
|
-
// If translation is active, translate this chunk immediately and append
|
|
1795
1941
|
if (this.translateLang && this.translateLang !== 'off' && normalized.length > 0) {
|
|
1796
1942
|
const currentLang = this.translateLang;
|
|
1797
1943
|
this._translateBatch(normalized, currentLang).then(translated => {
|
|
1798
|
-
// Only append if translation lang hasn't changed in the meantime
|
|
1799
1944
|
if (this.translateLang === currentLang) {
|
|
1800
1945
|
this.subtitlesTrans.push(...translated);
|
|
1801
1946
|
this.subtitlesTrans.sort((a, b) => a.start - b.start);
|
|
1947
|
+
|
|
1948
|
+
if (!this._transCache[currentLang]) this._transCache[currentLang] = [];
|
|
1949
|
+
this._transCache[currentLang].push(...translated);
|
|
1950
|
+
this._transCache[currentLang].sort((a, b) => a.start - b.start);
|
|
1951
|
+
this._saveToCache(this.subtitles, chunkIndex + 1, totalChunks, detectedLang);
|
|
1802
1952
|
}
|
|
1803
1953
|
}).catch(() => {
|
|
1804
|
-
// On error push originals so display doesn't go blank
|
|
1805
1954
|
if (this.translateLang === currentLang) {
|
|
1806
1955
|
this.subtitlesTrans.push(...normalized);
|
|
1807
1956
|
this.subtitlesTrans.sort((a, b) => a.start - b.start);
|
|
@@ -1814,17 +1963,13 @@ self.onmessage = async function(e) {
|
|
|
1814
1963
|
if (next < totalChunks) {
|
|
1815
1964
|
const pct = 64 + Math.round((next / totalChunks) * 33);
|
|
1816
1965
|
this._updatePanel('Transcribing chunk ' + (next + 1) + ' of ' + totalChunks + '...', pct);
|
|
1817
|
-
// Use detectedLang instead of lang so all chunks use the same detected language
|
|
1818
1966
|
this._sendChunk(worker, float32Audio, next, chunkSize, totalChunks, detectedLang, CHUNK_SEC);
|
|
1819
1967
|
} else {
|
|
1820
1968
|
if (this.player.options.debug) console.log('[AutoSub] All chunks done, subtitles:', this.subtitles.length);
|
|
1821
1969
|
worker.terminate(); this._worker = null;
|
|
1822
1970
|
resolve();
|
|
1823
|
-
// generate() chiamerà _saveToCache(subtitles, null, null, detectedLang)
|
|
1824
|
-
// ma detectedLang non è accessibile lì — salvalo su this
|
|
1825
1971
|
this._lastDetectedLang = detectedLang;
|
|
1826
1972
|
}
|
|
1827
|
-
|
|
1828
1973
|
}
|
|
1829
1974
|
|
|
1830
1975
|
if (type === 'error') {
|
|
@@ -1881,7 +2026,6 @@ self.onmessage = async function(e) {
|
|
|
1881
2026
|
const safe = String(this.opts.idCache).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1882
2027
|
return CACHE_PREFIX + safe;
|
|
1883
2028
|
}
|
|
1884
|
-
// Use the MPD URL or player.options.src as stable key — currentSrc may be a blob URL
|
|
1885
2029
|
const src = this._resolveMpdUrl()
|
|
1886
2030
|
|| this.player.options?.src
|
|
1887
2031
|
|| this.video.currentSrc
|
|
@@ -1909,7 +2053,9 @@ self.onmessage = async function(e) {
|
|
|
1909
2053
|
subtitles: parsed.subtitles,
|
|
1910
2054
|
chunksCompleted: parsed.chunksCompleted ?? null,
|
|
1911
2055
|
totalChunks: parsed.totalChunks ?? null,
|
|
1912
|
-
detectedLang: parsed.detectedLang ?? null
|
|
2056
|
+
detectedLang: parsed.detectedLang ?? null,
|
|
2057
|
+
transCache: parsed.transCache || {},
|
|
2058
|
+
isImportedSubs: parsed.isImportedSubs || false
|
|
1913
2059
|
};
|
|
1914
2060
|
} catch {
|
|
1915
2061
|
return null;
|
|
@@ -1925,6 +2071,8 @@ self.onmessage = async function(e) {
|
|
|
1925
2071
|
chunksCompleted,
|
|
1926
2072
|
totalChunks,
|
|
1927
2073
|
detectedLang,
|
|
2074
|
+
transCache: this._transCache || {},
|
|
2075
|
+
isImportedSubs: this.isImportedSubs || false,
|
|
1928
2076
|
savedAt: Date.now()
|
|
1929
2077
|
});
|
|
1930
2078
|
try {
|
|
@@ -1980,4 +2128,4 @@ self.onmessage = async function(e) {
|
|
|
1980
2128
|
};
|
|
1981
2129
|
|
|
1982
2130
|
register();
|
|
1983
|
-
})();
|
|
2131
|
+
})();
|