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.
@@ -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 via [MyMemory API](https://mymemory.translated.net/) (free, no key) or self-hosted [LibreTranslate](https://libretranslate.com/) / MarianMT (no limits)
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,21 +40,10 @@ 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
 
@@ -69,27 +56,26 @@ npm install myetv-autosub-plugin
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. |
85
- | `translationEngine` | `object` | `null` | Translation engine configuration. If `null`, uses MyMemory (free, limited). See [Translation Engines](#-translation-engines) below. |
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. |
86
73
  | `showButton` | `boolean` | `true` | Show the CC button in the player interface. |
87
74
  | `position` | `string` | `'topbar'` | Button position: `'right'` or `'left'` (control bar) or `'topbar'` (next to the ⚙️ settings icon). |
88
75
  | `subtitleStyle` | `object` | `{}` | Custom inline CSS style object applied to the subtitle text element. |
89
- | `cacheEnabled` | `boolean` | `true` | Cache transcription in `localStorage` for instant reload. |
76
+ | `cacheEnabled` | `boolean` | `true` | Cache transcription and translations in `localStorage` for instant reload. |
90
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. |
91
78
 
92
-
93
79
  ---
94
80
 
95
81
  ## 🌐 Translation Engines
@@ -99,12 +85,11 @@ The plugin supports three translation backends. By default it uses **MyMemory**
99
85
  ### Comparison
100
86
 
101
87
  | Engine | Limit | Privacy | Setup |
102
- | :-- | :-- | :-- | :-- |
88
+ | --- | --- | --- | --- |
103
89
  | **MyMemory** (default) | ⚠️ ~5.000 chars/day per IP | ☁️ External API | None |
104
90
  | **LibreTranslate** (self-hosted) | ✅ No limits | 🔒 Your server | Easy |
105
91
  | **MarianMT** (self-hosted) | ✅ No limits | 🔒 Your server | Medium |
106
92
 
107
-
108
93
  ---
109
94
 
110
95
  ### MyMemory (default)
@@ -116,8 +101,8 @@ Free public API, no configuration required. Limit is approximately **5.000 chara
116
101
  player.usePlugin('autoSubtitles', {
117
102
  autoTranslation: 'it',
118
103
  });
119
- ```
120
104
 
105
+ ```
121
106
 
122
107
  ---
123
108
 
@@ -130,18 +115,22 @@ player.usePlugin('autoSubtitles', {
130
115
  autoTranslation: 'it',
131
116
  translationEngine: {
132
117
  type: 'libretranslate',
133
- url: 'https://your-libretranslate-server.com',
118
+ url: '[https://your-libretranslate-server.com](https://your-libretranslate-server.com)',
134
119
  apiKey: 'your-api-key', // optional, required if --api-keys is enabled
135
120
  },
136
121
  });
122
+
137
123
  ```
138
124
 
139
125
  **Self-hosting with Docker:**
140
126
 
141
127
  ```bash
142
128
  docker run -p 5000:5000 libretranslate/libretranslate --disable-web-ui --api-keys
129
+
143
130
  ```
131
+
144
132
  LibreTranslate: [https://github.com/LibreTranslate/LibreTranslate](https://github.com/LibreTranslate/LibreTranslate)
133
+
145
134
  ---
146
135
 
147
136
  ### MarianMT
@@ -153,9 +142,10 @@ player.usePlugin('autoSubtitles', {
153
142
  autoTranslation: 'it',
154
143
  translationEngine: {
155
144
  type: 'marianmt',
156
- url: 'https://your-marianmt-server.com',
145
+ url: '[https://your-marianmt-server.com](https://your-marianmt-server.com)',
157
146
  },
158
147
  });
148
+
159
149
  ```
160
150
 
161
151
  Expected request/response format:
@@ -167,8 +157,8 @@ Expected request/response format:
167
157
 
168
158
  // Response
169
159
  { "translatedText": "Ciao mondo" }
170
- ```
171
160
 
161
+ ```
172
162
 
173
163
  ---
174
164
 
@@ -178,27 +168,27 @@ Clicking the **CC button** opens a context menu with three sections:
178
168
 
179
169
  ### Subtitles
180
170
 
181
- - **▶ Enable** — show subtitles
182
- - **✕ Disable** — hide subtitles
183
-
171
+ * **▶ Enable** — show subtitles
172
+ * **✕ Disable** — hide subtitles
184
173
 
185
174
  ### Management
186
175
 
187
- - **🎙️ Generate subtitles** start transcription (first time)
188
- - **🔄 Regenerate transcription** — force re-transcription (discards cache)
189
- - **📊 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).*
190
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)
191
181
 
192
182
  ### Translate to
193
183
 
194
- 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.
195
185
 
196
186
  ---
197
187
 
198
188
  ## 🌍 Supported Translation Languages
199
189
 
200
190
  | Code | Language |
201
- | :-- | :-- |
191
+ | --- | --- |
202
192
  | `off` | No translation (original) |
203
193
  | `it` | 🇮🇹 Italiano |
204
194
  | `en` | 🇬🇧 English |
@@ -217,13 +207,12 @@ Scrollable list of 17+ languages. Translations are cached in memory for the sess
217
207
  | `uk` | 🇺🇦 Українська |
218
208
  | `hi` | 🇮🇳 हिन्दी |
219
209
 
220
-
221
210
  ---
222
211
 
223
212
  ## 🧠 Whisper Models
224
213
 
225
214
  | Size | ID | Download | Speed | Accuracy |
226
- | :-- | :-- | :-- | :-- | :-- |
215
+ | --- | --- | --- | --- | --- |
227
216
  | `tiny` | `Xenova/whisper-tiny` | ~39 MB | ⚡⚡⚡ Fast | ★☆☆ Good |
228
217
  | `base` | `Xenova/whisper-base` | ~74 MB | ⚡⚡ Medium | ★★☆ Better |
229
218
  | `small` | `Xenova/whisper-small` | ~244 MB | ⚡ Slow | ★★★ Best |
@@ -239,11 +228,12 @@ If your site uses a Content Security Policy, add the following:
239
228
  ```http
240
229
  Content-Security-Policy:
241
230
  worker-src blob:;
242
- script-src blob: https://cdn.jsdelivr.net;
243
- connect-src https://cdn.jsdelivr.net
244
- https://huggingface.co
245
- https://api.mymemory.translated.net
246
- https://your-libretranslate-server.com;
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
+
247
237
  ```
248
238
 
249
239
  > If using LibreTranslate or MarianMT, replace `https://api.mymemory.translated.net` with your server URL (or add both if using MyMemory as fallback).
@@ -254,12 +244,12 @@ Use `crossorigin="anonymous"` on the `<video>` element.
254
244
 
255
245
  ## 💾 Cache
256
246
 
257
- 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>`.
258
248
 
259
- - Up to **30 entries** are kept — oldest are evicted automatically
260
- - Set `idCache: 'your-video-id'` for a stable, human-readable key
261
- - Set `cacheEnabled: false` to disable caching entirely
262
- - 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
263
253
 
264
254
  ---
265
255
 
@@ -267,8 +257,8 @@ Transcriptions are stored in `localStorage` under the key `myetv_autosub_<id>`.
267
257
 
268
258
  Hover over the subtitle text to reveal the **⠿ sposta** drag handle. Click and drag to reposition subtitles anywhere inside the video frame.
269
259
 
270
- - Works with mouse and touch (touch drag handle is hidden on mobile — drag directly on the subtitle text)
271
- - 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)
272
262
 
273
263
  ---
274
264
 
@@ -280,13 +270,13 @@ player.on('playerready', () => {
280
270
  language: 'english', // language of the audio: "english" or "en" (if empty auto-detect)
281
271
  modelSize: 'base', // AI model: 'tiny', 'base', 'small'
282
272
  autoGenerate: true, // start transcription automatically
283
- autoTranslation: 'it', // auto-translate to Italian when ready
273
+ autoTranslation: 'it', // auto-translate to Italian when ready
284
274
  position: 'topbar', // CC button position: 'left', 'right', 'topbar'
285
275
  cacheEnabled: true,
286
276
  idCache: `video-${videoId}`,
287
277
  translationEngine: {
288
278
  type: 'libretranslate',
289
- url: 'https://your-libretranslate-server.com',
279
+ url: '[https://your-libretranslate-server.com](https://your-libretranslate-server.com)',
290
280
  apiKey: 'your-api-key',
291
281
  },
292
282
  subtitleStyle: {
@@ -295,22 +285,29 @@ player.on('playerready', () => {
295
285
  },
296
286
  });
297
287
  });
298
- ```
299
288
 
289
+ ```
300
290
 
301
291
  ---
302
292
 
303
293
  ## 🏗️ How It Works
304
294
 
305
295
  ```
306
- 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
307
304
 
308
305
 
309
306
  Check localStorage cache
310
307
 
311
- Hit ──────────────────────► Load subtitles instantly
308
+ Hit ──────────────────────► Load transcriptions instantly
312
309
 
313
- Miss
310
+ Miss
314
311
 
315
312
 
316
313
  Extract audio (fetch or captureStream)
@@ -333,8 +330,8 @@ Save to localStorage cache
333
330
 
334
331
  Translate via selected engine
335
332
  (MyMemory / LibreTranslate / MarianMT)
336
- ```
337
333
 
334
+ ```
338
335
 
339
336
  ---
340
337
 
@@ -351,6 +348,3 @@ Pull requests welcome. For major changes, please open an issue first.
351
348
  ---
352
349
 
353
350
  > Built with ❤️ from the [MYETV](https://www.myetv.tv) team.
354
-
355
- ```
356
- ```
@@ -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
+