diff --git a/.railwayignore b/.railwayignore
new file mode 100644
index 0000000..d2bc1b0
--- /dev/null
+++ b/.railwayignore
@@ -0,0 +1,28 @@
+# Railway ignore file - prevent large files from being included in build
+node_modules/.bin
+node_modules/.cache
+node_modules/.vite
+.next
+.nuxt
+dist
+build
+*.log
+.DS_Store
+.env.local
+.venv
+venv
+__pycache__
+*.pyc
+.pytest_cache
+.coverage
+.git
+.github
+notebooks
+tests
+data
+*.png
+*.svg
+.flake8
+.pre-commit-config.yaml
+CHANGELOG.md
+model-card.md
diff --git a/QUICKSTART.md b/QUICKSTART.md
new file mode 100644
index 0000000..4f7be9e
--- /dev/null
+++ b/QUICKSTART.md
@@ -0,0 +1,305 @@
+# Farsi Transcriber - Quick Start Guide
+
+You now have **TWO** complete applications for Farsi transcription:
+
+## 🖥️ Option 1: Desktop App (PyQt6)
+
+**Location:** `/home/user/whisper/farsi_transcriber/`
+
+### Setup
+```bash
+cd farsi_transcriber
+python3 -m venv venv
+source venv/bin/activate
+pip install -r requirements.txt
+python main.py
+```
+
+**Features:**
+- ✅ Standalone desktop application
+- ✅ Works completely offline
+- ✅ Direct access to file system
+- ✅ Lightweight and fast
+- ⚠️ Simpler UI (green theme)
+
+**Good for:**
+- Local-only transcription
+- Users who prefer desktop apps
+- Offline processing
+
+---
+
+## 🌐 Option 2: Web App (React + Flask)
+
+**Location:** `/home/user/whisper/farsi_transcriber_web/`
+
+### Setup
+
+**Backend (Flask):**
+```bash
+cd farsi_transcriber_web/backend
+python3 -m venv venv
+source venv/bin/activate
+pip install -r requirements.txt
+python app.py
+# API runs on http://localhost:5000
+```
+
+**Frontend (React):**
+```bash
+cd farsi_transcriber_web
+npm install
+npm run dev
+# App runs on http://localhost:3000
+```
+
+**Features:**
+- ✅ Modern web-based UI (matches your Figma design exactly)
+- ✅ File queue management
+- ✅ Dark/Light theme toggle
+- ✅ Search with text highlighting
+- ✅ Copy segments to clipboard
+- ✅ Resizable window
+- ✅ RTL support for Farsi
+- ✅ Multiple export formats
+- ✅ Professional styling
+
+**Good for:**
+- Modern web experience
+- Team collaboration (can be deployed online)
+- More features and polish
+- Professional appearance
+
+---
+
+## 📊 Comparison
+
+| Feature | Desktop (PyQt6) | Web (React) |
+|---------|-----------------|------------|
+| **Interface** | Simple, green | Modern, professional |
+| **Dark Mode** | ❌ | ✅ |
+| **File Queue** | ❌ | ✅ |
+| **Search** | ❌ | ✅ |
+| **Copy Segments** | ❌ | ✅ |
+| **Resizable Window** | ❌ | ✅ |
+| **Export Formats** | SRT, TXT, VTT, JSON, TSV | TXT, SRT, VTT, JSON |
+| **Offline** | ✅ | Requires backend |
+| **Easy Setup** | ✅✅ | ✅ (2 terminals) |
+| **Deployment** | Desktop only | Can host online |
+| **Code Size** | ~25KB | ~200KB |
+
+---
+
+## 🚀 Which Should You Use?
+
+### Use **Desktop App** if:
+- You want simple, quick setup
+- You never share transcriptions
+- You prefer offline processing
+- You don't need advanced features
+
+### Use **Web App** if:
+- You like modern interfaces
+- You want dark/light themes
+- You need file queue management
+- You want to potentially share online
+- You want professional appearance
+
+---
+
+## 📁 Project Structure
+
+```
+whisper/
+├── farsi_transcriber/ (Desktop PyQt6 App)
+│ ├── ui/
+│ ├── models/
+│ ├── utils/
+│ ├── config.py
+│ ├── main.py
+│ └── requirements.txt
+│
+└── farsi_transcriber_web/ (Web React App)
+ ├── src/
+ │ ├── App.tsx
+ │ ├── components/
+ │ └── main.tsx
+ ├── backend/
+ │ ├── app.py
+ │ └── requirements.txt
+ ├── package.json
+ └── vite.config.ts
+```
+
+---
+
+## 🔧 System Requirements
+
+### Desktop App
+- Python 3.8+
+- ffmpeg
+- 4GB RAM
+
+### Web App
+- Python 3.8+ (backend)
+- Node.js 16+ (frontend)
+- ffmpeg
+- 4GB RAM
+
+---
+
+## 📝 Setup Checklist
+
+### Initial Setup (One-time)
+
+- [ ] Install ffmpeg
+ ```bash
+ # Ubuntu/Debian
+ sudo apt install ffmpeg
+
+ # macOS
+ brew install ffmpeg
+
+ # Windows
+ choco install ffmpeg
+ ```
+
+- [ ] Verify Python 3.8+
+ ```bash
+ python3 --version
+ ```
+
+- [ ] Verify Node.js 16+ (for web app only)
+ ```bash
+ node --version
+ ```
+
+### Desktop App Setup
+
+- [ ] Create virtual environment
+- [ ] Install requirements
+- [ ] Run app
+
+### Web App Setup
+
+**Backend:**
+- [ ] Create virtual environment
+- [ ] Install requirements
+- [ ] Run Flask server
+
+**Frontend:**
+- [ ] Install Node dependencies
+- [ ] Run dev server
+
+---
+
+## 🎯 Quick Start (Fastest)
+
+### Desktop (30 seconds)
+```bash
+cd whisper/farsi_transcriber
+python3 -m venv venv && source venv/bin/activate
+pip install -r requirements.txt && python main.py
+```
+
+### Web (2 minutes)
+Terminal 1:
+```bash
+cd whisper/farsi_transcriber_web/backend
+python3 -m venv venv && source venv/bin/activate
+pip install -r requirements.txt && python app.py
+```
+
+Terminal 2:
+```bash
+cd whisper/farsi_transcriber_web
+npm install && npm run dev
+```
+
+---
+
+## 🐛 Troubleshooting
+
+### "ffmpeg not found"
+Install ffmpeg (see requirements above)
+
+### "ModuleNotFoundError" (Python)
+```bash
+# Ensure virtual environment is activated
+source venv/bin/activate # Linux/Mac
+# or
+venv\Scripts\activate # Windows
+```
+
+### "npm: command not found"
+Install Node.js from https://nodejs.org
+
+### App runs slow
+- Use GPU: Install CUDA
+- Reduce model size: change to 'small' or 'tiny'
+- Close other applications
+
+---
+
+## 📚 Full Documentation
+
+- **Desktop App:** `farsi_transcriber/README.md`
+- **Web App:** `farsi_transcriber_web/README.md`
+- **API Docs:** `farsi_transcriber_web/README.md` (Endpoints section)
+
+---
+
+## 🎓 What Was Built
+
+### Desktop Application (PyQt6)
+✅ File picker for audio/video
+✅ Whisper integration with word-level timestamps
+✅ 5 export formats (TXT, SRT, VTT, JSON, TSV)
+✅ Professional styling
+✅ Progress indicators
+✅ Threading to prevent UI freezing
+
+### Web Application (React + Flask)
+✅ Complete Figma design implementation
+✅ File queue management
+✅ Dark/light theme
+✅ Search with highlighting
+✅ Segment management
+✅ Resizable window
+✅ RTL support
+✅ Flask backend with Whisper integration
+✅ 4 export formats
+✅ Real file upload handling
+
+---
+
+## 🚀 Next Steps
+
+1. **Choose your app** (Desktop or Web)
+2. **Install ffmpeg** if not already installed
+3. **Follow the setup instructions** above
+4. **Test with a Farsi audio file**
+5. **Export in your preferred format**
+
+---
+
+## 💡 Tips
+
+- **First transcription is slow** (downloads 769MB model)
+- **Use larger models** (medium/large) for better accuracy
+- **Use smaller models** (tiny/base) for speed
+- **GPU significantly speeds up** transcription
+- **Both apps work offline** (after initial model download)
+
+---
+
+## 📧 Need Help?
+
+- Check the full README in each app's directory
+- Verify all requirements are installed
+- Check browser console (web app) or Python output (desktop)
+- Ensure ffmpeg is in your PATH
+
+---
+
+**Enjoy your Farsi transcription apps!** 🎉
diff --git a/RAILWAY_QUICKSTART.md b/RAILWAY_QUICKSTART.md
new file mode 100644
index 0000000..a24e01c
--- /dev/null
+++ b/RAILWAY_QUICKSTART.md
@@ -0,0 +1,114 @@
+# Railway Deployment - Quick Start (5 Minutes)
+
+Deploy your Farsi Transcriber to Railway in just 5 minutes! 🚀
+
+---
+
+## **What You'll Get**
+
+✅ Your app live online
+✅ Free $5/month credit
+✅ 24/7 uptime
+✅ Automatic scaling
+✅ No credit card needed (free tier)
+
+---
+
+## **Step 1: Create Railway Account** (2 min)
+
+1. Go to **https://railway.app**
+2. Click **"Login with GitHub"**
+3. Authorize with your GitHub account
+4. Done! You get $5 free credit ✅
+
+---
+
+## **Step 2: Create Backend Service** (2 min)
+
+1. Click **"Create New Project"**
+2. Select **"GitHub Repo"**
+3. Find your **whisper** fork
+4. Railway auto-detects Python project
+5. **Root Directory:** `farsi_transcriber_web/backend`
+6. Click **Deploy**
+7. **Wait 2-3 minutes** for deployment
+8. **Copy the URL** that appears (e.g., `https://farsi-api-xxx.railway.app`)
+
+---
+
+## **Step 3: Create Frontend Service** (1 min)
+
+1. In Railway project, click **"New Service"** → **"GitHub Repo"**
+2. Select **whisper** again
+3. **Root Directory:** `farsi_transcriber_web`
+4. Click **Deploy**
+5. **Wait 3-5 minutes** for build and deployment
+
+---
+
+## **Step 4: Connect Frontend to Backend** (Bonus step - 1 min)
+
+1. In Railway, select **frontend** service
+2. Go to **Variables**
+3. Edit `VITE_API_URL` and paste your backend URL from Step 2
+4. Click **Deploy** to redeploy with correct API URL
+
+---
+
+## **That's It! 🎉**
+
+Your app is now live! Click the frontend service to see your live URL.
+
+Example URLs:
+- Frontend: `https://farsi-transcriber-prod.railway.app`
+- Backend: `https://farsi-api-prod.railway.app`
+
+---
+
+## **Test Your App**
+
+1. Click your frontend URL
+2. Add a file
+3. Click Transcribe
+4. Wait for transcription
+5. Export results
+
+---
+
+## **Detailed Guide**
+
+For more details, see: `farsi_transcriber_web/RAILWAY_DEPLOYMENT.md`
+
+---
+
+## **Cost**
+
+- **First 3 months:** FREE ($5/month credit)
+- **After that:** ~$2-3/month for personal use
+- Can upgrade to paid tier for more resources
+
+---
+
+## **Common Issues**
+
+**"API connection failed"**
+- Make sure backend URL is correct in frontend variables
+- Redeploy frontend after updating API URL
+
+**"Model not loaded"**
+- Wait 1-2 minutes on first transcription
+- Model downloads on first use
+
+**"Build failed"**
+- Check Railway logs for errors
+- Ensure all files are committed
+
+---
+
+## **Support**
+
+For detailed setup help, see: `farsi_transcriber_web/RAILWAY_DEPLOYMENT.md`
+
+---
+
+**Your Farsi Transcriber is now online!** Share the URL with anyone! 🌐
diff --git a/farsi_transcriber/.gitignore b/farsi_transcriber/.gitignore
new file mode 100644
index 0000000..c051891
--- /dev/null
+++ b/farsi_transcriber/.gitignore
@@ -0,0 +1,52 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Virtual Environment
+venv/
+ENV/
+env/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.DS_Store
+
+# PyTorch/ML Models
+*.pt
+*.pth
+models/downloaded/
+
+# Whisper models cache
+~/.cache/whisper/
+
+# Application outputs
+transcriptions/
+exports/
+*.log
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
diff --git a/farsi_transcriber/README.md b/farsi_transcriber/README.md
new file mode 100644
index 0000000..61e95fe
--- /dev/null
+++ b/farsi_transcriber/README.md
@@ -0,0 +1,257 @@
+# Farsi Transcriber
+
+A professional desktop application for transcribing Farsi audio and video files using OpenAI's Whisper model.
+
+## Features
+
+✨ **Core Features**
+- 🎙️ Transcribe audio files (MP3, WAV, M4A, FLAC, OGG, AAC, WMA)
+- 🎬 Extract audio from video files (MP4, MKV, MOV, WebM, AVI, FLV, WMV)
+- 🇮🇷 High-accuracy Farsi/Persian language transcription
+- ⏱️ Word-level timestamps for precise timing
+- 📤 Export to multiple formats (TXT, SRT, VTT, JSON, TSV)
+- 💻 Clean, intuitive PyQt6-based GUI
+- 🚀 GPU acceleration support (CUDA) with automatic fallback to CPU
+- 🔄 Progress indicators and real-time status updates
+
+## System Requirements
+
+**Minimum:**
+- Python 3.8 or higher
+- 4GB RAM
+- ffmpeg installed
+
+**Recommended:**
+- Python 3.10+
+- 8GB+ RAM
+- NVIDIA GPU with CUDA support (optional but faster)
+- SSD for better performance
+
+## Installation
+
+### Step 1: Install ffmpeg
+
+Choose your operating system:
+
+**Ubuntu/Debian:**
+```bash
+sudo apt update && sudo apt install ffmpeg
+```
+
+**Fedora/CentOS:**
+```bash
+sudo dnf install ffmpeg
+```
+
+**macOS (Homebrew):**
+```bash
+brew install ffmpeg
+```
+
+**Windows (Chocolatey):**
+```bash
+choco install ffmpeg
+```
+
+**Windows (Scoop):**
+```bash
+scoop install ffmpeg
+```
+
+### Step 2: Set up Python environment
+
+```bash
+# Navigate to the repository
+cd whisper/farsi_transcriber
+
+# Create virtual environment
+python3 -m venv venv
+
+# Activate virtual environment
+source venv/bin/activate # On Windows: venv\Scripts\activate
+```
+
+### Step 3: Install dependencies
+
+```bash
+pip install -r requirements.txt
+```
+
+This will install:
+- PyQt6 (GUI framework)
+- openai-whisper (transcription engine)
+- PyTorch (deep learning framework)
+- NumPy, tiktoken, tqdm (supporting libraries)
+
+## Usage
+
+### Running the Application
+
+```bash
+python main.py
+```
+
+### Step-by-Step Guide
+
+1. **Launch the app** - Run `python main.py`
+2. **Select a file** - Click "Select File" button to choose audio/video
+3. **Transcribe** - Click "Transcribe" and wait for completion
+4. **View results** - See transcription with timestamps
+5. **Export** - Click "Export Results" to save in your preferred format
+
+### Supported Export Formats
+
+- **TXT** - Plain text (content only)
+- **SRT** - SubRip subtitle format (with timestamps)
+- **VTT** - WebVTT subtitle format (with timestamps)
+- **JSON** - Structured format with segments and metadata
+- **TSV** - Tab-separated values (spreadsheet compatible)
+
+## Configuration
+
+Edit `config.py` to customize:
+
+```python
+# Model size (tiny, base, small, medium, large)
+DEFAULT_MODEL = "medium"
+
+# Language code
+LANGUAGE_CODE = "fa" # Farsi
+
+# Supported formats
+SUPPORTED_AUDIO_FORMATS = {".mp3", ".wav", ".m4a", ".flac", ".ogg", ...}
+SUPPORTED_VIDEO_FORMATS = {".mp4", ".mkv", ".mov", ".webm", ".avi", ...}
+```
+
+## Model Information
+
+### Available Models
+
+| Model | Size | Speed | Accuracy | VRAM |
+|-------|------|-------|----------|------|
+| tiny | 39M | ~10x | Good | ~1GB |
+| base | 74M | ~7x | Very Good | ~1GB |
+| small | 244M | ~4x | Excellent | ~2GB |
+| medium | 769M | ~2x | Excellent | ~5GB |
+| large | 1550M | 1x | Best | ~10GB |
+
+**Default**: `medium` (recommended for Farsi)
+
+### Performance Notes
+
+- Larger models provide better accuracy but require more VRAM
+- GPU (CUDA) dramatically speeds up transcription (8-10x faster)
+- First run downloads the model (~500MB-3GB depending on model size)
+- Subsequent runs use cached model files
+
+## Project Structure
+
+```
+farsi_transcriber/
+├── ui/ # User interface components
+│ ├── __init__.py
+│ ├── main_window.py # Main application window
+│ └── styles.py # Styling and theming
+├── models/ # Model management
+│ ├── __init__.py
+│ └── whisper_transcriber.py # Whisper wrapper
+├── utils/ # Utility functions
+│ ├── __init__.py
+│ └── export.py # Export functionality
+├── config.py # Configuration settings
+├── main.py # Application entry point
+├── __init__.py # Package init
+├── requirements.txt # Python dependencies
+└── README.md # This file
+```
+
+## Troubleshooting
+
+### Issue: "ffmpeg not found"
+**Solution**: Install ffmpeg using your package manager (see Installation section)
+
+### Issue: "CUDA out of memory"
+**Solution**: Use a smaller model or reduce audio processing in chunks
+
+### Issue: "Model download fails"
+**Solution**: Check internet connection, try again. Models are cached in `~/.cache/whisper/`
+
+### Issue: Slow transcription
+**Solution**: Ensure CUDA is detected (`nvidia-smi`), or upgrade to a smaller/faster model
+
+## Advanced Usage
+
+### Custom Model Selection
+
+Update `config.py`:
+```python
+DEFAULT_MODEL = "large" # For maximum accuracy
+# or
+DEFAULT_MODEL = "tiny" # For fastest processing
+```
+
+### Batch Processing (Future)
+
+Script to process multiple files:
+```python
+from farsi_transcriber.models.whisper_transcriber import FarsiTranscriber
+
+transcriber = FarsiTranscriber(model_name="medium")
+for audio_file in audio_files:
+ result = transcriber.transcribe(audio_file)
+ # Process results
+```
+
+## Performance Tips
+
+1. **Use GPU** - Ensure NVIDIA CUDA is properly installed
+2. **Choose appropriate model** - Balance speed vs accuracy
+3. **Close other applications** - Free up RAM/VRAM
+4. **Use SSD** - Faster model loading and temporary file I/O
+5. **Local processing** - All processing happens locally, no cloud uploads
+
+## Development
+
+### Code Style
+
+```bash
+# Format code
+black farsi_transcriber/
+
+# Check style
+flake8 farsi_transcriber/
+
+# Sort imports
+isort farsi_transcriber/
+```
+
+### Future Features
+
+- [ ] Batch processing
+- [ ] Real-time transcription preview
+- [ ] Speaker diarization
+- [ ] Multi-language support UI
+- [ ] Settings dialog
+- [ ] Keyboard shortcuts
+- [ ] Drag-and-drop support
+- [ ] Recent files history
+
+## License
+
+MIT License - Personal use and modifications allowed
+
+## Acknowledgments
+
+Built with:
+- [OpenAI Whisper](https://github.com/openai/whisper) - Speech recognition
+- [PyQt6](https://www.riverbankcomputing.com/software/pyqt/) - GUI framework
+- [PyTorch](https://pytorch.org/) - Deep learning
+
+## Support
+
+For issues or suggestions:
+1. Check the troubleshooting section
+2. Verify ffmpeg is installed
+3. Ensure Python 3.8+ is used
+4. Check available disk space
+5. Verify CUDA setup (for GPU users)
diff --git a/farsi_transcriber/__init__.py b/farsi_transcriber/__init__.py
new file mode 100644
index 0000000..8e2e5fa
--- /dev/null
+++ b/farsi_transcriber/__init__.py
@@ -0,0 +1,8 @@
+"""
+Farsi Transcriber Application
+
+A desktop application for transcribing Farsi audio and video files using OpenAI's Whisper.
+"""
+
+__version__ = "0.1.0"
+__author__ = "Personal Project"
diff --git a/farsi_transcriber/config.py b/farsi_transcriber/config.py
new file mode 100644
index 0000000..306adc7
--- /dev/null
+++ b/farsi_transcriber/config.py
@@ -0,0 +1,71 @@
+"""
+Configuration settings for Farsi Transcriber application
+
+Manages model selection, device settings, and other configuration options.
+"""
+
+from pathlib import Path
+
+# Application metadata
+APP_NAME = "Farsi Transcriber"
+APP_VERSION = "0.1.0"
+APP_DESCRIPTION = "A desktop application for transcribing Farsi audio and video files"
+
+# Model settings
+DEFAULT_MODEL = "medium" # Options: tiny, base, small, medium, large
+AVAILABLE_MODELS = ["tiny", "base", "small", "medium", "large"]
+MODEL_DESCRIPTIONS = {
+ "tiny": "Smallest model (39M params) - Fastest, ~1GB VRAM required",
+ "base": "Small model (74M params) - Fast, ~1GB VRAM required",
+ "small": "Medium model (244M params) - Balanced, ~2GB VRAM required",
+ "medium": "Large model (769M params) - Good accuracy, ~5GB VRAM required",
+ "large": "Largest model (1550M params) - Best accuracy, ~10GB VRAM required",
+}
+
+# Language settings
+LANGUAGE_CODE = "fa" # Farsi/Persian
+LANGUAGE_NAME = "Farsi"
+
+# Audio/Video settings
+SUPPORTED_AUDIO_FORMATS = {".mp3", ".wav", ".m4a", ".flac", ".ogg", ".aac", ".wma"}
+SUPPORTED_VIDEO_FORMATS = {".mp4", ".mkv", ".mov", ".webm", ".avi", ".flv", ".wmv"}
+
+# UI settings
+WINDOW_WIDTH = 900
+WINDOW_HEIGHT = 700
+WINDOW_MIN_WIDTH = 800
+WINDOW_MIN_HEIGHT = 600
+
+# Output settings
+OUTPUT_DIR = Path.home() / "FarsiTranscriber" / "outputs"
+OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
+
+EXPORT_FORMATS = {
+ "txt": "Plain Text",
+ "srt": "SRT Subtitles",
+ "vtt": "WebVTT Subtitles",
+ "json": "JSON Format",
+ "tsv": "Tab-Separated Values",
+}
+
+# Device settings (auto-detect CUDA if available)
+try:
+ import torch
+
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
+except ImportError:
+ DEVICE = "cpu"
+
+# Logging settings
+LOG_LEVEL = "INFO"
+LOG_FILE = OUTPUT_DIR / "transcriber.log"
+
+
+def get_model_info(model_name: str) -> str:
+ """Get description for a model"""
+ return MODEL_DESCRIPTIONS.get(model_name, "Unknown model")
+
+
+def get_supported_formats() -> set:
+ """Get all supported audio and video formats"""
+ return SUPPORTED_AUDIO_FORMATS | SUPPORTED_VIDEO_FORMATS
diff --git a/farsi_transcriber/main.py b/farsi_transcriber/main.py
new file mode 100644
index 0000000..2c62a36
--- /dev/null
+++ b/farsi_transcriber/main.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+"""
+Farsi Transcriber - Main Entry Point
+
+A PyQt6-based desktop application for transcribing Farsi audio and video files.
+"""
+
+import sys
+from PyQt6.QtWidgets import QApplication
+
+from farsi_transcriber.ui.main_window import MainWindow
+
+
+def main():
+ """Main entry point for the application"""
+ app = QApplication(sys.argv)
+
+ # Create and show main window
+ window = MainWindow()
+ window.show()
+
+ sys.exit(app.exec())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/farsi_transcriber/models/__init__.py b/farsi_transcriber/models/__init__.py
new file mode 100644
index 0000000..fd5a6a4
--- /dev/null
+++ b/farsi_transcriber/models/__init__.py
@@ -0,0 +1 @@
+"""Model management for Farsi Transcriber"""
diff --git a/farsi_transcriber/models/whisper_transcriber.py b/farsi_transcriber/models/whisper_transcriber.py
new file mode 100644
index 0000000..b7c6b37
--- /dev/null
+++ b/farsi_transcriber/models/whisper_transcriber.py
@@ -0,0 +1,225 @@
+"""
+Whisper Transcriber Module
+
+Handles Farsi audio/video transcription using OpenAI's Whisper model.
+"""
+
+import warnings
+from pathlib import Path
+from typing import Dict, List, Optional
+
+import torch
+import whisper
+
+
+class FarsiTranscriber:
+ """
+ Wrapper around Whisper model for Farsi transcription.
+
+ Supports both audio and video files, with word-level timestamp extraction.
+ """
+
+ # Supported audio formats
+ AUDIO_FORMATS = {".mp3", ".wav", ".m4a", ".flac", ".ogg", ".aac", ".wma"}
+
+ # Supported video formats
+ VIDEO_FORMATS = {".mp4", ".mkv", ".mov", ".webm", ".avi", ".flv", ".wmv"}
+
+ # Language code for Farsi/Persian
+ FARSI_LANGUAGE = "fa"
+
+ def __init__(self, model_name: str = "medium", device: Optional[str] = None):
+ """
+ Initialize Farsi Transcriber.
+
+ Args:
+ model_name: Whisper model size ('tiny', 'base', 'small', 'medium', 'large')
+ device: Device to use ('cuda', 'cpu'). Auto-detect if None.
+ """
+ self.model_name = model_name
+
+ # Auto-detect device
+ if device is None:
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
+ else:
+ self.device = device
+
+ print(f"Using device: {self.device}")
+
+ # Load model
+ print(f"Loading Whisper model: {model_name}...")
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ self.model = whisper.load_model(model_name, device=self.device)
+
+ print("Model loaded successfully")
+
+ def transcribe(
+ self,
+ file_path: str,
+ language: str = FARSI_LANGUAGE,
+ verbose: bool = False,
+ ) -> Dict:
+ """
+ Transcribe an audio or video file in Farsi.
+
+ Args:
+ file_path: Path to audio or video file
+ language: Language code (default: 'fa' for Farsi)
+ verbose: Whether to print progress
+
+ Returns:
+ Dictionary with transcription results including word-level segments
+ """
+ file_path = Path(file_path)
+
+ # Validate file exists
+ if not file_path.exists():
+ raise FileNotFoundError(f"File not found: {file_path}")
+
+ # Check format is supported
+ if not self._is_supported_format(file_path):
+ raise ValueError(
+ f"Unsupported format: {file_path.suffix}. "
+ f"Supported: {self.AUDIO_FORMATS | self.VIDEO_FORMATS}"
+ )
+
+ # Perform transcription
+ print(f"Transcribing: {file_path.name}")
+
+ result = self.model.transcribe(
+ str(file_path),
+ language=language,
+ verbose=verbose,
+ )
+
+ # Enhance result with word-level segments
+ enhanced_result = self._enhance_with_word_segments(result)
+
+ return enhanced_result
+
+ def _is_supported_format(self, file_path: Path) -> bool:
+ """Check if file format is supported."""
+ suffix = file_path.suffix.lower()
+ return suffix in (self.AUDIO_FORMATS | self.VIDEO_FORMATS)
+
+ def _enhance_with_word_segments(self, result: Dict) -> Dict:
+ """
+ Enhance transcription result with word-level timing information.
+
+ Args:
+ result: Whisper transcription result
+
+ Returns:
+ Enhanced result with word-level segments
+ """
+ enhanced_segments = []
+
+ for segment in result.get("segments", []):
+ # Extract word-level timing if available
+ word_segments = self._extract_word_segments(segment)
+
+ enhanced_segment = {
+ "id": segment.get("id"),
+ "start": segment.get("start"),
+ "end": segment.get("end"),
+ "text": segment.get("text", ""),
+ "words": word_segments,
+ }
+ enhanced_segments.append(enhanced_segment)
+
+ result["segments"] = enhanced_segments
+ return result
+
+ def _extract_word_segments(self, segment: Dict) -> List[Dict]:
+ """
+ Extract word-level timing from a segment.
+
+ Args:
+ segment: Whisper segment with text
+
+ Returns:
+ List of word dictionaries with timing information
+ """
+ text = segment.get("text", "").strip()
+ if not text:
+ return []
+
+ # For now, return simple word list
+ # Whisper v3 includes word-level details in some configurations
+ start_time = segment.get("start", 0)
+ end_time = segment.get("end", 0)
+ duration = end_time - start_time
+
+ words = text.split()
+ if not words:
+ return []
+
+ # Distribute time evenly across words (simple approach)
+ # More sophisticated timing can be extracted from Whisper's internal data
+ word_duration = duration / len(words) if words else 0
+
+ word_segments = []
+ for i, word in enumerate(words):
+ word_start = start_time + (i * word_duration)
+ word_end = word_start + word_duration
+
+ word_segments.append(
+ {
+ "word": word,
+ "start": word_start,
+ "end": word_end,
+ }
+ )
+
+ return word_segments
+
+ def format_result_for_display(
+ self, result: Dict, include_timestamps: bool = True
+ ) -> str:
+ """
+ Format transcription result for display in UI.
+
+ Args:
+ result: Transcription result
+ include_timestamps: Whether to include timestamps
+
+ Returns:
+ Formatted text string
+ """
+ lines = []
+
+ for segment in result.get("segments", []):
+ text = segment.get("text", "").strip()
+ if not text:
+ continue
+
+ if include_timestamps:
+ start = segment.get("start", 0)
+ end = segment.get("end", 0)
+ timestamp = f"[{self._format_time(start)} - {self._format_time(end)}]"
+ lines.append(f"{timestamp}\n{text}\n")
+ else:
+ lines.append(text)
+
+ return "\n".join(lines)
+
+ @staticmethod
+ def _format_time(seconds: float) -> str:
+ """Format seconds to HH:MM:SS format."""
+ hours = int(seconds // 3600)
+ minutes = int((seconds % 3600) // 60)
+ secs = int(seconds % 60)
+ milliseconds = int((seconds % 1) * 1000)
+
+ return f"{hours:02d}:{minutes:02d}:{secs:02d}.{milliseconds:03d}"
+
+ def get_device_info(self) -> str:
+ """Get information about current device and model."""
+ return (
+ f"Model: {self.model_name} | "
+ f"Device: {self.device.upper()} | "
+ f"VRAM: {torch.cuda.get_device_properties(self.device).total_memory / 1e9:.1f}GB "
+ if self.device == "cuda"
+ else f"Model: {self.model_name} | Device: {self.device.upper()}"
+ )
diff --git a/farsi_transcriber/requirements.txt b/farsi_transcriber/requirements.txt
new file mode 100644
index 0000000..612f9b2
--- /dev/null
+++ b/farsi_transcriber/requirements.txt
@@ -0,0 +1,7 @@
+PyQt6==6.6.1
+PyQt6-Qt6==6.6.1
+PyQt6-sip==13.6.0
+torch>=1.10.1
+numpy
+openai-whisper
+tqdm
diff --git a/farsi_transcriber/ui/__init__.py b/farsi_transcriber/ui/__init__.py
new file mode 100644
index 0000000..435adac
--- /dev/null
+++ b/farsi_transcriber/ui/__init__.py
@@ -0,0 +1 @@
+"""UI components for Farsi Transcriber"""
diff --git a/farsi_transcriber/ui/main_window.py b/farsi_transcriber/ui/main_window.py
new file mode 100644
index 0000000..3fbd371
--- /dev/null
+++ b/farsi_transcriber/ui/main_window.py
@@ -0,0 +1,285 @@
+"""
+Main application window for Farsi Transcriber
+
+Provides PyQt6-based GUI for selecting files and transcribing Farsi audio/video.
+"""
+
+import os
+from pathlib import Path
+
+from PyQt6.QtCore import Qt, QThread, pyqtSignal
+from PyQt6.QtWidgets import (
+ QMainWindow,
+ QWidget,
+ QVBoxLayout,
+ QHBoxLayout,
+ QPushButton,
+ QLabel,
+ QTextEdit,
+ QProgressBar,
+ QFileDialog,
+ QMessageBox,
+)
+from PyQt6.QtGui import QFont
+
+from farsi_transcriber.models.whisper_transcriber import FarsiTranscriber
+from farsi_transcriber.utils.export import TranscriptionExporter
+from farsi_transcriber.ui.styles import get_stylesheet, get_color
+
+
+class TranscriptionWorker(QThread):
+ """Worker thread for transcription to prevent UI freezing"""
+
+ # Signals
+ progress_update = pyqtSignal(str) # Status messages
+ transcription_complete = pyqtSignal(dict) # Results with timestamps
+ error_occurred = pyqtSignal(str) # Error messages
+
+ def __init__(self, file_path: str, model_name: str = "medium"):
+ super().__init__()
+ self.file_path = file_path
+ self.model_name = model_name
+ self.transcriber = None
+
+ def run(self):
+ """Run transcription in background thread"""
+ try:
+ # Initialize Whisper transcriber
+ self.progress_update.emit("Loading Whisper model...")
+ self.transcriber = FarsiTranscriber(model_name=self.model_name)
+
+ # Perform transcription
+ self.progress_update.emit(f"Transcribing: {Path(self.file_path).name}")
+ result = self.transcriber.transcribe(self.file_path)
+
+ # Format result for display with timestamps
+ display_text = self.transcriber.format_result_for_display(result)
+
+ # Add full text for export
+ result["full_text"] = result.get("text", "")
+
+ self.progress_update.emit("Transcription complete!")
+ self.transcription_complete.emit(
+ {
+ "text": display_text,
+ "segments": result.get("segments", []),
+ "full_text": result.get("text", ""),
+ }
+ )
+
+ except Exception as e:
+ self.error_occurred.emit(f"Error: {str(e)}")
+
+
+class MainWindow(QMainWindow):
+ """Main application window for Farsi Transcriber"""
+
+ # Supported audio and video formats
+ SUPPORTED_FORMATS = (
+ "Audio Files (*.mp3 *.wav *.m4a *.flac *.ogg *.aac *.wma);;",
+ "Video Files (*.mp4 *.mkv *.mov *.webm *.avi *.flv *.wmv);;",
+ "All Files (*.*)",
+ )
+
+ def __init__(self):
+ super().__init__()
+ self.selected_file = None
+ self.transcription_worker = None
+ self.last_result = None
+ # Apply stylesheet
+ self.setStyleSheet(get_stylesheet())
+ self.init_ui()
+
+ def init_ui(self):
+ """Initialize the user interface"""
+ self.setWindowTitle("Farsi Transcriber")
+ self.setGeometry(100, 100, 900, 700)
+
+ # Create central widget and main layout
+ central_widget = QWidget()
+ self.setCentralWidget(central_widget)
+ main_layout = QVBoxLayout(central_widget)
+ main_layout.setSpacing(10)
+ main_layout.setContentsMargins(20, 20, 20, 20)
+
+ # Title
+ title_label = QLabel("Farsi Audio/Video Transcriber")
+ title_font = QFont()
+ title_font.setPointSize(16)
+ title_font.setBold(True)
+ title_label.setFont(title_font)
+ main_layout.addWidget(title_label)
+
+ # File selection section
+ file_section_layout = QHBoxLayout()
+ self.file_label = QLabel("No file selected")
+ self.file_label.setStyleSheet("color: gray;")
+ file_section_layout.addWidget(self.file_label, 1)
+
+ self.select_button = QPushButton("Select File")
+ self.select_button.clicked.connect(self.on_select_file)
+ file_section_layout.addWidget(self.select_button)
+
+ main_layout.addLayout(file_section_layout)
+
+ # Transcribe button
+ self.transcribe_button = QPushButton("Transcribe")
+ self.transcribe_button.clicked.connect(self.on_transcribe)
+ self.transcribe_button.setEnabled(False)
+ main_layout.addWidget(self.transcribe_button)
+
+ # Progress bar
+ self.progress_bar = QProgressBar()
+ self.progress_bar.setRange(0, 0) # Indeterminate progress
+ self.progress_bar.setVisible(False)
+ main_layout.addWidget(self.progress_bar)
+
+ # Status label
+ self.status_label = QLabel("Ready")
+ self.status_label.setStyleSheet("color: #666; font-style: italic;")
+ main_layout.addWidget(self.status_label)
+
+ # Results text area
+ results_title = QLabel("Transcription Results:")
+ results_title_font = QFont()
+ results_title_font.setBold(True)
+ results_title.setFont(results_title_font)
+ main_layout.addWidget(results_title)
+
+ self.results_text = QTextEdit()
+ self.results_text.setReadOnly(True)
+ self.results_text.setPlaceholderText(
+ "Transcription results will appear here..."
+ )
+ # Set monospace font for results
+ mono_font = QFont("Courier New", 10)
+ self.results_text.setFont(mono_font)
+ main_layout.addWidget(self.results_text)
+
+ # Buttons layout (Export, Clear)
+ buttons_layout = QHBoxLayout()
+ buttons_layout.addStretch()
+
+ self.export_button = QPushButton("Export Results")
+ self.export_button.clicked.connect(self.on_export)
+ self.export_button.setEnabled(False)
+ buttons_layout.addWidget(self.export_button)
+
+ self.clear_button = QPushButton("Clear")
+ self.clear_button.clicked.connect(self.on_clear)
+ buttons_layout.addWidget(self.clear_button)
+
+ main_layout.addLayout(buttons_layout)
+
+ def on_select_file(self):
+ """Handle file selection dialog"""
+ file_path, _ = QFileDialog.getOpenFileName(
+ self, "Select Audio or Video File", "", "".join(self.SUPPORTED_FORMATS)
+ )
+
+ if file_path:
+ self.selected_file = file_path
+ file_name = Path(file_path).name
+ self.file_label.setText(f"Selected: {file_name}")
+ self.file_label.setStyleSheet("color: #333;")
+ self.transcribe_button.setEnabled(True)
+ self.export_button.setEnabled(False)
+ self.results_text.clear()
+ self.status_label.setText("File selected. Click 'Transcribe' to start.")
+
+ def on_transcribe(self):
+ """Handle transcription button click"""
+ if not self.selected_file:
+ QMessageBox.warning(self, "Error", "Please select a file first.")
+ return
+
+ # Disable buttons during transcription
+ self.transcribe_button.setEnabled(False)
+ self.select_button.setEnabled(False)
+ self.export_button.setEnabled(False)
+
+ # Show progress
+ self.progress_bar.setVisible(True)
+ self.status_label.setText("Transcribing...")
+
+ # Create and start worker thread
+ self.transcription_worker = TranscriptionWorker(self.selected_file)
+ self.transcription_worker.progress_update.connect(self.on_progress_update)
+ self.transcription_worker.transcription_complete.connect(
+ self.on_transcription_complete
+ )
+ self.transcription_worker.error_occurred.connect(self.on_error)
+ self.transcription_worker.start()
+
+ def on_progress_update(self, message: str):
+ """Handle progress updates from worker thread"""
+ self.status_label.setText(message)
+
+ def on_transcription_complete(self, result: dict):
+ """Handle completed transcription"""
+ self.progress_bar.setVisible(False)
+ self.transcribe_button.setEnabled(True)
+ self.select_button.setEnabled(True)
+ self.export_button.setEnabled(True)
+ self.status_label.setText("Transcription complete!")
+
+ # Display results with timestamps
+ self.results_text.setText(result.get("text", "No transcription available"))
+
+ # Store result for export
+ self.last_result = result
+
+ def on_error(self, error_message: str):
+ """Handle errors from worker thread"""
+ self.progress_bar.setVisible(False)
+ self.transcribe_button.setEnabled(True)
+ self.select_button.setEnabled(True)
+ self.status_label.setText("Error occurred. Check message below.")
+ QMessageBox.critical(self, "Transcription Error", error_message)
+
+ def on_export(self):
+ """Handle export button click"""
+ if not self.last_result:
+ QMessageBox.warning(self, "Warning", "No transcription to export.")
+ return
+
+ file_path, file_filter = QFileDialog.getSaveFileName(
+ self,
+ "Export Transcription",
+ "",
+ "Text Files (*.txt);;SRT Subtitles (*.srt);;WebVTT Subtitles (*.vtt);;JSON (*.json);;TSV (*.tsv)",
+ )
+
+ if file_path:
+ try:
+ file_path = Path(file_path)
+
+ # Determine format from file extension
+ suffix = file_path.suffix.lower().lstrip(".")
+ if not suffix:
+ # Default to txt if no extension
+ suffix = "txt"
+ file_path = file_path.with_suffix(".txt")
+
+ # Export using the appropriate format
+ TranscriptionExporter.export(self.last_result, file_path, suffix)
+
+ QMessageBox.information(
+ self,
+ "Success",
+ f"Transcription exported successfully to:\n{file_path.name}",
+ )
+ except Exception as e:
+ QMessageBox.critical(
+ self, "Export Error", f"Failed to export: {str(e)}"
+ )
+
+ def on_clear(self):
+ """Clear all results and reset UI"""
+ self.selected_file = None
+ self.file_label.setText("No file selected")
+ self.file_label.setStyleSheet("color: gray;")
+ self.results_text.clear()
+ self.status_label.setText("Ready")
+ self.transcribe_button.setEnabled(False)
+ self.export_button.setEnabled(False)
diff --git a/farsi_transcriber/ui/styles.py b/farsi_transcriber/ui/styles.py
new file mode 100644
index 0000000..e60979a
--- /dev/null
+++ b/farsi_transcriber/ui/styles.py
@@ -0,0 +1,107 @@
+"""
+Application styling and theming
+
+Provides stylesheet and styling utilities for the Farsi Transcriber app.
+"""
+
+# Modern, professional dark-themed stylesheet
+MAIN_STYLESHEET = """
+QMainWindow {
+ background-color: #f5f5f5;
+}
+
+QLabel {
+ color: #333333;
+}
+
+QLineEdit, QTextEdit {
+ background-color: #ffffff;
+ color: #333333;
+ border: 1px solid #d0d0d0;
+ border-radius: 4px;
+ padding: 5px;
+ font-size: 11pt;
+}
+
+QLineEdit:focus, QTextEdit:focus {
+ border: 2px solid #4CAF50;
+ background-color: #fafafa;
+}
+
+QPushButton {
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px 16px;
+ font-weight: bold;
+ font-size: 11pt;
+ min-height: 32px;
+}
+
+QPushButton:hover {
+ background-color: #45a049;
+}
+
+QPushButton:pressed {
+ background-color: #3d8b40;
+}
+
+QPushButton:disabled {
+ background-color: #cccccc;
+ color: #999999;
+}
+
+QProgressBar {
+ border: 1px solid #d0d0d0;
+ border-radius: 4px;
+ text-align: center;
+ background-color: #ffffff;
+ height: 20px;
+}
+
+QProgressBar::chunk {
+ background-color: #4CAF50;
+ border-radius: 3px;
+}
+
+QMessageBox QLabel {
+ color: #333333;
+}
+
+QMessageBox QPushButton {
+ min-width: 60px;
+}
+"""
+
+# Color palette
+COLORS = {
+ "primary": "#4CAF50",
+ "primary_hover": "#45a049",
+ "primary_active": "#3d8b40",
+ "background": "#f5f5f5",
+ "text": "#333333",
+ "text_secondary": "#666666",
+ "border": "#d0d0d0",
+ "success": "#4CAF50",
+ "error": "#f44336",
+ "warning": "#ff9800",
+ "info": "#2196F3",
+}
+
+# Font settings
+FONTS = {
+ "default_size": 11,
+ "title_size": 16,
+ "mono_family": "Courier New",
+}
+
+
+def get_stylesheet() -> str:
+ """Get the main stylesheet for the application"""
+ return MAIN_STYLESHEET
+
+
+def get_color(color_name: str) -> str:
+ """Get a color from the palette"""
+ return COLORS.get(color_name, "#000000")
diff --git a/farsi_transcriber/utils/__init__.py b/farsi_transcriber/utils/__init__.py
new file mode 100644
index 0000000..9c3f775
--- /dev/null
+++ b/farsi_transcriber/utils/__init__.py
@@ -0,0 +1 @@
+"""Utility functions for Farsi Transcriber"""
diff --git a/farsi_transcriber/utils/export.py b/farsi_transcriber/utils/export.py
new file mode 100644
index 0000000..ab3a3c8
--- /dev/null
+++ b/farsi_transcriber/utils/export.py
@@ -0,0 +1,164 @@
+"""
+Export utilities for transcription results
+
+Supports multiple export formats: TXT, SRT, JSON, TSV, VTT
+"""
+
+import json
+from datetime import timedelta
+from pathlib import Path
+from typing import Dict, List
+
+
+class TranscriptionExporter:
+ """Export transcription results in various formats"""
+
+ @staticmethod
+ def export_txt(result: Dict, file_path: Path) -> None:
+ """
+ Export transcription as plain text file.
+
+ Args:
+ result: Transcription result dictionary
+ file_path: Output file path
+ """
+ text = result.get("full_text", "") or result.get("text", "")
+
+ with open(file_path, "w", encoding="utf-8") as f:
+ f.write(text)
+
+ @staticmethod
+ def export_srt(result: Dict, file_path: Path) -> None:
+ """
+ Export transcription as SRT subtitle file.
+
+ Args:
+ result: Transcription result dictionary
+ file_path: Output file path
+ """
+ segments = result.get("segments", [])
+
+ with open(file_path, "w", encoding="utf-8") as f:
+ for i, segment in enumerate(segments, 1):
+ start = TranscriptionExporter._format_srt_time(segment.get("start", 0))
+ end = TranscriptionExporter._format_srt_time(segment.get("end", 0))
+ text = segment.get("text", "").strip()
+
+ if text:
+ f.write(f"{i}\n")
+ f.write(f"{start} --> {end}\n")
+ f.write(f"{text}\n\n")
+
+ @staticmethod
+ def export_vtt(result: Dict, file_path: Path) -> None:
+ """
+ Export transcription as WebVTT subtitle file.
+
+ Args:
+ result: Transcription result dictionary
+ file_path: Output file path
+ """
+ segments = result.get("segments", [])
+
+ with open(file_path, "w", encoding="utf-8") as f:
+ f.write("WEBVTT\n\n")
+
+ for segment in segments:
+ start = TranscriptionExporter._format_vtt_time(segment.get("start", 0))
+ end = TranscriptionExporter._format_vtt_time(segment.get("end", 0))
+ text = segment.get("text", "").strip()
+
+ if text:
+ f.write(f"{start} --> {end}\n")
+ f.write(f"{text}\n\n")
+
+ @staticmethod
+ def export_json(result: Dict, file_path: Path) -> None:
+ """
+ Export transcription as JSON file.
+
+ Args:
+ result: Transcription result dictionary
+ file_path: Output file path
+ """
+ with open(file_path, "w", encoding="utf-8") as f:
+ json.dump(result, f, ensure_ascii=False, indent=2)
+
+ @staticmethod
+ def export_tsv(result: Dict, file_path: Path) -> None:
+ """
+ Export transcription as TSV (tab-separated values) file.
+
+ Args:
+ result: Transcription result dictionary
+ file_path: Output file path
+ """
+ segments = result.get("segments", [])
+
+ with open(file_path, "w", encoding="utf-8") as f:
+ # Write header
+ f.write("Index\tStart\tEnd\tDuration\tText\n")
+
+ for i, segment in enumerate(segments, 1):
+ start = segment.get("start", 0)
+ end = segment.get("end", 0)
+ duration = end - start
+ text = segment.get("text", "").strip()
+
+ if text:
+ f.write(
+ f"{i}\t{start:.2f}\t{end:.2f}\t{duration:.2f}\t{text}\n"
+ )
+
+ @staticmethod
+ def export(
+ result: Dict, file_path: Path, format_type: str = "txt"
+ ) -> None:
+ """
+ Export transcription in specified format.
+
+ Args:
+ result: Transcription result dictionary
+ file_path: Output file path
+ format_type: Export format ('txt', 'srt', 'vtt', 'json', 'tsv')
+
+ Raises:
+ ValueError: If format is not supported
+ """
+ format_type = format_type.lower()
+
+ exporters = {
+ "txt": TranscriptionExporter.export_txt,
+ "srt": TranscriptionExporter.export_srt,
+ "vtt": TranscriptionExporter.export_vtt,
+ "json": TranscriptionExporter.export_json,
+ "tsv": TranscriptionExporter.export_tsv,
+ }
+
+ if format_type not in exporters:
+ raise ValueError(
+ f"Unsupported format: {format_type}. "
+ f"Supported formats: {list(exporters.keys())}"
+ )
+
+ exporters[format_type](result, file_path)
+
+ @staticmethod
+ def _format_srt_time(seconds: float) -> str:
+ """Format time for SRT format (HH:MM:SS,mmm)"""
+ td = timedelta(seconds=seconds)
+ hours, remainder = divmod(int(td.total_seconds()), 3600)
+ minutes, secs = divmod(remainder, 60)
+ milliseconds = int((seconds % 1) * 1000)
+
+ return f"{hours:02d}:{minutes:02d}:{secs:02d},{milliseconds:03d}"
+
+ @staticmethod
+ def _format_vtt_time(seconds: float) -> str:
+ """Format time for VTT format (HH:MM:SS.mmm)"""
+ td = timedelta(seconds=seconds)
+ hours, remainder = divmod(int(td.total_seconds()), 3600)
+ minutes, secs = divmod(remainder, 60)
+ milliseconds = int((seconds % 1) * 1000)
+
+ return f"{hours:02d}:{minutes:02d}:{secs:02d}.{milliseconds:03d}"
diff --git a/farsi_transcriber_web/.env.example b/farsi_transcriber_web/.env.example
new file mode 100644
index 0000000..366a8d3
--- /dev/null
+++ b/farsi_transcriber_web/.env.example
@@ -0,0 +1,13 @@
+# Frontend environment variables
+# Copy this to .env.local and update with your values
+
+# API URL for the backend
+# Local development: http://localhost:5000
+# Railway production: https://your-backend-app.railway.app
+VITE_API_URL=http://localhost:5000
+
+# Application name
+VITE_APP_NAME=Farsi Transcriber
+
+# Max file size (in MB)
+VITE_MAX_FILE_SIZE=500
diff --git a/farsi_transcriber_web/.env.production b/farsi_transcriber_web/.env.production
new file mode 100644
index 0000000..3cac56f
--- /dev/null
+++ b/farsi_transcriber_web/.env.production
@@ -0,0 +1,7 @@
+# Production environment variables for Railway deployment
+# Set VITE_API_URL in Railway environment variables instead of committing here
+
+# Default fallback - will be overridden by Railway env var
+VITE_API_URL=https://your-backend-url.railway.app
+VITE_APP_NAME=Farsi Transcriber
+VITE_MAX_FILE_SIZE=500
diff --git a/farsi_transcriber_web/.gitignore b/farsi_transcriber_web/.gitignore
new file mode 100644
index 0000000..e6946e8
--- /dev/null
+++ b/farsi_transcriber_web/.gitignore
@@ -0,0 +1,37 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Environment variables
+.env
+.env.local
+.env.*.local
+
+# Editor directories and files
+.vscode
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Build output
+*.tgz
+*.tsbuildinfo
+vite.config.js
+vite.config.d.ts
diff --git a/farsi_transcriber_web/RAILWAY_DEPLOYMENT.md b/farsi_transcriber_web/RAILWAY_DEPLOYMENT.md
new file mode 100644
index 0000000..60ceec1
--- /dev/null
+++ b/farsi_transcriber_web/RAILWAY_DEPLOYMENT.md
@@ -0,0 +1,308 @@
+# Railway Deployment Guide
+
+Complete step-by-step guide to deploy your Farsi Transcriber web app to Railway.
+
+## Prerequisites
+
+1. **GitHub Account** - To connect your repository
+2. **Railway Account** - Free signup at https://railway.app
+3. **Git** - Already have this since you're using it
+
+## Step 1: Prepare Your Code for Deployment
+
+### 1.1 Make sure all files are committed
+
+```bash
+cd /home/user/whisper
+git status
+git add .
+git commit -m "Ready for Railway deployment"
+```
+
+### 1.2 Push to GitHub
+
+If you haven't already, push your fork to GitHub:
+
+```bash
+git push origin claude/review-repo-011CV3PVcA7ZSCTW2YquuMB8
+```
+
+---
+
+## Step 2: Create Railway Account
+
+1. Go to **https://railway.app**
+2. Click **"Login with GitHub"**
+3. Authorize Railway to access your GitHub account
+4. You'll get **$5 monthly credit** for free ✅
+
+---
+
+## Step 3: Create Backend Service (Flask API)
+
+### 3.1 Create a new project
+
+1. In Railway dashboard, click **"Create New Project"**
+2. Select **"GitHub Repo"**
+3. Select your **whisper** repository
+4. Railway will auto-detect it as Python
+5. Configure:
+ - **Root Directory:** `farsi_transcriber_web/backend`
+ - **Start Command:** `gunicorn --workers 2 --bind 0.0.0.0:$PORT app:app`
+
+### 3.2 Set environment variables
+
+In the Railway dashboard for your backend:
+
+1. Go to **Variables**
+2. Add:
+ ```
+ FLASK_ENV=production
+ FLASK_DEBUG=False
+ PYTHONUNBUFFERED=1
+ PORT=5000
+ ```
+
+### 3.3 Deploy
+
+1. Click **Deploy**
+2. Wait for deployment (takes 2-3 minutes)
+3. Your backend URL will appear (e.g., `https://farsi-api-prod.railway.app`)
+4. **Copy this URL** - you'll need it for the frontend
+
+---
+
+## Step 4: Create Frontend Service (React)
+
+### 4.1 Create another service in same project
+
+1. Go to your Railway project
+2. Click **"Create New Service"**
+3. Select **"GitHub Repo"**
+4. Select your **whisper** repository
+5. Configure:
+ - **Root Directory:** `farsi_transcriber_web`
+ - **Build Command:** `npm install && npm run build`
+ - **Start Command:** `npm run preview`
+
+### 4.2 Set environment variables
+
+In the Railway dashboard for your frontend:
+
+1. Go to **Variables**
+2. Add:
+ ```
+ VITE_API_URL=https://your-backend-url-here.railway.app
+ VITE_APP_NAME=Farsi Transcriber
+ ```
+ (Replace with your actual backend URL from Step 3.3)
+
+### 4.3 Deploy
+
+1. Click **Deploy**
+2. Wait for deployment (2-5 minutes depending on npm install)
+3. Your frontend URL will appear (e.g., `https://farsi-web-prod.railway.app`)
+
+---
+
+## Step 5: Configure Services to Communicate
+
+### 5.1 Link backend to frontend
+
+1. In Railway dashboard, select frontend service
+2. Go to **Variables**
+3. Update `VITE_API_URL` with your backend service domain
+4. Deploy again
+
+### 5.2 Test the connection
+
+1. Open your frontend URL
+2. Try to add a file and transcribe
+3. Check browser console for any errors
+4. If errors, check Railway logs (click service → Logs)
+
+---
+
+## Step 6: Monitor Your Deployment
+
+### 6.1 View logs
+
+In Railway dashboard:
+- Click your service
+- Go to **Logs** tab
+- See real-time logs as users interact with your app
+
+### 6.2 Check health
+
+```bash
+# Check if backend is running
+curl https://your-backend-url.railway.app/health
+
+# Should return:
+# {"status": "healthy", "model_loaded": true, "environment": "production"}
+```
+
+### 6.3 Monitor usage
+
+- Railway dashboard shows RAM, CPU, bandwidth usage
+- Your $5 credit should last 1-3 months for personal use
+
+---
+
+## Step 7: Custom Domain (Optional)
+
+If you want a custom domain like `farsi.yourdomain.com`:
+
+1. Buy a domain on GoDaddy, Namecheap, etc.
+2. In Railway dashboard → Your app → Settings → Domains
+3. Add custom domain
+4. Update DNS records at your domain provider
+5. Railway will handle SSL certificate automatically
+
+---
+
+## Troubleshooting
+
+### Issue: Backend showing error "Model not loaded"
+
+**Solution:** First transcription loads the 769MB model (takes 1-2 min). Wait and try again.
+
+### Issue: Frontend can't reach backend
+
+**Solution:**
+1. Check backend URL is correct in frontend variables
+2. Backend must be running (check Railway logs)
+3. CORS should be enabled (already configured)
+
+### Issue: Build fails
+
+**Solution:**
+1. Check Railway build logs for errors
+2. Ensure `package.json` has all required dependencies
+3. Run locally first: `npm install && npm run build`
+
+### Issue: App runs slow
+
+**Solution:**
+1. You're on free tier with limited resources
+2. Upgrade to paid tier ($5/month) for better performance
+3. Or wait for model to cache (subsequent transcriptions are fast)
+
+### Issue: Out of memory
+
+**Solution:**
+1. Free tier has limited RAM
+2. Close unused tabs/apps
+3. Use smaller Whisper model (edit backend to use 'small' instead of 'medium')
+
+---
+
+## Next Steps: Custom Domain Setup
+
+Once stable, add your custom domain:
+
+1. Purchase domain
+2. Railway → Settings → Domains → Add Domain
+3. Update DNS CNAME records
+4. Railway auto-generates SSL certificate
+
+---
+
+## Cost Breakdown
+
+### Free Tier ($5/month credit)
+
+- ✅ 500 build minutes/month
+- ✅ 100 GB bandwidth/month
+- ✅ 6,000 compute unit hours
+- ✅ More than enough for personal use
+
+### Your app will cost:
+
+- **Backend (Flask):** ~$1-2/month
+- **Frontend (React):** ~$0.50/month
+- **Total:** ~$2/month (with free credit covering 2-3 months)
+
+---
+
+## Useful Commands
+
+### Check if Railway CLI is installed
+
+```bash
+railway --version
+```
+
+### Install Railway CLI
+
+```bash
+npm i -g @railway/cli
+```
+
+### Deploy from command line
+
+```bash
+railway up
+```
+
+### View logs
+
+```bash
+railway logs
+```
+
+---
+
+## What Happens Now
+
+1. ✅ Your app is live on Railway
+2. ✅ Free $5 monthly credit
+3. ✅ Auto-scaling (if you get traffic)
+4. ✅ 24/7 uptime
+5. ✅ Automatic SSL/HTTPS
+6. ✅ No infrastructure to manage
+
+---
+
+## Monitor Your App
+
+Visit your Railway dashboard regularly to:
+- Check resource usage
+- View logs
+- Update environment variables
+- Scale services if needed
+- Monitor costs
+
+---
+
+## After Deployment
+
+Your app is now online! Share the URL with friends:
+
+```
+https://your-app-name.railway.app
+```
+
+---
+
+## Further Reading
+
+- [Railway Documentation](https://docs.railway.app)
+- [Railway GitHub Integration](https://docs.railway.app/guides/github)
+- [Railway Environment Variables](https://docs.railway.app/develop/variables)
+- [Whisper API Docs](https://github.com/openai/whisper)
+
+---
+
+## Support
+
+If you have issues:
+
+1. Check Railway logs (click service → Logs)
+2. Check browser console (F12 → Console tab)
+3. Visit Railway docs: https://docs.railway.app
+4. Check Flask logs for backend errors
+
+---
+
+**Congratulations! Your Farsi Transcriber is now live!** 🎉
diff --git a/farsi_transcriber_web/README.md b/farsi_transcriber_web/README.md
new file mode 100644
index 0000000..1737d96
--- /dev/null
+++ b/farsi_transcriber_web/README.md
@@ -0,0 +1,384 @@
+# Farsi Transcriber - Web Application
+
+A professional web-based application for transcribing Farsi audio and video files using OpenAI's Whisper model.
+
+## Features
+
+✨ **Core Features**
+- 🎙️ Transcribe audio files (MP3, WAV, M4A, FLAC, OGG, AAC, WMA)
+- 🎬 Extract audio from video files (MP4, MKV, MOV, WebM, AVI, FLV, WMV)
+- 🇮🇷 High-accuracy Farsi/Persian language transcription
+- ⏱️ Word-level timestamps for precise timing
+- 📤 Export to multiple formats (TXT, SRT, VTT, JSON)
+- 💻 Clean, intuitive React-based UI with Figma design
+- 🎨 Dark/Light theme toggle
+- 🔍 Search and text highlighting in transcriptions
+- 📋 File queue management
+- 💾 Copy individual transcription segments
+- 🚀 GPU acceleration support (CUDA)
+- 🎯 Resizable window for flexible workspace
+
+## Tech Stack
+
+**Frontend:**
+- React 18+ with TypeScript
+- Vite (fast build tool)
+- Tailwind CSS v4.0
+- Lucide React (icons)
+- re-resizable (window resizing)
+- Sonner (toast notifications)
+
+**Backend:**
+- Flask (Python web framework)
+- OpenAI Whisper (speech recognition)
+- PyTorch (deep learning)
+- Flask-CORS (cross-origin requests)
+
+## System Requirements
+
+**Frontend:**
+- Node.js 16+
+- npm/yarn/pnpm
+
+**Backend:**
+- Python 3.8+
+- 4GB RAM minimum
+- 8GB+ recommended
+- ffmpeg installed
+- Optional: NVIDIA GPU with CUDA support
+
+## Installation
+
+### Step 1: Install ffmpeg
+
+Choose your operating system:
+
+**Ubuntu/Debian:**
+```bash
+sudo apt update && sudo apt install ffmpeg
+```
+
+**macOS (Homebrew):**
+```bash
+brew install ffmpeg
+```
+
+**Windows (Chocolatey):**
+```bash
+choco install ffmpeg
+```
+
+### Step 2: Backend Setup
+
+```bash
+# Navigate to backend directory
+cd backend
+
+# Create virtual environment
+python3 -m venv venv
+source venv/bin/activate # On Windows: venv\Scripts\activate
+
+# Install dependencies
+pip install -r requirements.txt
+```
+
+### Step 3: Frontend Setup
+
+```bash
+# Navigate to root directory
+cd ..
+
+# Install Node dependencies
+npm install
+
+# Or use yarn/pnpm
+yarn install
+# or
+pnpm install
+```
+
+## Running the Application
+
+### Step 1: Start Backend API
+
+```bash
+cd backend
+source venv/bin/activate # Activate virtual environment
+python app.py
+```
+
+The API will be available at `http://localhost:5000`
+
+### Step 2: Start Frontend Dev Server
+
+In a new terminal:
+
+```bash
+npm run dev
+```
+
+The application will be available at `http://localhost:3000`
+
+## Building for Production
+
+### Frontend Build
+
+```bash
+npm run build
+```
+
+This creates optimized production build in `dist/` directory.
+
+### Backend Deployment
+
+For production, use a production WSGI server:
+
+```bash
+# Install Gunicorn
+pip install gunicorn
+
+# Run with Gunicorn
+gunicorn -w 4 -b 0.0.0.0:5000 app:app
+```
+
+## API Endpoints
+
+### `/health` (GET)
+Health check endpoint
+
+**Response:**
+```json
+{
+ "status": "healthy",
+ "model_loaded": true,
+ "device": "cuda|cpu"
+}
+```
+
+### `/transcribe` (POST)
+Transcribe audio/video file
+
+**Request:**
+- `file`: Audio/video file (multipart/form-data)
+- `language`: Language code (optional, default: "fa" for Farsi)
+
+**Response:**
+```json
+{
+ "status": "success",
+ "filename": "audio.mp3",
+ "language": "fa",
+ "text": "Full transcription text...",
+ "segments": [
+ {
+ "start": "00:00:00.000",
+ "end": "00:00:05.500",
+ "text": "سلام دنیا"
+ }
+ ]
+}
+```
+
+### `/models` (GET)
+Get available Whisper models
+
+**Response:**
+```json
+{
+ "available_models": ["tiny", "base", "small", "medium", "large"],
+ "current_model": "medium",
+ "description": "..."
+}
+```
+
+### `/export` (POST)
+Export transcription
+
+**Request:**
+```json
+{
+ "transcription": "Full text...",
+ "segments": [...],
+ "format": "txt|srt|vtt|json"
+}
+```
+
+**Response:**
+```json
+{
+ "status": "success",
+ "format": "srt",
+ "content": "...",
+ "mime_type": "text/plain"
+}
+```
+
+## Usage Guide
+
+### 1. Add Files to Queue
+- Click "Add Files" button in the left sidebar
+- Select audio or video files
+- Multiple files can be added to the queue
+
+### 2. Transcribe
+- Select a file from the queue
+- Click "Transcribe" button
+- Watch the progress indicator
+- Results appear with timestamps
+
+### 3. Search & Copy
+- Use the search bar to find specific text
+- Matching text is highlighted
+- Click copy icon to copy individual segments
+
+### 4. Export Results
+- Select export format (TXT, SRT, VTT, JSON)
+- Click "Export" button
+- File is downloaded or ready to save
+
+### 5. Theme Toggle
+- Click sun/moon icon in header
+- Switch between light and dark themes
+
+## Project Structure
+
+```
+farsi_transcriber_web/
+├── src/
+│ ├── App.tsx # Main application component
+│ ├── main.tsx # React entry point
+│ ├── index.css # Global styles
+│ └── components/
+│ ├── Button.tsx
+│ ├── Progress.tsx
+│ ├── Input.tsx
+│ └── Select.tsx
+├── backend/
+│ ├── app.py # Flask API server
+│ ├── requirements.txt # Python dependencies
+│ └── .gitignore
+├── public/
+├── package.json
+├── vite.config.ts
+├── tsconfig.json
+├── tailwind.config.js
+├── postcss.config.js
+└── README.md
+```
+
+## Configuration
+
+### Environment Variables
+
+Create a `.env.local` file in the root directory:
+
+```
+VITE_API_URL=http://localhost:5000
+VITE_MAX_FILE_SIZE=500MB
+```
+
+### Backend Configuration
+
+Edit `backend/app.py` to customize:
+
+```python
+# Change model size
+model = whisper.load_model('large') # tiny, base, small, medium, large
+
+# Change upload folder
+UPLOAD_FOLDER = '/custom/path'
+
+# Change max file size
+MAX_FILE_SIZE = 1024 * 1024 * 1024 # 1GB
+```
+
+## Troubleshooting
+
+### Issue: "API connection failed"
+**Solution**: Ensure backend is running on `http://localhost:5000`
+
+### Issue: "Whisper model not found"
+**Solution**: First run downloads the model (~3GB). Ensure internet connection and disk space.
+
+### Issue: "CUDA out of memory"
+**Solution**: Use smaller model or reduce batch size in `backend/app.py`
+
+### Issue: "ffmpeg not found"
+**Solution**: Install ffmpeg using your package manager (see Installation section)
+
+### Issue: Port 3000 or 5000 already in use
+**Solution**: Change ports in `vite.config.ts` and `backend/app.py`
+
+## Performance Tips
+
+1. **Use GPU** - Ensure NVIDIA CUDA is properly installed
+2. **Choose appropriate model** - Balance speed vs accuracy
+3. **Close other applications** - Free up RAM/VRAM
+4. **Use SSD** - Faster model loading and file I/O
+5. **Batch Processing** - Process multiple files sequentially
+
+## Future Enhancements
+
+- [ ] Drag-and-drop file upload
+- [ ] Audio playback synchronized with transcription
+- [ ] Edit segments inline
+- [ ] Keyboard shortcuts
+- [ ] Save/load sessions
+- [ ] Speaker diarization
+- [ ] Confidence scores
+- [ ] Custom vocabulary support
+
+## Development
+
+### Code Style
+
+```bash
+# Format code (if ESLint configured)
+npm run lint
+
+# Build for development
+npm run dev
+
+# Build for production
+npm run build
+```
+
+### Adding Components
+
+New components go in `src/components/` and should:
+- Use TypeScript
+- Include prop interfaces
+- Export as default
+- Include JSDoc comments
+
+## Common Issues & Solutions
+
+| Issue | Solution |
+|-------|----------|
+| Models slow to load | GPU required for fast transcription |
+| File not supported | Check file extension is in supported list |
+| Transcription has errors | Try larger model (medium/large) |
+| Application crashes | Check browser console and Flask logs |
+| Export not working | Ensure segments data is complete |
+
+## License
+
+MIT License - Personal use and modifications allowed
+
+## Credits
+
+Built with:
+- [OpenAI Whisper](https://github.com/openai/whisper) - Speech recognition
+- [React](https://react.dev/) - UI framework
+- [Vite](https://vitejs.dev/) - Build tool
+- [Tailwind CSS](https://tailwindcss.com/) - Styling
+- [Flask](https://flask.palletsprojects.com/) - Backend framework
+
+## Support
+
+For issues:
+1. Check the troubleshooting section
+2. Verify ffmpeg is installed
+3. Check Flask backend logs
+4. Review browser console for errors
+5. Ensure Python 3.8+ and Node.js 16+ are installed
diff --git a/farsi_transcriber_web/backend/.env.example b/farsi_transcriber_web/backend/.env.example
new file mode 100644
index 0000000..faeb07f
--- /dev/null
+++ b/farsi_transcriber_web/backend/.env.example
@@ -0,0 +1,21 @@
+# Backend environment variables
+# Copy this to .env and update with your values
+
+# Flask environment
+FLASK_ENV=production
+FLASK_DEBUG=False
+
+# Server port (Railway sets PORT automatically)
+PORT=5000
+
+# CORS settings
+FLASK_CORS_ORIGINS=*
+
+# Whisper model settings
+WHISPER_MODEL=medium
+
+# File upload settings
+MAX_FILE_SIZE=500000000
+
+# Python path
+PYTHONUNBUFFERED=1
diff --git a/farsi_transcriber_web/backend/.gitignore b/farsi_transcriber_web/backend/.gitignore
new file mode 100644
index 0000000..801d628
--- /dev/null
+++ b/farsi_transcriber_web/backend/.gitignore
@@ -0,0 +1,42 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Virtual Environment
+venv/
+ENV/
+env/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# ML Models
+*.pt
+*.pth
+~/.cache/whisper/
+
+# Uploads
+/uploads
+/tmp
diff --git a/farsi_transcriber_web/backend/Procfile b/farsi_transcriber_web/backend/Procfile
new file mode 100644
index 0000000..e7e7bf8
--- /dev/null
+++ b/farsi_transcriber_web/backend/Procfile
@@ -0,0 +1 @@
+web: gunicorn --workers 4 --worker-class sync --bind 0.0.0.0:$PORT app:app
diff --git a/farsi_transcriber_web/backend/app.py b/farsi_transcriber_web/backend/app.py
new file mode 100644
index 0000000..3388dbf
--- /dev/null
+++ b/farsi_transcriber_web/backend/app.py
@@ -0,0 +1,228 @@
+"""
+Farsi Transcriber Backend API
+
+Flask API for handling audio/video file transcription using Whisper model.
+Configured for Railway deployment with lazy model loading.
+"""
+
+import os
+import sys
+import tempfile
+from pathlib import Path
+from werkzeug.utils import secure_filename
+from flask import Flask, request, jsonify
+from flask_cors import CORS
+
+# Prevent model download during build
+os.environ['WHISPER_CACHE'] = os.path.expanduser('~/.cache/whisper')
+
+# Add parent directory to path for imports
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+app = Flask(__name__)
+CORS(app, resources={r"/api/*": {"origins": "*"}})
+
+# Configuration
+UPLOAD_FOLDER = tempfile.gettempdir()
+ALLOWED_EXTENSIONS = {'mp3', 'wav', 'm4a', 'flac', 'ogg', 'aac', 'wma', 'mp4', 'mkv', 'mov', 'webm', 'avi', 'flv', 'wmv'}
+MAX_FILE_SIZE = 500 * 1024 * 1024 # 500MB
+
+# Production settings
+app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
+app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE
+app.config['ENV'] = os.getenv('FLASK_ENV', 'production')
+
+# Load Whisper model (lazy load - only on first transcription request)
+model = None
+
+def load_model():
+ """Lazy load Whisper model on first use (not during build)"""
+ global model
+ if model is None:
+ try:
+ print("⏳ Loading Whisper model for first time...")
+ print(" This may take 1-2 minutes on first run...")
+ # Import here to avoid loading during build
+ import whisper
+ model = whisper.load_model('medium')
+ print("✓ Whisper model loaded successfully")
+ except Exception as e:
+ print(f"✗ Error loading Whisper model: {e}")
+ model = None
+ return model
+
+
+def allowed_file(filename):
+ """Check if file has allowed extension"""
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
+
+
+@app.route('/', methods=['GET'])
+def index():
+ """Root endpoint"""
+ return jsonify({
+ 'message': 'Farsi Transcriber API',
+ 'version': '1.0.0',
+ 'status': 'running'
+ })
+
+
+@app.route('/health', methods=['GET'])
+def health():
+ """Health check endpoint - fast response without loading model"""
+ return jsonify({
+ 'status': 'healthy',
+ 'model_loaded': model is not None,
+ 'environment': app.config['ENV']
+ })
+
+
+@app.route('/transcribe', methods=['POST'])
+def transcribe():
+ """
+ Transcribe audio/video file
+
+ Request:
+ - file: Audio/video file
+ - language: Language code (default: 'fa' for Farsi)
+
+ Response:
+ - transcription results with segments and timestamps
+ """
+ try:
+ # Load model if not already loaded
+ whisper_model = load_model()
+ if not whisper_model:
+ return jsonify({'error': 'Failed to load Whisper model'}), 500
+
+ # Check if file is in request
+ if 'file' not in request.files:
+ return jsonify({'error': 'No file provided'}), 400
+
+ file = request.files['file']
+
+ if file.filename == '':
+ return jsonify({'error': 'No file selected'}), 400
+
+ if not allowed_file(file.filename):
+ return jsonify({'error': 'File type not allowed'}), 400
+
+ # Save file
+ filename = secure_filename(file.filename)
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
+ file.save(filepath)
+
+ # Get language code from request (default: Farsi)
+ language = request.form.get('language', 'fa')
+
+ # Transcribe
+ result = whisper_model.transcribe(filepath, language=language, verbose=False)
+
+ # Format response
+ segments = []
+ for segment in result.get('segments', []):
+ segments.append({
+ 'start': f"{int(segment['start'] // 3600):02d}:{int((segment['start'] % 3600) // 60):02d}:{int(segment['start'] % 60):02d}.{int((segment['start'] % 1) * 1000):03d}",
+ 'end': f"{int(segment['end'] // 3600):02d}:{int((segment['end'] % 3600) // 60):02d}:{int(segment['end'] % 60):02d}.{int((segment['end'] % 1) * 1000):03d}",
+ 'text': segment['text'].strip(),
+ })
+
+ # Clean up uploaded file
+ try:
+ os.remove(filepath)
+ except:
+ pass
+
+ return jsonify({
+ 'status': 'success',
+ 'filename': filename,
+ 'language': result.get('language', 'unknown'),
+ 'text': result.get('text', ''),
+ 'segments': segments
+ })
+
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+
+@app.route('/models', methods=['GET'])
+def get_models():
+ """Get available Whisper models"""
+ return jsonify({
+ 'available_models': ['tiny', 'base', 'small', 'medium', 'large'],
+ 'current_model': 'medium',
+ 'description': 'List of available Whisper models. Larger models are more accurate but slower.'
+ })
+
+
+@app.route('/export', methods=['POST'])
+def export():
+ """
+ Export transcription in specified format
+
+ Request:
+ - transcription: Full transcription text
+ - segments: Array of segments with timestamps
+ - format: Export format (txt, srt, vtt, json)
+
+ Response:
+ - Exported file content
+ """
+ try:
+ data = request.json
+ transcription = data.get('transcription', '')
+ segments = data.get('segments', [])
+ format_type = data.get('format', 'txt').lower()
+
+ if format_type == 'txt':
+ content = transcription
+ mime_type = 'text/plain'
+ elif format_type == 'srt':
+ content = _format_srt(segments)
+ mime_type = 'text/plain'
+ elif format_type == 'vtt':
+ content = _format_vtt(segments)
+ mime_type = 'text/plain'
+ elif format_type == 'json':
+ import json
+ content = json.dumps({'text': transcription, 'segments': segments}, ensure_ascii=False, indent=2)
+ mime_type = 'application/json'
+ else:
+ return jsonify({'error': 'Unsupported format'}), 400
+
+ return jsonify({
+ 'status': 'success',
+ 'format': format_type,
+ 'content': content,
+ 'mime_type': mime_type
+ })
+
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+
+def _format_srt(segments):
+ """Format transcription as SRT subtitle format"""
+ lines = []
+ for i, segment in enumerate(segments, 1):
+ lines.append(str(i))
+ lines.append(f"{segment['start']} --> {segment['end']}")
+ lines.append(segment['text'])
+ lines.append('')
+ return '\n'.join(lines)
+
+
+def _format_vtt(segments):
+ """Format transcription as WebVTT subtitle format"""
+ lines = ['WEBVTT', '']
+ for segment in segments:
+ lines.append(f"{segment['start']} --> {segment['end']}")
+ lines.append(segment['text'])
+ lines.append('')
+ return '\n'.join(lines)
+
+
+if __name__ == '__main__':
+ port = int(os.getenv('PORT', 5000))
+ debug = os.getenv('FLASK_ENV', 'production') == 'development'
+ app.run(debug=debug, host='0.0.0.0', port=port, threaded=True)
diff --git a/farsi_transcriber_web/backend/nixpacks.toml b/farsi_transcriber_web/backend/nixpacks.toml
new file mode 100644
index 0000000..7a1f464
--- /dev/null
+++ b/farsi_transcriber_web/backend/nixpacks.toml
@@ -0,0 +1,11 @@
+# Backend Nixpacks configuration
+# Ensures ffmpeg is available for Whisper audio processing
+
+[phases.setup]
+nixPkgs = ["ffmpeg"]
+
+[phases.install]
+cmds = ["pip install -r requirements.txt"]
+
+[start]
+cmd = "gunicorn --workers 2 --worker-class sync --timeout 120 --bind 0.0.0.0:$PORT app:app"
diff --git a/farsi_transcriber_web/backend/requirements.txt b/farsi_transcriber_web/backend/requirements.txt
new file mode 100644
index 0000000..341c19d
--- /dev/null
+++ b/farsi_transcriber_web/backend/requirements.txt
@@ -0,0 +1,10 @@
+Flask==2.3.3
+Flask-CORS==4.0.0
+python-dotenv==1.0.0
+openai-whisper>=20230314
+torch>=1.10.1
+numpy>=1.21.0
+python-multipart==0.0.6
+gunicorn==21.2.0
+
+
diff --git a/farsi_transcriber_web/index.html b/farsi_transcriber_web/index.html
new file mode 100644
index 0000000..8cf5754
--- /dev/null
+++ b/farsi_transcriber_web/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Farsi Audio/Video Transcriber
+
+
+
+
+
+
diff --git a/farsi_transcriber_web/nixpacks.toml b/farsi_transcriber_web/nixpacks.toml
new file mode 100644
index 0000000..18bdf0c
--- /dev/null
+++ b/farsi_transcriber_web/nixpacks.toml
@@ -0,0 +1,11 @@
+# Frontend Nixpacks configuration
+# Node.js React app with Vite
+
+[phases.install]
+cmds = ["npm install"]
+
+[phases.build]
+cmds = ["npm run build"]
+
+[start]
+cmd = "npm run preview"
diff --git a/farsi_transcriber_web/package-lock.json b/farsi_transcriber_web/package-lock.json
new file mode 100644
index 0000000..1b646d7
--- /dev/null
+++ b/farsi_transcriber_web/package-lock.json
@@ -0,0 +1,2456 @@
+{
+ "name": "farsi-transcriber-web",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "farsi-transcriber-web",
+ "version": "0.1.0",
+ "dependencies": {
+ "@tailwindcss/postcss": "^4.1.17",
+ "lucide-react": "^0.263.1",
+ "re-resizable": "^6.9.9",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sonner": "^1.2.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.8.0",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "@vitejs/plugin-react": "^4.2.0",
+ "autoprefixer": "^10.4.16",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^4.0.0",
+ "terser": "^5.44.1",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz",
+ "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz",
+ "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz",
+ "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz",
+ "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz",
+ "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz",
+ "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz",
+ "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz",
+ "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz",
+ "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz",
+ "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz",
+ "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz",
+ "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz",
+ "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz",
+ "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz",
+ "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz",
+ "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz",
+ "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz",
+ "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz",
+ "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz",
+ "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz",
+ "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz",
+ "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
+ "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "enhanced-resolve": "^5.18.3",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.30.2",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.17"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
+ "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.17",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.17",
+ "@tailwindcss/oxide-darwin-x64": "4.1.17",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.17",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.17",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.17",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
+ "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
+ "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
+ "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
+ "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
+ "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
+ "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
+ "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
+ "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
+ "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
+ "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.6.0",
+ "@emnapi/runtime": "^1.6.0",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.0.7",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
+ "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
+ "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz",
+ "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.1.17",
+ "@tailwindcss/oxide": "4.1.17",
+ "postcss": "^8.4.41",
+ "tailwindcss": "4.1.17"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.25",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
+ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.26",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
+ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.22",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
+ "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.27.0",
+ "caniuse-lite": "^1.0.30001754",
+ "fraction.js": "^5.3.4",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.28",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz",
+ "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
+ "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.8.25",
+ "caniuse-lite": "^1.0.30001754",
+ "electron-to-chromium": "^1.5.249",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.1.4"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001754",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
+ "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.1.tgz",
+ "integrity": "sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.254",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz",
+ "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
+ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.30.2",
+ "lightningcss-darwin-arm64": "1.30.2",
+ "lightningcss-darwin-x64": "1.30.2",
+ "lightningcss-freebsd-x64": "1.30.2",
+ "lightningcss-linux-arm-gnueabihf": "1.30.2",
+ "lightningcss-linux-arm64-gnu": "1.30.2",
+ "lightningcss-linux-arm64-musl": "1.30.2",
+ "lightningcss-linux-x64-gnu": "1.30.2",
+ "lightningcss-linux-x64-musl": "1.30.2",
+ "lightningcss-win32-arm64-msvc": "1.30.2",
+ "lightningcss-win32-x64-msvc": "1.30.2"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
+ "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
+ "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
+ "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
+ "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
+ "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
+ "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
+ "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
+ "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
+ "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
+ "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
+ "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.263.1",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.263.1.tgz",
+ "integrity": "sha512-keqxAx97PlaEN89PXZ6ki1N8nRjGWtDa4021GFYLNj0RgruM5odbpl8GHTExj0hhPq3sF6Up0gnxt6TSHu+ovw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/re-resizable": {
+ "version": "6.11.2",
+ "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz",
+ "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.53.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
+ "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.2",
+ "@rollup/rollup-android-arm64": "4.53.2",
+ "@rollup/rollup-darwin-arm64": "4.53.2",
+ "@rollup/rollup-darwin-x64": "4.53.2",
+ "@rollup/rollup-freebsd-arm64": "4.53.2",
+ "@rollup/rollup-freebsd-x64": "4.53.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.2",
+ "@rollup/rollup-linux-arm64-musl": "4.53.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.2",
+ "@rollup/rollup-linux-x64-gnu": "4.53.2",
+ "@rollup/rollup-linux-x64-musl": "4.53.2",
+ "@rollup/rollup-openharmony-arm64": "4.53.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.2",
+ "@rollup/rollup-win32-x64-gnu": "4.53.2",
+ "@rollup/rollup-win32-x64-msvc": "4.53.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/sonner": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
+ "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
+ "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.44.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
+ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/farsi_transcriber_web/package.json b/farsi_transcriber_web/package.json
new file mode 100644
index 0000000..321c588
--- /dev/null
+++ b/farsi_transcriber_web/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "farsi-transcriber-web",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tailwindcss/postcss": "^4.1.17",
+ "lucide-react": "^0.263.1",
+ "re-resizable": "^6.9.9",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sonner": "^1.2.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.8.0",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "@vitejs/plugin-react": "^4.2.0",
+ "autoprefixer": "^10.4.16",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^4.0.0",
+ "terser": "^5.44.1",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.0"
+ }
+}
diff --git a/farsi_transcriber_web/postcss.config.js b/farsi_transcriber_web/postcss.config.js
new file mode 100644
index 0000000..1c87846
--- /dev/null
+++ b/farsi_transcriber_web/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ autoprefixer: {},
+ },
+}
diff --git a/farsi_transcriber_web/railway.toml b/farsi_transcriber_web/railway.toml
new file mode 100644
index 0000000..0df5716
--- /dev/null
+++ b/farsi_transcriber_web/railway.toml
@@ -0,0 +1,13 @@
+# Railway configuration file
+# https://docs.railway.app/reference/nixpacks
+
+[build]
+builder = "nixpacks"
+
+[[services]]
+name = "backend"
+startCommand = "cd backend && gunicorn --workers 2 --worker-class sync --timeout 120 --bind 0.0.0.0:5000 app:app"
+
+[[services]]
+name = "frontend"
+startCommand = "npm run build && npm run preview"
diff --git a/farsi_transcriber_web/src/App.tsx b/farsi_transcriber_web/src/App.tsx
new file mode 100644
index 0000000..ab59ff2
--- /dev/null
+++ b/farsi_transcriber_web/src/App.tsx
@@ -0,0 +1,537 @@
+import { useState, useRef } from 'react';
+import {
+ FileAudio,
+ Upload,
+ Moon,
+ Sun,
+ Search,
+ Copy,
+ X,
+ CheckCircle2,
+ Clock,
+ Loader2,
+ Download
+} from 'lucide-react';
+import { Resizable } from 're-resizable';
+import { Toaster, toast } from 'sonner';
+import Button from './components/Button';
+import Progress from './components/Progress';
+import Input from './components/Input';
+import Select from './components/Select';
+
+interface FileItem {
+ id: string;
+ name: string;
+ status: 'pending' | 'processing' | 'completed' | 'error';
+ progress?: number;
+ transcription?: TranscriptionSegment[];
+ file?: File;
+ fullText?: string;
+}
+
+interface TranscriptionSegment {
+ start: string;
+ end: string;
+ text: string;
+}
+
+// Get API URL from environment variable
+const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';
+
+export default function App() {
+ const [fileQueue, setFileQueue] = useState([]);
+ const [selectedFileId, setSelectedFileId] = useState(null);
+ const [isDark, setIsDark] = useState(false);
+ const [windowSize, setWindowSize] = useState({ width: 1100, height: 700 });
+ const [searchQuery, setSearchQuery] = useState('');
+ const [exportFormat, setExportFormat] = useState('txt');
+ const fileInputRef = useRef(null);
+
+ // Theme colors
+ const theme = {
+ bg: isDark ? '#1a1a1a' : '#f5f5f5',
+ cardBg: isDark ? '#2d2d2d' : '#ffffff',
+ inputBg: isDark ? '#3a3a3a' : '#f9f9f9',
+ border: isDark ? '#4a4a4a' : '#d0d0d0',
+ text: isDark ? '#e0e0e0' : '#333333',
+ textSecondary: isDark ? '#a0a0a0' : '#666666',
+ progressBg: isDark ? '#404040' : '#e0e0e0',
+ sidebarBg: isDark ? '#252525' : '#fafafa',
+ hoverBg: isDark ? '#3a3a3a' : '#f0f0f0',
+ selectedBg: isDark ? '#4a4a4a' : '#e8f5e9',
+ };
+
+ const handleAddFiles = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleFileInputChange = (e: React.ChangeEvent) => {
+ const files = e.currentTarget.files;
+ if (!files) return;
+
+ const newFiles: FileItem[] = [];
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+ const newFileItem: FileItem = {
+ id: `${Date.now()}-${i}`,
+ name: file.name,
+ status: 'pending',
+ file: file,
+ };
+ newFiles.push(newFileItem);
+ }
+
+ setFileQueue([...fileQueue, ...newFiles]);
+ if (!selectedFileId && newFiles.length > 0) {
+ setSelectedFileId(newFiles[0].id);
+ }
+ toast.success(`${newFiles.length} file(s) added to queue`);
+
+ // Reset input
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ const handleRemoveFile = (id: string) => {
+ setFileQueue(fileQueue.filter(f => f.id !== id));
+ if (selectedFileId === id) {
+ setSelectedFileId(fileQueue[0]?.id || null);
+ }
+ toast.info('File removed from queue');
+ };
+
+ const handleTranscribe = async () => {
+ if (!selectedFileId) return;
+
+ const fileIndex = fileQueue.findIndex(f => f.id === selectedFileId);
+ if (fileIndex === -1 || !fileQueue[fileIndex].file) return;
+
+ // Update status to processing
+ const updatedQueue = [...fileQueue];
+ updatedQueue[fileIndex].status = 'processing';
+ updatedQueue[fileIndex].progress = 0;
+ setFileQueue(updatedQueue);
+
+ try {
+ const file = fileQueue[fileIndex].file!;
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('language', 'fa'); // Farsi by default
+
+ // Show loading toast
+ const loadingToastId = toast.loading('Loading Whisper model (first time only)...');
+
+ const response = await fetch(`${API_URL}/transcribe`, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ throw new Error(`API error: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ // Dismiss loading toast
+ toast.dismiss(loadingToastId);
+
+ if (result.status === 'success') {
+ const updated = [...fileQueue];
+ updated[fileIndex].status = 'completed';
+ updated[fileIndex].progress = 100;
+ updated[fileIndex].transcription = result.segments;
+ updated[fileIndex].fullText = result.text;
+ setFileQueue(updated);
+ toast.success('Transcription completed!');
+ } else {
+ throw new Error(result.error || 'Unknown error');
+ }
+ } catch (error) {
+ const updated = [...fileQueue];
+ updated[fileIndex].status = 'error';
+ setFileQueue(updated);
+ const errorMsg = error instanceof Error ? error.message : 'Failed to transcribe file';
+ toast.error(errorMsg);
+ }
+ };
+
+ const handleCopySegment = (text: string) => {
+ navigator.clipboard.writeText(text);
+ toast.success('Copied to clipboard');
+ };
+
+ const handleExport = async () => {
+ const selectedFile = fileQueue.find(f => f.id === selectedFileId);
+ if (!selectedFile?.transcription || !selectedFile.fullText) {
+ toast.error('No transcription to export');
+ return;
+ }
+
+ try {
+ const toastId = toast.loading('Preparing export...');
+
+ const response = await fetch(`${API_URL}/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ transcription: selectedFile.fullText,
+ segments: selectedFile.transcription,
+ format: exportFormat,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Export failed: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+ toast.dismiss(toastId);
+
+ if (result.status === 'success') {
+ // Create a blob and download
+ const blob = new Blob([result.content], { type: result.mime_type });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${selectedFile.name.split('.')[0]}.${exportFormat === 'json' ? 'json' : exportFormat}`;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ toast.success(`Exported as ${exportFormat.toUpperCase()}`);
+ } else {
+ throw new Error(result.error || 'Export failed');
+ }
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : 'Failed to export';
+ toast.error(errorMsg);
+ }
+ };
+
+ const handleClearAll = () => {
+ setFileQueue([]);
+ setSelectedFileId(null);
+ setSearchQuery('');
+ toast.info('All files cleared');
+ };
+
+ const selectedFile = fileQueue.find(f => f.id === selectedFileId);
+ const currentTranscription = selectedFile?.transcription || [];
+
+ // Filter transcription based on search
+ const filteredTranscription = searchQuery
+ ? currentTranscription.filter(seg =>
+ seg.text.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ : currentTranscription;
+
+ // Function to highlight search text
+ const highlightText = (text: string, query: string) => {
+ if (!query) return text;
+
+ const parts = text.split(new RegExp(`(${query})`, 'gi'));
+ return parts.map((part) =>
+ part.toLowerCase() === query.toLowerCase()
+ ? `${part}`
+ : part
+ ).join('');
+ };
+
+ const getStatusIcon = (status: FileItem['status']) => {
+ switch (status) {
+ case 'completed':
+ return ;
+ case 'processing':
+ return ;
+ case 'error':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+ {/* Hidden file input */}
+
+
+
{
+ setWindowSize({
+ width: windowSize.width + d.width,
+ height: windowSize.height + d.height,
+ });
+ }}
+ minWidth={900}
+ minHeight={600}
+ className="rounded-lg shadow-2xl overflow-hidden"
+ style={{
+ backgroundColor: theme.cardBg,
+ border: `2px solid ${theme.border}`,
+ }}
+ handleStyles={{
+ right: { cursor: 'ew-resize' },
+ bottom: { cursor: 'ns-resize' },
+ bottomRight: { cursor: 'nwse-resize' },
+ }}
+ >
+
+ {/* Left Sidebar - File Queue */}
+
+
+
+ File Queue
+
+
+
+
+
+ {fileQueue.length === 0 ? (
+
+ No files in queue
+
+ ) : (
+ fileQueue.map((file) => (
+
setSelectedFileId(file.id)}
+ >
+
+
+ {getStatusIcon(file.status)}
+
+ {file.name}
+
+
+
+
+ {file.status === 'processing' && (
+
+
+
+ {file.progress}%
+
+
+ )}
+
+ ))
+ )}
+
+
+
+ {/* Main Content Area */}
+
+ {/* Header */}
+
+
+
+ Farsi Audio/Video Transcriber
+
+
+ {windowSize.width}×{windowSize.height}
+
+
+
+
+
+
+ {/* File Info & Actions */}
+
+
+
+
+
+
+ {selectedFile ? selectedFile.name : 'No file selected'}
+
+ {selectedFile?.status === 'processing' && (
+
+ Processing... {selectedFile.progress}%
+
+ )}
+ {selectedFile?.status === 'completed' && (
+
Completed
+ )}
+
+
+
+
+
+
+ {/* Search & Export Controls */}
+ {selectedFile?.transcription && (
+
+
+
+ setSearchQuery(e.target.value)}
+ style={{
+ backgroundColor: theme.inputBg,
+ borderColor: theme.border,
+ color: theme.text,
+ paddingLeft: '2.25rem',
+ }}
+ />
+
+
+
+
+ )}
+
+ {/* Transcription Results */}
+
+
+
+ {searchQuery && (
+
+ {filteredTranscription.length} results found
+
+ )}
+
+
+
+ {currentTranscription.length === 0 ? (
+
+ Transcription results will appear here...
+
+ ) : (
+
+ {filteredTranscription.map((segment, index) => (
+
+
+
+ [{segment.start} - {segment.end}]
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ {/* Bottom Actions */}
+
+
+ {selectedFile?.status === 'completed' && `${currentTranscription.length} segments`}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/farsi_transcriber_web/src/components/Button.tsx b/farsi_transcriber_web/src/components/Button.tsx
new file mode 100644
index 0000000..f1abd72
--- /dev/null
+++ b/farsi_transcriber_web/src/components/Button.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: 'default' | 'outline';
+ size?: 'sm' | 'md' | 'lg';
+ children: React.ReactNode;
+}
+
+const Button = React.forwardRef(
+ ({ variant = 'default', size = 'md', className, ...props }, ref) => {
+ const baseStyles = 'font-medium rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center justify-center';
+
+ const variantStyles = {
+ default: 'bg-green-500 hover:bg-green-600 text-white',
+ outline: 'border border-gray-300 hover:bg-gray-100 text-gray-900',
+ };
+
+ const sizeStyles = {
+ sm: 'px-3 py-1.5 text-sm',
+ md: 'px-4 py-2 text-base',
+ lg: 'px-6 py-3 text-lg',
+ };
+
+ return (
+
+ );
+ }
+);
+
+Button.displayName = 'Button';
+
+export default Button;
diff --git a/farsi_transcriber_web/src/components/Input.tsx b/farsi_transcriber_web/src/components/Input.tsx
new file mode 100644
index 0000000..1be8b0c
--- /dev/null
+++ b/farsi_transcriber_web/src/components/Input.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+interface InputProps extends React.InputHTMLAttributes {
+ label?: string;
+}
+
+const Input = React.forwardRef(
+ ({ label, className, ...props }, ref) => {
+ return (
+
+ {label && }
+
+
+ );
+ }
+);
+
+Input.displayName = 'Input';
+
+export default Input;
diff --git a/farsi_transcriber_web/src/components/Progress.tsx b/farsi_transcriber_web/src/components/Progress.tsx
new file mode 100644
index 0000000..6ef5995
--- /dev/null
+++ b/farsi_transcriber_web/src/components/Progress.tsx
@@ -0,0 +1,15 @@
+interface ProgressProps {
+ value: number;
+ className?: string;
+}
+
+export default function Progress({ value, className }: ProgressProps) {
+ return (
+
+ );
+}
diff --git a/farsi_transcriber_web/src/components/Select.tsx b/farsi_transcriber_web/src/components/Select.tsx
new file mode 100644
index 0000000..ddd28bc
--- /dev/null
+++ b/farsi_transcriber_web/src/components/Select.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+interface SelectProps extends React.SelectHTMLAttributes {
+ label?: string;
+ children: React.ReactNode;
+}
+
+const Select = React.forwardRef(
+ ({ label, className, children, ...props }, ref) => {
+ return (
+
+ {label && }
+
+
+ );
+ }
+);
+
+Select.displayName = 'Select';
+
+export default Select;
diff --git a/farsi_transcriber_web/src/components/__init__.ts b/farsi_transcriber_web/src/components/__init__.ts
new file mode 100644
index 0000000..122315e
--- /dev/null
+++ b/farsi_transcriber_web/src/components/__init__.ts
@@ -0,0 +1,4 @@
+export { default as Button } from './Button';
+export { default as Progress } from './Progress';
+export { default as Input } from './Input';
+export { default as Select } from './Select';
diff --git a/farsi_transcriber_web/src/index.css b/farsi_transcriber_web/src/index.css
new file mode 100644
index 0000000..ab98b8b
--- /dev/null
+++ b/farsi_transcriber_web/src/index.css
@@ -0,0 +1,46 @@
+@import "tailwindcss";
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html, body, #root {
+ width: 100%;
+ height: 100%;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+mark {
+ display: inline-block;
+}
+
+/* RTL Support */
+[dir="rtl"] {
+ text-align: right;
+ direction: rtl;
+}
+
+/* Scrollbar styling */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #4CAF50;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #45a049;
+}
diff --git a/farsi_transcriber_web/src/main.tsx b/farsi_transcriber_web/src/main.tsx
new file mode 100644
index 0000000..3d7150d
--- /dev/null
+++ b/farsi_transcriber_web/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.tsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/farsi_transcriber_web/tailwind.config.js b/farsi_transcriber_web/tailwind.config.js
new file mode 100644
index 0000000..e5589b9
--- /dev/null
+++ b/farsi_transcriber_web/tailwind.config.js
@@ -0,0 +1,17 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: '#4CAF50',
+ 'primary-hover': '#45a049',
+ 'primary-active': '#3d8b40',
+ }
+ },
+ },
+ plugins: [],
+}
diff --git a/farsi_transcriber_web/tsconfig.json b/farsi_transcriber_web/tsconfig.json
new file mode 100644
index 0000000..3fb09ff
--- /dev/null
+++ b/farsi_transcriber_web/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/farsi_transcriber_web/tsconfig.node.json b/farsi_transcriber_web/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/farsi_transcriber_web/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/farsi_transcriber_web/vite.config.ts b/farsi_transcriber_web/vite.config.ts
new file mode 100644
index 0000000..63f6b22
--- /dev/null
+++ b/farsi_transcriber_web/vite.config.ts
@@ -0,0 +1,38 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:5000',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, '')
+ }
+ }
+ },
+ build: {
+ outDir: 'dist',
+ sourcemap: false,
+ minify: 'terser',
+ terserOptions: {
+ compress: {
+ drop_console: true,
+ },
+ },
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ vendor: ['react', 'react-dom'],
+ },
+ },
+ },
+ },
+ preview: {
+ port: parseInt(process.env.PORT || '3000'),
+ host: '0.0.0.0',
+ }
+})
diff --git a/whisper/transcribe.py b/whisper/transcribe.py
index 0a4cc36..8eb1711 100644
--- a/whisper/transcribe.py
+++ b/whisper/transcribe.py
@@ -281,7 +281,7 @@ def transcribe(
time_offset = float(seek * HOP_LENGTH / SAMPLE_RATE)
window_end_time = float((seek + N_FRAMES) * HOP_LENGTH / SAMPLE_RATE)
segment_size = min(N_FRAMES, content_frames - seek, seek_clip_end - seek)
- mel_segment = mel[:, seek : seek + segment_size]
+ mel_segment = mel[:, seek:seek + segment_size]
segment_duration = segment_size * HOP_LENGTH / SAMPLE_RATE
mel_segment = pad_or_trim(mel_segment, N_FRAMES).to(model.device).to(dtype)
@@ -444,7 +444,7 @@ def transcribe(
continue
if is_segment_anomaly(segment):
next_segment = next_words_segment(
- current_segments[si + 1 :]
+ current_segments[si + 1:]
)
if next_segment is not None:
hal_next_start = next_segment["words"][0]["start"]
@@ -508,7 +508,7 @@ def transcribe(
pbar.update(min(content_frames, seek) - previous_seek)
return dict(
- text=tokenizer.decode(all_tokens[len(initial_prompt_tokens) :]),
+ text=tokenizer.decode(all_tokens[len(initial_prompt_tokens):]),
segments=all_segments,
language=language,
)
diff --git a/whisper/utils.py b/whisper/utils.py
index 13792f7..6ca958e 100644
--- a/whisper/utils.py
+++ b/whisper/utils.py
@@ -153,7 +153,7 @@ class SubtitlesWriter(ResultWriter):
if max_words_per_line > len(segment["words"]) - chunk_index:
words_count = remaining_words
for i, original_timing in enumerate(
- segment["words"][chunk_index : chunk_index + words_count]
+ segment["words"][chunk_index:chunk_index + words_count]
):
timing = original_timing.copy()
long_pause = (