From 846693b097cfe6b02872ea90e21b4463d3f8eb18 Mon Sep 17 00:00:00 2001 From: DJP Date: Tue, 21 Oct 2025 11:54:39 -0400 Subject: [PATCH] Initial commit: Voice to Text with Whisper & DeepL Translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - OpenAI Whisper for audio transcription - DeepL API for translation (30+ languages) - Multiple output formats: TXT, VTT, SRT - Flask Python API backend - PHP frontend with black/gold theme - Support for 350MB files - Generates both original and translated files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 30 ++++ .htaccess | 5 + README.md | 208 +++++++++++++++++++++++++ V2T.svg | 49 ++++++ api.py | 209 +++++++++++++++++++++++++ config.php | 13 ++ download.php | 69 +++++++++ index.php | 160 +++++++++++++++++++ process.php | 86 +++++++++++ requirements.txt | 6 + setup.sh | 84 ++++++++++ start_api.sh | 9 ++ style.css | 386 ++++++++++++++++++++++++++++++++++++++++++++++ test_download.php | 41 +++++ 14 files changed, 1355 insertions(+) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 README.md create mode 100644 V2T.svg create mode 100644 api.py create mode 100644 config.php create mode 100644 download.php create mode 100644 index.php create mode 100644 process.php create mode 100644 requirements.txt create mode 100755 setup.sh create mode 100755 start_api.sh create mode 100755 style.css create mode 100644 test_download.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14b5e4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Python +venv/ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Output files +outputs/*.txt +outputs/*.vtt +outputs/*.srt +outputs/.DS_Store + +# Logs +*.log + +# OS Files +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Temporary files +temp_audio.wav +*.tmp diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..c2ffc94 --- /dev/null +++ b/.htaccess @@ -0,0 +1,5 @@ +php_value upload_max_filesize 350M +php_value post_max_size 350M +php_value max_execution_time 1200 +php_value max_input_time 1200 +php_value memory_limit 512M diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff94aba --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# Voice to Text with Whisper & DeepL Translation + +A web application that converts audio files to text using OpenAI's Whisper model and translates them using DeepL API. Supports multiple output formats: plain text, VTT (WebVTT), and SRT (SubRip). + +## Features + +- 🎤 Audio transcription using OpenAI Whisper +- 🌍 Translation using DeepL API (30+ languages) +- 📝 Multiple output formats: Text, VTT, SRT +- 🚀 Python Flask API backend +- 💻 PHP frontend (MAMP/Apache compatible) +- 📦 350MB file size limit +- 📄 Generates both original and translated files + +## Requirements + +- Python 3.8 or higher +- PHP 7.4 or higher +- MAMP or Apache server +- FFmpeg (for audio processing) + +## Installation + +### 1. Install FFmpeg + +**macOS:** +```bash +brew install ffmpeg +``` + +**Linux (Ubuntu/Debian):** +```bash +sudo apt update +sudo apt install ffmpeg +``` + +**Windows:** +Download from https://ffmpeg.org/download.html + +### 2. Setup Python Environment + +Run the setup script: +```bash +chmod +x setup.sh +./setup.sh +``` + +This will: +- Create a Python virtual environment +- Install all dependencies (Flask, Whisper, etc.) +- Create the outputs directory + +### 3. Start the API Server + +```bash +chmod +x start_api.sh +./start_api.sh +``` + +Or manually: +```bash +source venv/bin/activate +python api.py +``` + +The API will run on http://localhost:5010 + +### 4. Configure Web Server + +Ensure your MAMP/Apache server points to this directory and PHP is enabled. + +## Usage + +1. Start the Python API server (see step 3 above) +2. Open the web application in your browser +3. Select output format (Text/VTT/SRT) +4. (Optional) Enable translation and select target language +5. Upload an audio file (max 350MB) +6. Wait for processing +7. Download original and/or translated transcription + +### Translation + +The app uses DeepL API for high-quality translations. When translation is enabled: +- The audio is first transcribed in its original language +- The transcription is then translated to your selected target language +- Both original and translated files are generated +- Supports 30+ languages including English, Spanish, French, German, Portuguese, Japanese, Chinese, and more + +**Note:** PHP settings are configured via `.htaccess` for 350MB uploads. If you need larger files, adjust `php.ini`: +``` +upload_max_filesize = 350M +post_max_size = 350M +max_execution_time = 1200 +``` + +## API Endpoints + +### POST /transcribe +Transcribe audio file to text/VTT/SRT + +**Parameters:** +- `audio` (file): Audio file to transcribe +- `format` (string): Output format (txt/vtt/srt) + +**Response:** +```json +{ + "success": true, + "text": "transcribed text...", + "filename": "output.txt", + "format": "txt" +} +``` + +### GET /health +Health check endpoint + +### GET /download/ +Download transcribed file + +## Whisper Models + +The default model is `base` which provides a good balance of speed and accuracy. + +Available models: +- `tiny` - Fastest, least accurate +- `base` - Good balance (default) +- `small` - Better accuracy, slower +- `medium` - High accuracy, much slower +- `large` - Best accuracy, very slow + +To change the model, edit `api.py` line 24: +```python +model = whisper.load_model("base") # Change to desired model +``` + +## File Structure + +``` +. +├── api.py # Python Flask API +├── index.php # Frontend interface +├── process.php # PHP request handler +├── download.php # File download handler +├── config.php # Configuration +├── style.css # Styles +├── requirements.txt # Python dependencies +├── setup.sh # Setup script +├── start_api.sh # API start script +├── outputs/ # Transcribed files directory +└── venv/ # Python virtual environment +``` + +## Production Deployment + +For Apache deployment: + +1. Ensure mod_php is enabled +2. Point document root to this directory +3. Run the API as a systemd service (see below) + +### Systemd Service (Linux) + +Create `/etc/systemd/system/whisper-api.service`: + +```ini +[Unit] +Description=Whisper API Service +After=network.target + +[Service] +Type=simple +User=www-data +WorkingDirectory=/path/to/your/app +ExecStart=/path/to/your/app/venv/bin/python /path/to/your/app/api.py +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: +```bash +sudo systemctl enable whisper-api +sudo systemctl start whisper-api +``` + +## Troubleshooting + +**API not connecting:** +- Verify Python API is running on port 5010 +- Check `config.php` has correct API URL +- Ensure firewall allows port 5010 + +**Transcription fails:** +- Verify FFmpeg is installed: `ffmpeg -version` +- Check audio file format is supported +- Review API logs for errors + +**Out of memory:** +- Use a smaller Whisper model (tiny or base) +- Reduce audio file size +- Increase system memory + +## License + +MIT diff --git a/V2T.svg b/V2T.svg new file mode 100644 index 0000000..a57ac31 --- /dev/null +++ b/V2T.svg @@ -0,0 +1,49 @@ + + + + +T + +E + +X + +T + + 2 + + V + +O + +I + +C + +E + + + diff --git a/api.py b/api.py new file mode 100644 index 0000000..6990707 --- /dev/null +++ b/api.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +""" +Voice to Text API using OpenAI Whisper with DeepL Translation +Transcribes audio files to text, VTT, or SRT format and optionally translates them +""" + +from flask import Flask, request, jsonify, send_file +from flask_cors import CORS +import whisper +import deepl +import os +import tempfile +from datetime import timedelta +import logging + +app = Flask(__name__) +CORS(app) + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Load Whisper model (using base model for balance of speed and accuracy) +# Options: tiny, base, small, medium, large +logger.info("Loading Whisper model...") +model = whisper.load_model("base") +logger.info("Whisper model loaded successfully") + +# Initialize DeepL translator +DEEPL_API_KEY = "28743b40-d23f-416d-8223-9b868c9531dc" +translator = deepl.Translator(DEEPL_API_KEY) +logger.info("DeepL translator initialized") + +# Directory for output files +OUTPUT_DIR = os.path.join(os.path.dirname(__file__), 'outputs') +os.makedirs(OUTPUT_DIR, exist_ok=True) + + +def format_timestamp(seconds): + """Convert seconds to SRT timestamp format (HH:MM:SS,mmm)""" + td = timedelta(seconds=seconds) + hours = td.seconds // 3600 + minutes = (td.seconds % 3600) // 60 + secs = td.seconds % 60 + millis = td.microseconds // 1000 + return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}" + + +def format_timestamp_vtt(seconds): + """Convert seconds to VTT timestamp format (HH:MM:SS.mmm)""" + td = timedelta(seconds=seconds) + hours = td.seconds // 3600 + minutes = (td.seconds % 3600) // 60 + secs = td.seconds % 60 + millis = td.microseconds // 1000 + return f"{hours:02d}:{minutes:02d}:{secs:02d}.{millis:03d}" + + +def generate_srt(segments): + """Generate SRT format from Whisper segments""" + srt_content = [] + for i, segment in enumerate(segments, 1): + start = format_timestamp(segment['start']) + end = format_timestamp(segment['end']) + text = segment['text'].strip() + srt_content.append(f"{i}\n{start} --> {end}\n{text}\n") + return "\n".join(srt_content) + + +def generate_vtt(segments): + """Generate VTT format from Whisper segments""" + vtt_content = ["WEBVTT\n"] + for segment in segments: + start = format_timestamp_vtt(segment['start']) + end = format_timestamp_vtt(segment['end']) + text = segment['text'].strip() + vtt_content.append(f"{start} --> {end}\n{text}\n") + return "\n".join(vtt_content) + + +def translate_text(text, target_lang): + """Translate text using DeepL API""" + try: + logger.info(f"Translating text to {target_lang}...") + result = translator.translate_text(text, target_lang=target_lang) + return result.text + except deepl.exceptions.DeepLException as e: + logger.error(f"DeepL translation error: {str(e)}") + raise Exception(f"Translation failed: {str(e)}") + + +@app.route('/health', methods=['GET']) +def health_check(): + """Health check endpoint""" + return jsonify({"status": "healthy", "model": "whisper-base"}) + + +@app.route('/transcribe', methods=['POST']) +def transcribe(): + """ + Transcribe audio file to text, VTT, or SRT format with optional translation + Expects: multipart/form-data with 'audio' file, 'format' (txt/vtt/srt), + 'translate' (0/1), and 'target_lang' (e.g., 'EN-US') + """ + try: + # Check if audio file is present + if 'audio' not in request.files: + return jsonify({"error": "No audio file provided"}), 400 + + audio_file = request.files['audio'] + output_format = request.form.get('format', 'txt').lower() + enable_translation = request.form.get('translate', '0') == '1' + target_lang = request.form.get('target_lang', 'EN-US') + + if audio_file.filename == '': + return jsonify({"error": "Empty filename"}), 400 + + # Validate format + if output_format not in ['txt', 'vtt', 'srt']: + return jsonify({"error": "Invalid format. Use txt, vtt, or srt"}), 400 + + logger.info(f"Processing {audio_file.filename} - Format: {output_format}, Translation: {enable_translation}, Target: {target_lang}") + + # Save uploaded file temporarily + with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(audio_file.filename)[1]) as temp_audio: + audio_file.save(temp_audio.name) + temp_audio_path = temp_audio.name + + try: + # Transcribe with Whisper + logger.info(f"Transcribing {audio_file.filename}...") + result = model.transcribe(temp_audio_path, verbose=False) + logger.info("Transcription complete") + + # Generate output based on format + if output_format == 'txt': + content = result['text'] + mimetype = 'text/plain' + extension = 'txt' + elif output_format == 'vtt': + content = generate_vtt(result['segments']) + mimetype = 'text/vtt' + extension = 'vtt' + elif output_format == 'srt': + content = generate_srt(result['segments']) + mimetype = 'text/plain' + extension = 'srt' + + # Save original output file + base_filename = os.path.splitext(audio_file.filename)[0] + output_filename = f"{base_filename}_original.{extension}" + output_path = os.path.join(OUTPUT_DIR, output_filename) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write(content) + + response_data = { + "success": True, + "text": result['text'] if output_format == 'txt' else None, + "filename": output_filename, + "format": output_format + } + + # Handle translation if requested + if enable_translation: + logger.info(f"Translating to {target_lang}...") + translated_content = translate_text(content, target_lang) + + # Save translated output file + translated_filename = f"{base_filename}_translated.{extension}" + translated_path = os.path.join(OUTPUT_DIR, translated_filename) + + with open(translated_path, 'w', encoding='utf-8') as f: + f.write(translated_content) + + response_data["translated_filename"] = translated_filename + response_data["translated_text"] = translated_content if output_format == 'txt' else None + logger.info("Translation complete") + + return jsonify(response_data) + + finally: + # Clean up temporary audio file + if os.path.exists(temp_audio_path): + os.remove(temp_audio_path) + + except Exception as e: + logger.error(f"Error during transcription: {str(e)}") + return jsonify({"error": f"Transcription failed: {str(e)}"}), 500 + + +@app.route('/download/', methods=['GET']) +def download_file(filename): + """Download a transcribed file""" + try: + file_path = os.path.join(OUTPUT_DIR, filename) + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + return send_file(file_path, as_attachment=True) + except Exception as e: + logger.error(f"Error downloading file: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +if __name__ == '__main__': + # Run on port 5010 by default + port = int(os.environ.get('PORT', 5010)) + app.run(host='0.0.0.0', port=port, debug=False) diff --git a/config.php b/config.php new file mode 100644 index 0000000..eabdee6 --- /dev/null +++ b/config.php @@ -0,0 +1,13 @@ + 'text/plain; charset=utf-8', + 'vtt' => 'text/vtt; charset=utf-8', + 'srt' => 'text/plain; charset=utf-8' // Changed to text/plain for better compatibility +]; + +$contentType = $contentTypes[$extension] ?? 'application/octet-stream'; + +// Clear all output buffers +while (ob_get_level()) { + ob_end_clean(); +} + +// Prevent any caching +header('Content-Description: File Transfer'); +header('Content-Type: ' . $contentType); +header('Content-Disposition: attachment; filename="' . basename($filename) . '"'); +header('Content-Transfer-Encoding: binary'); +header('Content-Length: ' . filesize($filepath)); +header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); +header('Pragma: public'); +header('Expires: 0'); + +// Flush system output buffer +flush(); + +// Output file +readfile($filepath); +exit; diff --git a/index.php b/index.php new file mode 100644 index 0000000..7e61aa5 --- /dev/null +++ b/index.php @@ -0,0 +1,160 @@ + + + + + + + Voice to Text + + + + + +
+ + +
+ Before we start, select output format and upload the Voice File (Max 350 Megabytes in size) +
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + \ No newline at end of file diff --git a/process.php b/process.php new file mode 100644 index 0000000..08d0738 --- /dev/null +++ b/process.php @@ -0,0 +1,86 @@ + 350 * 1024 * 1024) { // 350 MB limit + echo json_encode(['success' => false, 'error' => "File is too large. Maximum size is 350 MB."]); + exit; + } + + // Prepare the file for sending to Python API + $formData = [ + 'audio' => new CURLFile($file['tmp_name'], $file['type'], $file['name']), + 'format' => $outputFormat, + 'translate' => $enableTranslation, + 'target_lang' => $targetLanguage + ]; + + // Send request to Python API + $ch = curl_init(PYTHON_API_URL . '/transcribe'); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $formData); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 5 minutes timeout for large files + + $response = curl_exec($ch); + + if (curl_errno($ch)) { + echo json_encode(['success' => false, 'error' => "Error processing file: " . curl_error($ch)]); + } else { + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ($httpCode === 200) { + $data = json_decode($response, true); + + if (isset($data['success']) && $data['success']) { + // For text format, return the text directly + if ($outputFormat === 'txt' && isset($data['text'])) { + $response = [ + 'success' => true, + 'response' => nl2br(htmlspecialchars($data['text'])), + 'format' => $outputFormat + ]; + + // Add translated text if available + if (isset($data['translated_text'])) { + $response['translatedResponse'] = nl2br(htmlspecialchars($data['translated_text'])); + } + + echo json_encode($response); + } else { + // For VTT/SRT, provide download links + $downloadUrl = 'download.php?file=' . urlencode($data['filename']); + $response = [ + 'success' => true, + 'fileUrl' => $downloadUrl, + 'filename' => $data['filename'], + 'format' => $outputFormat + ]; + + // Add translated file download link if available + if (isset($data['translated_filename'])) { + $response['translatedFileUrl'] = 'download.php?file=' . urlencode($data['translated_filename']); + $response['translatedFilename'] = $data['translated_filename']; + } + + echo json_encode($response); + } + } else { + echo json_encode(['success' => false, 'error' => $data['error'] ?? "Unknown error occurred"]); + } + } else { + echo json_encode(['success' => false, 'error' => "Server error: HTTP $httpCode"]); + } + } + + curl_close($ch); +} else { + echo json_encode(['success' => false, 'error' => "Invalid request."]); +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b40c0bc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flask>=3.0.0 +flask-cors>=4.0.0 +openai-whisper +numpy<2.0.0 +ffmpeg-python +deepl diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..fb7a5c5 --- /dev/null +++ b/setup.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Setup script for Voice to Text Whisper API + +echo "===================================" +echo "Voice to Text - Setup Script" +echo "===================================" +echo "" + +# Check if Python 3 is installed +if ! command -v python3 &> /dev/null; then + echo "Error: Python 3 is not installed. Please install Python 3.8 or higher." + exit 1 +fi + +PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))') +echo "Python 3 found: Python $PYTHON_VERSION" + +# Check if Python version is too new (3.12+) +PYTHON_MAJOR=$(python3 -c 'import sys; print(sys.version_info[0])') +PYTHON_MINOR=$(python3 -c 'import sys; print(sys.version_info[1])') + +if [ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -ge 12 ]; then + echo "" + echo "WARNING: Python 3.12+ detected. Some packages may have compatibility issues." + echo "Recommended: Use Python 3.10 or 3.11 for best compatibility." + echo "" + read -p "Continue anyway? (y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +echo "" + +# Remove old venv if exists +if [ -d "venv" ]; then + echo "Removing old virtual environment..." + rm -rf venv +fi + +# Determine which Python to use +if command -v python3.11 &> /dev/null; then + PYTHON_CMD=python3.11 + echo "Using Python 3.11" +elif command -v python3.10 &> /dev/null; then + PYTHON_CMD=python3.10 + echo "Using Python 3.10" +else + PYTHON_CMD=python3 + echo "Using default Python 3" +fi + +# Create virtual environment +echo "Creating virtual environment..." +$PYTHON_CMD -m venv venv + +# Activate virtual environment +echo "Activating virtual environment..." +source venv/bin/activate + +# Upgrade pip +echo "Upgrading pip..." +pip install --upgrade pip + +# Install dependencies +echo "Installing dependencies (this may take a few minutes)..." +pip install -r requirements.txt + +# Create outputs directory +echo "Creating outputs directory..." +mkdir -p outputs + +echo "" +echo "===================================" +echo "Setup complete!" +echo "===================================" +echo "" +echo "To start the API server:" +echo " 1. Activate the virtual environment: source venv/bin/activate" +echo " 2. Run the API: python api.py" +echo "" +echo "The API will run on http://localhost:5010" +echo "" diff --git a/start_api.sh b/start_api.sh new file mode 100755 index 0000000..5f0acb6 --- /dev/null +++ b/start_api.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Start the Whisper API server + +# Activate virtual environment +source venv/bin/activate + +# Start the API +echo "Starting Whisper API server on http://localhost:5010..." +python api.py diff --git a/style.css b/style.css new file mode 100755 index 0000000..894b5df --- /dev/null +++ b/style.css @@ -0,0 +1,386 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Montserrat', sans-serif; + background: #000000; + min-height: 100vh; + padding: 20px; + display: flex; + justify-content: center; + align-items: center; +} + +input, button, textarea, select, label { + font-family: 'Montserrat', sans-serif; +} + +.app-container { + background: #1a1a1a; + border-radius: 20px; + box-shadow: 0 20px 60px rgba(255, 196, 7, 0.2); + border: 1px solid #333; + padding: 40px; + max-width: 800px; + width: 100%; + animation: fadeIn 0.5s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.logo { + width: 400px; + height: auto; + display: block; + margin: 0 auto 30px; + filter: invert(1) brightness(2); +} + +.initial-instruction { + text-align: center; + font-size: 16px; + color: #999; + margin-bottom: 30px; + font-weight: 400; + line-height: 1.6; +} + +.format-selection { + background: #0a0a0a; + padding: 20px; + border-radius: 12px; + margin-bottom: 25px; + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + border: 1px solid #333; +} + +.format-selection label { + font-weight: 600; + color: #FFC407; + font-size: 15px; +} + +.format-selection select { + padding: 10px 20px; + border: 2px solid #333; + border-radius: 8px; + font-size: 15px; + font-weight: 500; + color: #FFC407; + background: #000; + cursor: pointer; + transition: all 0.3s ease; + min-width: 200px; +} + +.format-selection select:hover { + border-color: #FFC407; +} + +.format-selection select:focus { + outline: none; + border-color: #FFC407; + box-shadow: 0 0 0 3px rgba(255, 196, 7, 0.2); +} + +.translation-section { + background: #0a0a0a; + padding: 20px; + border-radius: 12px; + margin-bottom: 25px; + border: 1px solid #333; +} + +.translation-toggle { + margin-bottom: 15px; +} + +.toggle-label { + display: flex; + align-items: center; + cursor: pointer; + user-select: none; +} + +.toggle-label input[type="checkbox"] { + width: 20px; + height: 20px; + cursor: pointer; + margin-right: 10px; + accent-color: #FFC407; +} + +.toggle-text { + color: #FFC407; + font-weight: 600; + font-size: 15px; +} + +.language-selector { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + padding-top: 15px; + border-top: 1px solid #333; +} + +.language-selector label { + font-weight: 600; + color: #999; + font-size: 14px; +} + +.language-selector select { + padding: 10px 20px; + border: 2px solid #333; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + color: #FFC407; + background: #000; + cursor: pointer; + transition: all 0.3s ease; + min-width: 220px; +} + +.language-selector select:hover { + border-color: #FFC407; +} + +.language-selector select:focus { + outline: none; + border-color: #FFC407; + box-shadow: 0 0 0 3px rgba(255, 196, 7, 0.2); +} + +.file-upload-container { + text-align: center; + margin-bottom: 25px; +} + +.file-upload-label { + display: inline-block; + padding: 15px 40px; + cursor: pointer; + background: #FFC407; + color: #000; + border-radius: 50px; + font-weight: 700; + font-size: 16px; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(255, 196, 7, 0.4); +} + +.file-upload-label:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(255, 196, 7, 0.6); + background: #ffcd2e; +} + +.file-upload-label:active { + transform: translateY(0); +} + +.file-upload-label.disabled { + background: #333; + color: #666; + cursor: not-allowed; + box-shadow: none; +} + +.chat-area { + min-height: 200px; + max-height: 400px; + border: 2px solid #333; + border-radius: 12px; + overflow-y: auto; + padding: 20px; + background: #0a0a0a; + margin-bottom: 20px; +} + +.chat-area:empty { + display: none; +} + +.message { + padding: 15px 20px; + margin-bottom: 15px; + border-radius: 12px; + line-height: 1.6; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.bot-message { + background: rgba(255, 196, 7, 0.1); + border-left: 4px solid #FFC407; + color: #fff; +} + +.bot-message a { + color: #FFC407; + font-weight: 600; + text-decoration: none; + border-bottom: 2px solid #FFC407; + transition: all 0.2s ease; +} + +.bot-message a:hover { + color: #ffcd2e; + border-bottom-color: #ffcd2e; +} + +.error-message { + background: rgba(255, 0, 0, 0.1); + border-left: 4px solid #ff3333; + color: #ff6666; +} + +.processing-container { + padding: 30px; + text-align: center; +} + +.processing-text { + color: #FFC407; + font-weight: 600; + font-size: 18px; + margin-bottom: 20px; + animation: pulseAnimation 2s infinite; +} + +@keyframes pulseAnimation { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +.progress-bar { + width: 100%; + height: 8px; + background: #333; + border-radius: 10px; + overflow: hidden; + position: relative; +} + +.progress-bar-fill { + height: 100%; + background: linear-gradient(90deg, #FFC407, #ffcd2e, #FFC407); + background-size: 200% 100%; + border-radius: 10px; + animation: progressAnimation 1.5s ease-in-out infinite; + box-shadow: 0 0 10px rgba(255, 196, 7, 0.5); +} + +@keyframes progressAnimation { + 0% { + width: 0%; + background-position: 0% 0%; + } + 50% { + width: 70%; + background-position: 100% 0%; + } + 100% { + width: 100%; + background-position: 200% 0%; + } +} + +button { + padding: 12px 30px; + cursor: pointer; + background: #FFC407; + color: #000; + border: none; + border-radius: 50px; + font-weight: 700; + font-size: 15px; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(255, 196, 7, 0.4); +} + +button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(255, 196, 7, 0.6); + background: #ffcd2e; +} + +button:active { + transform: translateY(0); +} + +#downloadButton { + display: none; + margin: 0 auto; +} + +/* Responsive design */ +@media screen and (max-width: 768px) { + .app-container { + padding: 25px; + } + + .logo { + width: 300px; + } + + .initial-instruction { + font-size: 14px; + } + + .format-selection { + flex-direction: column; + gap: 10px; + } + + .format-selection select { + width: 100%; + } + + .file-upload-label { + padding: 12px 30px; + font-size: 14px; + } +} + +@media screen and (max-width: 480px) { + body { + padding: 10px; + } + + .app-container { + padding: 20px; + } + + .logo { + width: 250px; + } +} \ No newline at end of file diff --git a/test_download.php b/test_download.php new file mode 100644 index 0000000..7377142 --- /dev/null +++ b/test_download.php @@ -0,0 +1,41 @@ +Files in outputs directory:"; +echo "
    "; + +if (is_dir($outputDir)) { + $files = scandir($outputDir); + foreach ($files as $file) { + if ($file !== '.' && $file !== '..' && $file !== '.DS_Store') { + $filepath = $outputDir . $file; + $size = filesize($filepath); + $readable = is_readable($filepath) ? 'Yes' : 'No'; + $extension = pathinfo($file, PATHINFO_EXTENSION); + + echo "
  • "; + echo "$file
    "; + echo "Size: " . number_format($size) . " bytes
    "; + echo "Readable: $readable
    "; + echo "Extension: $extension
    "; + echo "Test Download"; + echo "

  • "; + } + } +} else { + echo "
  • Directory not found
  • "; +} + +echo "
"; + +// Test file operations +echo "

Directory permissions:

"; +echo "Directory: $outputDir
"; +echo "Exists: " . (is_dir($outputDir) ? 'Yes' : 'No') . "
"; +echo "Readable: " . (is_readable($outputDir) ? 'Yes' : 'No') . "
"; +echo "Writable: " . (is_writable($outputDir) ? 'Yes' : 'No') . "
"; +?>