oneword-ai 0.1.0__py3-none-any.whl
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.
- oneword_ai-0.1.0.dist-info/METADATA +237 -0
- oneword_ai-0.1.0.dist-info/RECORD +15 -0
- oneword_ai-0.1.0.dist-info/WHEEL +5 -0
- oneword_ai-0.1.0.dist-info/entry_points.txt +3 -0
- oneword_ai-0.1.0.dist-info/licenses/license.txt +7 -0
- oneword_ai-0.1.0.dist-info/top_level.txt +1 -0
- onewordai/__init__.py +2 -0
- onewordai/api/__init__.py +1 -0
- onewordai/api/main.py +262 -0
- onewordai/cli.py +67 -0
- onewordai/core/__init__.py +4 -0
- onewordai/core/engine.py +368 -0
- onewordai/web/app.js +361 -0
- onewordai/web/index.html +154 -0
- onewordai/web/style.css +485 -0
onewordai/web/app.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend JavaScript for OneWord AI
|
|
3
|
+
* Handles file upload, API communication, and UI updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ========== STATE ==========
|
|
7
|
+
let uploadedFileId = null;
|
|
8
|
+
let currentJobId = null;
|
|
9
|
+
let pollingInterval = null;
|
|
10
|
+
|
|
11
|
+
// ========== DOM ELEMENTS ==========
|
|
12
|
+
const dropzone = document.getElementById('dropzone');
|
|
13
|
+
const fileInput = document.getElementById('fileInput');
|
|
14
|
+
const fileInfo = document.getElementById('fileInfo');
|
|
15
|
+
const fileName = document.getElementById('fileName');
|
|
16
|
+
const fileSize = document.getElementById('fileSize');
|
|
17
|
+
const processBtn = document.getElementById('processBtn');
|
|
18
|
+
const progressCard = document.getElementById('progressCard');
|
|
19
|
+
const downloadCard = document.getElementById('downloadCard');
|
|
20
|
+
const progressFill = document.getElementById('progressFill');
|
|
21
|
+
const progressText = document.getElementById('progressText');
|
|
22
|
+
const statusText = document.getElementById('statusText');
|
|
23
|
+
const downloadBtn = document.getElementById('downloadBtn');
|
|
24
|
+
const resetBtn = document.getElementById('resetBtn');
|
|
25
|
+
const cancelBtn = document.getElementById('cancelBtn');
|
|
26
|
+
const instaPopup = document.getElementById('instaPopup');
|
|
27
|
+
const closePopup = document.getElementById('closePopup');
|
|
28
|
+
|
|
29
|
+
const modelSelect = document.getElementById('modelSelect');
|
|
30
|
+
const languageSelect = document.getElementById('languageSelect');
|
|
31
|
+
const modeSelect = document.getElementById('modeSelect');
|
|
32
|
+
|
|
33
|
+
// Prevent accidental page reload during processing
|
|
34
|
+
let isProcessing = false;
|
|
35
|
+
|
|
36
|
+
window.addEventListener('beforeunload', (e) => {
|
|
37
|
+
if (isProcessing) {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
e.returnValue = ''; // Required for Chrome
|
|
40
|
+
return ''; // Required for other browsers
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ========== FILE UPLOAD ==========
|
|
45
|
+
dropzone.addEventListener('click', () => {
|
|
46
|
+
fileInput.click();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
dropzone.classList.add('dragover');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
dropzone.addEventListener('dragleave', () => {
|
|
55
|
+
dropzone.classList.remove('dragover');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
dropzone.addEventListener('drop', (e) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
dropzone.classList.remove('dragover');
|
|
61
|
+
|
|
62
|
+
const files = e.dataTransfer.files;
|
|
63
|
+
if (files.length > 0) {
|
|
64
|
+
handleFile(files[0]);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
fileInput.addEventListener('change', (e) => {
|
|
69
|
+
if (e.target.files.length > 0) {
|
|
70
|
+
handleFile(e.target.files[0]);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
async function handleFile(file) {
|
|
75
|
+
// Validate file size (100MB limit)
|
|
76
|
+
const maxSize = 100 * 1024 * 1024;
|
|
77
|
+
if (file.size > maxSize) {
|
|
78
|
+
alert('File too large! Maximum size is 100MB.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Show file info
|
|
83
|
+
fileName.textContent = `📁 ${file.name}`;
|
|
84
|
+
fileSize.textContent = `📊 ${formatFileSize(file.size)}`;
|
|
85
|
+
fileInfo.classList.remove('hidden');
|
|
86
|
+
|
|
87
|
+
// Upload file
|
|
88
|
+
statusText.textContent = 'Uploading...';
|
|
89
|
+
progressCard.classList.remove('hidden');
|
|
90
|
+
|
|
91
|
+
const formData = new FormData();
|
|
92
|
+
formData.append('file', file);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const response = await fetch('/api/upload', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: formData
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error('Upload failed');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
uploadedFileId = data.file_id;
|
|
106
|
+
|
|
107
|
+
// Enable process button
|
|
108
|
+
processBtn.disabled = false;
|
|
109
|
+
progressCard.classList.add('hidden');
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
alert('Upload failed: ' + error.message);
|
|
113
|
+
progressCard.classList.add('hidden');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatFileSize(bytes) {
|
|
118
|
+
if (bytes === 0) return '0 Bytes';
|
|
119
|
+
const k = 1024;
|
|
120
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
121
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
122
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ========== PROCESSING ==========
|
|
126
|
+
processBtn.addEventListener('click', async () => {
|
|
127
|
+
if (!uploadedFileId) {
|
|
128
|
+
alert('Please upload a file first');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Get config
|
|
133
|
+
const model = modelSelect.value;
|
|
134
|
+
const language = languageSelect.value || null;
|
|
135
|
+
const mode = modeSelect.value;
|
|
136
|
+
|
|
137
|
+
// Start processing
|
|
138
|
+
const formData = new FormData();
|
|
139
|
+
formData.append('file_id', uploadedFileId);
|
|
140
|
+
formData.append('model', model);
|
|
141
|
+
if (language) {
|
|
142
|
+
formData.append('language', language);
|
|
143
|
+
}
|
|
144
|
+
formData.append('mode', mode);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch('/api/process', {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
body: formData
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error('Processing failed to start');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const data = await response.json();
|
|
157
|
+
currentJobId = data.job_id;
|
|
158
|
+
|
|
159
|
+
// Show progress card
|
|
160
|
+
progressCard.classList.remove('hidden');
|
|
161
|
+
processBtn.disabled = true;
|
|
162
|
+
|
|
163
|
+
// Start polling
|
|
164
|
+
startPolling();
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
alert('Error: ' + error.message);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ========== POLLING ==========
|
|
172
|
+
// ========== POLLING ==========
|
|
173
|
+
let simulatedProgress = 0;
|
|
174
|
+
|
|
175
|
+
function startPolling() {
|
|
176
|
+
simulatedProgress = 0;
|
|
177
|
+
|
|
178
|
+
// Status polling
|
|
179
|
+
pollingInterval = setInterval(async () => {
|
|
180
|
+
try {
|
|
181
|
+
const response = await fetch(`/api/status/${currentJobId}`);
|
|
182
|
+
|
|
183
|
+
if (response.status === 404) {
|
|
184
|
+
stopPolling();
|
|
185
|
+
alert('Connection lost or server restarted. Please try uploading again.');
|
|
186
|
+
resetUI();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new Error('Status check failed');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const job = await response.json();
|
|
195
|
+
|
|
196
|
+
// Update UI
|
|
197
|
+
updateProgress(job);
|
|
198
|
+
|
|
199
|
+
// Check if complete
|
|
200
|
+
if (job.status === 'completed') {
|
|
201
|
+
stopPolling();
|
|
202
|
+
// Force 100% just in case
|
|
203
|
+
progressFill.style.width = '100%';
|
|
204
|
+
progressText.textContent = '100%';
|
|
205
|
+
setTimeout(() => showDownload(), 500);
|
|
206
|
+
} else if (job.status === 'failed') {
|
|
207
|
+
stopPolling();
|
|
208
|
+
alert('Processing failed: ' + job.error);
|
|
209
|
+
resetUI();
|
|
210
|
+
} else if (job.status === 'cancelled') {
|
|
211
|
+
stopPolling();
|
|
212
|
+
alert('Process was cancelled.');
|
|
213
|
+
resetUI();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('Polling error:', error);
|
|
218
|
+
}
|
|
219
|
+
}, 1000);
|
|
220
|
+
|
|
221
|
+
// Simulation interval remains the same
|
|
222
|
+
simulationInterval = setInterval(() => {
|
|
223
|
+
if (simulatedProgress < 90) {
|
|
224
|
+
// Slower progress as it gets higher
|
|
225
|
+
const increment = simulatedProgress < 30 ? 2 : (simulatedProgress < 60 ? 1 : 0.5);
|
|
226
|
+
simulatedProgress += increment;
|
|
227
|
+
|
|
228
|
+
// Only update visualization if real progress isn't higher
|
|
229
|
+
const currentReal = parseFloat(progressFill.style.width) || 0;
|
|
230
|
+
if (simulatedProgress > currentReal) {
|
|
231
|
+
progressFill.style.width = simulatedProgress + '%';
|
|
232
|
+
progressText.textContent = Math.round(simulatedProgress) + '%';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}, 500);
|
|
236
|
+
|
|
237
|
+
// Set processing flag
|
|
238
|
+
isProcessing = true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let simulationInterval = null;
|
|
242
|
+
|
|
243
|
+
// Cancel button handler
|
|
244
|
+
cancelBtn.addEventListener('click', async () => {
|
|
245
|
+
if (!currentJobId) return;
|
|
246
|
+
|
|
247
|
+
if (confirm('Are you sure you want to cancel? Any progress will be lost.')) {
|
|
248
|
+
try {
|
|
249
|
+
await fetch(`/api/cancel/${currentJobId}`, { method: 'POST' });
|
|
250
|
+
stopPolling();
|
|
251
|
+
resetUI();
|
|
252
|
+
alert('Process cancelled successfully.');
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error('Cancel failed:', error);
|
|
255
|
+
alert('Failed to cancel. The process may have already completed.');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
function stopPolling() {
|
|
261
|
+
if (pollingInterval) {
|
|
262
|
+
clearInterval(pollingInterval);
|
|
263
|
+
pollingInterval = null;
|
|
264
|
+
}
|
|
265
|
+
if (simulationInterval) {
|
|
266
|
+
clearInterval(simulationInterval);
|
|
267
|
+
simulationInterval = null;
|
|
268
|
+
}
|
|
269
|
+
isProcessing = false; // Clear processing flag
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function updateProgress(job) {
|
|
273
|
+
const realProgress = job.progress || 0;
|
|
274
|
+
|
|
275
|
+
// Calculate display progress
|
|
276
|
+
// If real progress is < 5% (likely downloading), don't simulate too far ahead
|
|
277
|
+
// Max simulation cap: 95%
|
|
278
|
+
let displayProgress = realProgress;
|
|
279
|
+
|
|
280
|
+
if (realProgress < simulatedProgress) {
|
|
281
|
+
// We are simulating ahead
|
|
282
|
+
// But if real is 0 (downloading), cap simulation at 10% so we don't look like we're transcribing
|
|
283
|
+
if (realProgress === 0 && simulatedProgress > 10) {
|
|
284
|
+
simulatedProgress = 10;
|
|
285
|
+
} else {
|
|
286
|
+
simulatedProgress += (95 - simulatedProgress) * 0.05;
|
|
287
|
+
}
|
|
288
|
+
displayProgress = simulatedProgress;
|
|
289
|
+
} else {
|
|
290
|
+
// Real progress caught up
|
|
291
|
+
simulatedProgress = realProgress;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
progressFill.style.width = displayProgress + '%';
|
|
295
|
+
|
|
296
|
+
// Update Text Logic: Priority to Backend Message
|
|
297
|
+
if (job.status_message) {
|
|
298
|
+
progressText.textContent = `${job.status_message} (${Math.round(displayProgress)}%)`;
|
|
299
|
+
} else {
|
|
300
|
+
progressText.textContent = `Processing... ${Math.round(displayProgress)}%`;
|
|
301
|
+
}
|
|
302
|
+
// Update status text - PRIORITY TO BACKEND MESSAGE
|
|
303
|
+
if (job.status_message) {
|
|
304
|
+
// Use the backend's custom message (e.g., "Downloading...", "Transcribing...")
|
|
305
|
+
statusText.textContent = job.status_message;
|
|
306
|
+
} else if (job.status === 'processing') {
|
|
307
|
+
statusText.textContent = '🧠 Transcribing with Whisper... (This may take a moment)';
|
|
308
|
+
} else if (job.status === 'pending') {
|
|
309
|
+
statusText.textContent = '⏳ Waiting to start...';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function showDownload() {
|
|
314
|
+
progressCard.classList.add('hidden');
|
|
315
|
+
downloadCard.classList.remove('hidden');
|
|
316
|
+
|
|
317
|
+
// Show Instagram popup after 2 seconds
|
|
318
|
+
setTimeout(() => {
|
|
319
|
+
instaPopup.classList.remove('hidden');
|
|
320
|
+
}, 2000);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ========== DOWNLOAD ==========
|
|
324
|
+
downloadBtn.addEventListener('click', () => {
|
|
325
|
+
if (currentJobId) {
|
|
326
|
+
window.location.href = `/api/download/${currentJobId}`;
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ========== RESET ==========
|
|
331
|
+
resetBtn.addEventListener('click', () => {
|
|
332
|
+
resetUI();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
function resetUI() {
|
|
336
|
+
uploadedFileId = null;
|
|
337
|
+
currentJobId = null;
|
|
338
|
+
fileInfo.classList.add('hidden');
|
|
339
|
+
progressCard.classList.add('hidden');
|
|
340
|
+
downloadCard.classList.add('hidden');
|
|
341
|
+
processBtn.disabled = true;
|
|
342
|
+
progressFill.style.width = '0%';
|
|
343
|
+
progressText.textContent = '0%';
|
|
344
|
+
fileInput.value = '';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ========== POPUP ==========
|
|
348
|
+
closePopup.addEventListener('click', () => {
|
|
349
|
+
instaPopup.classList.add('hidden');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
instaPopup.addEventListener('click', (e) => {
|
|
353
|
+
if (e.target === instaPopup) {
|
|
354
|
+
instaPopup.classList.add('hidden');
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Show popup on load
|
|
359
|
+
setTimeout(() => {
|
|
360
|
+
instaPopup.classList.remove('hidden');
|
|
361
|
+
}, 1000);
|
onewordai/web/index.html
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>OneWord AI - Subtitle Generator</title>
|
|
8
|
+
<link rel="stylesheet" href="style.css">
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
<div class="grid-bg"></div>
|
|
13
|
+
|
|
14
|
+
<div class="container">
|
|
15
|
+
<!-- Header -->
|
|
16
|
+
<header class="header">
|
|
17
|
+
<h1 class="title">OneWord AI</h1>
|
|
18
|
+
<p class="subtitle">Cinematic Subtitle Generator</p>
|
|
19
|
+
</header>
|
|
20
|
+
|
|
21
|
+
<!-- Upload Section -->
|
|
22
|
+
<div class="card upload-card">
|
|
23
|
+
<div class="card-header">
|
|
24
|
+
<h2>Upload Video/Audio</h2>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div id="dropzone" class="dropzone">
|
|
28
|
+
<div class="dropzone-content">
|
|
29
|
+
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
30
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
31
|
+
<polyline points="17 8 12 3 7 8"></polyline>
|
|
32
|
+
<line x1="12" y1="3" x2="12" y2="15"></line>
|
|
33
|
+
</svg>
|
|
34
|
+
<p class="dropzone-text">Drag & drop your file here</p>
|
|
35
|
+
<p class="dropzone-subtext">or click to browse</p>
|
|
36
|
+
<p class="file-limit">Max 100MB • MP4, MP3, WAV</p>
|
|
37
|
+
</div>
|
|
38
|
+
<input type="file" id="fileInput" accept="video/*,audio/*" hidden>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div id="fileInfo" class="file-info hidden">
|
|
42
|
+
<p id="fileName"></p>
|
|
43
|
+
<p id="fileSize"></p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Configuration Section -->
|
|
48
|
+
<div class="card config-card">
|
|
49
|
+
<div class="card-header">
|
|
50
|
+
<h2>Configuration</h2>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="form-grid">
|
|
54
|
+
<!-- Model Selection -->
|
|
55
|
+
<div class="form-group">
|
|
56
|
+
<label for="modelSelect">Whisper Model</label>
|
|
57
|
+
<select id="modelSelect" class="select-input">
|
|
58
|
+
<option value="medium" selected>Medium (High Quality)</option>
|
|
59
|
+
<option value="large">Large (Best Quality)</option>
|
|
60
|
+
<option value="Oriserve/Whisper-Hindi2Hinglish-Prime">Hindi2Hinglish (Recommended)</option>
|
|
61
|
+
</select>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Language Selection -->
|
|
65
|
+
<div class="form-group">
|
|
66
|
+
<label for="languageSelect">Language</label>
|
|
67
|
+
<select id="languageSelect" class="select-input">
|
|
68
|
+
<option value="">Auto Detect</option>
|
|
69
|
+
<option value="en">English</option>
|
|
70
|
+
<option value="hi">Hindi</option>
|
|
71
|
+
<option value="ur">Urdu</option>
|
|
72
|
+
<option value="es">Spanish</option>
|
|
73
|
+
</select>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Subtitle Mode -->
|
|
77
|
+
<div class="form-group">
|
|
78
|
+
<label for="modeSelect">Subtitle Mode</label>
|
|
79
|
+
<select id="modeSelect" class="select-input">
|
|
80
|
+
<option value="oneword" selected>One Word</option>
|
|
81
|
+
<option value="twoword">Two Word Punch</option>
|
|
82
|
+
<option value="phrase">Phrase Mode</option>
|
|
83
|
+
</select>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<button id="processBtn" class="btn-primary" disabled>
|
|
88
|
+
Generate Subtitles
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Progress Section -->
|
|
93
|
+
<div id="progressCard" class="card progress-card hidden">
|
|
94
|
+
<div class="card-header">
|
|
95
|
+
<h2>Processing</h2>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="progress-container">
|
|
99
|
+
<div class="progress-bar">
|
|
100
|
+
<div id="progressFill" class="progress-fill"></div>
|
|
101
|
+
</div>
|
|
102
|
+
<p id="progressText" class="progress-text">0%</p>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<p id="statusText" class="status-text">Initializing...</p>
|
|
106
|
+
|
|
107
|
+
<p style="color: #ff6b6b; font-weight: bold; margin-top: 15px; font-size: 0.9rem;">
|
|
108
|
+
⚠️ DO NOT RELOAD this page or the process will be cancelled!
|
|
109
|
+
</p>
|
|
110
|
+
|
|
111
|
+
<button id="cancelBtn" class="btn-secondary" style="margin-top: 15px; background: #ff6b6b;">
|
|
112
|
+
❌ Cancel Process
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Download Section -->
|
|
117
|
+
<div id="downloadCard" class="card download-card hidden">
|
|
118
|
+
<div class="card-header">
|
|
119
|
+
<h2>✅ Complete!</h2>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<button id="downloadBtn" class="btn-download">
|
|
123
|
+
Download SRT File
|
|
124
|
+
</button>
|
|
125
|
+
|
|
126
|
+
<button id="resetBtn" class="btn-secondary">
|
|
127
|
+
Process Another File
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<!-- Instagram Popup -->
|
|
132
|
+
<div id="instaPopup" class="popup-overlay hidden">
|
|
133
|
+
<div class="popup-card">
|
|
134
|
+
<button id="closePopup" class="popup-close">×</button>
|
|
135
|
+
<h3>Developed by Ambrish</h3>
|
|
136
|
+
<p>Help me reach 1000 followers! 🚀</p>
|
|
137
|
+
<p style="font-size: 0.9rem; margin-top: -10px; margin-bottom: 20px; opacity: 0.8;">Get updates on more
|
|
138
|
+
free AI tools</p>
|
|
139
|
+
<a href="https://instagram.com/ambrish.yadav.1" target="_blank" class="btn-instagram">
|
|
140
|
+
Follow @ambrish.yadav.1
|
|
141
|
+
</a>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<!-- Footer -->
|
|
146
|
+
<footer class="footer">
|
|
147
|
+
<p>Made with ❤️ by <strong>Ambrish</strong></p>
|
|
148
|
+
</footer>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<script src="app.js"></script>
|
|
152
|
+
</body>
|
|
153
|
+
|
|
154
|
+
</html>
|