diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 919dca9..025a3c6 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -1,9 +1,9 @@
-{
- "permissions": {
- "allow": [
- "Bash(npm run server:*)"
- ],
- "deny": [],
- "ask": []
- }
-}
+{
+ "permissions": {
+ "allow": [
+ "Bash(npm run server:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
diff --git a/CLAUDE.md b/CLAUDE.md
index d6c04b3..3b00787 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,128 +1,128 @@
-# Video to MP3 Transcriptor - Instructions pour Claude
-
-## À propos du projet
-Ce projet est une API Node.js/Express pour télécharger des vidéos YouTube en MP3, les transcrire, les traduire et les résumer.
-
-## Documentation
-
-### Documentation API
-La documentation complète de l'API se trouve dans **`docs/API.md`**.
-
-**IMPORTANT** : Cette documentation doit TOUJOURS être maintenue à jour. Chaque fois qu'un endpoint est modifié, ajouté ou supprimé, la documentation doit être mise à jour en conséquence.
-
-### Responsabilités de maintenance de la documentation
-
-Quand tu modifies le code, tu DOIS mettre à jour `docs/API.md` si :
-- Un nouvel endpoint est ajouté
-- Un endpoint existant est modifié (paramètres, réponses, etc.)
-- Un endpoint est supprimé
-- Les modèles par défaut changent
-- De nouveaux paramètres sont ajoutés
-- Le format des réponses change
-
-## Structure du projet
-
-```
-videotoMP3Transcriptor/
-├── docs/
-│ └── API.md # Documentation complète de l'API
-├── src/
-│ ├── server.js # Serveur Express et routes API
-│ ├── services/
-│ │ ├── youtube.js # Téléchargement YouTube
-│ │ ├── transcription.js # Transcription OpenAI
-│ │ ├── translation.js # Traduction GPT
-│ │ └── summarize.js # Résumé GPT-5.1
-│ └── cli.js # Interface en ligne de commande
-├── public/ # Interface web (si présente)
-├── output/ # Répertoire de sortie par défaut
-├── .env # Variables d'environnement
-└── package.json
-
-```
-
-## Configuration
-
-### Port du serveur
-- Port par défaut : **8888**
-- Configurable via `process.env.PORT` dans `.env`
-
-### Modèles par défaut
-- **Transcription** : `gpt-4o-mini-transcribe`
-- **Résumé** : `gpt-5.1`
-- **Traduction** : `gpt-4o-mini` (hardcodé)
-
-### Variables d'environnement requises
-```env
-OPENAI_API_KEY=sk-...
-PORT=8888 # optionnel
-OUTPUT_DIR=./output # optionnel
-```
-
-## Commandes importantes
-
-```bash
-# Lancer le serveur
-npm run server
-
-# Lancer le CLI
-npm run cli
-
-# Installer les dépendances
-npm install
-```
-
-## Points d'attention
-
-### Paramètres outputPath
-Tous les endpoints supportent maintenant un paramètre `outputPath` optionnel pour spécifier un répertoire de sortie personnalisé. Si non spécifié, le répertoire par défaut `OUTPUT_DIR` est utilisé.
-
-### Modèles de transcription disponibles
-- `gpt-4o-mini-transcribe` (par défaut) - Rapide et économique
-- `gpt-4o-transcribe` - Qualité supérieure
-- `whisper-1` - Modèle original Whisper (supporte plus de formats)
-
-### Formats de sortie
-- **Transcription** : txt, json, srt, vtt (selon le modèle)
-- **Traduction** : txt
-- **Résumé** : txt
-
-## Règles de développement
-
-1. **Documentation d'abord** : Avant de modifier un endpoint, vérifie `docs/API.md`
-2. **Après modification** : Mets à jour immédiatement `docs/API.md`
-3. **Tests** : Redémarre le serveur après chaque modification
-4. **Cohérence** : Garde la même structure de réponse pour tous les endpoints similaires
-
-## Architecture des endpoints
-
-### Endpoints streaming (SSE)
-- `/download-stream`
-- `/process-stream`
-- `/summarize-stream`
-
-Ces endpoints utilisent Server-Sent Events pour envoyer des mises à jour de progression en temps réel.
-
-### Endpoints non-streaming
-- `/download`
-- `/process`
-- Tous les endpoints POST avec upload de fichiers
-
-Ces endpoints retournent une réponse unique une fois le traitement terminé.
-
-## Maintenance
-
-Lors de l'ajout de nouvelles fonctionnalités :
-1. Implémente la fonctionnalité dans le service approprié (`src/services/`)
-2. Ajoute les routes dans `src/server.js`
-3. **Mets à jour `docs/API.md` IMMÉDIATEMENT**
-4. Teste l'endpoint avec curl ou Postman
-5. Vérifie que la documentation est claire et complète
-
-## Notes importantes
-
-- Le serveur doit toujours être sur le port **8888**
-- Les clés API OpenAI sont requises pour transcription/traduction/résumé
-- Le répertoire `output/` est créé automatiquement si inexistant
-- Les fichiers uploadés sont stockés dans `OUTPUT_DIR`
-- Les vidéos YouTube sont téléchargées en MP3 automatiquement
+# Video to MP3 Transcriptor - Instructions pour Claude
+
+## À propos du projet
+Ce projet est une API Node.js/Express pour télécharger des vidéos YouTube en MP3, les transcrire, les traduire et les résumer.
+
+## Documentation
+
+### Documentation API
+La documentation complète de l'API se trouve dans **`docs/API.md`**.
+
+**IMPORTANT** : Cette documentation doit TOUJOURS être maintenue à jour. Chaque fois qu'un endpoint est modifié, ajouté ou supprimé, la documentation doit être mise à jour en conséquence.
+
+### Responsabilités de maintenance de la documentation
+
+Quand tu modifies le code, tu DOIS mettre à jour `docs/API.md` si :
+- Un nouvel endpoint est ajouté
+- Un endpoint existant est modifié (paramètres, réponses, etc.)
+- Un endpoint est supprimé
+- Les modèles par défaut changent
+- De nouveaux paramètres sont ajoutés
+- Le format des réponses change
+
+## Structure du projet
+
+```
+videotoMP3Transcriptor/
+├── docs/
+│ └── API.md # Documentation complète de l'API
+├── src/
+│ ├── server.js # Serveur Express et routes API
+│ ├── services/
+│ │ ├── youtube.js # Téléchargement YouTube
+│ │ ├── transcription.js # Transcription OpenAI
+│ │ ├── translation.js # Traduction GPT
+│ │ └── summarize.js # Résumé GPT-5.1
+│ └── cli.js # Interface en ligne de commande
+├── public/ # Interface web (si présente)
+├── output/ # Répertoire de sortie par défaut
+├── .env # Variables d'environnement
+└── package.json
+
+```
+
+## Configuration
+
+### Port du serveur
+- Port par défaut : **8888**
+- Configurable via `process.env.PORT` dans `.env`
+
+### Modèles par défaut
+- **Transcription** : `gpt-4o-mini-transcribe`
+- **Résumé** : `gpt-5.1`
+- **Traduction** : `gpt-4o-mini` (hardcodé)
+
+### Variables d'environnement requises
+```env
+OPENAI_API_KEY=sk-...
+PORT=8888 # optionnel
+OUTPUT_DIR=./output # optionnel
+```
+
+## Commandes importantes
+
+```bash
+# Lancer le serveur
+npm run server
+
+# Lancer le CLI
+npm run cli
+
+# Installer les dépendances
+npm install
+```
+
+## Points d'attention
+
+### Paramètres outputPath
+Tous les endpoints supportent maintenant un paramètre `outputPath` optionnel pour spécifier un répertoire de sortie personnalisé. Si non spécifié, le répertoire par défaut `OUTPUT_DIR` est utilisé.
+
+### Modèles de transcription disponibles
+- `gpt-4o-mini-transcribe` (par défaut) - Rapide et économique
+- `gpt-4o-transcribe` - Qualité supérieure
+- `whisper-1` - Modèle original Whisper (supporte plus de formats)
+
+### Formats de sortie
+- **Transcription** : txt, json, srt, vtt (selon le modèle)
+- **Traduction** : txt
+- **Résumé** : txt
+
+## Règles de développement
+
+1. **Documentation d'abord** : Avant de modifier un endpoint, vérifie `docs/API.md`
+2. **Après modification** : Mets à jour immédiatement `docs/API.md`
+3. **Tests** : Redémarre le serveur après chaque modification
+4. **Cohérence** : Garde la même structure de réponse pour tous les endpoints similaires
+
+## Architecture des endpoints
+
+### Endpoints streaming (SSE)
+- `/download-stream`
+- `/process-stream`
+- `/summarize-stream`
+
+Ces endpoints utilisent Server-Sent Events pour envoyer des mises à jour de progression en temps réel.
+
+### Endpoints non-streaming
+- `/download`
+- `/process`
+- Tous les endpoints POST avec upload de fichiers
+
+Ces endpoints retournent une réponse unique une fois le traitement terminé.
+
+## Maintenance
+
+Lors de l'ajout de nouvelles fonctionnalités :
+1. Implémente la fonctionnalité dans le service approprié (`src/services/`)
+2. Ajoute les routes dans `src/server.js`
+3. **Mets à jour `docs/API.md` IMMÉDIATEMENT**
+4. Teste l'endpoint avec curl ou Postman
+5. Vérifie que la documentation est claire et complète
+
+## Notes importantes
+
+- Le serveur doit toujours être sur le port **8888**
+- Les clés API OpenAI sont requises pour transcription/traduction/résumé
+- Le répertoire `output/` est créé automatiquement si inexistant
+- Les fichiers uploadés sont stockés dans `OUTPUT_DIR`
+- Les vidéos YouTube sont téléchargées en MP3 automatiquement
diff --git a/docs/API.md b/docs/API.md
index 7b02be2..c54bd38 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -1,561 +1,561 @@
-# API Documentation - Video to MP3 Transcriptor
-
-## Base URL
-```
-http://localhost:8888
-```
-
-## Table of Contents
-- [Health & Info](#health--info)
-- [Download Endpoints](#download-endpoints)
-- [Transcription Endpoints](#transcription-endpoints)
-- [Translation Endpoints](#translation-endpoints)
-- [Summarization Endpoints](#summarization-endpoints)
-- [File Management](#file-management)
-
----
-
-## Health & Info
-
-### GET /health
-Health check endpoint.
-
-**Response:**
-```json
-{
- "status": "ok",
- "timestamp": "2025-11-28T12:00:00.000Z"
-}
-```
-
-### GET /api
-Get API information and available endpoints.
-
-**Response:**
-```json
-{
- "name": "Video to MP3 Transcriptor API",
- "version": "1.0.0",
- "endpoints": { ... }
-}
-```
-
-### GET /info
-Get information about a YouTube video or playlist.
-
-**Query Parameters:**
-- `url` (required): YouTube URL
-
-**Example:**
-```bash
-curl "http://localhost:8888/info?url=https://www.youtube.com/watch?v=VIDEO_ID"
-```
-
-**Response:**
-```json
-{
- "success": true,
- "title": "Video Title",
- "type": "video",
- "duration": 300,
- "channel": "Channel Name",
- "videoCount": 1
-}
-```
-
----
-
-## Download Endpoints
-
-### GET /download-stream
-Download YouTube video(s) to MP3 with Server-Sent Events (SSE) progress updates.
-
-**Query Parameters:**
-- `url` (required): YouTube URL
-- `outputPath` (optional): Custom output directory path
-
-**Example:**
-```bash
-curl "http://localhost:8888/download-stream?url=https://www.youtube.com/watch?v=VIDEO_ID"
-```
-
-**SSE Events:**
-- `info`: Video/playlist information
-- `progress`: Download progress updates
-- `video-complete`: Individual video completion
-- `complete`: All downloads complete
-- `error`: Error occurred
-
-### POST /download
-Download YouTube video(s) to MP3 (non-streaming).
-
-**Body Parameters:**
-```json
-{
- "url": "https://www.youtube.com/watch?v=VIDEO_ID",
- "outputPath": "./custom/path" // optional
-}
-```
-
-**Example:**
-```bash
-curl -X POST http://localhost:8888/download \
- -H "Content-Type: application/json" \
- -d '{"url":"https://www.youtube.com/watch?v=VIDEO_ID"}'
-```
-
-**Response:**
-```json
-{
- "success": true,
- "playlistTitle": null,
- "totalVideos": 1,
- "successCount": 1,
- "failCount": 0,
- "videos": [
- {
- "success": true,
- "title": "Video Title",
- "filePath": "./output/video.mp3",
- "fileUrl": "/files/video.mp3"
- }
- ]
-}
-```
-
----
-
-## Transcription Endpoints
-
-### POST /transcribe
-Transcribe an existing audio file.
-
-**Body Parameters:**
-```json
-{
- "filePath": "./output/audio.mp3",
- "language": "en", // optional (auto-detect if not specified)
- "format": "txt", // optional: txt, json, srt, vtt
- "model": "gpt-4o-mini-transcribe", // optional: gpt-4o-mini-transcribe (default), gpt-4o-transcribe, whisper-1
- "outputPath": "./custom/path" // optional
-}
-```
-
-**Available Models:**
-- `gpt-4o-mini-transcribe` (default) - Fast and cost-effective
-- `gpt-4o-transcribe` - Higher quality
-- `whisper-1` - Original Whisper model (supports more formats)
-
-**Example:**
-```bash
-curl -X POST http://localhost:8888/transcribe \
- -H "Content-Type: application/json" \
- -d '{
- "filePath": "./output/audio.mp3",
- "language": "en",
- "model": "gpt-4o-mini-transcribe"
- }'
-```
-
-**Response:**
-```json
-{
- "success": true,
- "filePath": "./output/audio.mp3",
- "transcriptionPath": "./output/audio.txt",
- "transcriptionUrl": "/files/audio.txt",
- "text": "Transcribed text content..."
-}
-```
-
-### POST /upload-transcribe
-Upload and transcribe audio files.
-
-**Form Data:**
-- `files`: Audio file(s) (multiple files supported, max 50)
-- `language`: Language code (optional)
-- `model`: Transcription model (optional, default: gpt-4o-mini-transcribe)
-- `outputPath`: Custom output directory (optional)
-
-**Example:**
-```bash
-curl -X POST http://localhost:8888/upload-transcribe \
- -F "files=@audio1.mp3" \
- -F "files=@audio2.mp3" \
- -F "language=en" \
- -F "model=gpt-4o-mini-transcribe"
-```
-
-**Response:**
-```json
-{
- "success": true,
- "totalFiles": 2,
- "successCount": 2,
- "failCount": 0,
- "results": [
- {
- "success": true,
- "fileName": "audio1.mp3",
- "transcriptionPath": "./output/audio1.txt",
- "transcriptionUrl": "/files/audio1.txt",
- "text": "Transcription..."
- }
- ]
-}
-```
-
-### GET /process-stream
-Download + Transcribe with SSE progress updates.
-
-**Query Parameters:**
-- `url` (required): YouTube URL
-- `language` (optional): Language code
-- `model` (optional): Transcription model (default: gpt-4o-mini-transcribe)
-- `outputPath` (optional): Custom output directory
-
-**Example:**
-```bash
-curl "http://localhost:8888/process-stream?url=https://www.youtube.com/watch?v=VIDEO_ID&language=en&model=gpt-4o-mini-transcribe"
-```
-
-**SSE Events:**
-- `info`: Video information
-- `progress`: Progress updates (downloading or transcribing)
-- `video-complete`: Download complete
-- `transcribe-complete`: Transcription complete
-- `complete`: All operations complete
-- `error`: Error occurred
-
-### POST /process
-Download + Transcribe (non-streaming).
-
-**Body Parameters:**
-```json
-{
- "url": "https://www.youtube.com/watch?v=VIDEO_ID",
- "language": "en", // optional
- "format": "txt", // optional
- "model": "gpt-4o-mini-transcribe", // optional
- "outputPath": "./custom/path" // optional
-}
-```
-
-**Example:**
-```bash
-curl -X POST http://localhost:8888/process \
- -H "Content-Type: application/json" \
- -d '{
- "url": "https://www.youtube.com/watch?v=VIDEO_ID",
- "language": "en",
- "model": "gpt-4o-mini-transcribe"
- }'
-```
-
-**Response:**
-```json
-{
- "success": true,
- "playlistTitle": null,
- "totalVideos": 1,
- "downloadedCount": 1,
- "transcribedCount": 1,
- "results": [
- {
- "title": "Video Title",
- "downloadSuccess": true,
- "audioPath": "./output/video.mp3",
- "audioUrl": "/files/video.mp3",
- "transcriptionSuccess": true,
- "transcriptionPath": "./output/video.txt",
- "transcriptionUrl": "/files/video.txt",
- "text": "Transcription..."
- }
- ]
-}
-```
-
----
-
-## Translation Endpoints
-
-### GET /languages
-Get available translation languages.
-
-**Response:**
-```json
-{
- "languages": {
- "en": "English",
- "fr": "French",
- "es": "Spanish",
- "de": "German",
- "zh": "Chinese",
- "ja": "Japanese",
- ...
- }
-}
-```
-
-### POST /translate
-Translate text.
-
-**Body Parameters:**
-```json
-{
- "text": "Text to translate",
- "targetLang": "fr", // required: target language code
- "sourceLang": "en" // optional: source language (auto-detect if not specified)
-}
-```
-
-**Example:**
-```bash
-curl -X POST http://localhost:8888/translate \
- -H "Content-Type: application/json" \
- -d '{
- "text": "Hello, how are you?",
- "targetLang": "fr"
- }'
-```
-
-**Response:**
-```json
-{
- "success": true,
- "originalText": "Hello, how are you?",
- "translatedText": "Bonjour, comment allez-vous ?",
- "targetLanguage": "French",
- "sourceLanguage": "auto-detected",
- "chunks": 1
-}
-```
-
-### POST /translate-file
-Translate uploaded text files.
-
-**Form Data:**
-- `files`: Text file(s) (.txt, multiple files supported, max 50)
-- `targetLang`: Target language code (required)
-- `sourceLang`: Source language code (optional)
-- `outputPath`: Custom output directory (optional)
-
-**Example:**
-```bash
-curl -X POST http://localhost:8888/translate-file \
- -F "files=@document.txt" \
- -F "targetLang=fr" \
- -F "sourceLang=en"
-```
-
-**Response:**
-```json
-{
- "success": true,
- "totalFiles": 1,
- "successCount": 1,
- "failCount": 0,
- "results": [
- {
- "success": true,
- "fileName": "document.txt",
- "translationPath": "./output/document_fr.txt",
- "translationUrl": "/files/document_fr.txt",
- "translatedText": "Translated content..."
- }
- ]
-}
-```
-
----
-
-## Summarization Endpoints
-
-### GET /summary-styles
-Get available summary styles.
-
-**Response:**
-```json
-{
- "styles": {
- "concise": "A brief summary capturing main points",
- "detailed": "A comprehensive summary with nuances",
- "bullet": "Key points as bullet points"
- }
-}
-```
-
-### POST /summarize
-Summarize text using GPT-5.1.
-
-**Body Parameters:**
-```json
-{
- "text": "Long text to summarize...",
- "style": "concise", // optional: concise (default), detailed, bullet
- "language": "same", // optional: 'same' (default) or language code
- "model": "gpt-5.1" // optional: default is gpt-5.1
-}
-```
-
-**Example:**
-```bash
-curl -X POST http://localhost:8888/summarize \
- -H "Content-Type: application/json" \
- -d '{
- "text": "Long article content...",
- "style": "bullet",
- "language": "same"
- }'
-```
-
-**Response:**
-```json
-{
- "success": true,
- "summary": "Summary content...",
- "model": "gpt-5.1",
- "style": "bullet",
- "inputLength": 5000,
- "chunks": 1
-}
-```
-
-### POST /summarize-file
-Summarize uploaded text files using GPT-5.1.
-
-**Form Data:**
-- `files`: Text file(s) (.txt, multiple files supported, max 50)
-- `style`: Summary style (optional, default: concise)
-- `language`: Output language (optional, default: same)
-- `model`: AI model (optional, default: gpt-5.1)
-- `outputPath`: Custom output directory (optional)
-
-**Example:**
-```bash
-curl -X POST http://localhost:8888/summarize-file \
- -F "files=@article.txt" \
- -F "style=detailed" \
- -F "language=same"
-```
-
-**Response:**
-```json
-{
- "success": true,
- "totalFiles": 1,
- "successCount": 1,
- "failCount": 0,
- "results": [
- {
- "success": true,
- "fileName": "article.txt",
- "summaryPath": "./output/article_summary.txt",
- "summaryUrl": "/files/article_summary.txt",
- "summary": "Summary content...",
- "model": "gpt-5.1",
- "chunks": 1
- }
- ]
-}
-```
-
-### GET /summarize-stream
-Full pipeline: Download -> Transcribe -> Summarize with SSE progress.
-
-**Query Parameters:**
-- `url` (required): YouTube URL
-- `style` (optional): Summary style (default: concise)
-- `language` (optional): Output language (default: same)
-- `model` (optional): Transcription model (default: gpt-4o-mini-transcribe)
-- `outputPath` (optional): Custom output directory
-
-**Example:**
-```bash
-curl "http://localhost:8888/summarize-stream?url=https://www.youtube.com/watch?v=VIDEO_ID&style=bullet&model=gpt-4o-mini-transcribe"
-```
-
-**SSE Events:**
-- `info`: Video information
-- `progress`: Progress updates (downloading, transcribing, or summarizing)
-- `video-complete`: Download complete
-- `transcribe-complete`: Transcription complete
-- `summarize-complete`: Summary complete
-- `complete`: All operations complete
-- `error`: Error occurred
-
----
-
-## File Management
-
-### GET /files-list
-List all downloaded/generated files.
-
-**Example:**
-```bash
-curl http://localhost:8888/files-list
-```
-
-**Response:**
-```json
-{
- "files": [
- {
- "name": "video.mp3",
- "url": "/files/video.mp3",
- "path": "./output/video.mp3"
- },
- {
- "name": "video.txt",
- "url": "/files/video.txt",
- "path": "./output/video.txt"
- }
- ]
-}
-```
-
-### GET /files/:filename
-Serve a specific file.
-
-**Example:**
-```bash
-curl http://localhost:8888/files/video.mp3 --output video.mp3
-```
-
----
-
-## Error Responses
-
-All endpoints return error responses in the following format:
-
-```json
-{
- "error": "Error message describing what went wrong"
-}
-```
-
-Common HTTP status codes:
-- `400` - Bad Request (missing required parameters)
-- `500` - Internal Server Error (processing failed)
-
----
-
-## Notes
-
-### Output Paths
-All endpoints that support `outputPath` parameter:
-- If not specified, files are saved to the default `OUTPUT_DIR` (./output)
-- If specified, files are saved to the custom path provided
-
-### Models
-- **Transcription**: Default is `gpt-4o-mini-transcribe` (cost-effective)
-- **Summarization**: Default is `gpt-5.1` (latest GPT model)
-- **Translation**: Uses `gpt-4o-mini` (hardcoded)
-
-### File Formats
-- **Audio**: MP3, WAV, M4A, OGG, FLAC
-- **Text**: TXT files
-- **Transcription outputs**: TXT, JSON, SRT, VTT (depending on model)
-
-### API Key
-Ensure `OPENAI_API_KEY` is set in your `.env` file for transcription, translation, and summarization features to work.
+# API Documentation - Video to MP3 Transcriptor
+
+## Base URL
+```
+http://localhost:8888
+```
+
+## Table of Contents
+- [Health & Info](#health--info)
+- [Download Endpoints](#download-endpoints)
+- [Transcription Endpoints](#transcription-endpoints)
+- [Translation Endpoints](#translation-endpoints)
+- [Summarization Endpoints](#summarization-endpoints)
+- [File Management](#file-management)
+
+---
+
+## Health & Info
+
+### GET /health
+Health check endpoint.
+
+**Response:**
+```json
+{
+ "status": "ok",
+ "timestamp": "2025-11-28T12:00:00.000Z"
+}
+```
+
+### GET /api
+Get API information and available endpoints.
+
+**Response:**
+```json
+{
+ "name": "Video to MP3 Transcriptor API",
+ "version": "1.0.0",
+ "endpoints": { ... }
+}
+```
+
+### GET /info
+Get information about a YouTube video or playlist.
+
+**Query Parameters:**
+- `url` (required): YouTube URL
+
+**Example:**
+```bash
+curl "http://localhost:8888/info?url=https://www.youtube.com/watch?v=VIDEO_ID"
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "title": "Video Title",
+ "type": "video",
+ "duration": 300,
+ "channel": "Channel Name",
+ "videoCount": 1
+}
+```
+
+---
+
+## Download Endpoints
+
+### GET /download-stream
+Download YouTube video(s) to MP3 with Server-Sent Events (SSE) progress updates.
+
+**Query Parameters:**
+- `url` (required): YouTube URL
+- `outputPath` (optional): Custom output directory path
+
+**Example:**
+```bash
+curl "http://localhost:8888/download-stream?url=https://www.youtube.com/watch?v=VIDEO_ID"
+```
+
+**SSE Events:**
+- `info`: Video/playlist information
+- `progress`: Download progress updates
+- `video-complete`: Individual video completion
+- `complete`: All downloads complete
+- `error`: Error occurred
+
+### POST /download
+Download YouTube video(s) to MP3 (non-streaming).
+
+**Body Parameters:**
+```json
+{
+ "url": "https://www.youtube.com/watch?v=VIDEO_ID",
+ "outputPath": "./custom/path" // optional
+}
+```
+
+**Example:**
+```bash
+curl -X POST http://localhost:8888/download \
+ -H "Content-Type: application/json" \
+ -d '{"url":"https://www.youtube.com/watch?v=VIDEO_ID"}'
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "playlistTitle": null,
+ "totalVideos": 1,
+ "successCount": 1,
+ "failCount": 0,
+ "videos": [
+ {
+ "success": true,
+ "title": "Video Title",
+ "filePath": "./output/video.mp3",
+ "fileUrl": "/files/video.mp3"
+ }
+ ]
+}
+```
+
+---
+
+## Transcription Endpoints
+
+### POST /transcribe
+Transcribe an existing audio file.
+
+**Body Parameters:**
+```json
+{
+ "filePath": "./output/audio.mp3",
+ "language": "en", // optional (auto-detect if not specified)
+ "format": "txt", // optional: txt, json, srt, vtt
+ "model": "gpt-4o-mini-transcribe", // optional: gpt-4o-mini-transcribe (default), gpt-4o-transcribe, whisper-1
+ "outputPath": "./custom/path" // optional
+}
+```
+
+**Available Models:**
+- `gpt-4o-mini-transcribe` (default) - Fast and cost-effective
+- `gpt-4o-transcribe` - Higher quality
+- `whisper-1` - Original Whisper model (supports more formats)
+
+**Example:**
+```bash
+curl -X POST http://localhost:8888/transcribe \
+ -H "Content-Type: application/json" \
+ -d '{
+ "filePath": "./output/audio.mp3",
+ "language": "en",
+ "model": "gpt-4o-mini-transcribe"
+ }'
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "filePath": "./output/audio.mp3",
+ "transcriptionPath": "./output/audio.txt",
+ "transcriptionUrl": "/files/audio.txt",
+ "text": "Transcribed text content..."
+}
+```
+
+### POST /upload-transcribe
+Upload and transcribe audio files.
+
+**Form Data:**
+- `files`: Audio file(s) (multiple files supported, max 50)
+- `language`: Language code (optional)
+- `model`: Transcription model (optional, default: gpt-4o-mini-transcribe)
+- `outputPath`: Custom output directory (optional)
+
+**Example:**
+```bash
+curl -X POST http://localhost:8888/upload-transcribe \
+ -F "files=@audio1.mp3" \
+ -F "files=@audio2.mp3" \
+ -F "language=en" \
+ -F "model=gpt-4o-mini-transcribe"
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "totalFiles": 2,
+ "successCount": 2,
+ "failCount": 0,
+ "results": [
+ {
+ "success": true,
+ "fileName": "audio1.mp3",
+ "transcriptionPath": "./output/audio1.txt",
+ "transcriptionUrl": "/files/audio1.txt",
+ "text": "Transcription..."
+ }
+ ]
+}
+```
+
+### GET /process-stream
+Download + Transcribe with SSE progress updates.
+
+**Query Parameters:**
+- `url` (required): YouTube URL
+- `language` (optional): Language code
+- `model` (optional): Transcription model (default: gpt-4o-mini-transcribe)
+- `outputPath` (optional): Custom output directory
+
+**Example:**
+```bash
+curl "http://localhost:8888/process-stream?url=https://www.youtube.com/watch?v=VIDEO_ID&language=en&model=gpt-4o-mini-transcribe"
+```
+
+**SSE Events:**
+- `info`: Video information
+- `progress`: Progress updates (downloading or transcribing)
+- `video-complete`: Download complete
+- `transcribe-complete`: Transcription complete
+- `complete`: All operations complete
+- `error`: Error occurred
+
+### POST /process
+Download + Transcribe (non-streaming).
+
+**Body Parameters:**
+```json
+{
+ "url": "https://www.youtube.com/watch?v=VIDEO_ID",
+ "language": "en", // optional
+ "format": "txt", // optional
+ "model": "gpt-4o-mini-transcribe", // optional
+ "outputPath": "./custom/path" // optional
+}
+```
+
+**Example:**
+```bash
+curl -X POST http://localhost:8888/process \
+ -H "Content-Type: application/json" \
+ -d '{
+ "url": "https://www.youtube.com/watch?v=VIDEO_ID",
+ "language": "en",
+ "model": "gpt-4o-mini-transcribe"
+ }'
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "playlistTitle": null,
+ "totalVideos": 1,
+ "downloadedCount": 1,
+ "transcribedCount": 1,
+ "results": [
+ {
+ "title": "Video Title",
+ "downloadSuccess": true,
+ "audioPath": "./output/video.mp3",
+ "audioUrl": "/files/video.mp3",
+ "transcriptionSuccess": true,
+ "transcriptionPath": "./output/video.txt",
+ "transcriptionUrl": "/files/video.txt",
+ "text": "Transcription..."
+ }
+ ]
+}
+```
+
+---
+
+## Translation Endpoints
+
+### GET /languages
+Get available translation languages.
+
+**Response:**
+```json
+{
+ "languages": {
+ "en": "English",
+ "fr": "French",
+ "es": "Spanish",
+ "de": "German",
+ "zh": "Chinese",
+ "ja": "Japanese",
+ ...
+ }
+}
+```
+
+### POST /translate
+Translate text.
+
+**Body Parameters:**
+```json
+{
+ "text": "Text to translate",
+ "targetLang": "fr", // required: target language code
+ "sourceLang": "en" // optional: source language (auto-detect if not specified)
+}
+```
+
+**Example:**
+```bash
+curl -X POST http://localhost:8888/translate \
+ -H "Content-Type: application/json" \
+ -d '{
+ "text": "Hello, how are you?",
+ "targetLang": "fr"
+ }'
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "originalText": "Hello, how are you?",
+ "translatedText": "Bonjour, comment allez-vous ?",
+ "targetLanguage": "French",
+ "sourceLanguage": "auto-detected",
+ "chunks": 1
+}
+```
+
+### POST /translate-file
+Translate uploaded text files.
+
+**Form Data:**
+- `files`: Text file(s) (.txt, multiple files supported, max 50)
+- `targetLang`: Target language code (required)
+- `sourceLang`: Source language code (optional)
+- `outputPath`: Custom output directory (optional)
+
+**Example:**
+```bash
+curl -X POST http://localhost:8888/translate-file \
+ -F "files=@document.txt" \
+ -F "targetLang=fr" \
+ -F "sourceLang=en"
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "totalFiles": 1,
+ "successCount": 1,
+ "failCount": 0,
+ "results": [
+ {
+ "success": true,
+ "fileName": "document.txt",
+ "translationPath": "./output/document_fr.txt",
+ "translationUrl": "/files/document_fr.txt",
+ "translatedText": "Translated content..."
+ }
+ ]
+}
+```
+
+---
+
+## Summarization Endpoints
+
+### GET /summary-styles
+Get available summary styles.
+
+**Response:**
+```json
+{
+ "styles": {
+ "concise": "A brief summary capturing main points",
+ "detailed": "A comprehensive summary with nuances",
+ "bullet": "Key points as bullet points"
+ }
+}
+```
+
+### POST /summarize
+Summarize text using GPT-5.1.
+
+**Body Parameters:**
+```json
+{
+ "text": "Long text to summarize...",
+ "style": "concise", // optional: concise (default), detailed, bullet
+ "language": "same", // optional: 'same' (default) or language code
+ "model": "gpt-5.1" // optional: default is gpt-5.1
+}
+```
+
+**Example:**
+```bash
+curl -X POST http://localhost:8888/summarize \
+ -H "Content-Type: application/json" \
+ -d '{
+ "text": "Long article content...",
+ "style": "bullet",
+ "language": "same"
+ }'
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "summary": "Summary content...",
+ "model": "gpt-5.1",
+ "style": "bullet",
+ "inputLength": 5000,
+ "chunks": 1
+}
+```
+
+### POST /summarize-file
+Summarize uploaded text files using GPT-5.1.
+
+**Form Data:**
+- `files`: Text file(s) (.txt, multiple files supported, max 50)
+- `style`: Summary style (optional, default: concise)
+- `language`: Output language (optional, default: same)
+- `model`: AI model (optional, default: gpt-5.1)
+- `outputPath`: Custom output directory (optional)
+
+**Example:**
+```bash
+curl -X POST http://localhost:8888/summarize-file \
+ -F "files=@article.txt" \
+ -F "style=detailed" \
+ -F "language=same"
+```
+
+**Response:**
+```json
+{
+ "success": true,
+ "totalFiles": 1,
+ "successCount": 1,
+ "failCount": 0,
+ "results": [
+ {
+ "success": true,
+ "fileName": "article.txt",
+ "summaryPath": "./output/article_summary.txt",
+ "summaryUrl": "/files/article_summary.txt",
+ "summary": "Summary content...",
+ "model": "gpt-5.1",
+ "chunks": 1
+ }
+ ]
+}
+```
+
+### GET /summarize-stream
+Full pipeline: Download -> Transcribe -> Summarize with SSE progress.
+
+**Query Parameters:**
+- `url` (required): YouTube URL
+- `style` (optional): Summary style (default: concise)
+- `language` (optional): Output language (default: same)
+- `model` (optional): Transcription model (default: gpt-4o-mini-transcribe)
+- `outputPath` (optional): Custom output directory
+
+**Example:**
+```bash
+curl "http://localhost:8888/summarize-stream?url=https://www.youtube.com/watch?v=VIDEO_ID&style=bullet&model=gpt-4o-mini-transcribe"
+```
+
+**SSE Events:**
+- `info`: Video information
+- `progress`: Progress updates (downloading, transcribing, or summarizing)
+- `video-complete`: Download complete
+- `transcribe-complete`: Transcription complete
+- `summarize-complete`: Summary complete
+- `complete`: All operations complete
+- `error`: Error occurred
+
+---
+
+## File Management
+
+### GET /files-list
+List all downloaded/generated files.
+
+**Example:**
+```bash
+curl http://localhost:8888/files-list
+```
+
+**Response:**
+```json
+{
+ "files": [
+ {
+ "name": "video.mp3",
+ "url": "/files/video.mp3",
+ "path": "./output/video.mp3"
+ },
+ {
+ "name": "video.txt",
+ "url": "/files/video.txt",
+ "path": "./output/video.txt"
+ }
+ ]
+}
+```
+
+### GET /files/:filename
+Serve a specific file.
+
+**Example:**
+```bash
+curl http://localhost:8888/files/video.mp3 --output video.mp3
+```
+
+---
+
+## Error Responses
+
+All endpoints return error responses in the following format:
+
+```json
+{
+ "error": "Error message describing what went wrong"
+}
+```
+
+Common HTTP status codes:
+- `400` - Bad Request (missing required parameters)
+- `500` - Internal Server Error (processing failed)
+
+---
+
+## Notes
+
+### Output Paths
+All endpoints that support `outputPath` parameter:
+- If not specified, files are saved to the default `OUTPUT_DIR` (./output)
+- If specified, files are saved to the custom path provided
+
+### Models
+- **Transcription**: Default is `gpt-4o-mini-transcribe` (cost-effective)
+- **Summarization**: Default is `gpt-5.1` (latest GPT model)
+- **Translation**: Uses `gpt-4o-mini` (hardcoded)
+
+### File Formats
+- **Audio**: MP3, WAV, M4A, OGG, FLAC
+- **Text**: TXT files
+- **Transcription outputs**: TXT, JSON, SRT, VTT (depending on model)
+
+### API Key
+Ensure `OPENAI_API_KEY` is set in your `.env` file for transcription, translation, and summarization features to work.
diff --git a/nul b/nul
new file mode 100644
index 0000000..42de59f
--- /dev/null
+++ b/nul
@@ -0,0 +1 @@
+/usr/bin/bash: line 1: del: command not found
diff --git a/public/app.js b/public/app.js
index c1cf74d..76c17b3 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,1129 +1,1129 @@
-// API Base URL
-const API_URL = '';
-
-// Tab switching
-document.querySelectorAll('.tab').forEach(tab => {
- tab.addEventListener('click', () => {
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
- document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
- tab.classList.add('active');
- document.getElementById(tab.dataset.tab).classList.add('active');
- });
-});
-
-// Helper: Show result
-function showResult(elementId, success, content) {
- const el = document.getElementById(elementId);
- el.className = `result show ${success ? 'success' : 'error'}`;
- el.innerHTML = content;
-}
-
-// Helper: Set loading state
-function setLoading(button, loading) {
- button.disabled = loading;
- button.classList.toggle('loading', loading);
-}
-
-// Format seconds to MM:SS or HH:MM:SS
-function formatTime(seconds) {
- if (!seconds || seconds < 0) return '--:--';
- const hrs = Math.floor(seconds / 3600);
- const mins = Math.floor((seconds % 3600) / 60);
- const secs = Math.floor(seconds % 60);
- if (hrs > 0) {
- return `${hrs}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
- }
- return `${mins}:${String(secs).padStart(2, '0')}`;
-}
-
-// Format file size
-function formatSize(bytes) {
- if (!bytes) return '';
- const units = ['B', 'KB', 'MB', 'GB'];
- let i = 0;
- while (bytes >= 1024 && i < units.length - 1) {
- bytes /= 1024;
- i++;
- }
- return `${bytes.toFixed(1)} ${units[i]}`;
-}
-
-// ==================== DOWNLOAD TAB ====================
-
-const progressContainer = document.getElementById('download-progress');
-const progressFill = document.getElementById('progress-fill');
-const progressPercent = document.getElementById('progress-percent');
-const progressTitle = document.getElementById('progress-title');
-const progressEta = document.getElementById('progress-eta');
-const progressInfo = document.getElementById('progress-info');
-const progressSpeed = document.getElementById('progress-speed');
-const progressCurrent = document.getElementById('progress-current');
-
-function updateDownloadProgress(data) {
- progressFill.style.width = `${data.percent}%`;
- progressPercent.textContent = `${data.percent}%`;
-
- if (data.totalVideos > 1) {
- progressInfo.textContent = `Video ${data.currentVideo}/${data.totalVideos}`;
- } else {
- progressInfo.textContent = '';
- }
-
- if (data.speed) progressSpeed.textContent = data.speed;
- if (data.estimatedRemaining) {
- progressEta.textContent = `ETA: ${formatTime(data.estimatedRemaining)}`;
- } else if (data.eta) {
- progressEta.textContent = `ETA: ${data.eta}`;
- }
- if (data.title) {
- progressCurrent.innerHTML = `Downloading: ${data.title}`;
- }
-}
-
-function resetDownloadProgress() {
- progressFill.style.width = '0%';
- progressPercent.textContent = '0%';
- progressTitle.textContent = 'Downloading...';
- progressEta.textContent = '';
- progressInfo.textContent = '';
- progressSpeed.textContent = '';
- progressCurrent.textContent = '';
-}
-
-document.getElementById('download-form').addEventListener('submit', async (e) => {
- e.preventDefault();
- const button = e.target.querySelector('button[type="submit"]');
- const url = document.getElementById('download-url').value;
- const resultDiv = document.getElementById('download-result');
-
- setLoading(button, true);
- resetDownloadProgress();
- progressContainer.style.display = 'block';
- resultDiv.classList.remove('show');
-
- const eventSource = new EventSource(`${API_URL}/download-stream?url=${encodeURIComponent(url)}`);
-
- eventSource.addEventListener('status', (e) => {
- progressTitle.textContent = JSON.parse(e.data).message;
- });
-
- eventSource.addEventListener('info', (e) => {
- const data = JSON.parse(e.data);
- progressTitle.textContent = data.totalVideos > 1
- ? `Downloading playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
- : `Downloading: ${data.title}`;
- });
-
- eventSource.addEventListener('progress', (e) => updateDownloadProgress(JSON.parse(e.data)));
-
- eventSource.addEventListener('video-complete', (e) => {
- const data = JSON.parse(e.data);
- progressCurrent.innerHTML = `Completed: ${data.title} (${data.videosCompleted}/${data.totalVideos})`;
- });
-
- eventSource.addEventListener('complete', (e) => {
- const data = JSON.parse(e.data);
- eventSource.close();
- progressFill.style.width = '100%';
- progressPercent.textContent = '100%';
- progressTitle.textContent = 'Download Complete!';
- progressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
-
- showResult('download-result', true, `
-
Download Complete!
- ${data.successCount}/${data.totalVideos} videos downloaded
- ${data.playlistTitle ? `Playlist: ${data.playlistTitle}
` : ''}
- ${data.videos.map(v => `
- -
- ${v.success ? '✓' : '✗'}
- ${v.title}
- ${v.success && v.fileUrl ? `Download` : ''}
- ${v.error ? `(${v.error})` : ''}
-
- `).join('')}
- `);
- setLoading(button, false);
- });
-
- eventSource.addEventListener('error', (e) => {
- let errorMsg = 'Download failed';
- try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
- eventSource.close();
- progressContainer.style.display = 'none';
- showResult('download-result', false, `Error
${errorMsg}
`);
- setLoading(button, false);
- });
-
- eventSource.onerror = () => {
- eventSource.close();
- progressContainer.style.display = 'none';
- showResult('download-result', false, `Error
Connection lost
`);
- setLoading(button, false);
- };
-});
-
-// ==================== CONVERT TAB (Video to MP3) ====================
-
-let convertSelectedFiles = [];
-const convertDropZone = document.getElementById('convert-drop-zone');
-const convertFileInput = document.getElementById('convert-file-input');
-const convertSelectedFilesDiv = document.getElementById('convert-selected-files');
-const convertFilesList = document.getElementById('convert-files-list');
-const convertBtn = document.getElementById('convert-btn');
-const convertClearFilesBtn = document.getElementById('convert-clear-files');
-
-function updateConvertFilesList() {
- if (convertSelectedFiles.length === 0) {
- convertSelectedFilesDiv.style.display = 'none';
- convertBtn.disabled = true;
- return;
- }
-
- convertSelectedFilesDiv.style.display = 'block';
- convertBtn.disabled = false;
-
- convertFilesList.innerHTML = convertSelectedFiles.map((file, index) => `
-
- ${file.name}
- ${formatSize(file.size)}
-
-
- `).join('');
-
- // Add remove handlers
- convertFilesList.querySelectorAll('.remove-file').forEach(btn => {
- btn.addEventListener('click', () => {
- convertSelectedFiles.splice(parseInt(btn.dataset.index), 1);
- updateConvertFilesList();
- });
- });
-}
-
-function addConvertFiles(files) {
- const videoAudioFiles = Array.from(files).filter(f =>
- f.type.startsWith('video/') || f.type.startsWith('audio/') ||
- f.name.match(/\.(mp4|avi|mkv|mov|m4a|wav|flac|ogg|webm|wmv|flv)$/i)
- );
- convertSelectedFiles = [...convertSelectedFiles, ...videoAudioFiles];
- updateConvertFilesList();
-}
-
-// Drag & Drop events for convert
-convertDropZone.addEventListener('click', () => convertFileInput.click());
-
-convertDropZone.addEventListener('dragover', (e) => {
- e.preventDefault();
- convertDropZone.classList.add('drag-over');
-});
-
-convertDropZone.addEventListener('dragleave', () => {
- convertDropZone.classList.remove('drag-over');
-});
-
-convertDropZone.addEventListener('drop', (e) => {
- e.preventDefault();
- convertDropZone.classList.remove('drag-over');
- addConvertFiles(e.dataTransfer.files);
-});
-
-convertFileInput.addEventListener('change', () => {
- addConvertFiles(convertFileInput.files);
- convertFileInput.value = '';
-});
-
-convertClearFilesBtn.addEventListener('click', () => {
- convertSelectedFiles = [];
- updateConvertFilesList();
-});
-
-// Convert form submit
-document.getElementById('convert-form').addEventListener('submit', async (e) => {
- e.preventDefault();
- if (convertSelectedFiles.length === 0) return;
-
- const button = convertBtn;
- const bitrate = document.getElementById('convert-bitrate').value;
- const quality = document.getElementById('convert-quality').value;
-
- const convertProgress = document.getElementById('convert-progress');
- const convertProgressFill = document.getElementById('convert-progress-fill');
- const convertProgressTitle = document.getElementById('convert-progress-title');
- const convertProgressPercent = document.getElementById('convert-progress-percent');
- const convertProgressInfo = document.getElementById('convert-progress-info');
- const convertProgressCurrent = document.getElementById('convert-progress-current');
-
- setLoading(button, true);
- convertProgress.style.display = 'block';
- convertProgressFill.style.width = '0%';
- convertProgressTitle.textContent = 'Converting to MP3...';
- convertProgressPercent.textContent = '0%';
- convertProgressInfo.textContent = `0/${convertSelectedFiles.length} files`;
- document.getElementById('convert-result').classList.remove('show');
-
- const formData = new FormData();
- convertSelectedFiles.forEach(file => formData.append('files', file));
- formData.append('bitrate', bitrate);
- formData.append('quality', quality);
-
- try {
- const response = await fetch(`${API_URL}/convert-to-mp3`, {
- method: 'POST',
- body: formData
- });
-
- const data = await response.json();
-
- if (!response.ok) throw new Error(data.error || 'Conversion failed');
-
- convertProgressFill.style.width = '100%';
- convertProgressPercent.textContent = '100%';
- convertProgressTitle.textContent = 'Conversion Complete!';
- convertProgressInfo.textContent = `${data.successCount}/${data.totalFiles} files`;
-
- showResult('convert-result', true, `
- Conversion Complete!
- ${data.successCount}/${data.totalFiles} files converted to MP3
-
- ${data.results.map(r => r.success ? `
-
- ` : `
-
- ${r.fileName}
- ${r.error}
-
- `).join('')}
-
- `);
-
- // Clear selected files after successful conversion
- convertSelectedFiles = [];
- updateConvertFilesList();
-
- } catch (error) {
- showResult('convert-result', false, `Error
${error.message}
`);
- } finally {
- setLoading(button, false);
- setTimeout(() => {
- convertProgress.style.display = 'none';
- }, 1000);
- }
-});
-
-// ==================== TRANSCRIBE TAB (Drag & Drop) ====================
-
-let selectedFiles = [];
-const dropZone = document.getElementById('drop-zone');
-const fileInput = document.getElementById('file-input');
-const selectedFilesDiv = document.getElementById('selected-files');
-const filesList = document.getElementById('files-list');
-const transcribeBtn = document.getElementById('transcribe-btn');
-const clearFilesBtn = document.getElementById('clear-files');
-
-function updateFilesList() {
- if (selectedFiles.length === 0) {
- selectedFilesDiv.style.display = 'none';
- transcribeBtn.disabled = true;
- return;
- }
-
- selectedFilesDiv.style.display = 'block';
- transcribeBtn.disabled = false;
-
- filesList.innerHTML = selectedFiles.map((file, index) => `
-
- ${file.name}
- ${formatSize(file.size)}
-
-
- `).join('');
-
- // Add remove handlers
- filesList.querySelectorAll('.remove-file').forEach(btn => {
- btn.addEventListener('click', () => {
- selectedFiles.splice(parseInt(btn.dataset.index), 1);
- updateFilesList();
- });
- });
-}
-
-function addFiles(files) {
- const audioFiles = Array.from(files).filter(f =>
- f.type.startsWith('audio/') || f.name.match(/\.(mp3|wav|m4a|ogg|flac)$/i)
- );
- selectedFiles = [...selectedFiles, ...audioFiles];
- updateFilesList();
-}
-
-// Drag & Drop events
-dropZone.addEventListener('click', () => fileInput.click());
-
-dropZone.addEventListener('dragover', (e) => {
- e.preventDefault();
- dropZone.classList.add('drag-over');
-});
-
-dropZone.addEventListener('dragleave', () => {
- dropZone.classList.remove('drag-over');
-});
-
-dropZone.addEventListener('drop', (e) => {
- e.preventDefault();
- dropZone.classList.remove('drag-over');
- addFiles(e.dataTransfer.files);
-});
-
-fileInput.addEventListener('change', () => {
- addFiles(fileInput.files);
- fileInput.value = '';
-});
-
-clearFilesBtn.addEventListener('click', () => {
- selectedFiles = [];
- updateFilesList();
-});
-
-// Transcribe form submit
-document.getElementById('transcribe-form').addEventListener('submit', async (e) => {
- e.preventDefault();
- if (selectedFiles.length === 0) return;
-
- const button = transcribeBtn;
- const language = document.getElementById('transcribe-lang').value;
- const model = document.getElementById('transcribe-model').value;
-
- const transcribeProgress = document.getElementById('transcribe-progress');
- const transcribeProgressFill = document.getElementById('transcribe-progress-fill');
- const transcribeProgressTitle = document.getElementById('transcribe-progress-title');
- const transcribeProgressPercent = document.getElementById('transcribe-progress-percent');
- const transcribeProgressInfo = document.getElementById('transcribe-progress-info');
- const transcribeProgressCurrent = document.getElementById('transcribe-progress-current');
-
- setLoading(button, true);
- transcribeProgress.style.display = 'block';
- transcribeProgressFill.style.width = '0%';
- transcribeProgressTitle.textContent = 'Uploading and transcribing...';
- transcribeProgressPercent.textContent = '0%';
- transcribeProgressInfo.textContent = `0/${selectedFiles.length} files`;
- document.getElementById('transcribe-result').classList.remove('show');
-
- const formData = new FormData();
- selectedFiles.forEach(file => formData.append('files', file));
- if (language) formData.append('language', language);
- formData.append('model', model);
-
- try {
- const response = await fetch(`${API_URL}/upload-transcribe`, {
- method: 'POST',
- body: formData
- });
-
- const data = await response.json();
-
- if (!response.ok) throw new Error(data.error || 'Transcription failed');
-
- transcribeProgressFill.style.width = '100%';
- transcribeProgressPercent.textContent = '100%';
- transcribeProgressTitle.textContent = 'Transcription Complete!';
- transcribeProgressInfo.textContent = `${data.successCount}/${data.totalFiles} files`;
-
- showResult('transcribe-result', true, `
- Transcription Complete!
- ${data.successCount}/${data.totalFiles} files transcribed
- ${data.results.map(r => `
- -
- ${r.success ? '✓' : '✗'}
- ${r.fileName}
- ${r.success && r.transcriptionUrl ? `View` : ''}
- ${r.error ? `(${r.error})` : ''}
-
- `).join('')}
- ${data.results[0]?.text ? `
- Preview (first file):
- ${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}
- ` : ''}
- `);
-
- selectedFiles = [];
- updateFilesList();
-
- } catch (error) {
- transcribeProgress.style.display = 'none';
- showResult('transcribe-result', false, `Error
${error.message}
`);
- } finally {
- setLoading(button, false);
- }
-});
-
-// ==================== PROCESS TAB (Download + Transcribe) ====================
-
-const processProgress = document.getElementById('process-progress');
-const processProgressFill = document.getElementById('process-progress-fill');
-const processProgressTitle = document.getElementById('process-progress-title');
-const processProgressPercent = document.getElementById('process-progress-percent');
-const processProgressPhase = document.getElementById('process-progress-phase');
-const processProgressSpeed = document.getElementById('process-progress-speed');
-const processProgressCurrent = document.getElementById('process-progress-current');
-const processProgressEta = document.getElementById('process-progress-eta');
-
-function resetProcessProgress() {
- processProgressFill.style.width = '0%';
- processProgressPercent.textContent = '0%';
- processProgressTitle.textContent = 'Processing...';
- processProgressPhase.textContent = '';
- processProgressSpeed.textContent = '';
- processProgressCurrent.textContent = '';
- processProgressEta.textContent = '';
-}
-
-document.getElementById('process-form').addEventListener('submit', async (e) => {
- e.preventDefault();
- const button = e.target.querySelector('button[type="submit"]');
- const url = document.getElementById('process-url').value;
- const language = document.getElementById('process-lang').value;
- const model = document.getElementById('process-model').value;
- const resultDiv = document.getElementById('process-result');
-
- setLoading(button, true);
- resetProcessProgress();
- processProgress.style.display = 'block';
- resultDiv.classList.remove('show');
-
- const params = new URLSearchParams({ url });
- if (language) params.append('language', language);
- params.append('model', model);
-
- const eventSource = new EventSource(`${API_URL}/process-stream?${params}`);
-
- eventSource.addEventListener('status', (e) => {
- const data = JSON.parse(e.data);
- processProgressTitle.textContent = data.message;
- if (data.phase === 'transcribing') {
- processProgressPhase.textContent = 'Transcribing';
- }
- });
-
- eventSource.addEventListener('info', (e) => {
- const data = JSON.parse(e.data);
- processProgressTitle.textContent = data.totalVideos > 1
- ? `Processing playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
- : `Processing: ${data.title}`;
- });
-
- eventSource.addEventListener('progress', (e) => {
- const data = JSON.parse(e.data);
- processProgressFill.style.width = `${data.percent}%`;
- processProgressPercent.textContent = `${Math.round(data.percent)}%`;
- processProgressPhase.textContent = data.phaseLabel || '';
- if (data.speed) processProgressSpeed.textContent = data.speed;
- if (data.title) {
- processProgressCurrent.innerHTML = `${data.phaseLabel}: ${data.title}`;
- }
- if (data.totalVideos > 1) {
- processProgressCurrent.innerHTML += ` (${data.currentVideo}/${data.totalVideos})`;
- }
- });
-
- eventSource.addEventListener('video-complete', (e) => {
- const data = JSON.parse(e.data);
- processProgressCurrent.innerHTML = `Downloaded: ${data.title}`;
- });
-
- eventSource.addEventListener('transcribe-complete', (e) => {
- const data = JSON.parse(e.data);
- processProgressCurrent.innerHTML = `Transcribed: ${data.title} (${data.videosCompleted}/${data.totalFiles})`;
- });
-
- eventSource.addEventListener('complete', (e) => {
- const data = JSON.parse(e.data);
- eventSource.close();
- processProgressFill.style.width = '100%';
- processProgressPercent.textContent = '100%';
- processProgressTitle.textContent = 'Processing Complete!';
- processProgressPhase.textContent = '';
- processProgressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
-
- showResult('process-result', true, `
- Processing Complete!
- ${data.playlistTitle ? `Playlist: ${data.playlistTitle}
` : ''}
- Downloaded: ${data.downloadedCount}/${data.totalVideos}
- Transcribed: ${data.transcribedCount}/${data.totalVideos}
- ${data.results.map(r => `
- -
- ${r.transcriptionSuccess ? '✓' : '✗'}
- ${r.title}
- ${r.audioUrl ? `MP3` : ''}
- ${r.transcriptionUrl ? `TXT` : ''}
- ${r.error ? `(${r.error})` : ''}
-
- `).join('')}
- ${data.results[0]?.text ? `
- Preview (first file):
- ${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}
- ` : ''}
- `);
- setLoading(button, false);
- });
-
- eventSource.addEventListener('error', (e) => {
- let errorMsg = 'Processing failed';
- try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
- eventSource.close();
- processProgress.style.display = 'none';
- showResult('process-result', false, `Error
${errorMsg}
`);
- setLoading(button, false);
- });
-
- eventSource.onerror = () => {
- eventSource.close();
- processProgress.style.display = 'none';
- showResult('process-result', false, `Error
Connection lost
`);
- setLoading(button, false);
- };
-});
-
-// ==================== TRANSLATE CHECKBOXES (Transcribe & Process tabs) ====================
-
-// Transcribe tab checkbox
-const transcribeTranslateCheckbox = document.getElementById('transcribe-translate');
-const transcribeTranslateLang = document.getElementById('transcribe-translate-lang');
-
-transcribeTranslateCheckbox.addEventListener('change', () => {
- transcribeTranslateLang.disabled = !transcribeTranslateCheckbox.checked;
-});
-
-// Process tab checkbox
-const processTranslateCheckbox = document.getElementById('process-translate');
-const processTranslateLang = document.getElementById('process-translate-lang');
-
-processTranslateCheckbox.addEventListener('change', () => {
- processTranslateLang.disabled = !processTranslateCheckbox.checked;
-});
-
-// ==================== TRANSLATE TAB ====================
-
-// Mode switching
-const translateModeBtns = document.querySelectorAll('.mode-btn');
-const translateTextMode = document.getElementById('translate-text-mode');
-const translateFileMode = document.getElementById('translate-file-mode');
-
-translateModeBtns.forEach(btn => {
- btn.addEventListener('click', () => {
- translateModeBtns.forEach(b => b.classList.remove('active'));
- btn.classList.add('active');
-
- if (btn.dataset.mode === 'text') {
- translateTextMode.style.display = 'block';
- translateFileMode.style.display = 'none';
- } else {
- translateTextMode.style.display = 'none';
- translateFileMode.style.display = 'block';
- }
- });
-});
-
-// Text translation form
-document.getElementById('translate-text-form').addEventListener('submit', async (e) => {
- e.preventDefault();
- const button = document.getElementById('translate-text-btn');
- const text = document.getElementById('translate-input').value;
- const sourceLang = document.getElementById('translate-source').value;
- const targetLang = document.getElementById('translate-target').value;
-
- if (!text.trim()) {
- showResult('translate-text-result', false, 'Error
Please enter text to translate
');
- return;
- }
-
- setLoading(button, true);
- document.getElementById('translate-text-result').classList.remove('show');
-
- try {
- const response = await fetch(`${API_URL}/translate`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ text, targetLang, sourceLang: sourceLang || null })
- });
-
- const data = await response.json();
- if (!response.ok) throw new Error(data.error || 'Translation failed');
-
- showResult('translate-text-result', true, `
- Translation Complete!
- From: ${data.sourceLanguage} To: ${data.targetLanguage}
- ${data.translatedText}
- `);
- } catch (error) {
- showResult('translate-text-result', false, `Error
${error.message}
`);
- } finally {
- setLoading(button, false);
- }
-});
-
-// File translation - Drag & Drop
-let translateSelectedFiles = [];
-const translateDropZone = document.getElementById('translate-drop-zone');
-const translateFileInput = document.getElementById('translate-file-input');
-const translateSelectedFilesDiv = document.getElementById('translate-selected-files');
-const translateFilesList = document.getElementById('translate-files-list');
-const translateFileBtn = document.getElementById('translate-file-btn');
-const translateClearFilesBtn = document.getElementById('translate-clear-files');
-
-function updateTranslateFilesList() {
- if (translateSelectedFiles.length === 0) {
- translateSelectedFilesDiv.style.display = 'none';
- translateFileBtn.disabled = true;
- return;
- }
-
- translateSelectedFilesDiv.style.display = 'block';
- translateFileBtn.disabled = false;
-
- translateFilesList.innerHTML = translateSelectedFiles.map((file, index) => `
-
- ${file.name}
- ${formatSize(file.size)}
-
-
- `).join('');
-
- translateFilesList.querySelectorAll('.remove-file').forEach(btn => {
- btn.addEventListener('click', () => {
- translateSelectedFiles.splice(parseInt(btn.dataset.index), 1);
- updateTranslateFilesList();
- });
- });
-}
-
-function addTranslateFiles(files) {
- const textFiles = Array.from(files).filter(f =>
- f.type === 'text/plain' || f.name.endsWith('.txt')
- );
- translateSelectedFiles = [...translateSelectedFiles, ...textFiles];
- updateTranslateFilesList();
-}
-
-translateDropZone.addEventListener('click', () => translateFileInput.click());
-
-translateDropZone.addEventListener('dragover', (e) => {
- e.preventDefault();
- translateDropZone.classList.add('drag-over');
-});
-
-translateDropZone.addEventListener('dragleave', () => {
- translateDropZone.classList.remove('drag-over');
-});
-
-translateDropZone.addEventListener('drop', (e) => {
- e.preventDefault();
- translateDropZone.classList.remove('drag-over');
- addTranslateFiles(e.dataTransfer.files);
-});
-
-translateFileInput.addEventListener('change', () => {
- addTranslateFiles(translateFileInput.files);
- translateFileInput.value = '';
-});
-
-translateClearFilesBtn.addEventListener('click', () => {
- translateSelectedFiles = [];
- updateTranslateFilesList();
-});
-
-// File translation form submit
-document.getElementById('translate-file-form').addEventListener('submit', async (e) => {
- e.preventDefault();
- if (translateSelectedFiles.length === 0) return;
-
- const button = translateFileBtn;
- const sourceLang = document.getElementById('translate-file-source').value;
- const targetLang = document.getElementById('translate-file-target').value;
-
- setLoading(button, true);
- document.getElementById('translate-file-result').classList.remove('show');
-
- const formData = new FormData();
- translateSelectedFiles.forEach(file => formData.append('files', file));
- formData.append('targetLang', targetLang);
- if (sourceLang) formData.append('sourceLang', sourceLang);
-
- try {
- const response = await fetch(`${API_URL}/translate-file`, {
- method: 'POST',
- body: formData
- });
-
- const data = await response.json();
- if (!response.ok) throw new Error(data.error || 'Translation failed');
-
- showResult('translate-file-result', true, `
- Translation Complete!
- ${data.successCount}/${data.totalFiles} files translated
- ${data.results.map(r => `
- -
- ${r.success ? '✓' : '✗'}
- ${r.fileName || r.originalPath}
- ${r.success && r.translationUrl ? `View` : ''}
- ${r.error ? `(${r.error})` : ''}
-
- `).join('')}
- ${data.results[0]?.translatedText ? `
- Preview (first file):
- ${data.results[0].translatedText.substring(0, 1000)}${data.results[0].translatedText.length > 1000 ? '...' : ''}
- ` : ''}
- `);
-
- translateSelectedFiles = [];
- updateTranslateFilesList();
-
- } catch (error) {
- showResult('translate-file-result', false, `Error
${error.message}
`);
- } finally {
- setLoading(button, false);
- }
-});
-
-// ==================== SUMMARIZE TAB ====================
-
-// Mode switching
-const summarizeModeBtns = document.querySelectorAll('.summarize-mode-selector .mode-btn');
-const summarizeTextMode = document.getElementById('summarize-text-mode');
-const summarizeFileMode = document.getElementById('summarize-file-mode');
-
-const summarizeLinkMode = document.getElementById('summarize-link-mode');
-
-summarizeModeBtns.forEach(btn => {
- btn.addEventListener('click', () => {
- summarizeModeBtns.forEach(b => b.classList.remove('active'));
- btn.classList.add('active');
-
- summarizeTextMode.style.display = 'none';
- summarizeFileMode.style.display = 'none';
- if (summarizeLinkMode) summarizeLinkMode.style.display = 'none';
-
- if (btn.dataset.mode === 'text') {
- summarizeTextMode.style.display = 'block';
- } else if (btn.dataset.mode === 'file') {
- summarizeFileMode.style.display = 'block';
- } else if (btn.dataset.mode === 'link' && summarizeLinkMode) {
- summarizeLinkMode.style.display = 'block';
- }
- });
-});
-
-// Text summarization form
-document.getElementById('summarize-text-form').addEventListener('submit', async (e) => {
- e.preventDefault();
- const button = document.getElementById('summarize-text-btn');
- const text = document.getElementById('summarize-input').value;
- const style = document.getElementById('summarize-style').value;
- const language = document.getElementById('summarize-language').value;
-
- if (!text.trim()) {
- showResult('summarize-text-result', false, 'Error
Please enter text to summarize
');
- return;
- }
-
- setLoading(button, true);
- document.getElementById('summarize-text-result').classList.remove('show');
-
- try {
- const response = await fetch(`${API_URL}/summarize`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ text, style, language })
- });
-
- const data = await response.json();
- if (!response.ok) throw new Error(data.error || 'Summarization failed');
-
- showResult('summarize-text-result', true, `
- Summary Complete!
- Model: ${data.model} | Style: ${data.style} | Chunks: ${data.chunks}
- ${data.summary}
- `);
- } catch (error) {
- showResult('summarize-text-result', false, `Error
${error.message}
`);
- } finally {
- setLoading(button, false);
- }
-});
-
-// File summarization - Drag & Drop
-let summarizeSelectedFiles = [];
-const summarizeDropZone = document.getElementById('summarize-drop-zone');
-const summarizeFileInput = document.getElementById('summarize-file-input');
-const summarizeSelectedFilesDiv = document.getElementById('summarize-selected-files');
-const summarizeFilesList = document.getElementById('summarize-files-list');
-const summarizeFileBtn = document.getElementById('summarize-file-btn');
-const summarizeClearFilesBtn = document.getElementById('summarize-clear-files');
-
-function updateSummarizeFilesList() {
- if (!summarizeSelectedFilesDiv || !summarizeFileBtn || !summarizeFilesList) return;
-
- if (summarizeSelectedFiles.length === 0) {
- summarizeSelectedFilesDiv.style.display = 'none';
- summarizeFileBtn.disabled = true;
- return;
- }
-
- summarizeSelectedFilesDiv.style.display = 'block';
- summarizeFileBtn.disabled = false;
-
- summarizeFilesList.innerHTML = summarizeSelectedFiles.map((file, index) => `
-
- ${file.name}
- ${formatSize(file.size)}
-
-
- `).join('');
-
- summarizeFilesList.querySelectorAll('.remove-file').forEach(btn => {
- btn.addEventListener('click', () => {
- summarizeSelectedFiles.splice(parseInt(btn.dataset.index), 1);
- updateSummarizeFilesList();
- });
- });
-}
-
-function addSummarizeFiles(files) {
- console.log('Adding files:', files);
- const textFiles = Array.from(files).filter(f =>
- f.type === 'text/plain' || f.name.endsWith('.txt')
- );
- console.log('Filtered text files:', textFiles);
- summarizeSelectedFiles = [...summarizeSelectedFiles, ...textFiles];
- updateSummarizeFilesList();
-}
-
-if (summarizeDropZone) {
- summarizeDropZone.addEventListener('click', () => {
- console.log('Drop zone clicked');
- summarizeFileInput.click();
- });
-
- summarizeDropZone.addEventListener('dragover', (e) => {
- e.preventDefault();
- e.stopPropagation();
- summarizeDropZone.classList.add('drag-over');
- });
-
- summarizeDropZone.addEventListener('dragleave', (e) => {
- e.preventDefault();
- e.stopPropagation();
- summarizeDropZone.classList.remove('drag-over');
- });
-
- summarizeDropZone.addEventListener('drop', (e) => {
- e.preventDefault();
- e.stopPropagation();
- console.log('Files dropped:', e.dataTransfer.files);
- summarizeDropZone.classList.remove('drag-over');
- addSummarizeFiles(e.dataTransfer.files);
- });
-}
-
-if (summarizeFileInput) {
- summarizeFileInput.addEventListener('change', () => {
- console.log('File input changed:', summarizeFileInput.files);
- addSummarizeFiles(summarizeFileInput.files);
- summarizeFileInput.value = '';
- });
-}
-
-if (summarizeClearFilesBtn) {
- summarizeClearFilesBtn.addEventListener('click', () => {
- summarizeSelectedFiles = [];
- updateSummarizeFilesList();
- });
-}
-
-// File summarization form submit
-document.getElementById('summarize-file-form').addEventListener('submit', async (e) => {
- e.preventDefault();
- if (summarizeSelectedFiles.length === 0) return;
-
- const button = summarizeFileBtn;
- const style = document.getElementById('summarize-file-style').value;
- const language = document.getElementById('summarize-file-language').value;
-
- setLoading(button, true);
- document.getElementById('summarize-file-result').classList.remove('show');
-
- const formData = new FormData();
- summarizeSelectedFiles.forEach(file => formData.append('files', file));
- formData.append('style', style);
- formData.append('language', language);
-
- try {
- const response = await fetch(`${API_URL}/summarize-file`, {
- method: 'POST',
- body: formData
- });
-
- const data = await response.json();
- if (!response.ok) throw new Error(data.error || 'Summarization failed');
-
- showResult('summarize-file-result', true, `
- Summarization Complete!
- ${data.successCount}/${data.totalFiles} files summarized
- ${data.results.map(r => `
- -
- ${r.success ? '✓' : '✗'}
- ${r.fileName || r.filePath}
- ${r.success && r.summaryUrl ? `View` : ''}
- ${r.error ? `(${r.error})` : ''}
-
- `).join('')}
- ${data.results[0]?.summary ? `
- Preview (first file):
- ${data.results[0].summary.substring(0, 1000)}${data.results[0].summary.length > 1000 ? '...' : ''}
- ` : ''}
- `);
-
- summarizeSelectedFiles = [];
- updateSummarizeFilesList();
-
- } catch (error) {
- showResult('summarize-file-result', false, `Error
${error.message}
`);
- } finally {
- setLoading(button, false);
- }
-});
-
-// ==================== SUMMARIZE LINK MODE (Full Pipeline) ====================
-
-const summarizeLinkForm = document.getElementById('summarize-link-form');
-if (summarizeLinkForm) {
- const linkProgress = document.getElementById('summarize-link-progress');
- const linkProgressFill = document.getElementById('summarize-link-progress-fill');
- const linkProgressTitle = document.getElementById('summarize-link-progress-title');
- const linkProgressPercent = document.getElementById('summarize-link-progress-percent');
- const linkProgressPhase = document.getElementById('summarize-link-progress-phase');
- const linkProgressSpeed = document.getElementById('summarize-link-progress-speed');
- const linkProgressCurrent = document.getElementById('summarize-link-progress-current');
- const linkProgressEta = document.getElementById('summarize-link-progress-eta');
-
- function resetLinkProgress() {
- if (linkProgressFill) linkProgressFill.style.width = '0%';
- if (linkProgressPercent) linkProgressPercent.textContent = '0%';
- if (linkProgressTitle) linkProgressTitle.textContent = 'Processing...';
- if (linkProgressPhase) linkProgressPhase.textContent = '';
- if (linkProgressSpeed) linkProgressSpeed.textContent = '';
- if (linkProgressCurrent) linkProgressCurrent.textContent = '';
- if (linkProgressEta) linkProgressEta.textContent = '';
- }
-
- summarizeLinkForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- const button = document.getElementById('summarize-link-btn');
- const url = document.getElementById('summarize-url').value;
- const style = document.getElementById('summarize-link-style').value;
- const language = document.getElementById('summarize-link-language').value;
- const resultDiv = document.getElementById('summarize-link-result');
-
- setLoading(button, true);
- resetLinkProgress();
- linkProgress.style.display = 'block';
- resultDiv.classList.remove('show');
-
- const params = new URLSearchParams({ url, style, language });
- const eventSource = new EventSource(`${API_URL}/summarize-stream?${params}`);
-
- eventSource.addEventListener('status', (e) => {
- const data = JSON.parse(e.data);
- linkProgressTitle.textContent = data.message;
- if (data.percent !== undefined) {
- linkProgressFill.style.width = `${data.percent}%`;
- linkProgressPercent.textContent = `${Math.round(data.percent)}%`;
- }
- });
-
- eventSource.addEventListener('info', (e) => {
- const data = JSON.parse(e.data);
- linkProgressTitle.textContent = data.totalVideos > 1
- ? `Processing playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
- : `Processing: ${data.title}`;
- });
-
- eventSource.addEventListener('progress', (e) => {
- const data = JSON.parse(e.data);
- linkProgressFill.style.width = `${data.percent}%`;
- linkProgressPercent.textContent = `${Math.round(data.percent)}%`;
- linkProgressPhase.textContent = data.phaseLabel || '';
- if (data.speed) linkProgressSpeed.textContent = data.speed;
- if (data.title) {
- linkProgressCurrent.innerHTML = `${data.phaseLabel}: ${data.title}`;
- }
- if (data.totalVideos > 1) {
- linkProgressCurrent.innerHTML += ` (${data.currentVideo}/${data.totalVideos})`;
- }
- });
-
- eventSource.addEventListener('video-complete', (e) => {
- const data = JSON.parse(e.data);
- linkProgressCurrent.innerHTML = `Downloaded: ${data.title}`;
- });
-
- eventSource.addEventListener('transcribe-complete', (e) => {
- const data = JSON.parse(e.data);
- linkProgressCurrent.innerHTML = `Transcribed: ${data.title}`;
- });
-
- eventSource.addEventListener('summarize-complete', (e) => {
- const data = JSON.parse(e.data);
- linkProgressCurrent.innerHTML = `Summarized: ${data.title}`;
- });
-
- eventSource.addEventListener('complete', (e) => {
- const data = JSON.parse(e.data);
- eventSource.close();
- linkProgressFill.style.width = '100%';
- linkProgressPercent.textContent = '100%';
- linkProgressTitle.textContent = 'Complete!';
- linkProgressPhase.textContent = '';
- linkProgressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
-
- showResult('summarize-link-result', true, `
- Pipeline Complete!
- ${data.playlistTitle ? `Playlist: ${data.playlistTitle}
` : ''}
- Downloaded: ${data.downloadedCount}/${data.totalVideos} | Transcribed: ${data.transcribedCount} | Summarized: ${data.summarizedCount}
- ${data.results.map(r => `
- -
- ${r.success ? '✓' : '✗'}
- ${r.title}
- ${r.summaryUrl ? `Summary` : ''}
- ${r.transcriptionUrl ? `Transcript` : ''}
- ${r.error ? `(${r.error})` : ''}
-
- `).join('')}
- ${data.results[0]?.summary ? `
- Summary Preview:
- ${data.results[0].summary.substring(0, 2000)}${data.results[0].summary.length > 2000 ? '...' : ''}
- ` : ''}
- `);
- setLoading(button, false);
- });
-
- eventSource.addEventListener('error', (e) => {
- let errorMsg = 'Processing failed';
- try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
- eventSource.close();
- linkProgress.style.display = 'none';
- showResult('summarize-link-result', false, `Error
${errorMsg}
`);
- setLoading(button, false);
- });
-
- eventSource.onerror = () => {
- eventSource.close();
- linkProgress.style.display = 'none';
- showResult('summarize-link-result', false, `Error
Connection lost
`);
- setLoading(button, false);
- };
- });
-}
+// API Base URL
+const API_URL = '';
+
+// Tab switching
+document.querySelectorAll('.tab').forEach(tab => {
+ tab.addEventListener('click', () => {
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
+ tab.classList.add('active');
+ document.getElementById(tab.dataset.tab).classList.add('active');
+ });
+});
+
+// Helper: Show result
+function showResult(elementId, success, content) {
+ const el = document.getElementById(elementId);
+ el.className = `result show ${success ? 'success' : 'error'}`;
+ el.innerHTML = content;
+}
+
+// Helper: Set loading state
+function setLoading(button, loading) {
+ button.disabled = loading;
+ button.classList.toggle('loading', loading);
+}
+
+// Format seconds to MM:SS or HH:MM:SS
+function formatTime(seconds) {
+ if (!seconds || seconds < 0) return '--:--';
+ const hrs = Math.floor(seconds / 3600);
+ const mins = Math.floor((seconds % 3600) / 60);
+ const secs = Math.floor(seconds % 60);
+ if (hrs > 0) {
+ return `${hrs}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
+ }
+ return `${mins}:${String(secs).padStart(2, '0')}`;
+}
+
+// Format file size
+function formatSize(bytes) {
+ if (!bytes) return '';
+ const units = ['B', 'KB', 'MB', 'GB'];
+ let i = 0;
+ while (bytes >= 1024 && i < units.length - 1) {
+ bytes /= 1024;
+ i++;
+ }
+ return `${bytes.toFixed(1)} ${units[i]}`;
+}
+
+// ==================== DOWNLOAD TAB ====================
+
+const progressContainer = document.getElementById('download-progress');
+const progressFill = document.getElementById('progress-fill');
+const progressPercent = document.getElementById('progress-percent');
+const progressTitle = document.getElementById('progress-title');
+const progressEta = document.getElementById('progress-eta');
+const progressInfo = document.getElementById('progress-info');
+const progressSpeed = document.getElementById('progress-speed');
+const progressCurrent = document.getElementById('progress-current');
+
+function updateDownloadProgress(data) {
+ progressFill.style.width = `${data.percent}%`;
+ progressPercent.textContent = `${data.percent}%`;
+
+ if (data.totalVideos > 1) {
+ progressInfo.textContent = `Video ${data.currentVideo}/${data.totalVideos}`;
+ } else {
+ progressInfo.textContent = '';
+ }
+
+ if (data.speed) progressSpeed.textContent = data.speed;
+ if (data.estimatedRemaining) {
+ progressEta.textContent = `ETA: ${formatTime(data.estimatedRemaining)}`;
+ } else if (data.eta) {
+ progressEta.textContent = `ETA: ${data.eta}`;
+ }
+ if (data.title) {
+ progressCurrent.innerHTML = `Downloading: ${data.title}`;
+ }
+}
+
+function resetDownloadProgress() {
+ progressFill.style.width = '0%';
+ progressPercent.textContent = '0%';
+ progressTitle.textContent = 'Downloading...';
+ progressEta.textContent = '';
+ progressInfo.textContent = '';
+ progressSpeed.textContent = '';
+ progressCurrent.textContent = '';
+}
+
+document.getElementById('download-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const button = e.target.querySelector('button[type="submit"]');
+ const url = document.getElementById('download-url').value;
+ const resultDiv = document.getElementById('download-result');
+
+ setLoading(button, true);
+ resetDownloadProgress();
+ progressContainer.style.display = 'block';
+ resultDiv.classList.remove('show');
+
+ const eventSource = new EventSource(`${API_URL}/download-stream?url=${encodeURIComponent(url)}`);
+
+ eventSource.addEventListener('status', (e) => {
+ progressTitle.textContent = JSON.parse(e.data).message;
+ });
+
+ eventSource.addEventListener('info', (e) => {
+ const data = JSON.parse(e.data);
+ progressTitle.textContent = data.totalVideos > 1
+ ? `Downloading playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
+ : `Downloading: ${data.title}`;
+ });
+
+ eventSource.addEventListener('progress', (e) => updateDownloadProgress(JSON.parse(e.data)));
+
+ eventSource.addEventListener('video-complete', (e) => {
+ const data = JSON.parse(e.data);
+ progressCurrent.innerHTML = `Completed: ${data.title} (${data.videosCompleted}/${data.totalVideos})`;
+ });
+
+ eventSource.addEventListener('complete', (e) => {
+ const data = JSON.parse(e.data);
+ eventSource.close();
+ progressFill.style.width = '100%';
+ progressPercent.textContent = '100%';
+ progressTitle.textContent = 'Download Complete!';
+ progressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
+
+ showResult('download-result', true, `
+ Download Complete!
+ ${data.successCount}/${data.totalVideos} videos downloaded
+ ${data.playlistTitle ? `Playlist: ${data.playlistTitle}
` : ''}
+ ${data.videos.map(v => `
+ -
+ ${v.success ? '✓' : '✗'}
+ ${v.title}
+ ${v.success && v.fileUrl ? `Download` : ''}
+ ${v.error ? `(${v.error})` : ''}
+
+ `).join('')}
+ `);
+ setLoading(button, false);
+ });
+
+ eventSource.addEventListener('error', (e) => {
+ let errorMsg = 'Download failed';
+ try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
+ eventSource.close();
+ progressContainer.style.display = 'none';
+ showResult('download-result', false, `Error
${errorMsg}
`);
+ setLoading(button, false);
+ });
+
+ eventSource.onerror = () => {
+ eventSource.close();
+ progressContainer.style.display = 'none';
+ showResult('download-result', false, `Error
Connection lost
`);
+ setLoading(button, false);
+ };
+});
+
+// ==================== CONVERT TAB (Video to MP3) ====================
+
+let convertSelectedFiles = [];
+const convertDropZone = document.getElementById('convert-drop-zone');
+const convertFileInput = document.getElementById('convert-file-input');
+const convertSelectedFilesDiv = document.getElementById('convert-selected-files');
+const convertFilesList = document.getElementById('convert-files-list');
+const convertBtn = document.getElementById('convert-btn');
+const convertClearFilesBtn = document.getElementById('convert-clear-files');
+
+function updateConvertFilesList() {
+ if (convertSelectedFiles.length === 0) {
+ convertSelectedFilesDiv.style.display = 'none';
+ convertBtn.disabled = true;
+ return;
+ }
+
+ convertSelectedFilesDiv.style.display = 'block';
+ convertBtn.disabled = false;
+
+ convertFilesList.innerHTML = convertSelectedFiles.map((file, index) => `
+
+ ${file.name}
+ ${formatSize(file.size)}
+
+
+ `).join('');
+
+ // Add remove handlers
+ convertFilesList.querySelectorAll('.remove-file').forEach(btn => {
+ btn.addEventListener('click', () => {
+ convertSelectedFiles.splice(parseInt(btn.dataset.index), 1);
+ updateConvertFilesList();
+ });
+ });
+}
+
+function addConvertFiles(files) {
+ const videoAudioFiles = Array.from(files).filter(f =>
+ f.type.startsWith('video/') || f.type.startsWith('audio/') ||
+ f.name.match(/\.(mp4|avi|mkv|mov|m4a|wav|flac|ogg|webm|wmv|flv)$/i)
+ );
+ convertSelectedFiles = [...convertSelectedFiles, ...videoAudioFiles];
+ updateConvertFilesList();
+}
+
+// Drag & Drop events for convert
+convertDropZone.addEventListener('click', () => convertFileInput.click());
+
+convertDropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ convertDropZone.classList.add('drag-over');
+});
+
+convertDropZone.addEventListener('dragleave', () => {
+ convertDropZone.classList.remove('drag-over');
+});
+
+convertDropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ convertDropZone.classList.remove('drag-over');
+ addConvertFiles(e.dataTransfer.files);
+});
+
+convertFileInput.addEventListener('change', () => {
+ addConvertFiles(convertFileInput.files);
+ convertFileInput.value = '';
+});
+
+convertClearFilesBtn.addEventListener('click', () => {
+ convertSelectedFiles = [];
+ updateConvertFilesList();
+});
+
+// Convert form submit
+document.getElementById('convert-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ if (convertSelectedFiles.length === 0) return;
+
+ const button = convertBtn;
+ const bitrate = document.getElementById('convert-bitrate').value;
+ const quality = document.getElementById('convert-quality').value;
+
+ const convertProgress = document.getElementById('convert-progress');
+ const convertProgressFill = document.getElementById('convert-progress-fill');
+ const convertProgressTitle = document.getElementById('convert-progress-title');
+ const convertProgressPercent = document.getElementById('convert-progress-percent');
+ const convertProgressInfo = document.getElementById('convert-progress-info');
+ const convertProgressCurrent = document.getElementById('convert-progress-current');
+
+ setLoading(button, true);
+ convertProgress.style.display = 'block';
+ convertProgressFill.style.width = '0%';
+ convertProgressTitle.textContent = 'Converting to MP3...';
+ convertProgressPercent.textContent = '0%';
+ convertProgressInfo.textContent = `0/${convertSelectedFiles.length} files`;
+ document.getElementById('convert-result').classList.remove('show');
+
+ const formData = new FormData();
+ convertSelectedFiles.forEach(file => formData.append('files', file));
+ formData.append('bitrate', bitrate);
+ formData.append('quality', quality);
+
+ try {
+ const response = await fetch(`${API_URL}/convert-to-mp3`, {
+ method: 'POST',
+ body: formData
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) throw new Error(data.error || 'Conversion failed');
+
+ convertProgressFill.style.width = '100%';
+ convertProgressPercent.textContent = '100%';
+ convertProgressTitle.textContent = 'Conversion Complete!';
+ convertProgressInfo.textContent = `${data.successCount}/${data.totalFiles} files`;
+
+ showResult('convert-result', true, `
+ Conversion Complete!
+ ${data.successCount}/${data.totalFiles} files converted to MP3
+
+ ${data.results.map(r => r.success ? `
+
+ ` : `
+
+ ${r.fileName}
+ ${r.error}
+
+ `).join('')}
+
+ `);
+
+ // Clear selected files after successful conversion
+ convertSelectedFiles = [];
+ updateConvertFilesList();
+
+ } catch (error) {
+ showResult('convert-result', false, `Error
${error.message}
`);
+ } finally {
+ setLoading(button, false);
+ setTimeout(() => {
+ convertProgress.style.display = 'none';
+ }, 1000);
+ }
+});
+
+// ==================== TRANSCRIBE TAB (Drag & Drop) ====================
+
+let selectedFiles = [];
+const dropZone = document.getElementById('drop-zone');
+const fileInput = document.getElementById('file-input');
+const selectedFilesDiv = document.getElementById('selected-files');
+const filesList = document.getElementById('files-list');
+const transcribeBtn = document.getElementById('transcribe-btn');
+const clearFilesBtn = document.getElementById('clear-files');
+
+function updateFilesList() {
+ if (selectedFiles.length === 0) {
+ selectedFilesDiv.style.display = 'none';
+ transcribeBtn.disabled = true;
+ return;
+ }
+
+ selectedFilesDiv.style.display = 'block';
+ transcribeBtn.disabled = false;
+
+ filesList.innerHTML = selectedFiles.map((file, index) => `
+
+ ${file.name}
+ ${formatSize(file.size)}
+
+
+ `).join('');
+
+ // Add remove handlers
+ filesList.querySelectorAll('.remove-file').forEach(btn => {
+ btn.addEventListener('click', () => {
+ selectedFiles.splice(parseInt(btn.dataset.index), 1);
+ updateFilesList();
+ });
+ });
+}
+
+function addFiles(files) {
+ const audioFiles = Array.from(files).filter(f =>
+ f.type.startsWith('audio/') || f.name.match(/\.(mp3|wav|m4a|ogg|flac)$/i)
+ );
+ selectedFiles = [...selectedFiles, ...audioFiles];
+ updateFilesList();
+}
+
+// Drag & Drop events
+dropZone.addEventListener('click', () => fileInput.click());
+
+dropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ dropZone.classList.add('drag-over');
+});
+
+dropZone.addEventListener('dragleave', () => {
+ dropZone.classList.remove('drag-over');
+});
+
+dropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ dropZone.classList.remove('drag-over');
+ addFiles(e.dataTransfer.files);
+});
+
+fileInput.addEventListener('change', () => {
+ addFiles(fileInput.files);
+ fileInput.value = '';
+});
+
+clearFilesBtn.addEventListener('click', () => {
+ selectedFiles = [];
+ updateFilesList();
+});
+
+// Transcribe form submit
+document.getElementById('transcribe-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ if (selectedFiles.length === 0) return;
+
+ const button = transcribeBtn;
+ const language = document.getElementById('transcribe-lang').value;
+ const model = document.getElementById('transcribe-model').value;
+
+ const transcribeProgress = document.getElementById('transcribe-progress');
+ const transcribeProgressFill = document.getElementById('transcribe-progress-fill');
+ const transcribeProgressTitle = document.getElementById('transcribe-progress-title');
+ const transcribeProgressPercent = document.getElementById('transcribe-progress-percent');
+ const transcribeProgressInfo = document.getElementById('transcribe-progress-info');
+ const transcribeProgressCurrent = document.getElementById('transcribe-progress-current');
+
+ setLoading(button, true);
+ transcribeProgress.style.display = 'block';
+ transcribeProgressFill.style.width = '0%';
+ transcribeProgressTitle.textContent = 'Uploading and transcribing...';
+ transcribeProgressPercent.textContent = '0%';
+ transcribeProgressInfo.textContent = `0/${selectedFiles.length} files`;
+ document.getElementById('transcribe-result').classList.remove('show');
+
+ const formData = new FormData();
+ selectedFiles.forEach(file => formData.append('files', file));
+ if (language) formData.append('language', language);
+ formData.append('model', model);
+
+ try {
+ const response = await fetch(`${API_URL}/upload-transcribe`, {
+ method: 'POST',
+ body: formData
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) throw new Error(data.error || 'Transcription failed');
+
+ transcribeProgressFill.style.width = '100%';
+ transcribeProgressPercent.textContent = '100%';
+ transcribeProgressTitle.textContent = 'Transcription Complete!';
+ transcribeProgressInfo.textContent = `${data.successCount}/${data.totalFiles} files`;
+
+ showResult('transcribe-result', true, `
+ Transcription Complete!
+ ${data.successCount}/${data.totalFiles} files transcribed
+ ${data.results.map(r => `
+ -
+ ${r.success ? '✓' : '✗'}
+ ${r.fileName}
+ ${r.success && r.transcriptionUrl ? `View` : ''}
+ ${r.error ? `(${r.error})` : ''}
+
+ `).join('')}
+ ${data.results[0]?.text ? `
+ Preview (first file):
+ ${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}
+ ` : ''}
+ `);
+
+ selectedFiles = [];
+ updateFilesList();
+
+ } catch (error) {
+ transcribeProgress.style.display = 'none';
+ showResult('transcribe-result', false, `Error
${error.message}
`);
+ } finally {
+ setLoading(button, false);
+ }
+});
+
+// ==================== PROCESS TAB (Download + Transcribe) ====================
+
+const processProgress = document.getElementById('process-progress');
+const processProgressFill = document.getElementById('process-progress-fill');
+const processProgressTitle = document.getElementById('process-progress-title');
+const processProgressPercent = document.getElementById('process-progress-percent');
+const processProgressPhase = document.getElementById('process-progress-phase');
+const processProgressSpeed = document.getElementById('process-progress-speed');
+const processProgressCurrent = document.getElementById('process-progress-current');
+const processProgressEta = document.getElementById('process-progress-eta');
+
+function resetProcessProgress() {
+ processProgressFill.style.width = '0%';
+ processProgressPercent.textContent = '0%';
+ processProgressTitle.textContent = 'Processing...';
+ processProgressPhase.textContent = '';
+ processProgressSpeed.textContent = '';
+ processProgressCurrent.textContent = '';
+ processProgressEta.textContent = '';
+}
+
+document.getElementById('process-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const button = e.target.querySelector('button[type="submit"]');
+ const url = document.getElementById('process-url').value;
+ const language = document.getElementById('process-lang').value;
+ const model = document.getElementById('process-model').value;
+ const resultDiv = document.getElementById('process-result');
+
+ setLoading(button, true);
+ resetProcessProgress();
+ processProgress.style.display = 'block';
+ resultDiv.classList.remove('show');
+
+ const params = new URLSearchParams({ url });
+ if (language) params.append('language', language);
+ params.append('model', model);
+
+ const eventSource = new EventSource(`${API_URL}/process-stream?${params}`);
+
+ eventSource.addEventListener('status', (e) => {
+ const data = JSON.parse(e.data);
+ processProgressTitle.textContent = data.message;
+ if (data.phase === 'transcribing') {
+ processProgressPhase.textContent = 'Transcribing';
+ }
+ });
+
+ eventSource.addEventListener('info', (e) => {
+ const data = JSON.parse(e.data);
+ processProgressTitle.textContent = data.totalVideos > 1
+ ? `Processing playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
+ : `Processing: ${data.title}`;
+ });
+
+ eventSource.addEventListener('progress', (e) => {
+ const data = JSON.parse(e.data);
+ processProgressFill.style.width = `${data.percent}%`;
+ processProgressPercent.textContent = `${Math.round(data.percent)}%`;
+ processProgressPhase.textContent = data.phaseLabel || '';
+ if (data.speed) processProgressSpeed.textContent = data.speed;
+ if (data.title) {
+ processProgressCurrent.innerHTML = `${data.phaseLabel}: ${data.title}`;
+ }
+ if (data.totalVideos > 1) {
+ processProgressCurrent.innerHTML += ` (${data.currentVideo}/${data.totalVideos})`;
+ }
+ });
+
+ eventSource.addEventListener('video-complete', (e) => {
+ const data = JSON.parse(e.data);
+ processProgressCurrent.innerHTML = `Downloaded: ${data.title}`;
+ });
+
+ eventSource.addEventListener('transcribe-complete', (e) => {
+ const data = JSON.parse(e.data);
+ processProgressCurrent.innerHTML = `Transcribed: ${data.title} (${data.videosCompleted}/${data.totalFiles})`;
+ });
+
+ eventSource.addEventListener('complete', (e) => {
+ const data = JSON.parse(e.data);
+ eventSource.close();
+ processProgressFill.style.width = '100%';
+ processProgressPercent.textContent = '100%';
+ processProgressTitle.textContent = 'Processing Complete!';
+ processProgressPhase.textContent = '';
+ processProgressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
+
+ showResult('process-result', true, `
+ Processing Complete!
+ ${data.playlistTitle ? `Playlist: ${data.playlistTitle}
` : ''}
+ Downloaded: ${data.downloadedCount}/${data.totalVideos}
+ Transcribed: ${data.transcribedCount}/${data.totalVideos}
+ ${data.results.map(r => `
+ -
+ ${r.transcriptionSuccess ? '✓' : '✗'}
+ ${r.title}
+ ${r.audioUrl ? `MP3` : ''}
+ ${r.transcriptionUrl ? `TXT` : ''}
+ ${r.error ? `(${r.error})` : ''}
+
+ `).join('')}
+ ${data.results[0]?.text ? `
+ Preview (first file):
+ ${data.results[0].text.substring(0, 1000)}${data.results[0].text.length > 1000 ? '...' : ''}
+ ` : ''}
+ `);
+ setLoading(button, false);
+ });
+
+ eventSource.addEventListener('error', (e) => {
+ let errorMsg = 'Processing failed';
+ try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
+ eventSource.close();
+ processProgress.style.display = 'none';
+ showResult('process-result', false, `Error
${errorMsg}
`);
+ setLoading(button, false);
+ });
+
+ eventSource.onerror = () => {
+ eventSource.close();
+ processProgress.style.display = 'none';
+ showResult('process-result', false, `Error
Connection lost
`);
+ setLoading(button, false);
+ };
+});
+
+// ==================== TRANSLATE CHECKBOXES (Transcribe & Process tabs) ====================
+
+// Transcribe tab checkbox
+const transcribeTranslateCheckbox = document.getElementById('transcribe-translate');
+const transcribeTranslateLang = document.getElementById('transcribe-translate-lang');
+
+transcribeTranslateCheckbox.addEventListener('change', () => {
+ transcribeTranslateLang.disabled = !transcribeTranslateCheckbox.checked;
+});
+
+// Process tab checkbox
+const processTranslateCheckbox = document.getElementById('process-translate');
+const processTranslateLang = document.getElementById('process-translate-lang');
+
+processTranslateCheckbox.addEventListener('change', () => {
+ processTranslateLang.disabled = !processTranslateCheckbox.checked;
+});
+
+// ==================== TRANSLATE TAB ====================
+
+// Mode switching
+const translateModeBtns = document.querySelectorAll('.mode-btn');
+const translateTextMode = document.getElementById('translate-text-mode');
+const translateFileMode = document.getElementById('translate-file-mode');
+
+translateModeBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ translateModeBtns.forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+
+ if (btn.dataset.mode === 'text') {
+ translateTextMode.style.display = 'block';
+ translateFileMode.style.display = 'none';
+ } else {
+ translateTextMode.style.display = 'none';
+ translateFileMode.style.display = 'block';
+ }
+ });
+});
+
+// Text translation form
+document.getElementById('translate-text-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const button = document.getElementById('translate-text-btn');
+ const text = document.getElementById('translate-input').value;
+ const sourceLang = document.getElementById('translate-source').value;
+ const targetLang = document.getElementById('translate-target').value;
+
+ if (!text.trim()) {
+ showResult('translate-text-result', false, 'Error
Please enter text to translate
');
+ return;
+ }
+
+ setLoading(button, true);
+ document.getElementById('translate-text-result').classList.remove('show');
+
+ try {
+ const response = await fetch(`${API_URL}/translate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ text, targetLang, sourceLang: sourceLang || null })
+ });
+
+ const data = await response.json();
+ if (!response.ok) throw new Error(data.error || 'Translation failed');
+
+ showResult('translate-text-result', true, `
+ Translation Complete!
+ From: ${data.sourceLanguage} To: ${data.targetLanguage}
+ ${data.translatedText}
+ `);
+ } catch (error) {
+ showResult('translate-text-result', false, `Error
${error.message}
`);
+ } finally {
+ setLoading(button, false);
+ }
+});
+
+// File translation - Drag & Drop
+let translateSelectedFiles = [];
+const translateDropZone = document.getElementById('translate-drop-zone');
+const translateFileInput = document.getElementById('translate-file-input');
+const translateSelectedFilesDiv = document.getElementById('translate-selected-files');
+const translateFilesList = document.getElementById('translate-files-list');
+const translateFileBtn = document.getElementById('translate-file-btn');
+const translateClearFilesBtn = document.getElementById('translate-clear-files');
+
+function updateTranslateFilesList() {
+ if (translateSelectedFiles.length === 0) {
+ translateSelectedFilesDiv.style.display = 'none';
+ translateFileBtn.disabled = true;
+ return;
+ }
+
+ translateSelectedFilesDiv.style.display = 'block';
+ translateFileBtn.disabled = false;
+
+ translateFilesList.innerHTML = translateSelectedFiles.map((file, index) => `
+
+ ${file.name}
+ ${formatSize(file.size)}
+
+
+ `).join('');
+
+ translateFilesList.querySelectorAll('.remove-file').forEach(btn => {
+ btn.addEventListener('click', () => {
+ translateSelectedFiles.splice(parseInt(btn.dataset.index), 1);
+ updateTranslateFilesList();
+ });
+ });
+}
+
+function addTranslateFiles(files) {
+ const textFiles = Array.from(files).filter(f =>
+ f.type === 'text/plain' || f.name.endsWith('.txt')
+ );
+ translateSelectedFiles = [...translateSelectedFiles, ...textFiles];
+ updateTranslateFilesList();
+}
+
+translateDropZone.addEventListener('click', () => translateFileInput.click());
+
+translateDropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ translateDropZone.classList.add('drag-over');
+});
+
+translateDropZone.addEventListener('dragleave', () => {
+ translateDropZone.classList.remove('drag-over');
+});
+
+translateDropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ translateDropZone.classList.remove('drag-over');
+ addTranslateFiles(e.dataTransfer.files);
+});
+
+translateFileInput.addEventListener('change', () => {
+ addTranslateFiles(translateFileInput.files);
+ translateFileInput.value = '';
+});
+
+translateClearFilesBtn.addEventListener('click', () => {
+ translateSelectedFiles = [];
+ updateTranslateFilesList();
+});
+
+// File translation form submit
+document.getElementById('translate-file-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ if (translateSelectedFiles.length === 0) return;
+
+ const button = translateFileBtn;
+ const sourceLang = document.getElementById('translate-file-source').value;
+ const targetLang = document.getElementById('translate-file-target').value;
+
+ setLoading(button, true);
+ document.getElementById('translate-file-result').classList.remove('show');
+
+ const formData = new FormData();
+ translateSelectedFiles.forEach(file => formData.append('files', file));
+ formData.append('targetLang', targetLang);
+ if (sourceLang) formData.append('sourceLang', sourceLang);
+
+ try {
+ const response = await fetch(`${API_URL}/translate-file`, {
+ method: 'POST',
+ body: formData
+ });
+
+ const data = await response.json();
+ if (!response.ok) throw new Error(data.error || 'Translation failed');
+
+ showResult('translate-file-result', true, `
+ Translation Complete!
+ ${data.successCount}/${data.totalFiles} files translated
+ ${data.results.map(r => `
+ -
+ ${r.success ? '✓' : '✗'}
+ ${r.fileName || r.originalPath}
+ ${r.success && r.translationUrl ? `View` : ''}
+ ${r.error ? `(${r.error})` : ''}
+
+ `).join('')}
+ ${data.results[0]?.translatedText ? `
+ Preview (first file):
+ ${data.results[0].translatedText.substring(0, 1000)}${data.results[0].translatedText.length > 1000 ? '...' : ''}
+ ` : ''}
+ `);
+
+ translateSelectedFiles = [];
+ updateTranslateFilesList();
+
+ } catch (error) {
+ showResult('translate-file-result', false, `Error
${error.message}
`);
+ } finally {
+ setLoading(button, false);
+ }
+});
+
+// ==================== SUMMARIZE TAB ====================
+
+// Mode switching
+const summarizeModeBtns = document.querySelectorAll('.summarize-mode-selector .mode-btn');
+const summarizeTextMode = document.getElementById('summarize-text-mode');
+const summarizeFileMode = document.getElementById('summarize-file-mode');
+
+const summarizeLinkMode = document.getElementById('summarize-link-mode');
+
+summarizeModeBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ summarizeModeBtns.forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+
+ summarizeTextMode.style.display = 'none';
+ summarizeFileMode.style.display = 'none';
+ if (summarizeLinkMode) summarizeLinkMode.style.display = 'none';
+
+ if (btn.dataset.mode === 'text') {
+ summarizeTextMode.style.display = 'block';
+ } else if (btn.dataset.mode === 'file') {
+ summarizeFileMode.style.display = 'block';
+ } else if (btn.dataset.mode === 'link' && summarizeLinkMode) {
+ summarizeLinkMode.style.display = 'block';
+ }
+ });
+});
+
+// Text summarization form
+document.getElementById('summarize-text-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const button = document.getElementById('summarize-text-btn');
+ const text = document.getElementById('summarize-input').value;
+ const style = document.getElementById('summarize-style').value;
+ const language = document.getElementById('summarize-language').value;
+
+ if (!text.trim()) {
+ showResult('summarize-text-result', false, 'Error
Please enter text to summarize
');
+ return;
+ }
+
+ setLoading(button, true);
+ document.getElementById('summarize-text-result').classList.remove('show');
+
+ try {
+ const response = await fetch(`${API_URL}/summarize`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ text, style, language })
+ });
+
+ const data = await response.json();
+ if (!response.ok) throw new Error(data.error || 'Summarization failed');
+
+ showResult('summarize-text-result', true, `
+ Summary Complete!
+ Model: ${data.model} | Style: ${data.style} | Chunks: ${data.chunks}
+ ${data.summary}
+ `);
+ } catch (error) {
+ showResult('summarize-text-result', false, `Error
${error.message}
`);
+ } finally {
+ setLoading(button, false);
+ }
+});
+
+// File summarization - Drag & Drop
+let summarizeSelectedFiles = [];
+const summarizeDropZone = document.getElementById('summarize-drop-zone');
+const summarizeFileInput = document.getElementById('summarize-file-input');
+const summarizeSelectedFilesDiv = document.getElementById('summarize-selected-files');
+const summarizeFilesList = document.getElementById('summarize-files-list');
+const summarizeFileBtn = document.getElementById('summarize-file-btn');
+const summarizeClearFilesBtn = document.getElementById('summarize-clear-files');
+
+function updateSummarizeFilesList() {
+ if (!summarizeSelectedFilesDiv || !summarizeFileBtn || !summarizeFilesList) return;
+
+ if (summarizeSelectedFiles.length === 0) {
+ summarizeSelectedFilesDiv.style.display = 'none';
+ summarizeFileBtn.disabled = true;
+ return;
+ }
+
+ summarizeSelectedFilesDiv.style.display = 'block';
+ summarizeFileBtn.disabled = false;
+
+ summarizeFilesList.innerHTML = summarizeSelectedFiles.map((file, index) => `
+
+ ${file.name}
+ ${formatSize(file.size)}
+
+
+ `).join('');
+
+ summarizeFilesList.querySelectorAll('.remove-file').forEach(btn => {
+ btn.addEventListener('click', () => {
+ summarizeSelectedFiles.splice(parseInt(btn.dataset.index), 1);
+ updateSummarizeFilesList();
+ });
+ });
+}
+
+function addSummarizeFiles(files) {
+ console.log('Adding files:', files);
+ const textFiles = Array.from(files).filter(f =>
+ f.type === 'text/plain' || f.name.endsWith('.txt')
+ );
+ console.log('Filtered text files:', textFiles);
+ summarizeSelectedFiles = [...summarizeSelectedFiles, ...textFiles];
+ updateSummarizeFilesList();
+}
+
+if (summarizeDropZone) {
+ summarizeDropZone.addEventListener('click', () => {
+ console.log('Drop zone clicked');
+ summarizeFileInput.click();
+ });
+
+ summarizeDropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ summarizeDropZone.classList.add('drag-over');
+ });
+
+ summarizeDropZone.addEventListener('dragleave', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ summarizeDropZone.classList.remove('drag-over');
+ });
+
+ summarizeDropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('Files dropped:', e.dataTransfer.files);
+ summarizeDropZone.classList.remove('drag-over');
+ addSummarizeFiles(e.dataTransfer.files);
+ });
+}
+
+if (summarizeFileInput) {
+ summarizeFileInput.addEventListener('change', () => {
+ console.log('File input changed:', summarizeFileInput.files);
+ addSummarizeFiles(summarizeFileInput.files);
+ summarizeFileInput.value = '';
+ });
+}
+
+if (summarizeClearFilesBtn) {
+ summarizeClearFilesBtn.addEventListener('click', () => {
+ summarizeSelectedFiles = [];
+ updateSummarizeFilesList();
+ });
+}
+
+// File summarization form submit
+document.getElementById('summarize-file-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ if (summarizeSelectedFiles.length === 0) return;
+
+ const button = summarizeFileBtn;
+ const style = document.getElementById('summarize-file-style').value;
+ const language = document.getElementById('summarize-file-language').value;
+
+ setLoading(button, true);
+ document.getElementById('summarize-file-result').classList.remove('show');
+
+ const formData = new FormData();
+ summarizeSelectedFiles.forEach(file => formData.append('files', file));
+ formData.append('style', style);
+ formData.append('language', language);
+
+ try {
+ const response = await fetch(`${API_URL}/summarize-file`, {
+ method: 'POST',
+ body: formData
+ });
+
+ const data = await response.json();
+ if (!response.ok) throw new Error(data.error || 'Summarization failed');
+
+ showResult('summarize-file-result', true, `
+ Summarization Complete!
+ ${data.successCount}/${data.totalFiles} files summarized
+ ${data.results.map(r => `
+ -
+ ${r.success ? '✓' : '✗'}
+ ${r.fileName || r.filePath}
+ ${r.success && r.summaryUrl ? `View` : ''}
+ ${r.error ? `(${r.error})` : ''}
+
+ `).join('')}
+ ${data.results[0]?.summary ? `
+ Preview (first file):
+ ${data.results[0].summary.substring(0, 1000)}${data.results[0].summary.length > 1000 ? '...' : ''}
+ ` : ''}
+ `);
+
+ summarizeSelectedFiles = [];
+ updateSummarizeFilesList();
+
+ } catch (error) {
+ showResult('summarize-file-result', false, `Error
${error.message}
`);
+ } finally {
+ setLoading(button, false);
+ }
+});
+
+// ==================== SUMMARIZE LINK MODE (Full Pipeline) ====================
+
+const summarizeLinkForm = document.getElementById('summarize-link-form');
+if (summarizeLinkForm) {
+ const linkProgress = document.getElementById('summarize-link-progress');
+ const linkProgressFill = document.getElementById('summarize-link-progress-fill');
+ const linkProgressTitle = document.getElementById('summarize-link-progress-title');
+ const linkProgressPercent = document.getElementById('summarize-link-progress-percent');
+ const linkProgressPhase = document.getElementById('summarize-link-progress-phase');
+ const linkProgressSpeed = document.getElementById('summarize-link-progress-speed');
+ const linkProgressCurrent = document.getElementById('summarize-link-progress-current');
+ const linkProgressEta = document.getElementById('summarize-link-progress-eta');
+
+ function resetLinkProgress() {
+ if (linkProgressFill) linkProgressFill.style.width = '0%';
+ if (linkProgressPercent) linkProgressPercent.textContent = '0%';
+ if (linkProgressTitle) linkProgressTitle.textContent = 'Processing...';
+ if (linkProgressPhase) linkProgressPhase.textContent = '';
+ if (linkProgressSpeed) linkProgressSpeed.textContent = '';
+ if (linkProgressCurrent) linkProgressCurrent.textContent = '';
+ if (linkProgressEta) linkProgressEta.textContent = '';
+ }
+
+ summarizeLinkForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const button = document.getElementById('summarize-link-btn');
+ const url = document.getElementById('summarize-url').value;
+ const style = document.getElementById('summarize-link-style').value;
+ const language = document.getElementById('summarize-link-language').value;
+ const resultDiv = document.getElementById('summarize-link-result');
+
+ setLoading(button, true);
+ resetLinkProgress();
+ linkProgress.style.display = 'block';
+ resultDiv.classList.remove('show');
+
+ const params = new URLSearchParams({ url, style, language });
+ const eventSource = new EventSource(`${API_URL}/summarize-stream?${params}`);
+
+ eventSource.addEventListener('status', (e) => {
+ const data = JSON.parse(e.data);
+ linkProgressTitle.textContent = data.message;
+ if (data.percent !== undefined) {
+ linkProgressFill.style.width = `${data.percent}%`;
+ linkProgressPercent.textContent = `${Math.round(data.percent)}%`;
+ }
+ });
+
+ eventSource.addEventListener('info', (e) => {
+ const data = JSON.parse(e.data);
+ linkProgressTitle.textContent = data.totalVideos > 1
+ ? `Processing playlist: ${data.playlistTitle} (${data.totalVideos} videos)`
+ : `Processing: ${data.title}`;
+ });
+
+ eventSource.addEventListener('progress', (e) => {
+ const data = JSON.parse(e.data);
+ linkProgressFill.style.width = `${data.percent}%`;
+ linkProgressPercent.textContent = `${Math.round(data.percent)}%`;
+ linkProgressPhase.textContent = data.phaseLabel || '';
+ if (data.speed) linkProgressSpeed.textContent = data.speed;
+ if (data.title) {
+ linkProgressCurrent.innerHTML = `${data.phaseLabel}: ${data.title}`;
+ }
+ if (data.totalVideos > 1) {
+ linkProgressCurrent.innerHTML += ` (${data.currentVideo}/${data.totalVideos})`;
+ }
+ });
+
+ eventSource.addEventListener('video-complete', (e) => {
+ const data = JSON.parse(e.data);
+ linkProgressCurrent.innerHTML = `Downloaded: ${data.title}`;
+ });
+
+ eventSource.addEventListener('transcribe-complete', (e) => {
+ const data = JSON.parse(e.data);
+ linkProgressCurrent.innerHTML = `Transcribed: ${data.title}`;
+ });
+
+ eventSource.addEventListener('summarize-complete', (e) => {
+ const data = JSON.parse(e.data);
+ linkProgressCurrent.innerHTML = `Summarized: ${data.title}`;
+ });
+
+ eventSource.addEventListener('complete', (e) => {
+ const data = JSON.parse(e.data);
+ eventSource.close();
+ linkProgressFill.style.width = '100%';
+ linkProgressPercent.textContent = '100%';
+ linkProgressTitle.textContent = 'Complete!';
+ linkProgressPhase.textContent = '';
+ linkProgressEta.textContent = `Total: ${formatTime(data.totalTime)}`;
+
+ showResult('summarize-link-result', true, `
+ Pipeline Complete!
+ ${data.playlistTitle ? `Playlist: ${data.playlistTitle}
` : ''}
+ Downloaded: ${data.downloadedCount}/${data.totalVideos} | Transcribed: ${data.transcribedCount} | Summarized: ${data.summarizedCount}
+ ${data.results.map(r => `
+ -
+ ${r.success ? '✓' : '✗'}
+ ${r.title}
+ ${r.summaryUrl ? `Summary` : ''}
+ ${r.transcriptionUrl ? `Transcript` : ''}
+ ${r.error ? `(${r.error})` : ''}
+
+ `).join('')}
+ ${data.results[0]?.summary ? `
+ Summary Preview:
+ ${data.results[0].summary.substring(0, 2000)}${data.results[0].summary.length > 2000 ? '...' : ''}
+ ` : ''}
+ `);
+ setLoading(button, false);
+ });
+
+ eventSource.addEventListener('error', (e) => {
+ let errorMsg = 'Processing failed';
+ try { errorMsg = JSON.parse(e.data).message || errorMsg; } catch {}
+ eventSource.close();
+ linkProgress.style.display = 'none';
+ showResult('summarize-link-result', false, `Error
${errorMsg}
`);
+ setLoading(button, false);
+ });
+
+ eventSource.onerror = () => {
+ eventSource.close();
+ linkProgress.style.display = 'none';
+ showResult('summarize-link-result', false, `Error
Connection lost
`);
+ setLoading(button, false);
+ };
+ });
+}
diff --git a/public/index.html b/public/index.html
index 4c009c7..43b9ed7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,577 +1,577 @@
-
-
-
-
-
- Video to MP3 Transcriptor
-
-
-
-
-
-
-
-
-
-
-
- Download YouTube Video/Playlist
-
-
-
-
-
-
-
- Convert Video/Audio to MP3
-
-
-
-
Drag & drop video/audio files here
-
or click to select files
-
Supported: MP4, M4A, AVI, MKV, MOV, WAV, FLAC, OGG
-
-
-
-
-
Selected Files
-
-
-
-
-
-
-
-
-
-
- Transcribe Audio File
-
-
-
-
Drag & drop audio files here
-
or click to select files
-
-
-
-
-
Selected Files
-
-
-
-
-
-
-
-
-
-
- Download + Transcribe
-
-
-
-
-
-
-
- Translate Text
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Drag & drop text files here
-
or click to select files (.txt)
-
-
-
-
-
Selected Files
-
-
-
-
-
-
-
-
-
-
- Summarize Text (GPT-5.1)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Drag & drop text files here
-
or click to select files (.txt)
-
-
-
-
-
Selected Files
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Video to MP3 Transcriptor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Convert Video/Audio to MP3
+
+
+
+
Drag & drop video/audio files here
+
or click to select files
+
Supported: MP4, M4A, AVI, MKV, MOV, WAV, FLAC, OGG
+
+
+
+
+
Selected Files
+
+
+
+
+
+
+
+
+
+
+ Transcribe Audio File
+
+
+
+
Drag & drop audio files here
+
or click to select files
+
+
+
+
+
Selected Files
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Translate Text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Drag & drop text files here
+
or click to select files (.txt)
+
+
+
+
+
Selected Files
+
+
+
+
+
+
+
+
+
+
+ Summarize Text (GPT-5.1)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Drag & drop text files here
+
or click to select files (.txt)
+
+
+
+
+
Selected Files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/style.css b/public/style.css
index 9d5d9e1..d9b3f34 100644
--- a/public/style.css
+++ b/public/style.css
@@ -1,716 +1,716 @@
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
- min-height: 100vh;
- color: #fff;
-}
-
-.container {
- max-width: 900px;
- margin: 0 auto;
- padding: 2rem;
-}
-
-header {
- text-align: center;
- margin-bottom: 2rem;
-}
-
-header h1 {
- font-size: 2.5rem;
- background: linear-gradient(90deg, #e94560, #0f3460);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.subtitle {
- color: #8892b0;
- margin-top: 0.5rem;
-}
-
-/* Tabs */
-.tabs {
- display: flex;
- gap: 0.5rem;
- margin-bottom: 2rem;
- border-bottom: 2px solid #233554;
- padding-bottom: 0.5rem;
- flex-wrap: wrap;
-}
-
-.tab {
- padding: 0.75rem 1.5rem;
- background: transparent;
- border: none;
- color: #8892b0;
- cursor: pointer;
- font-size: 1rem;
- border-radius: 8px 8px 0 0;
- transition: all 0.3s ease;
-}
-
-.tab:hover {
- color: #e94560;
- background: rgba(233, 69, 96, 0.1);
-}
-
-.tab.active {
- color: #e94560;
- background: rgba(233, 69, 96, 0.2);
- border-bottom: 2px solid #e94560;
- margin-bottom: -2px;
-}
-
-/* Tab Content */
-.tab-content {
- display: none;
- background: rgba(255, 255, 255, 0.05);
- padding: 2rem;
- border-radius: 12px;
- backdrop-filter: blur(10px);
-}
-
-.tab-content.active {
- display: block;
- animation: fadeIn 0.3s ease;
-}
-
-@keyframes fadeIn {
- from { opacity: 0; transform: translateY(10px); }
- to { opacity: 1; transform: translateY(0); }
-}
-
-.tab-content h2 {
- margin-bottom: 1.5rem;
- color: #ccd6f6;
-}
-
-/* Forms */
-.form-group {
- margin-bottom: 1.5rem;
-}
-
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- color: #8892b0;
- font-size: 0.9rem;
-}
-
-.form-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1rem;
-}
-
-@media (max-width: 600px) {
- .form-row {
- grid-template-columns: 1fr;
- }
-}
-
-input[type="url"],
-input[type="text"],
-select {
- width: 100%;
- padding: 0.75rem 1rem;
- background: rgba(255, 255, 255, 0.1);
- border: 1px solid #233554;
- border-radius: 8px;
- color: #fff;
- font-size: 1rem;
- transition: all 0.3s ease;
-}
-
-input[type="url"]:focus,
-input[type="text"]:focus,
-select:focus {
- outline: none;
- border-color: #e94560;
- background: rgba(233, 69, 96, 0.1);
-}
-
-select option {
- background: #1a1a2e;
- color: #fff;
-}
-
-/* Buttons */
-.btn {
- padding: 0.75rem 2rem;
- border: none;
- border-radius: 8px;
- font-size: 1rem;
- cursor: pointer;
- transition: all 0.3s ease;
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.btn-primary {
- background: linear-gradient(90deg, #e94560, #0f3460);
- color: #fff;
-}
-
-.btn-primary:hover {
- transform: translateY(-2px);
- box-shadow: 0 5px 20px rgba(233, 69, 96, 0.4);
-}
-
-.btn-primary:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none;
-}
-
-.btn-secondary {
- background: rgba(255, 255, 255, 0.1);
- color: #ccd6f6;
- border: 1px solid #233554;
-}
-
-.btn-secondary:hover {
- background: rgba(255, 255, 255, 0.2);
-}
-
-.btn-small {
- padding: 0.5rem 1rem;
- font-size: 0.85rem;
- margin-left: 0.5rem;
-}
-
-.btn-loading {
- display: none;
-}
-
-.btn.loading .btn-text {
- display: none;
-}
-
-.btn.loading .btn-loading {
- display: inline;
-}
-
-/* Results */
-.result {
- margin-top: 1.5rem;
- padding: 1rem;
- border-radius: 8px;
- display: none;
-}
-
-.result.show {
- display: block;
- animation: fadeIn 0.3s ease;
-}
-
-.result.success {
- background: rgba(16, 185, 129, 0.2);
- border: 1px solid #10b981;
-}
-
-.result.error {
- background: rgba(239, 68, 68, 0.2);
- border: 1px solid #ef4444;
-}
-
-.result h3 {
- margin-bottom: 0.75rem;
- font-size: 1.1rem;
-}
-
-.result ul {
- list-style: none;
- margin-top: 0.5rem;
-}
-
-.result li {
- padding: 0.5rem 0;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.result li:last-child {
- border-bottom: none;
-}
-
-.result .icon-success {
- color: #10b981;
-}
-
-.result .icon-error {
- color: #ef4444;
-}
-
-.result a {
- color: #e94560;
- text-decoration: none;
-}
-
-.result a:hover {
- text-decoration: underline;
-}
-
-.result .preview {
- margin-top: 1rem;
- padding: 1rem;
- background: rgba(0, 0, 0, 0.3);
- border-radius: 8px;
- font-family: monospace;
- font-size: 0.9rem;
- white-space: pre-wrap;
- max-height: 300px;
- overflow-y: auto;
-}
-
-/* Files Grid */
-.files-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
- gap: 1rem;
- margin-top: 1.5rem;
-}
-
-.file-card {
- background: rgba(255, 255, 255, 0.05);
- padding: 1rem;
- border-radius: 8px;
- border: 1px solid #233554;
- transition: all 0.3s ease;
-}
-
-.file-card:hover {
- border-color: #e94560;
- transform: translateY(-2px);
-}
-
-.file-card .file-name {
- font-weight: 600;
- margin-bottom: 0.5rem;
- word-break: break-all;
- color: #ccd6f6;
-}
-
-.file-card .file-type {
- font-size: 0.8rem;
- color: #8892b0;
- margin-bottom: 0.75rem;
-}
-
-.file-card .file-actions {
- display: flex;
- gap: 0.5rem;
-}
-
-.file-card .file-actions a {
- padding: 0.4rem 0.8rem;
- background: rgba(233, 69, 96, 0.2);
- color: #e94560;
- text-decoration: none;
- border-radius: 4px;
- font-size: 0.85rem;
- transition: all 0.3s ease;
-}
-
-.file-card .file-actions a:hover {
- background: #e94560;
- color: #fff;
-}
-
-/* Loading spinner */
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-.spinner {
- display: inline-block;
- width: 16px;
- height: 16px;
- border: 2px solid rgba(255, 255, 255, 0.3);
- border-top-color: #fff;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- margin-right: 0.5rem;
-}
-
-/* Status indicator */
-.status {
- display: inline-block;
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- font-size: 0.8rem;
- margin-left: 0.5rem;
-}
-
-.status.downloading {
- background: rgba(59, 130, 246, 0.2);
- color: #3b82f6;
-}
-
-.status.transcribing {
- background: rgba(168, 85, 247, 0.2);
- color: #a855f7;
-}
-
-.status.complete {
- background: rgba(16, 185, 129, 0.2);
- color: #10b981;
-}
-
-/* Empty state */
-.empty-state {
- text-align: center;
- padding: 3rem;
- color: #8892b0;
-}
-
-.empty-state p {
- margin-top: 1rem;
-}
-
-/* Progress Bar */
-.progress-container {
- margin-top: 1.5rem;
- padding: 1.5rem;
- background: rgba(255, 255, 255, 0.05);
- border-radius: 12px;
- border: 1px solid #233554;
-}
-
-.progress-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1rem;
-}
-
-.progress-header #progress-title {
- font-weight: 600;
- color: #ccd6f6;
- font-size: 1rem;
-}
-
-.progress-header #progress-eta {
- color: #e94560;
- font-size: 0.9rem;
- font-weight: 500;
-}
-
-.progress-bar {
- width: 100%;
- height: 12px;
- background: rgba(255, 255, 255, 0.1);
- border-radius: 6px;
- overflow: hidden;
- position: relative;
-}
-
-.progress-fill {
- height: 100%;
- background: linear-gradient(90deg, #e94560, #ff6b8a);
- border-radius: 6px;
- width: 0%;
- transition: width 0.3s ease;
- position: relative;
-}
-
-.progress-fill::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(
- 90deg,
- transparent,
- rgba(255, 255, 255, 0.3),
- transparent
- );
- animation: shimmer 2s infinite;
-}
-
-@keyframes shimmer {
- 0% { transform: translateX(-100%); }
- 100% { transform: translateX(100%); }
-}
-
-.progress-details {
- display: flex;
- justify-content: space-between;
- margin-top: 0.75rem;
- font-size: 0.85rem;
- color: #8892b0;
-}
-
-.progress-details #progress-percent {
- color: #ccd6f6;
- font-weight: 600;
-}
-
-.progress-details #progress-speed {
- color: #10b981;
-}
-
-.progress-current {
- margin-top: 1rem;
- padding-top: 1rem;
- border-top: 1px solid rgba(255, 255, 255, 0.1);
- font-size: 0.9rem;
- color: #8892b0;
-}
-
-.progress-current .video-title {
- color: #ccd6f6;
- font-weight: 500;
-}
-
-/* Drag and Drop Zone */
-.drop-zone {
- border: 2px dashed #233554;
- border-radius: 12px;
- padding: 3rem 2rem;
- text-align: center;
- cursor: pointer;
- transition: all 0.3s ease;
- margin-bottom: 1.5rem;
- background: rgba(255, 255, 255, 0.02);
-}
-
-.drop-zone:hover {
- border-color: #e94560;
- background: rgba(233, 69, 96, 0.05);
-}
-
-.drop-zone.drag-over {
- border-color: #e94560;
- background: rgba(233, 69, 96, 0.1);
- transform: scale(1.02);
-}
-
-.drop-zone-icon {
- color: #8892b0;
- margin-bottom: 1rem;
- transition: color 0.3s ease;
-}
-
-.drop-zone:hover .drop-zone-icon {
- color: #e94560;
-}
-
-.drop-zone-text {
- color: #ccd6f6;
- font-size: 1.1rem;
- margin-bottom: 0.5rem;
-}
-
-.drop-zone-hint {
- color: #8892b0;
- font-size: 0.9rem;
-}
-
-.drop-zone-formats {
- color: #64748b;
- font-size: 0.75rem;
- margin-top: 0.5rem;
- font-style: italic;
-}
-
-/* Selected Files List */
-.selected-files {
- background: rgba(255, 255, 255, 0.05);
- border-radius: 8px;
- padding: 1rem;
- margin-bottom: 1.5rem;
-}
-
-.selected-files h3 {
- font-size: 0.9rem;
- color: #8892b0;
- margin-bottom: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.selected-files ul {
- list-style: none;
- margin-bottom: 1rem;
-}
-
-.selected-files li {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.5rem 0;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- color: #ccd6f6;
-}
-
-.selected-files li:last-child {
- border-bottom: none;
-}
-
-.selected-files .file-name {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.selected-files .file-size {
- color: #8892b0;
- font-size: 0.85rem;
- margin-left: 1rem;
-}
-
-.selected-files .remove-file {
- background: none;
- border: none;
- color: #ef4444;
- cursor: pointer;
- padding: 0.25rem;
- margin-left: 0.5rem;
- font-size: 1.2rem;
- line-height: 1;
-}
-
-.selected-files .remove-file:hover {
- color: #ff6b6b;
-}
-
-/* Checkbox Group */
-.checkbox-group {
- margin-bottom: 1rem;
-}
-
-.checkbox-label {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- cursor: pointer;
- color: #ccd6f6;
- font-size: 0.95rem;
-}
-
-.checkbox-label input[type="checkbox"] {
- width: 18px;
- height: 18px;
- accent-color: #e94560;
- cursor: pointer;
-}
-
-.translate-lang-select {
- margin-top: 0.75rem;
- width: auto;
- min-width: 150px;
-}
-
-.translate-lang-select:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-/* Translate Tab */
-.translate-mode-selector {
- display: flex;
- gap: 0.5rem;
- margin-bottom: 1.5rem;
-}
-
-.mode-btn {
- padding: 0.6rem 1.25rem;
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid #233554;
- color: #8892b0;
- border-radius: 8px;
- cursor: pointer;
- font-size: 0.95rem;
- transition: all 0.3s ease;
-}
-
-.mode-btn:hover {
- border-color: #e94560;
- color: #ccd6f6;
-}
-
-.mode-btn.active {
- background: rgba(233, 69, 96, 0.2);
- border-color: #e94560;
- color: #e94560;
-}
-
-.translate-mode {
- animation: fadeIn 0.3s ease;
-}
-
-/* Textarea */
-textarea {
- width: 100%;
- padding: 1rem;
- background: rgba(255, 255, 255, 0.1);
- border: 1px solid #233554;
- border-radius: 8px;
- color: #fff;
- font-size: 1rem;
- font-family: inherit;
- resize: vertical;
- min-height: 150px;
- transition: all 0.3s ease;
-}
-
-textarea:focus {
- outline: none;
- border-color: #e94560;
- background: rgba(233, 69, 96, 0.1);
-}
-
-textarea::placeholder {
- color: #8892b0;
-}
-
-/* Translation Result */
-.translation-output {
- margin-top: 1rem;
- padding: 1rem;
- background: rgba(0, 0, 0, 0.3);
- border-radius: 8px;
- white-space: pre-wrap;
- max-height: 400px;
- overflow-y: auto;
- color: #ccd6f6;
- line-height: 1.6;
-}
-
-/* Summarize Tab */
-.summarize-mode-selector {
- display: flex;
- gap: 0.5rem;
- margin-bottom: 1.5rem;
-}
-
-.summarize-mode {
- animation: fadeIn 0.3s ease;
-}
-
-/* Summary Result */
-.summary-output {
- margin-top: 1rem;
- padding: 1rem;
- background: rgba(0, 0, 0, 0.3);
- border-radius: 8px;
- white-space: pre-wrap;
- max-height: 400px;
- overflow-y: auto;
- color: #ccd6f6;
- line-height: 1.6;
-}
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
+ min-height: 100vh;
+ color: #fff;
+}
+
+.container {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+header {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+header h1 {
+ font-size: 2.5rem;
+ background: linear-gradient(90deg, #e94560, #0f3460);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.subtitle {
+ color: #8892b0;
+ margin-top: 0.5rem;
+}
+
+/* Tabs */
+.tabs {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid #233554;
+ padding-bottom: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.tab {
+ padding: 0.75rem 1.5rem;
+ background: transparent;
+ border: none;
+ color: #8892b0;
+ cursor: pointer;
+ font-size: 1rem;
+ border-radius: 8px 8px 0 0;
+ transition: all 0.3s ease;
+}
+
+.tab:hover {
+ color: #e94560;
+ background: rgba(233, 69, 96, 0.1);
+}
+
+.tab.active {
+ color: #e94560;
+ background: rgba(233, 69, 96, 0.2);
+ border-bottom: 2px solid #e94560;
+ margin-bottom: -2px;
+}
+
+/* Tab Content */
+.tab-content {
+ display: none;
+ background: rgba(255, 255, 255, 0.05);
+ padding: 2rem;
+ border-radius: 12px;
+ backdrop-filter: blur(10px);
+}
+
+.tab-content.active {
+ display: block;
+ animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.tab-content h2 {
+ margin-bottom: 1.5rem;
+ color: #ccd6f6;
+}
+
+/* Forms */
+.form-group {
+ margin-bottom: 1.5rem;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 0.5rem;
+ color: #8892b0;
+ font-size: 0.9rem;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+@media (max-width: 600px) {
+ .form-row {
+ grid-template-columns: 1fr;
+ }
+}
+
+input[type="url"],
+input[type="text"],
+select {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid #233554;
+ border-radius: 8px;
+ color: #fff;
+ font-size: 1rem;
+ transition: all 0.3s ease;
+}
+
+input[type="url"]:focus,
+input[type="text"]:focus,
+select:focus {
+ outline: none;
+ border-color: #e94560;
+ background: rgba(233, 69, 96, 0.1);
+}
+
+select option {
+ background: #1a1a2e;
+ color: #fff;
+}
+
+/* Buttons */
+.btn {
+ padding: 0.75rem 2rem;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.btn-primary {
+ background: linear-gradient(90deg, #e94560, #0f3460);
+ color: #fff;
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 5px 20px rgba(233, 69, 96, 0.4);
+}
+
+.btn-primary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.btn-secondary {
+ background: rgba(255, 255, 255, 0.1);
+ color: #ccd6f6;
+ border: 1px solid #233554;
+}
+
+.btn-secondary:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.btn-small {
+ padding: 0.5rem 1rem;
+ font-size: 0.85rem;
+ margin-left: 0.5rem;
+}
+
+.btn-loading {
+ display: none;
+}
+
+.btn.loading .btn-text {
+ display: none;
+}
+
+.btn.loading .btn-loading {
+ display: inline;
+}
+
+/* Results */
+.result {
+ margin-top: 1.5rem;
+ padding: 1rem;
+ border-radius: 8px;
+ display: none;
+}
+
+.result.show {
+ display: block;
+ animation: fadeIn 0.3s ease;
+}
+
+.result.success {
+ background: rgba(16, 185, 129, 0.2);
+ border: 1px solid #10b981;
+}
+
+.result.error {
+ background: rgba(239, 68, 68, 0.2);
+ border: 1px solid #ef4444;
+}
+
+.result h3 {
+ margin-bottom: 0.75rem;
+ font-size: 1.1rem;
+}
+
+.result ul {
+ list-style: none;
+ margin-top: 0.5rem;
+}
+
+.result li {
+ padding: 0.5rem 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.result li:last-child {
+ border-bottom: none;
+}
+
+.result .icon-success {
+ color: #10b981;
+}
+
+.result .icon-error {
+ color: #ef4444;
+}
+
+.result a {
+ color: #e94560;
+ text-decoration: none;
+}
+
+.result a:hover {
+ text-decoration: underline;
+}
+
+.result .preview {
+ margin-top: 1rem;
+ padding: 1rem;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 8px;
+ font-family: monospace;
+ font-size: 0.9rem;
+ white-space: pre-wrap;
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+/* Files Grid */
+.files-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 1rem;
+ margin-top: 1.5rem;
+}
+
+.file-card {
+ background: rgba(255, 255, 255, 0.05);
+ padding: 1rem;
+ border-radius: 8px;
+ border: 1px solid #233554;
+ transition: all 0.3s ease;
+}
+
+.file-card:hover {
+ border-color: #e94560;
+ transform: translateY(-2px);
+}
+
+.file-card .file-name {
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ word-break: break-all;
+ color: #ccd6f6;
+}
+
+.file-card .file-type {
+ font-size: 0.8rem;
+ color: #8892b0;
+ margin-bottom: 0.75rem;
+}
+
+.file-card .file-actions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.file-card .file-actions a {
+ padding: 0.4rem 0.8rem;
+ background: rgba(233, 69, 96, 0.2);
+ color: #e94560;
+ text-decoration: none;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ transition: all 0.3s ease;
+}
+
+.file-card .file-actions a:hover {
+ background: #e94560;
+ color: #fff;
+}
+
+/* Loading spinner */
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.spinner {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-top-color: #fff;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ margin-right: 0.5rem;
+}
+
+/* Status indicator */
+.status {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ margin-left: 0.5rem;
+}
+
+.status.downloading {
+ background: rgba(59, 130, 246, 0.2);
+ color: #3b82f6;
+}
+
+.status.transcribing {
+ background: rgba(168, 85, 247, 0.2);
+ color: #a855f7;
+}
+
+.status.complete {
+ background: rgba(16, 185, 129, 0.2);
+ color: #10b981;
+}
+
+/* Empty state */
+.empty-state {
+ text-align: center;
+ padding: 3rem;
+ color: #8892b0;
+}
+
+.empty-state p {
+ margin-top: 1rem;
+}
+
+/* Progress Bar */
+.progress-container {
+ margin-top: 1.5rem;
+ padding: 1.5rem;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 12px;
+ border: 1px solid #233554;
+}
+
+.progress-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.progress-header #progress-title {
+ font-weight: 600;
+ color: #ccd6f6;
+ font-size: 1rem;
+}
+
+.progress-header #progress-eta {
+ color: #e94560;
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 12px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 6px;
+ overflow: hidden;
+ position: relative;
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #e94560, #ff6b8a);
+ border-radius: 6px;
+ width: 0%;
+ transition: width 0.3s ease;
+ position: relative;
+}
+
+.progress-fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.3),
+ transparent
+ );
+ animation: shimmer 2s infinite;
+}
+
+@keyframes shimmer {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(100%); }
+}
+
+.progress-details {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 0.75rem;
+ font-size: 0.85rem;
+ color: #8892b0;
+}
+
+.progress-details #progress-percent {
+ color: #ccd6f6;
+ font-weight: 600;
+}
+
+.progress-details #progress-speed {
+ color: #10b981;
+}
+
+.progress-current {
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ font-size: 0.9rem;
+ color: #8892b0;
+}
+
+.progress-current .video-title {
+ color: #ccd6f6;
+ font-weight: 500;
+}
+
+/* Drag and Drop Zone */
+.drop-zone {
+ border: 2px dashed #233554;
+ border-radius: 12px;
+ padding: 3rem 2rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ margin-bottom: 1.5rem;
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.drop-zone:hover {
+ border-color: #e94560;
+ background: rgba(233, 69, 96, 0.05);
+}
+
+.drop-zone.drag-over {
+ border-color: #e94560;
+ background: rgba(233, 69, 96, 0.1);
+ transform: scale(1.02);
+}
+
+.drop-zone-icon {
+ color: #8892b0;
+ margin-bottom: 1rem;
+ transition: color 0.3s ease;
+}
+
+.drop-zone:hover .drop-zone-icon {
+ color: #e94560;
+}
+
+.drop-zone-text {
+ color: #ccd6f6;
+ font-size: 1.1rem;
+ margin-bottom: 0.5rem;
+}
+
+.drop-zone-hint {
+ color: #8892b0;
+ font-size: 0.9rem;
+}
+
+.drop-zone-formats {
+ color: #64748b;
+ font-size: 0.75rem;
+ margin-top: 0.5rem;
+ font-style: italic;
+}
+
+/* Selected Files List */
+.selected-files {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ padding: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.selected-files h3 {
+ font-size: 0.9rem;
+ color: #8892b0;
+ margin-bottom: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.selected-files ul {
+ list-style: none;
+ margin-bottom: 1rem;
+}
+
+.selected-files li {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.5rem 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ color: #ccd6f6;
+}
+
+.selected-files li:last-child {
+ border-bottom: none;
+}
+
+.selected-files .file-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.selected-files .file-size {
+ color: #8892b0;
+ font-size: 0.85rem;
+ margin-left: 1rem;
+}
+
+.selected-files .remove-file {
+ background: none;
+ border: none;
+ color: #ef4444;
+ cursor: pointer;
+ padding: 0.25rem;
+ margin-left: 0.5rem;
+ font-size: 1.2rem;
+ line-height: 1;
+}
+
+.selected-files .remove-file:hover {
+ color: #ff6b6b;
+}
+
+/* Checkbox Group */
+.checkbox-group {
+ margin-bottom: 1rem;
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+ color: #ccd6f6;
+ font-size: 0.95rem;
+}
+
+.checkbox-label input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ accent-color: #e94560;
+ cursor: pointer;
+}
+
+.translate-lang-select {
+ margin-top: 0.75rem;
+ width: auto;
+ min-width: 150px;
+}
+
+.translate-lang-select:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Translate Tab */
+.translate-mode-selector {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.mode-btn {
+ padding: 0.6rem 1.25rem;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid #233554;
+ color: #8892b0;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 0.95rem;
+ transition: all 0.3s ease;
+}
+
+.mode-btn:hover {
+ border-color: #e94560;
+ color: #ccd6f6;
+}
+
+.mode-btn.active {
+ background: rgba(233, 69, 96, 0.2);
+ border-color: #e94560;
+ color: #e94560;
+}
+
+.translate-mode {
+ animation: fadeIn 0.3s ease;
+}
+
+/* Textarea */
+textarea {
+ width: 100%;
+ padding: 1rem;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid #233554;
+ border-radius: 8px;
+ color: #fff;
+ font-size: 1rem;
+ font-family: inherit;
+ resize: vertical;
+ min-height: 150px;
+ transition: all 0.3s ease;
+}
+
+textarea:focus {
+ outline: none;
+ border-color: #e94560;
+ background: rgba(233, 69, 96, 0.1);
+}
+
+textarea::placeholder {
+ color: #8892b0;
+}
+
+/* Translation Result */
+.translation-output {
+ margin-top: 1rem;
+ padding: 1rem;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 8px;
+ white-space: pre-wrap;
+ max-height: 400px;
+ overflow-y: auto;
+ color: #ccd6f6;
+ line-height: 1.6;
+}
+
+/* Summarize Tab */
+.summarize-mode-selector {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.summarize-mode {
+ animation: fadeIn 0.3s ease;
+}
+
+/* Summary Result */
+.summary-output {
+ margin-top: 1rem;
+ padding: 1rem;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 8px;
+ white-space: pre-wrap;
+ max-height: 400px;
+ overflow-y: auto;
+ color: #ccd6f6;
+ line-height: 1.6;
+}
diff --git a/src/server.js b/src/server.js
index f16ed6d..ec7bf67 100644
--- a/src/server.js
+++ b/src/server.js
@@ -1,1137 +1,1137 @@
-import express from 'express';
-import cors from 'cors';
-import dotenv from 'dotenv';
-import path from 'path';
-import fs from 'fs';
-import { fileURLToPath } from 'url';
-import multer from 'multer';
-import { download, getInfo } from './services/youtube.js';
-import { transcribeFile, transcribeAndSave, transcribeMultiple } from './services/transcription.js';
-import { translateText, translateFile, translateMultiple, getLanguages } from './services/translation.js';
-import { summarizeText, summarizeFile, getSummaryStyles } from './services/summarize.js';
-import { convertToMP3, convertMultipleToMP3, getSupportedFormats } from './services/conversion.js';
-
-dotenv.config();
-
-const app = express();
-const PORT = process.env.PORT || 8888;
-const OUTPUT_DIR = process.env.OUTPUT_DIR || './output';
-
-// Ensure output directory exists
-if (!fs.existsSync(OUTPUT_DIR)) {
- fs.mkdirSync(OUTPUT_DIR, { recursive: true });
-}
-
-// Configure multer for file uploads
-const storage = multer.diskStorage({
- destination: (req, file, cb) => {
- cb(null, OUTPUT_DIR);
- },
- filename: (req, file, cb) => {
- // Keep original filename but sanitize it
- const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
- cb(null, safeName);
- }
-});
-
-const upload = multer({
- storage,
- fileFilter: (req, file, cb) => {
- const allowedTypes = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/m4a', 'audio/ogg', 'audio/flac', 'audio/x-m4a'];
- if (allowedTypes.includes(file.mimetype) || file.originalname.match(/\.(mp3|wav|m4a|ogg|flac)$/i)) {
- cb(null, true);
- } else {
- cb(new Error('Invalid file type. Only audio files are allowed.'));
- }
- }
-});
-
-// Upload handler for text files (for translation)
-const uploadText = multer({
- storage,
- fileFilter: (req, file, cb) => {
- if (file.mimetype === 'text/plain' || file.originalname.endsWith('.txt')) {
- cb(null, true);
- } else {
- cb(new Error('Invalid file type. Only text files (.txt) are allowed.'));
- }
- }
-});
-
-// Upload handler for video/audio files (for conversion)
-const uploadVideo = multer({
- storage,
- fileFilter: (req, file, cb) => {
- const videoTypes = ['video/mp4', 'video/avi', 'video/x-msvideo', 'video/quicktime', 'video/x-matroska', 'video/webm'];
- const audioTypes = ['audio/m4a', 'audio/x-m4a', 'audio/wav', 'audio/flac', 'audio/ogg', 'audio/aac'];
- const videoExtensions = /\.(mp4|avi|mkv|mov|wmv|flv|webm|m4v|m4a|wav|flac|ogg|aac)$/i;
-
- if (videoTypes.includes(file.mimetype) || audioTypes.includes(file.mimetype) || file.originalname.match(videoExtensions)) {
- cb(null, true);
- } else {
- cb(new Error('Invalid file type. Only video/audio files are allowed.'));
- }
- }
-});
-
-app.use(cors());
-app.use(express.json());
-
-// Set permissive CSP for development
-app.use((req, res, next) => {
- res.setHeader(
- 'Content-Security-Policy',
- "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'"
- );
- next();
-});
-
-// Serve static files (HTML interface)
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-app.use(express.static(path.join(__dirname, '../public')));
-
-// Serve downloaded files
-app.use('/files', express.static(OUTPUT_DIR));
-
-// API info endpoint
-app.get('/api', (req, res) => {
- res.json({
- name: 'Video to MP3 Transcriptor API',
- version: '1.0.0',
- endpoints: {
- 'GET /health': 'Health check',
- 'GET /info?url=': 'Get video/playlist info',
- 'POST /download': 'Download as MP3',
- 'POST /transcribe': 'Transcribe audio file',
- 'POST /process': 'Download + transcribe',
- 'GET /files-list': 'List downloaded files',
- 'GET /files/': 'Serve downloaded files',
- },
- });
-});
-
-// Health check
-app.get('/health', (req, res) => {
- res.json({ status: 'ok', timestamp: new Date().toISOString() });
-});
-
-/**
- * GET /info?url=
- * Get info about a video or playlist
- */
-app.get('/info', async (req, res) => {
- try {
- const { url } = req.query;
-
- if (!url) {
- return res.status(400).json({ error: 'URL parameter required' });
- }
-
- // Check if URL contains playlist parameter
- const hasPlaylist = url.includes('list=');
- const info = await getInfo(url, hasPlaylist);
-
- res.json({
- success: true,
- title: info.title,
- type: info._type || 'video',
- duration: info.duration,
- channel: info.channel,
- entries: info._type === 'playlist'
- ? info.entries?.map(e => ({ id: e.id, title: e.title }))
- : null,
- videoCount: info._type === 'playlist' ? info.entries?.length : 1,
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * GET /download-stream
- * Download with SSE progress updates
- * Query: url (required), outputPath (optional)
- */
-app.get('/download-stream', async (req, res) => {
- const { url, outputPath } = req.query;
-
- if (!url) {
- return res.status(400).json({ error: 'URL parameter required' });
- }
-
- // Set up SSE
- res.setHeader('Content-Type', 'text/event-stream');
- res.setHeader('Cache-Control', 'no-cache');
- res.setHeader('Connection', 'keep-alive');
- res.setHeader('Access-Control-Allow-Origin', '*');
-
- const sendEvent = (event, data) => {
- res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
- };
-
- // Track timing for estimation
- const startTime = Date.now();
- let videosCompleted = 0;
- let totalVideos = 1;
- const videoTimes = [];
-
- try {
- // First, get info to know total videos
- sendEvent('status', { message: 'Fetching video info...', phase: 'info' });
- const hasPlaylist = url.includes('list=');
- const info = await getInfo(url, hasPlaylist);
-
- totalVideos = info._type === 'playlist' ? (info.entries?.length || 1) : 1;
- sendEvent('info', {
- title: info.title,
- type: info._type || 'video',
- totalVideos,
- playlistTitle: info._type === 'playlist' ? info.title : null,
- });
-
- console.log(`Downloading: ${url}`);
-
- let videoStartTime = Date.now();
-
- const result = await download(url, {
- outputDir: outputPath || OUTPUT_DIR,
- onDownloadProgress: (progress) => {
- // Calculate overall progress
- const videoProgress = progress.percent || 0;
- const overallPercent = ((videosCompleted + (videoProgress / 100)) / totalVideos) * 100;
-
- // Estimate remaining time
- let estimatedRemaining = null;
- if (videosCompleted > 0 && videoTimes.length > 0) {
- const avgTimePerVideo = videoTimes.reduce((a, b) => a + b, 0) / videoTimes.length;
- const remainingVideos = totalVideos - videosCompleted - (videoProgress / 100);
- estimatedRemaining = Math.round(avgTimePerVideo * remainingVideos / 1000);
- } else if (progress.eta) {
- // Parse ETA from yt-dlp (format: MM:SS)
- const [mins, secs] = progress.eta.split(':').map(Number);
- const currentVideoRemaining = mins * 60 + secs;
- const remainingVideos = totalVideos - videosCompleted - 1;
- // Estimate based on current video
- if (videoProgress > 10) {
- const elapsed = (Date.now() - videoStartTime) / 1000;
- const estimatedVideoTime = (elapsed / videoProgress) * 100;
- estimatedRemaining = Math.round(currentVideoRemaining + (remainingVideos * estimatedVideoTime));
- }
- }
-
- sendEvent('progress', {
- percent: Math.round(overallPercent * 10) / 10,
- videoPercent: Math.round(videoProgress * 10) / 10,
- currentVideo: progress.videoIndex || 1,
- totalVideos: progress.totalVideos || totalVideos,
- title: progress.title,
- speed: progress.speed,
- eta: progress.eta,
- estimatedRemaining,
- phase: 'downloading',
- });
- },
- onVideoComplete: (video) => {
- const videoTime = Date.now() - videoStartTime;
- videoTimes.push(videoTime);
- videosCompleted++;
- videoStartTime = Date.now();
-
- sendEvent('video-complete', {
- title: video.title,
- success: video.success,
- videosCompleted,
- totalVideos,
- });
- },
- });
-
- // Send final result
- sendEvent('complete', {
- success: true,
- playlistTitle: result.playlistTitle,
- totalVideos: result.totalVideos,
- successCount: result.successCount,
- failCount: result.failCount,
- totalTime: Math.round((Date.now() - startTime) / 1000),
- videos: result.videos.map(v => ({
- success: v.success,
- title: v.title,
- filePath: v.filePath,
- fileUrl: v.filePath ? `/files/${path.basename(v.filePath)}` : null,
- error: v.error,
- })),
- });
-
- } catch (error) {
- sendEvent('error', { message: error.message });
- } finally {
- res.end();
- }
-});
-
-/**
- * POST /download
- * Download a video or playlist as MP3 (non-streaming version)
- * Body: { url: string, outputPath?: string }
- */
-app.post('/download', async (req, res) => {
- try {
- const { url, outputPath } = req.body;
- const outputDir = outputPath || OUTPUT_DIR;
-
- if (!url) {
- return res.status(400).json({ error: 'URL required in request body' });
- }
-
- console.log(`Downloading: ${url}`);
- const result = await download(url, { outputDir });
-
- res.json({
- success: true,
- playlistTitle: result.playlistTitle,
- totalVideos: result.totalVideos,
- successCount: result.successCount,
- failCount: result.failCount,
- videos: result.videos.map(v => ({
- success: v.success,
- title: v.title,
- filePath: v.filePath,
- fileUrl: v.filePath ? `/files/${path.basename(v.filePath)}` : null,
- error: v.error,
- })),
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * POST /transcribe
- * Transcribe an existing audio file
- * Body: { filePath: string, language?: string, format?: string, outputPath?: string }
- */
-app.post('/transcribe', async (req, res) => {
- try {
- const { filePath, language, format = 'txt', model = 'gpt-4o-mini-transcribe', outputPath } = req.body;
-
- if (!filePath) {
- return res.status(400).json({ error: 'filePath required in request body' });
- }
-
- if (!process.env.OPENAI_API_KEY) {
- return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
- }
-
- console.log(`Transcribing: ${filePath} with model ${model}`);
- const result = await transcribeAndSave(filePath, {
- language,
- responseFormat: format === 'txt' ? 'text' : format,
- outputFormat: format,
- model,
- outputDir: outputPath,
- });
-
- res.json({
- success: true,
- filePath: result.filePath,
- transcriptionPath: result.transcriptionPath,
- transcriptionUrl: `/files/${path.basename(result.transcriptionPath)}`,
- text: result.text,
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * POST /upload-transcribe
- * Upload audio files and transcribe them
- * Body: { language?: string, model?: string, outputPath?: string }
- */
-app.post('/upload-transcribe', upload.array('files', 50), async (req, res) => {
- try {
- if (!process.env.OPENAI_API_KEY) {
- return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
- }
-
- if (!req.files || req.files.length === 0) {
- return res.status(400).json({ error: 'No files uploaded' });
- }
-
- const { language, model = 'gpt-4o-mini-transcribe', outputPath } = req.body;
- const results = [];
-
- console.log(`Transcribing ${req.files.length} uploaded files with model ${model}`);
-
- for (let i = 0; i < req.files.length; i++) {
- const file = req.files[i];
- console.log(`[${i + 1}/${req.files.length}] Transcribing: ${file.originalname}`);
-
- try {
- const result = await transcribeAndSave(file.path, {
- language: language || undefined,
- responseFormat: 'text',
- outputFormat: 'txt',
- model,
- outputDir: outputPath,
- });
-
- results.push({
- success: true,
- fileName: file.originalname,
- filePath: file.path,
- transcriptionPath: result.transcriptionPath,
- transcriptionUrl: `/files/${path.basename(result.transcriptionPath)}`,
- text: result.text,
- });
- } catch (error) {
- console.error(`Failed to transcribe ${file.originalname}: ${error.message}`);
- results.push({
- success: false,
- fileName: file.originalname,
- error: error.message,
- });
- }
- }
-
- res.json({
- success: true,
- totalFiles: req.files.length,
- successCount: results.filter(r => r.success).length,
- failCount: results.filter(r => !r.success).length,
- results,
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * POST /convert-to-mp3
- * Upload video/audio files and convert them to MP3
- */
-app.post('/convert-to-mp3', uploadVideo.array('files', 50), async (req, res) => {
- try {
- if (!req.files || req.files.length === 0) {
- return res.status(400).json({ error: 'No files uploaded' });
- }
-
- const { bitrate = '192k', quality = '2' } = req.body;
- const results = [];
-
- console.log(`Converting ${req.files.length} files to MP3`);
-
- for (let i = 0; i < req.files.length; i++) {
- const file = req.files[i];
- console.log(`[${i + 1}/${req.files.length}] Converting: ${file.originalname}`);
-
- try {
- const result = await convertToMP3(file.path, {
- outputDir: OUTPUT_DIR,
- bitrate,
- quality,
- });
-
- results.push({
- success: true,
- fileName: file.originalname,
- inputPath: file.path,
- outputPath: result.outputPath,
- outputUrl: `/files/${path.basename(result.outputPath)}`,
- size: result.sizeHuman,
- });
- } catch (error) {
- console.error(`Failed to convert ${file.originalname}: ${error.message}`);
- results.push({
- success: false,
- fileName: file.originalname,
- error: error.message,
- });
- }
- }
-
- res.json({
- success: true,
- totalFiles: req.files.length,
- successCount: results.filter(r => r.success).length,
- failCount: results.filter(r => !r.success).length,
- results,
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * GET /supported-formats
- * Get supported video/audio formats for conversion
- */
-app.get('/supported-formats', (req, res) => {
- res.json({ formats: getSupportedFormats() });
-});
-
-/**
- * GET /process-stream
- * Download and transcribe with SSE progress updates
- * Query: url, language?, model?, outputPath?
- */
-app.get('/process-stream', async (req, res) => {
- const { url, language, model = 'gpt-4o-mini-transcribe', outputPath } = req.query;
-
- if (!url) {
- return res.status(400).json({ error: 'URL parameter required' });
- }
-
- if (!process.env.OPENAI_API_KEY) {
- return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
- }
-
- // Set up SSE
- res.setHeader('Content-Type', 'text/event-stream');
- res.setHeader('Cache-Control', 'no-cache');
- res.setHeader('Connection', 'keep-alive');
- res.setHeader('Access-Control-Allow-Origin', '*');
-
- const sendEvent = (event, data) => {
- res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
- };
-
- const startTime = Date.now();
- let videosDownloaded = 0;
- let videosTranscribed = 0;
- let totalVideos = 1;
- const videoTimes = [];
-
- try {
- // Phase 1: Get info
- sendEvent('status', { message: 'Fetching video info...', phase: 'info' });
- const hasPlaylist = url.includes('list=');
- const info = await getInfo(url, hasPlaylist);
-
- totalVideos = info._type === 'playlist' ? (info.entries?.length || 1) : 1;
- sendEvent('info', {
- title: info.title,
- type: info._type || 'video',
- totalVideos,
- playlistTitle: info._type === 'playlist' ? info.title : null,
- });
-
- // Phase 2: Download
- console.log(`Processing: ${url}`);
- let videoStartTime = Date.now();
-
- const downloadResult = await download(url, {
- outputDir: outputPath || OUTPUT_DIR,
- onDownloadProgress: (progress) => {
- const videoProgress = progress.percent || 0;
- // Download is 50% of total, transcribe is other 50%
- const overallPercent = ((videosDownloaded + (videoProgress / 100)) / totalVideos) * 50;
-
- sendEvent('progress', {
- percent: Math.round(overallPercent * 10) / 10,
- videoPercent: Math.round(videoProgress * 10) / 10,
- currentVideo: progress.videoIndex || 1,
- totalVideos: progress.totalVideos || totalVideos,
- title: progress.title,
- speed: progress.speed,
- eta: progress.eta,
- phase: 'downloading',
- phaseLabel: 'Downloading',
- });
- },
- onVideoComplete: (video) => {
- const videoTime = Date.now() - videoStartTime;
- videoTimes.push(videoTime);
- videosDownloaded++;
- videoStartTime = Date.now();
-
- sendEvent('video-complete', {
- title: video.title,
- success: video.success,
- phase: 'downloading',
- videosCompleted: videosDownloaded,
- totalVideos,
- });
- },
- });
-
- // Phase 3: Transcribe
- sendEvent('status', { message: 'Starting transcription...', phase: 'transcribing' });
-
- const successfulDownloads = downloadResult.videos.filter(v => v.success);
- const filePaths = successfulDownloads.map(v => v.filePath);
- const transcribeResults = [];
-
- for (let i = 0; i < filePaths.length; i++) {
- const filePath = filePaths[i];
- const video = successfulDownloads[i];
-
- sendEvent('progress', {
- percent: 50 + ((i / filePaths.length) * 50),
- currentVideo: i + 1,
- totalVideos: filePaths.length,
- title: video.title,
- phase: 'transcribing',
- phaseLabel: 'Transcribing',
- });
-
- try {
- const result = await transcribeAndSave(filePath, {
- language: language || undefined,
- responseFormat: 'text',
- outputFormat: 'txt',
- model,
- outputDir: outputPath,
- });
- transcribeResults.push(result);
- videosTranscribed++;
-
- sendEvent('transcribe-complete', {
- title: video.title,
- success: true,
- videosCompleted: videosTranscribed,
- totalFiles: filePaths.length,
- });
- } catch (error) {
- transcribeResults.push({
- success: false,
- filePath,
- error: error.message,
- });
-
- sendEvent('transcribe-complete', {
- title: video.title,
- success: false,
- error: error.message,
- videosCompleted: videosTranscribed,
- totalFiles: filePaths.length,
- });
- }
- }
-
- // Combine results
- const combinedResults = downloadResult.videos.map(v => {
- const transcription = transcribeResults.find(t => t.filePath === v.filePath);
- return {
- title: v.title,
- downloadSuccess: v.success,
- audioUrl: v.filePath ? `/files/${path.basename(v.filePath)}` : null,
- transcriptionSuccess: transcription?.success || false,
- transcriptionUrl: transcription?.transcriptionPath
- ? `/files/${path.basename(transcription.transcriptionPath)}`
- : null,
- text: transcription?.text,
- error: v.error || transcription?.error,
- };
- });
-
- sendEvent('complete', {
- success: true,
- playlistTitle: downloadResult.playlistTitle,
- totalVideos: downloadResult.totalVideos,
- downloadedCount: downloadResult.successCount,
- transcribedCount: videosTranscribed,
- totalTime: Math.round((Date.now() - startTime) / 1000),
- results: combinedResults,
- });
-
- } catch (error) {
- sendEvent('error', { message: error.message });
- } finally {
- res.end();
- }
-});
-
-/**
- * POST /process
- * Download and transcribe a video or playlist (non-streaming)
- * Body: { url: string, language?: string, format?: string, outputPath?: string }
- */
-app.post('/process', async (req, res) => {
- try {
- const { url, language, format = 'txt', outputPath, model = 'gpt-4o-mini-transcribe' } = req.body;
- const outputDir = outputPath || OUTPUT_DIR;
-
- if (!url) {
- return res.status(400).json({ error: 'URL required in request body' });
- }
-
- if (!process.env.OPENAI_API_KEY) {
- return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
- }
-
- // Step 1: Download
- console.log(`Step 1: Downloading ${url}`);
- const downloadResult = await download(url, { outputDir });
-
- // Step 2: Transcribe
- console.log(`Step 2: Transcribing with model ${model}...`);
- const successfulDownloads = downloadResult.videos.filter(v => v.success);
- const filePaths = successfulDownloads.map(v => v.filePath);
-
- const transcribeResult = await transcribeMultiple(filePaths, {
- language,
- responseFormat: format === 'txt' ? 'text' : format,
- outputFormat: format,
- model,
- outputDir,
- });
-
- // Combine results
- const combinedResults = downloadResult.videos.map(v => {
- const transcription = transcribeResult.results.find(
- t => t.filePath === v.filePath
- );
-
- return {
- title: v.title,
- downloadSuccess: v.success,
- audioPath: v.filePath,
- audioUrl: v.filePath ? `/files/${path.basename(v.filePath)}` : null,
- transcriptionSuccess: transcription?.success || false,
- transcriptionPath: transcription?.transcriptionPath,
- transcriptionUrl: transcription?.transcriptionPath
- ? `/files/${path.basename(transcription.transcriptionPath)}`
- : null,
- text: transcription?.text,
- error: v.error || transcription?.error,
- };
- });
-
- res.json({
- success: true,
- playlistTitle: downloadResult.playlistTitle,
- totalVideos: downloadResult.totalVideos,
- downloadedCount: downloadResult.successCount,
- transcribedCount: transcribeResult.successCount,
- results: combinedResults,
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * GET /files
- * List all downloaded files
- */
-app.get('/files-list', (req, res) => {
- try {
- if (!fs.existsSync(OUTPUT_DIR)) {
- return res.json({ files: [] });
- }
-
- const files = fs.readdirSync(OUTPUT_DIR).map(file => ({
- name: file,
- url: `/files/${file}`,
- path: path.join(OUTPUT_DIR, file),
- }));
-
- res.json({ files });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * GET /languages
- * Get available translation languages
- */
-app.get('/languages', (req, res) => {
- res.json({ languages: getLanguages() });
-});
-
-/**
- * POST /translate
- * Translate text
- * Body: { text: string, targetLang: string, sourceLang?: string }
- */
-app.post('/translate', async (req, res) => {
- try {
- const { text, targetLang, sourceLang } = req.body;
-
- if (!text) {
- return res.status(400).json({ error: 'text required in request body' });
- }
- if (!targetLang) {
- return res.status(400).json({ error: 'targetLang required in request body' });
- }
-
- if (!process.env.OPENAI_API_KEY) {
- return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
- }
-
- console.log(`Translating text to ${targetLang}`);
- const result = await translateText(text, targetLang, sourceLang);
-
- res.json({
- success: true,
- ...result,
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * POST /translate-file
- * Translate uploaded text files
- * Body: { targetLang: string, sourceLang?: string, outputPath?: string }
- */
-app.post('/translate-file', uploadText.array('files', 50), async (req, res) => {
- try {
- if (!process.env.OPENAI_API_KEY) {
- return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
- }
-
- if (!req.files || req.files.length === 0) {
- return res.status(400).json({ error: 'No files uploaded' });
- }
-
- const { targetLang, sourceLang, outputPath } = req.body;
-
- if (!targetLang) {
- return res.status(400).json({ error: 'targetLang required' });
- }
-
- const results = [];
- console.log(`Translating ${req.files.length} files to ${targetLang}`);
-
- for (let i = 0; i < req.files.length; i++) {
- const file = req.files[i];
- console.log(`[${i + 1}/${req.files.length}] Translating: ${file.originalname}`);
-
- try {
- const result = await translateFile(file.path, targetLang, sourceLang || null, outputPath);
- results.push({
- success: true,
- fileName: file.originalname,
- translationPath: result.translationPath,
- translationUrl: `/files/${path.basename(result.translationPath)}`,
- translatedText: result.translatedText,
- });
- } catch (error) {
- console.error(`Failed to translate ${file.originalname}: ${error.message}`);
- results.push({
- success: false,
- fileName: file.originalname,
- error: error.message,
- });
- }
- }
-
- res.json({
- success: true,
- totalFiles: req.files.length,
- successCount: results.filter(r => r.success).length,
- failCount: results.filter(r => !r.success).length,
- results,
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * GET /summary-styles
- * Get available summary styles
- */
-app.get('/summary-styles', (req, res) => {
- res.json({ styles: getSummaryStyles() });
-});
-
-/**
- * POST /summarize
- * Summarize text using GPT-5.1
- * Body: { text: string, style?: string, language?: string, model?: string }
- */
-app.post('/summarize', async (req, res) => {
- try {
- const { text, style = 'concise', language = 'same', model = 'gpt-5.1' } = req.body;
-
- if (!text) {
- return res.status(400).json({ error: 'text required in request body' });
- }
-
- if (!process.env.OPENAI_API_KEY) {
- return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
- }
-
- console.log(`Summarizing text with ${model} (style: ${style})`);
- const result = await summarizeText(text, { style, language, model });
-
- res.json({
- success: true,
- ...result,
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * POST /summarize-file
- * Summarize uploaded text files using GPT-5.1
- * Body: { style?: string, language?: string, model?: string, outputPath?: string }
- */
-app.post('/summarize-file', uploadText.array('files', 50), async (req, res) => {
- try {
- if (!process.env.OPENAI_API_KEY) {
- return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
- }
-
- if (!req.files || req.files.length === 0) {
- return res.status(400).json({ error: 'No files uploaded' });
- }
-
- const { style = 'concise', language = 'same', model = 'gpt-5.1', outputPath } = req.body;
- const results = [];
-
- console.log(`Summarizing ${req.files.length} files with ${model}`);
-
- for (let i = 0; i < req.files.length; i++) {
- const file = req.files[i];
- console.log(`[${i + 1}/${req.files.length}] Summarizing: ${file.originalname}`);
-
- try {
- const result = await summarizeFile(file.path, { style, language, model, outputDir: outputPath });
- results.push({
- success: true,
- fileName: file.originalname,
- summaryPath: result.summaryPath,
- summaryUrl: `/files/${path.basename(result.summaryPath)}`,
- summary: result.summary,
- model: result.model,
- chunks: result.chunks,
- });
- } catch (error) {
- console.error(`Failed to summarize ${file.originalname}: ${error.message}`);
- results.push({
- success: false,
- fileName: file.originalname,
- error: error.message,
- });
- }
- }
-
- res.json({
- success: true,
- totalFiles: req.files.length,
- successCount: results.filter(r => r.success).length,
- failCount: results.filter(r => !r.success).length,
- results,
- });
- } catch (error) {
- res.status(500).json({ error: error.message });
- }
-});
-
-/**
- * GET /summarize-stream
- * Full pipeline: Download -> Transcribe -> Summarize with SSE progress
- * Query: url, style?, language?, model?, outputPath?
- */
-app.get('/summarize-stream', async (req, res) => {
- const { url, style = 'concise', language = 'same', model = 'gpt-4o-mini-transcribe', outputPath } = req.query;
-
- if (!url) {
- return res.status(400).json({ error: 'URL parameter required' });
- }
-
- if (!process.env.OPENAI_API_KEY) {
- return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
- }
-
- // Set up SSE
- res.setHeader('Content-Type', 'text/event-stream');
- res.setHeader('Cache-Control', 'no-cache');
- res.setHeader('Connection', 'keep-alive');
- res.setHeader('Access-Control-Allow-Origin', '*');
-
- const sendEvent = (event, data) => {
- res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
- };
-
- const startTime = Date.now();
- let totalVideos = 1;
-
- try {
- // Phase 1: Get info
- sendEvent('status', { message: 'Fetching video info...', phase: 'info', percent: 0 });
- const hasPlaylist = url.includes('list=');
- const info = await getInfo(url, hasPlaylist);
-
- totalVideos = info._type === 'playlist' ? (info.entries?.length || 1) : 1;
- sendEvent('info', {
- title: info.title,
- type: info._type || 'video',
- totalVideos,
- playlistTitle: info._type === 'playlist' ? info.title : null,
- });
-
- // Phase 2: Download (0-33%)
- sendEvent('status', { message: 'Downloading...', phase: 'downloading', percent: 5 });
- console.log(`[Summarize Pipeline] Downloading: ${url}`);
-
- let videosDownloaded = 0;
- const downloadResult = await download(url, {
- outputDir: outputPath || OUTPUT_DIR,
- onDownloadProgress: (progress) => {
- const videoProgress = progress.percent || 0;
- const overallPercent = ((videosDownloaded + (videoProgress / 100)) / totalVideos) * 33;
-
- sendEvent('progress', {
- percent: Math.round(overallPercent * 10) / 10,
- phase: 'downloading',
- phaseLabel: 'Downloading',
- title: progress.title,
- speed: progress.speed,
- currentVideo: progress.videoIndex || videosDownloaded + 1,
- totalVideos,
- });
- },
- onVideoComplete: (video) => {
- videosDownloaded++;
- sendEvent('video-complete', {
- title: video.title,
- phase: 'downloading',
- videosCompleted: videosDownloaded,
- totalVideos,
- });
- },
- });
-
- // Phase 3: Transcribe (33-66%)
- sendEvent('status', { message: 'Transcribing...', phase: 'transcribing', percent: 33 });
- console.log(`[Summarize Pipeline] Transcribing ${downloadResult.successCount} files`);
-
- const successfulDownloads = downloadResult.videos.filter(v => v.success);
- const transcribeResults = [];
-
- for (let i = 0; i < successfulDownloads.length; i++) {
- const video = successfulDownloads[i];
- const percent = 33 + ((i / successfulDownloads.length) * 33);
-
- sendEvent('progress', {
- percent: Math.round(percent * 10) / 10,
- phase: 'transcribing',
- phaseLabel: 'Transcribing',
- title: video.title,
- currentVideo: i + 1,
- totalVideos: successfulDownloads.length,
- });
-
- try {
- const result = await transcribeAndSave(video.filePath, {
- responseFormat: 'text',
- outputFormat: 'txt',
- model,
- outputDir: outputPath,
- });
- transcribeResults.push({ ...result, title: video.title, success: true });
-
- sendEvent('transcribe-complete', {
- title: video.title,
- success: true,
- videosCompleted: i + 1,
- totalVideos: successfulDownloads.length,
- });
- } catch (error) {
- transcribeResults.push({ title: video.title, success: false, error: error.message });
- sendEvent('transcribe-complete', {
- title: video.title,
- success: false,
- error: error.message,
- });
- }
- }
-
- // Phase 4: Summarize (66-100%)
- sendEvent('status', { message: 'Summarizing with GPT-5.1...', phase: 'summarizing', percent: 66 });
- console.log(`[Summarize Pipeline] Summarizing ${transcribeResults.filter(t => t.success).length} transcriptions`);
-
- const summaryResults = [];
- const successfulTranscriptions = transcribeResults.filter(t => t.success);
-
- for (let i = 0; i < successfulTranscriptions.length; i++) {
- const transcription = successfulTranscriptions[i];
- const percent = 66 + ((i / successfulTranscriptions.length) * 34);
-
- sendEvent('progress', {
- percent: Math.round(percent * 10) / 10,
- phase: 'summarizing',
- phaseLabel: 'Summarizing',
- title: transcription.title,
- currentVideo: i + 1,
- totalVideos: successfulTranscriptions.length,
- });
-
- try {
- const result = await summarizeFile(transcription.transcriptionPath, { style, language, model: 'gpt-5.1', outputDir: outputPath });
- summaryResults.push({
- title: transcription.title,
- success: true,
- summary: result.summary,
- summaryPath: result.summaryPath,
- summaryUrl: `/files/${path.basename(result.summaryPath)}`,
- transcriptionUrl: `/files/${path.basename(transcription.transcriptionPath)}`,
- audioUrl: transcription.filePath ? `/files/${path.basename(transcription.filePath)}` : null,
- });
-
- sendEvent('summarize-complete', {
- title: transcription.title,
- success: true,
- videosCompleted: i + 1,
- totalVideos: successfulTranscriptions.length,
- });
- } catch (error) {
- summaryResults.push({
- title: transcription.title,
- success: false,
- error: error.message,
- transcriptionUrl: `/files/${path.basename(transcription.transcriptionPath)}`,
- });
- sendEvent('summarize-complete', {
- title: transcription.title,
- success: false,
- error: error.message,
- });
- }
- }
-
- // Final result
- sendEvent('complete', {
- success: true,
- playlistTitle: downloadResult.playlistTitle,
- totalVideos: downloadResult.totalVideos,
- downloadedCount: downloadResult.successCount,
- transcribedCount: transcribeResults.filter(t => t.success).length,
- summarizedCount: summaryResults.filter(s => s.success).length,
- totalTime: Math.round((Date.now() - startTime) / 1000),
- results: summaryResults,
- });
-
- } catch (error) {
- console.error(`[Summarize Pipeline] Error: ${error.message}`);
- sendEvent('error', { message: error.message });
- } finally {
- res.end();
- }
-});
-
-app.listen(PORT, () => {
- console.log(`Server running on http://localhost:${PORT}`);
- console.log('\nEndpoints:');
- console.log(' GET /health - Health check');
- console.log(' GET /info?url= - Get video/playlist info');
- console.log(' POST /download - Download as MP3');
- console.log(' POST /transcribe - Transcribe audio file');
- console.log(' POST /process - Download + transcribe');
- console.log(' POST /summarize - Summarize text (GPT-5.1)');
- console.log(' POST /summarize-file - Summarize files (GPT-5.1)');
- console.log(' GET /summarize-stream - Full pipeline: Download + Transcribe + Summarize');
- console.log(' GET /files-list - List downloaded files');
- console.log(' GET /files/ - Serve downloaded files');
-});
+import express from 'express';
+import cors from 'cors';
+import dotenv from 'dotenv';
+import path from 'path';
+import fs from 'fs';
+import { fileURLToPath } from 'url';
+import multer from 'multer';
+import { download, getInfo } from './services/youtube.js';
+import { transcribeFile, transcribeAndSave, transcribeMultiple } from './services/transcription.js';
+import { translateText, translateFile, translateMultiple, getLanguages } from './services/translation.js';
+import { summarizeText, summarizeFile, getSummaryStyles } from './services/summarize.js';
+import { convertToMP3, convertMultipleToMP3, getSupportedFormats } from './services/conversion.js';
+
+dotenv.config();
+
+const app = express();
+const PORT = process.env.PORT || 8888;
+const OUTPUT_DIR = process.env.OUTPUT_DIR || './output';
+
+// Ensure output directory exists
+if (!fs.existsSync(OUTPUT_DIR)) {
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
+}
+
+// Configure multer for file uploads
+const storage = multer.diskStorage({
+ destination: (req, file, cb) => {
+ cb(null, OUTPUT_DIR);
+ },
+ filename: (req, file, cb) => {
+ // Keep original filename but sanitize it
+ const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
+ cb(null, safeName);
+ }
+});
+
+const upload = multer({
+ storage,
+ fileFilter: (req, file, cb) => {
+ const allowedTypes = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/m4a', 'audio/ogg', 'audio/flac', 'audio/x-m4a'];
+ if (allowedTypes.includes(file.mimetype) || file.originalname.match(/\.(mp3|wav|m4a|ogg|flac)$/i)) {
+ cb(null, true);
+ } else {
+ cb(new Error('Invalid file type. Only audio files are allowed.'));
+ }
+ }
+});
+
+// Upload handler for text files (for translation)
+const uploadText = multer({
+ storage,
+ fileFilter: (req, file, cb) => {
+ if (file.mimetype === 'text/plain' || file.originalname.endsWith('.txt')) {
+ cb(null, true);
+ } else {
+ cb(new Error('Invalid file type. Only text files (.txt) are allowed.'));
+ }
+ }
+});
+
+// Upload handler for video/audio files (for conversion)
+const uploadVideo = multer({
+ storage,
+ fileFilter: (req, file, cb) => {
+ const videoTypes = ['video/mp4', 'video/avi', 'video/x-msvideo', 'video/quicktime', 'video/x-matroska', 'video/webm'];
+ const audioTypes = ['audio/m4a', 'audio/x-m4a', 'audio/wav', 'audio/flac', 'audio/ogg', 'audio/aac'];
+ const videoExtensions = /\.(mp4|avi|mkv|mov|wmv|flv|webm|m4v|m4a|wav|flac|ogg|aac)$/i;
+
+ if (videoTypes.includes(file.mimetype) || audioTypes.includes(file.mimetype) || file.originalname.match(videoExtensions)) {
+ cb(null, true);
+ } else {
+ cb(new Error('Invalid file type. Only video/audio files are allowed.'));
+ }
+ }
+});
+
+app.use(cors());
+app.use(express.json());
+
+// Set permissive CSP for development
+app.use((req, res, next) => {
+ res.setHeader(
+ 'Content-Security-Policy',
+ "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'"
+ );
+ next();
+});
+
+// Serve static files (HTML interface)
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+app.use(express.static(path.join(__dirname, '../public')));
+
+// Serve downloaded files
+app.use('/files', express.static(OUTPUT_DIR));
+
+// API info endpoint
+app.get('/api', (req, res) => {
+ res.json({
+ name: 'Video to MP3 Transcriptor API',
+ version: '1.0.0',
+ endpoints: {
+ 'GET /health': 'Health check',
+ 'GET /info?url=': 'Get video/playlist info',
+ 'POST /download': 'Download as MP3',
+ 'POST /transcribe': 'Transcribe audio file',
+ 'POST /process': 'Download + transcribe',
+ 'GET /files-list': 'List downloaded files',
+ 'GET /files/': 'Serve downloaded files',
+ },
+ });
+});
+
+// Health check
+app.get('/health', (req, res) => {
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
+});
+
+/**
+ * GET /info?url=
+ * Get info about a video or playlist
+ */
+app.get('/info', async (req, res) => {
+ try {
+ const { url } = req.query;
+
+ if (!url) {
+ return res.status(400).json({ error: 'URL parameter required' });
+ }
+
+ // Check if URL contains playlist parameter
+ const hasPlaylist = url.includes('list=');
+ const info = await getInfo(url, hasPlaylist);
+
+ res.json({
+ success: true,
+ title: info.title,
+ type: info._type || 'video',
+ duration: info.duration,
+ channel: info.channel,
+ entries: info._type === 'playlist'
+ ? info.entries?.map(e => ({ id: e.id, title: e.title }))
+ : null,
+ videoCount: info._type === 'playlist' ? info.entries?.length : 1,
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * GET /download-stream
+ * Download with SSE progress updates
+ * Query: url (required), outputPath (optional)
+ */
+app.get('/download-stream', async (req, res) => {
+ const { url, outputPath } = req.query;
+
+ if (!url) {
+ return res.status(400).json({ error: 'URL parameter required' });
+ }
+
+ // Set up SSE
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+
+ const sendEvent = (event, data) => {
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
+ };
+
+ // Track timing for estimation
+ const startTime = Date.now();
+ let videosCompleted = 0;
+ let totalVideos = 1;
+ const videoTimes = [];
+
+ try {
+ // First, get info to know total videos
+ sendEvent('status', { message: 'Fetching video info...', phase: 'info' });
+ const hasPlaylist = url.includes('list=');
+ const info = await getInfo(url, hasPlaylist);
+
+ totalVideos = info._type === 'playlist' ? (info.entries?.length || 1) : 1;
+ sendEvent('info', {
+ title: info.title,
+ type: info._type || 'video',
+ totalVideos,
+ playlistTitle: info._type === 'playlist' ? info.title : null,
+ });
+
+ console.log(`Downloading: ${url}`);
+
+ let videoStartTime = Date.now();
+
+ const result = await download(url, {
+ outputDir: outputPath || OUTPUT_DIR,
+ onDownloadProgress: (progress) => {
+ // Calculate overall progress
+ const videoProgress = progress.percent || 0;
+ const overallPercent = ((videosCompleted + (videoProgress / 100)) / totalVideos) * 100;
+
+ // Estimate remaining time
+ let estimatedRemaining = null;
+ if (videosCompleted > 0 && videoTimes.length > 0) {
+ const avgTimePerVideo = videoTimes.reduce((a, b) => a + b, 0) / videoTimes.length;
+ const remainingVideos = totalVideos - videosCompleted - (videoProgress / 100);
+ estimatedRemaining = Math.round(avgTimePerVideo * remainingVideos / 1000);
+ } else if (progress.eta) {
+ // Parse ETA from yt-dlp (format: MM:SS)
+ const [mins, secs] = progress.eta.split(':').map(Number);
+ const currentVideoRemaining = mins * 60 + secs;
+ const remainingVideos = totalVideos - videosCompleted - 1;
+ // Estimate based on current video
+ if (videoProgress > 10) {
+ const elapsed = (Date.now() - videoStartTime) / 1000;
+ const estimatedVideoTime = (elapsed / videoProgress) * 100;
+ estimatedRemaining = Math.round(currentVideoRemaining + (remainingVideos * estimatedVideoTime));
+ }
+ }
+
+ sendEvent('progress', {
+ percent: Math.round(overallPercent * 10) / 10,
+ videoPercent: Math.round(videoProgress * 10) / 10,
+ currentVideo: progress.videoIndex || 1,
+ totalVideos: progress.totalVideos || totalVideos,
+ title: progress.title,
+ speed: progress.speed,
+ eta: progress.eta,
+ estimatedRemaining,
+ phase: 'downloading',
+ });
+ },
+ onVideoComplete: (video) => {
+ const videoTime = Date.now() - videoStartTime;
+ videoTimes.push(videoTime);
+ videosCompleted++;
+ videoStartTime = Date.now();
+
+ sendEvent('video-complete', {
+ title: video.title,
+ success: video.success,
+ videosCompleted,
+ totalVideos,
+ });
+ },
+ });
+
+ // Send final result
+ sendEvent('complete', {
+ success: true,
+ playlistTitle: result.playlistTitle,
+ totalVideos: result.totalVideos,
+ successCount: result.successCount,
+ failCount: result.failCount,
+ totalTime: Math.round((Date.now() - startTime) / 1000),
+ videos: result.videos.map(v => ({
+ success: v.success,
+ title: v.title,
+ filePath: v.filePath,
+ fileUrl: v.filePath ? `/files/${path.basename(v.filePath)}` : null,
+ error: v.error,
+ })),
+ });
+
+ } catch (error) {
+ sendEvent('error', { message: error.message });
+ } finally {
+ res.end();
+ }
+});
+
+/**
+ * POST /download
+ * Download a video or playlist as MP3 (non-streaming version)
+ * Body: { url: string, outputPath?: string }
+ */
+app.post('/download', async (req, res) => {
+ try {
+ const { url, outputPath } = req.body;
+ const outputDir = outputPath || OUTPUT_DIR;
+
+ if (!url) {
+ return res.status(400).json({ error: 'URL required in request body' });
+ }
+
+ console.log(`Downloading: ${url}`);
+ const result = await download(url, { outputDir });
+
+ res.json({
+ success: true,
+ playlistTitle: result.playlistTitle,
+ totalVideos: result.totalVideos,
+ successCount: result.successCount,
+ failCount: result.failCount,
+ videos: result.videos.map(v => ({
+ success: v.success,
+ title: v.title,
+ filePath: v.filePath,
+ fileUrl: v.filePath ? `/files/${path.basename(v.filePath)}` : null,
+ error: v.error,
+ })),
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * POST /transcribe
+ * Transcribe an existing audio file
+ * Body: { filePath: string, language?: string, format?: string, outputPath?: string }
+ */
+app.post('/transcribe', async (req, res) => {
+ try {
+ const { filePath, language, format = 'txt', model = 'gpt-4o-mini-transcribe', outputPath } = req.body;
+
+ if (!filePath) {
+ return res.status(400).json({ error: 'filePath required in request body' });
+ }
+
+ if (!process.env.OPENAI_API_KEY) {
+ return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
+ }
+
+ console.log(`Transcribing: ${filePath} with model ${model}`);
+ const result = await transcribeAndSave(filePath, {
+ language,
+ responseFormat: format === 'txt' ? 'text' : format,
+ outputFormat: format,
+ model,
+ outputDir: outputPath,
+ });
+
+ res.json({
+ success: true,
+ filePath: result.filePath,
+ transcriptionPath: result.transcriptionPath,
+ transcriptionUrl: `/files/${path.basename(result.transcriptionPath)}`,
+ text: result.text,
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * POST /upload-transcribe
+ * Upload audio files and transcribe them
+ * Body: { language?: string, model?: string, outputPath?: string }
+ */
+app.post('/upload-transcribe', upload.array('files', 50), async (req, res) => {
+ try {
+ if (!process.env.OPENAI_API_KEY) {
+ return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
+ }
+
+ if (!req.files || req.files.length === 0) {
+ return res.status(400).json({ error: 'No files uploaded' });
+ }
+
+ const { language, model = 'gpt-4o-mini-transcribe', outputPath } = req.body;
+ const results = [];
+
+ console.log(`Transcribing ${req.files.length} uploaded files with model ${model}`);
+
+ for (let i = 0; i < req.files.length; i++) {
+ const file = req.files[i];
+ console.log(`[${i + 1}/${req.files.length}] Transcribing: ${file.originalname}`);
+
+ try {
+ const result = await transcribeAndSave(file.path, {
+ language: language || undefined,
+ responseFormat: 'text',
+ outputFormat: 'txt',
+ model,
+ outputDir: outputPath,
+ });
+
+ results.push({
+ success: true,
+ fileName: file.originalname,
+ filePath: file.path,
+ transcriptionPath: result.transcriptionPath,
+ transcriptionUrl: `/files/${path.basename(result.transcriptionPath)}`,
+ text: result.text,
+ });
+ } catch (error) {
+ console.error(`Failed to transcribe ${file.originalname}: ${error.message}`);
+ results.push({
+ success: false,
+ fileName: file.originalname,
+ error: error.message,
+ });
+ }
+ }
+
+ res.json({
+ success: true,
+ totalFiles: req.files.length,
+ successCount: results.filter(r => r.success).length,
+ failCount: results.filter(r => !r.success).length,
+ results,
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * POST /convert-to-mp3
+ * Upload video/audio files and convert them to MP3
+ */
+app.post('/convert-to-mp3', uploadVideo.array('files', 50), async (req, res) => {
+ try {
+ if (!req.files || req.files.length === 0) {
+ return res.status(400).json({ error: 'No files uploaded' });
+ }
+
+ const { bitrate = '192k', quality = '2' } = req.body;
+ const results = [];
+
+ console.log(`Converting ${req.files.length} files to MP3`);
+
+ for (let i = 0; i < req.files.length; i++) {
+ const file = req.files[i];
+ console.log(`[${i + 1}/${req.files.length}] Converting: ${file.originalname}`);
+
+ try {
+ const result = await convertToMP3(file.path, {
+ outputDir: OUTPUT_DIR,
+ bitrate,
+ quality,
+ });
+
+ results.push({
+ success: true,
+ fileName: file.originalname,
+ inputPath: file.path,
+ outputPath: result.outputPath,
+ outputUrl: `/files/${path.basename(result.outputPath)}`,
+ size: result.sizeHuman,
+ });
+ } catch (error) {
+ console.error(`Failed to convert ${file.originalname}: ${error.message}`);
+ results.push({
+ success: false,
+ fileName: file.originalname,
+ error: error.message,
+ });
+ }
+ }
+
+ res.json({
+ success: true,
+ totalFiles: req.files.length,
+ successCount: results.filter(r => r.success).length,
+ failCount: results.filter(r => !r.success).length,
+ results,
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * GET /supported-formats
+ * Get supported video/audio formats for conversion
+ */
+app.get('/supported-formats', (req, res) => {
+ res.json({ formats: getSupportedFormats() });
+});
+
+/**
+ * GET /process-stream
+ * Download and transcribe with SSE progress updates
+ * Query: url, language?, model?, outputPath?
+ */
+app.get('/process-stream', async (req, res) => {
+ const { url, language, model = 'gpt-4o-mini-transcribe', outputPath } = req.query;
+
+ if (!url) {
+ return res.status(400).json({ error: 'URL parameter required' });
+ }
+
+ if (!process.env.OPENAI_API_KEY) {
+ return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
+ }
+
+ // Set up SSE
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+
+ const sendEvent = (event, data) => {
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
+ };
+
+ const startTime = Date.now();
+ let videosDownloaded = 0;
+ let videosTranscribed = 0;
+ let totalVideos = 1;
+ const videoTimes = [];
+
+ try {
+ // Phase 1: Get info
+ sendEvent('status', { message: 'Fetching video info...', phase: 'info' });
+ const hasPlaylist = url.includes('list=');
+ const info = await getInfo(url, hasPlaylist);
+
+ totalVideos = info._type === 'playlist' ? (info.entries?.length || 1) : 1;
+ sendEvent('info', {
+ title: info.title,
+ type: info._type || 'video',
+ totalVideos,
+ playlistTitle: info._type === 'playlist' ? info.title : null,
+ });
+
+ // Phase 2: Download
+ console.log(`Processing: ${url}`);
+ let videoStartTime = Date.now();
+
+ const downloadResult = await download(url, {
+ outputDir: outputPath || OUTPUT_DIR,
+ onDownloadProgress: (progress) => {
+ const videoProgress = progress.percent || 0;
+ // Download is 50% of total, transcribe is other 50%
+ const overallPercent = ((videosDownloaded + (videoProgress / 100)) / totalVideos) * 50;
+
+ sendEvent('progress', {
+ percent: Math.round(overallPercent * 10) / 10,
+ videoPercent: Math.round(videoProgress * 10) / 10,
+ currentVideo: progress.videoIndex || 1,
+ totalVideos: progress.totalVideos || totalVideos,
+ title: progress.title,
+ speed: progress.speed,
+ eta: progress.eta,
+ phase: 'downloading',
+ phaseLabel: 'Downloading',
+ });
+ },
+ onVideoComplete: (video) => {
+ const videoTime = Date.now() - videoStartTime;
+ videoTimes.push(videoTime);
+ videosDownloaded++;
+ videoStartTime = Date.now();
+
+ sendEvent('video-complete', {
+ title: video.title,
+ success: video.success,
+ phase: 'downloading',
+ videosCompleted: videosDownloaded,
+ totalVideos,
+ });
+ },
+ });
+
+ // Phase 3: Transcribe
+ sendEvent('status', { message: 'Starting transcription...', phase: 'transcribing' });
+
+ const successfulDownloads = downloadResult.videos.filter(v => v.success);
+ const filePaths = successfulDownloads.map(v => v.filePath);
+ const transcribeResults = [];
+
+ for (let i = 0; i < filePaths.length; i++) {
+ const filePath = filePaths[i];
+ const video = successfulDownloads[i];
+
+ sendEvent('progress', {
+ percent: 50 + ((i / filePaths.length) * 50),
+ currentVideo: i + 1,
+ totalVideos: filePaths.length,
+ title: video.title,
+ phase: 'transcribing',
+ phaseLabel: 'Transcribing',
+ });
+
+ try {
+ const result = await transcribeAndSave(filePath, {
+ language: language || undefined,
+ responseFormat: 'text',
+ outputFormat: 'txt',
+ model,
+ outputDir: outputPath,
+ });
+ transcribeResults.push(result);
+ videosTranscribed++;
+
+ sendEvent('transcribe-complete', {
+ title: video.title,
+ success: true,
+ videosCompleted: videosTranscribed,
+ totalFiles: filePaths.length,
+ });
+ } catch (error) {
+ transcribeResults.push({
+ success: false,
+ filePath,
+ error: error.message,
+ });
+
+ sendEvent('transcribe-complete', {
+ title: video.title,
+ success: false,
+ error: error.message,
+ videosCompleted: videosTranscribed,
+ totalFiles: filePaths.length,
+ });
+ }
+ }
+
+ // Combine results
+ const combinedResults = downloadResult.videos.map(v => {
+ const transcription = transcribeResults.find(t => t.filePath === v.filePath);
+ return {
+ title: v.title,
+ downloadSuccess: v.success,
+ audioUrl: v.filePath ? `/files/${path.basename(v.filePath)}` : null,
+ transcriptionSuccess: transcription?.success || false,
+ transcriptionUrl: transcription?.transcriptionPath
+ ? `/files/${path.basename(transcription.transcriptionPath)}`
+ : null,
+ text: transcription?.text,
+ error: v.error || transcription?.error,
+ };
+ });
+
+ sendEvent('complete', {
+ success: true,
+ playlistTitle: downloadResult.playlistTitle,
+ totalVideos: downloadResult.totalVideos,
+ downloadedCount: downloadResult.successCount,
+ transcribedCount: videosTranscribed,
+ totalTime: Math.round((Date.now() - startTime) / 1000),
+ results: combinedResults,
+ });
+
+ } catch (error) {
+ sendEvent('error', { message: error.message });
+ } finally {
+ res.end();
+ }
+});
+
+/**
+ * POST /process
+ * Download and transcribe a video or playlist (non-streaming)
+ * Body: { url: string, language?: string, format?: string, outputPath?: string }
+ */
+app.post('/process', async (req, res) => {
+ try {
+ const { url, language, format = 'txt', outputPath, model = 'gpt-4o-mini-transcribe' } = req.body;
+ const outputDir = outputPath || OUTPUT_DIR;
+
+ if (!url) {
+ return res.status(400).json({ error: 'URL required in request body' });
+ }
+
+ if (!process.env.OPENAI_API_KEY) {
+ return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
+ }
+
+ // Step 1: Download
+ console.log(`Step 1: Downloading ${url}`);
+ const downloadResult = await download(url, { outputDir });
+
+ // Step 2: Transcribe
+ console.log(`Step 2: Transcribing with model ${model}...`);
+ const successfulDownloads = downloadResult.videos.filter(v => v.success);
+ const filePaths = successfulDownloads.map(v => v.filePath);
+
+ const transcribeResult = await transcribeMultiple(filePaths, {
+ language,
+ responseFormat: format === 'txt' ? 'text' : format,
+ outputFormat: format,
+ model,
+ outputDir,
+ });
+
+ // Combine results
+ const combinedResults = downloadResult.videos.map(v => {
+ const transcription = transcribeResult.results.find(
+ t => t.filePath === v.filePath
+ );
+
+ return {
+ title: v.title,
+ downloadSuccess: v.success,
+ audioPath: v.filePath,
+ audioUrl: v.filePath ? `/files/${path.basename(v.filePath)}` : null,
+ transcriptionSuccess: transcription?.success || false,
+ transcriptionPath: transcription?.transcriptionPath,
+ transcriptionUrl: transcription?.transcriptionPath
+ ? `/files/${path.basename(transcription.transcriptionPath)}`
+ : null,
+ text: transcription?.text,
+ error: v.error || transcription?.error,
+ };
+ });
+
+ res.json({
+ success: true,
+ playlistTitle: downloadResult.playlistTitle,
+ totalVideos: downloadResult.totalVideos,
+ downloadedCount: downloadResult.successCount,
+ transcribedCount: transcribeResult.successCount,
+ results: combinedResults,
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * GET /files
+ * List all downloaded files
+ */
+app.get('/files-list', (req, res) => {
+ try {
+ if (!fs.existsSync(OUTPUT_DIR)) {
+ return res.json({ files: [] });
+ }
+
+ const files = fs.readdirSync(OUTPUT_DIR).map(file => ({
+ name: file,
+ url: `/files/${file}`,
+ path: path.join(OUTPUT_DIR, file),
+ }));
+
+ res.json({ files });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * GET /languages
+ * Get available translation languages
+ */
+app.get('/languages', (req, res) => {
+ res.json({ languages: getLanguages() });
+});
+
+/**
+ * POST /translate
+ * Translate text
+ * Body: { text: string, targetLang: string, sourceLang?: string }
+ */
+app.post('/translate', async (req, res) => {
+ try {
+ const { text, targetLang, sourceLang } = req.body;
+
+ if (!text) {
+ return res.status(400).json({ error: 'text required in request body' });
+ }
+ if (!targetLang) {
+ return res.status(400).json({ error: 'targetLang required in request body' });
+ }
+
+ if (!process.env.OPENAI_API_KEY) {
+ return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
+ }
+
+ console.log(`Translating text to ${targetLang}`);
+ const result = await translateText(text, targetLang, sourceLang);
+
+ res.json({
+ success: true,
+ ...result,
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * POST /translate-file
+ * Translate uploaded text files
+ * Body: { targetLang: string, sourceLang?: string, outputPath?: string }
+ */
+app.post('/translate-file', uploadText.array('files', 50), async (req, res) => {
+ try {
+ if (!process.env.OPENAI_API_KEY) {
+ return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
+ }
+
+ if (!req.files || req.files.length === 0) {
+ return res.status(400).json({ error: 'No files uploaded' });
+ }
+
+ const { targetLang, sourceLang, outputPath } = req.body;
+
+ if (!targetLang) {
+ return res.status(400).json({ error: 'targetLang required' });
+ }
+
+ const results = [];
+ console.log(`Translating ${req.files.length} files to ${targetLang}`);
+
+ for (let i = 0; i < req.files.length; i++) {
+ const file = req.files[i];
+ console.log(`[${i + 1}/${req.files.length}] Translating: ${file.originalname}`);
+
+ try {
+ const result = await translateFile(file.path, targetLang, sourceLang || null, outputPath);
+ results.push({
+ success: true,
+ fileName: file.originalname,
+ translationPath: result.translationPath,
+ translationUrl: `/files/${path.basename(result.translationPath)}`,
+ translatedText: result.translatedText,
+ });
+ } catch (error) {
+ console.error(`Failed to translate ${file.originalname}: ${error.message}`);
+ results.push({
+ success: false,
+ fileName: file.originalname,
+ error: error.message,
+ });
+ }
+ }
+
+ res.json({
+ success: true,
+ totalFiles: req.files.length,
+ successCount: results.filter(r => r.success).length,
+ failCount: results.filter(r => !r.success).length,
+ results,
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * GET /summary-styles
+ * Get available summary styles
+ */
+app.get('/summary-styles', (req, res) => {
+ res.json({ styles: getSummaryStyles() });
+});
+
+/**
+ * POST /summarize
+ * Summarize text using GPT-5.1
+ * Body: { text: string, style?: string, language?: string, model?: string }
+ */
+app.post('/summarize', async (req, res) => {
+ try {
+ const { text, style = 'concise', language = 'same', model = 'gpt-5.1' } = req.body;
+
+ if (!text) {
+ return res.status(400).json({ error: 'text required in request body' });
+ }
+
+ if (!process.env.OPENAI_API_KEY) {
+ return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
+ }
+
+ console.log(`Summarizing text with ${model} (style: ${style})`);
+ const result = await summarizeText(text, { style, language, model });
+
+ res.json({
+ success: true,
+ ...result,
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * POST /summarize-file
+ * Summarize uploaded text files using GPT-5.1
+ * Body: { style?: string, language?: string, model?: string, outputPath?: string }
+ */
+app.post('/summarize-file', uploadText.array('files', 50), async (req, res) => {
+ try {
+ if (!process.env.OPENAI_API_KEY) {
+ return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
+ }
+
+ if (!req.files || req.files.length === 0) {
+ return res.status(400).json({ error: 'No files uploaded' });
+ }
+
+ const { style = 'concise', language = 'same', model = 'gpt-5.1', outputPath } = req.body;
+ const results = [];
+
+ console.log(`Summarizing ${req.files.length} files with ${model}`);
+
+ for (let i = 0; i < req.files.length; i++) {
+ const file = req.files[i];
+ console.log(`[${i + 1}/${req.files.length}] Summarizing: ${file.originalname}`);
+
+ try {
+ const result = await summarizeFile(file.path, { style, language, model, outputDir: outputPath });
+ results.push({
+ success: true,
+ fileName: file.originalname,
+ summaryPath: result.summaryPath,
+ summaryUrl: `/files/${path.basename(result.summaryPath)}`,
+ summary: result.summary,
+ model: result.model,
+ chunks: result.chunks,
+ });
+ } catch (error) {
+ console.error(`Failed to summarize ${file.originalname}: ${error.message}`);
+ results.push({
+ success: false,
+ fileName: file.originalname,
+ error: error.message,
+ });
+ }
+ }
+
+ res.json({
+ success: true,
+ totalFiles: req.files.length,
+ successCount: results.filter(r => r.success).length,
+ failCount: results.filter(r => !r.success).length,
+ results,
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+/**
+ * GET /summarize-stream
+ * Full pipeline: Download -> Transcribe -> Summarize with SSE progress
+ * Query: url, style?, language?, model?, outputPath?
+ */
+app.get('/summarize-stream', async (req, res) => {
+ const { url, style = 'concise', language = 'same', model = 'gpt-4o-mini-transcribe', outputPath } = req.query;
+
+ if (!url) {
+ return res.status(400).json({ error: 'URL parameter required' });
+ }
+
+ if (!process.env.OPENAI_API_KEY) {
+ return res.status(500).json({ error: 'OPENAI_API_KEY not configured' });
+ }
+
+ // Set up SSE
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+
+ const sendEvent = (event, data) => {
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
+ };
+
+ const startTime = Date.now();
+ let totalVideos = 1;
+
+ try {
+ // Phase 1: Get info
+ sendEvent('status', { message: 'Fetching video info...', phase: 'info', percent: 0 });
+ const hasPlaylist = url.includes('list=');
+ const info = await getInfo(url, hasPlaylist);
+
+ totalVideos = info._type === 'playlist' ? (info.entries?.length || 1) : 1;
+ sendEvent('info', {
+ title: info.title,
+ type: info._type || 'video',
+ totalVideos,
+ playlistTitle: info._type === 'playlist' ? info.title : null,
+ });
+
+ // Phase 2: Download (0-33%)
+ sendEvent('status', { message: 'Downloading...', phase: 'downloading', percent: 5 });
+ console.log(`[Summarize Pipeline] Downloading: ${url}`);
+
+ let videosDownloaded = 0;
+ const downloadResult = await download(url, {
+ outputDir: outputPath || OUTPUT_DIR,
+ onDownloadProgress: (progress) => {
+ const videoProgress = progress.percent || 0;
+ const overallPercent = ((videosDownloaded + (videoProgress / 100)) / totalVideos) * 33;
+
+ sendEvent('progress', {
+ percent: Math.round(overallPercent * 10) / 10,
+ phase: 'downloading',
+ phaseLabel: 'Downloading',
+ title: progress.title,
+ speed: progress.speed,
+ currentVideo: progress.videoIndex || videosDownloaded + 1,
+ totalVideos,
+ });
+ },
+ onVideoComplete: (video) => {
+ videosDownloaded++;
+ sendEvent('video-complete', {
+ title: video.title,
+ phase: 'downloading',
+ videosCompleted: videosDownloaded,
+ totalVideos,
+ });
+ },
+ });
+
+ // Phase 3: Transcribe (33-66%)
+ sendEvent('status', { message: 'Transcribing...', phase: 'transcribing', percent: 33 });
+ console.log(`[Summarize Pipeline] Transcribing ${downloadResult.successCount} files`);
+
+ const successfulDownloads = downloadResult.videos.filter(v => v.success);
+ const transcribeResults = [];
+
+ for (let i = 0; i < successfulDownloads.length; i++) {
+ const video = successfulDownloads[i];
+ const percent = 33 + ((i / successfulDownloads.length) * 33);
+
+ sendEvent('progress', {
+ percent: Math.round(percent * 10) / 10,
+ phase: 'transcribing',
+ phaseLabel: 'Transcribing',
+ title: video.title,
+ currentVideo: i + 1,
+ totalVideos: successfulDownloads.length,
+ });
+
+ try {
+ const result = await transcribeAndSave(video.filePath, {
+ responseFormat: 'text',
+ outputFormat: 'txt',
+ model,
+ outputDir: outputPath,
+ });
+ transcribeResults.push({ ...result, title: video.title, success: true });
+
+ sendEvent('transcribe-complete', {
+ title: video.title,
+ success: true,
+ videosCompleted: i + 1,
+ totalVideos: successfulDownloads.length,
+ });
+ } catch (error) {
+ transcribeResults.push({ title: video.title, success: false, error: error.message });
+ sendEvent('transcribe-complete', {
+ title: video.title,
+ success: false,
+ error: error.message,
+ });
+ }
+ }
+
+ // Phase 4: Summarize (66-100%)
+ sendEvent('status', { message: 'Summarizing with GPT-5.1...', phase: 'summarizing', percent: 66 });
+ console.log(`[Summarize Pipeline] Summarizing ${transcribeResults.filter(t => t.success).length} transcriptions`);
+
+ const summaryResults = [];
+ const successfulTranscriptions = transcribeResults.filter(t => t.success);
+
+ for (let i = 0; i < successfulTranscriptions.length; i++) {
+ const transcription = successfulTranscriptions[i];
+ const percent = 66 + ((i / successfulTranscriptions.length) * 34);
+
+ sendEvent('progress', {
+ percent: Math.round(percent * 10) / 10,
+ phase: 'summarizing',
+ phaseLabel: 'Summarizing',
+ title: transcription.title,
+ currentVideo: i + 1,
+ totalVideos: successfulTranscriptions.length,
+ });
+
+ try {
+ const result = await summarizeFile(transcription.transcriptionPath, { style, language, model: 'gpt-5.1', outputDir: outputPath });
+ summaryResults.push({
+ title: transcription.title,
+ success: true,
+ summary: result.summary,
+ summaryPath: result.summaryPath,
+ summaryUrl: `/files/${path.basename(result.summaryPath)}`,
+ transcriptionUrl: `/files/${path.basename(transcription.transcriptionPath)}`,
+ audioUrl: transcription.filePath ? `/files/${path.basename(transcription.filePath)}` : null,
+ });
+
+ sendEvent('summarize-complete', {
+ title: transcription.title,
+ success: true,
+ videosCompleted: i + 1,
+ totalVideos: successfulTranscriptions.length,
+ });
+ } catch (error) {
+ summaryResults.push({
+ title: transcription.title,
+ success: false,
+ error: error.message,
+ transcriptionUrl: `/files/${path.basename(transcription.transcriptionPath)}`,
+ });
+ sendEvent('summarize-complete', {
+ title: transcription.title,
+ success: false,
+ error: error.message,
+ });
+ }
+ }
+
+ // Final result
+ sendEvent('complete', {
+ success: true,
+ playlistTitle: downloadResult.playlistTitle,
+ totalVideos: downloadResult.totalVideos,
+ downloadedCount: downloadResult.successCount,
+ transcribedCount: transcribeResults.filter(t => t.success).length,
+ summarizedCount: summaryResults.filter(s => s.success).length,
+ totalTime: Math.round((Date.now() - startTime) / 1000),
+ results: summaryResults,
+ });
+
+ } catch (error) {
+ console.error(`[Summarize Pipeline] Error: ${error.message}`);
+ sendEvent('error', { message: error.message });
+ } finally {
+ res.end();
+ }
+});
+
+app.listen(PORT, () => {
+ console.log(`Server running on http://localhost:${PORT}`);
+ console.log('\nEndpoints:');
+ console.log(' GET /health - Health check');
+ console.log(' GET /info?url= - Get video/playlist info');
+ console.log(' POST /download - Download as MP3');
+ console.log(' POST /transcribe - Transcribe audio file');
+ console.log(' POST /process - Download + transcribe');
+ console.log(' POST /summarize - Summarize text (GPT-5.1)');
+ console.log(' POST /summarize-file - Summarize files (GPT-5.1)');
+ console.log(' GET /summarize-stream - Full pipeline: Download + Transcribe + Summarize');
+ console.log(' GET /files-list - List downloaded files');
+ console.log(' GET /files/ - Serve downloaded files');
+});
diff --git a/src/services/conversion.js b/src/services/conversion.js
index 9b5b23b..2fe0ff7 100644
--- a/src/services/conversion.js
+++ b/src/services/conversion.js
@@ -1,145 +1,145 @@
-import { exec } from 'child_process';
-import { promisify } from 'util';
-import path from 'path';
-import fs from 'fs';
-
-const execPromise = promisify(exec);
-
-/**
- * Convert a video/audio file to MP3 using FFmpeg
- * @param {string} inputPath - Path to input file
- * @param {object} options - Conversion options
- * @param {string} options.outputDir - Output directory (default: same as input)
- * @param {string} options.bitrate - Audio bitrate (default: 192k)
- * @param {string} options.quality - Audio quality 0-9 (default: 2, where 0 is best)
- * @returns {Promise