myetv-player 1.7.0 → 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.
@@ -1,19 +1,20 @@
1
1
  # myetv-autosub-plugin
2
2
 
3
- > 🎙️ Auto-subtitles plugin for **myetv-player** — client-side speech recognition via [Whisper AI](https://huggingface.co/Xenova/whisper-tiny) + [Transformers.js](https://github.com/huggingface/transformers.js), no server, no FFmpeg, no API key required.
3
+ > 🎙️ Auto-subtitles plugin for **myetv-player** — client-side speech recognition via [Whisper AI](https://huggingface.co/Xenova/whisper-tiny) + [Transformers.js](https://github.com/huggingface/transformers.js), no server, no FFmpeg, no API key required. Now with **Smart Hybrid Mode** to support native `.vtt` and `.srt` files!
4
4
 
5
5
  ---
6
6
 
7
7
  ## ✨ Features
8
8
 
9
- - 🧠 **100% client-side** — transcription runs in a Web Worker, never leaves the browser
10
- - **Streaming transcription** — subtitles appear chunk by chunk while the model processes audio
11
- - 🌍 **Auto-translation** — translate subtitles into 17+ languages via [MyMemory API](https://mymemory.translated.net/) (free, no key)
12
- - 🖱️ **Draggable subtitles** — reposition subtitles anywhere on the video (mouse & touch)
13
- - 📱 **Responsive text** — font size adapts automatically to screen size via `clamp()`
14
- - 💾 **localStorage cache** — transcriptions are cached and reloaded instantly on next visit
15
- - 🔌 **Zero dependencies** — works with the standard myetv-player plugin system
16
- - 🔒 **CSP-safe** — compatible with strict Content Security Policies (`worker-src blob:`)
9
+ - 🎯 **Smart Hybrid Mode** — Automatically detects existing `.vtt` or `.srt` tracks from the player, bypassing AI generation to save bandwidth while taking over custom rendering and translation.
10
+ - 🧠 **100% client-side** — AI transcription runs in a Web Worker, never leaves the browser.
11
+ - **Streaming transcription** — subtitles appear chunk by chunk while the model processes audio.
12
+ - 🌍 **Auto-translation** — translate subtitles (both AI-generated and native imported ones) via [MyMemory API](https://mymemory.translated.net/) (free, no key) or self-hosted [LibreTranslate](https://libretranslate.com/) / MarianMT (no limits).
13
+ - 🖱️ **Draggable subtitles** — reposition subtitles anywhere on the video (mouse & touch).
14
+ - 📱 **Responsive text** — font size adapts automatically to screen size via `clamp()`.
15
+ - 💾 **Persistent Cache** — transcriptions **and translations** are cached in `localStorage` and reloaded instantly on the next visit.
16
+ - 🔌 **Zero dependencies** — works with the standard myetv-player plugin system.
17
+ - 🔒 **CSP-safe** — compatible with strict Content Security Policies (`worker-src blob:`).
17
18
 
18
19
  ---
19
20
 
@@ -21,18 +22,15 @@
21
22
 
22
23
  ### Option A — Script tag
23
24
  ```html
24
- ```
25
-
26
25
  <script src="myetv-autosub-plugin.js"></script>
27
26
 
28
27
  ```
29
- ```
30
-
31
28
 
32
29
  ### Option B — Via npm (if bundled with myetv-player)
33
30
 
34
31
  ```bash
35
32
  npm install myetv-autosub-plugin
33
+
36
34
  ```
37
35
 
38
36
  > ⚠️ The plugin must be loaded **after** `myetv-player`.
@@ -42,52 +40,125 @@ npm install myetv-autosub-plugin
42
40
  ## 🚀 Quick Start
43
41
 
44
42
  ```html
45
- <!-- 1. Load the player -->
46
- ```
47
-
48
43
  <script src="myetv-player.min.js"></script>
49
44
 
50
- ```
51
-
52
- <!-- 2. Load the plugin -->
53
- ```
54
-
55
45
  <script src="myetv-autosub-plugin.js"></script>
56
46
 
57
- ```
58
-
59
- <!-- 3. Initialize -->
60
47
  <script>
61
48
  const player = new MYETVvideoplayer('my-video', { /* player options */ });
62
49
 
63
50
  player.on('playerready', () => {
64
51
  player.usePlugin('autoSubtitles', {
65
- language: 'en', // source language of the video
52
+ language: 'en', // source language of the video
66
53
  modelSize: 'tiny', // whisper model size
67
54
  position: 'topbar', // button position
68
55
  idCache: 'video-123', // stable cache key (recommended)
69
56
  });
70
57
  });
71
58
  </script>
72
- ```
73
59
 
60
+ ```
74
61
 
75
62
  ---
76
63
 
77
64
  ## ⚙️ Options
78
65
 
79
66
  | Option | Type | Default | Description |
80
- | :-- | :-- | :-- | :-- |
81
- | `language` | `string` | `null` | Source language of the video audio. Accepts ISO codes (`'it'`, `'en'`) or full names (`'italian'`, `'english'`). If `null`, Whisper auto-detects. |
67
+ | --- | --- | --- | --- |
68
+ | `language` | `string` | `null` | Source language of the video audio. Accepts ISO codes (`'it'`, `'en'`) or full names (`'italian'`, `'english'`). If `null`, Whisper auto-detects. *(If native tracks are detected, inherits the track language automatically).* |
82
69
  | `modelSize` | `string` | `'base'` | Whisper model size: `'tiny'` (~39 MB), `'base'` (~74 MB), `'small'` (~244 MB). Larger = more accurate but slower. |
83
- | `autoGenerate` | `boolean` | `false` | Automatically start transcription and show subtitles when the video player is loaded. |
70
+ | `autoGenerate` | `boolean` | `false` | Automatically start transcription and show subtitles when the video player is loaded. *(Ignored if native tracks are detected).* |
84
71
  | `autoTranslation` | `string` | `null` | Automatically translate subtitles into this language on load (ISO 2-letter code, e.g. `'en'`, `'it'`, `'fr'`). Requires subtitles to be ready first. |
72
+ | `translationEngine` | `object` | `null` | Translation engine configuration. If `null`, uses MyMemory (free, limited). See [Translation Engines](https://www.google.com/search?q=%23-translation-engines) below. |
85
73
  | `showButton` | `boolean` | `true` | Show the CC button in the player interface. |
86
74
  | `position` | `string` | `'topbar'` | Button position: `'right'` or `'left'` (control bar) or `'topbar'` (next to the ⚙️ settings icon). |
87
75
  | `subtitleStyle` | `object` | `{}` | Custom inline CSS style object applied to the subtitle text element. |
88
- | `cacheEnabled` | `boolean` | `true` | Cache transcription in `localStorage` for instant reload. |
76
+ | `cacheEnabled` | `boolean` | `true` | Cache transcription and translations in `localStorage` for instant reload. |
89
77
  | `idCache` | `string` | `null` | Stable unique key used for the localStorage cache entry (recommended: your video's database ID). Falls back to a hash of the video URL if not set. |
90
78
 
79
+ ---
80
+
81
+ ## 🌐 Translation Engines
82
+
83
+ The plugin supports three translation backends. By default it uses **MyMemory** (no configuration needed).
84
+
85
+ ### Comparison
86
+
87
+ | Engine | Limit | Privacy | Setup |
88
+ | --- | --- | --- | --- |
89
+ | **MyMemory** (default) | ⚠️ ~5.000 chars/day per IP | ☁️ External API | None |
90
+ | **LibreTranslate** (self-hosted) | ✅ No limits | 🔒 Your server | Easy |
91
+ | **MarianMT** (self-hosted) | ✅ No limits | 🔒 Your server | Medium |
92
+
93
+ ---
94
+
95
+ ### MyMemory (default)
96
+
97
+ Free public API, no configuration required. Limit is approximately **5.000 characters/day per IP**. Suitable for low-traffic or development use.
98
+
99
+ ```javascript
100
+ // Default — no translationEngine needed
101
+ player.usePlugin('autoSubtitles', {
102
+ autoTranslation: 'it',
103
+ });
104
+
105
+ ```
106
+
107
+ ---
108
+
109
+ ### LibreTranslate (recommended for production)
110
+
111
+ Self-hosted, no limits, supports **native array batch translation** — each subtitle segment is translated individually with guaranteed 1:1 index mapping, eliminating any desync issues.
112
+
113
+ ```javascript
114
+ player.usePlugin('autoSubtitles', {
115
+ autoTranslation: 'it',
116
+ translationEngine: {
117
+ type: 'libretranslate',
118
+ url: '[https://your-libretranslate-server.com](https://your-libretranslate-server.com)',
119
+ apiKey: 'your-api-key', // optional, required if --api-keys is enabled
120
+ },
121
+ });
122
+
123
+ ```
124
+
125
+ **Self-hosting with Docker:**
126
+
127
+ ```bash
128
+ docker run -p 5000:5000 libretranslate/libretranslate --disable-web-ui --api-keys
129
+
130
+ ```
131
+
132
+ LibreTranslate: [https://github.com/LibreTranslate/LibreTranslate](https://github.com/LibreTranslate/LibreTranslate)
133
+
134
+ ---
135
+
136
+ ### MarianMT
137
+
138
+ Custom self-hosted REST endpoint compatible with MarianMT or Argos Translate.
139
+
140
+ ```javascript
141
+ player.usePlugin('autoSubtitles', {
142
+ autoTranslation: 'it',
143
+ translationEngine: {
144
+ type: 'marianmt',
145
+ url: '[https://your-marianmt-server.com](https://your-marianmt-server.com)',
146
+ },
147
+ });
148
+
149
+ ```
150
+
151
+ Expected request/response format:
152
+
153
+ ```json
154
+ // POST /translate
155
+ // Request
156
+ { "q": "Hello world", "source": "en", "target": "it" }
157
+
158
+ // Response
159
+ { "translatedText": "Ciao mondo" }
160
+
161
+ ```
91
162
 
92
163
  ---
93
164
 
@@ -97,27 +168,27 @@ Clicking the **CC button** opens a context menu with three sections:
97
168
 
98
169
  ### Subtitles
99
170
 
100
- - **▶ Enable** — show subtitles
101
- - **✕ Disable** — hide subtitles
171
+ * **▶ Enable** — show subtitles
172
+ * **✕ Disable** — hide subtitles
102
173
 
174
+ ### Management
103
175
 
104
- ### Gestione
105
-
106
- - **🎙️ Generate subtitles** — start transcription (first time)
107
- - **🔄 Regenerate trnascription** — force re-transcription (discards cache)
108
- - **📊 Show progress** — open the transcription progress panel (visible while generating)
176
+ *(Note: This section is automatically hidden if native subtitles `.vtt`/`.srt` are detected and imported from the player).*
109
177
 
178
+ * **🎙️ Generate subtitles** — start transcription (first time)
179
+ * **🔄 Regenerate transcription** — force re-transcription (discards cache)
180
+ * **📊 Show progress** — open the transcription progress panel (visible while generating)
110
181
 
111
182
  ### Translate to
112
183
 
113
- Scrollable list of 17+ languages. Translations are cached in memory for the session — switching back to a previously selected language is instant.
184
+ Scrollable list of 17+ languages. Translations are cached in `localStorage` for the session — switching back to a previously translated language is completely instant and requires no API calls.
114
185
 
115
186
  ---
116
187
 
117
188
  ## 🌍 Supported Translation Languages
118
189
 
119
190
  | Code | Language |
120
- | :-- | :-- |
191
+ | --- | --- |
121
192
  | `off` | No translation (original) |
122
193
  | `it` | 🇮🇹 Italiano |
123
194
  | `en` | 🇬🇧 English |
@@ -136,13 +207,12 @@ Scrollable list of 17+ languages. Translations are cached in memory for the sess
136
207
  | `uk` | 🇺🇦 Українська |
137
208
  | `hi` | 🇮🇳 हिन्दी |
138
209
 
139
-
140
210
  ---
141
211
 
142
212
  ## 🧠 Whisper Models
143
213
 
144
214
  | Size | ID | Download | Speed | Accuracy |
145
- | :-- | :-- | :-- | :-- | :-- |
215
+ | --- | --- | --- | --- | --- |
146
216
  | `tiny` | `Xenova/whisper-tiny` | ~39 MB | ⚡⚡⚡ Fast | ★☆☆ Good |
147
217
  | `base` | `Xenova/whisper-base` | ~74 MB | ⚡⚡ Medium | ★★☆ Better |
148
218
  | `small` | `Xenova/whisper-small` | ~244 MB | ⚡ Slow | ★★★ Best |
@@ -158,21 +228,28 @@ If your site uses a Content Security Policy, add the following:
158
228
  ```http
159
229
  Content-Security-Policy:
160
230
  worker-src blob:;
161
- script-src blob: https://cdn.jsdelivr.net;
162
- connect-src https://cdn.jsdelivr.net https://huggingface.co https://api.mymemory.translated.net;
231
+ script-src blob: [https://cdn.jsdelivr.net](https://cdn.jsdelivr.net);
232
+ connect-src [https://cdn.jsdelivr.net](https://cdn.jsdelivr.net)
233
+ [https://huggingface.co](https://huggingface.co)
234
+ [https://api.mymemory.translated.net](https://api.mymemory.translated.net)
235
+ [https://your-libretranslate-server.com](https://your-libretranslate-server.com);
236
+
163
237
  ```
164
- Use crossorigin="anonymous" on the <video> element.
238
+
239
+ > If using LibreTranslate or MarianMT, replace `https://api.mymemory.translated.net` with your server URL (or add both if using MyMemory as fallback).
240
+
241
+ Use `crossorigin="anonymous"` on the `<video>` element.
165
242
 
166
243
  ---
167
244
 
168
245
  ## 💾 Cache
169
246
 
170
- Transcriptions are stored in `localStorage` under the key `myetv_autosub_<id>`.
247
+ Transcriptions and Translations are stored in `localStorage` under the key `myetv_autosub_<id>`.
171
248
 
172
- - Up to **30 entries** are kept — oldest are evicted automatically
173
- - Set `idCache: 'your-video-id'` for a stable, human-readable key
174
- - Set `cacheEnabled: false` to disable caching entirely
175
- - Use **"🔄 Regenerate transcription"** from the menu to force a fresh transcription
249
+ * Up to **30 entries** are kept — oldest are evicted automatically
250
+ * Set `idCache: 'your-video-id'` for a stable, human-readable key
251
+ * Set `cacheEnabled: false` to disable caching entirely
252
+ * Use **"🔄 Regenerate transcription"** from the menu to force a fresh AI transcription
176
253
 
177
254
  ---
178
255
 
@@ -180,8 +257,8 @@ Transcriptions are stored in `localStorage` under the key `myetv_autosub_<id>`.
180
257
 
181
258
  Hover over the subtitle text to reveal the **⠿ sposta** drag handle. Click and drag to reposition subtitles anywhere inside the video frame.
182
259
 
183
- - Works with mouse and touch (touch drag handle is hidden on mobile — drag directly on the subtitle text)
184
- - Position resets on page reload (not persisted)
260
+ * Works with mouse and touch (touch drag handle is hidden on mobile — drag directly on the subtitle text)
261
+ * Position resets on page reload (not persisted)
185
262
 
186
263
  ---
187
264
 
@@ -190,35 +267,47 @@ Hover over the subtitle text to reveal the **⠿ sposta** drag handle. Click and
190
267
  ```javascript
191
268
  player.on('playerready', () => {
192
269
  player.usePlugin('autoSubtitles', {
193
- language: 'english', // language of the audio: "english" or "en" (if empty auto-detect)
194
- modelSize: 'base', // ai model to use: 'tiny', 'base', 'small'
195
- autoGenerate: true, // start transcription and view subtitles automatically without user interacton
196
- autoTranslation: 'en', // auto-translate to English when ready
197
- position: 'topbar', // CC button next to ⚙️ settings icon; options are: 'left', 'right', 'topbar'
198
- cacheEnabled: true, // cache client-side with localstorage
199
- idCache: `video-${videoId}`, // id of the key for the cache (if empty hased url will be used)
270
+ language: 'english', // language of the audio: "english" or "en" (if empty auto-detect)
271
+ modelSize: 'base', // AI model: 'tiny', 'base', 'small'
272
+ autoGenerate: true, // start transcription automatically
273
+ autoTranslation: 'it', // auto-translate to Italian when ready
274
+ position: 'topbar', // CC button position: 'left', 'right', 'topbar'
275
+ cacheEnabled: true,
276
+ idCache: `video-${videoId}`,
277
+ translationEngine: {
278
+ type: 'libretranslate',
279
+ url: '[https://your-libretranslate-server.com](https://your-libretranslate-server.com)',
280
+ apiKey: 'your-api-key',
281
+ },
200
282
  subtitleStyle: {
201
283
  fontSize: '1.2em',
202
284
  color: '#ffe066',
203
285
  },
204
286
  });
205
287
  });
206
- ```
207
288
 
289
+ ```
208
290
 
209
291
  ---
210
292
 
211
293
  ## 🏗️ How It Works
212
294
 
213
295
  ```
214
- User clicks CC
296
+ User clicks CC (or Auto-Trigger starts)
297
+
298
+
299
+ Check for Native Player Subtitles (.vtt / .srt)
300
+
301
+ Yes ──────────────────────► Import seamlessly, take over UI rendering, skip AI.
302
+
303
+ No
215
304
 
216
305
 
217
306
  Check localStorage cache
218
307
 
219
- Hit ──────────────────────► Load subtitles instantly
308
+ Hit ──────────────────────► Load transcriptions instantly
220
309
 
221
- Miss
310
+ Miss
222
311
 
223
312
 
224
313
  Extract audio (fetch or captureStream)
@@ -237,8 +326,12 @@ Whisper ASR — chunks of 30s each
237
326
  │ (subtitles stream in live)
238
327
 
239
328
  Save to localStorage cache
240
- ```
329
+
330
+
331
+ Translate via selected engine
332
+ (MyMemory / LibreTranslate / MarianMT)
241
333
 
334
+ ```
242
335
 
243
336
  ---
244
337
 
@@ -255,6 +348,3 @@ Pull requests welcome. For major changes, please open an issue first.
255
348
  ---
256
349
 
257
350
  > Built with ❤️ from the [MYETV](https://www.myetv.tv) team.
258
-
259
- ```
260
- ```
@@ -0,0 +1,154 @@
1
+ <?php
2
+ ini_set('memory_limit', '512M');
3
+ ini_set('max_execution_time', '60');
4
+ //error_reporting(0);
5
+ //ini_set('display_errors', 0);
6
+ header('Content-Type: application/json');
7
+
8
+ $stationsFile = __DIR__ . '/stations_example.json';
9
+
10
+ // Check 1: file exists and is readable
11
+ if (!file_exists($stationsFile) || !is_readable($stationsFile)) {
12
+ http_response_code(503);
13
+ echo json_encode(['error' => 'stations.json not found', 'path' => $stationsFile]);
14
+ exit;
15
+ }
16
+
17
+ $raw = file_get_contents($stationsFile);
18
+
19
+ // Check 2: file was read correctly
20
+ if ($raw === false || empty($raw)) {
21
+ http_response_code(503);
22
+ echo json_encode(['error' => 'stations.json is empty or unreadable']);
23
+ exit;
24
+ }
25
+
26
+ $stations = json_decode($raw, true);
27
+
28
+ // Check 3: JSON is valid
29
+ if (!is_array($stations)) {
30
+ http_response_code(503);
31
+ echo json_encode(['error' => 'stations.json invalid JSON', 'json_error' => json_last_error_msg()]);
32
+ exit;
33
+ }
34
+
35
+ // Common filters (used by all endpoints)
36
+ $countryFilter = isset($_GET['country']) ? strtolower(trim($_GET['country'])) : '';
37
+ $languageFilter = isset($_GET['language']) ? strtolower(trim($_GET['language'])) : '';
38
+
39
+ // All light - lightweight station list (DEFAULT if no other params)
40
+ if (isset($_GET['channel']) && $_GET['channel'] === 'all_light') {
41
+ $offset = (int)($_GET['offset'] ?? 0);
42
+ $limit = (int)($_GET['limit'] ?? 50000);
43
+
44
+ $light = array_map(fn($s) => [
45
+ 'channel_id' => $s['channel_id'] ?? 0,
46
+ 'name' => $s['name'] ?? '',
47
+ 'url_resolved' => $s['url_resolved'] ?? ($s['url'] ?? ''),
48
+ 'countrycode' => $s['countrycode'] ?? '',
49
+ 'language' => $s['language'] ?? '',
50
+ 'tags' => $s['tags'] ?? '',
51
+ 'favicon' => $s['favicon'] ?? '',
52
+ ], $stations);
53
+
54
+ // Remove stations without URL
55
+ $light = array_values(array_filter(
56
+ $light,
57
+ fn($s) => !empty($s['url_resolved'])
58
+ ));
59
+
60
+ // Country filter (exact match on countrycode)
61
+ if ($countryFilter) {
62
+ $light = array_values(array_filter(
63
+ $light,
64
+ fn($s) => strtolower($s['countrycode'] ?? '') === $countryFilter
65
+ ));
66
+ }
67
+
68
+ // Language filter (substring match in language OR tags)
69
+ if ($languageFilter) {
70
+ $light = array_values(array_filter(
71
+ $light,
72
+ fn($s) =>
73
+ str_contains(strtolower($s['language'] ?? ''), $languageFilter) ||
74
+ str_contains(strtolower($s['tags'] ?? ''), $languageFilter)
75
+ ));
76
+ }
77
+
78
+ echo json_encode(array_values(array_slice($light, $offset, $limit)));
79
+ exit;
80
+ }
81
+
82
+ // Single channel by number
83
+ if (isset($_GET['channel']) && is_numeric($_GET['channel'])) {
84
+ $ch = (int)$_GET['channel'];
85
+ $result = array_values(array_filter($stations, fn($s) => ($s['channel_id'] ?? 0) === $ch));
86
+ echo json_encode($result[0] ?? null);
87
+ exit;
88
+ }
89
+
90
+ // Search by name
91
+ if (isset($_GET['search'])) {
92
+ $q = strtolower(trim($_GET['search']));
93
+
94
+ $result = array_values(array_filter($stations,
95
+ fn($s) => str_contains(strtolower($s['name'] ?? ''), $q)
96
+ ));
97
+
98
+ // Apply country/language filters to search results too
99
+ if ($countryFilter) {
100
+ $result = array_values(array_filter(
101
+ $result,
102
+ fn($s) => strtolower($s['countrycode'] ?? '') === $countryFilter
103
+ ));
104
+ }
105
+ if ($languageFilter) {
106
+ $result = array_values(array_filter(
107
+ $result,
108
+ fn($s) =>
109
+ str_contains(strtolower($s['language'] ?? ''), $languageFilter) ||
110
+ str_contains(strtolower($s['tags'] ?? ''), $languageFilter)
111
+ ));
112
+ }
113
+
114
+ echo json_encode(array_slice($result, 0, 20));
115
+ exit;
116
+ }
117
+
118
+ // NO PARAMETERS = return entire list (lightweight)
119
+ $offset = (int)($_GET['offset'] ?? 0);
120
+ $limit = (int)($_GET['limit'] ?? 50000);
121
+
122
+ $light = array_map(fn($s) => [
123
+ 'channel_id' => $s['channel_id'] ?? 0,
124
+ 'name' => $s['name'] ?? '',
125
+ 'url_resolved' => $s['url_resolved'] ?? ($s['url'] ?? ''),
126
+ 'countrycode' => $s['countrycode'] ?? '',
127
+ 'language' => $s['language'] ?? '',
128
+ 'tags' => $s['tags'] ?? '',
129
+ 'favicon' => $s['favicon'] ?? '',
130
+ ], $stations);
131
+
132
+ // Remove stations without URL
133
+ $light = array_values(array_filter(
134
+ $light,
135
+ fn($s) => !empty($s['url_resolved'])
136
+ ));
137
+
138
+ // Apply filters if specified
139
+ if ($countryFilter) {
140
+ $light = array_values(array_filter(
141
+ $light,
142
+ fn($s) => strtolower($s['countrycode'] ?? '') === $countryFilter
143
+ ));
144
+ }
145
+ if ($languageFilter) {
146
+ $light = array_values(array_filter(
147
+ $light,
148
+ fn($s) =>
149
+ str_contains(strtolower($s['language'] ?? ''), $languageFilter) ||
150
+ str_contains(strtolower($s['tags'] ?? ''), $languageFilter)
151
+ ));
152
+ }
153
+
154
+ echo json_encode(array_values(array_slice($light, $offset, $limit)));
@@ -0,0 +1 @@
1
+